This document is about: QUANTUM 1
SWITCH TO

Quantum DSL (game state)

Introduction

Quantum requires all game state data to be declared with its own DSL (domain-specific-language).
These definitions are written into text files with the ".qtn" extension (definitions can be split across as many of these as needed, the Quantum compiler will merge them and generate a single game state).

The goal of the DSL is to abstract away from the developer the complex memory alignment requirements imposed by Quantum's predict/rollbacks model.

The DSL-defined game state is parsed by the custom Quantum compiler and converted into regular C# (with the memory restrictions in place).

This conversion includes extensive use of unmanaged C# pointer and several code-generated functions to simplify the work and protect the developer from some tricks this high-performance pointer-based approach.

Entities

Entities should be used for transient gameplay concepts (instances that will need to be dynamically created/destroyed), and they normally contain a set of components, but can also include types (not components, which are special types on the DSL) declared as properties.

A minimally valid definition of an entity must provide its name, and the maximum number of instances (quantum pre-allocates all game state data):

C#

entity Character[19]
{

}

This will generate a C# struct for an entity called Character, and pre-allocate 19 "instances" of it in the game state. It is possible to add properties directly to an entity with the "field" scope:

C#

entity MyEntity[19]
{
    fields
    {
        FP Health;
        FP Mana;
    }
}

A pointer to an instance of the generated "MyEntity" struct would then the give direct access to the fields like this (please also refer to the next chapter - systems - to learn more about the generated state API):

C#

var mana = myEntity->Mana;
myEntity->Health = 10;

Components

Components are special reusable data containers/groups that can be attached to entities. This is a basic definition of a component:

C#

component Weapon
{
    FP Cooldown;
    FP Power;
}

The "use" keyword attaches a component to an entity:

C#

entity MyEntity[19]
{
    use Weapon;
    fields
    {
        FP Health;
        FP Mana;
    }
}

In the generated entity struct, the name/type of the component is also used as the property name:

C#

myEntity->Weapon.Power = 4;

There are some pre-built components in Quantum:

  • Transform2D - contains a position (FPVector2) and rotation (FP) in radians;
  • DynamicBody - current velocity, mass, shape, material data and other physics engine related data (position and rotation are taken from Transform2D, which is required when using physics for an entity);
  • Prefab - a simplified component to help link an entity to a Unity prefab during runtime (for rendering the entity in Unity);
  • Animator - the simulation part of the deterministic animation system in Quantum (which integrates with Unity's mecanim);

Here's the combined DSL for the topics covered in this section:

C#

component Weapon
{
    FP Cooldown;
    FP Power;
}

entity MyEntity[19]
{
    use Weapon;
    use Transform2D;
    use DynamicBody;
    fields
    {
        FP Health;
        FP Mana;
    }
}

Structs

The Quantum DSL also enable the definition of regular structs without the need to take care with memory alignment, and also protecting against the definition of non-alignable data:

C#

struct Resources
{
    FP Health;
    FP Mana;
}

This would let you use the "Resources" struct as a type in all other parts of the DSL, for example using it to replace the loose fields that were previously used in the entity definition:

C#

entity MyEntity[19]
{
    use Weapon;
    use Transform2D;
    use DynamicBody;
    fields
    {
        Resources Resources;
    }
}

Components vs. Structs

An important question is why and when should components be used instead of regular structs (components, in the end, are also structs).

Components contain generated meta-data that turns them into a special type:

  • An entity meta-data contains data to quickly filter for the existence of a component in its definition during runtime;
  • It is possible to traverse the game state for active instances of a component type (this is the simplest of high-performance entity filters, which are part of the Systems API - next chapter);

As will be clear in the next sessions, components can still be passed as pointers (in signals - later in this chapter) or as value types (to any other function or event - see later) without its containing entity, just like any other struct. But the features explained above are exclusive for components.

Unions, Enums and Bitsets

C-like unions and enums can be generated as well. The example below demonstrates how to save data memory by overlapping some mutually exclusive data types/values into a union, using an enum to specify which data is being stored:

C#

enum DataType
{
    typeA, typeB 
}

struct DataA
{
    FPVector2 Something;
    FP Anything;
}

struct DataB
{
    FPVector3 SomethingElse;
    Int32 AnythingElse;
}

union Data
{
    DataA A;
    DataB B;
}
 
struct DataContainer
{
    DataType Type;
    Data Data;
}

Bitsets can be used to declared fixed-size memory blocks for any desired purpose (for example fog-of-war, grid-like structures for pixel perfect game mechanics, etc):

C#

struct FOWData
{
    bitset[256] Map;
}

Input

In Quantum, the runtime input exchanged between clients is also declared in the DSL. This example defines a simple movement vector as input for a game:

C#

input
{
    FPVector2 Movement;
}

For button-like input, Quantum can safely compute the state changes (this will be covered in the next chapter - systems), so the special type "button" should be used:

C#

input
{
    FPVector2 Movement;
    button Fire;
}

A pre-defined input struct is a requirement, unions can be used to defined mutually exclusive input types and data to be used in the same gameplay session. This example shows how the same input definition can contain either normal movement/fire or a "buy something" command data in it:

C#

enum InputType
{
    BuyCommand, MovementAndFire
}

struct BuyData
{
    Int32 ItemID;
    Int32 UnitID;
    Boolean ApplyImmediately;
}

struct MovementAndFireData 
{
    FPVector2 Movement;
    button Fire;
}

union InputData
{
    BuyData BuyCommand;
    MovementAndFireData MovementAndFire;
}

input
{
    InputType Type;
    InputData Data;
}

Please refer to next chapters to understand how input is consumed by (Systems) and injected into Quantum from Unity (Bootstrap Unity Project).

Events

Events are a fine-grained solution to communicate things that happen inside the simulation to the rendering engine (they should never be used to modify/update part of the game state). Use the "event" keyword to define its name and data:

C#

event TriggerSound
{
    FPVector2 Position;
    FP Volume;
}

Inheritance is available to events (optionally some events may be marked abstract, so blocking them from being triggered directly, allowing only concrete subclasses):

C#

abstract event TriggerSound
{
    FPVector2 Position;
    Int32 SoundID; // this would be better handled with unity-side asset linking extensions, but this is out of the scope of this chapter
}

event TriggerShot : TriggerSound
{    
    FP Power;
}

To guarantee that certain events are only dispatched (to unity) when input data is confirmed from the server (avoiding rollback-induced false positives), just use the "synced" keyword:

C#

synced event TriggerDeathSound : TriggerSound
{    
}

To avoid undesired duplicate dispatches, Quantum auto-generates hashcode functions for all event types (using the event data as the source). In certain conditions, the developer might want to tightly control what the key-candidate data (that makes an event instance unique) is. One example is when slight rollback-induced changes in positions render two instances of the same event be wrongly interpreted as two really different events. To avoid that, it is possible to force the hash function to ignore some of the event data using the "nothashed" keyword:

C#

abstract event TriggerSound
{
    nothashed FPVector2 Position;
    Int32 SoundID;
}

Please also refer to next chapters to understand how events are triggered from Quantum (Systems) and dispatched to Unity callbacks (Bootstrap Unity Project).

Signals

Signals are function signatures used as a decoupled inter-system communication API (a bit like a publisher/subscriber API or observer pattern). This would define a simple signal:

C#

signal OnDamage(FP Damage);

This would generate the following interface (that can be implemented by any System):

C#

public interface ISignalOnDamage
{
    public void OnDamage(Frame f, FP Damage);
}

Signals are the only concept which allows the direct declaration of a pointer in Quantum's DSL, so passing data by reference can be used to modify the original data directly in their concrete implementations:

C#

signal OnDamageBefore(FP* Damage);

Special Types

Quantum has a few special types that are used to either abstract complex concepts (combined pointer offsets, player indexes, etc), or to protect against common mistakes with unmanaged code, or both. The following special types are available to be used inside other data types (including in components, as entity fields, in events, etc):

  • player_ref - represents a runtime player index (they cast to and from an Int32). when attached to an entity, can be used to mark entity instances controlled by a player (combined with Quantum's player-index-based input);
  • entity_ref - (generic) because each frame/tick data in quantum resides on a separate memory region/block (Quantum keeps a buffer of these for fast rollbacks), pointers cannot be cached in-between frames (nor in the game state neither on Unity). An entity ref abstracts an entity type, index and version (also protecting the developer from accidentally accessing deprecated data over destroyed or reused entity slots with old refs);
  • asset_ref - rollbackable reference to a data asset instance from the Quantum asset database (please refer to the data assets chapter)
  • array[size] - fixed sized "arrays" to represent data collections. A normal C# array would be a heap-allocated object reference (it has properties, etc), which violates Quantum's memory requirements, so the special array type generates a pointer based simple API to keep rolbackable data collections inside the game state;

A Note On Assets

Assets are a special feature of Quantum that let the developer define data-driven containers (normal classes, with inheritance, polymorphic methods, etc) that end up as immutable instances inside an indexed database. The "asset" keyword is used to assign an (existing) class as a data asset that can have references assigned inside the game state (please refer to the Data Assets chapter to learn more about features and restrictions):

C#

asset CharacterData; // the CharacterData class is partially defined in a normal C# file by the developer

The following struct show some valid examples of the types above (sometimes referencing previously defined types):

C#

struct SpecialData
{
    player_ref Player;
    entity_ref<Character> Character;
    entity_ref AnyEntity;
    asset_ref<CharacterData> CharacterData;
    array<FP>[10] TenNumbers;
    array<entity_ref<Character>>[4] FourCharacterRefs;
}

Available Types and Imports

Quantum's DSL parser has a list of pre-imported cross-platform deterministic types that can be used in the game state definition:

  • Boolean
  • Char
  • Byte
  • SByte
  • UInt16
  • Int16
  • UInt32
  • Int32
  • UInt64
  • Int64
  • FP
  • FPVector2
  • FPVector3
  • FPMatrix
  • FPQuaternion
  • PlayerRef - player_ref in the DSL
  • EntityRef - entity_ref in the DSL
  • LayerMask
  • NullableFP - use FP? in the DSL
  • NullableFPVector2 - use FPVector2? in the DSL
  • NullableFPVector3 - use FPVector3? in the DSL

Compiler Options

The following compiler options are currently available to be used inside Quantum's DSL files (more will be added in the future):

C#

// pre defining max number of players (default is 8)
#pragma max_players 16

// numeric constants (useable inside the DSL by MY_NUMBER and useable in code by Constants.MY_NUMBER)
#define MY_NUMBER 10

// overriding the base class name for the generated constants (default is "Constants")
#pragma constants_class_name MyFancyConstants
Back to top