LoginSignup
0
0

Strideでコンポーネントを作成、共有する

Last updated at Posted at 2024-02-27

はじめに

StrideR3拡張パッケージを書いた時に、FrameProvider用にコンポーネントを書いたので、その時にやったことや注意点等を書いていく。

なお、この記事は記事執筆時点の最新版である4.2.0.2067で確認をしている。
今回の件で検証として書いたソースコードは以下
https://github.com/itn3000/strideexternalnuget

コンポーネントとは

Strideでは、シーン内部の配置オブジェクトはEntity(Unityで言う所のGameObjectに当たる)という型で表現される。
Entityは単体では名前とID、タグ程度しか持たないものだが、そこにモデル表現や物理特性や光源等、様々な属性を持たせるため、コンポーネントを追加していく。

例えば何かの形を表現したい場合はModelComponent、何かプログラム的な処理を走らせたいのであればSyncScriptやAsyncScript、といった具合である。位置や大きさに関してはTransformComponentがデフォルトで設定されている。
コンポーネント自体は属性を定義したものに過ぎないので、そのコンポーネントに対しての処理をEntityProcessor<TComponent, TData>で行うという関係になっている。
複数コンポーネントの協調等の複雑な処理を行う場合は、更にStride.Games.GameSystemBaseに集約する。
例として、AsyncScriptやSyncScriptはScriptProcessorの処理中でScriptSystemに登録され、順番等が整理されて各種コンポーネントの処理が実行されている。

この辺りの用語や型の関係性等については、Entity-Component-System(ECS)パターンに基づいているので、ゲーム作成経験者には馴染みのある人も多いかもしれない。

Strideで用意されているコンポーネントだけでもある程度の形にはできるが、例えば別の物理エンジンを使いたい場合等、既存コンポーネントの組み合わせでは実現できないような事をしたい場合、独自のコンポーネントの作成を検討することになる。

プロジェクトの準備

同じプロジェクト内で使う場合は、普通にクラスの作成を行えばStrideエディタで設定できるようになる。

共有目的でライブラリ化する場合、Strideのランチャーから"Code Library"プロジェクトを新規作成する。
この時、AndroidやiOSのSDKを入れていないとリストアに失敗してエディタが起動しない が、失敗確認後、テキストエディタ等でcsprojを開き、TargetFrameworks要素の中身を"net8.0"のみにすれば、エディタの起動及びビルドが可能になる。
プラットフォーム依存のライブラリを書く場合に改めてTargetFrameworks要素に追加すれば良いだろう。

また、依存NuGetパッケージで最低限必要なのはStride.Engineのみなので、他は消しても構わない。
UIメニュー等の拡張を作りたい場合等、必要になったら追加するという運用で問題ない。

クラスの作成

新規コンポーネントの作成

一からコンポーネントを作成したい場合は、EntityComponentクラスを継承してクラスを作ることになる。
リファレンスを見るとわかると思うが、abstractなメソッド等は無いので、定義だけして中身は空というものも作ることが可能(あまり意味はないが)。
ここで追加したpublicなプロパティがStrideエディタに設定値として現れる。

そして、以下の属性を追加する

ExecutionModeについて

DefaultEntityComponentProcessorAttributeExecutionModeという追加引数があるが、これはプロセッサを適用するタイミングを指定できる(enum bitなので複数指定可)。
例えば見た目に影響を与えるようなコンポーネントに関しては、ExecutionMode.EditorやPreviewの設定をしないと、Strideエディタ上でパラメーターを変えても、エディタの見た目に反映されない状態になる。

見た目に影響しないコンポーネントであれば、ExecutionMode.Runtimeだけ指定していれば、エディタはEntityProcessorを通さないので無駄な処理を省くことができる。

また、Runtimeでは問題ない処理だがEditorやPreviewの時に触ってはいけない領域もたまにあったりする。
この辺りは筆者も余り把握してないので、問題が起きたら都度対処していくことになる。

コード例

    // アセットファイル上の名前
    // Inherited = trueをすると、派生クラスにも設定値が現れる
    [Stride.Core.DataContract(nameof(PiyoEntityComponent))]
    // Strideエディタ上の表示名
    [Stride.Core.Display("Sample Entity Component")]
    // Strideエディタ上のカテゴリ表示名
    [Stride.Engine.ComponentCategory("Category Name")]
    // デフォルトで使用するEntityProcessor(後述)
    [Stride.Engine.Design.DefaultEntityComponentProcessor(typeof(PiyoEntityProcessor))]
    public class PiyoEntityComponent: EntityComponent
    {
        // オーバーライドするメソッドは無し
        // publicプロパティがエディタ上に設定値として現れる
        // Display属性を付ければStrideエディタ上の表示名を変更可能
        [Display("Value1 Display Name")]
        public int Value1 { get; set; }
        // アセットファイルとしては値を残さず、プログラム上でのみ値を操作したい場合に使う
        [DataMemberIgnore]
        public int Value2 { get; set; }
    }

EntityProcessorの作成

コンポーネント自体は基本的にデータを保持するものなので、具体的にそのデータをどのように処理するのかを記述する必要がある。
処理の記述はEntityProcessor<TComponent, TData>を派生したクラスを使う。
EntityProcessor<TComponent>という型もあり、普通の運用では大体こちらを使うのだが、これは実際はEntityProcessor<TComponent, TComponent>の派生という型なので、この記事ではEntityProcessor<TComponent, TData>について書く。

これも基本的にはabstractなメソッドやプロパティは無いので任意のメソッドをオーバーライドして使用することになる。オーバーライドできるメソッドの中でも良く使うものをここに書く。残りはリファレンスを見て欲しい。
なお、EntityProcessor<TComponent>でオーバーライド可能なメソッドにはTDataが無い以外は同じとなる。

  • TData GenerateComponentData(Entity, TComponent)
    • TDataを生成する時に呼ばれる
    • EntityProcessor<TComponent>の場合はTComponentがそのまま返される事と同じになる
    • コンポーネント登録時に呼ばれる
  • OnSystemAdd()
    • EntityProcessorがシステムに追加された時に呼ばれる
    • 大体の場合はアプリケーションの初期化時に一回呼ばれるものと思っていい
  • OnSystemRemove()
    • EntityProcessorがシステムから削除される時に呼ばれる
    • 大体の場合はアプリケーションの終了時に一回呼ばれるものと思っていい
  • OnEntityComponentAdding(Entity, TComponent, TData)
    • Entityに対してComponentが追加される時に呼び出される
    • コンポーネントの初期化等を行いたい場合は大体ここで行う
    • 最後にbaseを呼び出せば大体うまくやってくれる
  • OnEntityComponentRemoved(Entity, TComponent, TData)
    • Entityに対してComponentが削除される時に呼び出される
    • コンポーネントの終了処理をここで書く
  • ProcessEntityComponent(Entity, EntityComponent, bool)
    • OnEntityComponent*の前に呼ばれる
    • Removedの時は最後のboolがtrueになる
    • 第二引数がTComponentではないので、こちらでは最小限のチェックにしておくことを推奨する
  • Update(GameTime gameTime)
    • 各メインループ内で一回実行される
    • 複雑な処理をする場合はここではなくGameSystemBaseを派生させてそちらにEntityとComponentを登録するのが吉

参考例として、SyncScript等のプロセッサーのソースをリンクしておく。

コード例

    public class MyEntityProcessor: EntityProcessor<MyEntityComponent, MyData>
    {
        protected override void OnSystemRemove()
        {
            base.OnSystemRemove();
        }
        protected override void OnSystemAdd()
        {
            var game = Services.GetService<IGame>();
            base.OnSystemAdd();
        }
        protected override void ProcessEntityComponent(Entity entity, EntityComponent entityComponentArg, bool forceRemove)
        {
            var game = Services.GetService<IGame>();
            base.ProcessEntityComponent(entity, entityComponentArg, forceRemove);
        }
        protected override void OnEntityComponentAdding(Entity entity, [NotNull] MyEntityComponent component, [NotNull] MyData data)
        {
            var game = Services.GetService<IGame>();
            base.OnEntityComponentAdding(entity, component, data);
        }
        protected override void OnEntityComponentRemoved(Entity entity, [NotNull] MyEntityComponent component, [NotNull] MyData data)
        {
            var game = Services.GetService<IGame>();
            base.OnEntityComponentRemoved(entity, component, data);
        }
        public override void Update(GameTime time)
        {
            foreach((MyEntityComponent k, MyData v) in ComponentDatas)
            {
                // 何かの処理
            }
            base.Update(time);
        }
    }

EntityProcessorの中でオブジェクトにアクセスする

EntityProcessorのコールバックの中で、例えば現在のUpdateTimeを取得したい場合はGameインスタンスにアクセスする必要があるが、こういう場合はStride.Core.ServiceRegistry Servicesを通してIGameオブジェクトを取得できる。
また、現在のシーン中にあるEntityを探すだけならばStride.Engine.EntityManager EntityManagerが使える。
ただし、OnSystemAddOnSystemRemove等、初期化時や終了時はnullになっている項目がある可能性に注意すること。

プログラムで後からEntityProcessorを追加する

EntityProcessorはSceneSystem.SceneInstance.Processorsで管理されているので、
最初に属性でEntityProcessorを指定する他に、後からEntityProcessorを追加するには、Processors.Addで追加を行う。追加を行うと、EntityProcessorのOnSystemAddが発生する

既存コンポーネントからの派生

EntityProcessorは必ずしも作る必要は無く、既存Componentから何かの処理に特化したものを作りたい場合は、単にそのComponentから派生したクラスを作成すればOK。
例えば、SyncScriptを派生してUpdate()をオーバーライドすれば、何かの処理を必ず定期的に行うComponentになる。
例えばR3のStride拡張では、FrameProviderのRunメソッドを定期的に走らせる必要があるが、それをSyncScript派生のコンポーネントを作って行っている。ただし、初期化の関係上EntityProcessorを更に設定しているので注意。
https://github.com/Cysharp/R3/blob/1.0.4/src/R3.Stride/R3FrameDispatcherComponent.cs

他のプロジェクトに公開する

他のプロジェクトに追加する場合は、追加したいプロジェクトに"ProjectReference"を追加するか、nupkg化した上で"PackageReference"を追加する。
基本的にはリフレクションで持ってくるので、普通のクラスライブラリと同じように扱えて、また、Strideエディタをリロードすれば、エディタのコンポーネントリストにも出てくる。

NuGetパッケージとして公開するときの注意点

基本的にProjectReferenceする場合は必要ないが、nuget公開する場合は以下の処理を行わないとStrideエディタ上のコンポーネントリストに出てこない(直接アセットファイルを編集して追加することは可能)。

  1. ライブラリ内部のどこかにpublic static voidのメソッドを作る
    • クラス自体はinternalを推奨
  2. Stride.Core.ModuleInitializerAttributeをメソッドに付加
  3. メソッドの中でStride.Core.Reflection.AssemblyRegistryRegister(System.Reflection.Assembly, string category)を実行する処理を追加

ModuleInitializerAttributeはベースクラスライブラリのModuleInitializerAttributeと役割はほぼ一緒だが、Strideエディタからのロード時等にも動くようになっている。

コード例

internal class Module
{
    [ModuleInitializer]
    public static void InitializeModule()
    {
        AssemblyRegistry.Register(typeof(Module).GetTypeInfo().Assembly, AssemblyCommonCategories.Assets);
    }
}

終わりに

実は最後のNuGetパッケージの件を書きたくて書き始めたが、思ったより長くなってしまった。
コンポーネントとEntity周りはStrideの中核に近い機能なのでやれることも多くなっているが、それでも使いこなせれば色々なことができるようになると思う。
正直AssemblyRegistry.Registerの件は罠だと思うが、リフレクションの重さを考えるとああいう仕様になるのかなという気にもなりつつ、とりあえずどうにか使いやすい形に落ち着いて欲しい。

参考URL

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0