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);
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
return ref GetSet<T>().Get(entity);
|
||||
|
|
@ -62,6 +72,22 @@ internal class ComponentStore
|
|||
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>
|
||||
/// Removes all components for the given entity across all component types.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -5,7 +7,8 @@ namespace OECS;
|
|||
/// Combines a 24-bit identifier with an 8-bit version to prevent
|
||||
/// use-after-free bugs when entity IDs are recycled.
|
||||
/// </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 VersionMask = 0xFF00_0000; // upper 8 bits
|
||||
|
|
@ -18,6 +21,7 @@ public readonly struct Entity : IEquatable<Entity>
|
|||
/// </summary>
|
||||
public static readonly Entity Null = new(0);
|
||||
|
||||
[Key(0)]
|
||||
private readonly uint _value;
|
||||
|
||||
internal Entity(uint id, uint version)
|
||||
|
|
@ -33,16 +37,19 @@ public readonly struct Entity : IEquatable<Entity>
|
|||
/// <summary>
|
||||
/// The 24-bit entity identifier.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
public uint Id => _value & IdMask;
|
||||
|
||||
/// <summary>
|
||||
/// The 8-bit generation version. Incremented each time the ID is recycled.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
public uint Version => (_value & VersionMask) >> VersionShift;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity is the null sentinel.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
public bool IsNull => _value == 0;
|
||||
|
||||
/// <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 ComponentStore _components;
|
||||
private readonly CommandQueue _commands;
|
||||
private readonly RelationshipIndex _relationships;
|
||||
|
||||
public World()
|
||||
{
|
||||
_allocator = new EntityAllocator();
|
||||
_components = new ComponentStore();
|
||||
_commands = new CommandQueue();
|
||||
_relationships = new RelationshipIndex();
|
||||
}
|
||||
|
||||
// ── Entity Management ────────────────────────────────────────────
|
||||
|
|
@ -31,6 +33,11 @@ public class World : IDisposable
|
|||
|
||||
/// <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)
|
||||
{
|
||||
|
|
@ -41,6 +48,19 @@ public class World : IDisposable
|
|||
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);
|
||||
}
|
||||
|
|
@ -58,19 +78,52 @@ public class World : IDisposable
|
|||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
_components.Add(entity, component);
|
||||
|
||||
// Update relationship index.
|
||||
if (component is IRelationship rel)
|
||||
{
|
||||
_relationships.OnAdded(typeof(T), entity, rel.Target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
_components.Remove<T>(entity);
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +144,18 @@ public class World : IDisposable
|
|||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
/// <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