Weapons & Shooting
Overview
The Weapons
component is responsible for handling player weapons. It maintains a list of current weapons and handles weapon switches. Additionally, it provides an interface for common weapon actions such as Fire
and Reload
. Weapon objects can be picked up or dropped to the ground, see more in Weapon Drops section.
There are two base classes for weapons: Weapon
and WeaponFirearm
. Weapon
holds the bare weapon minimum and WeaponFirearm
encapsulates the basic firearm functionality like fire logic, magazine handling, dispersion, and recoil.
Weapons and projectiles in BR200 are mostly based around hitscan functionality as it was convenient for this project. For a more elaborate approach to weapons and projectiles as well as more elaborate documentation about different projectile approaches please check the Projectiles Essentials and Projectiles Advanced samples.
Hitscan Weapons
Hitscan weapons are the primary weapons in the game. When fired, hitscan weapons use raycast to evaluate hits. There is no network object spawned for every projectile, instead only a small ProjectileData
struct is stored in a circular buffer to calculate visual effects on peers. Hitscan weapons have the option to create a dummy visual-only projectile that is flying to the target (check the DummyProjectile
script).
C#
public struct ProjectileData : INetworkStruct
{
public Vector3 Destination;
public Vector3 ImpactNormal;
public int ImpactTagHash;
}
[Networked, Capacity(10)]
private NetworkArray<ProjectileData> _projectileData { get; }
Projectile Weapons
Projectile weapons (ProjectileWeapon
, ThrowableWeapon
) spawn standalone projectile objects that travel over time. The projectiles execute short raycasts between their previous position and new position every simulation tick. Projectile weapons are only used for grenades in this project. For a more in-depth implementation check the Projectiles Essentials and Projectiles Advanced samples.
Recoil
Weapon recoil is a common game behavior based on real weapons where weapons tend to aim up during continuous shooting. Usually in games weapons follow a fixed or semi-random path that players can learn and muscle memorize through extensive play which leads to players being able to counter the recoil effect with their input.
BR200 comes with a recoil system. Every weapon can have a recoil pattern (also known as spray pattern) assigned. The recoil pattern is a scriptable object that defines the weapon's path during continuous shooting. Recoil directly influences player look rotation and players can fight against it. The countering process is called Recoil Reduction, and you can find it in the SetLookRotation
method in the Agent
script. After shooting stops, the look rotation returns automatically to the initial value.
The recoil pattern defines the exact position of every bullet in a sequence. After the initial recoil sequence (recoil start values) completes, recoil follows an endless loop (recoil endless values).
Be aware that weapon dispersion is applied on top of the recoil position, so there is still some shooting randomness based on the dispersion values.
Dynamic Dispersion
Every firearm weapon has a dispersion behavior setup.
Dispersion is increased during continuous fire and automatically decreased back to the default value after fire halt. Weapon dispersion is further multiplied by the character's state (running, airborne, aiming).
Piercing
Projectile piercing (also known as penetration) is the ability to shoot through certain objects in the game world. This usually comes with a damage penalty.
Piercing is calculated directly when processing hits in the HitscanWeapon
. Every projectile can have a piercing setup assigned that specifies a damage multiplier for different materials. Projectiles don’t penetrate an object with a damage multiplier of zero. Materials are differentiated based on the Unity Tag assigned to game objects and checked in runtime based on tag's hash.
Damage Drop
Projectile damage drops with distance from the fire origin. Damage drop off is controlled by the setup defined in the DummyProjectile
component.
Third Person Shooting
Third person shooting needs to handle the difference between what the player sees through the camera and what the player character can actually hit from their position in the game world.
When calculating hits we first need to know the target point. The target point specifies where the player is aiming and is calculated by firing a raycast directly from the camera in the weapon's script every FixedUpdateNetwork()
. When fire input is processed, the weapon will take the target point and shoot another raycast from the character fire position to the target point to get the actual hits.
On top of these basic calculations there are some additional improvements for a better shooting experience:
- When the target point cannot be reached from the player's character, the red cross is shown to indicate the actual landing point
- Raycast from the fire position to the target point ignores environment hits that aren’t either close to the fire position or target point to mitigate problematic behavior that we call The Corner Issue. This vastly improves shooting behavior around corners, trees, and other objects. The player is actually shooting where the crosshair is pointing - but introduces a small margin to shoot players behind a corner if aiming right next to the corner. This ignore behavior can be found in the
ProjectileUtility
that is used by all projectile calculations.
- When the target point is close to the player character and the angle between direction to target point and character forward is too large, target point is calculated some distance in forward direction instead.
Projectiles
Projectiles that travel through the game world over time aren't really used in BR200 except for grenades. Check the Projectiles Essentials and Projectiles Advanced samples for more elaborate projectiles examples.
Grenades
Grenades can be found in item boxes. When a grenade is armed, a small countdown decreases grenade detonation time up until a certain minimum (see ThrowableWeapon
). Grenade projectiles are handled by special weapons as if shot out of an invisible grenade launcher.
Explosive Grenade
Grenade that spawns an explosion object.
Flash Grenade
Grenade that blinds players within a certain distance if they look in the direction of the grenade when it detonates. The simple player blindness effect is calculated in the AgentSenses
component.
Smoke Grenade
Grenade that spawns a smoke effect.
Health & Damage System
Every hit is processed in HitUtility
where the HitData
struct is created, and damage is multiplied by the possible body part multiplier. BodyPart
is a child class of default lag compensated Hitbox
with the addition of a damage multiplier (e.g., the head has a multiplier higher than 1 while limbs have a multiplier lower than 1). HitData
is then processed by the target’s Health
component.
The health component creates a BodyHitData
struct with only the necessary information to synchronize hits to all clients. Hits are stored in a small networked circular buffer of BodyHitData
structures.
C#
public struct BodyHitData : INetworkStruct
{
public EHitAction Action;
public float Damage;
public Vector3 RelativePosition;
public Vector3 Direction;
public PlayerRef Instigator;
}
[Networked, Capacity(4)]
private NetworkArray<BodyHitData> _hitData { get; }
RelativePosition
of the hit is stored in BodyHitData
instead of an absolute position. Since the position of the proxies is interpolated between the last two received ticks from the server, the relative position is better to place hit effects such as blood splashes correctly onto the body.
Based on the changes in the hit buffer, the appropriate hit reactions are triggered - Hit direction in UI, showing hit confirmation and numbers to damage instigator, spawn of blood effect.