最終確認日
online game book 3 godot memo
オンラインゲームのしくみ-Unityで覚えるネットワークプログラミングの第3章「通信プログラムを作ってみよう」をGodot+GDScriptに置き換えながら読む。
3.1 のメモ
TCPのソケット通信
- ソケット
- ソケット通信
- TCPのソケット通信は携帯電話で電話をかけるまでに行う手続きと似ている。
- Unityにおける
new Socket
部分のサンプルコードをGodotで書いてみる。 - Socket クラスの
Accept
関数はクライアントからの接続要求があるまで処理をブロッキングするため、Poll関数を使って、データを受信したときだけ実行するようにする必要がある。- Poll関数はGodotの場合には必要がない?(デフォルトで非ブロッキングため)
- でも Godot 4.x にはPoll関数は存在している
- クライアント側に Poll関数を使わないと
CONNECTING
からCONNECTED
に変わらなかった
- クライアント側の実装 p40
- Godotでは
NoDelay
やSendBufferSize
にあたるものは用意されていない。
- Godotでは
receive
関数を呼び出すまでは、データはシステム側のバッファに保存され続ける- つまり以下のコードで
send_data()
を2回呼び出してからreveice_data()
を実行すると、2つのテキストが取り出される。
- つまり以下のコードで
- 通信の切断
- Godotでは
disconnect_from_host()
とsocket
へのnull
代入を使う - 待ち受けの終了は
server.stop()
- Godotでは
p38,39のコードをGodotに書き換え
extends Node
class_name MyTCPServer
var server: TCPServer
var socket: StreamPeerTCP = null
func _process(delta: float) -> void:
accept_client()
func start_listner(port: int):
server = TCPServer.new()
# 待ち受けを開始する
var error := server.listen(port)
if error != OK:
print("TCPサーバー起動失敗:", error)
else:
print("OK!")
func accept_client():
if server != null && server.is_connection_available():
socket = server.take_connection() # 接続ソケットを取得
print("クライアント接続!",socket.get_connected_host())
# このpeerは StreamPeerTCP のインスタンス
func receive_data():
if socket == null || socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
print("受け取り失敗", socket)
return
var available := socket.get_available_bytes()
if available > 0:
var result := socket.get_data(available)
# 0にはエラーが入る
var error = result[0]
var buffer = result[1]
if error == OK and buffer.size() > 0:
var message = buffer.get_string_from_utf8()
print("クライアントからのメッセージ:", message)
else:
print("受信エラー:", error)
else:
print("available:", available)
クライアント側
extends Node
class_name MyTCPServer
var server: TCPServer
var socket: StreamPeerTCP = null
func _process(delta: float) -> void:
accept_client()
func start_listner(port: int):
server = TCPServer.new()
# 待ち受けを開始する
var error := server.listen(port)
if error != OK:
print("TCPサーバー起動失敗:", error)
else:
print("OK!")
func accept_client():
if server != null && server.is_connection_available():
socket = server.take_connection() # 接続ソケットを取得
print("クライアント接続!",socket.get_connected_host())
# このpeerは StreamPeerTCP のインスタンス
func receive_data():
if socket == null || socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
print("受け取り失敗", socket)
return
var available := socket.get_available_bytes()
if available > 0:
var result := socket.get_data(available)
# 0にはエラーが入る
var result_code = result[0]
var buffer = result[1]
if result_code == OK and buffer.size() > 0:
var message = buffer.get_string_from_utf8()
print("クライアントからのメッセージ:", message)
else:
print("受信エラー:", result_code)
サーバーの待ち受け終了。
func stop_listener():
# 待ち受けを終了する
if server != null:
server.stop()
server = null
socket = null
print("End server communication.")
クライアントの切断。
func disconnect_from_server():
if socket == null or socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
return
socket.disconnect_from_host()
socket = null
print("End client communication.")
こんな感じで確認できる物を作った。
UDPのソケット通信
- UDPは接続処理を行わずに通信することができる。
- ソケットを生成して使用するポート番号を指定するだけ。
- 1つのソケットで複数の通信先に送信ができる。
- 受信時にはIPアドレスとポート番号を取得して送信元を識別
- PacketPeerUDPでは
udp.get_packet_ip()
とudp.get_packet_port()
- PacketPeerUDPでは
受け取り側
extends Node
# 便宜上サーバーという名前にする
class_name MyUDPServer
const IP_ADDRESS = "127.0.0.1"
const PORT = 12346
var udp: PacketPeerUDP = null
func _ready() -> void:
udp = PacketPeerUDP.new()
udp.bind(PORT)
func _process(delta):
if udp == null:
return
# 常に監視しておく
if udp.get_available_packet_count() > 0:
var packet := udp.get_packet()
var sender_ip := udp.get_packet_ip()
var sender_port := udp.get_packet_port()
var message := packet.get_string_from_utf8()
print("受信:", message, " ← ", sender_ip, ":", sender_port)
送信側
extends Node
class_name MyUDPClient
func send_message(text: String, address: String, port: int):
# サーバーへ接続
var udp = PacketPeerUDP.new()
udp.bind(port)
udp.set_dest_address(address, port)
# メッセージを送信
var data = text.to_utf8_buffer()
udp.put_packet(data)
# 切断
udp.close()
print("End client communication.")
ボタンを押してデータを送信
extends Node2D
@export var server: MyUDPServer
@export var client: MyUDPClient
@onready var send_button = %SendButton
func _ready() -> void:
send_button.pressed.connect(_on_pressed_send_button)
func _on_pressed_send_button():
if server.udp == null:
return
var address = server.IP_ADDRESS
var port = server.PORT
client.send_message("Hello, this is client.", address, port)
Godot におけるソケット通信のためのクラス
Godotクラス名 | UnityでのSocket種別に対応 | 用途 |
---|---|---|
StreamPeerTCP |
SocketType.Stream |
TCP通信(ストリーム型) |
PacketPeerUDP |
SocketType.Dgram |
UDP通信(パケット型) |
WebSocketPeer |
WebSocket |
ブラウザ対応や中継用のソケット |
3.2のメモ
- 2バイト以上のデータを送受信するときはバイトオーダーに注意する
- バイトオーダー
- ビッグエンディアン
- リトルエンディアン
- ネットワークバイトオーダー
- ホストバイトオーダー
- エンディアンはプロセッサによって異なる
- C#では
IPAddress
クラスにバイトオーダーを変換するメソッドがある。 - Godot は内部的にはリトルエンディアン(Little Endian)環境が多いらしい。
- C#の
HostToNetworkOrder
のようなものはないので、自分で用意する必要があるらしい。
- C#の
#ChatGPTより引用
データの種類 | エンディアン考慮必要? | 備考 |
---|---|---|
テキスト(UTF-8) | ❌ 不要 | バイト列のままでOK |
整数(int32等) | ✅ 必要 | ネットワーク標準はビッグ |
float(実数) | ✅ 必要 | 同上(ビット順も含め) |
JSONやXML | ❌ 不要 | テキスト形式なので関係ない |
自作バイナリ形式 | ✅ 必要 | プロトコル設計時は必須 |
[[Notes/Godot | Godot]]での対策 |
func int_to_big_endian_bytes(val: int) -> PackedByteArray:
var bytes := PackedByteArray()
bytes.append((val >> 24) & 0xFF)
bytes.append((val >> 16) & 0xFF)
bytes.append((val >> 8) & 0xFF)
bytes.append(val & 0xFF)
return bytes
スコアなどを送信するとき
# 変換が必要
socket.put_data(int_to_big_endian_bytes(100))
大変難しいおはなし。
3.3のメモ
簡単な通信ライブラリを作る
Socket
クラスのインスタンスをゲームプログラムから見えないようにする- そうそうこれがやりたかった。
- 通信ライブラリが処理を行うもの
- 待ち受け開始 (UDPは受信ポートの指定のみ) / 待ち受け終了
- 接続 / 切断
- 送信 / 受信
- 発生するイベント
- 接続
- 切断
- 送信エラー
- 受信エラー
- これらはGodotではシグナルを使ってライブラリからゲームプログラム側へ通知するとよさそう
- 通信処理をゲーム側のスレッド(メインスレッド)に負荷をかけずに別のスレッドで行ってみよう
- 排他制御をする
- ロックを使う
- メインスレッドと通信スレッドで共有するためのキューを用意する
MemoryStream
クラスはGDScriptではないので、代わりに StreamPeerBufferやPackedByteArrayを使うとよさそう?- 通信スレッドは5msごとに実行されるように設定した。
while
の内部でThread.Sleep(5)
を呼び出すことで5ms間隔で実行している
MonoBehaviour
を継承したTransportTCP
クラスとTransportUDP
クラスをそれぞれ作る- GDScriptでは
Node
を継承したクラスで良さそう。 - しかしインタフェースがないので難しいぞ。
- GDScriptでは
サポートファイル群
.
├── NetworkLibrary
│ ├── Assembly-CSharp-vs.csproj
│ ├── Assembly-CSharp.csproj
│ ├── Assets
│ │ ├── Prefab
│ │ │ ├── TransportTCP.prefab
│ │ │ ├── TransportTCP.prefab.meta
│ │ │ ├── TransportUDP.prefab
│ │ │ └── TransportUDP.prefab.meta
│ │ ├── Prefab.meta
│ │ ├── Scene
│ │ │ ├── NetworkLibrary.unity
│ │ │ └── NetworkLibrary.unity.meta
│ │ ├── Scene.meta
│ │ ├── Script ✅ ここに入っている!
│ │ │ ├── LibrarySample.cs
│ │ │ ├── LibrarySample.cs.meta
│ │ │ ├── NetworkDef.cs
│ │ │ ├── NetworkDef.cs.meta
│ │ │ ├── PacketQueue.cs
│ │ │ ├── PacketQueue.cs.meta
│ │ │ ├── TransportTCP.cs
│ │ │ ├── TransportTCP.cs.meta
│ │ │ ├── TransportUDP.cs
│ │ │ └── TransportUDP.cs.meta
│ │ └── Script.meta
│ ├── bin
GDScriptにはインターフェースはないのでabstractクラスを作る形にする
待ち受け開始/待ち受け終了
extends Node
class_name ClientInterface
# 接続を開始する(例: address = "127.0.0.1", port = 1234)
func connect_to_server(address: String, port: int) -> bool:
assert(false, "connect_to_server() must be overridden")
return false
# 接続を切断する
func disconnect_from_server() -> bool:
assert(false, "disconnect_from_server() must be overridden")
return false
接続/切断
extends Node
class_name ClientInterface
# 接続開始
func connect(address: String, port: int) -> bool:
assert(false, "connect() must be overridden")
return false
# 接続終了
func disconnect() -> bool:
assert(false, "disconnect() must be overridden")
return false
送信/受信
extends Node
class_name TransportInterface
# データ送信
func send(data: PackedByteArray, size: int) -> int:
assert(false, "send() must be implemented by subclass")
return -1
# データ受信
func receive(buffer: PackedByteArray, size: int) -> int:
assert(false, "receive() must be implemented by subclass")
return -1
イベント定義
enum NetEventType {
CONNECT = 0,
DISCONNECT,
SEND_ERROR,
RECEIVE_ERROR
}
イベント結果
enum NetEventResult {
Failure = -1,
Success = 0
}
通知するイベントの状態
class_name NetEventState
var type: int
var result: int
イベント通知のデリゲート定義(Signal(Godot))
signal net_event(state: NetEventState)
# 発火
net_event.emit(state)
もしくはハンドラで
# 任意の関数をハンドラとして受け取る
func invoke_event(handler: Callable, state: NetEventState) -> void:
if handler.is_valid():
handler.call(state)
メインスレッドと通信スレッドで共有するためのパケットキュー
extends Node
class_name PacketQueueInterface
# パケットをキューに追加する
func enqueue(data: PackedByteArray, size: int) -> int:
assert(false, "enqueue() must be implemented by subclass")
return -1
# パケットをキューから取り出す
func dequeue(buffer: PackedByteArray, size: int) -> int:
assert(false, "dequeue() must be implemented by subclass")
return -1
p55 PacketQueue.cs
を GDScriptで書き換え
extends Node
class_name PacketQueue
## 内部パケット情報構造
class PacketInfo:
var offset: int
var size: int
## 内部バッファとパケット管理
var _buffer := StreamPeerBuffer.new()
var _packet_infos: Array[PacketInfo] = []
var _write_offset := 0
## パケットを追加
func enqueue(data: PackedByteArray, size: int) -> int:
var info := PacketInfo.new()
info.offset = _write_offset
info.size = size
_packet_infos.append(info)
_buffer.seek(_write_offset)
_buffer.put_data(data.slice(0, size))
_write_offset += size
return size
## パケットを取り出す(取り出せたサイズを返す)
func dequeue() -> PackedByteArray:
if _packet_infos.is_empty():
return PackedByteArray()
var info: PacketInfo = _packet_infos.pop_front()
_buffer.seek(info.offset)
var packet := _buffer.get_data(info.size)
# 全パケット取り出し済みならリセット
if _packet_infos.is_empty():
clear()
return packet
## キューを空にしてメモリを解放
func clear() -> void:
_buffer.clear()
_write_offset = 0
3.4のメモ
まとめ。
- 3章を読んだだけではライブラリが完成しなかった。
TransportUDP.cs
はでてきてもないぞ? - サポートページでサンプルコードを見つけるぞい。
- あった。
- サポートファイルがUnityプロジェクトまるごとなので該当ファイルを探すのが大変
ITransport
インターフェースはいずこへ?!?!?!?- コメントはすごい書いてあるので丁寧だけど、あまりに本の内容と違いすぎるかも。
サンプルコードとあまりに違いすぎる問題があるが、ChatGPTに助けてもらいながらGodotで同様なライブラリを作ってみるぞい。
Godotへの移植
- GDScriptにはインターフェースが存在しない
- Godotのシーン(
.tscn
) を機能ごとに作り、Instantiate Node
で紐づけるのが良さそう。 - Node(Scene)単位で機能ごとに分離し、それらを動的に紐づける
- Godotでプラグインを自作してみる
色々頑張ってみたあと。
- インターフェースっぽく実装してみようと試みたけれど、非常に難しい。
extends
を使ってひとつのクラスのみしか継承できない。Node
をつけたり外したりして、テスト用のNode
を作れるとベストなわけだ。- ってことはやっぱりいまのであってるかな
- う〜ん、ちゃんとGodotらしいなら
MultiplayerPeer
とかをラップしたライブラリを作るほうがいいね - とりあえず本を読み進めていきたいので、本の通りに書いて実装してみる。インターフェースとか設計は諦める。
第4章へ
ライブラリ化されたサンプルコードが本書に書かれていない部分が多くあったため、まずは先に進んでみることにする。

公開日
更新日