NateTheGreatt/bitECS

how to fully delete/reset a game world?

bcolloran opened this issue · 8 comments

I'm having trouble fully resetting/deleting a bitecs game world. I have a client/server situation, and I'd like to completely blow away everything when the last client disconnects, so that I can be 100% confident that everything is completely deterministic when new clients connect. However, there seems to be some scrap of hidden state because when my clients connect the first time, a particular entity id associated with a some of the client state is e.g. eid=601, then when that client disconnects/reconnects, next time that eid is 1802, then next time 3003, 4204, etc etc etc.

Between disconnects, I call resetWorld, deleteWorld, I set the variable holding the reference to the world to null, and I call the function that defines everything relevant to my ecs implementation: components, de/serializers, systems, queries, entity spawning convenience functions, etc -- everything is wrapped in functions, so I had expected all references to the old world and components and whatever counter is tracking EIDs to be GCed away when replace the reference to the previous ecs world, but it seems like some state is leaking out somehow. Looking into it a bit, perhaps there is leakage to the module-level scope via this module-level variable:

let globalEntityCursor = 0

?

Is the only option here to fully reload the module, or is there a better path? Thanks!

(fyi, I was able to solve this by adding a hardResetAll function to a fork, which resets all (at least I think all...) of the module-scoped state in bitECS. It seems to work, but I don't know the bitECS internals well enough to know if this will be disastrous somehow later on)

when my clients connect the first time, a particular entity id associated with a some of the client state is e.g. eid=601, then when that client disconnects/reconnects, next time that eid is 1802, then next time 3003, 4204, etc etc etc.

Are you doing arithmetic operations on entity IDs? How did you end up with those numbers?

I had expected all references to the old world and components and whatever counter is tracking EIDs to be GCed away

Is this a memory leak?

From what you're describing, it seems like you're relying on entity IDs being deterministic across runs. Unless there's a bug in the code, resetWorld should clear all state that's relevant to the user. (but not invisible state not defined in the user API)

Also, regardless of whether the old world is GCed, this shouldn't affect other program behaviours.

It's a little unclear what your actual problem is.

I'm trying to set up an authoritative server with client side prediction. Since snapshotting is not yet implemented (#70 (comment)), I've been doing my best to work around that, and for for my workarounds the bookkeeping and debugging is much easier if I can reset the whole universe when the last client disconnects, so that when new clients connect they end up with the same ids etc.

Nope, not doing anything weird like arithmetic on entity ids, those are the eids returned by bitECS's addEntity function. The scenario is:

  • I launch a server process that starts a copy of bitECS
  • a client connects, and I log it's eid (e.g. 601)
  • the client disconnects; since it's the last (in this case, the only) connected client I call resetWorld()
  • another client connects, and I log it's eid (e.g. 1802)
  • so on...

It not a memory leak, it's just that bitECS maintains a little bit of module-level state that is not reset by the resetWorld function. The icky solution I threw together solution in my fork I resets that module-level state, and I therefore get eid = 601 across arbitrarily many disconnects and reconnects.

Is there any reason you want the id to be the same across runs? Since you did a reset, there's no previous information to compare against, so ideally you wouldn't even notice that the ID isn't the same. It's best to assume that the entity id allocation is nondeterministic and that the only guarantee is uniqueness.

Yes, as said, deterministic IDs have made my workarounds for snapshotting tractable given the currently available APIs. I was also encountering race conditions with hot module reloading client-side that have been totally resolved now that I'm able to do a hard reset of the server side state.

I hear you -- perhaps ideally I wouldn't notice that the entity ID isn't the same, but it has turned out that in actual practice in my application, having those be deterministic is indeed helpful, and non-deterministic behavior has led to confusing behavior... that's why I was looking for a way to do a hard reset of not just the "world" but the whole "universe" ;-)

I guess I also tend to prefer strict determinism when I can have it, since it can help to cage bugs in the non-deterministic parts of the system (like networking). Not saying that has to be a goal for bitECS or anything -- you guys do what works for your project :-) I've got a workaround, so I can keep moving forward...

Anyway, thanks for bitECS, I'm enjoying it. Feel free to close this issue if you think it's invalid.

I think I see what you're trying to do now, it's probably best to see what Nate says about this

entity IDs act very much like pointers which usually have no determinism, and treating them as such has a few benefits. if you need a persistent UUID per entity/client it's probably better to keep a map around for that.

also, fyi, all worlds share one entity ID space.

const worldA = createWorld()
const worldB = createWorld()

addEntity(worldA) // => 0
addEntity(worldB) // => 1

if an entire reset is required i'll probably need to add a new api function e.g. resetGlobals or something.

@bcolloran if you want to PR your fork i'll give it a scan.

admittedly, having this global state has been a pain point for some and i've been wanting to get rid of it by introducing the concept of a Universe or something that holds this global state that a user can have complete control over if they desire.

Hi @NateTheGreatt, I submitted PR #87. It's very simple, but it worked for me. It may admittedly be totally stupid or disastrous in some way I have not yet realized...