Discussion on how to handle hit detection replication?

I’m trying to make a raycast based system to simulate projectiles and its working well so far.

The projectiles are all synchronised since I’m using a quadratic equation with respect to time. (ie. At any time t, the same projectiles across all clients would be at the same position)

If you’re curious what I did this, feel free to ask.

However, eventually I will get to a point where it comes to replicate hit detection for other clients.

Ideally, the best method should have:

  • Low latency for the firer and other clients (delays in hit detection are not/barely noticeable),
  • Avoid sending data unnecessarily (avoid sending data back and fourth between the same clients),

Based on my own experimentation and suggestions from fellow devs, there are a few methods of doing this that I’m familiar with;

1. Purely from the firer’s client

Any hits that are registered by the firer are sent to the server and broadcasted to the rest of the connected clients (with little to no interpretation by the server).

Hits by the client’s copy of the projectile are not registered on all clients except the firer, and instead relies on the broadcast of the firer’s hit.

Suffers from relatively high latency for other clients since the message has to be sent from the firer —> server —> other clients.

Lowest latency for the client, since it is handles locally and doesn’t suffer the latency associated with client-server connections.

2. Purely from the server

The client sends the details of the projectile for the server to carry out its own simulation and hit detection, and the server transmits the hits to all connected clients.

Hits are not registered on all clients and instead rely on the server for any hit info.

This would cut the latency (compared to method 1) by about half since the message only has to be sent from the server —> all clients (as compared to client —> server —> other clients)

3. From all clients

Since the projectile is already being simulated on all clients, all clients can do their own hit detection.

Completely unsynchronised since all the clients are doing their own thing.

Almost no latency since theres no dependency on client-server connections.

Important note for method 3:

When implementing this, I kept a dictionary of the projectiles that were fired, indexed to a unique key that was generated by string.pack().

If a client detected a hit, it would not act on it, and instead just transmit the hit and the projectile’s to the server.

If the projectile’s index existed then it would be removed from the dictionary and the hit information would then be transmitted to all clients.

Subsequently, if any other client tried to register its own hit, the projectile wouldn’t exist in the dictionary and the hit would be disregarded.

This works similar to method 1.

However, this implementation has a lot more redundant data being sent.

Please feel free to clarify anything, add on to the methods I mentioned, and/or suggest your own methods.

For my own projectile framework, I plan on using a combination of all of the three methods you described.
When the firer shoots a projectile, it is immediately first simulated on their end to avoid latency, and the firing data is sent to the server for both server simulation (sanity check) and broadcasting (replication).
Both the server and the firer will control the projectile with the server having priority, and all the other clients simulating it will have listeners for synchronization events, which is when the projectile intersects something and its status/path needs to be manipulated (hit registration, penetration, ricochet, etc.)
Only the firer declares when a synchronization event should happen. It will first consult the server for a sanity check by sending the current state of the firer’s simulated projectile. If the sanity check algorithm (explained below) deems it passable, it will broadcast the correction data to all clients, including the firer themselves. If it is not passable, then the broadcast will use the server’s own projectile state. The correction data will be used to adjust the trajectory and statistics of the projectile in real time among all the clients and the server.
If for an unknown reason that the server is the first one to report an intersection before the firer does (which shouldn’t happen because the firer is always the first one to simulate the projectile), the projectile will be put on a very short stasis period to wait for the firer’s synchronization event request. If the stasis period expires, the event will occur regardless of the firer’s discretion so as to not delay the hitreg too much.

Now, for bandwidth optimizations, I implemented a proxy system for firing the projectiles, known as the “Gun” class. Instead of sending over the entire fire data of each projectile for replication, you only need to send the gun’s ID and the direction of fire.
Shotgun-type guns are especially benefited by this system; instead of sending the data of each pellet, you just send over a Random.new seed and the number of pellets, and the spread pattern will be universally consistent.
Of course, to prevent exploiters from spamming their sniper rifles, there will be a serversided debounce for the Gun object. The client can shoot the gun all they want and have it visually display the bullet, but it is the server who gets to decide if the projectile should be replicated or not.

Projectiles are identified by an ID system that utilizes the Gun class. It is formatted like this: <Gun ID>\<Trigger Counter>\<Pellet Counter>. Trigger counter just counts the number of times the gun has already fired. Pellet counter is only used for shotguns and it’s self-explanatory. For projectiles that do not have an attributed gun, the Gun ID is simply omitted.

The sanity check algorithm used to determine the acceptability of the provided correction data is simply just a distance comparison. When a projectile is fired, the server will actually record the position of the projectile at each frame, in a <age>:<position> dictionary. Part of the synchronization event request sent by the firer includes the current “age” of the projectile. The server will use this to find the closest attributed position to the given projectile age and compare the displacement between the server’s recorded position and the correction data’s position.


That’s a mouthful, so here’s the list of steps for a hypothetical projectile:

Spawning a projectile

  1. Firer shoots the gun.
  2. Projectile begins simulation on the firer’s side.
  3. Firing data is sent to the server.
  4. Server performs the initial sanity check for the gun.
  5. If the sanity check fails, stop right here.
  6. Server begins its own simulation of the projectile.
  7. The firing data is relayed to all other clients for replication.
  8. Recipients of the firing data begin the simulation.

Synchronization event, firer → server

  1. Firer’s simulation detects a hit.
  2. Synchronization event request is sent to the server.
  3. Firer’s simulation will continue as normal if applicable.
  4. Server reviews the request and performs sanity check.
    If the check fails, default the correction data to the server’s simulation.
    If the check passes, use the firer’s simulation for the correction data.
  5. Correction is applied to the server simulation.
  6. All relevant hit registration events are processed.
  7. Correction data of the synchronization event is broadcasted to everyone.
  8. Correction is applied to all recipients’ simulations.

Synchronization event, server detects hit first

  1. Server’s simulation detects a hit.
  2. Server’s simulation is put in stasis.
  3. Server waits for the firer’s synchronization request.
    If the stasis times out, continue the synchronization event using server’s simulation.
    If the firer sends the request in time, perform sanity check and continue the synchronization event.
3 Likes