oecs-sharp/OECS/World.cs

670 lines
24 KiB
C#
Raw Normal View History

using System.Diagnostics;
using R3;
namespace OECS;
/// <summary>
/// The central container for all ECS state.
///
/// Manages entity lifecycle, component storage, and provides the primary
/// API for adding, removing, and querying components.
/// </summary>
public class World : IDisposable
{
private readonly EntityAllocator _allocator;
private readonly ComponentStore _components;
private readonly CommandQueue _commands;
private readonly RelationshipIndex _relationships;
private readonly ChangeBuffer _changes;
private readonly Dictionary<Type, Entity> _singletonEntities = new();
private bool _disposed;
// Deferred structural mutation support: when inside a batching scope
// (e.g., a foreach iteration or a system run), AddComponent,
// RemoveComponent, and DestroyEntity are buffered and applied when
// the outermost scope ends.
private int _batchingDepth;
private readonly List<PendingMutation> _pendingMutations = new();
private enum PendingMutationKind { AddComponent, RemoveComponent, DestroyEntity }
private struct PendingMutation
{
public PendingMutationKind Kind;
public Entity Entity;
public object? Component; // boxed struct for AddComponent
public Type ComponentType;
}
// Auto-dirty-marking: during a batching scope, component accesses via
// GetComponent<T> are tracked. When the scope ends, all accessed
// components are automatically marked as modified.
// Outside of a batching scope, MarkModified<T> must be called explicitly.
private readonly HashSet<(Entity Entity, Type ComponentType)> _accessedComponents = new();
private readonly HashSet<(Entity Entity, Type ComponentType)> _markedModified = new();
// Interrupt store: injected by SystemGroup. Null when no SystemGroup is managing
// this world, in which case Interrupt() is a no-op.
internal InterruptStore? _interruptStore;
public World()
{
_allocator = new EntityAllocator();
_components = new ComponentStore();
_commands = new CommandQueue();
_relationships = new RelationshipIndex();
_changes = new ChangeBuffer();
}
// ── Entity Management ────────────────────────────────────────────
/// <summary>
/// Creates a new entity and returns its handle.
/// Automatically marks an <see cref="ChangeKind.EntityAdded"/> change.
/// </summary>
public Entity CreateEntity()
{
var entity = _allocator.Allocate();
_changes.Pending.MarkEntityAdded(entity);
return entity;
}
/// <summary>
/// Creates an entity with a specific handle. Used during deserialization
/// to restore entities with their original IDs and versions.
/// Does NOT mark an EntityAdded change (caller is responsible for
/// posting changes after the full load).
/// </summary>
internal Entity CreateEntity(Entity handle)
{
_allocator.Reserve(handle);
return handle;
}
/// <summary>
/// Destroys an entity, removing all its components and recycling its ID.
///
/// Cascade behavior:
/// - All relationships where this entity is the <b>source</b> are removed.
/// - All relationships where this entity is the <b>target</b> are removed
/// from the source entities.
/// </summary>
public void DestroyEntity(Entity entity)
{
if (_batchingDepth > 0)
{
_pendingMutations.Add(new PendingMutation
{
Kind = PendingMutationKind.DestroyEntity,
Entity = entity
});
return;
}
DestroyEntityImpl(entity);
}
private void DestroyEntityImpl(Entity entity)
{
if (!_allocator.IsAlive(entity))
return;
// Remove all relationships where this entity is the target.
// Materialize to avoid collection-modified-during-enumeration when
// OnRemoved mutates the same HashSet we're iterating.
var incoming = _relationships.GetIncomingRelationships(entity)
.Select(r => (r.RelationshipType, r.Sources.ToList()))
.ToList();
foreach (var (relType, sources) in incoming)
{
foreach (var source in sources)
{
_relationships.OnRemoved(relType, source, entity);
_components.Remove(source, relType);
_changes.Pending.MarkComponentRemoved(source, relType);
}
}
// Remove all relationships where this entity is the source.
_relationships.RemoveAllSourcesForEntity(entity);
// Mark component removals for each component type before removing them.
foreach (var componentType in _components.ComponentTypes.ToList())
{
if (_components.GetSet(componentType)?.Contains(entity) == true)
{
_changes.Pending.MarkComponentRemoved(entity, componentType);
}
}
_components.RemoveAll(entity);
_allocator.Free(entity);
_changes.Pending.MarkEntityRemoved(entity);
}
/// <summary>
/// Returns true if the entity is currently alive (not destroyed).
/// </summary>
public bool IsAlive(Entity entity)
{
return _allocator.IsAlive(entity);
}
// ── Component Management ─────────────────────────────────────────
/// <summary>
/// Adds a component to the entity. Replaces the existing component if
/// the entity already has one of type <typeparamref name="T"/>.
///
/// If <typeparamref name="T"/> implements <see cref="IRelationship"/>,
/// the reverse index is updated automatically.
/// </summary>
public void AddComponent<T>(Entity entity, T component) where T : struct
{
if (_batchingDepth > 0)
{
_pendingMutations.Add(new PendingMutation
{
Kind = PendingMutationKind.AddComponent,
Entity = entity,
Component = component,
ComponentType = typeof(T)
});
return;
}
AddComponentImpl(entity, component);
}
/// <summary>
/// Adds a boxed component to the entity. Used by <see cref="WorldSerializer"/>
/// during deserialization after relationship Source fixup.
/// Calls <see cref="AddComponentImpl{T}"/> via reflection to avoid
/// duplicating the relationship index logic.
/// </summary>
internal void AddComponentBoxed(Entity entity, object component, Type componentType)
{
var method = s_addComponentImpl.MakeGenericMethod(componentType);
method.Invoke(this, [entity, component]);
}
private static readonly System.Reflection.MethodInfo s_addComponentImpl =
typeof(World).GetMethod(
nameof(AddComponentImpl),
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
private void AddComponentImpl<T>(Entity entity, T component) where T : struct
{
ThrowIfNotAlive(entity);
// If replacing an existing relationship, remove the old index entry first.
if (component is IRelationship)
{
if (_components.TryGet<T>(entity, out var old))
{
var oldRel = (IRelationship)(object)old;
_relationships.OnRemoved(typeof(T), entity, oldRel.Target);
}
}
bool isNew = !_components.Has<T>(entity);
_components.Add(entity, component);
// Update relationship index.
if (component is IRelationship rel)
{
_relationships.OnAdded(typeof(T), entity, rel.Target);
}
// Mark change.
if (isNew)
_changes.Pending.MarkComponentAdded(entity, typeof(T));
}
/// <summary>
/// Removes the component of type <typeparamref name="T"/> from the entity.
/// No-op if the entity does not have the component.
///
/// If <typeparamref name="T"/> implements <see cref="IRelationship"/>,
/// the reverse index is updated automatically.
/// </summary>
public void RemoveComponent<T>(Entity entity) where T : struct
{
if (_batchingDepth > 0)
{
_pendingMutations.Add(new PendingMutation
{
Kind = PendingMutationKind.RemoveComponent,
Entity = entity,
ComponentType = typeof(T)
});
return;
}
RemoveComponentImpl<T>(entity);
}
private void RemoveComponentImpl<T>(Entity entity) where T : struct
{
// Update relationship index before removal.
if (typeof(IRelationship).IsAssignableFrom(typeof(T)))
{
if (_components.TryGet<T>(entity, out var existing))
{
var rel = (IRelationship)(object)existing;
_relationships.OnRemoved(typeof(T), entity, rel.Target);
}
}
bool existed = _components.Has<T>(entity);
_components.Remove<T>(entity);
if (existed)
_changes.Pending.MarkComponentRemoved(entity, typeof(T));
}
/// <summary>
/// Returns a reference to the component of type <typeparamref name="T"/>
/// for the given entity. Throws if the entity does not have the component.
///
/// During iteration, this access is auto-marked as modified since it
/// returns a mutable ref. Use <see cref="ReadComponent{T}"/> if you
/// only need to read the value.
/// </summary>
public ref T GetComponent<T>(Entity entity) where T : struct
{
if (_batchingDepth > 0)
_accessedComponents.Add((entity, typeof(T)));
return ref _components.Get<T>(entity);
}
/// <summary>
/// Tracks a component access for auto-dirty-marking without returning
/// a ref. Used by <see cref="SelectN"/> iterators when they expose
/// component refs directly from the sparse set.
/// </summary>
internal void TrackAccess(Entity entity, Type componentType)
{
if (_batchingDepth > 0)
_accessedComponents.Add((entity, componentType));
}
/// <summary>
/// Returns a copy of the component of type <typeparamref name="T"/>
/// for the given entity. Never auto-marks as modified — use this when
/// you only need to read the value.
/// </summary>
public T ReadComponent<T>(Entity entity) where T : struct
{
return _components.Get<T>(entity);
}
/// <summary>
/// Tries to get the component of type <typeparamref name="T"/> for the
/// given entity. Returns true and copies the value to
/// <paramref name="value"/> if the component exists; otherwise returns false.
/// Never auto-marks as modified.
/// </summary>
public bool TryGetComponent<T>(Entity entity, out T value) where T : struct
{
return _components.TryGet<T>(entity, out value);
}
/// <summary>
/// Returns true if the entity has a component of type <typeparamref name="T"/>.
/// </summary>
public bool HasComponent<T>(Entity entity) where T : struct
{
return _components.Has<T>(entity);
}
// ── Change Tracking ──────────────────────────────────────────────
/// <summary>
/// Marks a component of type <typeparamref name="T"/> on the given entity
/// as modified. Only needed outside of iteration scopes — during a
/// <c>Select</c> loop, components accessed via <see cref="GetComponent{T}"/>
/// are auto-marked.
/// </summary>
public void MarkModified<T>(Entity entity) where T : struct
{
_changes.Pending.MarkComponentModified(entity, typeof(T));
_markedModified.Add((entity, typeof(T)));
}
/// <summary>
/// Posts all pending changes to R3 subscribers, then clears the pending set.
/// Called automatically by <see cref="SystemGroup"/> after each system
/// and after the full tick.
/// </summary>
public void PostChanges()
{
_changes.Post();
}
/// <summary>
/// Returns an observable that emits all entity changes (adds, removes,
/// component adds, component removes, component modifications).
/// </summary>
public Observable<EntityChange> ObserveEntityChanges()
{
return _changes.ObserveEntityChanges();
}
/// <summary>
/// Returns an observable that emits changes for a specific component type
/// <typeparamref name="T"/> (added, removed, or modified).
/// </summary>
public Observable<EntityChange> ObserveComponentChanges<T>() where T : struct
{
return _changes.ObserveComponentChanges(typeof(T));
}
/// <summary>
/// Returns an observable that emits component-level changes matching
/// the given query's With types and excluding its Without types.
/// </summary>
public Observable<EntityChange> ObserveQuery<T1>(Query<T1> query = default)
where T1 : struct
{
return _changes.ObserveQuery(query);
}
/// <summary>
/// Returns an observable that emits component-level changes matching
/// the given query.
/// </summary>
public Observable<EntityChange> ObserveQuery<T1, T2>(Query<T1, T2> query = default)
where T1 : struct where T2 : struct
{
return _changes.ObserveQuery(query);
}
// ── Singletons ───────────────────────────────────────────────────
/// <summary>
/// Sets (adds or replaces) a singleton component of type <typeparamref name="T"/>.
/// Each singleton component type gets its own dedicated entity.
/// </summary>
public void SetSingleton<T>(T component) where T : struct
{
if (!_singletonEntities.TryGetValue(typeof(T), out var entity))
{
entity = _allocator.Allocate();
_singletonEntities[typeof(T)] = entity;
}
AddComponent(entity, component);
}
/// <summary>
/// Returns a reference to the singleton component of type <typeparamref name="T"/>.
/// Throws if the singleton has not been set. Auto-marks as modified during iteration.
/// </summary>
public ref T GetSingleton<T>() where T : struct
{
return ref GetComponent<T>(GetSingletonEntity<T>());
}
/// <summary>
/// Returns a copy of the singleton component of type <typeparamref name="T"/>.
/// Never auto-marks as modified — use this when you only need to read the singleton.
/// </summary>
public T ReadSingleton<T>() where T : struct
{
return ReadComponent<T>(GetSingletonEntity<T>());
}
/// <summary>
/// Returns true if a singleton component of type <typeparamref name="T"/> exists.
/// </summary>
public bool HasSingleton<T>() where T : struct
{
return _singletonEntities.TryGetValue(typeof(T), out var entity)
&& HasComponent<T>(entity);
}
/// <summary>
/// Removes the singleton component of type <typeparamref name="T"/>.
/// No-op if the singleton does not have the component.
/// </summary>
public void RemoveSingleton<T>() where T : struct
{
if (_singletonEntities.TryGetValue(typeof(T), out var entity))
RemoveComponent<T>(entity);
}
/// <summary>
/// Gets the entity backing the singleton of type <typeparamref name="T"/>,
/// creating it if it doesn't exist yet.
/// </summary>
private Entity GetSingletonEntity<T>() where T : struct
{
if (!_singletonEntities.TryGetValue(typeof(T), out var entity))
{
entity = _allocator.Allocate();
_singletonEntities[typeof(T)] = entity;
}
return entity;
}
// ── Relationships ────────────────────────────────────────────────
/// <summary>
/// Returns all source entities that have a relationship of type
/// <typeparamref name="T"/> pointing to the given target entity.
/// Sources are iterated in insertion order.
/// </summary>
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship
{
return _relationships.GetSources<T>(target);
}
/// <summary>
/// Reorders the source set for the given relationship type and target
/// to match the order of entities in <paramref name="ordered"/>.
/// All entities currently in the set must appear exactly once in the
/// provided collection; no entities are added or removed.
/// Fires a <see cref="ChangeKind.RelationshipReordered"/> change event
/// on the target entity.
/// </summary>
public void ReorderSources<T>(Entity target, IReadOnlyList<Entity> ordered)
where T : struct, IRelationship
{
_relationships.ReorderSources<T>(target, ordered);
_changes.Pending.MarkRelationshipReordered(target, typeof(T));
}
// ── Commands ──────────────────────────────────────────────────────
/// <summary>
/// The command queue for deferred, serializable commands.
/// Systems enqueue commands via <c>world.Commands.Enqueue(...)</c>.
/// </summary>
public CommandQueue Commands => _commands;
/// <summary>
/// Manually drains the command queue, executing all pending commands.
/// Called automatically by <see cref="SystemGroup"/> after each system
/// and after the full tick.
/// </summary>
public void ExecuteCommands()
{
_commands.ExecuteAll(this);
}
// ── Interrupts ────────────────────────────────────────────────────
/// <summary>
/// Issues an interrupt of type <typeparamref name="T"/>, blocking the
/// next tick from running systems until a matching
/// <see cref="IInterruptHandlerCommand{TInterrupt}"/> resolves it.
///
/// Only one interrupt of a given type may be pending at a time.
/// If no <see cref="SystemGroup"/> is managing this world, interrupts
/// are silently ignored (calls are no-ops).
/// </summary>
public void Interrupt<T>(T interrupt) where T : struct, IInterrupt
{
_interruptStore?.Add(interrupt);
}
/// <summary>
/// Returns true if an interrupt of type <typeparamref name="T"/> is
/// currently pending.
/// </summary>
public bool HasInterrupt<T>() where T : struct, IInterrupt
{
return _interruptStore != null && _interruptStore.Has<T>();
}
/// <summary>
/// Retrieves a pending interrupt for the handler's default Execute.
/// </summary>
internal bool TryGetPendingInterrupt<T>(out T interrupt) where T : struct, IInterrupt
{
if (_interruptStore != null)
return _interruptStore.TryGet(out interrupt);
interrupt = default;
return false;
}
/// <summary>
/// Resolves and removes a pending interrupt. Called by the default
/// <see cref="IInterruptHandlerCommand{TInterrupt}.TryResolve"/> implementation.
/// </summary>
internal void ResolveInterrupt<T>() where T : struct, IInterrupt
{
_interruptStore?.Remove<T>();
}
// ── Iteration Lifecycle ──────────────────────────────────────────
/// <summary>
/// Begins a batching scope. Structural mutations (AddComponent,
/// RemoveComponent, DestroyEntity) are buffered until the outermost
/// <see cref="EndBatching"/> call. Component accesses via
/// <see cref="GetComponent{T}"/> are auto-tracked for modification.
/// </summary>
internal void BeginBatching()
{
_batchingDepth++;
}
/// <summary>
/// Ends a batching scope. When the outermost scope ends, all
/// buffered structural mutations are applied and auto-tracked
/// component modifications are posted.
/// </summary>
internal void EndBatching()
{
_batchingDepth--;
if (_batchingDepth == 0)
{
// Auto-mark all components accessed via GetComponent<T>
// during the iteration as modified.
foreach (var (entity, componentType) in _accessedComponents)
{
if (!_markedModified.Contains((entity, componentType)))
{
_changes.Pending.MarkComponentModified(entity, componentType);
}
}
_accessedComponents.Clear();
_markedModified.Clear();
FlushPendingMutations();
}
}
public void FlushPendingMutations()
{
if (_pendingMutations.Count == 0)
return;
foreach (var m in _pendingMutations)
{
switch (m.Kind)
{
case PendingMutationKind.AddComponent:
// Use reflection to call AddComponentImpl<T>.
var addMethod = typeof(World).GetMethod(
nameof(AddComponentImpl),
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
var genericAdd = addMethod.MakeGenericMethod(m.ComponentType);
genericAdd.Invoke(this, [m.Entity, m.Component]);
break;
case PendingMutationKind.RemoveComponent:
var removeMethod = typeof(World).GetMethod(
nameof(RemoveComponentImpl),
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
var genericRemove = removeMethod.MakeGenericMethod(m.ComponentType);
genericRemove.Invoke(this, [m.Entity]);
break;
case PendingMutationKind.DestroyEntity:
DestroyEntityImpl(m.Entity);
break;
}
}
_pendingMutations.Clear();
}
// ── Internal Access ───────────────────────────────────────────────
/// <summary>
/// The component store. Exposed internally for query execution and
/// system infrastructure.
/// </summary>
internal ComponentStore Components => _components;
/// <summary>
/// Gets the entity that backs the singleton of type <typeparamref name="T"/>,
/// or <see cref="Entity.Null"/> if the singleton has not been set.
/// </summary>
internal Entity GetSingletonEntityOrNull<T>() where T : struct
{
_singletonEntities.TryGetValue(typeof(T), out var entity);
return entity;
}
/// <summary>
/// Registers an existing entity as the backing entity for a singleton component.
/// Used during deserialization to restore singleton state.
/// </summary>
internal void RegisterSingletonEntity(Type componentType, Entity entity)
{
_singletonEntities[componentType] = entity;
}
/// <summary>
/// Returns true if the given entity is a singleton entity (backs any singleton component).
/// </summary>
internal bool IsSingletonEntity(Entity entity)
{
return _singletonEntities.ContainsValue(entity);
}
// ── Helpers ───────────────────────────────────────────────────────
private void ThrowIfNotAlive(Entity entity)
{
if (!_allocator.IsAlive(entity))
throw new InvalidOperationException($"Entity {entity} is not alive.");
}
// ── Cleanup ───────────────────────────────────────────────────────
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_changes.Dispose();
}
}