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

// GDnD wiki example (2 of 3: ChemistryRules.cs, ChemistryBody.cs, ElementSource.cs)
// Demonstrates: the "material" side of a Breath of the Wild-style chemistry engine. Put this
//   component on props, enemies AND the player, so one rule set (ChemistryRules) governs all of
//   them. It stores the current MaterialState, raises StateChanged for VFX/audio/damage, times
//   out temporary states, and spreads fire to neighbours by passing them an element (rule 3).
// Related pages: [[breath-of-the-wild]], [[emergent-design]], [[game-atoms]],
//   [[source-botw-breaking-conventions-gdc-2017]]
// Unity 6 (6000.x), 2D physics.
//
// Setup: add to any GameObject with a Collider2D and tick its Material Tags. Add a Rigidbody2D
//   too if wind should push it (tag it Light). Listen to StateChanged from another script to
//   swap sprites, play particles or apply damage (e.g. the player takes damage while Burning).
 
using System;
using System.Collections.Generic;
using UnityEngine;
 
namespace GDnD.Systemic.Chemistry
{
    public class ChemistryBody : MonoBehaviour
    {
        [SerializeField] private MaterialTags tags = MaterialTags.Flammable;
        [SerializeField] private float burnDuration = 4f;      // Burning -> Burnt after this
        [SerializeField] private float shockDuration = 1f;     // Electrified -> Wet/Normal after this
        [SerializeField] private float spreadRadius = 0.75f;
        [SerializeField] private float spreadInterval = 0.5f;
        [SerializeField] private float windForce = 20f;
 
        public MaterialState State { get; private set; } = MaterialState.Normal;
        public MaterialTags Tags => tags;
        public event Action<MaterialState> StateChanged;
 
        private readonly List<Collider2D> neighbours = new();
        private MaterialState stateBeforeShock;
        private float stateTimer;
        private float spreadTimer;
        private Rigidbody2D body;
 
        private void Awake() => body = GetComponent<Rigidbody2D>();
 
        // The only way anything changes this body's state: an element arrives.
        public void ReceiveElement(Element element, Vector2 direction = default)
        {
            // Hand-off to physics: wind is the one element that moves things. ElementSource calls
            // this every physics step while the body is inside a gust, so use continuous force.
            if (element == Element.Wind && tags.HasFlag(MaterialTags.Light) && body != null)
                body.AddForce(direction.normalized * windForce, ForceMode2D.Force);
 
            MaterialState next = ChemistryRules.Apply(element, tags, State);
            if (next == MaterialState.Electrified && State != MaterialState.Electrified)
                stateBeforeShock = State;
            SetState(next);
        }
 
        private void SetState(MaterialState next)
        {
            if (next == State) return;
            State = next;
            stateTimer = 0f;
            StateChanged?.Invoke(State);
        }
 
        private void Update()
        {
            stateTimer += Time.deltaTime;
            if (State == MaterialState.Burning && stateTimer >= burnDuration) SetState(MaterialState.Burnt);
            if (State == MaterialState.Electrified && stateTimer >= shockDuration) SetState(stateBeforeShock);
 
            Element emitted = ChemistryRules.Emits(State);
            if (emitted == Element.None || (spreadTimer += Time.deltaTime) < spreadInterval) return;
            spreadTimer = 0f;
 
            // Rule 3: never edit a neighbour's state directly; pass it an element instead.
            Physics2D.OverlapCircle(transform.position, spreadRadius, ContactFilter2D.noFilter, neighbours);
            foreach (Collider2D other in neighbours)
                if (other.gameObject != gameObject && other.TryGetComponent(out ChemistryBody target))
                    target.ReceiveElement(emitted);
        }
    }
}