最終確認日
オンラインゲームの仕組みの本ー第3章をGodotに置き換えながら読む
オンラインゲームのしくみ-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
│   ├── binGDScriptにはインターフェースはないので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 -1p55 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 = 03.4のメモ
まとめ。
- 3章を読んだだけではライブラリが完成しなかった。TransportUDP.csはでてきてもないぞ?
- サポートページでサンプルコードを見つけるぞい。- あった。
- サポートファイルがUnityプロジェクトまるごとなので該当ファイルを探すのが大変
- ITransportインターフェースはいずこへ?!?!?!?
- コメントはすごい書いてあるので丁寧だけど、あまりに本の内容と違いすぎるかも。
 
サンプルコードとあまりに違いすぎる問題があるが、ChatGPTに助けてもらいながらGodotで同様なライブラリを作ってみるぞい。
Godotへの移植
- GDScriptにはインターフェースが存在しない
- Godotのシーン(.tscn) を機能ごとに作り、Instantiate Nodeで紐づけるのが良さそう。
- Node(Scene)単位で機能ごとに分離し、それらを動的に紐づける
- Godotでプラグインを自作してみる
色々頑張ってみたあと。
- インターフェースっぽく実装してみようと試みたけれど、非常に難しい。
- extendsを使ってひとつのクラスのみしか継承できない。
- Nodeをつけたり外したりして、テスト用の- Nodeを作れるとベストなわけだ。- ってことはやっぱりいまのであってるかな
 
- う〜ん、ちゃんとGodotらしいなら MultiplayerPeerとかをラップしたライブラリを作るほうがいいね
- とりあえず本を読み進めていきたいので、本の通りに書いて実装してみる。インターフェースとか設計は諦める。
第4章へ
ライブラリ化されたサンプルコードが本書に書かれていない部分が多くあったため、まずは先に進んでみることにする。

公開日
更新日
