Module Pattern
Canonical source. Every major system is structured as a module with four nested types. This is the foundational architecture pattern.
The 4-Struct Architecture
| Nested Type |
Kind (Plain C#) |
Kind (MonoBehaviour) |
Purpose |
Mutability |
Configuration |
readonly struct |
struct |
Serializable settings, tunables |
Immutable; replaceable via SetConfiguration(in) |
Reference |
readonly struct |
struct |
External dependencies, assets |
Immutable; set once at construction |
State |
struct |
struct |
Runtime mutable data |
External reads get copy (value type), internal writes via _state field |
Components |
class |
class |
Child sub-systems, composed modules |
Created once, used at runtime |
Plain C# uses readonly struct for zero-cost immutability. MonoBehaviour uses struct (not readonly) because Unity's Inspector serialization requires mutable fields.
flowchart LR
M[Module] --> C["Configuration\nWHAT — settings, tunables"]
M --> R["Reference\nWHO — dependencies, external refs"]
M --> S["State\nWHERE — runtime data, current values"]
M --> CO["Components\nHOW — sub-systems, child modules"]
Plain C# Module
For systems that don't live on a GameObject. Owned and composed by other modules.
using PixelEngine.Architecture;
using System;
public class WeaponSystem : IDisposable
{
// ── 1. CONFIGURATION (readonly struct — immutable after construction) ──
[Serializable]
public readonly struct Configuration : IConfiguration
{
public readonly int maxAmmo;
public readonly float fireRate;
public static readonly Configuration Default = new Configuration(
maxAmmo: 30,
fireRate: 0.1f
);
public Configuration(int maxAmmo, float fireRate)
{
this.maxAmmo = maxAmmo;
this.fireRate = fireRate;
}
}
// ── 2. REFERENCE (readonly struct — immutable after construction) ──
[Serializable]
public readonly struct Reference : IReference
{
public static readonly Reference Default = new Reference();
}
// ── 3. STATE (struct — mutable internally, external reads get copy) ──
public struct State : IState
{
public int currentAmmo;
public bool isFiring;
}
// ── 4. COMPONENTS (class — child sub-systems) ──
public class Components : IComponents
{
}
// ── Backing fields and properties ──
private Configuration _configuration;
private Reference _reference;
private State _state;
public Configuration configuration => _configuration;
public Reference reference => _reference;
public State state => _state;
public Components components { get; private set; }
// ── Events (primary communication — no Update polling) ──
public event Action<int> AmmoChanged;
public event Action AmmoEmpty;
public event Action Fired;
// ── Constructors (flexible overloads) ──
public WeaponSystem()
: this(Configuration.Default, Reference.Default) { }
public WeaponSystem(in Configuration configuration)
: this(in configuration, Reference.Default) { }
public WeaponSystem(in Reference reference)
: this(Configuration.Default, in reference) { }
public WeaponSystem(in Configuration configuration, in Reference reference)
{
_configuration = configuration;
_reference = reference;
_state = new State();
this.components = new Components();
Init();
}
// ── SetConfiguration (in keyword — zero-copy pass by ref) ──
public void SetConfiguration(in Configuration configuration)
{
_configuration = configuration;
}
public void Init()
{
_state.currentAmmo = _configuration.maxAmmo;
}
public void Dispose()
{
// de-initialize in last-in/first-out order
}
}
Key Features
| Feature |
Detail |
readonly struct |
Configuration and Reference are immutable value types |
in keyword |
SetConfiguration(in Configuration) passes by ref without copying |
Backing fields + => |
Struct state stored in _state field; property returns copy to external readers |
event Action<T> |
Primary communication mechanism — no Update() polling |
| Constructor overloads |
Create with any combination of config/reference |
IDisposable |
Explicit cleanup in LIFO order |
MonoBehaviour Module (Component Variant)
For modules that need to live on a GameObject. Inspector-visible config and references.
using PixelEngine.Architecture;
using System;
using UnityEngine;
public class WeaponSystemComponent : MonoBehaviour, IDisposable
{
// ── 1. CONFIGURATION (struct, not readonly — Inspector needs mutable fields) ──
[Serializable]
public struct Configuration : IConfiguration
{
public int maxAmmo;
public float fireRate;
public static Configuration Default = new Configuration
{
maxAmmo = 30,
fireRate = 0.1f,
};
}
// ── 2. REFERENCE (struct — Inspector-serializable) ──
[Serializable]
public struct Reference : IReference
{
public static Reference Default = new Reference { };
}
// ── 3. STATE (struct — mutable internally, external reads get copy) ──
public struct State : IState
{
public int currentAmmo;
public bool isFiring;
}
// ── 4. COMPONENTS (class — child sub-systems) ──
public class Components : IComponents
{
}
// ── Inspector fields ([SerializeField] private) ──
[SerializeField] private Configuration _configuration = Configuration.Default;
[SerializeField] private Reference _reference = Reference.Default;
private State _state;
// ── Properties (ref readonly for zero-copy read; backing field for struct state) ──
public ref readonly Configuration configuration => ref _configuration;
public ref readonly Reference reference => ref _reference;
public State state => _state;
public Components components { get; private set; }
// ── Events (primary communication — no Update polling) ──
public event Action<int> AmmoChanged;
public event Action AmmoEmpty;
public event Action Fired;
// ── Lifecycle: Awake → Init, OnDestroy → Dispose ──
private void Awake()
{
_state = new State();
components = new Components();
Init();
}
private void Init()
{
_state.currentAmmo = _configuration.maxAmmo;
}
public void Dispose()
{
// cleanup
}
private void OnDestroy()
{
Dispose();
}
}
MonoBehaviour Key Differences
| Feature |
Plain C# |
MonoBehaviour |
| Configuration/Reference kind |
readonly struct |
struct (Inspector needs mutability) |
| Inspector fields |
N/A |
[SerializeField] private |
| Config/Ref access |
Backing field + => property |
ref readonly return (zero-copy read) |
| State access |
_state backing field, => returns copy |
_state backing field, => returns copy |
| Lifecycle |
Manual Init() / Dispose() |
Awake() → Init(), OnDestroy() → Dispose() |
| Ownership |
Composed by other modules |
Lives on a GameObject |
Module vs Module Component
| Aspect |
Module (Plain C#) |
Module Component (MonoBehaviour) |
| Base |
IDisposable |
MonoBehaviour, IDisposable |
| Config / Reference |
Constructor-injected via in, backing field + => |
[SerializeField] private, ref readonly return |
| State / Components |
_state backing field + => (struct); private set (Components class) |
_state backing field + => (struct); private set (Components class) |
| Lifecycle |
Manual Init() / Dispose() |
Awake() / OnDestroy() |
| Communication |
event Action<T> |
event Action<T> |
Update() |
None — event-driven |
None — event-driven |
| Use when |
Logic-only systems, no scene presence |
Needs Inspector, scene lifecycle |
Rules
| Rule |
Details |
| Always implement all 4 nested types |
Even if empty — keeps the shape consistent |
Plain C#: readonly struct for Config/Ref |
Immutable value types, zero-copy with in |
MonoBehaviour: struct for Config/Ref |
Inspector serialization requires mutable fields |
State is a struct, Components is a class |
State is a value type stored in _state backing field; Components is a reference type with private set |
Provide a Default static field |
On both Configuration and Reference |
in keyword on SetConfiguration |
Zero-copy pass by reference |
ref readonly on MonoBehaviour properties |
Zero-copy read access for structs |
| Events for communication |
event Action / event Action<T> — no Update() polling |
No Update() in template |
Event-driven by default; add only when truly needed |
| Dispose in LIFO order |
Last initialized = first disposed |
[SerializeField] private |
MonoBehaviour Inspector fields are never public |
See Also