Projectiles Essentials
Overview
Fusion Projectiles demonstrates multiple ways to implement networked projectiles. For a simpler overview and learning experience, the example implementations have been split into two separate projects - Projectiles Essentials and Projectiles Advanced.
This project - Projectiles Essentials - serves as an entry point into the subject. It aims to explain and give simple examples of all common approaches. Every example is commented and has an independent structure that enables grabbing useful code snippets and understanding core concepts without complex relations.
Projectiles Advanced serves as a game sample that goes in depth by solving common use cases when building shooter games and presents solutions for different projectile types (bouncing, homing, exploding, etc.). Projectiles Advanced is not yet available for Fusion 2 but the principles remain the same.
This documentation covers only Projectiles Essentials. Please refer to a separate project page for Projectiles Advanced.
Client/Server
topology.Features
- Five small independent examples with commented code to help understand projectile concepts
- Simple FPS player that use KCC addon for movement
- Physics based environment where players can shoot boxes
Download
Version | Release Date | Download | |
---|---|---|---|
2.0.4 | Dec 20, 2024 | Fusion Projectiles Essentials 2.0.4 Build 749 |
Requirements
- Unity 2021.3
- Fusion AppId: To run the sample, first create a Fusion AppId in the PhotonEngine Dashboard and paste it into the
App Id Fusion
field in Real Time Settings (reachable from the Fusion menu). Continue with instruction in Starting The Game section.
Choosing the right approach
Before diving into the project, let's discuss some of the various options available for networking projectiles in Fusion.
A) NetworkObject with NetworkTransform/NetworkRigidbody
Spawn projectile as a NetworkObject
, use NetworkTransform
or NetworkRigidbody
to synchronize position.
- ✅ Simple solution when spawn delay is not an issue
- ✅ Interest management works out of the box
- ✅ Can use proper physics (PhysX)
- ❌
NetworkObject
instantiation overhead - ❌
NetworkTransform
/NetworkRigidbody
means sending every transform update over the network - ❌ Spawn delay (can be mitigated by pre-spawning projectiles in advance - see Example 1)
Takeaway: Suitable only for small numbers of projectiles in certain situations - for example when complex physics projectiles are needed (e.g. rolling ball). Otherwise usage of this approach should be avoided.
This approach is showcased in Example 1.
B) NetworkObject with Fire Data
Spawn projectile as a NetworkObject
, use networked custom data (usually fire tick, fire position and fire direction) in a NetworkBehaviour
on spawned object to calculate the full projectile trajectory on server and clients.
- ✅ Updating position costs no bandwidth
- ❌
NetworkObject
instantiation overhead - ❌ Manual interest management handling
- ❌ Spawn delay (can be mitigated by pre-spawning projectiles in advance - see Example 2)
Takeaway: Suitable only for small numbers of projectiles in certain situations - for example for very long living projectiles that can outlive the shooter (e.g. nuke rocket in Unreal Tournament). Although this approach is usually better than A for non-physical projectiles, it still has significant downfalls and except for mentioned use cases should be avoided in favor of further solutions.
This approach is showcased in Example 2 and Projectiles Advanced uses this approach for long living projectiles (see Standalone Projectile).
C) Projectile Count Property
Synchronize only a fired projectile count, stored in any NetworkBehaviour
. Projectile is just a visual and is spawned as a standard Unity object. On the Input Authority and the State Authority shooting will be precise, however for proxies the shooting is based on their interpolated position and rotation. When projectiles are fired with lower cadence, it is possible to save information about the last projectile (e.g. impact position) mitigating precision problems of this solution.
- ✅ Simple solution
- ✅ Least amount of data transferred over the network
- ✅ Spawning of visuals can be ignored on server
- ✅ Interest management as part of the controlling object (player, weapon)
- ✅ No spawn delay
- ❌ Suitable only for hitscan projectiles
- ❌ Imprecise on proxies. Proxies are in snapshot interpolated position, thus their fire position/direction can be quite different. (this point applies only when data about last projectile cannot be saved due to high fire cadence)
- ❌ There is no data about projectile target position (hit position) so precise projectile path is unknown on proxies. This implies that some projectile visuals could travel through walls and other objects since the projectile path end is unknown. (this point applies only when data about last projectile cannot be saved due to high fire cadence)
Takeaway: Good solution for simplistic use cases where we do not care about precision and visuals on proxies much - e.g. top down shooter with simple projectiles and short projectile visuals (trails) - or when having additional networked information only for last projectile is sufficient => maximum weapon cadence is quite low and firing multiple projectiles at once (shotgun style) is not needed. Usually though a solution that uses a projectile data buffer is a better option with just slightly increased complexity - see Example 4.
This approach is showcased in Example 3.
D) Projectile Data Buffer
Synchronize custom data for each projectile - stored in a networked array in the form of ring buffer in a NetworkBehaviour
. The projectile is just a visual which is spawned as a standard Unity object. For optimal bandwidth usage the projectile data set should be sparse - e.g. projectile trajectory is calculated from fire position, fire direction or other data - but for some special use cases updating position constantly is also possible.
- ✅ Good bandwidth consumption
- ✅ Covers most projectile use cases
- ✅ Spawning of visuals can be ignored on server
- ✅ Interest management as part of the controlling object (player, weapon)
- ✅ No spawn delay
- ❌ Projectiles cannot exist without the controlling object = cannot outlive it
- ❌ Projectile physics like bouncing is only manually calculated
Takeaway: Very good solution for most situations except for very long-living or important projectiles (projectile that can outlive the owner - e.g. Redeemer projectile in Unreal Tournament that should still travel in the level even when the owner was despawned or disconnected) or very long-traveling (due to interest management).
This approach is showcased in the Example 4 and Example 5 and is also a core part of the Projectiles Advanced sample.
Project Organization
Project is structured as a set of 5 small independent examples.
/01_NetworkObject | Example 1 - Network Object with NetworkRigidbody/NetworkTransform |
/02_NetworkObjectFireData | Example 2 - Network Object with Fire Data |
/03_ProjectileCountProperty | Example 3 - Projectile Count Property |
/04_ProjectileDataBuffer_Hitscan | Example 4 - Projectile Data Buffer - Hitscan |
/05_ProjectileDataBuffer_Kinematic | Example 5 - Projectile Data Buffer - Kinematic |
/Common | Prefabs, scripts and materials common to all examples |
/ThirdParty | Third party assets (models, effects) |
Starting The Game
Each example has its own scene file which can be opened and played. Alternatively all examples can be started from the Start scene (/Common/Start
).
After starting an example scene a slightly modified version of the Fusion standard NetworkDebugStart
window will appear where the mode in which the game should be started in can be chosen.
Multipeer
Choosing Start Host + 1 Client
or Start Host + 2 Clients
will start the game in multi-peer mode. This approach is highly recommended for testing how projectiles behave on proxies.
To switch between peers, press the 0
, 1
, 2
, and 3
keys on the numpad.
It is also possible to switch peers using the Runner Visibility Controls
window (top menu Fusion/Windows/Runner Visibility Controls
).
To see how shooting behaves on a proxy, set only Client A as visible, and only enable Client B as an input provider. Your shooting from Client B now can be observed from Client A's perspective.
Controls
Use W
, S
, A
, D
for movement and Mouse1
for fire.
Use the ENTER
key to lock or release your cursor.
Examples
Projectile Essentials provides examples for every approach introduced in the Choosing The Right Approach section. All scripts are commented so feel free to dive into investigating specific code.
All examples utilize the same physics playground where players can shoot dummy cubes. To better showcase prediction when firing projectiles, Physics Prediction is enabled (see the RunnerSimulatePhysics3D
component on the Common/Prefabs/RunnerBase prefab). Cubes need to be simulated on all clients, which is why simulation is activated for these objects in the Cube
script. However, predicting physics is performance-intensive, so unless your game heavily relies on reactive physics, this setting should generally be turned off.
Example 1 and Example 2 use spawning of network objects. It represents the most obvious approach for beginners however such approach is not recommended for many reasons stated above, one of which is complexity (especially due to spawn delay issues). For a simple approach to projectiles jump directly to Example 3.
Example 1 - Network Object with NetworkRigidbody/NetworkTransform
Example 1 shows a physical projectile that can bounce and roll in the environment. Projectile is a spawned network object with its own behavior. Projectile's position and rotation (as well as other physics properties) are synchronized via NetworkRigidbody
.
Firing such projectile can be as easy as spawning it with NetworkRunner
and calling Fire
on custom projectile script:
C#
var projectile = Runner.Spawn(projectilePrefab, fireTransform.position, fireTransform.rotation, Object.InputAuthority);
projectile.Fire(fireTransform.forward * 10f);
Fire
method could add impulse to the physical projectile:
C#
public void Fire(Vector3 impulse)
{
_rigidbody.AddForce(impulse, ForceMode.Impulse);
}
However this simplistic approach has a downside. Since NetworkObject spawn is not predicted in Fusion 2, all projectiles are spawned by the State Authority. There will be a significant pause after fire input before the projectile is actually fired when network conditions are not ideal. This spawn delay can be hidden behind animation or fire visual effect. If hiding is not an option it can be mitigated by pre-spawning projectiles in advance and utilizing these prepared objects for predicted projectile firing. This approach is demonstrated in Example 1 (refer to the FireWithBuffer
method in Weapon_NetworkObject script). However, as it is a more advanced solution, it is advisable to revisit this after exploring the other examples.
Example 2 - Network Object with Fire Data
Example 2 shows a kinematic projectile that travels in time. The projectile is still a spawned network object but its position and rotation is calculated on every client based on some initial fire data instead of synchronizing position/rotation every tick over the network.
Note: Difference between hitscan and kinematic projectiles is explained in the Projectiles Types section of the Projectiles Advanced sample.
Firing is similar as in previous example:
C#
var projectile = Runner.Spawn(projectilePrefab, fireTransform.position, fireTransform.rotation, Object.InputAuthority);
projectile.Fire(fireTransform.position, fireTransform.forward * 10f);
public void Fire(Vector3 position, Vector3 velocity)
{
// Save fire data
_fireTick = Runner.Tick;
_firePosition = position;
_fireVelocity = velocity;
}
Movement of the projectile is however calculated from the initial parameters:
C#
[Networked]
private int _fireTick { get; set; }
[Networked]
private Vector3 _firePosition { get; set; }
[Networked]
private Vector3 _fireVelocity { get; set; }
// Same method can be used both for FUN and Render calls
private Vector3 GetMovePosition(float currentTick)
{
float time = (currentTick - _fireTick) * Runner.DeltaTime;
if (time <= 0f)
return _firePosition;
return _firePosition + _fireVelocity * time;
}
Usually it is enough to move the actual projectile transform in Render
calls only. In FixedUpdateNetwork
previous and next position can be calculated to fire raycasts and resolve potential collisions.
C#
public override void FixedUpdateNetwork()
{
if (IsProxy == true)
return;
// Previous and next position is calculated based on the initial parameters.
// There is no point in actually moving the object in FUN.
var previousPosition = GetMovePosition(Runner.Tick - 1);
var nextPosition = GetMovePosition(Runner.Tick);
var direction = nextPosition - previousPosition;
if (Runner.LagCompensation.Raycast(previousPosition, direction, direction.magnitude, Object.InputAuthority,
out var hit, _hitMask, HitOptions.IncludePhysX | HitOptions.IgnoreInputAuthority))
{
// Resolve collision
}
}
Similar to Example 1, this approach has a downside by waiting for State Authority to spawn the projectile. Solution is the same as in Example 1.
Example 3 - Projectile Count Property
Example 3 shows a very efficient approach of synchronizing just a projectile count directly on the weapon itself. This approach represents hitscan projectiles - projectile hits are immediately evaluated on fire. It means that the hit effect (e.g. damage, physics impulse) is immediately applied to the target but usually it is just fine in many cases to spawn dummy projectile visuals that still travel through the air for a short time. Dummy projectiles are used in this example (see Common/Scripts/DummyFlyingProjectile.cs
).
C#
public class Weapon : NetworkBehaviour
{
[SerializeField]
private LayerMask _hitMask;
[SerializeField]
private Transform _fireTransform;
[Networked]
private int _fireCount { get; set; }
private int _visibleFireCount;
public void FireProjectile()
{
// Whole projectile path and effects are immediately processed (= hitscan projectile)
if (Runner.LagCompensation.Raycast(_fireTransform.position, _fireTransform.forward,
100f, Object.InputAuthority, out var hit, _hitMask))
{
// Resolve collision
}
_fireCount++;
}
public override void Spawned()
{
_visibleFireCount = _fireCount;
}
public override void Render()
{
if (_visibleFireCount < _fireCount)
{
// Show fire effect
}
_visibleFireCount = _fireCount;
}
}
This approach is very simple, efficient and prediction works out of the box (no need to deal with spawn delay because there is no network object spawning involved). As mentioned in the Choosing the Right Approach section it has some disadvantages that are easily mitigated by using projectile data buffer (Example 4).
Example 4 - Projectile Data Buffer - Hitscan
Example 4 showcases usage of projectile data buffer. The hitscan version of the buffer is a step up from Projectile Count Property from the previous example while retaining the simplicity.
Projectile Data Buffer approach uses a fixed array of ProjectileData
structs that acts as a circular buffer. This means that when firing, the Input/State authority is filling the buffer with data based on the current buffer head (fireCount
property) and all clients are visually catching up in Render calls based on their local head (visibleFireCount
). ProjectileData
contains all necessary data for reconstructing projectiles on all clients.
C#
public class Weapon : NetworkBehaviour
{
[SerializeField]
private LayerMask _hitMask;
[SerializeField]
private Transform _fireTransform;
[Networked]
private int _fireCount { get; set; }
[Networked, Capacity(32)]
private NetworkArray<ProjectileData> _projectileData { get; }
private int _visibleFireCount;
public void FireProjectile()
{
var hitPosition = Vector3.zero;
var hitOptions = HitOptions.IncludePhysX | HitOptions.IgnoreInputAuthority;
// Whole projectile path and effects are immediately processed (= hitscan projectile)
if (Runner.LagCompensation.Raycast(_fireTransform.position, _fireTransform.forward,
100f, Object.InputAuthority, out var hit, _hitMask))
{
// Resolve collision
hitPosition = hit.Point;
}
_projectileData.Set(_fireCount % _projectileData.Length, new ProjectileData()
{
HitPosition = hitPosition,
});
_fireCount++;
}
public override void Spawned()
{
_visibleFireCount = _fireCount;
}
public override void Render()
{
if (_visibleFireCount < _fireCount)
{
// Play fire effects (e.g. fire sound, muzzle particle)
}
for (int i = _visibleFireCount; i < _fireCount; i++)
{
var data = _projectileData[i % _projectileData.Length];
// Show projectile visuals (e.g. spawn dummy flying projectile or trail
// from fireTransform to data.HitPosition or spawn impact effect on data.HitPosition)
}
_visibleFireCount = _fireCount;
}
private struct ProjectileData : INetworkStruct
{
public Vector3 HitPosition;
}
}
It is recommended to keep ProjectileData
struct as small as possible and use data that does not change in time (if possible) to achieve good bandwidth even when firing large numbers of projectiles.
This approach is fairly simple, efficient and flexible and is recommended for most games. In case kinematic projectiles are needed in your game, a slightly more complex version of the buffer needs to be used - proceed to Example 5.
Example 5 - Projectile Data Buffer - Kinematic
In many cases projectiles in games travel through the environment for some time before they hit something or expire. We call such projectiles Kinematic Projectiles or simply just Projectiles. Projectile data buffer solution needs to be adjusted to update the projectile data based on the state of the projectile on Input/State authority and handle spawning and updating of visual representation of the projectiles for all clients in Render.
C#
private struct ProjectileData : INetworkStruct
{
public int FireTick;
public int FinishTick;
public Vector3 FirePosition;
public Vector3 FireVelocity;
public Vector3 HitPosition;
}
It still applies that ProjectileData
struct should be as small as possible and preferably the data should not change much. In this example the projectile data is set twice per single projectile - once when projectile is fired (FirePosition
, FireVelocity
and FireTick
is set) and once when projectile collides with the environment (HitPosition
and FinishTick
is set). In special justified cases it is of course possible to update the data in a more frequent manner - see Homing Projectiles in Projectiles Advanced.
About Projectiles Timing
Note: Make sure to understand how Prediction works in Fusion first.
In Fusion we recognize two main time frames. Local time frame is the time of the local player and local objects. Remote time frame is the time of remote players and remote objects. All objects on the Host/Server are always simulated and rendered in the Local time frame. Clients are rendering objects in both time frames - usually Local is used for local objects and Remote is used for remote objects. For simulation, clients are usually only simulating their local objects (= FixedUpdateNetwork
is not executed for proxies) unless proxy objects (projectiles of other players) are rendered in Local time frame and data about possible collisions and other interactions is needed to avoid overshooting.
In reality this means that when two different players A and B that stand next to each other and fire a projectile at the same tick. From player A's perspective the projectile A is immediately fired, however player A does not have any information about the projectile B yet as this information still travels from player B to server, is processed and then sent to player A. When this information finally arrives, player A is already several ticks ahead (= is predicting from the last known server state into the future) and rendering of the projectile A is ahead as well. Let's say projectile A is already 3 meters from player A. Now we have two options on how to render projectile B. We can choose to render it "correctly" in the Local time frame on position where it should be - 3 meters from player B - but that would mean that projectile visuals appear out of nowhere 3 meters from player B. Since such behavior is usually undesirable we choose to render projectile B in the Remote time frame. Which means that the projectile B is fired directly from player B's weapon but is rendered 3 meters behind player A's projectile even though projectiles were fired in the same tick. This is precisely why in all relevant examples in this sample the time for rendering projectiles is calculated as follows:
C#
float renderTime = Object.IsProxy ? Runner.RemoteRenderTime : Runner.LocalRenderTime;
Advanced Projectiles Timing Note
Please read only if curious about intricate timing topic. It is not required to understand this topic in such detail if all you want to do is shoot some projectiles.
There are technically several time frames when it comes to rendering (or rather reading values from Render
calls):
- Local Time (Simulation Time)
- = values from last forward tick
- Value in Render: Reading network property on Input/State authority
- Time in Render:
Runner.SimulationTime
orRunner.Tick * Runner.DeltaTime
- Local Render Time
- = values interpolated between last two forward ticks
- Value in Render: Reading Interpolator value on Input/State authority
- Time in Render:
Runner.LocalRenderTime
or(Runner.Tick - 1 + Runner.LocalAlpha) * Runner.DeltaTime
- Local Render Time - Extrapolated
- = values extrapolated from last forward tick
- Value in Render: Reading network property on Input/State authority + calculating render part of the value locally
- Time in Render:
Runner.LocalRenderTime + Runner.DeltaTime
or(Runner.Tick + Runner.LocalAlpha) * Runner.DeltaTime
- Remote Time
- = values from latest server tick (=> last received tick)
- Value in Render: Reading network property on proxies (if properties are not modified on proxies during resimulation)
- Time in Render:
Runner.LatestServerTick * Runner.DeltaTime
- Remote Render Time
- = values interpolated between remote interpolation ticks (there needs to be a safety window before latest server tick is used for interpolation, therefore remote interpolation ticks are a little bit more in the past than latest server tick)
- Value in Render: Reading Interpolator value on proxies
- Time in Render:
Runner.RemoteRenderTime
Looking at such time frame definitions, projectiles from example 5 are rendered in Local Interpolated time frame for Input/State authority and in Remote Interpolated time frame for proxies. However for this to be exactly correct fireCount
and projectileData
properties should be read from respective interpolators. This is omitted for simplicity - We are using Remote Render time but Remote values (and Local Render time but Local values). Therefore the shooting happens slightly sooner on proxies than it technically should. The difference does not justify complexity in this case. For a full interpolation example check Projectiles Advanced.
- Overview
- Features
- Download
- Requirements
- Choosing the right approach
- A) NetworkObject with NetworkTransform/NetworkRigidbody
- B) NetworkObject with Fire Data
- C) Projectile Count Property
- D) Projectile Data Buffer
- Project Organization
- Starting The Game
- Examples
- Example 1 - Network Object with NetworkRigidbody/NetworkTransform
- Example 2 - Network Object with Fire Data
- Example 3 - Projectile Count Property
- Example 4 - Projectile Data Buffer - Hitscan
- Example 5 - Projectile Data Buffer - Kinematic
- About Projectiles Timing