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
|
internal interface ISparseSet
|
||||||
{
|
{
|
||||||
void Remove(Entity entity);
|
void Remove(Entity entity);
|
||||||
|
bool Contains(Entity entity);
|
||||||
int Count { get; }
|
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);
|
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 ───────────────────────────────────────────────
|
// ── Internal Access ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <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