Since my first post on dev.to last week, I've had time to take a deeper look at how BOCS could work for reactive and dynamic simulations. This goes past its previous applications in narrative games, applying the same principles and code, just in a different environment.
So how does the Behaviour and Module pattern work in simulations?
General Idea Recap
Like in narrative games, BOCS for simulations focuses around Behaviours. Each entity or object in the simulation contains a number of Behaviours that define how it, well, behaves. These Behaviours are then acted upon by external systems through an interface (the Modules that each Behaviour implements). For a more detailed breakdown of how BOCS works in general, check out my first article introducing BOCS.
Some examples
Let's take a look at some use cases / example objects where BOCS can be used to compose them efficiently. These are mostly taken from my WIP Mars colony simulation (fittingly named Incident Report). First, some quick example Behaviours (each one represents a distinct capability):
-
PowerConsumerBehaviour- can use electrical energy. -
HeatSourceBehaviour- can emit thermal energy. -
NetworkNodeBehaviour<T>- can connect to a resource network / graph as a node. -
PowerStorerBehaviour- can store power. -
OxygenConsumerBehaviour- can use oxygen. - etc.
These, and more, let us create highly specific entities without hardcoding dozens of special cases. Now let's dive into some deeper examples.
1. Example: A Simple Power Generator
Say we want a small power generator that converts stored fuel into electricity.
Behaviours:
-
FuelConsumerBehaviour(implementsIConsumer<Fuel>) -
PowerSourceBehaviour(implementsISource<Power>) -
PowerNetworkNodeBehaviour(implementsINetworkNode<Power>) -
FuelNetworkNodeBehaviour(implementsINetworkNode<Fuel>)
Example FuelConsumerBehaviour implementation:
public class FuelConsumerBehaviour : BehaviourBase, IConsumer<Fuel>
{
private Fuel _storedFuel;
private readonly Fuel _capacity;
public FuelConsumerBehaviour(double capacity)
{
_capacity = capacity;
_storedFuel = 0.0;
}
// Attempt to withdraw up to `amount`. Returns actual withdrawn amount.
public Fuel TryConsume(Fuel fuel)
{
if (fuel.Amount <= 0) return 0;
Fuel taken = Math.Min(fuel.Amount, _storedFuel.Amount);
_storedFuel.Amount -= taken.Amount;
// notify fuel change listeners
return taken;
}
}
This is powerful because the Behaviours don't know anything about systems or simulation loops - they only coordinate the Modules it composes, making testing a lot easier.
2. Example: Heat Generator (Power turns into Heat)
A heat generator that converts electrical power into thermal energy.
It could be a reactor coolant pump, a habitat heater, or even a solar array radiating heat.
Behaviours:
-
PowerConsumerBehaviour(implementsIConsumer<Power>) -
HeatSourceBehaviour(implements ISource`) -
PowerNetworkNodeBehaviour(implementsINetworkNode<Power>) -
ThermalNetworkNodeBehaviour(implementsINetworkNode<Thermal>)
Any system that ticks all IConsumer<Power> and ISource<Heat> interfaces will automatically handle this generator correctly, no special cases required.
3. Example: A Power Storage Unit (Battery)
A battery just contains a PowerStorerBehaviour, and (if you think about it) a PowerStorerBehaviour is just consuming given power and providing requested power. In other words, it's simply an IConsumer and an ISource!
Behaviours:
-
PowerStorerBehaviour(implementsIConsumer<Power>andISource<Power>) -
PowerNetworkNodeBehaviour(implementsINetworkNode<Power>)
Example PowerStorerBehaviour Implementation:
public class PowerStorerBehaviour : BehaviourBase, IConsumer<Power>, ISource<Power>
{
private readonly double _capacity;
private readonly double _maxChargeRate;
private readonly double _maxDischargeRate;
private double _charge;
public PowerStorerBehaviour(double capacity, double maxChargeRate, double maxDischargeRate)
{
_capacity = capacity;
_maxChargeRate = maxChargeRate;
_maxDischargeRate = maxDischargeRate;
_charge = 0.0;
}
public double Stored => _charge;
public double Capacity => _capacity;
// "Consume" some amount of energy
public double Consume(double amount, double dtSecs)
{
// consumption logic...
// notify listeners...
}
// "Extract" some amount of energy
public Energy Extract(double amount, double dtSecs)
{
// extraction logic...
// notify listeners...
}
// support for efficiency losses and degradation...
}
All we need to do to modify the battery's capacity, charge/discharge rate, or anything else, is change the parameters.
4. Example: An Air Processor
This takes power and produces oxygen. We can also make it consume carbon dioxide - simply add a CarbonDioxideConsumerBehaviour.
Behaviours:
PowerConsumerBehaviour-
OxygenSourceBehaviour(implementsISource<Oxygen>) PowerNetworkNodeBehaviour-
AtmosphereNetworkNodeBehaviour(implementsINetworkNode<Atmosphere>)
BOCS makes this design emergent quickly and easily, and what's amazing is that the simulation just sees a network of IConsumer and ISource behaviours doing their job.
5. Example: A Room
Even rooms in Incident Report can themselves be treated as BOCS objects with behaviours:
-
AtmosphereContainerBehaviour- stores and tracks gas composition, temperature, and pressure. -
AtmosphereNetworkNodeBehaviour- connects to the base’s air ducts. -
ThermalNetworkNodeBehaviour- allows conduction and radiation exchange. -
PhysicalNetworkNodeBehaviour- simulates real, physical connections, allowing for moving entities to traverse in and out of this object.
Example AtmosphereContainerBehaviour Implementation (taken straight from Incident Report with some removed functionality, including some data types and documentation):
// Uses ideal-gas approximations and per-gas mole bookkeeping.
public class AtmosphereContainerBehaviour : BehaviourBase
{
// Physical constants / defaults
private const double R = 8.314462618; // J / (mol K) - universal gas constant
private const double DefaultMolarHeatCapacity = 29.1; // J / (mol K) approximate Cp for air
// State
private readonly double _volumeM3; // fixed container volume (m^3)
private readonly Dictionary<string, double> _moles = new(); // gas name to moles
private double _temperatureK; // absolute temperature (K)
public AtmosphereContainerBehaviour(double volumeM3, double initialTempK = 293.15)
{
if (volumeM3 <= 0) throw new ArgumentOutOfRangeException(nameof(volumeM3));
_volumeM3 = volumeM3;
_temperatureK = initialTempK;
}
// Read-only properties useful for networks and telemetry:
public double Volume => _volumeM3;
public double TemperatureK => _temperatureK;
public double TotalMoles => _moles.Values.Sum();
// Ideal gas law: P = n R T / V
public double PressurePa => (TotalMoles <= 0) ? 0.0 : (TotalMoles * R * _temperatureK) / _volumeM3;
// Per-gas helpers
public double GetMoles(string gas) => _moles.TryGetValue(gas, out var v) ? v : 0.0;
public double MoleFraction(string gas) => (TotalMoles <= 0) ? 0.0 : GetMoles(gas) / TotalMoles;
public double PartialPressure(string gas) => MoleFraction(gas) * PressurePa;
// Atmosphere API used by atmosphere network adapters
// Add gas (moles). Returns moles actually added (accepts all in this simple model).
// Network can call this to push gas into the room.
public double AddGas(string gas, double moles)
{
if (string.IsNullOrEmpty(gas)) throw new ArgumentNullException(nameof(gas));
if (moles <= 0) return 0;
if (!_moles.ContainsKey(gas)) _moles[gas] = 0.0;
_moles[gas] += moles;
return moles;
}
// Remove up to `moles` of a named gas. Returns how many moles were removed.
public double RemoveGas(string gas, double moles)
{
if (string.IsNullOrEmpty(gas)) throw new ArgumentNullException(nameof(gas));
if (moles <= 0) return 0;
if (!_moles.TryGetValue(gas, out var available) || available <= 0) return 0;
double removed = Math.Min(moles, available);
_moles[gas] = available - removed;
if (_moles[gas] == 0) _moles.Remove(gas);
return removed;
}
// Transfer a portion (by moles) of the container's entire mixture out to another container.
// Network adapters (or Physical node on opening) can call this to equalize or move gas.
public Dictionary<string, double> TransferMixture(double molesToTransfer)
{
var result = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
if (molesToTransfer <= 0 || TotalMoles <= 0) return result;
// scale each gas according to current mole fraction
double fraction = Math.Min(1.0, molesToTransfer / TotalMoles);
var keys = _moles.Keys.ToArray();
foreach (var gas in keys)
{
double amount = _moles[gas] * fraction;
if (amount <= 0) continue;
_moles[gas] -= amount;
if (_moles[gas] <= 0) _moles.Remove(gas);
result[gas] = amount;
}
return result;
}
// Thermal API used by thermal network adapters
// Apply energy (J). Positive adds heat, negative cools. Returns energy actually applied (here we accept all).
public double ApplyHeatEnergy(double energyJ)
{
if (Math.Abs(energyJ) < 1e-12) return 0;
double cpTotal = TotalMoles * DefaultMolarHeatCapacity; // J / K
if (cpTotal <= 0)
{
// no gas -> energy raises empty volume (ignored in simple model)
return 0;
}
double deltaT = energyJ / cpTotal;
_temperatureK += deltaT;
// clamp temperature to physical bounds and record energy balance...
return energyJ;
}
// Exchange energy with another container (energy flows from this -> target).
public double ExchangeEnergyTo(AtmosphereContainerBehaviour target, double energyJ)
{
// Simple direct transfer (no losses)
double applied = Math.Min(energyJ, energyJ); // placeholder
this.ApplyHeatEnergy(-applied);
target.ApplyHeatEnergy(applied);
return applied;
}
// Utility / Debugging
public override string ToString()
{
return $"Atmosphere(V={_volumeM3}m^3, T={_temperatureK:F1}K, P={PressurePa:F1}Pa, n={TotalMoles:F3}mol)";
}
// Omitted complexities:
// - Use species-specific molar heat capacities and non-ideal-gas corrections (esp. under high pressure).
// - Track mass vs moles if condensables (water) are present, handle phase changes.
// - Support diffusive exchange, leak rates through small openings, and door/windows transient flows.
// - Proper unit conversion helpers and notify listeners to pressure/temperature change
// - Provide per-gas partial heat capacities for accurate thermal coupling.
}
This makes rooms active participants in the simulation rather than passive data blobs like they would be in an ECS.
Advanced Example: A Reactor Core System
Let’s simulate a fusion reactor that generates energy and heat using fuel and power, with automatic safety shutdown when things go wrong. NOTE: the code snippets are simplified to get the general idea across easier.
Each behaviour is isolated but composed to create the overall system.
Behaviours:
PowerSourceBehaviour
public class PowerSourceBehaviour : BehaviourBase, ISource<Power>
{
private Power _maxPowerExtractionKW;
private Power _currentPowerExtraction = Power.FromKW(0.0);
public Power MaxPowerExtraction { get => _maxPowerExtractionKW; }
private Percentage _extractionEfficiency = new Percentage(1.0);
public Percentage ExtractionEfficiency { get => _extractionEfficiency; }
public PowerSourceBehaviour(Power maxPowerUsageKW)
{
_maxPowerExtractionKW = maxPowerUsageKW;
}
public Energy Extract(double dtSecs)
{
// _currentPowerExtraction is in kW, so convert to W, then multiply by time (s) for Joules
double joules = ((double)_currentPowerExtraction * (double)_extractionEfficiency) * 1000.0 * dtSecs;
// notify listeners...
return Energy.FromJoules(joules);
}
}
HeatSourceBehaviour
public class HeatSourceBehaviour : BehaviourBase, ISource<Heat>
{
// various fields and properties
public Temperature Extract(double dtSecs, Power power)
{
Temperature temperature = new Temperature(0);
// logic to calculate how much heat is emitted
// logic to notify any heat stat listener behaviours
return temperature;
}
}
FuelConsumerBehaviour
public class FuelConsumerBehaviour : BehaviourBase, IConsumer<Fuel>
{
// various fields and properties
public Fuel Use(double dtSecs)
{
Fuel fuel = new Fuel(0);
// logic to calculate how much fuel is consumed after using this object for dtSecs seconds
// notify listeners...
return fuel;
}
}
PowerConsumerBehaviour
public class PowerConsumerBehaviour : BehaviourBase, IConsumer<Power>
{
// various fields and properties
public Power Use(double dtSecs)
{
Power power = new Fuel(0);
// logic to calculate how much power is consumed after using this object for dtSecs seconds
// notify listeners...
return power;
}
}
HeatStatListenerBehaviour
public class HeatStatListenerBehaviour : BehaviourBase, IActOnParentStatUpdated<Heat>
{
public readonly BOCSObject Parent;
private Temperature _heatThreshold = new Temperature(500);
private bool _emergencyTriggered;
// instantiate logic
public void OnParentTargetStatUpdate()
{
if (Parent.CurrentHeat > _heatThreshold && !_emergencyTriggered)
{
_emergencyTriggered = true;
Parent.DisableBehaviour<PowerSourceBehaviour>();
Parent.DisableBehaviour<HeatSourceBehaviour>();
// notify listeners...
}
}
}
Composing the Object
var reactor = new BOCSObject("FusionReactor");
reactor.AddBehaviour<PowerSourceBehaviour>();
reactor.AddBehaviour<HeatSourceBehaviour>();
reactor.AddBehaviour<FuelConsumerBehaviour>();
reactor.AddBehaviour<PowerConsumerBehaviour>();
reactor.AddBehaviour<HeatStatListenerBehaviour>();
var power = reactor.GetBehaviour<PowerSourceBehaviour>();
var heat = reactor.GetBehaviour<HeatSourceBehaviour>();
What’s Happening
-
PowerSourceBehaviourproduces power every tick. -
HeatSourceBehaviourreacts to power output and calls any stat listeners. -
FuelConsumerBehaviouruses up fuel to generate power. -
PowerConsumerBehaviouruses up some power to use fuel and generate power. -
HeatStatListenerBehaviourlistens for updates to its parent Reactor heat stat and can disable all functionality if it gets too hot.
The system is modular, extensible, testable: however, just by themselves, Behaviours can't do much. It is up to external systems to activate these Behaviours and to coordinate and use them properly. Some extra code could be included to automate this coordination such that only a public method Tick() could be exposed, which gets the Behaviours to act with a single call. Furthermore, common objects can be created easily (including all their Behaviours added) through a factory pattern.
For the reactor, we could later add:
- A
HeatConsumerBehaviour(a coolant pump) that reduces heat when active. - A
PowerStorerBehaviour(a battery) that captures unused power. - Another
HeatStatListenerBehaviourthat re-enables systems after cooldown, or just modify the currentHeatStatListenerBehaviour.
Now, Why Use BOCS In These Instances?
BOCS gives every object in the simulation a composable identity.
We no longer need to define hundreds of bespoke classes like Heater, Battery, or OxygenPump.
Instead, we just combine Behaviours:
When NOT to Use BOCS
BOCS is brilliant for objects with identity or complex emergent behaviour: machines, rooms, or actors that do something.
But for purely structural or relational elements, such as connections, BOCS can be overkill.
Example: Network Connections
Incident Report has Networks which handle resource transfer between NetworkNodeBehaviours via NetworkConnections. However, a pipe between two nodes or a wire between two terminals doesn’t need Behaviours.
It’s just a structural link with defined flow characteristics:
public class NetworkConnection<T>
{
public NetworkNodeBehaviour<T> A;
public NetworkNodeBehaviour<T> B;
public float MaxFlowRate;
public float Resistance;
public float CurrentFlow;
private List<NetworkConnectionSegment> segments = new();
}
We don’t need to attach this to a BOCS object, with all its overhead and extra code - there's no need for a wire to have modular behaviour, or a pipe to have emergent characteristics.
Instead, a NetworkSystem can own and tick all NetworkConnection<T> instances directly.
BOCS should handle functional entities, NOT structural edges. And that's a really important distinction as well - if the line between using BOCS and not using BOCS is not bolded and solid, the complexity of a project may increase exponentially, a problem that BOCS (when used well) wouldn't usually have.
You might've also noticed I didn't go into detail the specifics of NetworkNodeBehaviour and NetworkConnection. This is because I haven't fully decided on how they should work in relation to everything else in Incident Report, and thus haven't fleshed out the implementation details. I will, however, make a post entirely on the Network system in Incident Report later down the line!
Closing Thoughts
At its core, BOCS is about letting objects define what they are by what they can do, not by which inheritance chain they belong to. When applied to simulations, that principle becomes even more powerful. Every entity - from a fusion reactor to a simple air processor - becomes a living, reactive agent in a larger network, capable of communicating, changing, and adapting through composition alone.
This makes simulations written with BOCS inherently modular, reactive, and testable. You can tweak a single behaviour, add or remove capabilities, or swap modules entirely without touching other code. And because everything is defined in terms of capabilities (ISource, IConsumer, INetworkNode...), it encourages systems thinking - building worlds where every piece, big or small, can participate in the larger emergent story of the simulation.
But that flexibility comes with discipline. As I mentioned above, BOCS isn’t for everything. The trick is to use it where identity matters, where an object’s purpose, reaction, and behaviour make it need more than just a line in a data table.
What do you think?
Do you see potential in using BOCS for simulation frameworks or complex game systems?
How would you structure the coordination layer - the part that “ticks” all Behaviours and manages how they talk to each other?
I’d love to hear your perspective, whether you’ve worked with ECS, reactive systems, or your own flavour of modular architecture!

Top comments (0)