【Akashic Engine】イベント・ローカル/非ローカルについて


> この記事で書いている内容は普通Akashic Engineを利用してゲームを作る場合には不要な知識です  
> ここに書いている知識が必要になる場面は公式のCOEライブラリと同じ事を自分で実装する人くらいです

この記事はAkashic Engineのゲームエンジンのシステムが発生させるイベントについての解説です  
プレイヤーが画面をクリックした時のPointDownEventや、生主が参加した時のJoinEventなどのことです  
プログラマが任意のデータを送受信するために使うMessageEventもイベントの１つです

また、後半ではイベントフィルターについて解説しています  
イベントフィルター以降の項目を読むにはより詳細なエンジン内部の知識が必要です  
特に`g.Scene`の`tickGenerationMode`やHOGEHOGE‥


## g.Eventとpl.Eventについて

`E.onPointDown`や`scene.onMessage`などゲーム開発者がよく利用するイベントは全て
`g.Event`の派生です

`g.Event`をネットワークでのやり取りに特化させたものが`pl.Event`です  
(plはplaylogの略称です. `g.Event`と区別するために`pl.Event`と表記します)

> `g.Event`の定義 https://github.com/akashic-games/akashic-engine/blob/main/src/Event.ts  
> `pl.Event`の定義 https://github.com/akashic-games/playlog/blob/main/src/Event.ts

### g.Eventの説明
`E.onPointDown`や`scene.onMessage`など主にゲーム開発者が利用するイベントは全て以下の`g.Event`の派生です
```typescript
// パッケージは @akashic/akashic-engine
// 参照する場合は `g.Event`
interface Event {
  /** イベントの種別 */
  type: EventTypeString;
  /** イベントフラグ値 */
  eventFlags: number;
  /** このイベントがローカルであるか否か */
  local?: boolean;
}
```

### pl.Eventの説明
`pl.Event`は`g.Event`をネットワーク上で送受信する場合に利用する形式です  
詳細はこのページで解説されています https://github.com/akashic-games/playlog
```typescript
// パッケージは @akashic/playlog
// 参照する場合は `pl.Event`
// (インポート文 `import * as pl from "@akashic/playlog";` が必要)
interface Event extends Array<any> {
  [index: number]: any;
  /** @param EventCode */
  0: EventCode;
  /** @param イベントフラグ */
  1: number;
  /** @param プレイヤーID */
  2: string | null;
}
```


## イベントの発生から消費まで
イベントの生成、イベント関数の呼び出しまでの一連の動作はゲームエンジンが行なっています  
例えばプレイヤーが画面をクリックしてから`E.onPointDown`が呼び出されるまでの一連の流れは以下のようになっています
1. プレイヤーが画面をクリックする
2. 該当するエンティティに登録されているイベント関数が実行される

上記の説明はイベントの流れを**とても簡易的に**説明したものであり実際のイベントの流れとは異なります  
特に`g.Event`にある`local`によってイベントの流れが大きく変わります

> `g.Even`の`local`値が  
> `true`なイベントを**ローカルイベント**  
> `false`なイベントを**非ローカルイベント**  
> と呼びます

このローカル/非ローカルの違いについて説明する前に、アカシックエンジンの仕組みを説明します

実際のイベントの流れは[イベントのローカル性](#イベントのローカル性)を参照  


## ホスト端末
> ゲームが実行されている環境の事を**端末**と呼びます  
> 端末とは１つのデバイスを指す単語ではなく、ゲームが実行されている環境 (例えばブラウザの１つのタブ) の事を指します  
> １つのPCで２つタブから同時にゲームに参加している場合、それぞれ別の (合計２つの) 端末です

端末の中で１つだけ、`g.game.isActiveInstance()`の戻り値が`true`な端末が存在します  
ここではその端末のことを特別に**ホスト端末**と呼びます  

> Akashic Engineの説明では、ホスト端末を「アクティブインスタンス」と表記しています

ホスト端末は基本的にその端末を操作するプレイヤー（人間）は存在しません  
ニコ生ゲームで遊んでいる時はニコニコのサーバー、デバッグ環境（`akashic serve`で実行している時など）はコンソールにホスト端末が存在します  

ホスト端末はプレイヤーは居ませんが、例えばグローバルなエンティティがクリックされた時にはそのエンティティに登録されている`onPointDown`が呼び出されます  
振る舞いは他の端末と同じです

特に気をつけておくべきポイントは以下です
* ホスト端末は生主の端末**ではありません**
* ホスト端末は１つのみ存在します
* 端末はゲームを遊んでいるプレイヤー＋１つ（ホスト端末）分存在します
  * が、例外もあります → [ソロプレイモード](#ソロプレイモード)

擬似的なコードで説明すると以下のような動作になります
```typescript
const globalRect = new g.FillRect({
  scene: scene, // このsceneはlocal:falseである
  width: 100,
  height: 100,
  cssColor: "red",
  touchabel: true
});
globalRect.onPointDown(() => {
  // 非ローカルイベントは全端末で共有されるので、ホスト端末もこれが実行される可能性がある
  console.log(g.game.isActiveInstance());   // true または false
});

const localRect = new g.FillRect({
  scene: scene, // このsceneはlocal:falseである
  width: 100,
  height: 100,
  cssColor: "red",
  touchabel: true,
  local: true   // ローカルなエンティティである
});
localRect.onPointDown(() => {
  // 基本的にホスト端末を操作するプレイヤー (人間) は存在しないので、基本的にホスト端末の可能性はない
  console.log(g.game.isActiveInstance());   // 基本的に false
});
```

### ソロプレイモード
上記の説明の中でしつこく**基本的に**と付けている理由は、  
ゲームが一人プレイ用に実行された場合には、そのプレイヤーの端末がホスト端末となるためです
(アツマールの一人プレイがこの状態)

マルチプレイ用のニコ生ゲームとして`akashic export`した場合でも、ソロプレイで遊ぶ環境で実行される可能性があるため、  
ホスト端末 = プレイヤーが存在しない  
と決めつけない方が良いです


## イベントのローカル性
イベント (`g.Event`) には`local`プロパティがあり、これによりローカル性が決定します

例えばプレイヤーが画面をクリックした場合には`g.PointDownEvent`が生成されます  
この時以下の条件で生成されるイベントのローカル性が決まります

<table>
<tr>
  <th>シーン</th>
  <th>クリック位置のエンティティ</th>
  <th>生成されるイベント</th>
</tr>
<tr>
  <td>ローカル</td>
  <td>なし/非ローカル/ローカル</td>
  <td>ローカル</td>
</tr>
<tr>
  <td rowspan="2">非ローカル</td>
  <td>なし/非ローカル</td>
  <td>非ローカル</td>
</tr>
<tr>
  <td>ローカル</td>
  <td>ローカル</td>
</tr>
</table>

<details><summary>シーンが非ローカルでありエンティティがローカルの場合の小話</summary><div>

```
シーンには`onPonitDownCapture`が存在します  
これはそのシーンでクリック操作が発生した場合に呼び出されます  
しかし、クリック位置にローカルエンティティがある場合はローカルイベントになるため、
`onPointDownCapture`もクリックされた端末でのみ呼び出されます
```

</div></details>



### 非ローカルなイベントの流れ
１つの端末で発生した非ローカルなイベントは、他の全ての端末で実行されます  
例えば１人のプレイヤーが非ローカルなシーンの非ローカルなエンティティをクリックした場合、全ての端末でそのエンティティのイベント関数が実行されます

特に気をつけるべきなのは、クリックしたプレイヤーの端末でイベント関数は**すぐには実行されません**  
これは、全ての端末でイベント関数の実行タイミングを同期するためです

実際のイベントの流れは以下のようになります
1. プレイヤーが画面をクリックする
   * クリックされたタイミングではクリックイベント関数は呼ばれない
2. クリックしたプレイヤーの端末から、ホスト端末へイベントが送信される
3. ホスト端末がイベントを受信すると、全端末へイベントをブロードキャストする
   * イベントフィルターが存在した場合はここでイベントフィルターに渡される
4. 全ての端末が受信して、クリックイベント関数が実行される
   * この時にはイベントフィルターされない

> イベントフィルターについては[#イベントフィルター](#イベントフィルター)を参照して下さい

### ローカルなイベントの流れ
１つの端末で発生したローカルなイベントは、その端末でのみ処理されます  

非ローカルイベントと違い、その端末ですぐにイベント関数が実行されます

実際のイベントの流れは以下のようになります
1. プレイヤーが画面をクリックする
   * イベントフィルターが存在した場合はここでイベントフィルターに渡される
2. クリックしたプレイヤーの端末でのみイベント関数が実行される

ローカルイベントの場合、  
自身が送信したイベントを自分自身で受信する  
というイメージです

> イベントフィルターについては[#イベントフィルター](#イベントフィルター)を参照して下さい

## イベントの送信と受信
ここで書くイベントは`g.MessageEvent`を`g.game.raiseMessage`で送信した場合の流れです

* 非ローカルイベントの場合
  * ホスト端末が送信した場合
    1. イベントを送信  
       * 実際は送信しておらず、ゲームエンジンはそのまま自分に返す
    2. ホスト端末のイベントフィルターに渡される
       * イベントフィルターで除外されればイベントは消滅する
    3. 全端末へブロードキャストされる
    4. 全端末でイベント関数が実行される
       * ここで受信時にイベントフィルターは呼ばれない  
         (ホスト端末のイベントフィルターも呼ばれない)
  * その他の端末が送信した場合
    1. イベントを送信
      * ホスト端末へ向けて送信される
    2. 2以降は同じ

* ローカルイベントの場合  
  (ローカルイベントの場合は、ホスト/その他端末で動作に差はありません)
  1. イベントを送信
     * 実際は送信しておらず、ゲームエンジンはそのまま自分に返す
  2. 送信者のイベントフィルターに渡される
     * ホスト端末以外のイベントフィルターが実行されるのはここのみ
  3. 送信者のイベント関数が実行される
     * もちろん送信者以外の端末ではイベント関数は実行されない



## イベントフィルターの使い方
説明を簡単にするために、先にイベントフィルターの使い方を説明します

イベントの除去/追加が可能なイベントフィルターという機能があります  
これは、`g.game.addEventFilter`で登録することでエンジンに呼び出されます  
複数のイベントフィルターが登録された場合、登録順に呼び出されます

`addEventFilter`の引数の説明
* 第１引数は、イベントフィルター関数  
* 第２引数は、フィルターすべきイベントがない場合でも定期的にイベントフィルターを呼び出すか
  (定期的とはおそらく毎フレーム)

イベントフィルターは次の形式の関数です  
`(pl.Event[], g.EventFilterController): pl.Event[]`
* 第１引数は、処理するイベントの配列
* 第２引数は、`{ processNext(Event): void }`のオブジェクト
  * `processNext`は渡したイベントを次のフレームに再度呼び出す (次のフレームでまたイベントフィルターされる)
* 戻り値は、そのフレームで実行されるイベント
  * 第１引数をそのまま返す動作は、イベントフィルターがない場合の動作に等しい

また、イベントフィルターの中で新しいイベントを作成して戻り値の配列に追加して返すことも許容されています

イベントフィルターの例は以下
```typescript
function eventFilter(events: pl.Event[], { processNext }: g.EventFilterController): pl.Event[] {
  const filtered: pl.Event[] = [];

  for (const event of events) {
    const [eventCode, eventFlag, playerId] = event;

    if (eventCode === 0x20) {
      // g.MessageEvent (プログラマが利用する任意のイベント)
      // g.MessageEvent[3] は任意の値が入っている
      const data = event[3];
      // 取り除いたイベントを色々する
    } else {
      filtered.push(event);
    }
  }

  // 例えばここで新しいイベントを作成して返すことも許容されている
  // その場合の動作は[イベントフィルター](#イベントフィルターを参照して下さい
  // filterd.push([0x20, 0, null, "NEW MESSAGE"] as any);

  return filtered;
}
```


## イベントフィルター
> この項目は次のissueでの質問を元にしています
> https://github.com/akashic-games/akashic-engine/issues/449

イベントフィルターの基本的な役割は  
**ブロードキャストされるイベントをフィルターで減らす**  
です

そのため、イベントフィルターは基本的にホスト端末でのみ呼び出されます  
しかしローカルイベントの場合その端末自身のイベントフィルターが呼び出されます  
> 参照 https://github.com/akashic-games/akashic-engine/issues/449#issuecomment-1592771610  
> ローカルイベントは自分にしか送られないイベントなので、通信量に関係せず、間引く必要もありません。そもそもアクティブインスタンスに送られないので、普通の ((C) の) イベントフィルタにはかけようもありません。  
> そのため単純に「イベントフィルタ非対応」としてもよかったのですが、将来の応用の可能性を考え、「ローカルイベントは、それが発生したゲームインスタンス自身のイベントフィルタに渡す」という挙動になっています。

[イベントフィルターの使い方](#イベントフィルターの使い方)の項目で少し触れた、イベントフィルターの中で新しいイベントを追加して返す場合の挙動は以下です
* 現在のシーンの`tickGenerationMode`が`"manual"`の場合
  * 完全に無視される
* 現在のシーンの`tickGenerationMode`が`"by-clock"`の場合
  * ブロードキャストされる

### g.game.raiseTickの説明
`g.game.raiseTick`という関数がありますが、この関数の詳しい説明は書きません  
（Timestamp関連は詳しくないので分かりません）

この関数の簡単な説明のみ書きます

* 現在のシーンの`tickGenerationMode`が`"by-clock"`の場合エラーが発生します
  * `"manual"`の場合のみ実行可能です
* `g.game.raiseTick`はホスト端末のみで実行されます
  * それ以外の端末では呼び出しても完全に無視されます（呼び出さないのと同義）
* `g.game.raiseTick`で発生させたイベントはイベントフィルターを超えてブロードキャストされる
  * 普通ホスト端末でイベントフィルターされた後にブロードキャストされるが、イベントフィルターされない
* `g.game.raiseEvent` イベントフィルターされる
  * `g.game.raiseTick`と比較して書いたが、他のイベント（クリックイベント等）と同じ感じ

### 補足
COEライブラリではブロードキャストするイベントを`this._controller.getBroadcastDataBuffer()`で取得して送信しています

この記述が２箇所ありますがそれは上記の[イベントフィルター](#イベントフィルター)と[g.game.raiseTickの説明](#g.game.raiseTickの説明)が関連しています

現在のシーンの`tickGenerationMode`が`"manual"`の場合はイベントフィルターでイベントをブロードキャスト出来ません  
そのため`g.game.raiseTick`を使用してブロードキャストしています

現在のシーンの`tickGenerationMdoe`が`"by-clock"`の場合は`g.game.raiseTick`を使用できません  
そのためイベントフィルターを利用してブロードキャストしています

上記の違いのためCOEライブラリはそのシーンの`tickGenerationMode`に応じてイベントフィルターと`g.game.raiseTick`を使い分けたています（だから分かりにくい）