[SpatialOS] UnityでMMOゲームを作成する際にやること

> ※追記: 先日ローンチされた [SpatialOS GDK for Unity](https://gdk.improbable.io/spatialos-gdk-unity) を触ってみましたので内容を更新しました。旧SDK時の内容は[こちら](https://qiita.com/tsukitsutsuki/items/186e1c7aac2da5d7522f/revisions/0?type=preview)。


インターネットをご覧の皆さん こんばんは。
SpatialOS([https://improbable.io/games](https://improbable.io/games))で簡単なMMOゲームを作ってみて、Unityで作る際にやることがなんとなくわかったような気がしてきたので、忘れないうちにメモしておこうと思います。

ちなみに、SpatialOSってなに？といった内容は[こちら](http://qiita.com/tsukitsutsuki/items/56d4c5deefad3bb4c0eb)に、
SpatialOS GDK for Unity のインストール＆サンプルの実行は[こちら](https://qiita.com/tsukitsutsuki/items/1d9950256fd9c6ff66bf)に書いてみましたので、興味のある方はあわせてどうぞ。

# 大まかな考え方
SpatialOSでは大まかに次の役割を想定します。

- プレーヤが操作するクライアント
    - 自分が操作するクライアント
    - 他人が操作するクライアント
- ゲームロジックの制御や物理処理等のサーバサイド処理

呼びかたとしては、処理単位を[Worker](https://docs.improbable.io/reference/13.3/shared/glossary#worker)、クライアント/サーバそれぞれを

- [Client-workers](https://docs.improbable.io/reference/13.3/shared/glossary#client-worker)（External workerとも）
- [Server-workers](https://docs.improbable.io/reference/13.3/shared/glossary#server-worker)（Managed workerとも）

と表現しています。
（参考：[Workers and load balancing](https://docs.improbable.io/reference/13.3/shared/concepts/workers-load-balancing) / [Workers: Workers in the GDK](https://docs.improbable.io/unity/alpha/content/workers/workers-in-the-gdk)）

Worker はさらに役割ごとに worker_type で定義でき、標準では
UnityClient=client-server / UnityGameLogic=server-worder
などと定義されています。
他にも、モバイル版のAndroidClient=Android用client-worker や、サンプルアプリでは、SimulatedPlayer=敵AI関連のworker などが定義されています。
（参考：[Designing workers](https://docs.improbable.io/reference/13.3/shared/design/design-workers) / [Worker launch configuration](https://docs.improbable.io/reference/13.3/shared/worker-configuration/launch-configuration) / [Configuring a worker](https://docs.improbable.io/reference/13.3/shared/worker-configuration/worker-configuration)）


処理の流れとしては以下のような感じです。
※旧SDK[チュートリアル](https://docs.improbable.io/reference/11.0/tutorials/pirates/overview)より。海賊船が大砲を撃ち、当たったら沈むといった動作のシーケンス。
※旧SDKのでは UnityClient=client-worker / UnityWorker=server-workerです。
![events-diagram-2.png](https://qiita-image-store.s3.amazonaws.com/0/191218/df3b32c1-3d6e-005a-c06f-70948037f0f5.png)
※図は[旧SDK公式ドキュメントのチュートリアル](https://docs.improbable.io/reference/11.0/tutorials/pirates/lesson5)から引用させていただきました。
※worker と SpatialOS 間のやり取りはこちらも参考になります。[Operations: how workers communicate with SpatialOS](https://docs.improbable.io/reference/13.3/shared/design/operations)

# Unityで作る際にやること
Unityで実際にゲームを作る際には、[Create a new project](https://docs.improbable.io/unity/alpha/setup-and-installing) を参考に始めるのが良いと思います。GitHub には blank project も用意されています。
gdk-for-unity-blank-project
https://github.com/spatialos/gdk-for-unity-blank-project

ゲームの機能を作る際にやることは、だいたい以下の作業です。

- Entity＆Component を定義する
- クライアント/サーバで処理の分担を行う
- Component を同期する

## Entity＆Component を定義する
SpatialOS では、空間上に存在するオブジェクトを [Entity](https://docs.improbable.io/reference/13.3/shared/glossary#entity) として表現し、Entity は複数の [Component](https://docs.improbable.io/reference/13.3/shared/glossary#component) を保持します。
Component は

- Property: 同期させるデータ
- Event: 他のWorkerへの通知
- Command: 他のWorkerへの処理実行依頼（引数、戻り値が定義できる）

から構成されます。
（参考：[World, entities, components](https://docs.improbable.io/reference/13.3/shared/concepts/world-entities-components)）

例えば、RPG風のゲームの場合、プレーヤキャラクターや敵モンスター、フィールド上の建物等が Entity になります。
HPなどのステータスや攻撃イベントなどは Component として定義されます。

- プレーヤキャラクターEntity
    - 位置情報 Component (すべてのEntityが持つ[標準Component](https://docs.improbable.io/reference/13.3/shared/glossary#required-components))
    - ステータス Component
        - HP Property
        - MP Property
    - アクション Component
        - 攻撃 Event
        - 回避 Event

- 敵モンスターEntity
    - 位置情報 Component (すべてのEntityが持つ標準Component)
    - ステータス Component (プレーヤキャラクターと同じComponent)
    - アクション Component (〃)
    - アイテムドロップ Component
        - アイテムドロップ Event

（参考: [Designing entities](https://docs.improbable.io/reference/13.3/shared/design/design-entities) / [Designing components](https://docs.improbable.io/reference/13.3/shared/design/design-components) / [Component best practices](https://docs.improbable.io/reference/13.3/shared/design/component-best-practices)）

Unityでは、schemaファイルという形式で Component を定義します。
コード上から扱うC#クラスは、schemaファイルをもとに自動生成されます。
（参考: [schema](https://docs.improbable.io/reference/13.3/shared/concepts/schema) / [Schemalang reference](https://docs.improbable.io/reference/13.3/shared/schema/reference)）

```例-Status.schema
package improbable.rpg;

component Status {
  id = 1000;  // プロジェクトで一意なIDを振る

  int32 current_hp = 1;  // コンポーネント内で一意なIDを振る。初期値ではないので注意
  int32 current_mp = 2;
}
```

C#クラスは、Unityエディタ の SpatialOS - Generate code を使用して自動生成されます。
![generatecode.png](https://qiita-image-store.s3.amazonaws.com/0/191218/6758ab35-d9ce-b3a4-59b7-06ecb691fdad.png)


Entity はコード上で定義します。
Prefab の指定やコンポーネントの追加、アクセス権の指定等を行います。

```csharp:雰囲気
var AllWorkerAttributes = new List<string> { "UnityGameLogic", "UnityClient" };
// schemaから生成されたStatusクラスをコンポーネントとして追加
var statusComponent = Status.Component.CreateSchemaComponentData(100,50);

var entityTemplate = EntityBuilder.Begin()
  .AddPosition(0, 0, 0, "UnityGameLogic")
  .AddMetadata("Prefab名", "UnityGameLogic")
  .SetPersistence(true)
  .SetReadAcl(AllWorkerAttributes)
  .AddComponent(statusComponent, "UnityGameLogic")
  .Build();
```

## クライアント/サーバで処理の分担を行う
クライアント/サーバのどこで処理を行うかを分岐させるには、以下の仕組みを使用します。

- [Access Control Lists (ACL)](https://docs.improbable.io/reference/13.3/shared/glossary#acl)
- Improbable.Gdk.GameObjectRepresentation.WorkerTypeAttribute
- Improbable.Gdk.GameObjectRepresentation.RequireAttribute

※SpatialOSを使用する際には、ここが一番のポイントな気がするのですが、ちゃんと理解できているかはいまひとつ自信がないです。正確な情報は公式ドキュメント等を参照してください。
（※参考: [Understanding read and write access (“authority”)](https://docs.improbable.io/reference/13.3/shared/design/understanding-access)）

### ACL
Entity を定義する際に、Component に対してのアクセス権（Read/Write）を設定することができます。
Write権限を与えた Worker が更新処理を担当することになります。

例えば[サンプルFPSゲーム](https://docs.improbable.io/unity/alpha/projects/fps/overview)のプレーヤキャラの場合は以下のように定義されています。

```csharp:FpsEntityTemplates.Player(一部抜粋＆調整)
var AllWorkerAttributes = new List<string> { "UnityGameLogic", "UnityClient" };
var gameLogic = "UnityGameLogic";

// workerId=workerごとに一意のID
// $"workerId:{workerId}"で特定の Worker のみに権限付与できる
var client = $"workerId:{workerId}";

return EntityBuilder.Begin()
  // 位置情報のWrite権限はサーバのみ許可
  .AddPosition(spawnPosition.x, spawnPosition.y, spawnPosition.z, gameLogic)
  .AddMetadata("Player", gameLogic)
  .SetPersistence(false)
  // Read権限は全Worker許可
  .SetReadAcl(AllWorkerAttributes)
  // ComponentのWrite権限はそれぞれの用途に合わせて自分orサーバに許可
  .AddComponent(serverMovement, gameLogic)
  .AddComponent(clientMovement, client)
  .AddComponent(clientRotation, client)
  .AddComponent(shootingComponent, client)
  .AddComponent(gunComponent, gameLogic)
  .AddComponent(gunStateComponent, client)
  .AddComponent(healthComponent, gameLogic)
  .AddComponent(healthRegenComponent, gameLogic)
  .AddPlayerLifecycleComponents(workerId, client, gameLogic)
  .Build();
```

（参考: [Setting permissions (ACLs)](https://docs.improbable.io/unity/alpha/projects/fps/tutorial#setting-permissions-acls)）


### WorkerTypeAttribute
Entity として設定した Prefab に追加する MonoBehaviourクラスでは、WorkerTypeAttribute を使用してクライアント/サーバ処理を切り分けられます。

```csharp:雰囲気
// クライアントでのみ、このクラスの処理が実行される。
[WorkerType("UnityClient")]
public class PlayerInput : MonoBehaviour
{
}
```

### RequireAttribute
WorkerTypeAttribute ではクラス単位での指定でしたが、RequireAttribute を使用すると ACLで指定した Component のアクセス権によって制御することができます。

schemaから自動生成されるクラスには、
コンポーネント名.Requirable.Reader / コンポーネント名.Requirable.Writer
というクラスが含まれます。Readerは読み取り専用、Writerは読み書きが行えます。
これらのインスタンスに対して RequireAttribute を指定すると、権限がある場合のみが処理対象になします。

例えば先のACL例では、プレーヤに ClientMovement の書き込み権限設定（`AddComponent(clientMovement, client)`）を行っていました。
プレーヤキャラを動かす MonoBehaviour を設定する場合、ClientMovement.Requirable.Writer を Require に指定すると自分のキャラのみ操作できるようになります。（他人のキャラは操作できない）

```csharp:雰囲気
public class FpsDriver : MonoBehaviour
{
  // ACLで書き込み権限を付与したWorkerでのみ、このクラスの処理が実行される。
  // ClientMovement は自身のみ Write 許可されているので、自分のキャラのみ操作可（他人のキャラは操作不可）。
  [Require] private ClientMovement.Requirable.Writer authority;
}
```


## Component を同期する
schemaから自動生成されるクラスで Component の読み取り/更新が行えます。
Component の用途に合わせてクライアント/サーバで処理を行います。

```csharp:雰囲気
// 更新
public class UpdateSample : MonoBehaviour
{
  [Require] private Status.Requirable.Writer statusWriter;

  void OnDamage(int hp)
  {
    // Componentで定義された current_hp を更新
    statusWriter?.Send(new Status.Update
    {
      CurrentHp = new Option<int>(hp)
    });
  }
}

// 読み取り
public class ReadSample : MonoBehaviour
{
  [Require] protected Status.Requirable.Reader statusReader;

  void Update()
  {
    // Component で定義された current_hp の現在の値を読み取り
    var currentHp = statusReader.Data.CurrentHp;
    // 何かの処理が続く
  }
}

// 更新通知を待つ方式
public class ListenSample : MonoBehaviour
{
  [Require] private Status.Requirable.Reader statusReader;

  void OnEnable()
  {
    // Componentの更新通知を待つ
    statusReader.ComponentUpdated += OnStatusUpdated;
  }

  void OnDisable()
  {
    // 自動でイベントハンドラがクリアされるため手動で外す必要はないようです
    //statusReader.ComponentUpdated -= OnStatusUpdated;
  }

  void OnStatusUpdated(Status.Update update)
  {
    if (update.CurrentHp.HasValue) {
      var currentHp = update.CurrentHp.Value;
      // 何かの処理が続く
    }
  }
}
```

# おわりに
クライアント/サーバの処理分担の仕組みが特殊ですが、Unityでクライアント/サーバの処理が記述できるというニュアンスが伝わったでしょうか。
[Health pick-up tutorial](https://docs.improbable.io/unity/alpha/projects/fps/tutorial) をやってみると、より詳細な手順がわかると思います。

以上 おつきあいいただきありがとうございました。