Please note: development of Photon TrueSync & Thunder is ceased and we will not publish any further updates. While existings apps are not affected, we recommend to migrate apps that are still in development.

TrueSync Tutorial Part 3

Tutorial Contents

This tutorial series explains how to use Photon TrueSync to build a simple multiplayer game in Unity from the ground up. In parts 1 and 2, we showed basic setup and code to connect to photon, and how to create a physics-based, player-controlled game object whose movement is deterministic across different game clients.

What To Expect From Part 3:

A working multiplayer game client that can dynamically instantiate and destroy projectiles. Players can move their boxes around and shoot at each other, keeping a perfectly synchronized score over the internet.

Synchronous Prefab Instantiate/Destroy

Player prefabs are good as a starting point, but you might need to create objects in real time based on what happens in the game. In a deterministic lockstep simulation, the code must result the same at the end of every single frame, so TrueSync needs to control game objects instantiation and destruction as well.

As an example, we'll create a projectile prefab that will be instantiated every time a player presses the "Fire1" default input from Unity.

First, create a new TrueSync sphere (Game Object/TrueSync/Sphere) into the game scene and drag it to a new prefab inside the "Tutorial" folder.

Edit the prefab in the inspector, removing the TSRigidbody component, and making the TSSphereCollider a trigger, by checking the appropriate toggle (see figure bellow).

sphere inspector
Sphere component on inspector.

Rename the prefab and call it "ProjectilePrefab". Remember to always remove the game object from the scene and save it (we only need the prefabs).

Now create a new C# script and name it "PlayerWeapon.cs". Edit it and make it inherit from "TrueSyncBehavior", just like the one that controls player movement. Lets also add a reference to the projectile prefab we created previously in the new script, so the code looks like the one below.

C#

using UnityEngine;
using System.Collections;
using TrueSync;

public class PlayerWeapon : TrueSyncBehaviour {

    public GameObject projectilePrefab;

}

Attach this script to the "PlayerBox" player prefab that we created in the last part of the tutorial. Now we'll have two custom scripts to this prefab: one for movement, and this new one for the weapon control.

Drag the "ProjectilePrefab" from the project tab to the slot in the "PlayerWeapon" script. The inspector for the player prefab should now look like the following figure.

player weapon on inspector
Player weapon on inspector.

The "PlayerWeapon.cs" script will have a similar structure to that of "PlayerMovement.cs", so it will collect and queue fire input in the "OnSyncedInput" method, and shoot the weapon and update a cooldown variable in the "OnSyncedUpdate" method.

For input, simply queue a byte that tells when the player has pressed the "Fire1" button (queue zero when not pressing, one when pressing). Add the code bellow to "PlayerWeapon.cs".

C#

public override void OnSyncedInput() {
    if (Input.GetButton("Fire1"))
        TrueSyncInput.SetByte (2, 1);
    else
        TrueSyncInput.SetByte (2, 0);
}

Notice that we register this input value with the key 2 (two), since we already used 0 and 1 for the movement input values (accell and steer).

Now, the code bellow makes TrueSync instantiate a copy of the projectile prefab whenever the player pressed the fire button and the cooldown variable allows it to. Add it to the "PlayerWeapon.cs" script.

C#

private FP cooldown = 0;

public override void OnSyncedUpdate () {
    byte fire = TrueSyncInput.GetByte (2);
    if (fire == 1 && cooldown <= 0) {
        TrueSyncManager.SyncedInstantiate (projectilePrefab, tsTransform.position, TSQuaternion.identity);
        cooldown = 1;
    }
    cooldown -= TrueSyncManager.DeltaTime;
}

Notice that the "cooldown" variable is a FixedPoint number, and decremented by TrueSync's version of "DeltaTime", so the whole code is deterministic.

Moving the bullet

Now we need to make the projectiles move in the forward direction, and destroy it either when it hits an opponent player box or after some time. For this, we'll create a new C# script and call it "Projectile.cs". As with others, make it inherit from "TrueSyncBehaviour" as shown bellow.

C#

using UnityEngine;
using System.Collections;
using TrueSync;

public class Projectile : TrueSyncBehaviour {

}

Attach this script to the projectile prefab. Now, we need to add a few attributes to it:

  • speed: so we can set how fast we want the projectile to move (this needs to be a FixedPoint number);
  • direction: a TSVector to store the current forward direction, so the projectile will keep moving that way;
  • destroyTime (private): another FP to keep track of the time this projectile is still moving, so we can destroy it if some amount has passed;

The "Projectile.cs" script with these attributes and the movement code shall now look like this:

C#

using UnityEngine;
using System.Collections;
using TrueSync;

public class Projectile : TrueSyncBehaviour {

    public FP speed = 15;
    public TSVector direction;
    private FP destroyTime = 3;

    public override void OnSyncedUpdate () {
        if (destroyTime <= 0) {
            TrueSyncManager.SyncedDestroy (this.gameObject);
        }
        tsTransform.Translate (direction * speed * TrueSyncManager.DeltaTime);
        destroyTime -= TrueSyncManager.DeltaTime;
    }

}

We also need to upgrade the code that instantiates the projectile in "PlayerWeapon.cs" to look like this:

C#

public override void OnSyncedUpdate () {
    byte fire = TrueSyncInput.GetByte (2);
    if (fire == 1 && cooldown <= 0) {
        GameObject projectileObject = TrueSyncManager.SyncedInstantiate (projectilePrefab, tsTransform.position, TSQuaternion.identity);

        Projectile projectile = projectileObject.GetComponent<Projectile> ();
        projectile.direction = tsTransform.forward;
        projectile.owner; = owner;

        cooldown = 1;
    }
    cooldown -= TrueSyncManager.DeltaTime;
}

We now set the direction the projectile should move to, and also mark whose player (TrueSync's custom ownerID) this projectile belongs to (this will be used in the next section to identify targets of the projectile).

Physics Callbacks and Score

We need to have code that identifies when the projectile hits any player box and make it react accordingly. TrueSync's physics engine has callbacks that are similar to those of Unity, such as "OnSyncedCollision[Enter/Stay/Exit]" and "OnTrigger[Enter/Stay/Exit]".

We'll use "OnSyncedTriggerEnter" because we marked the projectile prefab collider as a trigger. Add the following code to "Projectile.cs".

C#

public void OnSyncedTriggerEnter(TSCollision other) {
    if (other.gameObject.tag == "Player") {
        PlayerMovement hitPlayer = other.gameObject.GetComponent<PlayerMovement> ();
        if (hitPlayer.owner != owner) {
            TrueSyncManager.SyncedDestroy (this.gameObject);
            hitPlayer.Respawn ();
        }
    }
}

The code above will only affect other TrueSync physics objects that have the "Player" tag set, and a different owner, meaning only enemy player boxes will be hit.

Notice that besides destroying the projectile, we also get a reference to the "PlayerMovement.cs" script of the box we hit and call a new method there ("Respawn").

This method, shown bellow, respawns the hit player box randomly somewhere and increments the number of deaths it got. Add this to "PlayerMovement.cs".

C#

public int deaths = 0;

public override void OnSyncedStart () {
    tsTransform.position = new TSVector (TSRandom.Range(-5,5), 0, TSRandom.Range(-5,5));
}

public void Respawn() {
    tsTransform.position = new TSVector (TSRandom.Range(-5,5), 0, TSRandom.Range(-5,5));
    deaths++;
}

We also added the same random spawn code to the "OnSyncedStart" callback, so every player will be positioned in a different place at the start.

Notice that a TrueSync game relies on all computation running exactly the same in all computers, as nothing is being synchronized through the network itself, except the input values from the players.

To make sure you can see the score of the game, add the following GUI code to "PlayerMovement.cs".

C#

void OnGUI() {
    GUI.Label (new Rect(10, 100 + 30 * owner.Id, 300, 30), "player: " + owner.Id + ", deaths: " + deaths);
}

Testing

Please, make sure both scenes (Tutorial/Menu and Tutorial/Game) are included into the build settings, and that Tutorial/Menu is the first one (the one Unity will load at start up).

Create a build of your choice and run two copies of it, or one copy and run the Menu scene from inside the Unity editor.

You should now see both clients connecting to Photon, and then loading the game scene, where each client controls its own box and can shoot at each other.

Now let's move on to Part 4 to learn how to hide latency from players with rollbacks.

Back to top