はじめに
StrideでR3拡張パッケージを書いた時に、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エディタに設定値として現れる。
そして、以下の属性を追加する
-
Stride.Core.DataContractAttribute(必須)
- シリアライズ可能というマークを付けて、Strideエディタ側にコンポーネントとして追加可能な要素と認識させる
-
aliasName
はymlの中で省略系で記述できるようになるので設定しておいた方が便利- 設定しない場合は名前空間も含めたクラス名となる
- 派生コンポーネントを作る場合は
Inherited = true
にしておく -
System.Runtime.Serialization.DataContractAttribute
とは別物なので注意
-
Stride.Core.DisplayAttribute(任意)
- Strideエディタで表示する際の表示名を設定する
-
Stride.Engine.ComponentCategoryAttribute
- Strideエディタ上のコンポーネントリストに表示されるカテゴリ名を設定する
-
Stride.Engine.Design.DefaultEntityComponentProcessorAttribute(任意)
- コンポーネントに対するデフォルトの処理を行うものとして、
ComponentProcessor<TComponent>
あるいはComponentProcessor<TComponent, TData>
派生クラスのSystem.Typeを渡す
- コンポーネントに対するデフォルトの処理を行うものとして、
ExecutionModeについて
DefaultEntityComponentProcessorAttribute
はExecutionMode
という追加引数があるが、これはプロセッサを適用するタイミングを指定できる(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等のプロセッサーのソースをリンクしておく。
-
ScriptComponent
- SyncScriptやStartupScript等のベースクラス
- ScriptProcessor
-
ScriptSystem
- ScriptProcessorの中でScriptComponentがここに追加されて順次処理されていく
コード例
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
が使える。
ただし、OnSystemAdd
やOnSystemRemove
等、初期化時や終了時は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エディタ上のコンポーネントリストに出てこない(直接アセットファイルを編集して追加することは可能)。
- ライブラリ内部のどこかにpublic static voidのメソッドを作る
- クラス自体はinternalを推奨
-
Stride.Core.ModuleInitializerAttribute
をメソッドに付加 - メソッドの中で
Stride.Core.Reflection.AssemblyRegistry
のRegister(System.Reflection.Assembly, string category)
を実行する処理を追加- categoryは大体の場合
Stride.Core.Reflection.AssemblyCommonCategories.Assets
を指定する
- 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
- ECS/Usage
- ECS/Manage entities
-
Stride.BepuPhysics
- サードパーティパッケージとして追加できる物理エンジン
- コンポーネントの実装で物理エンジンの差し替えを実現している