Handling Projectiles in Fusion Shared Mode
This page accompanies the How to Handle Projectiles in Photon Fusion 2 (Shared Mode) video and compares three approaches for implementing moving projectiles in Photon Fusion Shared Mode.
Use this page when a weapon needs a visible projectile that travels through the world instead of resolving the shot immediately through hitscan.
You can find out more about NetworkObjects on this dedicated documentation page.
Overview
Projectile synchronization can become expensive when every bullet is spawned as a separate NetworkObject.
A projectile may exist for only a few seconds, but during that time it can require:
- network spawning;
- transform synchronization;
- authority management;
- collision detection;
- despawning;
- state replication to every interested player.
The tutorial compares three approaches:
- NetworkObject with NetworkTransform
Spawn every projectile as a networked object and synchronize its transform. - Data-driven NetworkObject
Spawn a networked projectile, but synchronize only its initial trajectory data and reconstruct its position locally. - Visual-only projectile with buffered Networked data
Store projectile data in a Networked collection and create only local visual GameObjects.
Each approach has different implementation complexity, bandwidth cost, visual quality, and gameplay flexibility.
Video Timeline
| Time | Section |
|---|---|
| 00:00 | Introduction |
| 00:14 | Initial setup |
| 03:59 | Approach 1: Projectiles as NetworkObjects |
| 16:18 | Approach 2: Data-driven projectiles |
| 28:43 | Approach 3: Visual-only projectiles with buffered data |
| 46:25 | Outro |
Hitscan Compared with Projectiles
The starter project already includes a hitscan weapon.
Hitscan does not require a bullet object to travel through the scene. When the player fires, the game immediately performs a raycast or another query from the weapon or camera.
Hitscan is commonly used for:
- rifles;
- sniper weapons;
- laser weapons;
- very fast bullets;
- weapons where travel time is not important.
A moving projectile is more appropriate when gameplay depends on:
- visible travel time;
- gravity;
- curved trajectories;
- dodging;
- slow rockets;
- grenades;
- arrows;
- plasma shots;
- collision during flight.
Initial Project Setup
The tutorial begins with the Photon Shared Mode starter project.
Before implementing projectiles:
- Import or open the Shared Mode starter.
- Add the project App ID from the Photon Dashboard.
- Verify that players can connect.
- Confirm that movement and the existing shooter logic work.
- Test the project with multiple clients.
The existing template includes:
- room connection;
- player spawning;
- Shared Mode authority;
- input handling;
- a player controller;
- hitscan shooting.
Creating Separate Projectile Input
The sample adds a second fire action so the existing hitscan implementation can remain available for comparison.
The projectile fire input should enter the same Fusion input pipeline used by the player.
A simplified input structure might contain:
C#
public NetworkBool TutorialFire;
The player simulation consumes the input:
C#
if (input.TutorialFire)
{
_fireCount++;
_simpleWeaponController.Shoot();
}
The _fireCount or equivalent fire-event handling should prevent one buffered input from spawning several projectiles unintentionally.
Buffering Fire Input
Unity input and Fusion simulation ticks do not always occur at the same frequency.
If a one-frame button press is read only inside FixedUpdateNetwork(), it can be missed.
A common pattern is:
- Read the local button in Unity
Update(). - Store the request in the player input structure.
- Submit the input to Fusion.
- Consume the action once during
FixedUpdateNetwork().
The projectile should be spawned or registered by the peer with the required authority.
SimpleWeaponController
SimpleWeaponController provides one place for choosing which projectile technique to test.
It stores:
- the player reference;
- the projectile spawn transform;
- projectile prefabs;
- optional buffered projectile controller;
- booleans selecting the active approach.
Conceptually:
C#
public class SimpleWeaponController : NetworkBehaviour
{
[SerializeField]
private Transform _firePoint;
[SerializeField]
private NetworkObject _transformProjectilePrefab;
[SerializeField]
private NetworkObject _dataDrivenProjectilePrefab;
[SerializeField]
private VisualsOnlyProjectileWithBufferData
_bufferedProjectileController;
[SerializeField]
private bool _useTransformApproach = true;
[SerializeField]
private bool _useDataDrivenApproach;
[SerializeField]
private bool _useBufferedApproach;
public void Shoot()
{
Vector3 spawnPosition =
_firePoint.position;
Quaternion spawnRotation =
_firePoint.rotation;
if (_useTransformApproach)
{
SpawnTransformProjectile(
spawnPosition,
spawnRotation
);
}
else if (_useDataDrivenApproach)
{
SpawnDataDrivenProjectile(
spawnPosition,
spawnRotation
);
}
else if (_useBufferedApproach)
{
_bufferedProjectileController.Fire(
spawnPosition,
_firePoint.forward
);
}
}
}
For production code, prefer an enum instead of several booleans so only one approach can be selected.
Projectile Approach Enum
A clearer configuration can use:
C#
public enum ProjectileImplementation
{
NetworkTransform,
DataDrivenNetworkObject,
BufferedVisual
}
Then:
C#
[SerializeField]
private ProjectileImplementation _implementation;
This prevents conflicting Inspector settings.
Approach 1: NetworkObject with NetworkTransform
The most direct approach is to spawn each projectile as a Fusion NetworkObject.
The projectile contains:
NetworkObject;NetworkTransform;- collider or collision-query logic;
- a
NetworkBehaviour; - visual mesh or effect.
The projectile moves during FixedUpdateNetwork(), and the NetworkTransform synchronizes its position.
Spawning the Networked Projectile
The weapon spawns the projectile through the active runner:
C#
Runner.Spawn(
_transformProjectilePrefab,
spawnPosition,
spawnRotation,
Runner.LocalPlayer
);
In Shared Mode, the local player can receive authority over the object when it is spawned, depending on the object configuration and spawn call.
NetworkedProjectileEntity
The first projectile script can contain:
- projectile lifetime;
- movement speed;
- collision layer mask;
- authority validation;
- despawn logic.
Example structure:
C#
using Fusion;
using UnityEngine;
public class NetworkedProjectileEntity :
NetworkBehaviour
{
[SerializeField]
private float _speed = 15f;
[SerializeField]
private float _lifetime = 10f;
[SerializeField]
private LayerMask _targetLayers;
private TickTimer _lifeTimer;
public override void Spawned()
{
if (HasStateAuthority)
{
_lifeTimer =
TickTimer.CreateFromSeconds(
Runner,
_lifetime
);
}
}
public override void FixedUpdateNetwork()
{
if (!HasStateAuthority)
{
return;
}
Vector3 movement =
transform.forward *
_speed *
Runner.DeltaTime;
transform.position += movement;
if (_lifeTimer.Expired(Runner))
{
Runner.Despawn(Object);
}
}
}
The sample also performs collision detection before advancing the projectile.
Swept Collision Detection
Fast projectiles can move through a collider between ticks.
Instead of checking only the projectile’s current position, cast along the movement path:
C#
Vector3 movement =
transform.forward *
_speed *
Runner.DeltaTime;
if (Runner.GetPhysicsScene().Raycast(
transform.position,
transform.forward,
out RaycastHit hit,
movement.magnitude,
_targetLayers))
{
ProcessHit(hit);
Runner.Despawn(Object);
return;
}
transform.position += movement;
This is a simplified form of swept collision detection.
Ignoring the Shooter
The projectile should not immediately collide with the player who fired it.
Possible strategies include:
- compare the hit object’s authority with the projectile’s authority;
- store the shooter’s
PlayerRef; - store the shooter’s
NetworkObject; - use collision layers;
- temporarily ignore the shooter collider.
The data-driven sample checks whether the hit NetworkObject has the same authority as the projectile.
For more complex games, store an explicit shooter identifier.
Advantages of the NetworkTransform Approach
This approach is easy to understand.
It provides:
- standard Fusion spawning;
- a visible network object in the scene;
- direct component attachment;
- simple collision ownership;
- simple despawn handling;
- compatibility with object-based gameplay systems.
It can be appropriate for:
- rockets;
- grenades;
- rare projectiles;
- projectiles players can interact with;
- objects that change direction unpredictably;
- persistent or destructible projectile entities.
Limitations of the NetworkTransform Approach
The projectile transform is continuously synchronized.
Remote clients see the object after network delay, and linear transform synchronization may make fast bullets appear behind their expected position.
Potential costs include:
- one NetworkObject per projectile;
- spawn and despawn messages;
- NetworkTransform state;
- repeated transform changes;
- increased object count;
- latency on proxy clients;
- allocation and pooling requirements.
For a rapid-fire weapon, this can become expensive.
When Approach 1 Is Appropriate
Use a fully networked projectile when it needs its own persistent network identity.
Examples include:
- a rocket that can be shot down;
- a grenade that can be picked up;
- a projectile with changing ownership;
- a mine that remains in the scene;
- a projectile with complex state;
- an object that can be redirected after spawning.
For simple bullets with a known trajectory, a complete NetworkObject may be unnecessary.
Approach 2: Data-Driven NetworkObject
The second approach still spawns a NetworkObject, but it does not continuously synchronize the projectile position through a NetworkTransform.
Instead, it synchronizes the data required to reconstruct the trajectory:
- fire tick;
- fire position;
- fire velocity.
Every client calculates the projectile position from the same starting data.
Data-Driven Spawn Flow
The weapon spawns the data-driven projectile:
C#
Runner.Spawn(
_dataDrivenProjectilePrefab,
spawnPosition,
spawnRotation,
Runner.LocalPlayer,
OnBeforeSpawned
);
The spawn callback initializes the projectile:
C#
void OnBeforeSpawned(
NetworkRunner runner,
NetworkObject spawnedObject)
{
spawnedObject
.GetComponent<DataDrivenProjectile>()
.Init(
spawnPosition,
_firePoint.forward
);
}
Using the spawn callback ensures the initial Networked data is set as part of the object’s spawn state.
DataDrivenProjectile State
The projectile stores:
C#
[Networked]
private int FireTick { get; set; }
[Networked]
private Vector3 FirePosition { get; set; }
[Networked]
private Vector3 FireVelocity { get; set; }
The projectile may not need a Networked transform because its position is derived from these values.
Initializing the Trajectory
C#
public void Init(
Vector3 position,
Vector3 direction)
{
FireTick = Runner.Tick;
FirePosition = position;
FireVelocity =
direction.normalized * _force;
}
All clients receive the same starting trajectory data.
Calculating Projectile Position
For constant acceleration, use:
position =
initial position +
velocity × time +
0.5 × acceleration × time²
In Unity:
C#
private Vector3 GetPosition(float time)
{
if (time <= 0f)
{
return FirePosition;
}
Vector3 gravity =
Physics.gravity *
time *
time *
0.5f;
return FirePosition +
FireVelocity * time +
gravity;
}
For a projectile without gravity:
C#
return FirePosition +
FireVelocity * time;
Calculating Time from Ticks
Simulation collision checks use Fusion ticks:
C#
private Vector3 GetPosition(int tick)
{
float time =
(tick - FireTick) *
Runner.DeltaTime;
return GetPosition(time);
}
This allows the projectile to determine its expected position at any simulation tick.
Collision Detection
During FixedUpdateNetwork(), calculate:
- position at the current tick;
- position at the next tick;
- direction between both positions.
Then raycast across that interval:
C#
public override void FixedUpdateNetwork()
{
if (_hasHit)
{
return;
}
Vector3 currentPosition =
GetPosition(Runner.Tick);
Vector3 nextPosition =
GetPosition(Runner.Tick + 1);
Vector3 movement =
nextPosition - currentPosition;
if (Runner.GetPhysicsScene().Raycast(
currentPosition,
movement.normalized,
out RaycastHit hit,
movement.magnitude,
_hitMask))
{
ProcessHit(hit);
}
}
Only the appropriate authority should finalize gameplay effects and despawn the projectile.
Rendering from Local and Remote Time
The projectile visual should be placed according to the correct render timeline.
Conceptually:
C#
public override void Render()
{
if (_hasHit)
{
return;
}
float renderTime =
HasStateAuthority
? Runner.LocalRenderTime
: Runner.RemoteRenderTime;
float elapsed =
renderTime -
FireTick * Runner.DeltaTime;
transform.position =
elapsed <= 0f
? FirePosition
: GetPosition(elapsed);
}
The State Authority can use local render time.
Proxies use remote render time so the projectile aligns with their interpolated network timeline.
Why Data-Driven Rendering Looks Better
Remote clients do not wait for a new NetworkTransform position every tick.
Once they receive the projectile’s initial data, they can calculate where the projectile should be at the current render time.
This reduces the visible delay associated with synchronizing a rapidly moving transform.
Advantages of Data-Driven NetworkObjects
Compared with transform synchronization, this approach provides:
- smoother proxy movement;
- less continuous transform state;
- trajectory reconstruction from compact data;
- consistent interpolation;
- normal NetworkObject lifecycle;
- straightforward despawning;
- support for gravity and known trajectories.
Limitations of Data-Driven NetworkObjects
A NetworkObject is still spawned for every projectile.
The project still pays for:
- spawn messages;
- object registration;
- authority state;
- despawn messages;
- component lifecycle;
- GameObject management.
It is also best suited to trajectories that can be reconstructed from known data.
If the projectile changes direction unpredictably, additional state is required.
When Approach 2 Is Appropriate
Use a data-driven NetworkObject when:
- the projectile needs a NetworkObject identity;
- trajectory is mostly predictable;
- transform synchronization is too delayed;
- the object has additional Networked state;
- the projectile count is moderate;
- object-based interaction is still required.
Approach 3: Visual-Only Projectiles with Buffered Data
The third approach avoids spawning a NetworkObject for each projectile.
Instead:
- A persistent NetworkBehaviour stores projectile records.
- The records are synchronized through a
NetworkArray. - Every client creates local visual objects.
- Simulation collision logic uses the shared projectile data.
- Visuals are destroyed when the synchronized record becomes inactive.
This separates gameplay data from Unity GameObject representation.
Core Principle
The network does not need to synchronize the projectile GameObject itself.
It only needs to synchronize enough information for clients to reconstruct the projectile:
- when it was fired;
- where it started;
- its initial velocity;
- whether it is still active.
The local visual object has no NetworkObject.
ProjectileData
Define a Fusion-compatible struct:
C#
private struct ProjectileData :
INetworkStruct
{
public int FireTick;
public Vector3 FirePosition;
public Vector3 FireVelocity;
public NetworkBool IsActive;
}
The struct is stored in a Networked collection.
NetworkArray Buffer
Create a fixed-capacity array:
C#
[Networked, Capacity(32)]
private NetworkArray<ProjectileData>
ProjectileBuffer => default;
Also store the total number of shots:
C#
[Networked]
private int FireCount { get; set; }
The buffer behaves as a ring buffer.
Ring Buffer Index
Convert the fire count into a buffer index:
C#
int index =
FireCount %
ProjectileBuffer.Length;
When the buffer reaches its capacity, newer projectile records replace older slots.
The buffer capacity must be large enough that an active projectile is not overwritten before it expires.
Firing a Buffered Projectile
Only the correct authority should add entries:
C#
public void Fire(
Vector3 firePosition,
Vector3 fireDirection)
{
if (!Object.HasStateAuthority)
{
return;
}
int index =
FireCount %
ProjectileBuffer.Length;
ProjectileBuffer.Set(
index,
new ProjectileData
{
FireTick = Runner.Tick,
FirePosition = firePosition,
FireVelocity =
fireDirection.normalized *
_projectileSpeed,
IsActive = true
}
);
FireCount++;
}
The persistent controller can be:
- attached to the player weapon;
- attached to the player object;
- attached to a shared projectile manager;
- separated by weapon or projectile type.
Fixed Buffer Capacity
A Fusion NetworkArray has fixed capacity.
The required size depends on:
- weapon fire rate;
- maximum projectile lifetime;
- number of active weapons;
- whether each player has a separate buffer;
- expected simultaneous projectile count.
A rough minimum per weapon is:
fire rate per second × maximum lifetime
Add safety margin for bursts and delayed deactivation.
Buffer Capacity Example
A weapon firing 5 shots per second with a 4-second maximum lifetime can have approximately:
5 × 4 = 20
active records.
A capacity of 32 may be sufficient for that isolated weapon.
A machine gun firing 20 shots per second with a 10-second lifetime could exceed 32 entries very quickly.
Simulating Active Projectiles
The authority checks active projectile records during FixedUpdateNetwork().
For each active entry:
- Calculate the current tick position.
- Calculate the next tick position.
- Raycast between them.
- Apply gameplay effects if a collision occurs.
- Mark the record inactive.
- Mark it inactive if its lifetime expires.
Collision Processing
Conceptually:
C#
Vector3 currentPosition =
GetProjectilePosition(
data,
Runner.Tick
);
Vector3 nextPosition =
GetProjectilePosition(
data,
Runner.Tick + 1
);
Vector3 movement =
nextPosition - currentPosition;
if (Runner.GetPhysicsScene().Raycast(
currentPosition,
movement.normalized,
out RaycastHit hit,
movement.magnitude,
_hitMask))
{
Health health =
hit.collider
.GetComponentInParent<Health>();
if (health != null)
{
health.TakeHit(1, true);
}
data.IsActive = false;
ProjectileBuffer.Set(index, data);
}
Gameplay damage must be applied only by the peer authorized to resolve the projectile.
Projectile Lifetime
Deactivate projectiles that do not hit anything:
C#
float age =
(Runner.Tick - data.FireTick) *
Runner.DeltaTime;
if (age > _maxLifetime)
{
data.IsActive = false;
ProjectileBuffer.Set(index, data);
}
A lifetime limit prevents records from remaining active indefinitely.
Visual Projectile Representation
Each client creates a local visual for every new buffer entry.
A local record can contain:
C#
private class ProjectileVisual
{
public GameObject Visual;
public int DataIndex;
public int FireSequence;
public bool HasShownImpact;
}
Including a fire sequence identifier helps distinguish a new projectile from an older projectile that used the same ring-buffer slot.
Detecting New Projectile Records
Track the last processed fire count:
C#
private int _lastProcessedCount;
During Render():
C#
while (_lastProcessedCount < FireCount)
{
int dataIndex =
_lastProcessedCount %
ProjectileBuffer.Length;
CreateVisual(
dataIndex,
_lastProcessedCount
);
_lastProcessedCount++;
}
This creates one local visual for each newly observed shot.
Updating Visual Position
Use local render time for authority and remote render time for proxies:
C#
float renderTime =
Object.IsProxy
? Runner.RemoteRenderTime
: Runner.LocalRenderTime;
Calculate elapsed time:
C#
float elapsedTime =
renderTime -
data.FireTick *
Runner.DeltaTime;
Then reconstruct the visual position:
C#
Vector3 position =
data.FirePosition +
data.FireVelocity *
elapsedTime;
visual.transform.position =
position;
Gravity or other known trajectory effects can also be included.
Hiding Future Projectiles
A proxy can receive state while rendering an earlier remote time.
If elapsedTime is negative, the projectile should not be visible yet:
C#
if (elapsedTime < 0f)
{
visual.SetActive(false);
return;
}
When the remote render timeline reaches the projectile’s fire tick, activate the visual.
Projectile Rotation
For a linear projectile:
C#
visual.transform.rotation =
Quaternion.LookRotation(
data.FireVelocity
);
For a curved trajectory, calculate the current velocity or the direction from the current position to the next position.
Deactivating Visuals
When the synchronized data becomes inactive:
- Optionally create an impact effect.
- Destroy or return the visual to a pool.
- Remove its local tracking entry.
Conceptually:
C#
if (!data.IsActive)
{
if (!visual.HasShownImpact)
{
SpawnImpactEffect(
visual.Visual.transform.position
);
visual.HasShownImpact = true;
}
Destroy(visual.Visual);
_visuals.RemoveAt(index);
}
The impact position in the basic sample uses the last rendered projectile position.
For a more accurate impact effect, synchronize or locally reconstruct the actual collision point.
Local Impact Effects
Impact visuals are normally presentation-only.
The networked state only needs to tell clients that the projectile became inactive.
Each client can create:
- sparks;
- particles;
- decals;
- sounds;
- camera effects.
These effects do not need their own NetworkObjects unless their gameplay state must be synchronized.
Pooling Projectile Visuals
Avoid repeatedly calling:
C#
Instantiate()
and:
C#
Destroy()
for high-rate weapons.
Use a local object pool:
- Get an inactive projectile visual.
- Reset its transform and effects.
- Activate it.
- Return it to the pool on impact or expiration.
The network buffer and the visual pool solve different problems:
- the buffer stores synchronized projectile records;
- the pool manages local Unity GameObjects efficiently.
Advantages of the Buffered Visual Approach
This approach can provide:
- no NetworkObject spawn per projectile;
- no NetworkTransform per projectile;
- compact fixed-capacity state;
- smooth local reconstruction;
- local visual pooling;
- fewer networked GameObjects;
- lower spawn and despawn overhead;
- efficient handling of many predictable projectiles.
Limitations of the Buffered Visual Approach
It requires more custom code.
The project must manage:
- fixed buffer capacity;
- overwritten ring-buffer entries;
- visual creation;
- visual cleanup;
- collision authority;
- event sequencing;
- late join behavior;
- projectile lifetime;
- impact positioning;
- buffer wraparound.
It is best suited to projectiles whose behavior can be reconstructed from compact data.
Buffer Wraparound
A ring-buffer slot can be reused while an older visual still references it.
If the local visual stores only the data index, it may begin reading the data of a newer projectile after wraparound.
To avoid this, also track a sequence number.
For example, add to the data:
C#
public int Sequence;
When reading a slot, verify that its sequence still matches the visual.
Processing Only Valid Buffer Entries
Avoid iterating from 0 to the total lifetime FireCount indefinitely.
FireCount can continue growing for the entire match.
Instead, process only the current bounded buffer window or maintain active indices.
Conceptually:
C#
int start =
Mathf.Max(
0,
FireCount -
ProjectileBuffer.Length
);
for (int sequence = start;
sequence < FireCount;
sequence++)
{
int index =
sequence %
ProjectileBuffer.Length;
ProjectileData data =
ProjectileBuffer[index];
if (data.Sequence != sequence)
{
continue;
}
ProcessProjectile(
index,
data
);
}
This keeps simulation cost bounded by the buffer capacity.
Late Joiners
A late joiner receives the current Networked buffer and FireCount.
The visual system should create visuals only for entries that:
- still contain a valid sequence;
- are still active;
- have not exceeded maximum lifetime;
- belong to the currently valid buffer window.
Do not attempt to reconstruct shots whose records have already been overwritten.
Data Compression
The sample uses regular Vector3 fields for clarity.
Projects can reduce state size with compressed network types where appropriate.
For example:
C#
Vector3Compressed
can reduce vector storage at the cost of precision and configured range.
Consider compression for:
- fire position;
- fire velocity;
- impact position.
Measure whether the precision remains sufficient for gameplay.
State Authority
The buffered controller’s State Authority should be responsible for:
- adding projectile records;
- running authoritative collision checks;
- applying damage;
- marking projectiles inactive.
Proxy clients should:
- read the Networked buffer;
- create local visuals;
- interpolate or reconstruct positions;
- display impact effects.
Input Authority and Shooter Identity
Store enough information to identify the shooter when required.
Possible fields include:
C#
public PlayerRef Shooter;
or a network object reference supported by the project.
This is necessary for:
- ignoring the shooter;
- damage attribution;
- kill feeds;
- team checks;
- assists;
- ownership-based effects.
Do not infer shooter identity only from the current authority of a shared projectile manager if several players use the same controller.
Applying Damage
Damage should be resolved by authoritative simulation logic.
The local visual must not decide whether a target was hit.
A safe sequence is:
- Authority simulates the projectile.
- Authority performs the collision query.
- Authority validates the target.
- Authority applies damage.
- Authority marks the projectile inactive.
- Proxies observe state and remove visuals.
Lag Compensation
The examples focus on forward-simulated moving projectiles.
Depending on game design, collision validation may also require:
- lag-compensated hitboxes;
- rewind queries;
- target interpolation awareness;
- authority-side validation.
A visual projectile appearing to overlap a target on one client does not automatically mean the authoritative simulation registered a hit.
Gravity
The data-driven NetworkObject example includes gravity.
The original buffered example uses linear movement.
The buffered approach can support gravity by using the same position formula:
C#
private Vector3 GetProjectilePosition(
ProjectileData data,
float time)
{
Vector3 gravity =
Physics.gravity *
time *
time *
0.5f;
return data.FirePosition +
data.FireVelocity *
time +
gravity;
}
Use the same formula for:
- collision simulation;
- local rendering;
- trajectory previews.
Non-Linear Projectiles
More complex trajectories can still be data-driven if their behavior is reproducible.
Examples include:
- gravity;
- constant acceleration;
- homing toward a synchronized target;
- predefined curves;
- deterministic spread patterns;
- bouncing with synchronized bounce events.
When trajectory changes depend on unpredictable physics interactions, more state or a NetworkObject may be required.
Three Approaches Compared
| Criteria | NetworkTransform Projectile | Data-Driven NetworkObject | Buffered Visual Projectile |
|---|---|---|---|
| NetworkObject per projectile | Yes | Yes | No |
| NetworkTransform required | Yes | Usually no | No |
| Continuous transform sync | Yes | No | No |
| Spawn and despawn messages | Yes | Yes | No per projectile |
| Local trajectory reconstruction | Limited | Yes | Yes |
| Implementation complexity | Low | Medium | High |
| Proxy visual quality | Can show latency | Improved | Usually best for predictable trajectories |
| Supports complex object state | Strong | Strong | Requires custom data |
| Suitable for high fire rates | Limited | Better | Strongest |
| Object interaction | Straightforward | Straightforward | Custom |
| Buffer management | No | No | Yes |
| Local visual pooling | Optional | Optional | Recommended |
| Persistent projectile identity | Yes | Yes | No separate identity |
Choosing an Approach
Use a NetworkTransform Projectile When
- projectile count is low;
- implementation simplicity is important;
- the projectile changes unpredictably;
- the projectile requires its own network identity;
- players can interact with it;
- the projectile contains substantial Networked state.
Use a Data-Driven NetworkObject When
- a NetworkObject identity is still needed;
- trajectory can be reconstructed;
- transform synchronization looks delayed;
- projectile count is moderate;
- the projectile has additional network behavior.
Use Buffered Visual Projectiles When
- projectile count is high;
- trajectories are predictable;
- objects do not require individual NetworkObject identities;
- minimizing spawn and transform traffic is important;
- the project can support custom buffer and visual management.
Hitscan May Still Be Better
Not every weapon benefits from a moving projectile.
Use hitscan when:
- projectile travel time is negligible;
- visual tracers can be cosmetic;
- immediate hit resolution is desired;
- high fire rate makes projectile simulation unnecessary;
- competitive validation requires ray-based hit detection.
A game can use different solutions for different weapon categories.
Recommended NetworkObject Workflow
- Create a projectile prefab.
- Add
NetworkObject. - Add
NetworkTransform. - Add the projectile behavior.
- Spawn it through
Runner.Spawn. - Assign the correct authority.
- Move it during
FixedUpdateNetwork(). - perform swept collision checks.
- Despawn it on hit or expiration.
- Test proxy delay under simulated latency.
Recommended Data-Driven Workflow
- Create a network projectile without continuous transform synchronization.
- Store fire tick, position, and velocity.
- Initialize data during spawning.
- Reconstruct position from Fusion time.
- Simulate collision between consecutive ticks.
- Render authority using local render time.
- Render proxies using remote render time.
- Despawn after collision or lifetime expiration.
Recommended Buffered Workflow
- Create a persistent projectile controller.
- Define a compact
INetworkStruct. - Store entries in a fixed-capacity
NetworkArray. - Track a monotonically increasing fire sequence.
- Add records only from State Authority.
- Simulate active records during
FixedUpdateNetwork(). - Apply gameplay effects authoritatively.
- Render local visuals from the synchronized records.
- Pool visual GameObjects.
- Validate ring-buffer sequences before using slots.
- Bound iteration by buffer capacity.
- Mark entries inactive after collision or expiration.
Key Concepts
| Concept | Description |
|---|---|
| Hitscan | Immediate ray or shape query without a traveling network projectile. |
| Networked projectile | Projectile represented by its own Fusion NetworkObject. |
| Data-driven trajectory | Position reconstructed from fire time, origin, velocity, and acceleration. |
NetworkTransform |
Fusion component that synchronizes object transform state. |
NetworkArray |
Fixed-capacity Fusion Networked collection. |
INetworkStruct |
Fusion-compatible structure stored inside Networked state. |
| Ring buffer | Fixed-size buffer that reuses slots in sequence. |
| Fire tick | Fusion simulation tick on which the projectile was created. |
| Local render time | Render timeline used by the authority or local simulation. |
| Remote render time | Interpolated timeline used for proxy rendering. |
| Visual-only projectile | Local GameObject reconstructed from synchronized data. |
| Swept collision | Query across the projectile’s movement interval to prevent tunneling. |
Vector3Compressed |
Compact vector representation for reducing Networked state size. |
| State Authority | Peer responsible for changing authoritative projectile state. |
Best Practices
Use this checklist when implementing Fusion projectiles:
- Choose the simplest approach that meets the game’s requirements.
- Use hitscan when visible travel time is unnecessary.
- Avoid a NetworkObject per bullet for very high fire rates.
- Simulate projectile gameplay from the correct authority.
- Keep proxy projectile objects presentation-only.
- Use swept collision checks for fast movement.
- Use
Runner.GetPhysicsScene()for Fusion physics queries. - Store the fire tick with trajectory data.
- Use remote render time for proxy visuals.
- Give every buffered projectile a unique sequence.
- Keep ring-buffer iteration bounded.
- Size the buffer for fire rate and maximum lifetime.
- Deactivate projectiles after a fixed lifetime.
- Pool visual projectile objects.
- Synchronize shooter identity when damage attribution matters.
- Compress vectors only after validating precision.
- Test with realistic latency and multiple clients.
- Profile state size, spawn rate, and CPU cost independently.
Common Pitfalls
| Pitfall | Why It Is a Problem |
|---|---|
| Spawning every rapid-fire bullet as a NetworkObject | Creates spawn, state, and despawn overhead. |
| Synchronizing a fast projectile only with NetworkTransform | Remote clients can see delayed movement. |
Moving a projectile only in Render() |
Gameplay collision must be processed in simulation logic. |
| Letting visual proxies apply damage | Gameplay results can conflict between clients. |
| Checking collision only at the current position | Fast projectiles can tunnel through targets. |
| Forgetting to ignore the shooter | The projectile may hit its owner immediately. |
| Using several approach booleans simultaneously | More than one projectile implementation can run for one shot. |
| Using an undersized ring buffer | Active projectiles can be overwritten. |
| Tracking visuals only by buffer index | A reused slot can make an old visual read a new projectile. |
Iterating from zero to an ever-growing FireCount |
Simulation cost grows for the entire session. |
| Instantiating and destroying visuals for every bullet | Causes avoidable allocation and CPU spikes. |
| Using the last visual position as the exact impact point | The displayed effect may not match the authoritative collision. |
| Forgetting late-join initialization | Old projectiles can be recreated incorrectly. |
| Assuming visible overlap guarantees a hit | Authoritative simulation determines the gameplay result. |
| Using large uncompressed trajectory data without measurement | Buffer state can become unnecessarily large. |
Summary
Fusion projectiles can be implemented with different levels of network object synchronization.
The simplest approach spawns a NetworkObject with a NetworkTransform for every projectile. This is easy to implement but can produce visible proxy delay and significant overhead at high fire rates.
A data-driven NetworkObject improves the result by synchronizing only the initial trajectory data and reconstructing the projectile position from Fusion time.
The most scalable approach for predictable projectiles stores projectile records in a fixed Networked buffer and creates only local visual GameObjects. Collision and damage remain authoritative, while every client reconstructs the visual trajectory from shared data.
The correct choice depends on projectile count, lifetime, trajectory complexity, interaction requirements, and whether each projectile needs a persistent NetworkObject identity.
Last updated on
Back to top- Overview
- Video Timeline
- Hitscan Compared with Projectiles
- Initial Project Setup
- Creating Separate Projectile Input
- Buffering Fire Input
- SimpleWeaponController
- Projectile Approach Enum
- Approach 1: NetworkObject with NetworkTransform
- Spawning the Networked Projectile
- NetworkedProjectileEntity
- Swept Collision Detection
- Ignoring the Shooter
- Advantages of the NetworkTransform Approach
- Limitations of the NetworkTransform Approach
- When Approach 1 Is Appropriate
- Approach 2: Data-Driven NetworkObject
- Data-Driven Spawn Flow
- DataDrivenProjectile State
- Initializing the Trajectory
- Calculating Projectile Position
- Calculating Time from Ticks
- Collision Detection
- Rendering from Local and Remote Time
- Why Data-Driven Rendering Looks Better
- Advantages of Data-Driven NetworkObjects
- Limitations of Data-Driven NetworkObjects
- When Approach 2 Is Appropriate
- Approach 3: Visual-Only Projectiles with Buffered Data
- Core Principle
- ProjectileData
- NetworkArray Buffer
- Ring Buffer Index
- Firing a Buffered Projectile
- Fixed Buffer Capacity
- Buffer Capacity Example
- Simulating Active Projectiles
- Collision Processing
- Projectile Lifetime
- Visual Projectile Representation
- Detecting New Projectile Records
- Updating Visual Position
- Hiding Future Projectiles
- Projectile Rotation
- Deactivating Visuals
- Local Impact Effects
- Pooling Projectile Visuals
- Advantages of the Buffered Visual Approach
- Limitations of the Buffered Visual Approach
- Buffer Wraparound
- Processing Only Valid Buffer Entries
- Late Joiners
- Data Compression
- State Authority
- Input Authority and Shooter Identity
- Applying Damage
- Lag Compensation
- Gravity
- Non-Linear Projectiles
- Three Approaches Compared
- Choosing an Approach
- Use a NetworkTransform Projectile When
- Use a Data-Driven NetworkObject When
- Use Buffered Visual Projectiles When
- Hitscan May Still Be Better
- Recommended NetworkObject Workflow
- Recommended Data-Driven Workflow
- Recommended Buffered Workflow
- Key Concepts
- Best Practices
- Common Pitfalls
- Summary