Tanknarok
Overview
The Fusion Tanknarok sample illustrates how to build a small multiplayer arena-style Tank game running in either Hosted Mode
, with an authoritative server (be that a standalone application or a single application running both the server and a client) or in Shared Mode
where each client has authority over its own objects and one client controls "shared" objects.
The game in this example may appear familiar from Steam as the gameplay logic, audio and wonderful graphics were provided by our friends at Polyblock Studios. This sample is just a small slice of the actual game ported to Photon Fusion; the original game was made using Photon Bolt.
The Fusion Tanknarok sample shows how physics-like effects can be achieved without the complications resulting from mixing a predictive network system with an non-deterministic rigidbody physics engine such as PhysX; Fusion fully supports synchronizing Unity rigidbodies if there is a need for it.
Before You Start
Create a new Unity project with the 3D template, make sure to set color space to "Linear" in
Project Settings > Player > Other Settings > Color Space
.
Before downloading and importing the sample, ensure the Unity Post Processing package is included in the project.
- Navigate to
Window > Package Manager
; - Select
Packages: Unity Registry
; - Search for "Post Processing"; and,
- Install the package.
Screenshots
Download
Version | Release Date | Download | ||
---|---|---|---|---|
1.1.7 | Jun 28, 2023 | Fusion Tanknarok 1.1.7 Build 202 |
Highlights
- Shared and Hosted mode support
- Lag Compensated Raycasts
- Predictive Spawning
- Object Pooling
- Complete Game Loop
Project
Before running the demo, a Fusion App Id for the Photon Cloud has to be created and copy-pasted into the PhotonAppSettings
asset. App Ids can be created from the Photon Dashboard. Make sure to create a Fusion App Id, not just a Realtime Id.
The photon app settings assets can be selected from the Fusion menu Fusion > Realtime Settings
Simply paste the generated App Id into the App Id Fusion
field.
Folder Structure
The code for the Tanknarok derived sample is in the /Scripts
folder, with a Utility
sub folder for general purpose utilities and a FusionHelpers
folder for Fusion utilities that are not specific to this example.
The remaining Tanknarok
folder contains the actual game code separated into the following sub folders:
- Audio - Sound Effects and Music
- Camera - Camera placement code
- Level - All level logic and behaviours, powerups, and other non player items
- Player - All tank controls, weapons and bullet logic as well as tank visualisation and effects
- UI - User interface components
In the main folder the main entry point is the GameLauncher
class and the top level managers GameManager
, LevelManager
and PlayerManager
.
Quick Fusion Primer
Fusion identifies network state by the NetworkObject
component; any gameobject with a networked state must also have a NetworkObject
on it. The NetworkObject
itself simply assigns a network-wide identity to the gameobject, actual network state is stored in components derived from NetworkBehaviour
. There are several default behaviours included with Fusion, for example NetworkTransform
which synchronizes the Unity Transform.
Similarly to how physics behaviours transform the physics state in FixedUpdate()
, NetworkBehaviour
's transform their network state in the FixedUpdateNetwork()
method. This happens independently of the rendering framerate and independently of updates from the network, at a fixed time step referred to as a tick. Each update will simply work off of the state from the previous tick. When the state of a given tick is confirmed by the network, Fusion will roll back the object state to that tick and re-apply all intermediate calls to FixedUpdateNetwork()
between then and the current tick.
Because the current local tick is always ahead of the last confirmed tick, the update is referred to as a "Prediction" and the application of a verified state and subsequent re-execution of the FixedUpdateNetwork
method is referred to as "Rollback" and "Resimulation".
It is possible for a component to be part of the simulation without having any network state, but it should then derive from SimulationBehaviour
instead of NetworkBehaviour
to reduce overhead. Note that because of re-simulation, the FixedUpdateNetwork()
method may be called many times per frame - this is not an issue when working exclusively with network state because this gets reset, but be careful when applying delta changes to non networked state.
For more information about simulation, prediction and network objects, consult the Fusion Manual.
GameLauncher
The main UI of the Tank game is handled by the GameLauncher
class.
Once a game mode has been selected, the GameLauncher will call FusionLauncher.Launch()
to establish a session. FusionLauncher
will respond to Fusion connection events and call the provided callbacks in order to spawn the initial network objects:
- GameManager (Spawned by the host in hosted mode, or the master client in shared mode)
- Player (Spawned by the host in hosted mode, or the respective client in shared mode)
Ready Up
Player tanks spawn immediately when a player connects and are fully controllable in a "lobby" mode where they can play around while waiting for the remaining tanks to spawn.
The game itself does not start until all connected players indicate that they are ready. This logic runs on all clients, but only the client that has StateAuthority
of the GameManager
instance may load the level.
N.B.: In this simplified example, the level is not so much "loaded" as "enabled" since both levels are in the initial scene from the start.
Loading is done with a Remote Procedure Call. The caller generates the random level index and pass that to all clients, ensuring that everyone will load the same level.
C#
if (Object.HasStateAuthority) {
RPC_ScoreAndLoad(-1,0, _levelManager.GetRandomLevelIndex());
}
The RPC itself is defined as
C#
[Rpc(sources: RpcSources.StateAuthority, targets: RpcTargets.All, InvokeLocal = true, Channel = RpcChannel.Reliable)]
private void RPC_ScoreAndLoad(int winningPlayerIndex, byte winningPlayerScore, int nextLevelIndex)
{
...
}
Level Transition
The transition from lobby to level and vice-versa is handled by the LevelManager
in the TransitionSequence()
co-routine. The transition itself runs entirely on local time but synchronize across clients when it is triggered (by the RPC_ScoreAndLoad
Remote Procedure Call) as well as when it terminates (by setting the playState
to LEVEL
, since this is a Networked property that can only be set by the GameManager
State Authority).
At the end of the game, the level transition will return to the lobby and show the winner in much the same way, completing the loop and returning the game to the Ready Up state.
Handling Input
Fusion captures player input using Unity's standard input handling mechanism, stores it in a data structure that can be sent across the Network, and then work off of this data structure in the FixedUpdateNetwork()
method. In this example, all of this is implemented by the InputController
class, though it hands off the actual state changes to the Player
class.
Shooting
The Tanks in this example has 4 different weapons with two distinct types of hit detection:
- Instant Hit
- Projectile
Implemented by the HitScan
and Bullet
classes, respectively. Both uses Object Pooling, Predictive Spawning and Lag Compensation, three important Fusion features.
Object Pooling
To avoid frame drops when spawning new objects it is advisable to re-use old objects instead of destroying and instantiating new objects all the time. This is true for any game, Unity in particular, and even for Fusion.
To facilitate this, Fusion allows the application to specify a hook for providing and collecting recycled gameobjects.
The object pool has to implement NetworkObjectPool
which basically has a method that takes an object from the pool based on a prefab, and another that returns an object to the pool for re-use.
Predictive Spawning
Predictive Spawning allows a client to predict the creation of a new networked object, creating a temporary local placeholder, until the creation has been confirmed by the state authority. Fusion will automatically promote the placeholder to an actual networked object once confirmed.
What does need to be handled manually are failed predictions. This can be as simple as destroying the placeholder (remember, it is just a Unity object); the application is free to implement various forms of fade-out or failure visualisations.
Also, since the placeholder has no state, the application will need to manage movement during the predicted stage in a way that does not involve accessing any networked properties.
Lag Compensation
Locally, each player sees a predicted future version of their own (Input Authority) objects and inter- or extra-polated versions of other clients' objects; neither of which being exactly aligned with what the server sees. Therefore a fast moving object, e.g. a bullet, will very likely hit different things on each machine. The person who fired the bullet is the one most likely to notice if something is off. However, at the same time, the server should have authority over hit detection to avoid a rouge client simply deciding they have scored a successful hit.
To solve this problem, Fusion supports lag compensated ray casts. These are essentially ray casts that, even when running on the server, will respect what the client saw at the time the shot was taken. There is obviously a lot of snapshot interpolation magic going on behind the scenes to make this work; luckily for devs, implementing and using lag compensated ray casts is every bit as simple as using a regular Unity ray cast.
The only thing one needs to know is that it comes with its own type of collider objects called a HitBox
. The HitBox
should be a sibling or a child of a fully encompassing HitBoxRoot
node in the object hierarchy. This allow Fusion to do a quick dismissal of roots before performing a more expensive check on the child nodes.
A HitBox
should not be applied to static entities for performance reasons. Static environments are still needed to block ray casts though, thus the lag compensated ray casts may optionally check for Unity colliders as well.
The caveat is that lag compensation does not work for dynamic -i.e. moving- PhysX colliders; furthermore, Unity does not provide a ray cast query which makes a distinction between static and dynamic colliders. Therefore the recommendation, to filter the PhysX colliders and have only the static results, is is to have HitBox
/HitBoxRoot
and PhysX Collider
(if both are required) on different layers of a dynamic object .