This document is about: QUANTUM 2
SWITCH TO

コンポーネント

イントロダクション

コンポーネントはエンティティにアタッチできる構造体で、エンティティのフィルタリングに使用されます(アタッチされたコンポーネントにもとづき、アクティブなエンティティのサブセットのみを 反復します)。

カスタムコンポーネントのほか、Quantumには複数の組み込みコンポーネントが実装されています:

  • Transform2D/Transform3D: 定点(FP)値を使用した位置と回転。
  • PhysicsCollider, PhysicsBody, PhysicsCallbacks, PhysicsJoints (2D/3D): Quantumのステート物理エンジンで使用されます。
  • PathFinderAgent, SteeringAgent, AvoidanceAgent, AvoidanceOBstacle: NavMeshベースの経路探索と移動。

コンポーネント

これは、DSL内のコンポーネントのサンプル定義です。

C#

component Action
{
    FP Cooldown;
    FP Power;
}

構造ではなく(上記のように)コンポーネントとしてラベリングすると、適切なコード構造が生成されます(マーカーインターフェース、IDプロパティなど)。コンパイルが完了すると、これらはエンティティプロトタイプとともにUnityエディターで利用可能となります。エディターでは、カスタムコンポーネントは Entity Component ComponentName という名前になります。

コンポーネントに作用するAPIは、Frame クラスで表示されます。 コンポーネント上のコピーで作業するか、またはポインタ経由のコンポーネントで作業するかを選択できます。アクセスタイプで区別すると、コピー上で作業するAPIはFrame経由でアクセス可能で、ポインタにアクセスするAPIはFrame.Unsafeで利用可能です。これは、後者はメモリを修正するためです。

コンポーネントの追加、取得、設定に必要となるもっとも基本的な関数は、同じ名前の関数です。

Add<T>はエンティティにコンポーネントを追加するのに使用されます。各エンティティは特定コンポーネントの1つのコピーのみを保持します。デバッグの際、Add<T>AddResult enumを返します。

C#

public enum AddResult {
    EntityDoesNotExist     = 0, // The EntityRef passed in is invalid.
    ComponentAlreadyExists = 1, // The Entity in question already has this component attached to it.
    ComponentAdded         = 2  // The component was successfully added to the entity.
}

エンティティがコンポーネントを持つと、Get<T>で取得できるようになります。これはコンポーネント値のコピーを返します。コピー上で作業しているため、Set<T>を使用して修正された値を保存する必要があります。 Add メソッドと同様に SetResult を返し、オペレーション結果を検証したり、その結果に反応したりします。

C#

public enum SetResult {
    EntityDoesNotExist = 0, // The EntityRef passed in is invalid.
    ComponentUpdated   = 1, // The component values were successfully updated.
    ComponentAdded     = 2  // The Entity did not have a component of this type yet, so it was added with the new values.
}

たとえば、ヘルスコンポーネントの始めの値を設定するには、以下をおこないます:

C#

private void SetHealth(Frame f, EntityRef entity, FP value){
    var health = f.Get<Health>(entity);
    health.Value = value;
    f.Set(entity, health);
}

この表はすでに提示されたメソッドや、コンポーネントを操作するために提供されたメソッドを記載しています。値は以下を参照してください。

メソッド 戻り値 補足情報
Add<T>(EntityRef entityRef) 上記参照。 無効なエンティティ参照を許可します。
Get<T>(EntityRef entityRef) T
最新の値をともなう、Tのコピー
無効なエンティティ参照を許可しません。
エンティティ上にコンポーネントTが表示されない場合に例外をスローします。
Set<T>(EntityRef entityRef) SetResult enum、上記を参照ください。 無効なエンティティ参照を許可します。
Has<T>(EntityRef entityRef) bool
true = エンティティが存在し、コンポーネントがアタッチされます。
false = エンティティが存在しないか、またはコンポーネントがアタッチされません。
無効なエンティティ参照を許可し、
コンポーネントが存在しません。
TryGet<T>(EntityRef entityRef, out T value) bool
true = エンティティが存在し、コンポーネントがアタッチされます。
false = エンティティが存在しないか、またはコンポーネントがアタッチされません
無効なエンティティ参照を許可します。
TryGetComponentSet(EntityRef entityRef,
out ComponentSet componentSet)
bool
true = エンティティが存在し、コンポーネントのすべてのコンポーネントがアタッチされます。
false = エンティティが存在しないか、またはセットの1つ以上のコンポーネントが
アタッチされません。
無効なエンティティ参照を許可します。
Remove<T>(EntityRef entityRef) 戻り値がありません。
エンティティが存在し、コンポーネントを保持する場合にコンポーネントを削除します。
コンポーネントを保持しない場合には、なにもおこないません。
無効なエンティティ参照を許可します。

コンポーネントへの直接の作用を促進し、Get/Setの使用による若干のオーバーヘッドを回避するため、Frame.UnsafeはGetおよびTryGet (下表を参照ください)の安全でないバージョンを提供します。

メソッド 戻り値 補足情報
GetPointer<T>(EntityRef entityRef) T* 無効なエンティティ参照を許可しません。
エンティティ上にコンポーネントTが表示されない場合に例外をスローします
TryGetPointer<T>(EntityRef entityRef
out T* value)
bool
true = エンティティが存在し、コンポーネントがアタッチされます。
false = エンティティが存在せず、コンポーネントはアタッチされません。
無効なエンティティ参照を許可します。

シングルトンコンポーネント

シングルトンコンポーネント は特別な種類のコンポーネントで、任意の時点に1つのみ存在できます。特定のTシングルトンコンポーネントのインスタンスは、ゲームステート全体で任意のエンティティ上に1つのみ存在可能です。これはECSデータバッファのコアで実施され、またQuantumによって厳密に実施されます。

カスタムの シングルトンコンポーネント は、シングルトンコンポーネント を使用してDSL内で定義できます。

C#

singleton component MySingleton{
    FP Foo;
}

シングルトンはIComponentSingletonと呼ばれるインターフェースを継承し、このインターフェースはIComponentを継承します。このため、これは通常のコンポーネントと同様の処理をおこなえます。

  • エンティティへのアタッチが可能です。
  • 通常の安全なメソッド、および安全でないメソッドで管理できます(たとえばGet、Set、TryGetPointerなど)。
  • Unityエディタ経由でエンティティプロトタイプ上に設置でき、またはエンティティ上のコード内でインスタンス化が可能です。

通常のコンポーネントに関するメソッドのほか、シングルトン特有の特別なメソッドがあります。通常のコンポーネントと同様に、これらのメソッドは値型またはポインタのどちらを返すかで、 SafeUnsafe に分けられます。

メソッド 戻り値 補足情報
API - Frame
SetSingleton<T> (T component,
EntityRef optionalAddTarget = default)
void シングルトンが存在しない場合、シングルトンを設定します。
-------
EntityRef (任意)、どのエンティティにアタッチするかを指定します。
エンティティが存在しない場合、シングルトンを追加するためのエンティティが新規に作成されます。
GetSingleton<T>() T シングルトンが存在しない場合、例外をスローします。
エンティティ参照は不要で、自動的に検出されます。
TryGetSingleton<T>(out T component) bool
true = singleton exists
false = singleton does NOT exist
シングルトンが存在しない場合、例外をスローしません。
エンティティ参照は不要で、自動的に検出されます。
GetOrAddSingleton<T>(EntityRef optionalAddTarget = default) T シングルトンを取得して返します。
シングルトンが存在しない場合、SetSingleton内のように作成されます。
-----
作成する必要がある場合、EntityRef (optional任意)がどのエントリに追加するかを指定します。
EntityRefが渡されない場合、シングルトンを追加するための新しいエンティティが作成されます。
GetSingletonEntityRef<T>() EntityRef 現在シングルトンを保持しているエンティティを返します。
そのシングルトンが存在しない場合にスローします。
TryGetSingletonEntityRef<T>(out EntityRef entityRef) bool
true = シングルトンが存在します。
false = シングルトンが存在しません。
シングルトンを保持するエンティティを取得してください。シングルトンが存在しない場合にはスローしません。
API - Frame.Unsafe
Unsafe.GetPointerSingleton<T>() T* シングルトンポインタを取得します。
存在しない場合に例外をスローします。
TryGetPointerSingleton<T>(out T* component) bool
true = シングルトンが存在します。
false = シングルトンが存在しません。
シングルトンポインタを取得します。
GetOrAddSingletonPointer<T>(EntityRef optionalAddTarget = default) T* シングルトンを取得または追加し、返します。
シングルトンが存在しない場合には、シングルトンが作成されます。
-----
EntityRef (任意)、作成する必要がある場合、どのエンティティを追加するかを指定します。
EntityRefが渡されない場合、シングルトンを追加するための新しいエンティティが作成されます。

機能追加

コンポーネントは特別な構造なため、C#ファイル内に partial 構造定義を記述することでカスタムメソッドによって拡張できます。 たとえば、アクションコンポーネントは以下のように拡張できます:

C#

namespace Quantum
{
    public partial struct Action
    {
        public void UpdateCooldown(FP deltaTime){
            Cooldown -= deltaTime;
        }
    }
}

反応コールバック

反応コールバック固有のコンポーネントは2つあります:

  • ISignalOnAdd<T>: コンポーネントタイプTがエンティティに追加されると呼び出されます。
  • ISignalOnRemove<T>: コンポーネントタイプTがエンティティから削除されると呼び出されます。

これらは、コンポーネントの一部が追加/削除され、それらを操作する必要がある場合に非常に有用ですーたとえば、カスタムコンポーネント内でリストを割り当て、および割り当て解除する場合などです。

これらのシグナルを受信するには、シグナルをシステムに実装してください。

コンポーネントイテレーター

シングルコンポーネントのみが必要な場合には、ComponentIterator (安全) と ComponentBlockIterator (安全でない)が最適です。

C#

foreach (var pair in frame.Unsafe.GetComponentBlockIterator<Transform3D>())
{
    pair.Component->Position += FPVector3.Forward * frame.DeltaTime;
}

コンポーネントブロックイテレーターは、可能な限り最速のポインタ経由のアクセスを提供しますー

C#

foreach(var t in frame.Unsafe.GetComponentBlockIterator<Transform3D>()) {
    t->Position += FPVector3.Forward * frame.DeltaTime;
}

フィルター

フィルターは、一連のコンポーネントにもとづいてエンティティをフィルタリングするうえで便利な方法です。また、システムで必要とされるコンポーネントのみを取得できる点も有用です。フィルターは安全なコード(Get/Set)と安全でないコード(ポインタ)に使用できます。

ジェネリック

フィルターを作成するには、フレームが提供する Filter() を使用してください。

C#

var filtered = frame.Filter<Transform3D, PhysicsBody3D>();

ジェネリックなフィルターは最大8つのコンポーネントを含むことができます。 さらに詳細なフィルターが必要な場合には、withoutany ComponentSet フィルターを作成してください。

C#

var without = ComponentSet.Create<CharacterController3D>();
var any = ComponentSet.Create<NavMeshPathFinder, NavMeshSteeringAgent>();
var filtered = frame.Filter<Transform3D, PhysicsBody3D>(without, any);

ComponentSet は最大8つのコンポーネントを保持できます。 without パラメータとして渡された ComponentSet はセット内で指定されたコンポーネントの少なくとも1つを保持するすべてのエンティティを除外します。 any セットは、エンティティが少なくとも1つ以上の指定したコンポーネントを確実に保持するようにします。エンティティに指定されたコンポーネントがない場合、このエンティティはフィルターによって除外されます。

フィルターでの反復は、filter.Next()でwhileループを使用するのと同様に簡単です。これによって、コンポーネントのすべてのコピーや、それらにアタッチされたエンティティのEntityRefが充填されます。

C#

while (filtered.Next(out var e, out var t, out var b)) {
  t.Position += FPVector3.Forward * frame.DeltaTime;
  frame.Set(e, t);
}

注: 現在はコンポーネントのコピーによって、またコンポーネントのコピー上で反復しています。このため、各エンティティに新しいデータを設定しなおす必要があります。

ジェネリックフィルターは、コンポーネントポインタとの協業も実現しています。

C#

while (filtered.UnsafeNext(out var e, out var t, out var b)) {
  t->Position += FPVector3.Forward * frame.DeltaTime;
}

このインスタンスでは、コンポーネントのデータを直接修正しています。

FilterStruct

通常のフィルターのほか、FilterStruct アプローチを使用する必要が生じる場合があります。 この場合には、受信を希望する各コンポーネントタイプについて、まず public プロパティで構造を定義する必要があります。

C#

struct PlayerFilter
{
    public EntityRef Entity;
    public CharacterController3D* KCC;
    public Health* Health;
    public FP AccumulatedDamage;
}

ComponentSet と同様に、FilterStruct は最大で8つのコンポーネントポインタをフィルタリングできます。

注: FilterStruct として使用される構造は、EntityRef フィールドを保持する必要があります!

FilterStruct 内の コンポーネントタイプ メンバーは、ポインタである必要があります。 ポインタのみがフィルターで充填されます。コンポーネントポインタに加えて、 他の変数を定義できます。フィルターはこれらの変数を無視するため、管理は自分で行わなければなりません。

C#

var players = f.Unsafe.FilterStruct<PlayerFilter>();
var playerStruct = default(PlayerFilter);

while (players.Next(&playerStruct))
{
    // Do stuff
}

Frame.Unsafe.FilterStruct<T>() には、フィルターをさらに指定するために任意のComponentSets any および without を使用する多重定義があります。

Countについての注

フィルターは、何個のエンティティにタッチし、繰り返すかを事前に把握していません。これは、フィルターが Sparse-Set ECS内で作動する方法によるものです:

  1. フィルターは、提供されるコンポーネントのうち関連するエンティティがもっとも少ないものはどれか(共通部分を確認するための、より小さなセット)を検出し、また
  2. フィルターはセット内すべてを検索し、クエリされたその他のコンポーネントを持たないエンティティを破棄します。

事前に正確な数を把握するには、フィルターを1度通過する必要があります。これはオペレーションに該当するため、効率的ではありません。

コンポーネントゲッター

特定のコンポーネントのセットを 既知の エンティティから取得したい場合は、Frame.Unsafe.ComponentGetterと組み合わせてフィルタ構造体を使用します。N.B.: これは安全ではないコンテキストでのみ利用可能です!

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    public override void Update(Frame f) {
        MyFilter result = default;

        if (f.Unsafe.ComponentGetter<MyFilter>().TryGet(f, f.Global->MyEntity, &result)) {
            // Do Stuff
        }
    }

この操作を頻繁に行う必要がある場合は、以下のようにシステム内のルックアップ構造体をキャッシュすることができます(100%安全です)。

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    ComponentGetter<MyFilter> _myFilterGetter;

    public override void OnInit(Frame f) {
      _myFilterGetter = f.Unsafe.ComponentGetter<MyFilter>();
    }

    public override void Update(Frame f) {
      MyFilter result = default;

      if (_myFilterGetter.TryGet(f, f.Global->MyEntity, &result)) {
        // Do Stuff
      }
    }

フィルタリングのストラテジー

多くのエンティティがあるが、サブセットのみがほしいという状況がしばしば発生すると思います。以前は、Quantumで利用可能なコンポーネントとツールを導入してフィルタリングをおこなっていました。このセクションでは、これらを活用するストラテジーについて記載します。 注: 最適なアプローチは、個々のゲームやシステムによって異なります。個々の状況に適した方法を検討するための起点として、以下のストラテジーを推奨しています。

注: 以下で使用される用語は社内で作成されたものであり、言語的な概念を要約しています。

Micro-component

多くのエンティティが同じコンポーネントタイプを使用する可能性がありますが、同じコンポーネントの構成を使用するエンティティはほとんどありません。構成を特殊化する方法の1つが micro-components の使用です。micro-components は特定のシステムまたは挙動のためのデータをともなう、高度に特殊化されたコンポーネントです。この特殊性によって、コンポーネントを保持するエンティティを迅速に特定できるフィルターの作成が実現されます。

Flag-component

エンティティを特定できる一般的な方法の1つが、flag-component の追加です。ECSには、フラグ の概念が元来存在しません。Quantumサポート エンティティ タイプも同様です。それでは、 flag-components とは具体的に何でしょうか?これはデータをまったく、またはほとんど含まないコンポーネントで、エンティティを特定する排他的な目的で作成されています。

たとえば、チームベースのゲームでは以下を設定することが可能です:

  1. TeamAおよびTeamB向けのenumをともなう「Team」コンポーネント、または
  2. 「TeamA」および「TeamB」コンポーネント

上記のオプション1は、ビューからのデータのポーリングが主な目的な場合に役立ちます。一方、オプション2は関連するシミュレーションシステム内でパフォーマンスをフィルタリングしたい場合に有用です。

注: flag-componentはtag-componentと呼ばれる場合もあります。これは、エンティティのタギングとフラギングは交互に使用されるためです。

追加/削除

エンティティへのflag-componentまたはmicro-componentのアタッチが一時的に必要な場合には、Add操作もRemove操作もO(1)なため、これらのコンポーネントは依然として適切なオプションとして存在します。

グローバルリスト

flag-componentの代替として、ECS的な度合が減りますがFrameContext.User.cs内にグローバルリストを保持することができます。Nチームのトラッキングを保持する必要がある場合、これは不要なスケールですが、サブセットが限定されているセットの場合には役立ちます。

ヘルスが50%未満のすべてのプレイヤーをハイライトしたい場合には、グローバルリストを保持して以下をおこなうことが可能です。

  • リストにentity_refsの追加/削除をおこなうシミュレーションの最初にシステムを設定する、
  • 後続のすべてのシステムで同じリストを使用する。

注: これらのタイプを単発的に認識する必要がある場合には、グローバルリストを保持するのではなく、必要に応じて動的に計算をおこなうことを推奨します。

Back to top