This document is about: QUANTUM 2
SWITCH TO

Overview

Introduction

Quantum is agnostic to the concept of player entities. All entities are the same in the eyes of the simulation. Therefore, when we refer to "the player" in this document, we mean the player controlled entity.

Player Identification

A player can be identified in two ways:

  • their player index; and,
  • their PlayerRef.

Player Index Assignment

The Quantum player index is assigned by the server based on the order in which the Session.Join() messages arrive. This is not to be confused with the Photon Id which is based on order in players joined the Photon room.
It is not possible to set the "Desired Quantum Id" on a Photon Player.

N.B.: In the event of a disconnect, we guarantee the client gets the same player index IF it reconnects with the same ClientId; regardless of their Photon Id - public static QuantumRunner StartGame(String clientId, Int32 playerCount, StartParameters param).

Player Index vs PlayerRef

The PlayerRef is a wrapper for the player index in the Quantum ECS. The PlayerRef is 1-based, while player index starts at 0. The reason is that default(PlayerRef) will return a "null/invalid" player ref struct for convenience.

There are automatic cast operators that can cast an Integer to a PlayerRef and vice-versa.

  • default(PlayerRef), internally a 0, means NOBODY
  • PlayerRef, internally 1, is the same as player index 0
  • PlayerRef, internally 2, is the same as player index 1

Photon Id

You can identify a player's corresponding Photon Id via the Frame API:

  • Frame.PlayerToActorId(PlayerRef player) converts a Quantum PlayerRef to an ActorId (Photon client id); or,
  • Frame.ActorIdToAllPlayers(Int32 actorId) the reverse process of the previous method.

Use this if you plan on showing player names via PhotonPlayer.Nickname for example.

IMPORTANT: The Photon Id is irrelevant to the Quantum simulation.

Join the Game

When a game starts, the followings things happen in sequence:

  1. QuantumRunner.Session.Join() sends join request to server with desired player count.
  2. The request is received by the server where it is validated and a confirmation is sent back to the user. If the information attached to the request is not valid, the request will be refused.
  3. The Start game message is received by the client.
  4. The player can now send and receive inputs.
  5. (OPTIONAL) - In case of a late join, the client may receive a snapshot-resync; in this case step 4 would not be sending inputs while waiting for the snapshot.
  6. (OPTIONAL) - SendPlayerData can now be used. It may be used as many times as needed during the game session (at game start and/or during the session itself). Every time SendPlayerData is called, it sends a serialized version of RuntimePlayer to the server, which then attaches to a tick input set confirmation and thus deterministically triggers the signal.
Config Sequence Diagram
Sequence Diagram

For more information on the configuration files involved, please refer to the Configuration Files document of the manual.

SendPlayerData

QuantumGame.SendPlayerData(RuntimePlayer features) is a data path to deterministically inject a special kind of data (serialized RuntimePlayer) into the input stream. Although SendPlayerData is commonly called at game start to set up all the player; it is also possible to call it during the game session if the data needs to be updated.

After starting, joining the Quantum Game the CallbackGameStarted callback fires. It is at this moment that each player may call the SendPlayerData method to be added as a player in everyone else's simulation. Calling this explicitly greatly simplifies the process for late-joining players.

C#

public class MyCallbacks : MonoBehaviour {
  private void OnEnable() {
    QuantumCallback.Subscribe<CallbackGameStarted>(this, OnGameStart);
  }

  private void OnGameStart(CallbackGameStarted callback) {
    // paused on Start means waiting for Snapshot
    if (callback.Game.Session.IsPaused) return;

  // It needs to be sent for each local player.
    foreach (var lp in callback.Game.GetLocalPlayers()) {
      Debug.Log("CustomCallbacks - sending player: " + lp);
      callback.Game.SendPlayerData(lp, new Quantum.RuntimePlayer { });
    }
  }
}

Player entities are instantiated in local mode but not in multiplayer mode

Most likely QuantumGame.SendPlayerData() was not executed for each player. If you are using the demo menus to start the game add the script CustomCallbacks.cs anywhere to the menu scene.

PlayerConnectedSystem

To keep track of players' connection to a quantum session Input & Connection Flags are used. The PlayerConnectedSystem automates the procedure and notifies the simulation if a player has connected to the session or disconnected from it. To make use of the system, it has to be added to the SystemSetup.

C#

public static class SystemSetup {
  public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) {
    return new SystemBase[] {
      // pre-defined core systems
      ...
  
      new PlayerConnectedSystem(),
      
      // custom systems
      ...
    }
  }
}

In order to receive the connection and disconnection callbacks the ISignalOnPlayerConnected and ISignalOnPlayerDisconnected have to be implemented in a system.

ISignalOnPlayerDataSet

Implementing ISignalOnPlayerDataSet in a system give you access to public void OnPlayerDataSet(Frame f, PlayerRef playerRef). OnPlayerDataSet is called every time a serialized RuntimePlayer is part of a specific tick input.

RuntimePlayer

The class RuntimePlayer is meant to hold player specific information such as for instance the character selected. For RuntimePlayer to work with your custom needs, you have to implement the serialization method - in the case of asset links, the GUID needs to be serialized.

RuntimePlayer is a partial class in the quantum.code project. To facilitate upgrading SDKs and future proofing, your custom implementations are to be done in RuntimePlayer.User.cs. In here you can add the parameters you would like to specify for each player and their serialization can be implemented in the SerializeUserData method. The result will resemble this:

C#

namespace Quantum {
  partial class RuntimePlayer {
    public AssetRefCharacterSpec CharacterSpec;

    partial void SerializeUserData(BitStream stream)
    {
      stream.Serialize(ref CharacterSpec.Id.Value);
    }
  }
}

Accessing at Runtime

The RuntimePlayer asset associated with a player can be retrieved by querying Frame.GetPlayerData() with their PlayerRef.

C#

public void OnPlayerDataSet(Frame f, PlayerRef player){
    var data = f.GetPlayerData(player);
}

Initializing a Player Entity

The entity controlled by a player can be initialized at any point during the simulation. A common approach is to initialize it when the player connects(ISignalOnPlayerConnected) and / or the player data is received (ISignalOnPlayerDataSet).

  • ISignalOnPlayerConnected: The player entity can be initialized with whatever information is already available in the simulation or the asset database.
  • ISignalOnPlayerDataSet: The player entity can be initialized with the information associated with the player's RuntimePlayer specific information. This is convenient for things such as selected character model / skin or inventory loadout.

Simulation vs View

First, a few clarifications:

  1. From the simulation's perspective (Quantum), player controlled entity are entities with player input. It does not know of local or remote players.
  2. From the view's perspective (Unity), we poll input from the players on the local client.

To recap, in the simulation there is no such thing as "local" or "remote" players; however, in the view a player is either "local" or it is not.

C#

Photon.Deterministic.DeterministicGameMode.Local
Photon.Deterministic.DeterministicGameMode.Multiplayer
Photon.Deterministic.DeterministicGameMode.Replay

Max amount of players

The max player count is essential to know in advance for it defines how much space needs to be allocated inside the memory chunk for each frame. By default the maximum amount of players is 6.
To change it add the following lines to any of your qtn-files:

#define PLAYER_COUNT 8
#pragma max_players PLAYER_COUNT
  • The define acts like a define and can be used inside the DSL (e.g. for allocating arrays with the player count).
  • The pragma actually defines how many player the simulation can handle.

Current Player Count

Quantum 2 does not have an automatic way to determine the current "active" player count. However, it is possible to use the before mentioned PlayerConnectedSystem to keep track of the players that are connected to the session.

Example Setup:

  1. Create a new global field in any .qtn file:
global {
    int ActivePlayerCount;
} 
  1. Implement the ISignalOnPlayerConnected and ISignalOnPlayerDisconnected in a system:

C#

public class ActivePlayerSystem : SystemSignalsOnly, ISignalOnPlayerConnected, ISignalOnPlayerDisconnected {
    public void OnPlayerConnected(Frame f, PlayerRef player) {
        f.Global->ActivePlayerCount++;
    }

    public void OnPlayerDisconnected(Frame f, PlayerRef player) {
        f.Global->ActivePlayerCount--;
    }
}
  1. Add the system to the SystemSetup:

C#

public static class SystemSetup {
  public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) {
    return new SystemBase[] {
      // pre-defined core systems
      ...
  
      new PlayerConnectedSystem(),
      new ActivePlayerSystem(),
      ...
    }
  }
}

Now, access an up-to-date player count via frame.Global->ActivePlayerCount.

Checking Connection State

You can check a player's connection state via the Frame API PlayerLastConnectionState.

For example, you could use this to make a helper method that returns if a player is connected:

C#

public static bool IsPlayerConnected(this Frame f, int player) {
  return f.PlayerLastConnectionState.IsSet(player);
}

Local Player

Quantum offers to APIs in the View to check if a player is local:

  • QuantumRunner.Default.Game.Session.IsLocalPlayer(int player); and,
  • QuantumRunner.Default.Game.PlayerIsLocal(PlayerRef playerRef).

Multiple Local Players

QuantumRunner.Default.Game.GetLocalPlayers() returns an array that is unique for every client and represents the indexes for players that your local machine controls in the Quantum simulation.

  • It returns one index if there is only one local player. Should several players be on the same local machine controls, then the array will have the length of the local player count.
  • These are exactly the same indexes that are passed into QuantumInput.Instance.PollInput(int player).
  • The indexes are defined by the server (unless it is a local game).
  • The indexes are always within [0, PlayerCount-1]. PlayerCount represents the total player count in the match. It is passed into QuantumRunner.StartGame.
  • The index values are arbitrary (within the range of 0 to max players for this session) and depend on the order of multiple players connecting and disconnecting and when their messages reach the server.
  • If a local machine has more than one player, the values are not necessarily consecutive.
  • When rejoining the game you can be assigned the same player index as long as you call Session.Join() with the same GUID and the room has not been filled with new players since you disconnected.

Use the local player index from the function above to send the runtime player data: QuantumGame.SendPlayerData(int player, RuntimePlayer data). Do this for every player on one machine.

Back to top