最終確認日

オンラインゲームの仕組みの本ー第6章をGodotに置き換えながら読む

概要

ちょこっとアクションゲーム「たたいて!かわして!じゃんけんぽん!」を作る

まずは読む

つくるのは、いわゆる叩いてかぶってじゃんけんぽん。

  • まずはゲームデザイン。じゃんけんパートとアクションパートがある。
  • 厳密に既存のルールを適用すると、技術的な問題が発生したり、仕様に矛盾が生じることがある。
    • 現実世界では、じゃんけんの手を選ぶ瞬間はじゃんけんポンのポンの一瞬
    • じゃんけんの手を選ぶ時間を設けてタイミングゲームにならないようにする
  • 早く出しすぎた場合や出さなかったときのパターンの話は難しすぎて何を言われているのかわからなくなるレベル。考えもつかずに無視しそうなじゃんけんの仕様。
  • 今回のゲーム仕様では間違えて叩いてもペナルティポイントはなく、単純化したゲームにする
  • オンラインゲームの仕様を検討する前に、1台の端末で複数人がプレイする状況を検討する。

ジャンケンパート

  • 出す手が決まったらお互いに送信する。
  • お互いの出す手が揃ったタイミングでジャンケンが始まる。
  • 選択のタイミングでは、先に相手の手を受け取っている状態になるが、ユーザーに見えているわけではないので、問題なし。

アクションパート

  • どちらが先に攻撃/防御アクションを入力できるかを競う
  • さまざまな状況を考えなければいけない
    • 端末Aは攻撃、端末Bは何もしなかった場合
      • 何もしない場合は情報が届かないので防御できていない判定とする
    • 端末Aは何もしない、端末Bが防御した場合
      • 攻撃の情報が届かなかったので、引き分け
    • 端末Aの攻撃の入力が早く、端末Bの防御が遅かった場合
      • このときは遅延の影響を受けるので、結果が異ならないように注意
  • 同期させるための情報を加えて通信する
    • アクション開始から入力までの経過時間を相手に送る

オンラインゲームで生じる問題の解決

  • ローカルではボタンを押した瞬間にローカル側のモーションが再生される
  • リモートの操作は届いたあとにモーションが再生される
  • 見た目と結果がズレる可能性がある
    • 攻撃したのにあたっていない、避けたのに当たった問題が起きる。
  • このゲームでは、モーションの再生速度を速くすることで解決する。
  • もともと矛盾しているものを矛盾がないように見せるので、いい妥協点を見つけることが重要
  • 通信遅延の許容値を設定する
    • 許容時間分は予備のアクション動作を入れる
    • もちろんこれにより若干ゲームのテンポは悪くなるが、それは妥協点になる。
  • 仕様が決まったらゲームのフローチャートとゲームの状態enumを定義するのはよさそう
private enum GameProgress
{
    None = 0,
    Ready,      // ゲーム事前準備.
    SelectRPS,  // ジャンケン選択.
    WaitRPS,    // 受信待ち.
    Shoot,      // ジャンケン演出.
    Actions,    // 叩いて被っての選択. 受信待ち.
    EndAction,  // 叩いて被って演出.
    Result,     // 結果発表
    EndGame     // おわり.
}
  • じゃんけんの手を選ぶ前から受信待機をしていないといけない。
  • 「お互いの情報を送受信し合って」は同タイミングの送受信のようだけど、実際は違うからね
  • RPSKind でじゃんけんの手を定義 Kind って単語いいね。
  • シーケンス
  • m_inputData[m_playerId ^ 1] = rps; という記述は、「m_playerId が 0 または 1 であることを前提に、もう一方のプレイヤーIDにアクセスする」ためのテクニック。
  • じゃんけん情報にの受信タイミングによっては、じゃんけんの開始タイミングが端末ごとに異なるが、アクションパートで経過時間を送って判断するのでok
    • このズレによってアクションパートを受け取れない可能性はないのかな??
  • 経過時間(ms)はリアルタイムクロックを使う(マシンスペックやフレームレートの影響を受けないようにするため)
  • バタンと経過時間のデータはセットで送信する。3バイトで。
  • エンディアンに気をつけて。HostToNetworkOrder で変換する

たいせつ!

  • オンラインゲームの作成はの1台の端末で複数人が遊ぶ時の仕様を先に考える!
  • 結果と表示がズレる現象はオンラインゲームでしか発生しない問題!
  • 通信/遅延処理を含めたゲームの仕様にする必要があるため、通信機能は後付けできない!

サンプルコードを見る

含まれるScripts

- ActionController.cs
- AsciiCharacter.cs
- BattlePanel.cs
- BattleSelect.cs
- BGM.cs
- BoardYou.cs
- Damage.cs
- HeavyDamage.cs
- InputData.cs
- Kurukuru.cs
- LightDamage.cs
- NetworkController.cs
- NetworkDef.cs
- PacketQueue.cs
- Player.cs
- ResultScene.cs
- RockPaperScissor.cs
- RPSPanel.cs
- RPSSelector.cs
- RPSSpriteChanger.cs
- Score.cs
- ShootCall.cs
- Timer.cs
- TransportTCP.cs

ネットワーク系

  • TransportTCP.cs
  • NetworkController.cs
  • NetworkDef.cs
  • PacketQueue.cs

アニメーション操作系

  • ActionController.cs
  • Kurukuru.cs
  • LightDamage.cs
  • RPSSpriteChanger.cs
  • Damage.cs
  • HeavyDamage.cs
  • BoardYou.cs
  • ShootCall.cs
  • Score.cs
  • Player.cs

UIコンポーネント系

  • BattlePanel.cs
  • RPSPanel.cs
  • RPSSelector.cs
    • じゃんけんの選択パネルを管理する

じゃんけんとアクションの処理

  • RockPaperScissor.cs
    • じゃんけんのメイン処理
    • GameState を持つ
  • BattleSelect.cs
    • SelectWait / Selected の2つのStateをもつ
  • InputData.cs
    • じゃんけんの手の種類やアタックの種類、送信するデータなどを定義
    • ここにジャンケンの結果と、攻撃/防御から勝敗を求めるも書いてある

リザルト処理

  • ResultScene.cs

その他

  • AsciiCharacter.cs
  • BGM.cs
  • Timer.cs
    • タイマーは経過時間を取得する処理などのutil

Godotに置き換える

とりあえずメインのシーンから作る。 GameStateRockPaperScissor.gd 内に定義する。他からは意識しないようにしてね。

  • RockPaperScissor.tscn

まずは GameState の定義から各関数を実装していく。

extends Node2D
class_name RockPaperScissor

enum GameState {
    NONE = 0,
    READY,       # 対戦相手のログイン待ち.
    SELECT_RPS,  # ジャンケン選択.
    WAIT_RPS,    # 受信待ち.
    SHOOT,       # ジャンケン演出.
    ACTION,      # 叩いて被っての選択・受信待ち.
    END_ACTION,  # 叩いて被って演出.
    RESULT,      # 結果発表.
    END_GAME,    # おわり.
    DISCONNECT,  # エラー.
}

var game_state: GameState = GameState.NONE


func _ready() -> void:
    game_state = GameState.NONE


func _process(delta: float) -> void:
    match game_state:
        GameState.NONE:
            pass
        GameState.READY:
            update_ready()
        GameState.SELECT_RPS:
            update_select_rps()
        GameState.WAIT_RPS:
            update_wait_rps()
        GameState.SHOOT:
            update_shoot()
        GameState.ACTION:
            update_action()
        GameState.END_ACTION:
            update_end_action()
        GameState.RESULT:
            update_result()
        GameState.END_GAME:
            update_end_game()
        GameState.DISCONNECT:
            pass
  • NeworkController.cs にあたるものを実装する
    • RPSNetworkController.tscn にしておく
  • UDPを使っているのかと思ったら、今回もTCPでいいのね。
  • class_name を使った enum 専用スクリプトならAutoloadにしなくても使えるのね。
    • これができるならもう3~5章ももう少しマシに実装できそう。
  • _init(a: int, b: String) -> void といったように _init コンストラクタが使える!
    • よかった。
var hoge = RPSActionInfo.new(RPSType.ActionKind.ATTACK, 3000)
print("hoge: ", hoge.action_time)
# hoge: 3000
# ちゃんと使える。
  • Godotでスプライトシートを使って画像を設定する

  • HBoxContainer 内のアイテムは visible = false にすると真ん中寄せの場合は真ん中へ寄る

  • TransportTCP.data_received.connect(_on_data_received) は受け取りたい画面が表示されていない時はオフにしても良さそう。

  • 負けと引き分けになるときがある。

    • このジャッジのテストを作りたい。
    • Godotで単体テストをしてみたい
サイトアイコン
公開日
更新日