This document is about: QUANTUM 1
SWITCH TO

Asset Linking

Data Asset Classes

Quantum assets are normal C# classes that will act as immutable data containers during runtime.
A few rules define how these assets must be designed, implemented and used in Quantum.

Here's a minimal definition of an asset class (for a character spec) with some simple deterministic properties:

C#

namespace Quantum 
{
  partial class CharacterSpec 
  {
    public FP Speed;
    public FP MaxHealth;
  }
}

Notice that the asset class definition must be partial and be contained in the Quantum namespace.

Creating and loading instances of asset classes into the database (editing from Unity) will be covered later in this chapter.

Linking And Using Assets

To tell Quantum this is an asset class (adding internal meta-data by making it inherit the basic AssetObject class and preparing the database to contain instances of it):

C#

// this goes into a DSL file
asset CharacterSpec;

Asset instances are immutable objects that must be carried as references.
Because normal C# object references are not allowed to be included into our memory aligned ECS structs, the asset_ref special type can be used inside the DSL to declare properties inside the game state (from entities, components or any other transient data structure):

C#

entity Character[8]
{
    use Transform2D;
    use DynamicBody;
    fields
    {
        // reference to an immutable instance of CharacterSpec (from the Quantum asset database)
        asset_ref<CharacterSpec> Spec;
        FP Health;
    }
}

To assign an asset reference when creating a Character entity, one option is to obtain the instance directly from the assets database and set it to the property:

C#

var c = f.CreateCharacter();
c->CharacterSpec = DB.FindAsset<CharacterSpec>("mage");

Basic use of assets is to read data in runtime and apply it to any computation inside systems.
The following example uses the Speed value from the assigned CharacterSpec to compute the corresponding character velocity (physics engine):

C#

// consider c a Character* (from an iterator, for example)
c->DynamicBody.Velocity = FPVector2.Right * c->CharacterSpec.Speed;

A Note On Determinism

Notice that the above code reads and the Speed property to compute the desired velocity for the character during runtime, but its value (speed) is not changed.

It is completely safe and valid to switch a game state asset reference in runtime from inside an Update (as asset_ref is a rollbackable type which hence can be part of the game state).
However, changing the values of properties of a data asset is NOT DETERMINISTIC (as the internal data on assets is not considered part of the game state, so it is never rolled back).

The following snippet shows examples of what is safe (switching refs) and not safe (changing internal data) during runtime:

C#

// c is a Character*

// this is VALID and SAFE, as the CharacterSpec asset ref is part of the game state
c->CharacterSpec = DB.FindAsset<CharacterSpec>("anotherCharacterSpec");

// this is NOR valid NEITHER deterministic, as the internal data from an asset is NOT part of the transient game state:
// DON'T do this:
c->CharacterSpec.Speed = 10;

AssetObjectConfig Attribute

You can fine-tune the asset linking script generation with the AssetObjectConfig attribute.

C#

[AssetObjectConfig(GenerateLinkingScripts = false)]
partial class CharacterSpec 
{
    // ...
}
  • GenerateLinkingScripts (default=true) - prevent the generation of any scripts that make the asset editable in Unity.
  • GenerateAssetCreateMenu (default=true) - prevent the generation of the Unity CreateAssetMenu attribute for this asset.
  • GenerateAssetResetMethod (default=true) - prevent the generation of the Unity Scriptable Object Reset() method (where a Guid is automatically generated when the asset is created).
  • CustomCreateAssetMenuName (default=null) - overwrite the CreateAssetMenu name. If set to null an menu path is generated automatically using the inheritance graph.
  • CustomCreateAssetMenuOrder (default=-1) - overwrite the CreateAssetMenu order. If set to -1 an alphabetical order is used.

Overwrite Asset Script Location And Disable AOT File generation

Create the file tools\codegen_unity\quantum.codegen.unity.dll.config. Caveat: careful when upgrading, as the file can get lost in that process.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="RelativeLinkDrawerFilePath" value="Quantum/Editor/PropertyDrawers/Generated/AssetLinkDrawers.cs"/>
    <add key="RelativeAssetScriptFolderPath" value="Quantum/AssetTypes/Generated"/>
    <add key="RelativeGeneratedFolderPath" value="Quantum/Generated"/>
    <add key="GenerateAOTFile" value="true"/>
  </appSettings>
</configuration>

Asset Inheritance

It is possible to use inheritance in data assets, which gives much more flexibility to the developer (specially when used together with polymorphic methods).

The basic step for inheritance is to create an abstract base asset class (we'll continue with our CharacterSpec example):

C#

namespace Quantum 
{
  partial abstract class CharacterSpec 
  {
    public FP Speed;
    public FP MaxHealth;
  }
}

Concrete sub-classes of CharacterSpec may add custom data properties of their own, and must be marked as Serializable types:

C#

namespace Quantum 
{
  [Serializable]
  public partial class MageSpec : CharacterSpec 
  {
    public FP HealthRegenerationFactor;
  }

  [Serializable]
  public partial class WarriorSpec : CharacterSpec 
  {
    public FP Armour;
  }
}

An important remark: only CharacterSpec needs to be marked as an asset from the DSL, and asset_refs would also point to the base class as well (to fully use the flexibility of inheritance - see next section on polymorphism).

Data-Driven Polymorphism

Having gameplay logic to direct evaluate (in if or switch statements) the concrete CharacterSpec class would be very bad design, so asset inheritance makes more sense when coupled with polymorphic methods.

Notice that adding logic to data assets means implementing logic in the quantum.state project and this logic still have to consider the following restrictions:

  • Operate on transient game state data: that means logic methods in data assets must receive transient data as parameters (either entity pointers or the Frame object itself);
  • Only read, never modify data on the assets themselves: assets must still be treated as immutable read-only instances;

The following example adds a virtual method to the base class, and a custom implementation to one of the subclasses (notice we use the Health field defined for the Character entity more to the top of this document):

C#

namespace Quantum 
{
  partial unsafe abstract class CharacterSpec 
  {
    public FP Speed;
    public FP MaxHealth;
    public virtual void Update(Frame f, Character* c)
    {
      if (c->Health < 0)
        f.DestroyCharacter(c);
    }
  }

  [Serializable]
  public partial unsafe class MageSpec : CharacterSpec 
  {
    public FP HealthRegenerationFactor;
    // reads data from own instance and uses it to update transient health of Character pointer passed as param
    public virtual void Update(Frame f, Character* c)
    {
      c->Health += HealthRegenerationFactor * f.DeltaTime;
      base.Update(f, c);
    }
  }
}

To use this flexible method implementation independently of the concrete asset assigned to each Character, this could be executed from any System:

C#

// All currently active Character entities
var all = f.GetAllCharacters();

// looping through the iterator
while (all.Next())
{
  // retrieving the Character pointer 
  var c = all.Current;
  // Updating Health using data-driven polymorphism (behavior depends on the data asset type and instance assigned to character
  c->CharacterSpec.Update(c, f);
}

Should you want to keep a reference specific to a sub-class, simple need to declare it in the a qtn using asset import.

C#

asset CharacterSpec;
asset import MageSpec;

Once the sub-class has been declared in the DSL, you can use asset_ref<T> just like for the base class. For instance, to use the MageSpec directly in the DSL instead of CharacterSpec, we would have to write the following:

C#

component MageData {
    asset_ref<MageSpec> ClassDefinition; // Only accepts asset references to MageSpec class.
    FP CooldownTimer;
}

Editing Assets in Unity

The asset database (DB class from inside Quantum simulation Systems) must be initialized with asset instances.
The default mechanism for that is included with the bootstrap Unity project in the form of a generated mirror hierarchy of Scriptable Objects.

Everytime a new asset class is created, clicking on Quantum/Generate Asset Linking Scripts from Unity will generate the Unity mirror asset containers.

A list of available asset types will be available from the context menu inside the project tab (right click inside the Assets/Resources/DB folder and navigate the context menu to Create/Quantum/Assets/....

The generated ScriptableObjects assets can be edited directly from the object inspector:

Editing a data asset
Editing properties of a data asset from Unity.

Notice that the unique GUIDs can be either inserted manually or generated by clicking on Quantum/Refresh Database.

Every instance of an asset data class that has a ScriptableObject created inside the Resources/DB folder (in the Unity bootstrap project) will be automatically loaded into the database by default (refer to custom asset loaders section for other more advanced options).

Dragging And Dropping Assets In Unity

Adding asset instances and searching them in the DB class from inside simulation Systems can only go so far.
A much more interesting approach is the ability to have asset instances point to database references of one another (and being able to drag and drop these references from Unity).

One common use would be to have the RuntimePlayer pre-built class to include a link to the CharacterSpec asset chosen by that particular player (technically that boils down to the GUID string that can be assigned procedurally, but in this example we'll be looking into a quick way to drag and drop the asset instances into slots from the Unity editor - a common thing specially during prototyping).

For anything that's not the transient game state, instead of using an asset_ref, the best option is to use the generated CharacterSpecLink class, which provides the capabilities needed (including type safety):

C#

// this is added to the RuntimPlayer.User.cs file
namespace Quantum 
{
  partial class RuntimePlayer 
  {
    public CharacterSpecLink CharacterSpec;
  }
}

This would show up like this from the Unity inspector (looking into the QuantumDebugRunner from the bootstrap gameplay scene - please also refer to training videos and chapter on the topic):

Drag & Drop Asset
Asset link properties are shown as type-safe slots for Quantum scriptable objects.

With this approach, it is possible to remove any hardcoded DB lookups from inside the simulation Systems:

C#

public override void OnInit(Frame f)
{
  for (Int32 i = 0; i < f.RuntimeConfig.Players.Length; ++i)
  {
    var c = f.CreateCharacter();
    c->CharacterSpec = f.RuntimeConfig.Players[i].CharacterSpec; // notice CharacterSpecLink casts to CharacterSpec asset ref
  }
}

Extending Assets With Unity Data

You can manually add fields to the generated Quantum asset scripts that are only accessible in Unity and are not used by the simulation by simply adding a partial class declaration in your own file.

This can be useful to keep the data size for the configuration files that the simulation uses small while adding more data to the same configuration files for the presentation or even add GameObject references.

See the generated FooAsset.cs

C#

using Quantum;
using UnityEngine;

[CreateAssetMenu(menuName = "Quantum/Assets/Foo", order = 0)]
public partial class FooAsset : AssetBase {
  public Foo Settings;

  public override AssetObject AssetObject {
    get { return Settings; }
  }
}

Create a new file called MyFooAsset.cs

C#

public partial class FooAsset {
  public int Bar;
}

Custom Asset Loaders

The default Quantum asset loader looks for instances of AssetBase, a subclass of Unity's ScriptableObject from inside the Resources/DB folder and loads the asset themselves into the Quantum DB class (available from the simulation Systems API) and the Unity mirror-asset into the UnityDB class (to be used for the Unity data extensions mentioned in the previous section).

This basic implementation for this default loader can be found in Assets/Quantum/UnityDB.cs (look at the LoadAll function).

There are two options that might be interesting depending on the game being implemented:

  • Loading assets from an external CMS system;
  • Procedurally generating assets;

Loading from a CMS

The case for an external CMS providing data assets makes a lot of sense when the game in question might require a lot of balancing rounds after release.

This would allow the balancing sheets with everything data-driven such as character specs, maps, NPC specs, etc, to be updated independently from the game build itself.
Game clients would always try to connect to a CMS service to upgrade their game data to the most recent version before starting or joining online matches.

Any custom serializer (from binary, JSON, etc) could be used to convert the data back to Quantum Asset objects, that could be then injected into the database just like the sample implementation.

Procedural Generation

Similarly, assets could be procedurally generated before the match starts (the whole or part of the database could be created this way).

A few important remarks here:

  • Procedural generation would normally run on each client independently, so care has to be taken so that the generated data is the same in all clients (using a shared seed and using deterministic algorithms would be a good compromise);
  • If there`s need to procedurally generate data assets from inside the simulation Systems (during the online match), live adding assets in the DB class must be done together with the "expose verified frame status" advanced configuration option (explained in the chapter about deterministic configuration);

Map Asset Baking Pipeline

Another flexible entry point for generating custom data in Quantum is the map baking pipeline.
The Map asset is required by a Quantum simulation and contains basic information such as Navmeshes and static colliders, but it also includes a custom asset slot (can be an instance of any custom data asset).

This custom asset can be used to store any static data to be used during initialization or runtime (for example an array of spawn point data - such as position, spawned type, etc).

To assign a custom piece of code to be called everytime the bake button is called from the Unity Editor, a class extending MapDataBakerCallback just has to override the mandatory OnBake(MapData data) method:

C#

public class MyCustomDataBaker: MapDataBakerCallback
{
  public void OnBake(MapData data)
  {
    // any custom code to live-load data from scene to be baked into a custom asset

    // generated custom asset can then be assigned to data.Asset.Settings.UserAsset
  }
}

See examples of the asset pipeline in our community-wiki:

Back to top