This document is about: QUANTUM 2
SWITCH TO

This page is a work in progress and could be pending updates.

Assets in Simulation

資料資產類別

Quantum資產是正常C#類別,其在運行階段將作為不可變的資料容器。一些規則定義了這些資產在Quantum中必須被設計、執行及使用的方式。

這是一個附有一些簡單的確定性屬性的資產類別(針對一個角色規格)的一個最小的定義:

C#

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

請注意,資產類別定義必須是 部分,並且被內含在Quantum命名空間之中。

本章節後面將討論建立及載入資產類別的執行個體到資料庫(從Unity編輯)。

使用及連接資產

為了告訴Quantum這是一個資產類別(透過使內部中繼資料繼承基本 資產物件 類別,並且準備資料庫來內含內部中繼資料的執行個體,來新增內部中繼資料):

C#

// this goes into a DSL file
asset CharacterSpec;

資產執行個體是不可變的物件,其必須以參照來被執行。因為正常C#物件參照不允許被包含在我們的記憶體對齊的ECS架構中,因此必須在DSL中使用 asset_ref 特殊類型,以在遊戲狀態中宣告屬性(從實體、元件或任何其他暫時性資料架構):

C#

component CharacterData {
    // reference to an immutable instance of CharacterSpec (from the Quantum asset database)
    asset_ref<CharacterSpec> Spec;
    // other component data
}

為了在建立一個角色實體時,指派一個資產參照,一個選項是從幀資產資料庫直接地取得執行個體,並且將其設定到屬性:

C#

// assuming cd is a pointer to the CharacterData component
// using the SLOW string path option (fast data driven asset refs will be explained next)
cd->Spec = frame.FindAsset<CharacterSpec>("path-to-spec");

資產的基本使用是在運行階段讀取資料,並且將其應用到系統之中的任何計算。以下實例從被指派的 角色規格 使用 速度 值,以計算相應的角色速度(物理引擎):

C#

// consider cd a CharacterData*, and body a PhysicsBody2D* (from a component filter, for example)
var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
body->Velocity = FPVector2.Right * spec.Speed;

關於確定性的注意事項

請注意,上述程式碼只 讀取 速度 屬性,以在運行階段針對角色計算所需的速度,但是它的值(速度)將永不改變。

在運行階段從一個更新中轉換一個遊戲狀態資產參照,是完全安全且有效的(因為asset_ref是一個可復原的類型,因此其可以是遊戲狀態的一部分)。

然而,更改一個資料資產的屬性的 並不是確定性的(因為在資產上的內部資料不被認為是遊戲狀態的一部分,因此它永不被復原)。

以下的程式碼片段展示了在運行階段哪些是安全(轉換參照)及不安全(更改內部資料)的實例:

C#

// cd is a CharacterData*

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

// this is NOR valid NEITHER deterministic, as the internal data from an asset is NOT part of the transient game state:
var spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// (DO NOT do this) changing a value directly in the asset object instance
spec.Speed = 10;

資產物件組態屬性

您可以透過AssetObjectConfig屬性來微調資產連結指令碼生成。

C#

[AssetObjectConfig(GenerateLinkingScripts = false)]
partial class CharacterSpec {
    // ...
}
  • 生成連結指令碼 (預設=真)——防止生成任何讓資產在Unity中成為可編輯的指令碼。
  • 生成資產建立選單 (預設=真)——針對此資產,防止Unity CreateAssetMenu屬性的生成。
  • 生成資產重新設定方法 (預設=真)——防止Unity可指令碼物件Reset()方法的生成(其中當建立資產時,一個Guid被自動地生成)。
  • 自訂建立資產選單名稱 (預設=空)——覆寫CreateAssetMenu名稱。如果設定為null,將使用繼承圖表來自動地生成一個選單路徑。
  • 自訂建立資產選單順序 (預設=-1)——覆寫CreateAssetMenu順序,如果設定為-1,將使用字母的順序。

覆寫資產指令碼位置及停用AOT檔案生成

建立檔案 tools\codegen_unity\quantum.codegen.unity.dll.config。警示:升級時需小心,因為檔案在該過程中可能遺失。

XML

<?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"/>
    <add key="EndOfLineCharacters" value="&#13;&#10;"/>
  </appSettings>
</configuration>

資產繼承

在資料資產中可以使用繼承,其提供開發者非常多的彈性(特別是當與多型方法一起使用的時候)。

繼承的基本步驟是建立一個抽象基礎資產類別(我們將繼續使用我們的 角色規格 實例):

C#

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

角色規格 的實體子類別可能會新增它們自己的自訂資料屬性,並且必須被標記為 可序列化的 類型:

C#

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

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

在DSL中

當您已經在DSL中宣告一個資產,您可以在DSL中使用asset_ref<T>類型,以參照到基礎類別和其任何子類別。

C#

component CharacterData {
  // Accepts asset references to CharacterSpec base class and its sub-classes(MageSpec and WarriorSpec).
  asset_ref<CharacterSpec> ClassDefinition;
  FP CooldownTimer;
}

如果您希望保留一個特定到一個子類別的參照,首先需要在DSL中使用asset import來宣告被衍生的資產:

C#

asset CharacterSpec;
asset import MageSpec;

如果在DSL中已經宣告被衍生的資產,只需使用asset_ref<T>作為基礎類別。舉例而言,為了使用直接地在DSL中MageSpec而非CharacterSpec,我們需要寫下列程式碼:

C#

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

由資料驅動的多型

讓遊戲遊玩邏輯直接地評估(在假如或轉換陳述式中)實體 角色規格 類別,將是一個非常糟糕的設計,所以當和多型方法搭配時,資產繼承更有道理。

請注意,新增邏輯到資料資產,意味著在quantum.state專案中執行邏輯,而這個邏輯仍然必須考慮以下限制:

  • 在暫時性遊戲狀態資料上操作:這意味著在資料資產中的邏輯方法必須收到暫時性資料作為參數(實體指標或是幀物件本身);
  • 只讀取,永不修改資產上的資料:資產必須仍然被對待為 不可變的 唯讀執行個體;

以下實例新增一個虛擬方法到基礎類別,並且新增一個自訂執行方式到子類別中的一個(請注意,我們在此文檔頂部使用了更多的針對 角色 實體所定義的 健康 欄位):

C#

namespace Quantum {
  partial unsafe abstract class CharacterSpec {
    public FP Speed;
    public FP MaxHealth;
    public virtual void Update(Frame f, EntityRef e, CharacterData* cd) {
      if (cd->Health < 0)
        f.Destroy(e);
    }
  }

  [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 override void Update(Frame f, EntityRef e, CharacterData* cd) {
      cd->Health += HealthRegenerationFactor * f.DeltaTime;
      base.Update(f, e, cd);
    }
  }
}

為了獨立地使用這個指派到各個 角色資料 的實體資產的有彈性的方法執行方式,這可以從任何系統被執行:

C#

// Assuming cd is the pointer to a specific entity's CharacterData component, and entity is the corresponding EntityRef:

var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
// Updating Health using data-driven polymorphism (behavior depends on the data asset type and instance assigned to character
spec.Update(frame, entity, cd);

在資產中使用DSL生成的架構

在DSL中定義的Structs也可以在資產上被使用。DSL架構必須以[Serializable]屬性來標註,不然資料在Unity中無法被檢查。

Unknown

[Serializable]
struct Foo {
  int Bar;
}

asset FooUser;

在一個Quantum資產中使用DSL struct

C#

namespace Quantum {
  public partial class FooUser {
    public Foo F;
  }
}

如果一個架構不是[Serializable]——友善地(比如,因為它是一個聯集或含有一個Quantum集合),可取而代之地使用原型:

C#

using Quantum.Prototypes;
namespace Quantum {
  public partial class FooUser {
    public Foo_Prototype F;
  }
}

在需要時,原型可以被具體化到模擬架構:

C#

Foo f = new Foo();
fooUser.F.Materialize(frame, ref f, default);

動態資產

在運行階段可以透過模擬來建立資產。這個功能稱為 動態資產DB

C#

var assetGuid = frame.AddAsset(new MageSpec() {
  Speed = 1,
  MaxHealth = 100,
  HealthRegenerationFactor = 1
});

該資產可以如同任何其他資產一樣被載入和處置:

C#

MageSpec asset = frame.FindAsset<MageSpec>(assetGuid);
frame.DisposeAsset(assetGuid);

動態資產不在同儕節點間同步。取而代之地,建立新資產的程式碼需要是確定性,並且確保各個同儕節點將使用相同的值來處理一個資產。

針對上述規則的唯一的例外是當有一個延遲加入的時候——新的客戶端將收到一個 動態資產DB 的快照以及最新的幀資料。不同於幀的序列化,動態資產的序列化及取消序列化是在模擬之外被委派到IAssetSerializer介面。當在Unity中運行時,預設使用QuantumUnityJsonSerializer:可以序列化/取消序列化任何可Unity序列化的類型。

初始化動態資產DB

可透過預先存在的動態資產來初始化模擬。類似於在模擬時新增資產,在客戶端間,這些需要是確定性。

首先,需要建立及以資產填入DynamicAssetDB的一個執行個體:

C#

var initialAssets = new DynamicAssetDB();
initialAssets.AddAsset(new MageSpec() {
    HealthRegenerationFactor = 10
});
initialAssets.AddAsset(new WarriorSpec() {
    Armour = 100
});
...

第二,需要使用QuantumGame.StartParameters.InitialDynamicAssets來傳遞執行個體到一個新的模擬。在Unity中,因為管理一個QuantumGame的是QuantumRunner行為,因此取而代之地使用QuantumRunner.StartParamters.InitialDynamicAssets

Back to top