Unity ECSを使ってゲームを作っています
はじめに
Happy Elements株式会社 カカリアスタジオで新規タイトルを開発中のチームで働いているU.S.です。
カカリアスタジオに転職する前は動画用のSoC上で動作するOSを作っていました。
入社するまでは、ほぼ組み込み系のプログラムを書いていたので、通信やインフラやDBやらと業務で触れてこなかった分野ばかりで大変でした。
今年の初めごろから新規タイトルを開発しています。
カカリアスタジオではゲームのクライアントはUnityで作っており、新規タイトルも同様にUnityで開発することにしました。
開発中のゲームが今まで以上にパフォーマンスを求められる可能性が高い仕様であるため、ECSを積極的に取り入れてゲーム開発をおこなっています。
Unity DOTSでマルチプレイヤーゲームを作ろうとしてつまづいた話
弊社のゲームエンジニアが去年のアドベントカレンダーで書いたときとは状況が変わり、数年間動きがなかったECSもバージョンアップされてUnity2021でも動くようになり、つい先日 1.0(Experimental版)がリリースされました。
ECSはメモリ効率などには利点がある反面、デバッグのしづらさなども感じています。
ECSの詳細や使い方については多くの解説サイトがあるので割愛し、本記事では数ヶ月間、ECSでゲームを作った経験をもとに感じたことを書きたいと思います。
※ 本記事はECS 0.51版での開発をもとに書かれています。
所属グループでの主な業務内容
現在は新規タイトルを開発中です。
ゲームエンジニアとして、Unityを使ってクライアントサイドの開発を行なっています。
Unity ECSとは
今までのGameObjectを使用したコンポーネント指向とは異なり、ECSはデータ指向のフレームワークです。
ECSソフトウェアアーキテクチャパターンを用いて開発することで、効率の良いデータ配置を実現することができるようになり、Job Systemと連携することでCPUのマルチコア対応が容易であったりと、パフォーマンスアップが期待できます。
ECSを実際に使ってみて良いところ
メモリ効率と速度
GameObject版を作って比較したりはできていませんが、メモリ効率が良く速度も速いようです。
現時点ではECSに対応したミドルウェアがほとんどなくGameObjectを一緒に使うHybrid版として開発しているのですが、順次対応されていくと思います。
ほとんどのものをGameObjectではなくPure ECSで表現できるようになった時、どうなるんだろうと楽しみです。
GCの抑制
Entityは、C#のマネージドヒープを基本的に使わないように設計されているため、GCの発生頻度を低減することができ、ゲームの大事な場面でのスパイク(プチフリーズ)を抑制することができます。
Job SystemやBurstコンパイラーとの連携
Job Systemはとても良くできており、スレッドセーフではない書き方をしているとエラーを出してくれたりと安心してマルチスレッドを使うことができるようになっていてありがたいです。
Burstコンパイラーも速度向上に貢献してくれるはずです。
実際に使ってみて不便なところ
現在はECS 0.51版を使っており、1.0では解消されるかもしれませんが、不便に感じるところはあります。
普段意識しないような書き方が必要
Job SystemやBurstコンパイラーとセットで使用するためには、Componentはstruct型で宣言する必要があります。
また、マネージドヒープを使用しないような作りにするためにポインタを駆使する必要があったり、クラスのインスタンスが使えなかったり、クラスのメンバ変数は一度ローカル変数に受けないといけなかったり、と戸惑うことが多かったです。
最初はなぜコンパイルエラーが出るのかわからないこともありました。
GameObjectでの開発ならささっとできるところが、ECSだとどう作ったら良いだろうと考えたり調べたりするのに時間がかかっています。
ComponentがどのSystemで処理されているかが追いづらい
Systemと1:1になっているComponentは問題ないのですが、Translationみたいな汎用的なComponentはどこで処理したかがわからなくなりました。
例えば、プレイヤーを移動させるSystemと、攻撃を受けて吹っ飛ぶSystemの両方でTranslationを変更しているような場合です。
設計でプレイヤーのTranslationを変更するSystemは1つだけみたいにするのもひとつの方法ですが、今回はグラフ化するようにしました。
GitHub Actionsを使って、日時バッチでソースファイルを解析して、どのComponentがどのSystemで使われているかをグラフにしています。
デバッグがしづらい
Entity化されるとヒエラルキーからは見えなくなります。
キャラクターが10体出ているとすると一番左にいるキャラクターがどのEntityか見つけるのがすごく大変です。
こちらはEntityに一意な名前をつけたりすることで何とかなるのですが、一番困ったのは当たり判定でした。
プレイヤーの飛び道具が敵に当たっているように見えるのですが、衝突判定が行われず悩みました。
結論としては、GameObjectとして当たっているように見えるのですが、ECSでのTranslationの位置を更新できていなかったためPhysics Colliderが初期位置から動いていないことが原因でした。
ここらへんはGameObjectとECSとの連携がまだまだうまくとれていないからですね。
SyncGameObjectAuthoringというclassを作成して、SyncGameObjectComponentをEntityにAdd、SyncGameObjectSystemでTransformとTranslationの同期を取るようにして解決してみました。
・SyncGameObjectAuthoring
public sealed class SyncGameObjectAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
[SerializeField] SyncGameObjectComponent syncGameObjectComponent;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
dstManager.SetEntityName(entity, $"SyncWith {name}");
dstManager.AddComponentData(entity, syncGameObjectComponent);
}
}
・SyncGameObjectComponent
[Serializable]
public struct SyncGameObjectComponent : IComponentData
{
}
・SyncGameObjectSystem
public partial class SyncGameObjectSystem : SystemBase
{
protected override void OnUpdate()
{
Entities
.WithoutBurst()
.WithAll<SyncGameObjectComponent>()
.ForEach((
Entity entity,
Transform transform,
in Translation translation,
in Rotation rotation
) => {
transform.localPosition = translation.Value;
transform.localRotation = rotation.Value;
}).Run();
}
}
ECS 1.0ではGameObjectとの連携を強化するとあったので、何かしら対応が入っているかもしれません。
またPlay中、EntityにAddされたPhysics Colliderは画面上のどこからも表示確認できなくなってしまうため、確認がしづらく、上記のような問題に気づきにくかったです。
対策として、Physics確認モードのようなものを作成し、Scene Viewでクリックした位置からRayを飛ばして、Physics Colliderに当たったらそのコライダを表示するような機能を作りました。
現在のGameObjectでの開発と同様にヒエラルキーみたいなものに一覧表示されて、選択したら、Scene上でコライダを表示してくれるようになると良いのになと思っています。
・Unity DOTS roadmap( https://unity.com/ja/roadmap/unity-platform/dots ) Editor Windows & Workflows より
Hierarchy window: Supports the use of both GameObjects and entities
ECS 1.0の正式版では様々な改善が予定されており、ヒエラルキーウィンドウでのEntityサポートもできるようになりそうです。
Component増えていく問題
まだアプリの仕様も固まっていない状態なので、いろいろ試作をしている段階です。
その中でComponentの整理とかもできず、こういう機能が欲しい => ではComponentとSystemを作りますねといった風に、どんどんComponentとSystemのセットが増えていきます。
ある程度はまとまった機能単位で処理をまとめているのですが、どうしても〜Componentが増えていってしまっています。
例えば敵キャラのHpが0になったときに処理をさせたいような場合です。
敵キャラのHpが0になったらEntityにDyingComponentをAddして、DyingSystemで処理をさせています。
・アニメーションさせて欲しい
・音楽鳴らして欲しい
・他の敵キャラが登場するようにして欲しい
といった要望があり、今後も増え続けていきそうです。
Componentの中にTypeのようなものを定義してそこで種別を定義するのか、それともAnimationOnDyingComponentのようなものを増やしていくのかは悩ましいところです。
終わりに
まだまだ使いづらかったり慣れが必要だったりと、不便なところも感じるECSですが、パフォーマンスが良いというところは非常に魅力的です。
売り切りではなく運用型のタイトルは、年数を重ねるたびに仕様や、やりたいことがどんどん増えていきます。
その時に「性能が限界なのでそういうことはできないです」と言わなくていいように、今からECSを使って開発していけたらと思います。
Discussion