This document is about: QUANTUM 2


The systems in the Quantum Golf Sample are split into two categories:

  • systems driving gameplay; and,
  • systems dedicated to managing player turns.


Gameplay systems handle all game logic specific to the Quantum Golf Sample.

Maximum Amount of Players

The maximum amount of players is 2 and fixed in the DSL using the following snippet: #pragma max_players 2. The value is also defined as a constant #define MAX_PLAYERS 2 in order to reused it for array initialization.


The SetupSystems handles the initial game setup - this covers:

  • receive the RuntimePlayer via the ISignalOnPlayerDataSet signal;
  • initialize the players based on their respective RuntimePlayer
  • resetting player’s TurnData instances; and,
  • triggering the ball spawning via the SpawnSystem (see related system).


The SpawnSystem's sole responsibility is to spawn the golf ball upon request from the SetupSystem. To achieve this it first looks up the GameConfig asset and uses a filter to find all active SpawnPoint. The SpawnSystem will spawn the ball according to the SpawnPointType provided in the GameConfig, after which the SpawnPoint will be disabled.


A SpawnPoint is a component used on scene prototype to define spawn point locations.

spawnpoint prototype
spawnpoint prototype

It holds data on its SpawnPointType and whether an entity has been spawn on it.

  • NearHole: defines SpawnPoints to be used when the NearHole option is selected for spawning balls in the GameConfig asset;
  • Regular: for spawning balls on regular games.


asset ConfigAssets;
asset GameConfig;
asset UserMap;

enum SpawnPointType { Regular, NearHole }

component SpawnPoint 
  SpawnPointType Type;
  Boolean IsAvailable;
  entity_ref Entity;

synced event GameplayEnded { }


The PlaySystem handles the ball's physics and determine the turn state based on its velocity. The information on a player's strike is received via the ISignalOnSkipCommandReceived signal.


The ball model uses two components, Actor and BallFields; both of which are defined in the Ball.qtn file.


The Actor component holds a player_ref to the player controlling / owning the entity it is placed on. The Active boolean is used to check if a player is currently active in this game and it is used when loading a game from a saved file.


component Actor 
    Boolean Active;
    player_ref Player;


The BallFields component is used to hold the state of a player's ball.


component BallFields
    asset_ref<BallSpec> Spec;
    TurnData TurnStats;
    FPVector3 LastPosition;
    Boolean HasCompletedCourse;
    Int32 EndOfMovementTimer;
    Int32 Score;
  • TurnStats: An instance of TurnData to accumulate stats from the global CurrentTurn when it is that particular player's turn. This particular field is maintained by the TurnSystem.
  • LastPosition: last position before being shot. It is used to reset the ball’s position when it lands on a Rough Field.
  • HasCompletedCourse: keeps track of whether the player has completed the current course. It is checked and updated by the PlaySystem.
  • EnOfMovementTimer: A timer that is increased by the PlaySystem until the ball has come to a stop.
  • Score: Updated by the ScoreSystem when a player’s turn has ended.
  • Score: Updated by the ScoreSystem when a player’s turn has ended.


The ISignalOnBallShot signal triggered by the PlaySystem when a ball is shot.


In addition to the PlayCommand sent by a player when striking a ball, the regular input struct is used to share the current aiming direction and force bar mark position with other players. This allows to replicate the active player’s aiming on the waiting player's side.

    FPVector3 Direction;
    FP ForceBarMarkPos;

These values are meant exclusively for visual feedback purposes and are not used for any gameplay related logic despite holding the same information as PlayCommands.


The forces applied to the ball are defined by the player strike sent as input via the PlayCommand. The PlaySystem implements the ISignalOnPlayCommandReceived which allows it to receive PlayCommand. The systems processes the command to ensure it is valid; this takes place in 3 steps:

  1. clamping the strike data (direction and force) according to the values defined on the GameConfig asset;
  2. striking the ball by applying a force to its PhysicsBody3D; and finally,
  3. triggering the ISignalOnBallShot signal.


In addition to applying the forces on the ball, the PlaySystem also evaluates the collisions triggered by the course's hole and the outer Rough Field.

If a ball stops on a Rough Field static collider, the outer dark green field, its position will be reset to position it had before the strike.

golf header

When a ball triggers a static collider on the Hole layer AND its velocity is below the HitHoleVelocityThreshold defined in the GameConfig asset, the OnHitHole() method is called and the course is considered completed.


If the global variable CurrentTurn.Status is set to TurnStatus.Resolving, it means the ball is still moving. During that time, the PlaySystem will check if the ball has stopped moving at each Update(). The resting threshold is determined by the value set in the BallSpec asset.

If the ball's velocity is below the threshold value, it is considering at rest. At this point, the EndOfMovementTimer field in the BallFields component will start increasing. Once it has reached the EndOfMovementWaitingInTicks value, the EndOfPlay() method will be called and the ISignalOnTurnEnded signal will be triggered thus ending the current player's turn.

When a play ends, the ball's PhysicsBody3D component is disabled to prevent unwanted interactions while that player's TurnStatus is Inactive.


The TurnSystem handles the turn related logic used by the turn based features.

Each player has their own TurnData instance to aggregate the of their turn from the global CurrentTurn data instance. The active player’s TurnStatus is also kept updated by the system.

When the ball is struck in the PlaySystem, the TurnSystem set the global CurrentTurn.Status to TurnStatus.Resolving and calls SetStatus() on the active player's BallFields component TurnStats field.

If a valid SkipCommand is received from the active player, the TurnSystem simply triggers a the ISignalOnTurnEnded signal.

When a ISignalOnTurnEnded signal is received, the TurnSystem checks the TurnType.

  • If it is of type Countdown and there is a eligible ball for the next turn, it activates next ball's turn.
  • If it is of type Play, it calls AccumulateStats() and SetStatus() on the current player's ball to render it inactive. Afterwards it calls Reset() on the global CurrentTurn.


The Turn-Based SDK is an addon for the default Quantum SDK. It aims to help you store and operate logic over your turn-related data.

Most of this happens on your Turn Data struct instances, which have configurable parameters through a data asset and can be used on each of your players/entities and/or globally to manage your game flow.


The TurnData struct holds references to a Player and an Entity for convenience; e.g. to easily access the player or entity the data refers. It also keeps a reference to a TurnConfig asset that carries the information about a turn's configurable parameters.

Every turn has a current TurnType that suggests but does not mandate what kind of interactions are allowed/forbidden during the current turn. The Quantum Golf Sample includes two pre-defined types:

  • Play: interactions are allowed.
  • Countdown: interactions are forbidden and the gameplay is on hold.

Every turn also has a keeps track of its TurnStatus; it is used to define which logic path will be used to manipulate data while that status is active. The Quantum Golf Sample uses three pre-defined status:

  • Active: the turn timer can be increased and player commands are accepted.
  • Inactive: the timer is paused and player commands are not accepted.
  • Resolving: signalizes a temporary status after a PlayCommand has been received by the current player and game-specific logic is being executed - e.g. simulating ball physics.

The Number keeps track of the number of turns played by a player and is increased by 1 when calling AccumulateStats(). This way the active player's TurnData can be updated when the global CurrentTurn ends. In contrast, the Ticks variables is increased every frame while the global CurrentTurn is marked as Active. This allows to know exactly how many ticks each player was active during all turns they has played thus far.


enum TurnType { Play, Countdown }
enum TurnStatus { Inactive, Active, Resolving }

struct TurnData 
  player_ref Player;
  entity_ref Entity;
  asset_ref<TurnConfig> ConfigRef;
  TurnType Type;
  TurnStatus Status;
  Int32 Number;
  Int32 Ticks;

The CurrentTurn is a global TurnData instance which is reset at the beginning of every new turn to track of the current player's turn. When a turn ends, the data is aggregated / accumulated to the existing player's turn data instance.

A turn may end for many different reasons; the Quantum Golf Sample enumerates some of them via the TurnEndReasons enum. These can be changed and added to signalizing a turn has ended and trigger different game-specific logic routines.


enum TurnEndReason { Time, Skip, Play, Resolved }

  TurnData CurrentTurn;


If the current turn uses a timer when a TurnData instance is updated and the status is Active, the Ticks are incremented by 1. If the Ticks value reach the maximum allowed amount for this turn's as defined in the TurnConfig asset, the ISignalOnTurnEnded signal is triggered with the TurnEndReason.Time enum passed in as the turn end reason.


public void Update(Frame f)
  var config = f.FindAsset<TurnConfig>(ConfigRef.Id);
  if (config == null || !config.UsesTimer || Status != TurnStatus.Active)
  if (Ticks >= config.TurnDurationInTicks)
    f.Signals.OnTurnEnded(this, TurnEndReason.Time);

A TurnData instance can also AccumulateStats() from another instance; the implementation in the Quantum Golf Sample translates this to increasing the Number of turns by 1 and accumulating the Ticks from the instances. Usually, a player's TurnData instance accumulates its stats when the global CurrentTurn ends.


public void AccumulateStats(TurnData from)
  Ticks += from.Ticks;

A TurnData instance offers two different setter methods:

  • SetType()
  • SetStatus()

Although they do not required a Frame to be provided, doing so allows to trigger events if so desired.


public void SetType(TurnType newType, Frame f = null){
  if (Type == newType){
  var previousType = Type;
  Type = newType;
  f?.Events.TurnTypeChanged(this, previousType);

public void SetStatus(TurnStatus newStatus, Frame f = null){
  if (Status == newStatus){
  var previousStatus = Status;
  Status = newStatus;
  f?.Events.TurnStatusChanged(this, previousStatus);
  if (Status == TurnStatus.Active){

A player's TurnData instance can be Reset() at the beginning of the game; all overloads will reset the Ticks value while also offering the ability to set other fields depending on which overload is chosen. It can also be used to reset global current turn when a turn ends to prepare it to track another player's turn. If a Frame is provided (not mandatory), a Turn Timer Reset event is raised on that frame.


public void ResetTicks(Frame f = null) {
  ResetData(Type, Status, Entity, Player, ConfigRef, f);

public void Reset(TurnConfig config, TurnType type, TurnStatus status, Frame f = null){
  ResetData(type, status, Entity, Player, config, f);

public void Reset(EntityRef entity, PlayerRef owner, Frame f = null){
  ResetData(Type, Status, entity, owner, ConfigRef, f);

public void Reset(TurnConfig config, TurnType type, TurnStatus status, EntityRef entity, PlayerRef owner, Frame f = null){
  ResetData(type, status, entity, owner, config, f);


The ScoreSystem handles end game scoring. If the game ends as part of a play (TurnType.Play) - i.e. the a player managed to put their ball inside the hole -, the score for the winning player is tabulated as follows: the maximum number of allowed strokes + 1 (defined in the MaxStrokes field of the GameConfig) minus the amount of strokes it took the player to hit the ball into the hole.

If the course has not been completed yet, the score is 0.


The GameEnd systems checks at the end of every turn whether the game's end conditions have been met. The Quantum Golf Sample implements 2 end conditions:

  • all players have taken the maximum amount of allowed strikes.
  • the course has been completed (i.e. a ball was put in the hole)

Once either of these conditions is reached, the global CurrentTurn data is reset and a the GameplayEnded event is raised. The GameplayEnded event is used to communicate with Unity and display a visual feedback.

Turn Based Systems

The turn based systems are game agnostic and can be used for any sort of turn based gameplay.


The CommandSystem is responsible for controlling the type of command sent by players and trigger the corresponding signals / logic if these are valid. The two commands included in the Quantum Golf Sample are PlayCommand and SkipCommand. Only commands sent by the currently active player are accepted; the active player is referenced in the global CurrentTurn data field.

When a PlayCommand is received, the global CurrentTurn status is set to Resolving to stop the timer during that time while game-specific logic might need to be executed.


public override void Update(Frame f)
  var currentTurn = f.Global->CurrentTurn;
  if (currentTurn.Status != TurnStatus.Active)
  var currentPlayer = f.Global->CurrentTurn.Player;
  switch (f.GetPlayerCommand(currentPlayer))
    case PlayCommand playCommand:
      if (currentTurn.Type != TurnType.Play)
      f.Signals.OnPlayCommandReceived(currentPlayer, playCommand.Data);
      f.Events.PlayCommandReceived(currentPlayer, playCommand.Data);

    case SkipCommand skipCommand:
    var config = f.FindAsset<TurnConfig>(currentTurn.ConfigRef.Id);
    if (!config.IsSkippable)
    f.Signals.OnSkipCommandReceived(currentPlayer, skipCommand.Data);
    f.Events.SkipCommandReceived(currentPlayer, skipCommand.Data);


The TurnTimerSystem is responsible for calling Update() on the global CurrentTurn every tick.


public unsafe class TurnTimerSystem : SystemMainThread
  public override void Update(Frame f)


Quantum Signals are meant for inter-system communication.

A Turn Ended signal can be triggered to signalize that a turn has ended by a given turn end reason and should be used to trigger game-specific turn-control logic, like passing the turn to the next player on the Quantum Golf Sample.

A Play/Skip Command Received signal is raised when a respective valid command is received (more about player commands validity on Systems) and can be used to trigger the desired game-specific logic, like striking a ball or ending a turn on Quantum Golf Sample.


signal OnTurnEnded (TurnData data, TurnEndReason reason);
signal OnPlayCommandReceived (PlayerRef player, PlayCommandData data);
signal OnSkipCommandReceived (PlayerRef player, SkipCommandData data);


Quantum Events are meant to communicate things that happen inside the simulation to the rendering engine, so it can respond accordingly with a desired audio-visual feedback. More on regular/synced/abstract Quantum Events here.

  • When a turn type/status changes via a Turn Data Set method (more on Methods), an event is raised carrying that Turn Data instance value and the previous Type/Status.
  • When a turn timer is reset by calling its Reset method passing a non-null frame, an event is raised carrying that Turn Data instance value.
  • When a turn is activated by changing its Status to Active in a Set Status method, or a turn ends for a given reason, an event is raised to signalize so, passing the reason in the second case.
  • When a valid Play/Skip command is received by the respective command System, an event is raised to signalize so.


abstract event TurnEvent                   { TurnData      Turn; }
synced event TurnTypeChanged   : TurnEvent { TurnType      PreviousType; }
synced event TurnStatusChanged : TurnEvent { TurnStatus    PreviousStatus; }
synced event TurnEnded         : TurnEvent { TurnEndReason Reason; }
synced event TurnTimerReset    : TurnEvent { }
synced event TurnActivated     : TurnEvent { }

abstract event CommandEvent              { player_ref Player; }
event PlayCommandReceived : CommandEvent { PlayCommandData Data; }
event SkipCommandReceived : CommandEvent { SkipCommandData Data; }
Back to top