Akashic Engine でマルチプレイゲームを作る際、通信遅延を考慮した他プレイヤーアクションのアニメーションタイミングの調整テクニックを紹介します。
はじめに
マルチプレイゲームの場合、他プレイヤーのアクションはサーバを介して通知されるため、時間が遅れて自マシンに届きます。そのため、音ゲーなどのアニメーションのタイミングが重要なゲームだと、他プレイヤーの行動が遅れて反映されるような望ましくない形で表示されてしまいます。そこで、通信ラグを計測してアニメーションを調整することで、全プレーヤが同じ内容の画面を見ているようにします。
作りたい動作
飲み会などで最後に行う一本締めのワイワイ感をマルチプレイゲームにしたいと思い、幹事のかけ声にタイミングを合わせて「一本」するゲームを作ろうと思いました。仕様は以下の通りです。
- ゲーム開始時、プレイヤーが「一本」すべき時刻をプログラム内部で決める
- 上記時刻に合わせて幹事を模したキャラクターがかけ声をかける
- プレイヤーが幹事のタイミングに合わせて「一本」ボタンを押す
- 「一本」ボタンが押されたことを他プレイヤーに通知する
- 他プレイヤーが「一本」したことはリアルタイムに自分の画面に反映される
「一本」はリアリティを持たせるために下記のようなアニメーションを再生して表現します。
他プレイヤーが「一本」している様子は下記のように見えるようにします。
実装方法
まず、以下のように「一本」を実装しました。
- プレイヤーがボタンをクリックした際、自分が「一本」したことをイベントとして通知する
const button = new g.Sprite({ /* ... */ })
// ボタンが押下されたとき、イベントを発火する
button.onPointDown.add(() => {
g.game.raiseEvent(
new g.MessageEvent({
type: "ippon",
playerID: g.game.selfId
})
);
});
- イベントを受信したら、他プレイヤーの「一本」としてアニメーションを再生する
// avatars.プレイヤーID に、指定したプレイヤーの画像を格納する
const avatars: { [index: string]: g.FrameSprite } = { /* ... */ };
scene.onMessage.add(e => {
// イベント受信時、アニメーションを再生する
if (e.type === "ippon") {
avatars[e.playerID].start();
}
});
困ったこと
scene.onMessage
に登録したイベントハンドラで、イベント受信時に g.FrameSprite#start()
を呼び出してアニメーションするようにしました。
ところが、サーバ処理による通信遅延によって、本来の開始タイミングより遅れてアニメーションが開始するため、他プレイヤーが遅れて「一本」したように見えてしまいました。
この問題を解決する前に、Akashic Engineの tick
管理とイベント通知仕様について説明します。
クライアントの同期の仕組み
Akashic Engineではクライアントから送られたイベント情報を、同じタイミングで全クライアントに送信する機能を提供しています。クライアントのゲーム時刻は tick
というフレームカウントで管理されていますが、すべてのクライアントが同じ tick
に同じイベント内容を受け取るよう動作します。
描画内容を同期させるには
通信遅延の分、アニメーションが遅く開始されてしまいます。送信元と同じアニメーションにするため、受信側はアニメーションの開始フレームをずらしましょう。そのため、送信イベントに何 tick
目に「一本」したか情報を付加します。
// 現在tick目かを変数に格納する
let tick = 0;
scene.onUpdate.add(() => {
tick++
});
const button = new g.Sprite({ /* ... */ })
// ボタンが押下されたとき、イベントを発火する
button.onPointDown.add(() => {
g.game.raiseEvent(
new g.MessageEvent({
type: "ippon",
playerID: g.game.selfId,
tick: tick // 「一本」した時刻を他プレイヤーに通知する
})
);
});
// avatars.プレイヤーID に、指定したプレイヤーの画像を格納する
const avatars: { [index: string]: g.FrameSprite } = { /* ... */ };
scene.onMessage.add(e => {
// イベント受信時、アニメーションを再生する
if (e.type === "ippon") {
avatars[e.playerID].frameNumber = tick - e.tick;
avatars[e.playerID].modified();
avatars[e.playerID].start();
}
});
g.FrameSprite
のアニメーション開始フレームを tick - e.tick
とすることで、すでに通信遅延フレーム分進めたところでアニメーションを開始します。
これにより、プレイヤー間でアニメーションを擬似的に同じにすることができます。