sebas77/Svelto.ECS

EGID usage

Closed this issue · 59 comments

I have a case about entity relations using EGID.

  1. I have a player entity that has many troop entities.
public interface IPlayerTroopComponent
{
         IList <EGID> Troops {get; } // EGID is the troop entity id
}
  1. Some of my Engine will change the group of troop entities.

The problem is: Since EGID is a struct, IPlayerTroopComponent will contains inconsistent groupIds.

What do you think about this?

I firstly think that you shouldn't hold list of entities IDs. Most of the times when you need entities ID you actually need a dedicated group. Can this be the case too?

BTW the EGID design is in its early stages, so I may change it. I understand that having a changing ID can be confusing. When I thought about the EGID, I was always thinking it as a mix of groupID and entityID, so it's natural that it changes. Let me know if you think this can be limiting in some cases.

Edit: I thought more about it. I am not sure if having a dynamic ID for entities is bad, I want to think even more about it, but I believe that it's not good for coders writing their own data structures to handle entities. I need to have the time to think if this is feasible somehow. The concept of meta entity was actually to give this kind of flexibility, in the sense that you could create meta entities to regroup other entities, but it was feeling a bit of a hack. Food for thought.

I've been using groups to group troops by teams.

This is the complete case:
I have Player troops and Non-Player Troops.

  • Player troops are owned by players.
  • Non-player troops are owned by teams.

Both are team troops. Then,

  • I need to iterate all the troops by certain player. [result: Player Troops]
  • I need to iterate all the troops by certain team. [result : Player Troops and Non-Player Troops]

I can't use group because each entity limited in 1 group.

ok I suspected this was the case, I need to think a bit about it. If you are stuck you can revert to the previous version of ECS, if you have ideas, let me know ;)

My project is still in development, back to old version is not good idea i think
:)

What do you think about entity not limited in 1 group?

I think the example is still not very clear to me.

there can be players and not players (I guess AI?) troops. Troops can be split in to teams right? You have engines that iterate over teams regardless if they are player or AI troops. You have also engines that iterate only over the AI troops and only over the player troops. Is all of this correct? If this is correct, what are your current svelto groups? A component that hold a list of entities sounds quite wrong anyway, I need to understand why you needed it. I guess it's like all the TroopEntities owned by the PlayerEntity?

can you show me what you do inside the engines? I wonder if the concept of meta entity can help in this case

it's theoretically possible to put an entity on several groups, but the awkward part will be the remove entity though.

OK this is my take. To solve your problem I would use two different entitiydescriptor like:

PlayerTroopEntityDescriptor
NonPlayerTroopEntityDescriptor

they are gouped by team. but I can query two different entityview...how does it sound? I guess the problem here is that you don't want your engine to know if it's an NonPlayerTroopEntityView or a PlayerTroop. In this case you can still do:

PlayerTroopEntityDescriptor<PlayerTroopEntityView, TroopEntityView>
NonPlayerTroopEntityDescriptor<NonPlayerTroopEntityView, TroopEntityView>

carefull that this stuff works as long as we are talking about EntityViewClass, if you were using EntitStructs it would have been trickier.

We can go on, I use EntityViewClass.

This is multiplayer game, so player and enemy terms just work in client-side, right?
But not in server-side.

Then in server, should I make static player count? like Player1TroopEntityView, Player2TroopEntityView, Player3EntityView?

no if you have more than a player is quite bad

another idea I was pondering is to return an unique ID from the BuildEntity function. I can make this ID never change, but I didn't like the idea, maybe it's the only solution?

I still don't like the idea to let coders have their own data structures, but I can't find another solution at the moment

so let me understand you can have N players and each player has M troops and you want to query all the combinations?

You want to:

have an engine that iterates the units for each player
have an engine that iterates the units for each troop for each player
have an engine that iterates the units for all the troops regardless the player

I will think about the idea to let the BuildEntity returns an unique ID, remove the BuildEntityInGroup and add a function PutEntityInGroup, let an entity be in multiple groups. I need to check the pros and cons.

Since you said "It's not good for coders writing their own data structures to handle entities", the best to do it which i'm thinking about is using multiple group.

But, can you explain more about "The awkward part of using multiple group will be the remove"?

It is quite hard for me to explain in english, so let me explain by code :)
Here what I want, I believe you can get it.

     entityFactory.BuildInGroup<PlayerTroopDescriptror<TroopEntityView>>(id, components, playerID, teamID);
     entityFactory.BuildInGroup<NonPlayerTroopDescriptror<TroopEntityView>>(id, components, teamID);

     var troops= entityDB.QueryGroupedEntityViews<TroopEntityView>(playerID); // return player troops
     var troops= entityDB.QueryGroupedEntityViews<TroopEntityView>(teamID); // return player and non-player troops

Can you?
:)

I understand you want to query the same entity from different groups. I need to think about it, all the consequences and how it works with Entity structs (entity structs cannot be on multiple groups)

BTW what you are doing can be done already, you are effectively building two entities, but sharing the same implementors.

It should work already :)

my thinking is about doing the same with the same entity, but TBH why not doing what you have done, I think is OK!

Actually I think it's a good solution, if this is already working for you, I won't change anything.

P.S.: if it works I need to add it in the tips and tricks :)

Let me be clearer what I meant is that you can build two different entities in two different groups using the same entity descriptor and implementors. Will this be good enough? It means that you can effectively query the same entity data from different groups

If I build two different entities in two different group, but what about destroying entity?

How to identify which entities should be destroyed when destroying entity.

For example, I have an entity, X entity, so I should build 2 entities with id 100 and 200.

Then when i want to destory the X entity, how to identify which entities should be destoryed?
Should I create new component or new data stucture to hold that 2 entity ids (100 and 200)?

At the end, I will break "It's not good for coders writing their own data structures to handle entities" principle.

Sorry, i missed that point.

give me time to try, and I'll back.
:)

Sorry, I need more to upgrade my project and ensure everthing well.

Some sort problems :(

  1. You removed QueryIndexableEntityViews<T>(groupId);
  2. I need create EGID when QueryEntityView<T>(EGID);

The first function was redundant you can query the entity directly without
passing through the dictionary right?

Yes, not big problem, but make me change how I write my code :)

About the second, I think we have painful part when working EGID.

Since we need create EGID when querying entity, QueryEntityView<T>(EGID), we need to pass groupId to every engines even though we have unique entityId.
This is happen especially when working with Networking.

The case of networking:
I have 2 projects, client and server.
Client need to upgrade level specific troop, and send upgrade command to server with parameter TroopID.
Then in server, we need to identify the groupId of that TroopID. It is hard part, right?

What do you think?

Sorry not metion it. Yes, It is different.

But, I just found the solution. Because of all my entities have unique Id, I just need build the entity in ExclusiveGroups.StandardEntity group.
It will make me just need to access using ExclusiveGroups.StandardEntity.

Is it ok?

Yeah :)

But will it cause performance issue?

When I build 3 entity using same entityID, will it build 3 EntityViews? Will it call reflection 3 times?

Nice one about cached reflection.

About our solution above, "Building multiple entity using same EntityId".
I just checked it. When we build 3 entity using same entityId, it will build 3 EntityViews.
It will cause problem when engine using EntityDB.QueryEntityViews<T>(). It will return 3 EntityView with shared components.

let me know if the solution worked out for you in the end or you found it too awkward ;)

Yes, I think little awkward. :)

I changed the internal implementation of EntityDB.QueryEntityViews<T>() be querying from ExclusiveGroups.StandardEntity.
Then All entities should be built in ExclusiveGroups.StandardEntity group.
:D

Btw, I just see coming update announcement. Are there updates related to this?

No they are not. I want to see the code, can you share it? Also can you highlight where you find it awkward? The change you made seems very bad, I don't understand why you had to do it. You can query an entity view from any group using the same function, it's the EGID to define the group now.

QueryEntityViews -> Queries from the standard group
QueryGroupedEntityViews -> Queries from whatever group
TryQueryEntityView -> gets the specific entity from whatever group using the EGID

About our solution above, "Building multiple entity using same EntityId".
I just checked it. When we build 3 entity using same entityId, it will build 3 EntityViews.
It will cause problem when engine using EntityDB.QueryEntityViews<T>(). It will return 3 EntityView with shared components.

Here the problem.

QueryEntityViews not queries by group. It queries from GlobalEntityViews.

if (_globalEntityViewsDB.TryGetValue(type, out entityViews) == false)

why aren't you using QueryGroupedEntityViews though?

EntityDB.QueryGroupedEntityViews(int groupID)

Regardless your problem, the _globalEntityViewsDB is something that I am torn if removing or not. This changes the design a lot, as currently you can create engines that handle grouped entity views and engines that handle all the entityviews of a given type regardless which group they are in. If I remove the globalEntityViewsDB, then you must always know in which groups your entities are and you can't design anymore engines that are not aware of groups.

At the same time if groups are not needed, the StandardGroup can be used as "global pool", while if groups are needed the engines must be aware of the group.

However I think this is not a good idea, as if I have grouped entities (like enemy entities) with HealthEntityView, I cannot write anymore an engine that handles all the HealthEntityView regardless the group the entities are in.

Just for making consistent code.

Since I built multiple entities using same EntityId which causing problem above, QueryEntityViews<T>() becomes a not reliable API.
Then, I don't like "Sometimes we use QueryEntityViews<T>() to query all EntityViews, sometimes we use QueryGroupedEntityViews<T>(ExclusiveGroups.StandardEntity) to query all EntityViews "

So I changed it, and make new constaint that all entities should be built in ExclusiveGroups.StandardEntity group.

no, read above, that is not how it works. There is currently a very good reason why the global entity DB must be used separated by the groups. Unless I find another solution, it must stay as it is.

QueryEntityViews and QueryGroupedEntityViews are two different and separate concepts at the moment and must be used to solve different problems.

Remember that entities are now always built in a group (meaning that they are found in the standard group at least), but they are also in the global group. The weird thing is that if groups are not explicitly used, the standard group and the global db are the same thing.

  • sometimes we use QueryEntityViews() to query all EntityViews => correct
  • sometimes we use QueryGroupedEntityViews(ExclusiveGroups.StandardEntity) to query all EntityViews => wrong. This doesn't mean query all the entities, this means query only the entities that are in the StandardGroup. It's just a coincidence that it can coincide with the global one!

if I remove the global pool, then the new design really must be to build multiple entities to use groups.
I.E.:

  • BuildEntity<SoldierEntityDescriptor>(ID); //standard group
  • BuildEntity<SoldierEntityDescriptor>(ID, teamGroupID);
  • BuildEntity<SoldierEntityDescriptor>(ID, playerGroupID);

in this way I build the entity three times because I want it in the global standard pool, grouped per team and grouped per player.

I mean, if we agree it's not too bad, it may be a solution, but this can work only with EntityView as it exploits the fact that you use the same implementors.

EntityStruct cannot be built multiple times, as they are copies of each other and not references. On the other hand this is true even with the global pool, as entity struct cannot be grouped basically...hmm now I am thinking to remove the globalDB and actually go with this direction.

I agree about QueryEntityViews() and QueryGroupedEntityViews(ExclusiveGroups.StandardEntity) are two different things.

But, what I'm thinking now, with current Svelto.ECS, the main problem is about how we build entities, not querying entities.
And I also believe (by intuition), that you are also thinking about it. XD

So, to make my project easier to upgrade to new version of Svelto.ECS, I chose using modified QueryEntityView<T>() to query all EntityViews instead of using QueryGroupedEntityViews<T>(ExclusiveGroups.StandardEntity). I think it is not bad as long I use it to query all EntityViews.

What is the easier part? The easier part is I just need to focus at my factories, not factories and which engines using QueryGroupedEntity(ExclusiveGroups.StandardEntity).

does it make sense?
:)

If you agree that what I wrote above makes sense, I'll take that direction, but you shouldn't change the framework code unless you know that I could break it :) I still want to see your code though, I want to see how it looks and if it looks to awkward

Yeah, my bad. :)

What do you think like this?

void BuildEntityInGroup(int id, params int[] groups);

Or may be better just using BuildEntity

void BuildEntity(int id, params int[] groups);

you want me to hide the fact that the entity is actually built multiple times? It may be an idea

I only fear that managing multiple groups may become very hard!

Yeah it is much cleaner.

Then, when destroying, will we still need EGID? or just EntityID?

you will need the EGID, removing and swapping will become hard

Since you already have live product which not using unique Id, I think it will be much harder.

Sorry, causing problems for you.
:D

it's not just that, if the user decides to use groups, they must mind them. If the user decides to use more than one group, they must mind them. This means that the user must in any moment know in which groups the entities are. If not, you can easily get lost and having same entities all over the places! We must be very careful and keep it under control.

@leopripos If you need just to iterate combinations of 2 groups, and you want to avoid the overhead of copying all elements of 2 groups into a new array, why you just don't iterate groups in sequence?

The most silly implementation I can come with is

public class CombinedGroupIterator<T> where T : EntityView, new()
{
    private readonly int[] groupsToCombine;

    public CombinedGroupIterator( int[] groupsToCombine)
    {
        this.groupsToCombine = groupsToCombine;
    }

    public IEnumerator< T> GetEnumerator( IEntityViewsDB entityViewsDB)
    {
        foreach(var group in groupsToCombine)
        {
            int count = 0;
            foreach(var view in entityViewsDB.QueryGroupedEntityViewsAsArray<T>( group, out count))
            {
                yield return view;
            }
        }
    }

}

Usage in your engine:

    public class MyEngine: IQueryingEntityViewEngine
{
    private readonly CombinedGroupIterator<MyEntityView> combinedGroup = null;

    public MyEngine()
    {
        int group1 = 3;
        int group3 = 4;
        int group5 = 7;

        combinedGroup = new CombinedGroupIterator< MyEntityView>( 
            new int[] { group1, group3, group5 }); 
    }

    public IEntityViewsDB entityViewsDB { set; private get; }

    public void Ready() {  }

    public void DoSomething()
    {
        var enumerator = combinedGroup.GetEnumerator( entityViewsDB); // 1 allocation

        while (enumerator.MoveNext())
        {
            MyEntityView view = enumerator.Current;
        }
    }
}

Of course Getting a enumerator implies doing 1 allocation, but you can workaround that, this is the slightly better version:

public class BetterCombinedGroupIterator<T> where T : EntityView, new()
{
    private readonly int[] groupsToCombine;

    public BetterCombinedGroupIterator( int[] groupsToCombine)
    {
        this.groupsToCombine = groupsToCombine;
    }

    public IEnumerator< T> GetEnumerator( IEntityViewsDB entityViewsDB)
    {
        while (true)
        {
            foreach (var group in groupsToCombine)
            {
                int count = 0;
                foreach (var view in entityViewsDB.QueryGroupedEntityViewsAsArray<T>(group, out count))
                {
                    yield return view;
                }
            }

            yield return null; // this notify the end of a cycle of iterations
        }
    }

}

This can still be used, but have the advantage of performing no more allocations at runtime:

public class MyEngine: IQueryingEntityViewEngine
{
    private readonly BetterCombinedGroupIterator< MyEntityView> combinedGroup = null;
    private IEnumerator< MyEntityView> viewCollection = null;

    public MyEngine()
    {
        int group1 = 3;
        int group3 = 4;
        int group5 = 7;

        combinedGroup = new BetterCombinedGroupIterator< MyEntityView>( 
            new int[] { group1, group3, group5 }); 
    }

    public IEntityViewsDB entityViewsDB { set; private get; }

    public void Ready()
    {
        viewCollection = combinedGroup.GetEnumerator( entityViewsDB);
    }

    public void DoSomething()
    {
        MyEntityView currentView = null;

        while( viewCollection.MoveNext() && (currentView = viewCollection.Current) != null)
        {
            // do what ya want with currentview.. No allocations at all!
        }
    }
}

And I think with minor effort you can create a class from which your engine Inherit to further reduce the boilerplate code.

it's an interesting approach, I think it can be improved too. Something to keep in mind.

@leopripos another way you can explore is actually to build multiple EnginesRoot. There is no limitation to the number of EnginesRoot you can build. Multiple engines root are useful to encapsulate logic even more.

@Darelbi Looks interesting, Can you explain more how to handle this case?

entityFactory.BuildInGroup<PlayerTroopDescriptror>(id, components, playerID, teamID);
entityFactory.BuildInGroup<NonPlayerTroopDescriptror>(id, components, teamID);

var troops= entityDB.QueryGroupedEntityViews(playerID); // return player troops
var troops= entityDB.QueryGroupedEntityViews(teamID); // return player and non-player troops

@sebas77 I think, creating new context or EngineRoot will increase complexity. It will make project harder to maintain.

I don't think this long thread is useful anymore.