Closed jhlgns closed 4 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?
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.
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?
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:
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'?
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?
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.
which is why i picked entt :wink:
And you've not starred it yet. Shame on you!! :smile:
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.
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.
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;
});
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. :)
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;
});
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.
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 .
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?
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).
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 .
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!
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).
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.
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:
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.
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.
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.
@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?
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.
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.
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.
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.
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.