最終確認日
オンラインゲームの仕組みの本ー第4章をGodotに置き換えながら読む
概要
チャットプログラムを作る
まずは読む
- 片方がサーバー、片方がクライアントとなる
- 3章で使った
TranportTCP.cs
とPacketQueue.cs
が必要になる - チャット参加後は 毎フレーム呼ばれるUpdate 関数で、送信と受信の処理を退出するまで繰り返す
ChatState
を用意しておくといい- ユーザーからのインプットがあったときに何かを実行するのではなく、Stateを変更して、毎フレームそれを監視してアクションを行う感じ
- 退出ボタンを押す
- この時は
ChatState
のみを変更する
- この時は
- 次のUpdate関数が呼ばれるときに実際に退出処理をしている
- UIイベントで直接重い処理をせず、状態遷移だけを行うことで、ロジックの整合性を保ち、処理の分離を実現している。
- GUI関数(
ChattingGUI()
)ではm_state
の変更にとどめ、重い/危険な処理はUpdate()
側に委ねる設計。 - Swiftだとユーザーからのイベント駆動で処理をすることが多いので、ボタン押下で直接処理を呼び出しちゃいそうだけど、頭を切り替えないとダメね。
- 退出ボタンを押す
置き換えてみる
画面
Robby.tscn
最初のタイトル画面CreateRoomButton
ホストを選択して部屋を作成するJoinRoomButton
クライアントとして部屋に入る- IPアドレスを入力する
- エラーハンドリングをする
ChatRoom.tscn
チャットルーム画面LeaveButton
退出ボタンSendMessageButton
メッセージを送信ボタンMessageField
メッセージを表示するテキストフィールド
通信
TransportTCP.tscn
通信ライブラリ(3章で作った)~~`PacketQueue.gd` 送受信するデータを保存するキュー(3章でつくった)
- 使わなかった
Chat.tscn
チャットプログラムChatState
を定義するChatRoom
から送信/受信処理を呼び出す
これらは Autoload で設定する必要がありそう。
画像を使ったボタンの配置方法を忘れた。
call_deferred("emit_signal", "data_received", data)
のようにバックグラウンドスレッドからシグナルを呼んでメインスレッドに渡して実行する場合にはcall_deffered
をしないとエラーになる。LineEdit
(UIKitでいうとTextField
) とTextEdit
(UIKitでいうとTextView
)の2種類ある
このPoll関数がないとやっぱりうまくいかない。書く必要がある。
func _process(delta):
if _socket == null or _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
return
_socket.poll() # ここが必要(クライアント側だけ)
if _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
print("接続完了!")
チャットルーム画面
- 自分が送る内容は送った瞬間そのまま表示して良い?
- それとも届いた時間をタイムスタンプで返してもらって前後関係を正しくしたほうが良い?
- チャット欄に追加するタイミングはメッセージを送信する時とメッセージを受信した時
- 送信した内容を自分自身のチャット欄に表示するのを忘れないように。
- 相手が切断したのを検知したい
- 定期的な"PING/PONG"通信を行うといいらしい(ChatGPTいわく)
- 複数人同時にはむずそう?接続したと思ったけど1人としかできてない
できた
あんまりキレイとは言えないかもだけど、とりあえずできた。
気をつけた点
TransportTCP
の処理はChat
から呼び出すようにしたChatRoom
やRobby
からはChat
を呼び出して、直接TransportTCP
は呼ばないようにしたpoll()
でクライアントのステータスを更新するのが大切- 退出メッセージを送ってから退出するようにした。
transport_tcp.gd
extends Node
# 通信イベント通知
signal connection_success
signal connection_failed
signal disconnected
signal join_new_user
signal data_received(data: PackedByteArray)
# 状態フラグ
var is_server := false
var is_connected := false
# 内部メンバ
var _listener: TCPServer
var _socket: StreamPeerTCP
var _thread := Thread.new()
var _thread_loop := false
# 送受信バッファ(ゲームスレッドと通信スレッド間のやりとり)
var _send_queue: Array[PackedByteArray] = []
var _recv_queue: Array[PackedByteArray] = []
const MTU := 1400
func _ready():
_listener = TCPServer.new()
_socket = StreamPeerTCP.new()
func _process(delta):
# クライアント側のみの処理
if _socket == null or is_server:
return
if _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
return
# ステータスが反映される
_socket.poll()
match _socket.get_status():
StreamPeerTCP.STATUS_CONNECTED:
if not is_connected:
print("接続完了!")
is_connected = true
_start_thread()
connection_success.emit()
StreamPeerTCP.STATUS_NONE:
print("接続されていません")
is_connected = false
StreamPeerTCP.STATUS_ERROR:
print("接続エラーが発生しました")
connection_failed.emit()
_socket = null
# サーバーを起動する
func start_server(port: int) -> bool:
print("サーバーを作成します.")
if _listener.is_listening():
return false
var err := _listener.listen(port)
if err != OK:
push_error("サーバー起動失敗: %s" % err)
return false
is_server = true
_start_thread()
return true
# サーバーを止める
func stop_server():
_thread_loop = false
if _thread.is_alive():
_thread.wait_to_finish()
if _listener.is_listening():
_listener.stop()
close_connection()
is_server = false
# ホストに接続する
func connect_to_host(address: String, port: int) -> bool:
_socket = StreamPeerTCP.new()
var err := _socket.connect_to_host(address, port)
if err != OK:
is_connected = false
connection_failed.emit()
return false
return true
# 接続を解除する
func close_connection():
if is_connected and _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
_socket.disconnect_from_host()
is_connected = false
_thread_loop = false
_thread = Thread.new()
# 前回のデータが残る場合がある.
_send_queue.clear()
_recv_queue.clear()
disconnected.emit()
func send(data: PackedByteArray):
_send_queue.append(data)
func _start_thread():
if _thread.is_alive():
_thread.wait_to_finish()
_thread_loop = true
_thread.start(_dispatch)
func _dispatch():
print("Dispatch thread started.")
while _thread_loop:
if is_server:
_accept_new_client()
if _check_socket_connected():
_process_send()
_process_receive()
OS.delay_msec(20)
print("Dispatch thread ended.")
# サーバー側の処理.
# クライアントの待ち受けをする.
func _accept_new_client():
if _listener.is_connection_available():
_socket = _listener.take_connection()
if _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
is_connected = true
call_deferred("emit_signal", "join_new_user")
print("クライアント接続成功")
else:
print("クライアント接続失敗 状態:")
func _check_socket_connected() -> bool:
if is_connected and _socket and _socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
return true
return false
func _process_send():
while not _send_queue.is_empty():
var data = _send_queue.pop_front()
_socket.put_data(data)
func _process_receive():
while _socket.get_available_bytes() > 0:
var result = _socket.get_partial_data(MTU)
var error = result[0]
var data: PackedByteArray = result[1]
if error != OK:
close_connection()
break
_recv_queue.append(data)
call_deferred("emit_signal", "data_received", data)
## Public
func receive() -> PackedByteArray:
if _recv_queue.is_empty():
return PackedByteArray()
return _recv_queue.pop_front()
chat.gd
extends Node
# Autoload: Chat
signal received_message(text: String)
enum State {
HOST_TYPE_SELECT = 0,
CHATTING,
LEAVE,
ERROR
}
var current_state: State = State.HOST_TYPE_SELECT
var _has_left := false
## Life cycle
func _ready() -> void:
TransportTCP.data_received.connect(_on_data_received)
TransportTCP.disconnected.connect(_on_disconnected_server)
TransportTCP.connection_success.connect(_on_connection_success)
TransportTCP.join_new_user.connect(_on_join_new_user)
func _process(delta: float) -> void:
match current_state:
State.LEAVE:
_update_leave()
## Private
func _update_leave():
if TransportTCP.is_server:
TransportTCP.stop_server()
else:
TransportTCP.close_connection()
## Public
func leave():
if _has_left:
return
_has_left = true
var message = "hogeが退出しました"
TransportTCP.send(message.to_utf8_buffer())
await get_tree().create_timer(0.25).timeout
current_state = State.LEAVE
## Signal
func _on_disconnected_server():
current_state = State.HOST_TYPE_SELECT
_has_left = false
# サーバー側/クライアント側問わずに自分自身はロビーに戻る
get_tree().change_scene_to_file("res://scripts/screens/robby/robby.tscn")
func _on_data_received(data: PackedByteArray):
var message = data.get_string_from_utf8()
received_message.emit(message)
func _on_connection_success():
print("ルームに入ります")
var message = "hogeが参加しました。"
TransportTCP.send(message.to_utf8_buffer())
func _on_join_new_user():
print("新しいユーザーが参加しました")
robby.gd
extends Node2D
@onready var create_room_button = %CreateRoomButton
@onready var join_room_button = %JoinRoomButton
@onready var join_view = %JoinView
@onready var join_button = %JoinButton
@onready var ip_address_edit = %IPAddressEdit
@onready var error_popup_view = %ErrorPopupView
@onready var close_join_view_button = %CloseJoinViewButton
const PORT = 12345
## Life cycle
func _ready() -> void:
create_room_button.pressed.connect(_on_pressed_create_room_button)
join_room_button.pressed.connect(_on_pressed_join_room_button)
join_button.pressed.connect(_on_pressed_join_button)
close_join_view_button.pressed.connect(_on_pressed_close_join_view_button)
TransportTCP.connection_success.connect(_on_connection_success)
TransportTCP.connection_failed.connect(_on_connection_failed)
log_local_ip()
Chat.current_state = Chat.State.HOST_TYPE_SELECT
func log_local_ip():
var local_ips = IP.get_local_addresses()
for ip in local_ips:
if ip.begins_with("192.") or ip.begins_with("10.") or ip.begins_with("172."):
print("自分のIPアドレス: ", ip)
## シグナル
func _on_pressed_create_room_button():
var is_start_server = TransportTCP.start_server(PORT)
if is_start_server:
get_tree().change_scene_to_file("res://scripts/screens/chat_room/chat_room.tscn")
func _on_pressed_join_room_button():
join_view.visible = true
func _on_pressed_join_button():
print("IPAddress: ", ip_address_edit.text)
var ip_address = ip_address_edit.text
TransportTCP.connect_to_host(ip_address, PORT)
func _on_connection_success():
get_tree().change_scene_to_file("res://scripts/screens/chat_room/chat_room.tscn")
func _on_connection_failed():
error_popup_view.show_error("接続に失敗しました", "接続が失敗しました。")
func _on_pressed_close_join_view_button():
join_view.visible = false
chat_room.gd
extends Node2D
@onready var leave_button = %LeaveButton
@onready var chat_message_view = %ChatMessageView
@onready var chat_message_edit = %ChatMessageEdit
## Life cycle
func _ready() -> void:
leave_button.pressed.connect(_on_pressed_leave_button)
chat_message_edit.text_submitted.connect(_on_text_submitted_chat_message_edit)
add_message("チャットルームへようこそ")
Chat.current_state = Chat.State.CHATTING
Chat.received_message.connect(_on_received_message)
## Method
func add_message(new_message: String):
if len(chat_message_view.text) == 0:
chat_message_view.text = new_message
else:
chat_message_view.text = chat_message_view.text + "\n" + new_message
## Signal
func _on_pressed_leave_button():
Chat.leave()
func _on_text_submitted_chat_message_edit(new_text: String):
if len(new_text) == 0:
return
Chat.send(new_text)
# ちゃんと届いているか確認したほうがよい??
add_message(new_text)
# 文字列をリセットする
chat_message_edit.text = ""
func _on_received_message(text: String):
add_message(text)
次へ
とりあえずオンラインゲームのしくみ-Unityで覚えるネットワークプログラミングの本の4章部分はできたんじゃなかろうかと思うので、次は5章を読み始める。

公開日
更新日