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);
}
}
}