refactor: remove Source property from IRelationship

Remove the redundant `Source` property from `IRelationship` and its
implementations. Since the relationship component is inherently
stored on the source entity, the `Source` field was unnecessary and
required manual synchronization during deserialization.

This change simplifies the relationship model, reduces memory usage,
and removes the need for reflection-based fixups in the serializer.
This commit is contained in:
2026-07-20 22:59:27 +08:00
parent 2de735498c
commit 91c6f4aa2c
21 changed files with 39 additions and 98 deletions
+2 -7
View File
@@ -2,19 +2,14 @@ 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.
/// between two entities. The component is stored on the source entity
/// (the entity it's added to) 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>
+1 -8
View File
@@ -14,7 +14,6 @@ namespace OECS;
/// // Define a "ChildOf" relationship between a child and a parent entity.
/// world.AddComponent(child, new Relationship&lt;ChildOf, Parent&gt;
/// {
/// Source = child,
/// Target = parent
/// });
///
@@ -27,15 +26,9 @@ namespace OECS;
public struct Relationship<TSelf, TTarget> : IRelationship
where TSelf : struct where TTarget : struct
{
/// <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)]
[Key(0)]
public Entity Target { get; set; }
}
+1 -12
View File
@@ -203,19 +203,8 @@ public class World : IDisposable
ThrowIfNotAlive(entity);
// If replacing an existing relationship, remove the old index entry first.
if (component is IRelationship newRel)
if (component is IRelationship)
{
// Guard against mismatched Source: the relationship must be stored
// on the entity it claims as its Source, otherwise the reverse
// index becomes corrupted.
if (newRel.Source != entity)
{
throw new InvalidOperationException(
$"Relationship of type {typeof(T).Name} has Source={newRel.Source} " +
$"but is being added to entity {entity}. " +
$"The Source must match the entity the component is added to.");
}
if (_components.TryGet<T>(entity, out var old))
{
var oldRel = (IRelationship)(object)old;
+1 -24
View File
@@ -1,4 +1,3 @@
using System.Reflection;
using MessagePack;
namespace OECS;
@@ -94,32 +93,10 @@ public static class WorldSerializer
$"Ensure the component type is used with the World " +
$"API so the source generator can discover it.");
// Deserialize and fix up IRelationship.Source before adding.
// Deserialize and add via the typed internal method.
var component = desc.Deserialize(ce.Data);
if (component is IRelationship rel)
FixupRelationshipSource(rel, entity);
// Add via the typed internal method — no reflection for the add itself.
world.AddComponentBoxed(entity, component, desc.Type);
}
}
}
/// <summary>
/// Sets the Source field on a deserialized IRelationship to match
/// the entity it's being restored to. Uses a small cache of PropertyInfo
/// to avoid repeated reflection lookups.
/// </summary>
private static void FixupRelationshipSource(IRelationship rel, Entity entity)
{
var type = rel.GetType();
if (!_sourcePropCache.TryGetValue(type, out var prop))
{
prop = type.GetProperty("Source");
_sourcePropCache[type] = prop;
}
prop?.SetValue(rel, entity);
}
private static readonly Dictionary<Type, PropertyInfo?> _sourcePropCache = new();
}