How to optimally sync `IBufferElementData`, how to set it up, alternatives?

I’m trying to add server-authoritative multiplayer to a small game I made with DOTS, and I have some questions…

I’m using:

  • Entities 1.0.0-exp.12
  • Netcode for Entities 1.0.0-exp.13
  • Unity Transport 2.0.0-exp.6
  • Unity 2022.2.1

(I want to keep updating to the latest versions for the foreseeable future.)

The game has a large number of Agents that follow the closest Player.

Since ECS-based Navigation is far away in the roadmap, and the game’s level is a simple square, I divided it into a grid and implemented a basic A* algorithm.

Each Agent has a WaypointData buffer:

[InternalBufferCapacity(20)]
public struct WaypointData : IBufferElementData
{
    public int2 Position;
}

The AStarSystem populates these buffers with the correct path toward each Agent’s target, and the AgentMovementSystem moves the agents along their paths.

For the multiplayer version, I want:

  • The AStarSystem to run only in the Server.

  • So I will add [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)].

  • The AgentMovementSystem runs in both the client and the server, because movement is client-predicted.

Q1: Is the attribute [UpdateInGroup(typeof(PredictedSimulationSystemGroup))] the correct one to add to AgentMovementSystem?

Q2: Should I add .WithAll<Simulate>() to AgentMovementSystem’s entity query? What does this Simulate tag mean exactly?

Q3: Given the following requirements, how do I set up the ghost/serialization for the Agents (and specifically for the WaypointData buffer)?

  • The Agents are NPCs, so they are NOT owned by any Player.
  • The WaypointData buffer will be written by the server and read by the clients.
  • The clients should not try to predict or interpolate the WaypointData buffer.
  • I only want to sync the WaypointData buffer when there are changes, which will NOT happen many times per second.
  • The WaypointData buffer will have changes when the Agent’s target moves from one cell to another. If there are many Agents following that same target, they will all try to sync at the same time.
[GhostComponent]
[InternalBufferCapacity(20)]
public struct WaypointData : IBufferElementData
{
    [GhostField] public int2 Position;
}

So I found this: Synchronize IBufferElementData or any other array of data

Q4: Do you recommend to not have the WaypointData buffer be a GhostComponent, and simply send a message from the server to all clients containing all the changed buffers to all clients?

Q5: Am I right to assume that, for this purpose, I can identify each Agent by their Ghost ID?

Q6: Can you give me an example of how to send a message from the server to all clients through TCP communication from within an ECS system?

Q7: If I don’t add GhostComponent to an IBufferElementData component, it is NOT synchronized by default, right?

Thank you for your time!

Hey EmmaPrats!

Q1: Correct.
Q2: Yes, correct. The Simulate component is added by default to all entities. It’s essentially a global tag saying “this entity should be simulated” for the purposes of game logic. In Netcode specifically, we use it to control how prediction is applied during rollback. I recommend reading Prediction | Netcode for Entities | 1.0.17 to get an overview of prediction.

So: When the server sends a snapshot to a specific client, it likely will not include all ghosts. When that snapshot arrives on the client, we undo all predicted ghosts, and re-simulate them all using this new data (in this new snapshot). But, this also means that different ghosts will have different amounts of historic data. Thus, during re-simulation (in the GhostPredictionSystemGroup), we may not need to re-predict a specific entity yet. Thus, we use the Simulate Tag component to filter out entities that don’t need to be re-predicted.

Q3 & 4: I’d recommend having two components. ServerWaypointPath, which stores the full path and is NOT replicated. Then a new struct, containing a single [GhostField] int2: NextWaypoint. This will replicate only the current waypoint (from ServerWaypointPath), and will delta-compress extremely well (as it’ll always be one tile away).

If you also need the next one after that (due to how frequently you’re resending agents), you can store the next 2 paths. I recommend against using an IBufferElementData here, because replicating the count is redundant.

Then, use default(int2) to denote “no path”.

It is almost never recommended to use RPCs as a substitute for often changing entity state data. This is because:

  • GhostFields are designed for this purpose. RPCs are not.
  • GhostFields handle pre-spawns, destroyed entities, new joiners, relevancy, and importance.
  • RPCs are reliable, which is overkill for this use-case.

Q5: Correct. Note that [GhostField] public Entity Target; will also work. I.e. Entity references are supported (and automatically handled).

Q6: Unfortunately no. UTP does not support TCP out of the box. You can, however, send reliable RPCs. What kind of data are you trying to send?
Q7: The GhostComponent is actually optional. It is actually the presence of [GhostField]'s (or [GhostEnabledBit]) that denotes whether or not a component is replicated.

Cheers!

3 Likes