oecs-sharp/src/OECS/World.cs

394 lines
14 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;
#if DEBUG
// Tracks entities whose components were accessed via GetComponent<T>
// but never had MarkModified<T> called. Used to warn about missing
// MarkModified calls after a system runs.
private readonly HashSet<(Entity Entity, Type ComponentType)> _accessedComponents = new();
private readonly HashSet<(Entity Entity, Type ComponentType)> _markedModified = new();
#endif
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>
/// 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 (!_allocator.IsAlive(entity))
return;
// Singleton entity is never destroyed.
if (entity.Id == 1)
return;
// Remove all relationships where this entity is the target.
foreach (var (relType, sources) in _relationships.GetIncomingRelationships(entity))
{
foreach (var source in sources)
{
_relationships.OnRemoved(relType, source, entity);
_components.Remove(source, relType);
}
}
// Remove all relationships where this entity is the source.
_relationships.RemoveAllSourcesForEntity(entity);
_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
{
ThrowIfNotAlive(entity);
// If replacing an existing relationship, remove the old index entry first.
if (component is IRelationship newRel)
{
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
{
// 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.
///
/// After mutating the component through this reference, you must call
/// <see cref="MarkModified{T}"/> so that observers are notified.
/// In DEBUG builds, a warning is logged if you forget.
/// </summary>
public ref T GetComponent<T>(Entity entity) where T : struct
{
#if DEBUG
_accessedComponents.Add((entity, typeof(T)));
#endif
return ref _components.Get<T>(entity);
}
/// <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. Must be called after mutating a component via
/// <see cref="GetComponent{T}"/> so that observers are notified.
/// </summary>
public void MarkModified<T>(Entity entity) where T : struct
{
_changes.Pending.MarkComponentModified(entity, typeof(T));
#if DEBUG
_markedModified.Add((entity, typeof(T)));
#endif
}
/// <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()
{
#if DEBUG
WarnUnmarkedModifications();
#endif
_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 changes matching the given query.
/// Only component-level changes whose component type is in the query's
/// "with" set and not in the "without" set are emitted.
/// </summary>
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
{
return _changes.ObserveQuery(query);
}
// ── Relationships ────────────────────────────────────────────────
/// <summary>
/// Returns all source entities that have a relationship of type
/// <typeparamref name="T"/> pointing to the given target entity.
/// </summary>
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
where T : struct, IRelationship
{
return _relationships.GetSources<T>(target);
}
// ── 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);
}
// ── Queries ──────────────────────────────────────────────────────
/// <summary>
/// Returns a new <see cref="QueryBuilder"/> for constructing queries.
/// </summary>
public QueryBuilder Query() => new();
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// one component type.
/// </summary>
public void ForEach<T1>(
QueryDescriptor query,
ForEachAction<T1> action)
where T1 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// two component types.
/// </summary>
public void ForEach<T1, T2>(
QueryDescriptor query,
ForEachAction<T1, T2> action)
where T1 : struct where T2 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// three component types.
/// </summary>
public void ForEach<T1, T2, T3>(
QueryDescriptor query,
ForEachAction<T1, T2, T3> action)
where T1 : struct where T2 : struct where T3 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// four component types.
/// </summary>
public void ForEach<T1, T2, T3, T4>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// five component types.
/// </summary>
public void ForEach<T1, T2, T3, T4, T5>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
/// <summary>
/// Iterates all entities matching the query, providing ref access to
/// six component types.
/// </summary>
public void ForEach<T1, T2, T3, T4, T5, T6>(
QueryDescriptor query,
ForEachAction<T1, T2, T3, T4, T5, T6> action)
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct
{
QueryExecutor.ForEach(_components, query, action);
}
// ── Internal Access ───────────────────────────────────────────────
/// <summary>
/// The component store. Exposed internally for query execution and
/// system infrastructure.
/// </summary>
internal ComponentStore Components => _components;
// ── Helpers ───────────────────────────────────────────────────────
private void ThrowIfNotAlive(Entity entity)
{
if (!_allocator.IsAlive(entity))
throw new InvalidOperationException($"Entity {entity} is not alive.");
}
// ── Cleanup ───────────────────────────────────────────────────────
public void Dispose()
{
_changes.Dispose();
}
#if DEBUG
/// <summary>
/// Logs warnings for any components that were accessed via
/// <see cref="GetComponent{T}"/> but never had
/// <see cref="MarkModified{T}"/> called.
/// </summary>
private void WarnUnmarkedModifications()
{
foreach (var (entity, componentType) in _accessedComponents)
{
if (!_markedModified.Contains((entity, componentType)))
{
Debug.WriteLine(
$"[OECS] Warning: Component '{componentType.Name}' on {entity} " +
$"was accessed via GetComponent but MarkModified was never called. " +
$"Observers will not be notified of the change.");
}
}
_accessedComponents.Clear();
_markedModified.Clear();
}
#endif
}