Let’s talk about Relationships in Entitas-CSharp
First I would like to set some ground terminology. There are one to one, one to many and many to many relationships. A relationship can be unidirectional or bidirectional.
For simplicity I would like to stick with unidirectional one to one relationship.
Now, we want to have a relationship between two entities.
Entity as define in Entitas-CSharp is an array of components. If I want to reflect that Entity A is a friend of Entity B I have to create a FriendComponent. What should this FriendComponent contain?
Here we have 2 options.
1. Direct reference to an Entity object:
class FriendComponent : IComponent {Entity reference;}
2. Reference an entity by unique id:
class FriendComponent : IComponent {string name;}
The option number one is more obvious and resonates with OOP thinking.
The relationship would be established as following:
var maxim = pool.CreateEntity().AddName(“Maxim”);
var simon = pool.CreateEntity().AddName(“Simon”);
maxim.AddFriend(simon);
simon.Retain(maxim);
This looks very nice however what would happen if we destroy entity simon?
Entity maxim still have a friend, but it is a zombie.
Btw. simon.Retain(maxim) is very important. It tells the pool that simon is referenced by maxim so even if simon get destroyed the pool will not reuese the entity object which rpresented simon until simon.Release(maxim) will be called.
So if we want to call RemoveFriend, we have to call Release first:
maxim.friend.value.Release(maxim);
maxim.RemoveFriend();
As we can see, referencing entities directly has its caviets because of entity reference counting.
However there is even tougher problem, when we start thinking about serialisation.
Imagine we want to serialise entity maxim as part of player state.
A direct reference to another object is defined by its current runtime pointer.
This alone is not enough information to persist the relationship.
So lets have a look at option number two:
var maxim = pool.CreateEntity().AddName(“Maxim”);
var simon = pool.CreateEntity().AddName(“Simon”);
maxim.AddFriend(simon.name.value);
Now we use name value as a relationship id (foreign key if we want to follow the terminology of relational databases)
If we destroy entity simon, maxim still has a friend whos name is “Simon”. The question is if we can find an entity which has name “Simon”.
And btw. how do we find an entity with name “Simon”?
Naively we could get a group of all entities which have name component, and than loop through it until we find a name value equal to “Simon”. This implies that we should not have two entities with name “Simon”. This is also why it is called unique id, using a name in context of unique ids is actually not that smart, but I do it here to keep the example simple.
If we want to do it more efficient we would have to implement a NameIndex:
This seems to be a lot of code, but it encapsulates the whole management of relationships nicely for us.
It has the retain release cycle management. And it checks if the id we use is unique.
So with this strategy, it would be easy to serialise maxim, because the FriendComponent is just a string value. So we cover the serialisation problem.
If simon gets destroyed we can clean up the friend component of maxim lazyly next time we dereference “Simon” through the name index and find it being null. And this helps us with the zombie problem.
However to be fair, this strategy implies a little bit of foot work.
This is why, I would say it is up to developer to decide which path s/he wants to go.