For the write state, you're absolutely right that entities will need to transition between chunks. The server looks up the destination in the "active chunk set" I mentioned and then uses per-chunk std::vector<>s guarded by a mutex to push "migrating" entities. This requires a mutex since multiple CPUs/cores/threads/chunks can be pushing entities to the same destination chunk. These mutexes largely have zero to little contention.
Does that answer your question in an acceptable way?
> The server looks up the destination in the "active chunk set" I mentioned
but what if the entity teleports to an inactive chunk?
> and then uses per-chunk std::vector<>s guarded by a mutex to push "migrating" entities.
Will these migrating entities participate in later calculation within the same tick, or are they excluded? If excluded, what happens when there are two mechanisms that mutates that entity, but the first one put it into the migrating list of another chunk? If included, the thread of which chunk will continue this calculation?
And when some kind of mechanism mutates two entities in different chunks, which thread is chosen to run this mutation? And how does it mutate the state for an entity in the other chunk?
Meanwhile since you mentioned ticks, (just to be sure) are you using a barrier per tick to wait for all threads to finish the same tick? When are the migrating entities merged into the main list, and are you using another barrier for that?
As for the processing per tick: the entire active chunk set gets semi-randomly processed by threads. You'd be absolutely right that the order of operations isn't always deterministic. An entity not being processed for a tick doesn't happen since the old chunk will still process entities that are "on hold", but only perform a subset of operations. Double-processing does occur and this is sometimes visible while playing with a chat message being duplicated. This happens pretty rarely and I took care to make sure occasional double-processing doesn't interfere with gameplay mechanics.
As for synchronization: yes, each tick all processing threads join together and the server performs minor central processing and scheduling. Then the next tick begins and all threads are started again.
I'm still unsure about the following question from your answers: > when some kind of mechanism mutates two entities in different chunks, which thread is chosen to run this mutation? And how does it mutate the state for the entity in the other chunk? My initial thought was this would require adding locking on every entity, since it might be accessed from another chunk/thread. But that's clearly not ideal. Maybe have a separate list for outside-chunk changes, and merge them with internal changes later?
And regarding having two states: How/when are you sending world state to clients? My thought was to clone the current state periodically and send it to clients (can sending takes less than one tick? I don't have an idea). Sending state to clients seems to be needed if the game needs client side predication, hence this question.
All external effects that can happen to an entity are written to an atomic integer that tracks healing points, damage points and effects like poison and stunning. Angeldust currently runs on CPUs supporting atomic integers everywhere, so this doesn't have to fall back to mutexes. On platforms/compilers without atomic integers I have a software-fallback that uses mutexes for this, but currently it's not used anywhere.
Most world state is compiled into flat byte arrays each tick to save processing time, since many clients can be in the same area at once. The server bundles these world state byte arrays for chunks near a client together and sends them over the network. The client interpolates from these updates. What you see on screen lags behind subtly to make sure animations are smooth. I think this happens in most online games since you don't really want to extrapolate.
Angeldust's game pace and mechanics are designed so that the interpolation doesn't affect your aiming accuracy or game experience too much.
The Angeldust game world is tracked in an "active chunk set". A chunk is a 2D part of the world, 32x32x64 in-game blocks (meters). The active chunk set tracks only the chunks that contain, or are near, players. This chunk set is processed in parallel on any number of threads/cores configured in the server settings file.
Angeldust tracks two states for the data contained in each chunk, kind of like an array of size 2. Each tick, one state in the array is the immutable "read"-state and the other is the mutable "write"-state. Every tick these states ping-pong between the roles.
During processing the server processes the read state and copies over unchanged entity/world data by simply pushing pointers to that data into std::vector<>s in the write state. For things that did change I push a new pointer into the write state and clear up the old pointer in the next tick. Pushing pointers saves memory bandwidth since you don't have to copy all entity data for every tick. And still you can guarantee that the "read"-state will be unmodified so that other CPUs/cores/threads can freely access all of the data and pointers.
On the livestream I discussed a few cases where I do use (largely uncontested) mutexes for data synchronization: for friend chat and private chat I do a lookup of destination hero pointers in the "hero map" and then lock/unlock the hero's mutex for pushing chat messages into a vector.
Does this make sense? Let me know if I should elaborate. I will probably take a nap now, but I'll answer you (and others) tomorrow.
I'll also do another livestream tomorrow, because I feel everyone on HN has very useful questions, feedback and ideas.