テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】ECSで簡単なゲームを作ってみたので、その解説

f:id:tsubaki_t1:20180519211935j:plain

ECSで技術デモ的な物は結構あるのですが、ゲーム的になっているものが余り無いので、ECSで簡単なゲームを作ってみました。

 

 

そうだ、ECSで球転がしを作ろう

ふと思いついてECSで球転がしのゲームを作ってみました。所謂Roll a BallでUnityで多分一番簡単に作れるゲームの一つです。
ルールは単純で「ステージ内に配置した全てのコインを取得すれば勝ち」というもの。
プレイヤーは球を操作してステージ内のコインを全て回収します。残りの個数が左上に表示され、これが0になれば勝ちです。

なお、まだECSは機能が足りない状態なので一部車輪の再開発をします

f:id:tsubaki_t1:20180519221046j:plain

プロジェクトは下のリンクに配置しておきます。
前提としてUnity 2018.1f以降が必要です。ECSを動せるのはそのバージョンからです。

github.com

ステージの作成は今までどおりシーンに作る

まずステージの作成ですが、これは今までどおりです。つまり単純にモデルを置いても良いですし、今ならProBuilderでガガっと作っても良いです。
CameraやLightの設定、ポストプロセス等も普通に配置していきます。

f:id:tsubaki_t1:20180519221723j:plain

球のコントロールはGameObjectをシステムからコントロール

プレイヤーが操作する球のコントロールについてです。これはRigidbodyを使いたいのでGameObjectとECSを連携するアプローチを使用します。

GameObjectEntityを使用すると、ECSのSystemからGameObjectの各コンポーネントへアクセスすることが可能になります
この機能を使用して、球の持つRigidbodyへアクセスしていきます。

f:id:tsubaki_t1:20180519222325j:plain

システムが球がプレイヤーかどうかを識別する方法は、ラベルとしてPlayerコンポーネントをもたせる事で実現します。Systemの要求するグループに「Rigidbody」と「Player」を指定する訳です。
この仕組は中々に楽で、Inspectorでオブジェクトをドラッグ&ドロップ等をしなくてもシーン内の特定条件のオブジェクトを見つけ出してくれます。

f:id:tsubaki_t1:20180519223114j:plain

f:id:tsubaki_t1:20180519223735j:plain

このGameObjectをECSの文脈で制御するアプローチは、ECSのメモリレイアウト的にはそれ程メリットを生みませんが、そもそも数が少なければ問題ないという判断です。
それよりInjectによるバインドの簡略化、システム単位で制御がコントロール出来る点(後述)、Rigidbodyの挙動を作るのが面倒くさい、そして何よりシーン上で調整出来る点などの方が楽で良いです。

GameObjectの情報はECSにアップロードしておく

上で球の操作はRigidbody&Playerラベルという二つの要素でコントロールしていました。これは前述下通り数が少なければ問題無いのですが、関係するデータの数が増えてくる場合や並列処理したい場合にはComponentDataとして登録しておきたいです。
なのでEntityに球のPositionを毎フレームアップロードしておきます。

その役割はPositionComponentCopyTransformFromGameObjectComponentが果たします。

f:id:tsubaki_t1:20180519224955j:plain

CopyTransformFromGameObjectComponentは、PositionにTransformの現在値を毎フレーム渡してくれるシステムを呼び出します。
要するに他のSystemからアクセスする際にTransform(ComponentArray)ではなくPosition(ComponentDataArray)経由でアクセス可能になる訳です。これでC#JobSystemなどで使いやすくなります

 

また今回はRigidbody(毎フレーム座標を更新される)を使用しているので値の同期にCopyTransformFromGameObjectComponentを使用していますが、もし単純に初期座標が欲しい(GameObjectも使う)という場合にはCopyInitialTransformFromGameObjectComponentを使用するのが良さそうです。例えばAnimatorが必要だからGameObjectを使うが、座標を毎フレーム更新しない…みたいな場合です。

 

カメラが球を追跡するシステムでは、二つのグループを要求

カメラの追跡です。カメラの追跡システムは「カメラ」と「追跡対象(プレイヤー)」の二つで成り立つので、システムも二つのグループを要求します。
なおCameraコンポーネントはラベル的な用途です。

f:id:tsubaki_t1:20180519230318j:plain

なおPositionコンポーネントはVector3ではなくfloat3が格納されているので、加算する座標データもfloat3で指定しています。

 

あ、Entityに登録するのでGameObjectEntityを忘れずに!
(自分は忘れて30分近く悩んでしまった)

f:id:tsubaki_t1:20180519230932j:plain

 

コインの生成はPrefabをEntity化して配置

球が回収するコインを生成していきます。このコインはソコソコの量があるので、出来ればピュアなECSのような形で生成したい所ですが、コードで全部制御するのは正直面倒くさいので避けたいです。
そこで、PrefabからEntityを取得して生成するアプローチを取ります。

Entityの生成はPrefabSpawnerで行っています。

f:id:tsubaki_t1:20180519231852j:plain

まずはEntityとしてPrefabを作成します*1。PrefabにはEntityに含めるコンポーネントを設定していきます。

例えばコインの識別のためにItemComponent、描画のためにMeshInstanceRenderComponent、座標のアクセスがマトリクスなのは面倒なのでpositionとrotationComponentなどです。
これらのコンポーネントの幾つかは、今までのUnityと同じようにInspectorから内容を調整が出来ます。例えばMeshInstanceRendererComponentで何を描画するか・描画設定などを調整しています。

f:id:tsubaki_t1:20180519231907j:plain

このPrefabをEntityManagerのInsntaitateで複製し、座標を登録していきます。ここでEntityの量が超多いならJobで並列に処理してしまうのも良いかもしれません。

f:id:tsubaki_t1:20180519232536j:plain

なおentityManager.Instantiateで生成されるEntityに含まれるのはComponentDataのみです。ComponentDataWrapperやTransformを始めとしたコンポーネント群はココに含まれないので、そこんとこ注意(安心?)です。

 

なおコインの配置自体はMonobehaviourを継承したコンポーネントで行っています。これはシーン上にコインの情報を焼き付けたかったからです。

f:id:tsubaki_t1:20180519233339j:plain

 

当たり判定は並列化、Mathematicsライブラリも活用

プレイヤーと各コインの当たり判定ですが、これは自前で適当なものを用意しました。これはコインをピュアなECSにした関係上EntityがColliderを持てなくなったのが原因です。
Collider辺りはまだECSに対応していないので、ピュアなECSを使用しようと思ったら車輪を再開発する必要があります。
内容? 単純に総当たりで距離計算ですよハハ

この当たり判定ですが、JobComponentSystemで並列化しています。JobSystemに乗せる事でBurstや新しいMathライブラリの恩恵を受けられる為です。この時プレイヤーの座標はGroupを使ってInjectして持ってくればOKです。

なお接触時にオブジェクトを削除する訳ですが、ここでは行わず一旦ラベルに値を付けて後で行っています。
これはやり方が分からなかったのが本音ですが、削除時にエフェクトを出したかったという部分もあります。

f:id:tsubaki_t1:20180519234959j:plain

Entityの削除はHitcheckとは別に後で行います。この時System間の依存関係を作っておくと安心して使えます。

また削除時にパーティクルも出してみました。DestroyEntityも一緒に呼んでますが、PostUpdateCommandsの名前から即座に実行じゃないと思うんで、まぁ良いかなと。

なおピュアなECSの場合、生成・削除コストはかなり安いので、下手にプーリングするよりは生成・破棄しまくったほうが良いんじゃないかなって意見があります。

 

ゲームの進行はコンポーネント、システムのON/OFF管理も担当

最後にゲームの進行管理ですが、これはコンポーネント側で行いました。何故かといえばコルーチンが使いたかったからです。
ECSには現状コルーチンに該当する機能がないので、その辺りをやろうと思ったらコンポーネント使ったほうが楽です。UniRX的な物でも良いかもしれません。
あとシーン毎に要求するフェーズがあるかもしれないので、その辺りも含めて。

さて、この部分では各フェイズ毎に動かすシステム一覧を決めて制御しています。
例えばプレイヤーの入力システムなどはPLAY時には有効ですが、それ以外のINITとCLEAR時に停止させたりしています。

f:id:tsubaki_t1:20180520000629j:plain

またゲームの進行把握にどうしてもEntityの情報がほしかったので、グループを作って観測するようにしました。Disposeし忘れは、普通に忘れてました。

グループの作成がMonobehaviour側でできなくなったみたいです。

 

感想

簡単な玉転がしをECSにしてみました。

どうしてもピュアなECSを使用すると書くコードが多くなるので今は少しシンドイ所がありますが、割と今までどおりの感じで作れるなという印象でもあります。

むしろInjectをガンガン使うとSceneでやってたドラッグ&ドロップとか、Singleton的なアレがスッキリするんで使いやすいと思った部分もあります。

まぁ、もう少し機能が増えてきたら楽になりそうですが…Collider! Animator! SpriteRendere! SphereCheck!

 

なお、この作り方が良いかどうかは自分の胸に聞いて下さい。真実はいつも一つ以上!

関連

ECSの概念的な説明

tsubakit1.hateblo.jp

日本語で分かりやすく詳しいECS&JobSystemの説明

esprog.hatenablog.com

今回散々使ったECSハイブリッドについて

tsubakit1.hateblo.jp

 

サバイバルシューターのECS版。かなりハイブリッド

http://www.davidpol.com/

 

*1:正確には別にPrefabではなくても良いですが、理解しやすさのためPrefabとします