Table of Contents

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