Source file unity-csharp/systemic-design/ChemistryRules.cs from the GDnD code examples. Download raw file

// GDnD wiki example (1 of 3: ChemistryRules.cs, ChemistryBody.cs, ElementSource.cs)
// Demonstrates: a small "chemistry engine" in the style described for The Legend of Zelda:
//   Breath of the Wild (Dohta, GDC 2017). Elements (fire, water, ice, electricity, wind) carry
//   state changes; materials (anything with a ChemistryBody: crates, grass, enemies, the player)
//   have tags that decide how they react. Three rules, applied the same way to everything:
//     1. Elements can change the state of materials (fire sets a Flammable crate burning).
//     2. Elements can change each other (water puts out fire, fire melts ice into water).
//     3. Materials cannot change each other directly - only through an element they give off.
//   Physics stays separate: Rigidbody2D moves things; the chemistry code only changes state and
//   hands over to physics in one place (wind pushes Light bodies, see ChemistryBody).
// This file holds the data types and the whole rule table, so every object obeys the same rules.
// Related pages: [[breath-of-the-wild]], [[emergent-design]], [[game-atoms]],
//   [[source-botw-breaking-conventions-gdc-2017]], [[unity-collider2d-and-triggers]]
// Unity 6 (6000.x), 2D physics. No input code, so it works with either input backend.
 
using System;
 
namespace GDnD.Systemic.Chemistry
{
    public enum Element { None, Fire, Water, Ice, Electricity, Wind }
 
    [Flags]
    public enum MaterialTags
    {
        None = 0,
        Flammable = 1 << 0,   // wood, grass, cloth: catches fire
        Conductive = 1 << 1,  // metal: carries electricity even when dry
        Freezable = 1 << 2,   // can be frozen solid by ice even when dry
        Light = 1 << 3,       // pushed by wind
    }
 
    public enum MaterialState { Normal, Burning, Burnt, Wet, Frozen, Electrified }
 
    public static class ChemistryRules
    {
        // Rule 2: what an element source becomes when another element reaches it.
        public static Element Combine(Element present, Element incoming) => (present, incoming) switch
        {
            (Element.Fire, Element.Water) => Element.None,  // water extinguishes fire
            (Element.Ice, Element.Fire) => Element.Water,   // fire melts ice
            (Element.Water, Element.Ice) => Element.Ice,    // ice freezes water
            _ => present,                                   // everything else: no change
        };
 
        // Rule 1: how an element changes a material's state, given its tags and current state.
        public static MaterialState Apply(Element element, MaterialTags tags, MaterialState current)
        {
            if (current == MaterialState.Burnt) return current; // burnt is final
            return element switch
            {
                Element.Fire when current == MaterialState.Frozen => MaterialState.Wet,   // thaws
                Element.Fire when current == MaterialState.Wet => MaterialState.Normal,   // dries out
                Element.Fire when tags.HasFlag(MaterialTags.Flammable) => MaterialState.Burning,
                Element.Water => MaterialState.Wet,                                       // also puts out Burning
                Element.Ice when tags.HasFlag(MaterialTags.Freezable) || current == MaterialState.Wet
                    => MaterialState.Frozen,
                Element.Electricity when tags.HasFlag(MaterialTags.Conductive) || current == MaterialState.Wet
                    => MaterialState.Electrified,
                _ => current,
            };
        }
 
        // The element a body gives off in a given state. Burning things spread fire; that is
        // the only way one material affects another (rule 3).
        public static Element Emits(MaterialState state) =>
            state == MaterialState.Burning ? Element.Fire : Element.None;
    }
}