Framework
Introduction
The Turn-Based Framework will 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 framework makes use of Quantum Signals and Events to keep your game's simulation and rendering engine, respectively, aware of what is happening on your turn data.
Make use of them to integrate the framework to your game needs.
Besides Quantum Inputs being a better pick in some cases, turn-based games are great candidates for using Quantum Commands as the main way of interaction of players with the simulation.
These greatly save your bandwidth usage and are used by default on the framework.
Last, but not least, we have the framework Systems to receive our player commands and take care of updating the turn clock.
Turn Data Fields
The Turn Data struct holds references to a Player and an Entity that might be used for convenience, e.g. to easily access the player or entity this data refers to, and also a reference to a Config asset instance, that carries information about turn configurable parameters (more on Turn Configuration).
Every turn has a current Type that suggests (but don't oblige by default) what kind of interactions are currently allowed/active and what are not.
The framework comes packed with two pre-defined types: Play, to be used while player interactions are allowed, like drawing cards in card games, and Countdown, to be used when they are not allowed and the gameplay is on hold.
You can interpret the existing types and/or create your owns to adjust the framework to your game specificities.
Every turn also has a current Status, that is used to define which logic will be acting on that data.
The framework uses three pre-defined status: Active, when the turn timer might be increased and player commands are accepted; Inactive, when the timer is not increased and player commands are not accepted; and Resolving, that signalizes a temporary status between Active and Inactive when a Play Command is received by the current player, when game-specific logic might need to be executed, like simulating ball physics on Golf Sample.
A turn Number is increased by 1 when accumulating stats from another turn data.
That way, when the global current turn ends, the active player's turn data number can be updated.
The turn Ticks is increased every frame on the global current turn when it is Active.
On the players' turn data instances, it is used to accumulate the ticks that that player was active on all previous turns.
The Current Turn is a global Turn Data instance that is reset every new turn to track the current player's turn.
When it ends, the data can be then accumulated on that player's own turn data instance.
A turn may end for many different reasons, so the framework enumerates some of them as Turn End Reasons, which can be incremented and used when signalizing that a turn has ended to trigger different game-specific logic routines, which can be observed as a use case on Golf Sample.
C#
enum TurnType { Play, Countdown }
enum TurnStatus { Inactive, Active, Resolving }
enum TurnEndReason { Time, Skip, Play, Resolved }
struct TurnData
{
player_ref Player;
entity_ref Entity;
asset_ref<TurnConfig> Config;
TurnType Type;
TurnStatus Status;
Int32 Number;
Int32 Ticks;
}
global
{
TurnData CurrentTurn;
}
Turn Data Methods
When a Turn Data instance is Updated, if it uses a timer (check Turn Configuration) and the status is Active, the Ticks are incremented by 1.
If the ticks reach the turn duration defined on the that turn's configuration asset, an On Turn Ended signal is triggered with Time as turn end reason.
C#
public void Update(Frame f)
{
if (Config == null || !Config.UsesTimer || Status != TurnStatus.Active)
{
return;
}
Ticks++;
if (Ticks >= Config.TurnDurationInTicks)
{
f.Signals.OnTurnEnded(this, TurnEndReason.Time);
}
}
A Turn Data can also Accumulate Stats from another instance, increasing the turn Number by 1 and accumulating the Ticks from the instance.
Usually, player's turn data instances accumulate stats from the global current turn when it ends.
C#
public void AccumulateStats(TurnData from)
{
Ticks += from.Ticks;
Number++;
}
A turn Set Type and Status methods can be used to set them to a given value.
If a Frame is provided (not mandatory) and the value actually changes, Events are called to communicate that such thing happened and/or that the turn has been activated.
C#
public void SetType(TurnType newType, Frame f = null)
{
if (Type == newType)
{
return;
}
var previousType = Type;
Type = newType;
f?.Events.TurnTypeChanged(this, previousType);
}
public void SetStatus(TurnStatus newStatus, Frame f = null)
{
if (Status == newStatus)
{
return;
}
var previousStatus = Status;
Status = newStatus;
f?.Events.TurnStatusChanged(this, previousStatus);
if (Status == TurnStatus.Active)
{
f?.Events.TurnActivated(this);
}
}
A player's turn data instance can be Reset at the beginning of the game to reset the Ticks value and set other instance fields, by use one of the provided overloads.
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.
C#
public void ResetTicks(Frame f = null)
{
ResetData(Type, Status, Entity, Player, Config, 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, Config, f);
}
public void Reset(TurnConfig config, TurnType type, TurnStatus status, EntityRef entity, PlayerRef owner, Frame f = null)
{
ResetData(type, status, entity, owner, config, f);
}
Turn Configuration
Quantum Assets are a feature for defining data-driven containers that end up as immutable instances inside an indexed database.
The framework uses a Turn Config data asset that carries information about a turn: if it uses a timer, meaning that its Ticks field is increased every frame when the status is Active; its turn duration in ticks, used to track if a turn that uses a timer has ended; and if it is skippable, meaning that a player can skip his own turn.
More configurable data can and should be added to tailor the turn configuration to each game.
C#
public partial class TurnConfig
{
public Boolean UsesTimer;
public Int32 TurnDurationInTicks;
public Boolean IsSkippable;
}
Signals
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 Golf Sample.
A Play/Skip Command Received signal is raised by the framework 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 Golf Sample.
C#
signal OnTurnEnded (TurnData data, TurnEndReason reason);
signal OnPlayCommandReceived (PlayerRef player, PlayCommandData data);
signal OnSkipCommandReceived (PlayerRef player, SkipCommandData data);
Events
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.
C#
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; }
Commands
Quantum Commands are similar to Quantum Inputs but are not required to be sent every tick, then using less bandwidth.
Both commands defined on the framework use generic structs to carry game-specific command data, that must be serialized accordingly.
A Play Command can be sent by a player on his active play turn to send information about the decisions/moves made during the turn.
It is received and validated by the command's respective System, which checks if player's turn is of type Play and is Active.
On the code below you can also see the definition and serialization of the Golf Sample command data.
C#
[Serializable]
public struct PlayCommandData
{
// game-specific command data here
public FP Force;
public FPVector3 Direction;
}
public class PlayCommand : DeterministicCommand
{
public PlayCommandData Data;
public override void Serialize(BitStream stream)
{
// serialize command data here
stream.Serialize(ref Data.Force);
stream.Serialize(ref Data.Direction);
}
}
A Skip Command can be sent by a player on his active play turn to skip it.
It is received and validated by the command's respective System, which checks if the player's turn is Active and the turn is skippable.
C#
[Serializable]
public struct SkipCommandData
{
// game-specific command data here
}
public class SkipCommand : DeterministicCommand
{
public SkipCommandData Data;
public override void Serialize(BitStream stream)
{
// serialize command data here
}
}
Systems
Quantum Systems are stateless pieces of logic that will be executed every tick update by Quantum's client simulation loop.
The Play/Skip Command Systems are responsible for receiving the currently active player's Play and Skip Commands, respectively.
Only the player referenced on the global current turn has its commands accepted and only if the global current turnis Active.
Also, it is checked if the turn is of type Play when receiving a Play Command, and if the turn is marked as is skippable when receiving a Skip Command.
When a Play Command is received, the global current turn status is set to Resolving, so the Ticks timer is not increased anymore (turn is no longer Active) and game-specific logic might need to be executed.
When either of the commands is received, the correspondent signal and event is triggered/raised.
C#
public unsafe class CommandSystem : SystemBase {
public override void Update(Frame f) {
var currentTurn = f.Global->CurrentTurn;
if (currentTurn.Status != TurnStatus.Active) return;
var currentPlayer = f.Global->CurrentTurn.Player;
switch (f.GetPlayerCommand(currentPlayer)) {
case PlayCommand playCommand:
if (currentTurn.Type != TurnType.Play) return;
f.Signals.OnPlayCommandReceived(currentPlayer, playCommand.Data);
f.Events.PlayCommandReceived(currentPlayer, playCommand.Data);
break;
case SkipCommand skipCommand:
if (!currentTurn.Config.IsSkippable) return;
f.Signals.OnSkipCommandReceived(currentPlayer, skipCommand.Data);
f.Events.SkipCommandReceived(currentPlayer, skipCommand.Data);
break;
}
}
}
The Turn Timer System is responsible only for calling global current turn's Update method every tick update.
C#
public unsafe class TurnTimerSystem : SystemBase
{
public override void Update(Frame f)
{
f.Global->CurrentTurn.Update(f);
}
}
Fair Timer
FairTimer.cs
presents a pattern for displaying to the local player a fairer and more realistic timer, taking into consideration the client's RTT and/or its Input Offset.
Note that this does not interfere with anything on the simulation, the current turn timer runs synced in every client, it's just the value that is displayed to the player that is changed proportionally to its lag.
This is a configurable and optional feature and should be tweaked to reach the desired behaviour.
Fair Timer is not a golf-specific pattern. Any game that uses timers can benefit from this pattern. The Fair Timer solution present on the Golf sample can be used separated from the Turn-Based framework with minor changes.
Back to top