この記事で書いている内容は普通Akashic Engineを利用してゲームを作る場合には不要な知識です
ここに書いている知識が必要になる場面は公式のCOEライブラリと同じ事を自分で実装する人くらいです
この記事はAkashic Engineのゲームエンジンのシステムが発生させるイベントについての解説です
プレイヤーが画面をクリックした時のPointDownEventや、生主が参加した時のJoinEventなどのことです
プログラマが任意のデータを送受信するために使うMessageEventもイベントの1つです
また、後半ではイベントフィルターについて解説しています
イベントフィルター以降の項目を読むにはより詳細なエンジン内部の知識が必要です
特に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
の派生です
// パッケージは @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
// パッケージは @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
が呼び出されるまでの一連の流れは以下のようになっています
- プレイヤーが画面をクリックする
- 該当するエンティティに登録されているイベント関数が実行される
上記の説明はイベントの流れをとても簡易的に説明したものであり実際のイベントの流れとは異なります
特にg.Event
にあるlocal
によってイベントの流れが大きく変わります
g.Even
のlocal
値が
true
なイベントをローカルイベント
false
なイベントを非ローカルイベント
と呼びます
このローカル/非ローカルの違いについて説明する前に、アカシックエンジンの仕組みを説明します
実際のイベントの流れはイベントのローカル性を参照
ホスト端末
ゲームが実行されている環境の事を端末と呼びます
端末とは1つのデバイスを指す単語ではなく、ゲームが実行されている環境 (例えばブラウザの1つのタブ) の事を指します
1つのPCで2つタブから同時にゲームに参加している場合、それぞれ別の (合計2つの) 端末です
端末の中で1つだけ、g.game.isActiveInstance()
の戻り値がtrue
な端末が存在します
ここではその端末のことを特別にホスト端末と呼びます
Akashic Engineの説明では、ホスト端末を「アクティブインスタンス」と表記しています
ホスト端末は基本的にその端末を操作するプレイヤー(人間)は存在しません
ニコ生ゲームで遊んでいる時はニコニコのサーバー、デバッグ環境(akashic serve
で実行している時など)はコンソールにホスト端末が存在します
ホスト端末はプレイヤーは居ませんが、例えばグローバルなエンティティがクリックされた時にはそのエンティティに登録されているonPointDown
が呼び出されます
振る舞いは他の端末と同じです
特に気をつけておくべきポイントは以下です
- ホスト端末は生主の端末ではありません
- ホスト端末は1つのみ存在します
- 端末はゲームを遊んでいるプレイヤー+1つ(ホスト端末)分存在します
- が、例外もあります → ソロプレイモード
擬似的なコードで説明すると以下のような動作になります
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
が生成されます
この時以下の条件で生成されるイベントのローカル性が決まります
シーン | クリック位置のエンティティ | 生成されるイベント |
---|---|---|
ローカル | なし/非ローカル/ローカル | ローカル |
非ローカル | なし/非ローカル | 非ローカル |
ローカル | ローカル |
シーンが非ローカルでありエンティティがローカルの場合の小話
シーンには`onPonitDownCapture`が存在します
これはそのシーンでクリック操作が発生した場合に呼び出されます
しかし、クリック位置にローカルエンティティがある場合はローカルイベントになるため、
`onPointDownCapture`もクリックされた端末でのみ呼び出されます
非ローカルなイベントの流れ
1つの端末で発生した非ローカルなイベントは、他の全ての端末で実行されます
例えば1人のプレイヤーが非ローカルなシーンの非ローカルなエンティティをクリックした場合、全ての端末でそのエンティティのイベント関数が実行されます
特に気をつけるべきなのは、クリックしたプレイヤーの端末でイベント関数はすぐには実行されません
これは、全ての端末でイベント関数の実行タイミングを同期するためです
実際のイベントの流れは以下のようになります
- プレイヤーが画面をクリックする
- クリックされたタイミングではクリックイベント関数は呼ばれない
- クリックしたプレイヤーの端末から、ホスト端末へイベントが送信される
- ホスト端末がイベントを受信すると、全端末へイベントをブロードキャストする
- イベントフィルターが存在した場合はここでイベントフィルターに渡される
- 全ての端末が受信して、クリックイベント関数が実行される
- この時にはイベントフィルターされない
イベントフィルターについては#イベントフィルターを参照して下さい
ローカルなイベントの流れ
1つの端末で発生したローカルなイベントは、その端末でのみ処理されます
非ローカルイベントと違い、その端末ですぐにイベント関数が実行されます
実際のイベントの流れは以下のようになります
- プレイヤーが画面をクリックする
- イベントフィルターが存在した場合はここでイベントフィルターに渡される
- クリックしたプレイヤーの端末でのみイベント関数が実行される
ローカルイベントの場合、
自身が送信したイベントを自分自身で受信する
というイメージです
イベントフィルターについては#イベントフィルターを参照して下さい
イベントの送信と受信
ここで書くイベントはg.MessageEvent
をg.game.raiseMessage
で送信した場合の流れです
-
非ローカルイベントの場合
- ホスト端末が送信した場合
- イベントを送信
- 実際は送信しておらず、ゲームエンジンはそのまま自分に返す
- ホスト端末のイベントフィルターに渡される
- イベントフィルターで除外されればイベントは消滅する
- 全端末へブロードキャストされる
- 全端末でイベント関数が実行される
- ここで受信時にイベントフィルターは呼ばれない
(ホスト端末のイベントフィルターも呼ばれない)
- ここで受信時にイベントフィルターは呼ばれない
- イベントを送信
- その他の端末が送信した場合
- イベントを送信
- ホスト端末へ向けて送信される
- 2以降は同じ
- ホスト端末が送信した場合
-
ローカルイベントの場合
(ローカルイベントの場合は、ホスト/その他端末で動作に差はありません)- イベントを送信
- 実際は送信しておらず、ゲームエンジンはそのまま自分に返す
- 送信者のイベントフィルターに渡される
- ホスト端末以外のイベントフィルターが実行されるのはここのみ
- 送信者のイベント関数が実行される
- もちろん送信者以外の端末ではイベント関数は実行されない
- イベントを送信
イベントフィルターの使い方
説明を簡単にするために、先にイベントフィルターの使い方を説明します
イベントの除去/追加が可能なイベントフィルターという機能があります
これは、g.game.addEventFilter
で登録することでエンジンに呼び出されます
複数のイベントフィルターが登録された場合、登録順に呼び出されます
addEventFilter
の引数の説明
- 第1引数は、イベントフィルター関数
- 第2引数は、フィルターすべきイベントがない場合でも定期的にイベントフィルターを呼び出すか
(定期的とはおそらく毎フレーム)
イベントフィルターは次の形式の関数です
(pl.Event[], g.EventFilterController): pl.Event[]
- 第1引数は、処理するイベントの配列
- 第2引数は、
{ processNext(Event): void }
のオブジェクト-
processNext
は渡したイベントを次のフレームに再度呼び出す (次のフレームでまたイベントフィルターされる)
-
- 戻り値は、そのフレームで実行されるイベント
- 第1引数をそのまま返す動作は、イベントフィルターがない場合の動作に等しい
また、イベントフィルターの中で新しいイベントを作成して戻り値の配列に追加して返すことも許容されています
イベントフィルターの例は以下
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()
で取得して送信しています
この記述が2箇所ありますがそれは上記のイベントフィルターとg.game.raiseTickの説明が関連しています
現在のシーンのtickGenerationMode
が"manual"
の場合はイベントフィルターでイベントをブロードキャスト出来ません
そのためg.game.raiseTick
を使用してブロードキャストしています
現在のシーンのtickGenerationMdoe
が"by-clock"
の場合はg.game.raiseTick
を使用できません
そのためイベントフィルターを利用してブロードキャストしています
上記の違いのためCOEライブラリはそのシーンのtickGenerationMode
に応じてイベントフィルターとg.game.raiseTick
を使い分けたています(だから分かりにくい)