skypjack / entt

Gaming meets modern C++ - a fast and reliable entity component system (ECS) and much more
https://github.com/skypjack/entt/wiki
MIT License
9.99k stars 877 forks source link

Specify forced entity id upon creation #303

Closed jhlgns closed 4 years ago

jhlgns commented 5 years ago

It would be convenient to be able to specify an entity's id upon creation. For multiplayer games this would mean that clients do not have to translate the server id to the local id to know which entity the server is referring to.

skypjack commented 5 years ago

What should happen in this case if the id already exists or the versions differ? As an example, if you ask to create id 1 with version 0 and it already exists, either alive or not, either with the same or a different version?

jhlgns commented 5 years ago
  1. I think the version should also be forced to make the entity id unique in the specified scenario (as it usually does everywhere)
  2. The registry should throw an error if the entity is already existing: there should be none since entities are always synchronized with the server, which means that if the server wants to create entity id=123|version=12 then the client should not have an entity with id=123 at this point (maybe it did, but since the server destroyed that entity, the client should've done the same before creating the new one). This would be the way i thought about replicating entities via network, but any way of forcing entity ids would be extremely helpful.
skypjack commented 5 years ago

Ok and what about entities in the middle? I mean, imagine the server create 3 entities for its purposes, then as to the client to force-create entity 3. The client create it but there are also entities 0, 1 and 2 there and they are not in use yet. Should they be put in the list of destroyed entities (even if they haven't been destroyed yet) or what? I cannot easily keep track of the fact that they are available otherwise.

jhlgns commented 5 years ago

Sadly i did not have the time to look into implementing this yet. Before i used entt, i implemented a "slotmap" which created entities in chunks of lets say 256 entities. If the server requested to create entity id=532|version=1, which would live in the third chunk, then this chunk was created and the entity was put in there. When it was removed and the chunk became empty, the chunk was deleted. Now for entt, if the server has entities 1-10000 and it tells the client to create entity 10000, at the first sight it would be suboptimal for the client to create 9999 'destroyed' entities, but still doable in most non-MMO situations. This would be the way you thought of it i guess, maybe one of us will have a better idea over time. I'll have to think about the implementation - but first i wanted to ask if this would even somehow fit into this framework. PS: isn't forcing entity id's what the snapshot loader does?

skypjack commented 5 years ago

Don't worry, it's not a matter of implementing it, I'm just trying to figure out how it should work. :wink: The fact is that entity 1 is considered alive but an orphan (no components) if you force-create entity 2. Therefore it won't be returned in any case when you create new entities and you've to explicitly clean up it. That's all. In both cases, everything works just fine btw.

PS: isn't forcing entity id's what the snapshot loader does?

Yes and no. It force-create entities but to an empty registry. Moreover, it has more or less the same problem, that is the fact that it can leave around orphans if you don't guarantee that all the entities have at least one component. That's why it exposes also a member function to remove them. :wink:

jhlgns commented 5 years ago

Forgot to mention: in my case the server does not simply tell the clients to create an entity, it tells them to instantiate prototypes which are registered and referred to by simple handles, so there will never be any orphans.

Therefore it won't be returned in any case when you create new entities and you've to explicitly clean up it.

What do you mean with 'it won't be returned'?

skypjack commented 5 years ago

Forgot to mention: in my case the server does not simply tell the clients to create an entity, it tells them to instantiate prefabs which are registered and referred to by simple handles, so there will never be any orphans.

That's good. New spawn & stomp stuff will be of interest for you in this case. What game are you working on? Is it an open source or a private one?

Therefore it won't be returned in any case when you create new entities and you've to explicitly clean up it.

What do you mean with 'it won't be returned'?

I mean, if you force-create entity 1 and never delete entity 0, the latter is created anyway in the array of entities but it isn't appended to the one of destroyed entities. Therefore, from the point of view of the registry it's alive, it's just an entity without components attached. When you create a new entity, destroyed ones are returned first if any. In this case, entity 0 won't be returned because it's not considered as destroyed. The only problem is that if you never force-create entity 1 or explicitly destroy it, there will be an unused entity of which you know nothing. The good news is that it won't affect performance nor anything else (unless you create 1M of them probably, but just because they occupy memory, iterations are never affected y them anyway).

Does it make sense?

jhlgns commented 5 years ago

What game are you working on? Is it an open source or a private one?

It will be an MMORTS, somehow inspired by factorio - procedurally generated worlds and automation. Performance will be a huge concern, which is why i picked entt 😉 I don't know about open sourcing yet - i love the idea and i love looking at other game's source code but there's always that other ugly, incompatible side called monetization... still possible though.

Does it make sense?

Yes, thank you 👍

And thanks for the hint - i will look into stomping later.

skypjack commented 5 years ago

which is why i picked entt :wink:

And you've not starred it yet. Shame on you!! :smile:

jhlgns commented 5 years ago

And you've not starred it yet. Shame on you!! 😄

Now it's starred 😄 From what i could get from the documentation for stomp, what it does is transfer components from entities living in other registries, right? I think this does not solve the original problem - which was creating and identifying server entities on clients. Do you have any other mechanisms one could use to replicate entities for multiplayer games? Since the game is in a early stage, architecural changes are still possible.

skypjack commented 5 years ago

stomp allows to copies entities (and components) from a source to a target. Therefore, you can also copy within the same registry. I'm not working on a multiplayer game but I'm loading stuff from a json file and I'm using the meta part for that. How do you transfer information from the server to the client? If it's in a similar format, you can transparently do that in an opaque way without having to rely on the C++ type system in the net module.

jhlgns commented 5 years ago

Json is out of question for performance reasons. I have not yet looked into the meta stuff - the way I do it (for now, just a first prototype):

using ReplicationCallback = void (*)(Packet& packet, Entity entity, Registry& registry); // Registered for one component: tries to get the component and writes its data into the packet
void World::update() {    
    // Executed on server
    for (every player) {
        Packet packet; // The packet the replication stuff goes into
        for (every entity in range of player) {
            for (every registered replication callback) {
                replicationCallback(packet, entity, m_registry)
            }
        }

        player.send(packet);
    }
}

// For every component that should get replicated there will be a registered callback:
world->registerReplicationCallback(
    [] (Packet& packet, Entity entity, Registry& registry) {
        // Executed on server
        if (const auto playerCharComponent = registry.try_get<PlayerCharComponent>(entity)) {
            packet << playerCharComponent->someThing << playerCharComponent->anotherThing;
        }
    },
    [] (Packet& packet, Entity entity, Registry& registry) {
       // Executed on client
        const auto playerCharComponent = registry.try_get<PlayerCharComponent>(entity));
        assert(playerCharComponent); 
        packet >> playerCharComponent->someThing >> playerCharComponent->anotherThing;        
    });
skypjack commented 5 years ago

If you are fine with working with the C++ type system in this part for performance reasons, then you've no need to use meta. I must admit that the pseudocode doesn't help much with to have a clear idea of your approach. :)

jhlgns commented 5 years ago

The pseudocode was the answer to

How do you transfer information from the server to the client

Every player is sent information about the entities in the range of their character via callbacks which check if an entity has some component and if so, write it into a packet. The pseudocode shows how the replication of components would work if server and client could refer to entities with their 'unified' id. Since there is no way of ensuring this, the approach is only working with another indirection:

world->registerReplicationCallback(  // Example for PlayerCharComp, similar callbacks will be registered for other components
    [] (Packet& packet, Entity entity, Registry& registry) {
        // Executed on server, writes component data which is sent to clients
        if (const auto playerCharComponent = registry.try_get<PlayerCharComponent>(entity)) {
            packet << playerCharComponent->someThing << playerCharComponent->anotherThing;
        }
    },
    [] (Packet& packet, Entity entity, Registry& registry) {
       // Executed on client, reads the data sent by the server
        const auto translatedEntityId = lookupEntitiyId(entity); // <<<<<<<<<<< have to look up the clientside entity id for the server entity
        const auto playerCharComponent = registry.try_get<PlayerCharComponent>(translatedEntityId));
        assert(playerCharComponent); 
        packet >> playerCharComponent->someThing >> playerCharComponent->anotherThing;        
    });
skypjack commented 5 years ago

I am delaying this ticket because most likely it will become part of the review of the snapshot part (for various reasons that I won't explain in details right now because it's half past midnight here). :) To be honest, I'm still not sure of the semantics of this operation, but something similar is definitely useful to offer serialization/deserialization of the registry that is much faster than the current one. As a side effect it will probably be possible to do this too, although I don't recommend it. My two cents: I would still separate local and remote ids rather than pretending to have an exact copy of the server locally.

trashuj commented 5 years ago

Maybe you can use some kind of netId in a network component to identify the entity over the network. You could also use this component to know what to replicate.

On Mon, Sep 9, 2019, 00:22 Michele Caini notifications@github.com wrote:

I am delaying this ticket because most likely it will become part of the review of the snapshot part (for various reasons that I won't explain in details right now because it's half past midnight here). :) To be honest, I'm still not sure of the semantics of this operation, but something similar is definitely useful to offer serialization/deserialization of the registry that is much faster than the current one. As a side effect it will probably be possible to do this too, although I don't recommend it. My two cents: I would still separate local and remote ids rather than pretending to have an exact copy of the server locally.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/skypjack/entt/issues/303?email_source=notifications&email_token=ACF5UVQCSW6FSSELBIFWOCTQIV3KLA5CNFSM4ISMECHKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6F2WZA#issuecomment-529247076, or mute the thread https://github.com/notifications/unsubscribe-auth/ACF5UVQ3OCGTIOIUTJSUN2TQIV3KLANCNFSM4ISMECHA .

jhlgns commented 5 years ago

Maybe you can use some kind of netId in a network component to identify the entity over the network. You could also use this component to know what to replicate.

So if the server sends some message referencing an entity by its id - how would the client know which one the server meant? By linearly searching all the local entities for a network component with a matching "server entity id"? I don't know how that would work in constant time... Or should the server store a client entity id for every connected client for each replicated entity?

skypjack commented 5 years ago

I usually decouple the local simulation from the remote one, so that both can evolve independently from each other. To do that, I use a map local/remote id. That said, the new changes for the serialization should allow also to create locally a mirror copy of the server, but then you've to manage all the potential errors for yourself (the one already discussed above and that could create inconsistencies).

trashuj commented 5 years ago

having a network component is the same as a map then :) yes i was thinking using a view to find your entity (which doesnt have to be linear since you can sort the component pool). if you think that's a concern in terms of perfs then maybe you have too much entities to synchronize and you should go with something like lockstep. this entity networkID can be unique, generated by the server or composed from the peer id, that's up to you and depends on your model. if you have only 1 entity per player then it could be it's peer id.

On Mon, Sep 9, 2019 at 1:20 PM Michele Caini notifications@github.com wrote:

I usually decouple the local simulation from the remote one, so that both can evolve independently from each other. To do that, I use a map local/remote id. That said, the new changes for the serialization should allow also to create locally a mirror copy of the server, but then you've to manage all the potential errors for yourself (the one already discussed above and that could create inconsistencies).

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/skypjack/entt/issues/303?email_source=notifications&email_token=ACF5UVXERHIMHW2O62XCW33QIYWRLA5CNFSM4ISMECHKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6HGBLI#issuecomment-529424557, or mute the thread https://github.com/notifications/unsubscribe-auth/ACF5UVTXTZSWOZ7CMDSW4ELQIYWRLANCNFSM4ISMECHA .

jhlgns commented 5 years ago

having a network component is the same as a map then :) yes i was thinking using a view to find your entity (which doesnt have to be linear since you can sort the component pool). if you think that's a concern in terms of perfs then maybe you have too much entities to synchronize and you should go with something like lockstep. this entity networkID can be unique, generated by the server or composed from the peer id, that's up to you and depends on your model. if you have only 1 entity per player then it could be it's peer id.

Interesting idea - but binary searching for entities with a view would still require O(log n) time - I think I'll stay with my specialized hashmap for now. Yes, there will be many entities to synchronize. Lockstep is an excellent suggestion, but i can't imagine how to secure my game which is competetive from cheaters with lockstep since every client knows everything... but I'm still thinking and doing some research about this. Thanks for the suggestion!

AquariusPower commented 4 years ago

every client knows everything

may be, clients should not know everything, just enough related to what is visible, even if that means to keep non visible outdated (exist locally but wont get updated until seen).

jhlgns commented 4 years ago

may be, clients should not know everything, just enough related to what is visible, even if that means to keep non visible outdated (exist locally but wont get updated until seen).

Thats exactly what i do and why i created this issue in the first place. What you just descibed just doesn't work with lockstep, because lockstep means that every client only sends its inputs to the server and the server then distributes the input to all other clients, so every gamestate is the same since it is simulated deterministically and every client has received the same set of inputs in each frame. You can't leave out any (input) information because the gamestate would get desynced. So your architecture is what I had i mind and is called something like "authorative server-dumb clients" Anyway, this is getting off topic. If there are people who are interested in this, I'd be happy to chat on other platforms.

skypjack commented 4 years ago

Then, may I invite you in the gitter channel of EnTT? This topic is really interesting imho and I have an answer to your last comment but I don't want to go off-topic as you correctly pointed out. :wink:

Qix- commented 4 years ago

I'm in the same shoes as @roepel. I want to use ENTT but don't see a way to synchronize entities to/from the server.

Just a quick side-note, @roepel: Factorio uses lock-step. They mentioned it in one of their Friday dev updates.

However, in my case, lock-step just isn't a possibility. Using ENTT's built-in serialization is out of the question as well, as my entities are being updated out-of-order based on both priority as well as bandwidth caps.

Therefore, I need a quick way to refer to single entities that maps well on the server and client. I think the only way to do this is with a map of server IDs -> ENTT IDs, unfortunately.

skypjack commented 4 years ago

Ironically, yesterday I merged experimental on master and started to write the create-with-hint function (and a few others actually) so as to close this issue and review the snapshot part with a dual model (that is a line in the TODO file). The timing of your comment is impressive. @Qix- :smile:

The final result for this specific issue will be something like this:

if(auto entity = registry.create(hint); entity == hint) {
    // ...
} else {
    // ...
}

You can only provide the registry with an hint because it cannot guarantee all the times that the entity you want is returned. The identifier may already be in use and with a different version, in this case we cannot just override it for many reasons that I won't explain here. However, if you keep in sync the entities and delete them on both sides, it will work as expected and you can even get rid of the if. It's up to you to make it work properly in a sense.

The review of the snapshot part is something larger than this and aimed at offering a dual model that... Well, I'll write everything in the documentation, I'm from mobile now and it's a bit long to explain. :wink: I'm pretty sure you'll appreciate it though.

Qix- commented 4 years ago

That's not going to work for my case, unfortunately. The entity deletions and creations are not guaranteed to come in order, so there's no point in using hints if it's not guaranteed.

I'm not sure what the hint system provides anyway, actually. A map still makes more sense.

skypjack commented 4 years ago

@Qix- it couldn't work anyway though. If you've a sequence destroy/create on one side and you receive it as create/destroy on the other side, the final result is that you've entity E alive and death at the same time in different spaces, no matter what create does here. Even the map won't solve this problem actually. If you can't guarantee consistency in the long term, how could it work?

Qix- commented 4 years ago

Should have been more specific.

If an entity is deleted on the server and a new entity is created that re-uses the old ID, but the events are sent reversed to the client, then the client won't have the same IDs when using creation hints, but can safely solve this with a map.

skypjack commented 4 years ago

Yeah, I was re-reading your previous comments and I've just realized you are not using lockstep. :+1: Sorry for the noise. In this case, yes, I think decoupling local and remote ids is the best bet.

NixAJ commented 4 years ago

If you cannot gaurantee the order in which the client receives the updates from the server, it would make sense to include a sort of "order".

The scenario I imagined in my head was something along the lines of

Server - Delete Entity 60
Server - Create Entity 60 (This is a new entity different from the previous, aka reused id)
**Server - Packs Packets**

**Client - Receives Packets**
Client - Create Entity 60
Client - Delete Entity 60

So the problem here is pretty apparent, if the client does already have a entity 60, which would be implied by the fact that the server sent a specific request to delete that entity, then how exactly do you handle create? Regardless of what you do, you'll end up with no entity with id 60, because if you ignore the create when an entity already exists, then the existing entity will be deleted after by "Delete Entity 60", and if you override the existing entity, the same problem occurs.

So how exactly would we solve this? Fundamentally, I would always recommend writing code in such a way that you can gaurantee the order in which a packet is received, but we don't have to rely on the order of the packet, instead we can keep an internal id on the server that is used and incremented when sending a packet related to deleting or creating a new entity, thus it would look something like this

**Server - Enttiy Update Id = 1**
Server - Delete Entity 60 ( Update Id: 1)
Server - Create Entity 60 ( Update Id: 2)
**Server - Packs Packets**

**Client - Receives Packets**
Client - Create Entity 60 (Update Id: 2)
Client - Delete Entity 60 (Update Id: 1)
**Client - Sort Entity Update Packets by Packet Id**

You should ideally receive both packets in the same read, but if not, you can always create a list of entity updates, and sort them, thus if you receive Id 2 first, and then later Id 1, you know that you should ignore Id 1, or you can backtrack and figure out if you should execute 1 and 2 again.

You could also just simply check if the client's local Update Id matches the packet that you just received, then delay on executing that order until you have received a packet with the correct id.

So how would this work with Entt? Since we know that you can provide a "Hint", as to what ID you'd want a specific entity to use, you can always use a single registry just for entities provided by the server, and by keeping track of what Entity Updates happened, you can always ensure that you can safely reconstruct server state on the client.

Let me know if I missed something.

skypjack commented 4 years ago

I've pushed a create-with-hint on experimental. As @NixAJ said, you won't write anything that works if you can't guarantee the order of construction/destruction of entities on both sides. In all other cases, this function can help you. It's part of the upcoming review for the snapshot stuff but I've pushed this separately to match this issue.

There is no guarantee that the requested entity is returned. This happens only in case the entity isn't already in use (that is, it hasn't been generated yet or it has been destroyed and never recycled). Otherwise, a randomly generated identifier is returned as if you invoked create().

Please, do not close this issue. It will be closed automatically when I merge everything on master. Let me know if it works as expected or if you spot any issue. Thank you.