feat: add query system and system execution logic
Introduces a complete query system for iterating over entities with specific component combinations, including support for "with" and "without" filters. Also adds: - `ISystem` and `ITickedSystem` interfaces for defining logic. - `SystemGroup` for managing and running groups of systems. - `QueryBuilder` for a fluent API to construct queries. - `QueryExecutor` for high-performance iteration using ref parameters. - `Tick` and `TickType` to support timed and logical updates.
This commit is contained in:
parent
6739cf4ac9
commit
e4afaafd02
|
|
@ -0,0 +1,39 @@
|
|||
namespace OECS;
|
||||
|
||||
// Custom delegate types for query iteration with ref parameters.
|
||||
// The built-in Action<...> delegates do not support ref parameters.
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with one component type.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with two component types.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1, T2>(Entity entity, ref T1 c1, ref T2 c2)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with three component types.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1, T2, T3>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with four component types.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1, T2, T3, T4>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with five component types.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for iterating entities with six component types.
|
||||
/// </summary>
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5, T6>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5, ref T6 c6)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// A system that runs on every tick.
|
||||
/// Systems declare a query and receive the world in their <see cref="Run"/> method.
|
||||
/// </summary>
|
||||
public interface ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// The query that defines which entities this system operates on.
|
||||
/// </summary>
|
||||
QueryDescriptor Query { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the system logic for this tick.
|
||||
/// </summary>
|
||||
void Run(World world);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// A system that receives tick data (delta time or logical tick) in addition
|
||||
/// to the world reference.
|
||||
/// </summary>
|
||||
public interface ITickedSystem : ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the system logic with tick information.
|
||||
/// </summary>
|
||||
void Run(World world, Tick tick);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for constructing <see cref="QueryDescriptor"/> instances.
|
||||
/// Returned by <see cref="World.Query"/>.
|
||||
/// </summary>
|
||||
public class QueryBuilder
|
||||
{
|
||||
private readonly HashSet<Type> _with = new();
|
||||
private readonly HashSet<Type> _without = new();
|
||||
|
||||
/// <summary>
|
||||
/// Requires entities to have a component of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
public QueryBuilder With<T>() where T : struct
|
||||
{
|
||||
_with.Add(typeof(T));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excludes entities that have a component of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
public QueryBuilder Without<T>() where T : struct
|
||||
{
|
||||
_without.Add(typeof(T));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the query descriptor.
|
||||
/// </summary>
|
||||
public QueryDescriptor Build()
|
||||
{
|
||||
return new QueryDescriptor(
|
||||
new HashSet<Type>(_with),
|
||||
new HashSet<Type>(_without));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a query over the ECS world.
|
||||
/// Composed of a set of required component types ("with") and
|
||||
/// a set of excluded component types ("without").
|
||||
/// </summary>
|
||||
public class QueryDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Component types that must be present on matching entities.
|
||||
/// </summary>
|
||||
public IReadOnlySet<Type> With { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Component types that must NOT be present on matching entities.
|
||||
/// </summary>
|
||||
public IReadOnlySet<Type> Without { get; }
|
||||
|
||||
internal QueryDescriptor(HashSet<Type> with, HashSet<Type> without)
|
||||
{
|
||||
With = with;
|
||||
Without = without;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this query has no "with" components (matches nothing).
|
||||
/// </summary>
|
||||
internal bool IsEmpty => With.Count == 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Provides query execution logic for <see cref="World"/>.
|
||||
/// Iterates one sparse set as the driver and probes the remaining sets
|
||||
/// for membership. The "without" filter is checked after all "with" probes.
|
||||
/// </summary>
|
||||
internal static class QueryExecutor
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool PassesWithoutFilter(ComponentStore store, Entity entity, IReadOnlySet<Type> withoutTypes)
|
||||
{
|
||||
foreach (var type in withoutTypes)
|
||||
{
|
||||
var set = store.GetSet(type);
|
||||
if (set != null && set.Contains(entity))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── ForEach overloads ──────────────────────────────────────────────
|
||||
|
||||
public static void ForEach<T1>(
|
||||
ComponentStore store,
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1> action)
|
||||
where T1 : struct
|
||||
{
|
||||
var set = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set == null) return;
|
||||
|
||||
var dense = set.Dense;
|
||||
var denseEntities = set.DenseEntities;
|
||||
var count = set.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2>(
|
||||
ComponentStore store,
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1, T2> action)
|
||||
where T1 : struct where T2 : struct
|
||||
{
|
||||
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set1 == null) return;
|
||||
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||
if (set2 == null) return;
|
||||
|
||||
if (set1.Count <= set2.Count)
|
||||
{
|
||||
IterateTwo(set1, set2, store, query.Without, action);
|
||||
}
|
||||
else
|
||||
{
|
||||
IterateTwoSwapped(set2, set1, store, query.Without, action);
|
||||
}
|
||||
}
|
||||
|
||||
private static void IterateTwo<TDriver, TOther>(
|
||||
SparseSet<TDriver> driveSet,
|
||||
SparseSet<TOther> otherSet,
|
||||
ComponentStore store,
|
||||
IReadOnlySet<Type> withoutTypes,
|
||||
ForEachAction<TDriver, TOther> action)
|
||||
where TDriver : struct where TOther : struct
|
||||
{
|
||||
var dense = driveSet.Dense;
|
||||
var denseEntities = driveSet.DenseEntities;
|
||||
var count = driveSet.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!otherSet.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, withoutTypes))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref otherSet.Get(entity));
|
||||
}
|
||||
}
|
||||
|
||||
private static void IterateTwoSwapped<TOther, TDriver>(
|
||||
SparseSet<TDriver> driveSet,
|
||||
SparseSet<TOther> otherSet,
|
||||
ComponentStore store,
|
||||
IReadOnlySet<Type> withoutTypes,
|
||||
ForEachAction<TOther, TDriver> action)
|
||||
where TDriver : struct where TOther : struct
|
||||
{
|
||||
var dense = driveSet.Dense;
|
||||
var denseEntities = driveSet.DenseEntities;
|
||||
var count = driveSet.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!otherSet.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, withoutTypes))
|
||||
continue;
|
||||
action(entity, ref otherSet.Get(entity), ref dense[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3>(
|
||||
ComponentStore store,
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1, T2, T3> action)
|
||||
where T1 : struct where T2 : struct where T3 : struct
|
||||
{
|
||||
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set1 == null) return;
|
||||
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||
if (set2 == null) return;
|
||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||
if (set3 == null) return;
|
||||
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4>(
|
||||
ComponentStore store,
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1, T2, T3, T4> action)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct
|
||||
{
|
||||
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set1 == null) return;
|
||||
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||
if (set2 == null) return;
|
||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||
if (set3 == null) return;
|
||||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||
if (set4 == null) return;
|
||||
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4, T5>(
|
||||
ComponentStore store,
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1, T2, T3, T4, T5> action)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
|
||||
{
|
||||
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set1 == null) return;
|
||||
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||
if (set2 == null) return;
|
||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||
if (set3 == null) return;
|
||||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||
if (set4 == null) return;
|
||||
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
||||
if (set5 == null) return;
|
||||
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4, T5, T6>(
|
||||
ComponentStore store,
|
||||
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
|
||||
{
|
||||
var set1 = store.GetSet(typeof(T1)) as SparseSet<T1>;
|
||||
if (set1 == null) return;
|
||||
var set2 = store.GetSet(typeof(T2)) as SparseSet<T2>;
|
||||
if (set2 == null) return;
|
||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||
if (set3 == null) return;
|
||||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||
if (set4 == null) return;
|
||||
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
||||
if (set5 == null) return;
|
||||
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
||||
if (set6 == null) return;
|
||||
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ namespace OECS;
|
|||
internal interface ISparseSet
|
||||
{
|
||||
void Remove(Entity entity);
|
||||
bool Contains(Entity entity);
|
||||
int Count { get; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a group of systems, running them in registration order
|
||||
/// against a specific <see cref="World"/>.
|
||||
/// </summary>
|
||||
public class SystemGroup
|
||||
{
|
||||
private readonly World _world;
|
||||
private readonly List<ISystem> _systems = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a system group bound to the given world.
|
||||
/// </summary>
|
||||
public SystemGroup(World world)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of systems in this group.
|
||||
/// </summary>
|
||||
public int Count => _systems.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system to the group. Systems execute in the order they are added.
|
||||
/// </summary>
|
||||
public void Add(ISystem system)
|
||||
{
|
||||
_systems.Add(system);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a system from the group.
|
||||
/// </summary>
|
||||
public void Remove(ISystem system)
|
||||
{
|
||||
_systems.Remove(system);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all systems with a timed tick.
|
||||
/// Systems that implement <see cref="ITickedSystem"/> receive the tick data;
|
||||
/// plain <see cref="ISystem"/> implementations receive only the world.
|
||||
/// </summary>
|
||||
public void RunTimed(float deltaTime)
|
||||
{
|
||||
RunAll(Tick.Timed(deltaTime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all systems with a logical tick.
|
||||
/// </summary>
|
||||
public void RunLogical()
|
||||
{
|
||||
RunAll(Tick.Logical());
|
||||
}
|
||||
|
||||
private void RunAll(Tick tick)
|
||||
{
|
||||
foreach (var system in _systems)
|
||||
{
|
||||
if (system is ITickedSystem ticked)
|
||||
{
|
||||
ticked.Run(_world, tick);
|
||||
}
|
||||
else
|
||||
{
|
||||
system.Run(_world);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the kind of tick being processed.
|
||||
/// </summary>
|
||||
public enum TickType
|
||||
{
|
||||
/// <summary>
|
||||
/// A timed tick, carrying a delta time (e.g., frame-based update).
|
||||
/// </summary>
|
||||
Timed,
|
||||
|
||||
/// <summary>
|
||||
/// A logical tick, carrying no delta time (e.g., fixed-step simulation).
|
||||
/// </summary>
|
||||
Logical
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data passed to systems during a tick.
|
||||
/// </summary>
|
||||
public readonly struct Tick
|
||||
{
|
||||
/// <summary>
|
||||
/// The kind of tick.
|
||||
/// </summary>
|
||||
public TickType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The elapsed time since the last tick, in seconds.
|
||||
/// Always 0 for logical ticks.
|
||||
/// </summary>
|
||||
public float DeltaTime { get; }
|
||||
|
||||
private Tick(TickType type, float deltaTime)
|
||||
{
|
||||
Type = type;
|
||||
DeltaTime = deltaTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a timed tick with the given delta time.
|
||||
/// </summary>
|
||||
public static Tick Timed(float deltaTime) => new(TickType.Timed, deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a logical tick (no delta time).
|
||||
/// </summary>
|
||||
public static Tick Logical() => new(TickType.Logical, 0f);
|
||||
}
|
||||
|
|
@ -89,6 +89,85 @@ public class World : IDisposable
|
|||
return _components.Has<T>(entity);
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,306 @@
|
|||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace OECS.Tests;
|
||||
|
||||
public class QueryTests
|
||||
{
|
||||
private struct Position { public float X; public float Y; }
|
||||
private struct Velocity { public float X; public float Y; }
|
||||
private struct Health { public int Value; }
|
||||
private struct Frozen { }
|
||||
|
||||
[Fact]
|
||||
public void Query_WithSingleComponent_ReturnsMatchingEntities()
|
||||
{
|
||||
var world = new World();
|
||||
var a = world.CreateEntity();
|
||||
var b = world.CreateEntity();
|
||||
var c = world.CreateEntity();
|
||||
|
||||
world.AddComponent(a, new Position { X = 1, Y = 2 });
|
||||
world.AddComponent(b, new Position { X = 3, Y = 4 });
|
||||
// c has no Position
|
||||
|
||||
var query = world.Query().With<Position>().Build();
|
||||
var results = new List<Entity>();
|
||||
|
||||
world.ForEach(query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
results.Add(entity);
|
||||
});
|
||||
|
||||
results.Should().BeEquivalentTo([a, b]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_WithMultipleComponents_ReturnsIntersection()
|
||||
{
|
||||
var world = new World();
|
||||
var a = world.CreateEntity();
|
||||
var b = world.CreateEntity();
|
||||
var c = world.CreateEntity();
|
||||
|
||||
world.AddComponent(a, new Position { X = 1, Y = 2 });
|
||||
world.AddComponent(a, new Velocity { X = 1, Y = 0 });
|
||||
|
||||
world.AddComponent(b, new Position { X = 3, Y = 4 });
|
||||
// b has no Velocity
|
||||
|
||||
world.AddComponent(c, new Velocity { X = 0, Y = 1 });
|
||||
// c has no Position
|
||||
|
||||
var query = world.Query().With<Position>().With<Velocity>().Build();
|
||||
var results = new List<Entity>();
|
||||
|
||||
world.ForEach(query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
results.Add(entity);
|
||||
});
|
||||
|
||||
results.Should().BeEquivalentTo([a]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_Without_ExcludesEntities()
|
||||
{
|
||||
var world = new World();
|
||||
var a = world.CreateEntity();
|
||||
var b = world.CreateEntity();
|
||||
|
||||
world.AddComponent(a, new Position { X = 1, Y = 2 });
|
||||
world.AddComponent(b, new Position { X = 3, Y = 4 });
|
||||
world.AddComponent(b, new Frozen());
|
||||
|
||||
var query = world.Query().With<Position>().Without<Frozen>().Build();
|
||||
var results = new List<Entity>();
|
||||
|
||||
world.ForEach(query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
results.Add(entity);
|
||||
});
|
||||
|
||||
results.Should().BeEquivalentTo([a]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_EmptyResult_WhenNoEntitiesMatch()
|
||||
{
|
||||
var world = new World();
|
||||
var query = world.Query().With<Position>().Build();
|
||||
var count = 0;
|
||||
|
||||
world.ForEach(query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
count++;
|
||||
});
|
||||
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_RefMutation_IsVisible()
|
||||
{
|
||||
var world = new World();
|
||||
var entity = world.CreateEntity();
|
||||
world.AddComponent(entity, new Position { X = 0, Y = 0 });
|
||||
|
||||
var query = world.Query().With<Position>().Build();
|
||||
|
||||
world.ForEach(query, (Entity e, ref Position pos) =>
|
||||
{
|
||||
pos.X = 42;
|
||||
});
|
||||
|
||||
ref var pos = ref world.GetComponent<Position>(entity);
|
||||
pos.X.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_DestroyedEntities_DoNotAppear()
|
||||
{
|
||||
var world = new World();
|
||||
var a = world.CreateEntity();
|
||||
var b = world.CreateEntity();
|
||||
|
||||
world.AddComponent(a, new Position { X = 1, Y = 2 });
|
||||
world.AddComponent(b, new Position { X = 3, Y = 4 });
|
||||
|
||||
world.DestroyEntity(a);
|
||||
|
||||
var query = world.Query().With<Position>().Build();
|
||||
var results = new List<Entity>();
|
||||
|
||||
world.ForEach(query, (Entity entity, ref Position pos) =>
|
||||
{
|
||||
results.Add(entity);
|
||||
});
|
||||
|
||||
results.Should().BeEquivalentTo([b]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_ThreeComponents_Works()
|
||||
{
|
||||
var world = new World();
|
||||
var entity = world.CreateEntity();
|
||||
|
||||
world.AddComponent(entity, new Position { X = 1, Y = 2 });
|
||||
world.AddComponent(entity, new Velocity { X = 3, Y = 4 });
|
||||
world.AddComponent(entity, new Health { Value = 100 });
|
||||
|
||||
var query = world.Query()
|
||||
.With<Position>()
|
||||
.With<Velocity>()
|
||||
.With<Health>()
|
||||
.Build();
|
||||
|
||||
var count = 0;
|
||||
world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp) =>
|
||||
{
|
||||
count++;
|
||||
pos.X.Should().Be(1);
|
||||
vel.Y.Should().Be(4);
|
||||
hp.Value.Should().Be(100);
|
||||
});
|
||||
|
||||
count.Should().Be(1);
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemTests
|
||||
{
|
||||
private struct Position { public float X; public float Y; }
|
||||
private struct Velocity { public float X; public float Y; }
|
||||
|
||||
private class MovementSystem : ITickedSystem
|
||||
{
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public MovementSystem(World world)
|
||||
{
|
||||
Query = world.Query().With<Position>().With<Velocity>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
||||
public void Run(World world, Tick tick)
|
||||
{
|
||||
float dt = tick.DeltaTime;
|
||||
world.ForEach(Query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X * dt;
|
||||
pos.Y += vel.Y * dt;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System_RunTimed_UpdatesComponents()
|
||||
{
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
var system = new MovementSystem(world);
|
||||
group.Add(system);
|
||||
|
||||
var entity = world.CreateEntity();
|
||||
world.AddComponent(entity, new Position { X = 0, Y = 0 });
|
||||
world.AddComponent(entity, new Velocity { X = 1, Y = 2 });
|
||||
|
||||
group.RunTimed(0.5f);
|
||||
|
||||
ref var pos = ref world.GetComponent<Position>(entity);
|
||||
pos.X.Should().Be(0.5f);
|
||||
pos.Y.Should().Be(1.0f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System_RunLogical_DoesNotUseDeltaTime()
|
||||
{
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
|
||||
var system = new TestLogicalSystem(world);
|
||||
group.Add(system);
|
||||
|
||||
group.RunLogical();
|
||||
|
||||
system.WasCalled.Should().BeTrue();
|
||||
system.ReceivedTick.Type.Should().Be(TickType.Logical);
|
||||
system.ReceivedTick.DeltaTime.Should().Be(0);
|
||||
}
|
||||
|
||||
private class TestLogicalSystem : ITickedSystem
|
||||
{
|
||||
public QueryDescriptor Query { get; }
|
||||
public bool WasCalled { get; private set; }
|
||||
public Tick ReceivedTick { get; private set; }
|
||||
|
||||
public TestLogicalSystem(World world)
|
||||
{
|
||||
Query = world.Query().With<Position>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
||||
public void Run(World world, Tick tick)
|
||||
{
|
||||
WasCalled = true;
|
||||
ReceivedTick = tick;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Systems_RunInRegistrationOrder()
|
||||
{
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
var order = new List<int>();
|
||||
|
||||
group.Add(new OrderTrackingSystem(world, 1, order));
|
||||
group.Add(new OrderTrackingSystem(world, 2, order));
|
||||
group.Add(new OrderTrackingSystem(world, 3, order));
|
||||
|
||||
group.RunLogical();
|
||||
|
||||
order.Should().BeInAscendingOrder();
|
||||
order.Should().BeEquivalentTo([1, 2, 3]);
|
||||
}
|
||||
|
||||
private class OrderTrackingSystem : ISystem
|
||||
{
|
||||
private readonly int _id;
|
||||
private readonly List<int> _order;
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public OrderTrackingSystem(World world, int id, List<int> order)
|
||||
{
|
||||
_id = id;
|
||||
_order = order;
|
||||
Query = world.Query().With<Position>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
{
|
||||
_order.Add(_id);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System_CanBeRemoved()
|
||||
{
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
var system = new TestLogicalSystem(world);
|
||||
|
||||
group.Add(system);
|
||||
group.Count.Should().Be(1);
|
||||
|
||||
group.Remove(system);
|
||||
group.Count.Should().Be(0);
|
||||
|
||||
group.RunLogical();
|
||||
system.WasCalled.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue