Binding

Docs / Core / Binding

Connect ECS data to view layer (Unity GameObjects, UI, etc.) reactively using the binding system.

Overview

Binding enables:

  • Reactive Updates: Views update automatically when components change
  • Clean Separation: View layer doesn't write to ECS
  • ComponentDelta: Change notifications (Added/Changed/Removed)
  • Type-Safe: Compile-time type checking

Key Concepts

  • IBinder: Reactive component change handler
  • ComponentDelta: Change notification structure
  • Context: Shared or per-entity view data
  • Binding Router: Manages binders and applies changes

How It Works

Binding Flow

Component Change
    ↓
ComponentDelta Generated
    ↓
IBinder.OnDelta() Called
    ↓
View Updated

IBinder Interface

public interface IBinder<T> where T : struct
{
    void OnDelta(ComponentDelta<T> delta);
}

ComponentDelta Structure

public struct ComponentDelta<T>
{
    public bool IsAdded { get; }
    public bool IsChanged { get; }
    public bool IsRemoved { get; }
    public T? OldValue { get; }
    public T NewValue { get; }
}

API Surface

Registering Binders

// Register binder for entity
world.RegisterBinder(entity, new PositionBinder(transform));

// Unregister binder
world.UnregisterBinder(entity, binder);

Binder Implementation

public class PositionBinder : IBinder<Position>
{
    private readonly Transform _transform;
    
    public PositionBinder(Transform transform)
    {
        _transform = transform;
    }
    
    public void OnDelta(ComponentDelta<Position> delta)
    {
        if (delta.IsAdded || delta.IsChanged)
        {
            var pos = delta.NewValue;
            _transform.position = new Vector3(pos.X, pos.Y, pos.Z);
        }
        
        if (delta.IsRemoved)
        {
            // Handle removal
        }
    }
}

Examples

Basic Binder

using ZenECS.Core;
using ZenECS.Core.Binding;

public class PositionBinder : IBinder<Position>
{
    private readonly Transform _transform;
    
    public PositionBinder(Transform transform)
    {
        _transform = transform;
    }
    
    public void OnDelta(ComponentDelta<Position> delta)
    {
        if (delta.IsAdded || delta.IsChanged)
        {
            var pos = delta.NewValue;
            _transform.position = new Vector3(pos.X, pos.Y, pos.Z);
        }
    }
}

// Usage
var world = kernel.CreateWorld(null, "GameWorld");
Entity entity;
using (var cmd = world.BeginWrite())
{
    entity = cmd.CreateEntity();
    cmd.AddComponent(entity, new Position(0, 0, 0));
}

var binder = new PositionBinder(transform);
world.RegisterBinder(entity, binder);

// Component changes automatically trigger binder
world.ReplaceComponent(entity, new Position(1, 0, 0));
// PositionBinder.OnDelta() called automatically

Multiple Component Binder

public class TransformBinder : IBinder<Position>, IBinder<Rotation>
{
    private readonly Transform _transform;
    
    public TransformBinder(Transform transform)
    {
        _transform = transform;
    }
    
    public void OnDelta(ComponentDelta<Position> delta)
    {
        if (delta.IsAdded || delta.IsChanged)
        {
            var pos = delta.NewValue;
            _transform.position = new Vector3(pos.X, pos.Y, pos.Z);
        }
    }
    
    public void OnDelta(ComponentDelta<Rotation> delta)
    {
        if (delta.IsAdded || delta.IsChanged)
        {
            var rot = delta.NewValue;
            _transform.rotation = Quaternion.Euler(rot.X, rot.Y, rot.Z);
        }
    }
}

Best Practices

✅ Do

  • Use binders for views: Connect ECS to presentation
  • Handle all deltas: Added, Changed, Removed
  • Keep binders focused: One responsibility
  • Register properly: Use world.RegisterBinder()

❌ Don't

  • Don't mutate components: Binders are read-only
  • Don't create entities: Use messages instead
  • Don't access GameObjects directly: Use EntityViewRegistry
  • Don't store entity references: Use EntityLink

See Also