最終確認日
オンラインゲームの仕組みの本ー第5章をGodotに置き換えながら読む
概要
ターン制ゲームを作る。三目並べを作ろう!
- オンラインゲームのしくみ-Unityで覚えるネットワークプログラミング
- オンラインゲームの仕組みの本ー第3章をGodotに置き換えながら読む
- オンラインゲームの仕組みの本ー第4章をGodotに置き換えながら読む
まずは読む
- まずは1つの端末で2人で遊ぶ場合の処理を考えてみる
- 次に2つの端末で2人で遊ぶ場合の処理を考えてみる
- 離れた場所の場合は、○ × をつけた位置を届けるメッセンジャー君が必要
- プレイヤーAの行動をメッセンジャー君がプレイヤーBに届けて書き写すイメージ
- 通信相手の知らない情報を適切なタイミングで送受信し合う!
- なにを と いつ をはっきりさせればおk!
- XY方向に番号を割り振って2次元でマス目番号を指定する
- int型が縦横2つで合計サイズは 4バイト x 2方向 = 8バイト
- 通信時は1次元の通し番号としてマス目番号に変換することで送信時のデータを小さくする
- できるだけ少ない情報をできるだけ少ない回数で送信しよう!
- 勝敗の判定はそれぞれの端末で別々に行っても必ず同じ結果になるので通信しなくて良い
- サーバーとクライアントでそれぞれ
localMark
とremoteMark
が逆転するようにする。 - 相手が入力するまで何もできない。待たされるストレスを考えよう
- 一定時間名にマークを置かなかった場合は...を考える
- これも、ゲームルールを考える最初の段階から想定して考えておこう
- 10秒でタイムアウトをして、10秒を超えたらマウス入力を無効にし、ランダムな位置マークを配置する。
- ちゃんとユーザーに見えるように残り時間を表記しよう
- 回線切断と通信タイムアウトのエラーが発生する。
要点
- 通信相手の知らない情報を適切なタイミングで送受信し合う!
- できるだけ少ない情報をできるだけ少ない回数で送信しよう!
- 相手が知っている情報は送る必要がないよ!
- ターン制ゲームでは待たされている時間のことを考えてゲームデザインをしよう
- 回線切断と通信タイムアウトのエラーハンドリングをしよう
置き換えてみる
画面
TicTacToeRoom.tscn
三目並べを行うゲームルーム- エラー画面を持つ
通信
TransportTCP.tscn
通信ライブラリ(3章/4章で使った)TicTacToe.tscn
ゲーム処理を行う
ゲームの開始の接続処理は第4章をGodotに置き換えながら読むのところを再利用する。
メモ
HBoxContainer
は 子要素の幅を Expand すれば、等しいサイズで詰め込むことができる。- 4章で作ったチャットの中に組み込もうとしているので、送ったデータの種類を区別する必要がある
- ゲーム途中では、なるべく少ない通信で送るべきだけど、チャット中やゲーム招待時は遅延があっても大丈夫なので、JSONで送るようにしてみる。
- ボードを画面中央に配置する時に座標計算をしておいていたけど、Camera2Dを使えばいいのでは?!?!
- 画面内でリトライするのが難しくてあきらめてしまった。(2人ともリトライを押している必要があるってことよね)
SendType を追加してみた
TransportTCP
に SendType
を追加してみた。
enum SendType {
CHAT,
TICTACTOE_INVITE,
TICTACTOE_REJECT,
TICTACTOE_JOIN
}
送るとき
func send(type: SendType, content: String):
var message = {
"type": type,
"content": content
}
var data = JSON.stringify(message).to_utf8_buffer()
_send(data)
受け取るとき
func _on_data_received(data: PackedByteArray):
var text = data.get_string_from_utf8()
var msg = JSON.parse_string(text)
if msg == null:
return
var type = msg["type"]
match int(type):
TransportTCP.SendType.CHAT:
var message = msg["content"]
received_message.emit(message)
TransportTCP.SendType.TICTACTOE_INVITE:
received_message.emit("三目並べに招待されました")
TransportTCP.SendType.TICTACTOE_REJECT:
received_message.emit("三目並べへの招待を拒否されました")
TransportTCP.SendType.TICTACTOE_JOIN:
received_message.emit("三目並べをはじめます")
複数のメッセージを同時に送信した場合
例えば招待をキャンセルした場合に次のように2回同時に送ったとする
func _on_pressed_cancel_button():
TransportTCP.send(TransportTCP.SendType.TICTACTOE_REJECT, "")
TransportTCP.send(TransportTCP.SendType.CHAT, "三目並べの招待をキャンセルしました")
invite_view.visible = false
そうすると、受け取り側の text は次のようにまとめて届くことになる
func _on_data_received(data: PackedByteArray):
var text = data.get_string_from_utf8()
print("受信したデータ: ", text)
# ここでJSONをパースできずにエラーになる
var msg = JSON.parse_string(text)
if msg == null:
return
var type = msg["type"]
if type == TransportTCP.SendType.CHAT:
var message = msg["content"]
received_message.emit(message)
受信したデータ: {"content":"","type":2}{"content":"三目並べの招待をキャンセルしました","type":0}
JSONのパースに失敗することになる。
とりあえず、同時には送らないようにした。
ゲームボードを画面の中心に配置したい
最初は座標計算を頑張ってやってみた。
extends Node2D
@onready var game_board = $GameBoard
const ROW = 3
const COLUMN = 3
const CELL_SIZE: float = 100
const SPACING: float = 16
var cell_scene = preload("res://scripts/tic_tac_toe/cell/tic_tac_toe_cell.tscn")
var viewport_size: Vector2
func _ready() -> void:
viewport_size = get_viewport().get_visible_rect().size
setup_board()
func setup_board():
var game_board_width = CELL_SIZE * COLUMN + SPACING * (COLUMN - 1)
var game_board_height = CELL_SIZE * ROW + SPACING * (ROW - 1)
var board_center_offset = Vector2(game_board_width, game_board_height) / 2
var cell_center_offset = Vector2(CELL_SIZE, CELL_SIZE) / 2
game_board.position = get_viewport_rect().size / 2
for row in ROW:
for col in COLUMN:
var cell = cell_scene.instantiate()
var x = col * (CELL_SIZE + SPACING)
var y = row * (CELL_SIZE + SPACING)
cell.position = Vector2(x, y) - board_center_offset + cell_center_offset
game_board.add_child(cell)
この方法の欠点は
- 画面に合わせたセルサイズを考慮しなきゃいけない。
- 計算が少し複雑
- 大きい端末の場合にも、固定サイズになる。
つづいて、Camera2Dを使った方法に書き換えてみた。
書き換え1
extends Node2D
@onready var game_board = $GameBoard
@onready var camera = $Camera2D
const ROW = 3
const COLUMN = 3
const CELL_SIZE: float = 100
const SPACING: float = 16
var cell_scene = preload("res://scripts/tic_tac_toe/cell/tic_tac_toe_cell.tscn")
var viewport_size: Vector2
func _ready() -> void:
viewport_size = get_viewport().get_visible_rect().size
setup_board()
adjust_camera_to_fit_board()
func setup_board():
# ゲームボードの中身が原点にくるように調整しておく
var step = CELL_SIZE + SPACING
var total_size = Vector2(COLUMN, ROW) * step
var offset = total_size / 2
for row in ROW:
for col in COLUMN:
var cell = cell_scene.instantiate()
var x = col * step - offset.x + CELL_SIZE / 2
var y = row * step - offset.y + CELL_SIZE / 2
cell.position = Vector2(x, y)
game_board.add_child(cell)
# カメラをゲームボードのサイズに合わせる.
func adjust_camera_to_fit_board():
var step = CELL_SIZE + SPACING
var total_size = Vector2(COLUMN, ROW) * step
var zoom_x = total_size.x / viewport_size.x
var zoom_y = total_size.y / viewport_size.y
var zoom_factor = max(zoom_x, zoom_y)
# 少し余裕を持たせる
zoom_factor *= 1.1
camera.zoom = Vector2(zoom_factor, zoom_factor)
この書き換えのメリット
- 少し計算が楽になった?
- 画面上に思ったサイズで表示されていない場合には、camera_zoom をいじればいい。
- スマホ・PCなどの両対応に備える場合には、Camera2Dは使った方がいい。
- ズームイン・ズームアウトなどのアニメーションをするならCamera2Dは必須。
- そもそも
Control
で作っちゃいなよということを言われちゃったが。
でも基本はCamera2Dは使う!で良さそう。
adjust_camera_to_fit_board()
なしにすると、画面と同じサイズになる。- カメラは
viewport
のサイズに紐づく。縦長なら縦長、横長なら横長の範囲をうつす。 - なのでゲームボードの場所を少し上に配置したいなどの場合は、ゲームボード自体を上に中心配置ではなく、y座標をあげないといけないね。
背景画像をつけてみた
extends Node2D
@onready var game_board = $GameBoard
@onready var camera = $Camera2D
@onready var background_sprite = $GameBoard/BackgroundSprite
const ROW = 3
const COLUMN = 3
const CELL_SIZE: float = 100
const SPACING: float = 16
var cell_scene = preload("res://scripts/tic_tac_toe/cell/tic_tac_toe_cell.tscn")
var viewport_size: Vector2
func _ready() -> void:
viewport_size = get_viewport().get_visible_rect().size
setup_board()
adjust_camera_to_fit_board()
func setup_board():
# ゲームボードの中身が原点にくるように調整しておく
var step = CELL_SIZE + SPACING
# 左上と左横にも余白を与える
var total_size = Vector2(
(COLUMN * CELL_SIZE) + (SPACING * (COLUMN + 1)),
(ROW * CELL_SIZE) + (SPACING * (ROW + 1))
)
var offset = total_size / 2
# 背景画像のサイズを調整する
var texture_size = background_sprite.texture.get_size()
background_sprite.scale = total_size / texture_size
for row in ROW:
for col in COLUMN:
var cell = cell_scene.instantiate()
var x = col * step - offset.x + CELL_SIZE / 2 + SPACING
var y = row * step - offset.y + CELL_SIZE / 2 + SPACING
cell.position = Vector2(x, y)
game_board.add_child(cell)
# カメラをゲームボードのサイズに合わせる.
func adjust_camera_to_fit_board():
var step = CELL_SIZE + SPACING
var total_size = Vector2(COLUMN, ROW) * step
var zoom_x = total_size.x / viewport_size.x
var zoom_y = total_size.y / viewport_size.y
var zoom_factor = max(zoom_x, zoom_y)
# 少し余裕を持たせる
zoom_factor *= 1.5
camera.zoom = Vector2(zoom_factor, zoom_factor)
GameBoard
オブジェクトの位置を少し上に配置して調整した。
この TicTacToeGameBoard
を使う方のシーンで、場所を変えたとしても、正しく表示される。
TicTacToeGameBoard
はデフォルトではこの位置に表示される。
これを、カメラの角を左上に合わせて、実際のUIと重ねてみやすくすることができる。
とりあえずゲーム自体はできた。
今後の課題や気づき
- コードがとても汚い気がする。
- 本書のサンプルコードが非常に参考になる
- 例えばステートマシンとして
GameProgress
を用意しているため流れが見えやすい。
- リトライの処理
- チャットに組み込んでいるのもあり、切断の処理が甘い
- ボードを理想のサイズにして配置する方法をもっと学びたい
- 今回はCamera2Dを使うことを学んだ
- Assets Store 等からアセットを取得して、アセットを使う練習もしないとマズイ。
- そういえば Godotのリソースを全く使ってないな
- やればやるほど、GDScriptってつらくね?と思ってきてしまっている。
- でもGodot 4.5 で abstract クラスが生えてね??だいぶ快適になりそう。
- https://godotengine.org/article/dev-snapshot-godot-4-5-dev-5/
次へ

公開日
更新日