Hi,
I am kinda stuck on something that seems pretty trivial in non-ecs network projects. I need to implement some type of event system, for example when A thing happens, multiple other things can react to that thing. For example, when a character uses an ability the following should happen:
- Check if the ability hit an enemy(Server, no client prediction)
- Reduce health of the enemy (Server)
- Spawn a floating text UI entity that shows the damage done (Client)
- Plays a sound (Client)
Let’s first look at the first two things that are done on the server. I am using a DynamicBuffer to store HealthEvent data on each unit, then I have a system that goes through all the events, reduces (or increases if it’s a heal) the health of the unit and then clears the dynamic buffer. The health is then propagated to all clients and they update the unit’s health. Sounds simple. However it starts to get a bit complicated when you want the client to also react to the health event (for step 3 and 4).
Problem #1:
The health event buffer is populated and cleared on the same tick on the server. If there is packet loss, the client might never see the updated dynamic buffer.
I solved this by keeping track of the NetworkTick that a health event is added to the buffer and make sure that:
a) It is handled only on the tick that was added (So it’s handled only once on the server)
b) It is kept in the buffer for a few ticks to give time for it to be propagated to all clients
Problem #2: The client receives the health events and needs to react to them (To play sound and spawn some UI element). Since the server now keeps the health events for multiple ticks, the client now also needs to make sure that he reacts to them only once. It can’t clear the buffer since it is coming from the server and will be re-populated on the next sync.
To do that I introduced a new component HealthEventTracker that tracks the latest event tick handled for that particular entity to ensure that an event is not handled twice. This component is only added on the client as it’s not needed on the server. To make things more complicated, the different reactions happen in different systems. For example there is a system that is responsible for the floating text UI and another system that is responsible for sounds. So I need to either make all HealthEvent reactions happen on the same system or introduce another system that runs after the rest and updates the HealthEventTracker so that in the next iteration all the other systems don’t react to the same events.
This seems like a lot of complexity just to do something when a character is hit. I can imagine having dozens of types of events happening in a game. Is there a better way to achieve something like this?
Thank you!
I think there are many ways to solve this problem. Myself tend to use circular buffers for things that are fine if they are missed on client side if it is throttled e.g. floating texts. So storing tick for each entry and having keeping track on last processed one on client side sounds good to me at least.
Then from this there are choices, you can make it generic “client event” to have more than effects on health changes, which can handle maybe both text, sounds, particles etc. Or you can have one buffer for each event type, or do what you propose have one buffer per event but different system with last processed. Consider how often these events will be played, if some will overflow the buffer easier, if some should have higher priority than others. Having more buffers will be additional sent data as well with 1 bit for each element and 2 for buffer itself if I don’t remember wrong. So depending on game etc it might be worth considering as well of how many elements you default to and how many buffers you use.
1 Like
Not currently, so I agree with Jawsarn here. We eventually want to provide an append-only IBufferElementData
API (or similar), but that work had to be deprioritized slightly, missing 1.3 & 1.4 (and I cannot promise it’ll be in 1.5 either).
Sounds like a reliable RPC call? Not familiar with the API in question, but this is normally handled with a reliable RPC call. And not to halt other entities in the game its important to select good channels for your reliable RPCs.
For exmple the players avatar can be one channel so if a RPC on another channel needs to be resent or if there is out of order issues then other channels are not affected by it. Again common stuff in a networking solution.
Similar, yes. Netcode for Entities only supports reliable RPCs today (unreliable RPCs are also on our roadmap), but often you want unreliable “event-like” data on the ghosts themselves, which would also support relevancy and importance scaling.
Good point, and indeed: Netcode for Entities uses three different pipelines for RPCs vs Commands vs Snapshots, so RPCs (which have a throttling rule of one MTU packet per tick, not configurable) shouldn’t materially affect snapshots (again with a throttling rule of one MTU packet per tick, configurable), nor commands (up to 4kb per tick, but typically a few hundred bytes into a single packet) in practice (…except in extraordinarily contrived circumstances).
On this note: We don’t currently support user-defined pipeline channels, unfortunately. You’ll need to make package modifications for this level of granularity.
1 Like
I dont really see how OP can get into the problem he have with a ordered reliable RPC. But again I have ZERO knowledge of this API. We use Steam networking with their built in reliable RPC
Btw, unreliable RPC is nice when you want a networked synced variable but want to control when have it enabled. But maybe your API have disable/enable function on the synced variables instead. Again I have no knowledge of this API but have made a e-sport level network game, with custom lag comp etc, so I do have some knowledge
edit: to have custom channel support is supersweet. We are a VR game and need more reliable RPC than most normal games becasue weapons states are more engagning. So each interactible in our game sends on its own channel. What can happen is that someone that shot first is handeled as shot last on the server. But a trade off we are willing to live with. Edit: maybe we should move ballistics to a separate channel, food for fought
Thank you all for your feedback.
I didn’t attempt to implement this with RPCs , mainly for the reasons specified by NikiWalker. I would basically need to re-implement relevancy and priority. I ended up keeping my original implementation as described in the original post.
1 Like