Fusion Projectiles

Level Intermediate

Overview

Fusion Projectiles demonstrates how to implement networking for different types of projectiles in a shooter game. Programming multiplayer projectiles can be a difficult task which involves balancing performance, bandwidth consumption, and precision. It also requires smooth rendering which accounts for any discrepancy between the simulated and rendered projectile path (fire from camera vs. fire from weapon barrel). Fusion Projectiles aims to clarify and simplify this task a little bit.

To contextualize the projectiles, the sample is built as a simple FPS game with other supporting systems (gameplay handling, health and damage system, weapons) and can be used as a solid groundwork for building shooter games.

This sample uses the HostMode topology.

Back To Top

Features

  • Handling of projectiles without individual NetworkObjects using projectile data ring buffer
  • Numerous projectile types from hitscan to homing projectiles
  • Weapon component system
  • 7 different weapons
    • Pulse Gun
    • Rifle
    • Shotgun
    • Laser Gun
    • Sniper
    • Homing Gun
    • Ricochet Gun
  • Solved interpolation from weapon barrel to real projectile path (shot from camera)
  • Health and damage system
  • Explosions
  • Predictive spawning of projectiles

Back To Top

Before You Start

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.

Back To Top

Download

Version Release Date Download
1.1.2 Sep 08, 2022 Fusion Projectiles 1.1.2 Build 10

Back To Top

Choosing The Right Approach

Before diving into the project, let’s discuss some of the various options available for networking projectiles in Fusion.

Back To Top

A) NetworkObject With NetworkTransform/NetworkRigidbody

Spawn projectile as a NetworkObject, use NetworkTransform or NetworkRigidbody to synchronize position.

  • ✅ Simple solution
  • ✅ 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
  • ❌ Handling of predicted spawn if needed

Bottom Line: Suitable only for small amounts of projectiles in certain situations.

Back To Top

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
  • ❌ Handling of predicted spawn if needed
  • ❌ Manual interest management handling

Bottom Line: Suitable only for small amounts of projectiles in certain situations, usually better than A.

Back To Top

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.

  • ✅ 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 need for predictive spawning
  • ❌ Imprecise on proxies. Proxies are in snapshot interpolated position, thus their fire position/direction can be quite different.
  • ❌ Possible only for simplest forward heading projectiles since no additional data is available
  • ❌ Projectile travel is unknown on proxies thus projectile visuals will travel through walls and other objects

Bottom Line: 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).

Back To Top

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 need for predictive spawning
  • ❌ Projectiles cannot exist without the controlling object = cannot outlive it
  • ❌ Projectile physics like bouncing is only manually calculated

Bottom Line: Very good solution for most situations except for very long-living projectiles (that can outlive the owner) or very long-traveling and important projectiles (due to interest management).


This sample focuses mainly on solution D - usage of projectile data ring buffer (see Projectile Manager section) but it also contains an example of approach B (see Standalone Projectile).

For more examples of projectiles spawned as NetworkedObject, see the Tanknarok sample.

Back To Top

Starting The Game

Debug Start

Start a game by opening and playing the Game scene (Scenes/Game). 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.

Debug Start

Back To Top

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).

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.

Runner Visibility Controls

Note: For multipeer mode to work properly it is essential to avoid referencing scene objects through static fields (access to UI, camera rig, player data etc.) but avoiding statics is a good and recommended practice in general as well. Check the Scene and SceneContext section for one solution to how common objects can be accessed throughout your codebase.

Back To Top

Controls

Use W, S, A, D for movement, Mouse1 for fire and Mouse2 for alternative fire if available. Weapons can be switched with the mouse scroll wheel or by pressing the appropriate alpha number key. Check the ingame UI for information about the selected weapon and types of projectiles it shoots.

Use the ENTER key to lock or release your cursor.

First Play

Back To Top

Project Organization

Prefab Type Location
Player and Agent /Prefabs
Weapons /Prefabs/Weapons
Projectiles /Prefabs/Projectiles

Back To Top

Player

Player (Player script, Player prefab) represents a connected peer in the game and has no visuals. Use Player to sync statistics, nickname, selected hero, their desire to join gameplay etc. Player also handles input.

Agent (PlayerAgent script, Agent prefab) represents an ingame character that is controlled by the player, is spawned by Gameplay, has Health, Weapons and other usual stuff. Can be spawned and despawned as needed.

Photon Dummy

Back To Top

Projectile Manager

ProjectileManager is the core of the Fusion Projectiles sample. It handles the projectile data buffer and creation/update/deletion of the visual representation of the projectiles. Projectile manager lives on the controlling object (e.g. Agent, Turret) and is responsible for updating projectiles from all weapons owned by that object.

Projectile Manager

Projectile manager requires projectile prefabs array to correctl#y pair projectile data to specific prefab. For simplicity projectile manager has references to prefabs directly in the inspector.

Note: Better solution of accessing prefabs array will be game specific but it could be for example a scriptable object with settings data.

ProjectileManager stores ProjectileData that is used to calculate projectile trajectory and other projectile behavior:

[StructLayout(LayoutKind.Explicit)]
public struct ProjectileData : INetworkStruct
{
    public bool    IsActive          { get { return _state.IsBitSet(0); } set { _state.SetBit(0, value); } }
    public bool    IsFinished        { get { return _state.IsBitSet(1); } set { _state.SetBit(1, value); } }

    [FieldOffset(0)]
    private byte   _state;

    [FieldOffset(1)]
    public byte    PrefabId;
    [FieldOffset(2)]
    public byte    WeaponAction;
    [FieldOffset(3)]
    public int     FireTick;
    [FieldOffset(7)]
    public Vector3 FirePosition;
    [FieldOffset(19)]
    public Vector3 FireVelocity;
    [FieldOffset(31)]
    public Vector3 ImpactPosition;
    [FieldOffset(43)]
    public Vector3 ImpactNormal;

    // Custom projectile data

    [FieldOffset(55)]
    public HomingData    Homing;
    [FieldOffset(55)]
    public KinematicData Kinematic;

    public struct HomingData : INetworkStruct
    {
        public NetworkId Target;
        public Vector3   TargetPosition; // Used for position prediction
        public Vector3   Position;
        public Vector3   Direction;
    }

    public struct KinematicData : INetworkStruct
    {
        public NetworkBool HasStopped;
        public Vector3     FinishedPosition;
        public int         StartTick;
        public byte        BounceCount;
    }
}

This structure is currently quite large due to the number of projectile features being demonstrated in this project. Also, since there are multiple different projectile behaviors being demonstrated which do not use the same data values, the data struct uses union of specific projectile data.

In actual practice, it is recommended to keep this ProjectileData structure as minimalistic as possible. For most games fewer features will be needed than are demonstrated here. Check the commented simplified version of the ProjectileData:

public struct ProjectileData : INetworkStruct
{
    public bool    IsActive          { get { return _state.IsBitSet(0); } set { _state.SetBit(0, value); } }
    public bool    IsFinished        { get { return _state.IsBitSet(1); } set { _state.SetBit(1, value); } }

    private byte   _state;

    public byte    PrefabId;
    public byte    WeaponAction;
    public int     FireTick;
    public Vector3 FirePosition;
    public Vector3 FireVelocity;
    [Networked, Accuracy(0.01f)]
    public Vector3 ImpactPosition { get; set; }
    [Networked, Accuracy(0.01f)]
    public Vector3 ImpactNormal { get; set; }
}
Note: Usage of union (= StructLayout(LayoutKind.Explicit) attribute) prevents having properties inside the struct and thus prevents usage of Networked and Accuracy attributes. See how without the union properties could be used and the Accuracy attribute for ImpactPosition and ImpactNormal to save some bandwidth.

Back To Top

Projectiles

Projectile script (prefab) has two usages:

1) Implements GetFireData and OnFixedUpdate methods which are used to generate and manipulate ProjectileData. These methods are called from FixedUpdateNetwork on both the State Authority and the Input Authority, and are called directly on prefabs not on prefab instances.

public abstract ProjectileData GetFireData(NetworkRunner runner, Vector3 firePosition, Vector3 fireDirection);
public abstract void OnFixedUpdate(ProjectileContext context, ref ProjectileData data);
Note: Projectile is not a NetworkBehaviour and is not associated with a NetworkObject. So all network data used in GetFiredData or OnFixedUpdate must be included in the ProjectileData struct.

2) Projectile instances are also used as a visual representation of the projectile. This allows for shared FUN(simulation) and Render functionality such as movement code. Methods that are executed for projectile instances are Activate, OnRender and Deactivate and are purely visual.

public void Activate(ProjectileContext context, ref ProjectileData data)
{
    PrefabId = data.PrefabId;
    IsFinished = false;

    OnActivated(context, ref data);
}

public void Deactivate(ProjectileContext context)
{
    IsFinished = true;

    OnDeactivated(context);
}

public virtual void OnRender(ProjectileContext context, ref ProjectileData data)
{
}

public virtual void Discard()
{
    IsFinished = true;
}

It is also possible to override the Discard method (e.g. to play dissolve effect) for custom behavior for missed predictions, when projectile wasn’t fired on the server.

Every projectile can spawn visual effects on impact as well as a new NetworkObject (e.g. explosion).

Back To Top

Projectile Types

Fusion Projectiles include several projectile types which may be used in many common scenarios.

Hitscan Projectile

Also known as Raycast or Instant projectiles

Raycast with specified max distance which fires immediately on fire input, and hits are immediately evaluated.

InstantHitscanProjectile
Visual trail can be displayed all the way from weapon barrel to target position. Trail disappears in time.
Example weapon: Sniper (disappearing trail), Shotgun (instant visuals)

Sniper
Hitscan projectile with trail from barrel to impact

FlyingHitscanProjectile
Hit is processed immediately but there is a dummy flying projectile that travels with specified speed to the target position.
Example weapon: Rifle

Rifle
Hitscan projectile with flying dummy bullets

Back To Top

Kinematic Projectile

Also known as just Projectile

Projectile that travels over time, executing short raycasts between the previous position and new position every simulation tick.

SimpleKinematicProjectile
Kinematic projectile flying in a straight line.
Example weapon: Pulse Gun

Pulse Gun
Kinematic projectile

AdvancedKinematicProjectile
Kinematic projectile that can be influenced by gravity and bounce off walls and other objects. Multiple projectile behaviors can be managed by this projectile type:

  • Falling projectile with explosion on touch
    Example weapon: Pulse Gun (alternative fire - Grenade)

  • Falling and bouncing projectile with explosion after some time
    Example weapon: Rifle (alternative fire - Grenade)

  • Straight bounce projectile
    Example weapon: Ricochet Gun

Ricochet Gun
Ricocheting projectile of the Ricochet Gun

HomingKinematicProjectile
Projectile that actively turns to hit the target. It can predict target position to hit moving targets. Possibility to specify what body part should be targeted, properties of the turn and target seek behavior.
Example weapon: Homing Gun (primary fire for fast homing projectile, secondary fire - Rocket - for slower retargeting projectile)

Homing Gun
Homing projectiles of the Homing Gun

Back To Top

Ray

Also known as Beam or Laser

Constant ray which does damage over time. Ray is implemented using the weapon component WeaponBeam (more in Weapon Components section), it isn’t really a projectile in this project.
Example weapon: Laser Gun

Laser Gun
Laser ray

Back To Top

Standalone Projectile

Projectile that can be spawned as a standalone network object. Should be used only for some special circumstances. See also Choosing The Right Approach section.

DataStandaloneProjectile
Wrapper for standard projectile functionality. Creates and updates ProjectileData in a similar fashion as ProjectileManager, just does it only for one projectile. Can be spawned predictively.
Example weapon: Ricochet Gun (alternative fire - Bouncing Drop)

Back To Top

Projectiles Interpolation

When firing projectiles in first person games one can choose to do the projectile calculations directly from the weapon barrel (realistic approach) or from center of the camera (standard approach).

Note: Realistic approach is used in a few (hard core) games. It makes firing feel more realistic, but also less stable (fire position is dependent on player animations) and prone to unwanted hits (corner issue) because the weapon is offset from the camera.

Fusion Projectiles uses the more standard approach of firing from the camera center. This method however creates a discrepancy between the simulated path (originating at the camera), and the rendered path (originating from the weapon barrel). This is solved by interpolating projectile position over time from the weapon barrel to the real projectile path. Interpolation values (interpolation time, interpolation ease) can be setted up on all kinematic projectiles.

Projectile interpolation
Projectile interpolation from the top-down view

Kinematic projectile setup
Kinematic projectile setup

Note: Projectile interpolation is needed only for kinematic projectiles. For hitscan projectiles no interpolation is needed because we already know where the projectile will impact so the dummy flying visuals can be moving directly to that point.

Back To Top

Weapons

Overview

Fusion Projectiles contain a small weapon handling system. It is a basic implementation which only handles weapon switching. For more elaborate usages like weapon drop, pickup, reload, recoil patterns, dynamic dispersion and other check Fusion BR sample.

To manage all kinds of weapons however, a convenient weapon action-component system is included.

Back To Top

Weapon Actions

A weapon action is a set of weapon components that represent a single weapon action. One weapon action is for example a standard fire and other action is an alternative fire. Weapons can have various weapon actions.

Weapon Action
Primary weapon action on Pulse Gun with all necessary weapon components

Back To Top

Weapon Components

The Weapon component represents part of the weapon that has its own logic and can be reused in different scenarios. Weapons can be assembled from multiple components to form the desired functionality. Fusion Projectiles comes with several weapon components:

  • WeaponMagazine - Provides ammo
  • WeaponTrigger - Says when the weapon should fire (checks player input, controls weapon cadence)
  • WeaponBarrel - Fires the projectile
  • WeaponFireEffect - Shows muzzle, plays fire sound, applies knockback, starts camera shake
  • WeaponBeam - Fires continuous beam

Weapon components are loosely coupled through WeaponDesires struct.

Note: When building various weapons the idea is to just swap one or more components while reusing the others. One example is Laser Gun where the WeaponBarrel component is swapped for WeaponBeam - magazine still provides ammo, trigger still tells when to fire, just the fire effect is different. Another example would be a minigun where it takes time for the rotating barrel to gain speed before fire starts - in this instance, a DelayedTrigger component could be created to replace the standard trigger.

Back To Top

Shake Effect

ShakeEffect utility is used for driving camera shake (GameplayScene/SceneCamera/Shaker object in Game scene) in response to weapon fire. The effect is triggered from the WeaponFireEffects component, where position and rotation shake ranges can be set. The ShakeEffect utility supports stacking of different camera shakes and continuous shaking (continuous fire).

Camera shake
Camera shake setup

Back To Top

Health And Damage System

For hit synchronization an approach similar to projectile data is used. Hits are stored in a small networked circular buffer of Hit structures.

public struct Hit : INetworkStruct
{
    public EHitAction Action;
    public float      Damage;
    [Networked, Accuracy(0.01f)]
    public Vector3    RelativePosition { get; set; }
    [Networked, Accuracy(0.1f)]
    public Vector3    Direction { get; set; }
    public PlayerRef  Instigator;
}

From the Hit struct the appropriate hit reactions can be constructed - e.g. playing hit animation, showing hit direction in UI, showing hit confirmation and numbers to damage instigator, spawn of blood effect, etc.

Note: Notice how RelativePosition of the hit is stored in Hit data instead of an absolute position. Since position of the proxies is interpolated between last two received ticks from the server, relative position is better to place hit effects such as blood splash correctly onto the body.

Back To Top

Gameplay Elements

Explosion

Simple spawned Network Object that deals damage within a certain radius.

Explosion

Back To Top

Damage Area

Damage is applied to all targets inside this area over time. Use it to test incoming damage.

Damage Area

Back To Top

Ragdoll

Agent death animation is for simplicity handled just by enabling rigidbody physics when the agent dies. Check the SimpleAgentRagdoll script.

Ragdoll

Back To Top

Turret

SimpleTurret is used to spawn standalone projectiles in some intervals. There is no search for target logic nor rotation toward a target. Used for testing weapons outside of an Agent prefab.

Turret

Back To Top

Game Core

GameManager

Handles joining and leaving of connected players and spawns Player prefab. For more elaborate handling of connected players with saving of player data for reconnection refer to the Fusion BR sample.

Back To Top

Gameplay

Gameplay is responsible for spawning/despawning Agent prefabs. This base implementation acts as an endless deathmatch. Can be inherited from and extended to add different gameplay functionality such as proper deathmatch, capture the flag, elimination etc.

Back To Top

Pooling

In projects that involve projectiles, a large amount of object spawning is expected (projectiles, muzzle effect, impacts, hit effects,...). Since instantiation of a new object is an expensive operation everything in Fusion Projectiles is pooled.

There are two types of pool:

  • NetworkObjectPool is used to pool the NetworkObjects. For network object pool to work it needs to implement the INetworkObjectPool interface and be assigned as a starting parameter when NetworkRunner is started (this happens in CustomNetworkDebugStart script). For more information check out Network Object Pool section in the Fusion Manual.

  • ObjectCache is a generic GameObject pool that can be used for non-networked objects such as impact effects. It has a handy feature to return objects with a specified delay.

Back To Top

Scene And SceneContext

Scene handles scene specific functionality and services (SceneService) such as UI, camera, music, minimap and others. Scene services are updated manually from here so they can be initialized, activated, deactivated and updated at specific times needed for the project.

SceneContext provides safe access to common services or other information that are needed across the codebase without the use of statics. Scene context is passed to scene services automatically and is also assigned to networked objects in GameplayScene and NetworkObjectPool. Inherit from ContextBehaviour and ContextSimulationBehaviour instead of NetworkBehaviour and SimulationBehaviour if access to the SceneContext is needed.

Back To Top

Expanding Project

Adding New Weapon

1) Create a new prefab or duplicate an existing weapon.

2) Make sure that there is at least one WeaponAction component and appropriate weapon components on the prefab. WeaponAction and weapon components can be all on one GameObject (see Sniper prefab) or in case of multiple weapon actions it needs to be placed in hierarchy (see Rifle prefab).

3) Set the WeaponSlot in the Weapon component to specify which place it should occupy in the player weapons array - Slot 1 is the first visible weapon.

4) Optionally assign weapon name and icon in the Weapon component.

5) Assign weapon to Initial Weapons field in the Weapons component on the Agent prefab.

Weapons inspector

Back To Top

Adding New Projectile

1) Create a new prefab or duplicate existing projectile. Make sure there is a component that inherits from Projectile.

2) Assign projectile prefab to weapon prefab in WeaponBarrel component.

WeaponBarrel inspector

3) Assign projectile prefab in the ProjectileManager on the controlling object (e.g. the Agent prefab) to make sure it will spawn the projectile when fired.

Back To Top

Switch To Third Person

Fusion Projectiles is built as an FPS game but most of the functionality applies for TPS games as well. When firing in TPS games, first a ray needs to be cast from the camera to find the point at which the player is trying to shoot. The actual projectile cast will then be from a position on character (fixed position near character shoulder usually works well) to the point obtained from the camera cast. There are usually some tricks (like ignoring some collisions with second cast) involved so players won’t be hitting unwanted objects too much (corners issue). Check out Fusion BR for a TPS handling example.


To Document Top