This document is about: FUSION 2
SWITCH TO

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.

How to Handle Projectiles in Photon Fusion 2 (Shared Mode)

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:

  1. NetworkObject with NetworkTransform
    Spawn every projectile as a networked object and synchronize its transform.
  2. Data-driven NetworkObject
    Spawn a networked projectile, but synchronize only its initial trajectory data and reconstruct its position locally.
  3. 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:

  1. Import or open the Shared Mode starter.
  2. Add the project App ID from the Photon Dashboard.
  3. Verify that players can connect.
  4. Confirm that movement and the existing shooter logic work.
  5. 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:

  1. Read the local button in Unity Update().
  2. Store the request in the player input structure.
  3. Submit the input to Fusion.
  4. 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:

  1. A persistent NetworkBehaviour stores projectile records.
  2. The records are synchronized through a NetworkArray.
  3. Every client creates local visual objects.
  4. Simulation collision logic uses the shared projectile data.
  5. 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:

  1. Calculate the current tick position.
  2. Calculate the next tick position.
  3. Raycast between them.
  4. Apply gameplay effects if a collision occurs.
  5. Mark the record inactive.
  6. 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:

  1. Optionally create an impact effect.
  2. Destroy or return the visual to a pool.
  3. 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:

  1. Get an inactive projectile visual.
  2. Reset its transform and effects.
  3. Activate it.
  4. 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:

  1. Authority simulates the projectile.
  2. Authority performs the collision query.
  3. Authority validates the target.
  4. Authority applies damage.
  5. Authority marks the projectile inactive.
  6. 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.

  1. Create a projectile prefab.
  2. Add NetworkObject.
  3. Add NetworkTransform.
  4. Add the projectile behavior.
  5. Spawn it through Runner.Spawn.
  6. Assign the correct authority.
  7. Move it during FixedUpdateNetwork().
  8. perform swept collision checks.
  9. Despawn it on hit or expiration.
  10. Test proxy delay under simulated latency.
  1. Create a network projectile without continuous transform synchronization.
  2. Store fire tick, position, and velocity.
  3. Initialize data during spawning.
  4. Reconstruct position from Fusion time.
  5. Simulate collision between consecutive ticks.
  6. Render authority using local render time.
  7. Render proxies using remote render time.
  8. Despawn after collision or lifetime expiration.
  1. Create a persistent projectile controller.
  2. Define a compact INetworkStruct.
  3. Store entries in a fixed-capacity NetworkArray.
  4. Track a monotonically increasing fire sequence.
  5. Add records only from State Authority.
  6. Simulate active records during FixedUpdateNetwork().
  7. Apply gameplay effects authoritatively.
  8. Render local visuals from the synchronized records.
  9. Pool visual GameObjects.
  10. Validate ring-buffer sequences before using slots.
  11. Bound iteration by buffer capacity.
  12. 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