feat: add relationship components and reverse indexing
Introduces a relationship system to allow entities to point to other entities. Includes a `Relationship<TSelf, TTarget>` component and a `RelationshipIndex` to enable efficient reverse lookups (finding all sources pointing to a specific target). The `World.DestroyEntity` method now performs cascading removals: - Relationships where the entity is the source are removed. - Relationships where the entity is the target are removed from their respective source entities. Also adds MessagePack support to `Entity` and `Relationship`.
This commit is contained in:
parent
c5578d4412
commit
48f3319615
|
|
@ -52,6 +52,16 @@ internal class ComponentStore
|
||||||
GetSetIfExists<T>()?.Remove(entity);
|
GetSetIfExists<T>()?.Remove(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the component of the given type from the entity.
|
||||||
|
/// No-op if the entity does not have the component or the type is not registered.
|
||||||
|
/// Used for cascading relationship removal during entity destruction.
|
||||||
|
/// </summary>
|
||||||
|
public void Remove(Entity entity, Type componentType)
|
||||||
|
{
|
||||||
|
GetSet(componentType)?.Remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
public ref T Get<T>(Entity entity) where T : struct
|
public ref T Get<T>(Entity entity) where T : struct
|
||||||
{
|
{
|
||||||
return ref GetSet<T>().Get(entity);
|
return ref GetSet<T>().Get(entity);
|
||||||
|
|
@ -62,6 +72,22 @@ internal class ComponentStore
|
||||||
return GetSetIfExists<T>()?.Contains(entity) ?? false;
|
return GetSetIfExists<T>()?.Contains(entity) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get the component value for the given entity.
|
||||||
|
/// Returns true if the component exists, with the value copied to <paramref name="value"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGet<T>(Entity entity, out T value) where T : struct
|
||||||
|
{
|
||||||
|
var set = GetSetIfExists<T>();
|
||||||
|
if (set != null && set.Contains(entity))
|
||||||
|
{
|
||||||
|
value = set.Get(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all components for the given entity across all component types.
|
/// Removes all components for the given entity across all component types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
namespace OECS;
|
namespace OECS;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -5,7 +7,8 @@ namespace OECS;
|
||||||
/// Combines a 24-bit identifier with an 8-bit version to prevent
|
/// Combines a 24-bit identifier with an 8-bit version to prevent
|
||||||
/// use-after-free bugs when entity IDs are recycled.
|
/// use-after-free bugs when entity IDs are recycled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct Entity : IEquatable<Entity>
|
[MessagePackObject(AllowPrivate = true)]
|
||||||
|
public readonly partial struct Entity : IEquatable<Entity>
|
||||||
{
|
{
|
||||||
private const uint IdMask = 0x00FF_FFFF; // lower 24 bits
|
private const uint IdMask = 0x00FF_FFFF; // lower 24 bits
|
||||||
private const uint VersionMask = 0xFF00_0000; // upper 8 bits
|
private const uint VersionMask = 0xFF00_0000; // upper 8 bits
|
||||||
|
|
@ -18,6 +21,7 @@ public readonly struct Entity : IEquatable<Entity>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Entity Null = new(0);
|
public static readonly Entity Null = new(0);
|
||||||
|
|
||||||
|
[Key(0)]
|
||||||
private readonly uint _value;
|
private readonly uint _value;
|
||||||
|
|
||||||
internal Entity(uint id, uint version)
|
internal Entity(uint id, uint version)
|
||||||
|
|
@ -33,16 +37,19 @@ public readonly struct Entity : IEquatable<Entity>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The 24-bit entity identifier.
|
/// The 24-bit entity identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[IgnoreMember]
|
||||||
public uint Id => _value & IdMask;
|
public uint Id => _value & IdMask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The 8-bit generation version. Incremented each time the ID is recycled.
|
/// The 8-bit generation version. Incremented each time the ID is recycled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[IgnoreMember]
|
||||||
public uint Version => (_value & VersionMask) >> VersionShift;
|
public uint Version => (_value & VersionMask) >> VersionShift;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if this entity is the null sentinel.
|
/// Returns true if this entity is the null sentinel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[IgnoreMember]
|
||||||
public bool IsNull => _value == 0;
|
public bool IsNull => _value == 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace OECS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marker interface for components that represent a directed relationship
|
||||||
|
/// between two entities. The component is stored on the <see cref="Source"/>
|
||||||
|
/// entity and points to the <see cref="Target"/> entity.
|
||||||
|
///
|
||||||
|
/// The <see cref="World"/> automatically maintains a reverse index so that
|
||||||
|
/// all sources pointing to a given target can be looked up efficiently.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRelationship
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that owns this relationship component.
|
||||||
|
/// </summary>
|
||||||
|
Entity Source { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that this relationship points to.
|
||||||
|
/// </summary>
|
||||||
|
Entity Target { get; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace OECS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A convenience base struct for relationship components.
|
||||||
|
///
|
||||||
|
/// The type parameters <typeparamref name="TSelf"/> and <typeparamref name="TTarget"/>
|
||||||
|
/// are phantom types that differentiate relationship kinds at the type level,
|
||||||
|
/// enabling type-safe queries and reverse lookups.
|
||||||
|
///
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // Define a "ChildOf" relationship between a child and a parent entity.
|
||||||
|
/// world.AddComponent(child, new Relationship<ChildOf, Parent>
|
||||||
|
/// {
|
||||||
|
/// Source = child,
|
||||||
|
/// Target = parent
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// // Later, find all children of a parent.
|
||||||
|
/// var children = world.GetSources<Relationship<ChildOf, Parent>>(parent);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public struct Relationship<TSelf, TTarget> : IRelationship
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that owns this relationship component.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public Entity Source { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that this relationship points to.
|
||||||
|
/// </summary>
|
||||||
|
[Key(1)]
|
||||||
|
public Entity Target { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace OECS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maintains a reverse index for relationship components, mapping
|
||||||
|
/// target entities to the set of source entities that point to them.
|
||||||
|
///
|
||||||
|
/// Updated automatically by <see cref="World"/> when relationship
|
||||||
|
/// components are added, removed, or when entities are destroyed.
|
||||||
|
/// </summary>
|
||||||
|
internal class RelationshipIndex
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per relationship type: target entity → set of source entities.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<Type, Dictionary<Entity, HashSet<Entity>>> _index = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers that a source entity now has a relationship pointing to a target.
|
||||||
|
/// </summary>
|
||||||
|
public void OnAdded(Type relationshipType, Entity source, Entity target)
|
||||||
|
{
|
||||||
|
if (!_index.TryGetValue(relationshipType, out var targetMap))
|
||||||
|
{
|
||||||
|
targetMap = new Dictionary<Entity, HashSet<Entity>>();
|
||||||
|
_index[relationshipType] = targetMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetMap.TryGetValue(target, out var sources))
|
||||||
|
{
|
||||||
|
sources = new HashSet<Entity>();
|
||||||
|
targetMap[target] = sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.Add(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregisters a relationship from a source entity pointing to a target.
|
||||||
|
/// </summary>
|
||||||
|
public void OnRemoved(Type relationshipType, Entity source, Entity target)
|
||||||
|
{
|
||||||
|
if (!_index.TryGetValue(relationshipType, out var targetMap))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!targetMap.TryGetValue(target, out var sources))
|
||||||
|
return;
|
||||||
|
|
||||||
|
sources.Remove(source);
|
||||||
|
|
||||||
|
if (sources.Count == 0)
|
||||||
|
targetMap.Remove(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all source entities that have a relationship of the given type
|
||||||
|
/// pointing to the specified target entity.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
||||||
|
where T : struct, IRelationship
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
if (!_index.TryGetValue(type, out var targetMap))
|
||||||
|
return Array.Empty<Entity>();
|
||||||
|
|
||||||
|
if (!targetMap.TryGetValue(target, out var sources))
|
||||||
|
return Array.Empty<Entity>();
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all target entities that the given source entity points to
|
||||||
|
/// via the specified relationship type. Returns an empty collection if none.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<Entity> GetTargets(Type relationshipType, Entity source)
|
||||||
|
{
|
||||||
|
if (!_index.TryGetValue(relationshipType, out var targetMap))
|
||||||
|
return Array.Empty<Entity>();
|
||||||
|
|
||||||
|
var result = new List<Entity>();
|
||||||
|
foreach (var (target, sources) in targetMap)
|
||||||
|
{
|
||||||
|
if (sources.Contains(source))
|
||||||
|
result.Add(target);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all index entries where the given entity appears as a source.
|
||||||
|
/// Called before the entity's components are removed during destruction.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveAllSourcesForEntity(Entity entity)
|
||||||
|
{
|
||||||
|
foreach (var (_, targetMap) in _index)
|
||||||
|
{
|
||||||
|
foreach (var (target, sources) in targetMap)
|
||||||
|
{
|
||||||
|
sources.Remove(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up empty target entries.
|
||||||
|
foreach (var (_, targetMap) in _index)
|
||||||
|
{
|
||||||
|
var emptyTargets = new List<Entity>();
|
||||||
|
foreach (var (target, sources) in targetMap)
|
||||||
|
{
|
||||||
|
if (sources.Count == 0)
|
||||||
|
emptyTargets.Add(target);
|
||||||
|
}
|
||||||
|
foreach (var target in emptyTargets)
|
||||||
|
{
|
||||||
|
targetMap.Remove(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all relationship types for which the given entity is a target,
|
||||||
|
/// along with the set of source entities pointing to it.
|
||||||
|
/// Used during entity destruction to cascade-remove incoming relationships.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<(Type RelationshipType, IReadOnlyCollection<Entity> Sources)> GetIncomingRelationships(Entity target)
|
||||||
|
{
|
||||||
|
foreach (var (relType, targetMap) in _index)
|
||||||
|
{
|
||||||
|
if (targetMap.TryGetValue(target, out var sources) && sources.Count > 0)
|
||||||
|
{
|
||||||
|
yield return (relType, sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the index has any entries for the given relationship type.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasType(Type relationshipType)
|
||||||
|
{
|
||||||
|
return _index.ContainsKey(relationshipType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,14 @@ public class World : IDisposable
|
||||||
private readonly EntityAllocator _allocator;
|
private readonly EntityAllocator _allocator;
|
||||||
private readonly ComponentStore _components;
|
private readonly ComponentStore _components;
|
||||||
private readonly CommandQueue _commands;
|
private readonly CommandQueue _commands;
|
||||||
|
private readonly RelationshipIndex _relationships;
|
||||||
|
|
||||||
public World()
|
public World()
|
||||||
{
|
{
|
||||||
_allocator = new EntityAllocator();
|
_allocator = new EntityAllocator();
|
||||||
_components = new ComponentStore();
|
_components = new ComponentStore();
|
||||||
_commands = new CommandQueue();
|
_commands = new CommandQueue();
|
||||||
|
_relationships = new RelationshipIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entity Management ────────────────────────────────────────────
|
// ── Entity Management ────────────────────────────────────────────
|
||||||
|
|
@ -31,6 +33,11 @@ public class World : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Destroys an entity, removing all its components and recycling its ID.
|
/// 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>
|
/// </summary>
|
||||||
public void DestroyEntity(Entity entity)
|
public void DestroyEntity(Entity entity)
|
||||||
{
|
{
|
||||||
|
|
@ -41,6 +48,19 @@ public class World : IDisposable
|
||||||
if (entity.Id == 1)
|
if (entity.Id == 1)
|
||||||
return;
|
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);
|
_components.RemoveAll(entity);
|
||||||
_allocator.Free(entity);
|
_allocator.Free(entity);
|
||||||
}
|
}
|
||||||
|
|
@ -58,19 +78,52 @@ public class World : IDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a component to the entity. Replaces the existing component if
|
/// Adds a component to the entity. Replaces the existing component if
|
||||||
/// the entity already has one of type <typeparamref name="T"/>.
|
/// 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>
|
/// </summary>
|
||||||
public void AddComponent<T>(Entity entity, T component) where T : struct
|
public void AddComponent<T>(Entity entity, T component) where T : struct
|
||||||
{
|
{
|
||||||
ThrowIfNotAlive(entity);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_components.Add(entity, component);
|
_components.Add(entity, component);
|
||||||
|
|
||||||
|
// Update relationship index.
|
||||||
|
if (component is IRelationship rel)
|
||||||
|
{
|
||||||
|
_relationships.OnAdded(typeof(T), entity, rel.Target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the component of type <typeparamref name="T"/> from the entity.
|
/// Removes the component of type <typeparamref name="T"/> from the entity.
|
||||||
/// No-op if the entity does not have the component.
|
/// 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>
|
/// </summary>
|
||||||
public void RemoveComponent<T>(Entity entity) where T : struct
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_components.Remove<T>(entity);
|
_components.Remove<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +144,18 @@ public class World : IDisposable
|
||||||
return _components.Has<T>(entity);
|
return _components.Has<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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 ──────────────────────────────────────────────────────
|
// ── Commands ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OECS.Tests;
|
||||||
|
|
||||||
|
// ── Phantom types for relationship differentiation ───────────────────
|
||||||
|
|
||||||
|
public struct ChildOf { }
|
||||||
|
public struct ParentOf { }
|
||||||
|
public struct OwnedBy { }
|
||||||
|
public struct MemberOf { }
|
||||||
|
|
||||||
|
// ── Tests ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class RelationshipTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AddRelationship_UpdatesReverseIndex()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
|
||||||
|
sources.Should().BeEquivalentTo([child]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveRelationship_UpdatesReverseIndex()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
world.RemoveComponent<Relationship<ChildOf, ParentOf>>(child);
|
||||||
|
|
||||||
|
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
|
||||||
|
sources.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReplaceRelationship_UpdatesReverseIndex()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent1 = world.CreateEntity();
|
||||||
|
var parent2 = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace with a new target.
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent2
|
||||||
|
});
|
||||||
|
|
||||||
|
world.GetSources<Relationship<ChildOf, ParentOf>>(parent1).Should().BeEmpty();
|
||||||
|
world.GetSources<Relationship<ChildOf, ParentOf>>(parent2).Should().BeEquivalentTo([child]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestroySourceEntity_CleansUpRelationships()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
world.DestroyEntity(child);
|
||||||
|
|
||||||
|
// The relationship should be gone from the reverse index.
|
||||||
|
world.GetSources<Relationship<ChildOf, ParentOf>>(parent).Should().BeEmpty();
|
||||||
|
|
||||||
|
// The destroyed entity should not be alive.
|
||||||
|
world.IsAlive(child).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestroyTargetEntity_CleansUpIncomingRelationships()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child,
|
||||||
|
Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
world.DestroyEntity(parent);
|
||||||
|
|
||||||
|
// The relationship component should be removed from the source entity.
|
||||||
|
world.HasComponent<Relationship<ChildOf, ParentOf>>(child).Should().BeFalse();
|
||||||
|
|
||||||
|
// The reverse index should be empty for the destroyed target.
|
||||||
|
world.GetSources<Relationship<ChildOf, ParentOf>>(parent).Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleSources_ForSameTarget()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child1 = world.CreateEntity();
|
||||||
|
var child2 = world.CreateEntity();
|
||||||
|
var child3 = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child1, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child1, Target = parent
|
||||||
|
});
|
||||||
|
world.AddComponent(child2, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child2, Target = parent
|
||||||
|
});
|
||||||
|
world.AddComponent(child3, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child3, Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
|
||||||
|
sources.Should().BeEquivalentTo([child1, child2, child3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DifferentRelationshipTypes_AreIndependent()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var entity = world.CreateEntity();
|
||||||
|
var owner = world.CreateEntity();
|
||||||
|
var group = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(entity, new Relationship<OwnedBy, object>
|
||||||
|
{
|
||||||
|
Source = entity, Target = owner
|
||||||
|
});
|
||||||
|
world.AddComponent(entity, new Relationship<MemberOf, object>
|
||||||
|
{
|
||||||
|
Source = entity, Target = group
|
||||||
|
});
|
||||||
|
|
||||||
|
world.GetSources<Relationship<OwnedBy, object>>(owner).Should().BeEquivalentTo([entity]);
|
||||||
|
world.GetSources<Relationship<MemberOf, object>>(group).Should().BeEquivalentTo([entity]);
|
||||||
|
|
||||||
|
// Cross-check: OwnedBy sources should not appear in MemberOf lookups.
|
||||||
|
world.GetSources<Relationship<MemberOf, object>>(owner).Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReverseLookup_ReturnsEmpty_WhenNoRelationships()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var entity = world.CreateEntity();
|
||||||
|
|
||||||
|
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(entity);
|
||||||
|
sources.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Relationship_CanBeQueried_LikeNormalComponent()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var child = world.CreateEntity();
|
||||||
|
var parent = world.CreateEntity();
|
||||||
|
|
||||||
|
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = child, Target = parent
|
||||||
|
});
|
||||||
|
|
||||||
|
var query = world.Query().With<Relationship<ChildOf, ParentOf>>().Build();
|
||||||
|
var results = new List<Entity>();
|
||||||
|
|
||||||
|
world.ForEach(query, (Entity e, ref Relationship<ChildOf, ParentOf> rel) =>
|
||||||
|
{
|
||||||
|
results.Add(e);
|
||||||
|
rel.Target.Should().Be(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.Should().BeEquivalentTo([child]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestroyEntity_WithBothIncomingAndOutgoingRelationships()
|
||||||
|
{
|
||||||
|
var world = new World();
|
||||||
|
var a = world.CreateEntity();
|
||||||
|
var b = world.CreateEntity();
|
||||||
|
var c = world.CreateEntity();
|
||||||
|
|
||||||
|
// a → b (a is child of b)
|
||||||
|
world.AddComponent(a, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = a, Target = b
|
||||||
|
});
|
||||||
|
|
||||||
|
// c → a (c is child of a)
|
||||||
|
world.AddComponent(c, new Relationship<ChildOf, ParentOf>
|
||||||
|
{
|
||||||
|
Source = c, Target = a
|
||||||
|
});
|
||||||
|
|
||||||
|
// Destroy a. This should:
|
||||||
|
// 1. Remove the a→b relationship (a is source).
|
||||||
|
// 2. Remove the c→a relationship (a is target, so c's component is removed).
|
||||||
|
world.DestroyEntity(a);
|
||||||
|
|
||||||
|
world.IsAlive(a).Should().BeFalse();
|
||||||
|
world.HasComponent<Relationship<ChildOf, ParentOf>>(c).Should().BeFalse();
|
||||||
|
world.GetSources<Relationship<ChildOf, ParentOf>>(b).Should().BeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue