2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Akashic Engineマルチプレイゲームにおける通信遅延を考慮したアニメーション同期

Posted at

Akashic Engine でマルチプレイゲームを作る際、通信遅延を考慮した他プレイヤーアクションのアニメーションタイミングの調整テクニックを紹介します。

はじめに

マルチプレイゲームの場合、他プレイヤーのアクションはサーバを介して通知されるため、時間が遅れて自マシンに届きます。そのため、音ゲーなどのアニメーションのタイミングが重要なゲームだと、他プレイヤーの行動が遅れて反映されるような望ましくない形で表示されてしまいます。そこで、通信ラグを計測してアニメーションを調整することで、全プレーヤが同じ内容の画面を見ているようにします。

作りたい動作

飲み会などで最後に行う一本締めのワイワイ感をマルチプレイゲームにしたいと思い、幹事のかけ声にタイミングを合わせて「一本」するゲームを作ろうと思いました。仕様は以下の通りです。

  • ゲーム開始時、プレイヤーが「一本」すべき時刻をプログラム内部で決める
  • 上記時刻に合わせて幹事を模したキャラクターがかけ声をかける
  • プレイヤーが幹事のタイミングに合わせて「一本」ボタンを押す
  • 「一本」ボタンが押されたことを他プレイヤーに通知する
  • 他プレイヤーが「一本」したことはリアルタイムに自分の画面に反映される

「一本」はリアリティを持たせるために下記のようなアニメーションを再生して表現します。
Videotogif.gif

他プレイヤーが「一本」している様子は下記のように見えるようにします。

d87307b88eb2249345ac021b6a454091.png

実装方法

まず、以下のように「一本」を実装しました。

  • プレイヤーがボタンをクリックした際、自分が「一本」したことをイベントとして通知する
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()を呼び出してアニメーションするようにしました。
ところが、サーバ処理による通信遅延によって、本来の開始タイミングより遅れてアニメーションが開始するため、他プレイヤーが遅れて「一本」したように見えてしまいました。

Qiita.png

この問題を解決する前に、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 とすることで、すでに通信遅延フレーム分進めたところでアニメーションを開始します。

これにより、プレイヤー間でアニメーションを擬似的に同じにすることができます。

Qiita (1).png

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?