Implementing shooting and hit detection
This page runs through some guidance on how to implement shooting in a SpatialOS game. It covers a few different options and what they’ll let you achieve, and should give you the tools to decide how you want to implement it in your game, depending on what your particular gameplay needs.
This page looks at two main areas:
- where to detect a hit and who gets final say over whether someone was actually shot
- how you can model shooting itself
Detecting a hit
When a shot has been fired, which worker detects the hit? And which has the final say on whether someone got shot? In other words: which type of worker has write access authority over the decision?
You might not want to give the power of final say to clients: this would mean a hacked client could do whatever damage it likes to other players, potentially regardless of where they are in the world.
And if server-side workers are responsible for hit detection, that can cause problems too. For example, a player could shoot at an entity that their client has knowledge of (because the client has active read access to it), but that the server-side worker doesn’t, if that entity is outside the server-side worker’s interest radius. You also have to think about making it feel right for a client: if a player thinks they shot something, but they ‘actually’ hit something else, it can result in a frustrating user experience.
Client-side hit detection, server-side write access authority
The pattern we recommend is client-side hit detection, but with server-side write access authority to make the changes (and some level of validation on the server side too). In this pattern, a player shoots at an entity and - on their own client worker - determines whether or not there was a hit. We’ll explain this in detail below.
If that client thinks it hit the other entity, it sends a command to its own player entity. (In the entity ACL, you’ll need to restrict write access authority over the component for the command server-side worker.) Whichever managed worker instance has write access authority over the player receives the command. The command request should include, at minimum, the entityId of entity that was hit. (You’d want to use a command here because you only want this information to be sent to that specific server-side worker.)
However, as mentioned above, you don’t necessarily want to trust what the client says. If the player has hacked their client, they could claim to have hit any arbitrary entity, even if there’s an obstruction in the way, or if that entity is kilometres away.
At this point the server-side worker can do some validation. How much complexity to include here is up to you. As an example, at the simplest, you might want to check whether the entity targeted is actually nearby, whether there are any obvious obstructions (like a mountain) between the entities, or whether the player actually has enough ammunition to shoot.
Once the server-side worker has checked to its satisfaction that the hit is plausible, it sends a further command to the targeted entity, informing it that it’s been hit. (A command is appropriate here for the same reasons as the command from the shooter.) It’s worth noting you can’t be sure that the same server-side worker has write access authority over the shooting entity and the targeted entity. So it might be received by the same worker, or it might not.
The server-side worker, on receiving the command, can then apply the damage. Unlike the original “I hit something” command, this command comes from a server-side worker and so can be trusted. When a command is received, the payload includes the ID of the worker that sent it (assigned by the runtime, not by the worker that sent the command): so you can use that to check that the command was sent by the right type of worker.
Alternatives: server-side hit detection
The alternative is to do hit detection on the server side - you can see an example of this in the PiratesTutorial project. In that pattern, the client worker announces its attempt to shoot, but volunteers no opinion on whether it resulted in a hit. Workers can still execute related logic, such as visualising the shot (on all client workers) and calculating potential hits (on the server-side worker).
A disadvantage of this is that it’ll only work if the same server-side worker has active read access to a component on both players: the managed worker that has write access authority over the player shooting will need to at least have active read access to a component on the target entity. Otherwise it’ll never be able to detect the hit, because it doesn’t know the target exists.
You can see the problem clearly in the the diagram above. If Player 1 shoots at Player 2, Server-side worker 1 won’t be able to detect a hit, because Player 2 is not in its area of interest.
One solution to this problem is to make sure that workers will always be able to see players that can see each other. You can do this by making the worker overlap zone is at least as big as a player client’s interest area, ie as far as the client can see. But increasing the interest area increases the load on the worker, and the bigger that area, the more inefficient it’ll be (because they receive data for entities that aren’t actually necessary for most of the worker’s computation).
There are some other disadvantages of this pattern: how the client experiences shooting (client-side responsiveness), and server-side correctness. There’s a full round-trip’s worth of delay before the client can perform any logic in relation to a hit.
In many games, during a laggy period, when a user shoots someone it might immediately visualise a successful hit on their client, but the target only dies once the server confirms the valid hit and that the target’s health is now zero. However, with server-side hit detection, you wouldn’t even be able to visualise the hit. What a user would probably see is that after too long a moment the target suddenly at once displays all the hits that just got confirmed, and they drop dead. As mentioned above, this pattern isn’t a great user experience.
Visualising shooting happens at two main points: the origin of the shot, and where it hits.
Events are an appropriate tool to use here. If an event is emitted by an entity when it shoots and when it gets shot, all clients that have active read access to a component on the shooting or shot entity will receive the event and visualise that occurrence appropriately.
You can modify the commands described above to get a more exact visualisation. For example in the “I hit something” command, you can include a position (in the local space of the target entity) identifying the exact point it was shot, so that you can display an animation on that point if you want.
You could also modify the message flow above to have a smoother client experience and remove lag. To do this, instead of triggering the “shots fired” event, you could play both the firing and the hit animation immediately on Client 1. Then, after Server-side worker 1 validates the hit, it can trigger the “shots fired” event so that Client 2 can play the animation too. You’ll need to adapt the component event so that it includes the EntityId of the firer, so that Client 1 knows not to replay the animation when it receives an event that comes from its own shot.
As for visualising what happens in between when a shot is fired and when it hits - that’ll be covered later on when discussing models for shooting.
Shooting at things that aren’t entities
Most of the discussion so far has assumed that the player is shooting at another player or an NPC, both of which will be entities. You may also want to think about what happens if a player doesn’t hit an entity: how (if at all) do you want it to interact with the environment? This will depend heavily on exactly how the environment of your world is set up. Some options are:
- If you have fine-grained environment entities, follow the same pattern as above.
Broadcast an event from the player who fired the shot.
However, this won’t be observed by clients that have the target position in their interest region but not the firing player entity.
Run a query for all players around the point that was shot, and send commands to all of them.
This means waiting for the query result to return before sending many commands, which increases latency and is inefficient.
Other effects of shooting
You may want to have more complex effects of a shot than just doing damage to an entity: for example, explosive ammunition. This could get pretty complicated in implementation terms - for example, what happens if you want to damage multiple entities? Some of the options described above in Shooting at things that aren’t entities can apply here.
Players shooting vs NPCs shooting
The discussion so far has also talked mostly about implementing a player shooting. The flow is pretty similar if the entity shooting is an NPC.
The main difference is that, if the entity shooting is a player, the shooting originate from a client worker. If the entity shooting is an NPC, the shooting comes from a managed worker. So when it’s an NPC shooting, because it comes from a server-side managed worker, you don’t need to think so much about validation - you can trust it.
However, it also comes with a downside, similar to the one discussed in Alternatives: server-side hit detection above: an NPC won’t be able to shoot at something that the server-side worker hasn’t got active read access to.
If the managed worker’s interest radius is the same as the client’s, then this shouldn’t cause too much of a problem. On the client side, the radius will be determined by how far your client workers/players need to be able to ‘see’ around them. On the server-side, in general you want to minimize a server-side worker’s interest area while maintaining gameplay correctness. The larger the interest area, the more updates your server-side worker will receive, and the slower it will run. How small this interest area is depends on which entity components a server-side worker has write access authority over, and how far away the things they need to know about are. Having NPCs able to see far into the distance would definitely be a limiting factor on efficiency.
Models for shooting
On to the second aspect of implementation. The question you need to consider here is: what does your shooting actually look like?
Straight ray casts
For laser-style weapons that shoot in a straight line, you can use the simplest model for shooting: a straightforward raycast.
This is effectively not to model projectiles at all. The advantage here is the simplicity: no physics simulation of projectiles needed, no overhead of adding something to the scene.
Because the bullet is going too fast to be seen anyway, there’s no need to do anything more. Just have the visualisation described above.
Sphere cast/trajectory calculation
If you want to have a more complex effect, you can model a fast moving projectile using a series of ray/sphere casts to approximate an easy to calculate trajectory.
If you fire a bullet that has an arc, that arc’s fairly easy to calculate. You could do a sphere cast each time step, from where the abstract bullet was last frame to where it is this frame.
You still don’t need to create any physical objects (either local game objects or SpatialOS entities), avoiding that overhead.
Local game objects
For something slower-moving, like a cannonball, you’ll want to be able to see the projectile in flight. At this point you’ll probably want to create some kind of local game object. If you’ve done the Pirates tutorial, you’ll have come across this model: the cannonballs in Pirates work this way.
However, adding stuff to the scene has a cost, which is why you probably don’t want to do it otherwise. Just calculating where it goes and whether it collides with anything would be enough.
Using entities is a pretty heavyweight solution for shooting. In general, objects that make sense as entities are relatively long-lived, and have a rich presence in the world. A bullet - which would be created when it’s fired, and usually destroyed when it hits a target - is very short-lived, and so probably isn’t worth the overhead. There’s overhead of entity creation, delay of initial write access authority allocation (if you’re using a delay on handover), bandwidth usage when keeping the position synchronised, and also potential for creating the entity to fail, which complicates it further.
But there are options where you actually want to model your projectile as an entity. Most of these wouldn’t come under the banner of “shooting”, exactly: for example, one of these cases is for things that are fully physical, like grenades. Another case is long-range, slow-moving things like rockets.
In general, the bigger, longer-lived, and slower your projectiles, the more it makes sense for them to be entities. And the more likely they are to cross worker boundaries, the more having them as entities will be helpful. A long-range missile, which will almost certainly cross large parts of the world, will need to be an entity so that it doesn’t get lost when it moves from one server-side worker to another.
Firing at objects far away
Lots of the discussion above is predicated on the idea that players can only shoot at things fairly nearby: that is, things that their client has active read access to. By using this client-side hit validation, you don’t have to think about how to synchronize hit detection across worker boundaries.
If you do want players to fire beyond the boundaries of their area of interest, a lot of the above will be more complicated.