Skip to content

OOP Integration

Trecs is a pure ECS framework, but Unity games need GameObjects, MonoBehaviours, and other managed objects. This recipe describes a three-layer architecture for bridging ECS and non-ECS code cleanly.

The Three Layers

┌─────────────────────────────────────────┐
│  Layer 1: Non-ECS → ECS (Input)         │
│  MonoBehaviours, services, Unity APIs   │
│  → Queues input into ECS                │
└─────────────┬───────────────────────────┘
              │ AddInput()
┌─────────────▼───────────────────────────┐
│  Layer 2: Pure ECS (Simulation)         │
│  Systems, components, templates         │
│  No Unity APIs, fully deterministic     │
└─────────────┬───────────────────────────┘
              │ Read components
┌─────────────▼───────────────────────────┐
│  Layer 3: ECS → Non-ECS (Output)        │
│  Sync transforms, spawn GameObjects     │
│  [VariableUpdate] systems               │
└─────────────────────────────────────────┘

Layer 1: Input Bridge

MonoBehaviours read player input and queue it into the ECS world:

public class InputBridge : MonoBehaviour
{
    WorldAccessor _world;
    EntityHandle _globalEntity;

    void Update()
    {
        var dir = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
        _world.AddInput(_globalEntity, new MoveInput { Direction = dir });
    }
}

Use [InputSystem] systems to process queued input at the start of each fixed update.

Layer 2: Pure ECS

Simulation systems contain only ECS logic — no GameObject, no MonoBehaviour, no UnityEngine APIs:

public partial class MovementSystem : ISystem
{
    [ForEachEntity(Tags = new[] { typeof(GameTags.Player) })]
    void Execute(in PlayerView player)
    {
        player.Position += player.Velocity * World.DeltaTime;
    }
}

This layer is fully deterministic and can be recorded/replayed.

Layer 3: Output Bridge

Variable-update systems sync ECS state to GameObjects:

[VariableUpdate]
public partial class GameObjectSyncSystem : ISystem
{
    readonly GameObjectRegistry _registry;

    [ForEachEntity(MatchByComponents = true)]
    void Execute(in Position pos, in Rotation rot, in GameObjectId id)
    {
        var go = _registry.Resolve(id);
        go.transform.position = (Vector3)pos.Value;
        go.transform.rotation = rot.Value;
    }
}

Spawning and Despawning GameObjects

Use entity events with [ForEachEntity] to manage GameObject lifecycle:

public partial class EnemyGameObjectManager : IDisposable
{
    readonly GameObjectRegistry _registry;
    readonly DisposeCollection _disposables = new();

    public EnemyGameObjectManager(World world, GameObjectRegistry registry)
    {
        World = world.CreateAccessor();
        _registry = registry;

        World.Events.EntitiesWithTags<GameTags.Enemy>()
            .OnRemoved(OnEnemyRemoved)
            .AddTo(_disposables);
    }

    WorldAccessor World { get; }

    [ForEachEntity]
    void OnEnemyRemoved(in GameObjectId id)
    {
        var go = _registry.Resolve(id);
        GameObject.Destroy(go);
        _registry.Unregister(id);
    }

    public void Dispose() => _disposables.Dispose();
}

Referencing Managed Objects

Use heap pointer types to store managed references in components:

public struct AudioSourceRef : IEntityComponent
{
    public SharedPtr<AudioClip> Clip;
}

Heap blob types must be serializable

If you save/load or record/replay your world, every T you allocate on the heap (SharedPtr<T>, UniquePtr<T>, NativeSharedPtr<T>, NativeUniquePtr<T>) must have a serializer registered — blobs are written as part of world state using their registered ISerializer<T>. Unmanaged T is covered by RegisterBlit<T>; managed types like Unity AudioClip / Mesh need a custom ISerializer<T> (or RegisterSkip<T> if the pointed-to data can be safely reconstructed from elsewhere on load). See Serialization for details.

Why This Separation Matters

  • Determinism — Layer 2 has no external dependencies, enabling recording and replay
  • Testability — pure ECS logic can be tested without Unity
  • Portability — simulation code doesn't depend on specific rendering or input systems
  • Clarity — each layer has a single responsibility