What do we want to do?
Synchronization of player actions in the game with a client-server architecture. It should be possible to play from the browser.
For example, let's implement a simple chat room:
When connecting:
The client receives a unique ID;
The client receives information about all other players (ID + name);
All other players receive information about the new player (ID + default name);
A login message appears on the console.
On loss of connection:
All other players receive information about the player's exit from the server (ID);
An exit message appears on the console.
When changing the name:
If the name is already taken, the player receives an error;
All players are notified of the name change;
A message appears on the console.
When sending a message to chat:
All players see the message in the log / console.
Note: nothing prevents you from implementing more complex networking (for example, the movement of players, some other actions) - but this is beyond the scope of this article and in itself is a rather complex topic. Chat is the simplest example to demonstrate that such an approach for transferring data, in principle, works - and this is the purpose of my article.
What happened?
The finished project can be studied here: https://github.com/ktori/godobuf-over-websocket-demo
Screenshots can be found at the end of the article.
What will we use?
Godot - free and open source ;
Protobuf - / ;
Godobuf - Godot, .gd (GDScript) .proto;
Ktor - Kotlin ( Kotlin - , - - - Protobuf, ).
, , :
;
, ;
VCS, .. ;
- - /.
Protobuf - , , , JSON - ;
Protobuf , .
- :
/ protobuf , , , ;
, protobuf , , .
.proto-, - game.proto. , ( - ).
:
syntax = "proto3";
//
option java_package = "me.ktori.game.proto";
//
option java_outer_classname = "GameProto";
, :
-
, - RPC Cl**Result . gRPC - godobuf gRPC-. :
//
// -
//
//
message ClSetName {
string name = 1;
}
//
message ClSendChatMessage {
string text = 1;
}
// ,
message ClMessage {
// ,
// ,
oneof data {
ClSetName set_name = 1;
ClSendChatMessage send_chat_message = 2;
}
}
-
//
// -
//
// ClSetName
message ClSetNameResult {
// -
bool success = 1;
}
// -
message ClMessageResult {
oneof result {
ClSetNameResult set_name = 1;
}
}
//
// ID
message SvConnected {
int32 id = 1;
string name = 2;
}
//
// ID
message SvClientConnected {
int32 id = 1;
string name = 2;
}
//
// ID
message SvClientDisconnected {
int32 id = 1;
}
//
// ID
message SvNameChanged {
int32 id = 1;
string name = 2;
}
//
message SvChatMessage {
int32 from = 1;
string text = 2;
}
//
message SvMessage {
// SvMessage
oneof data {
ClMessageResult result = 1;
SvConnected connected = 2;
SvClientConnected client_connected = 3;
SvClientDisconnected client_disconnected = 4;
SvNameChanged name_changed = 5;
SvChatMessage chat_message = 6;
}
}
:
ClMessage
;
SvMessage
;
result -
ClMessageResult
.
naming convention:
ClFooBar
, ;
SvFooBar
, , :
ClFooBarResult
ClFooBar
.
Godot
( 2D ).
Godobuf
: https://github.com/oniksan/godobuf, README - addons.
WebSocketClient
( WebSocketClient). : , URL .
, - :
extends Node2D
var ws: WebSocketClient
#
func _ready():
# WebSocketClient
ws = WebSocketClient.new()
ws.connect("connection_established", self, "_on_ws_connection_established")
ws.connect("data_received", self, "_on_ws_data_received")
# 8080
ws.connect_to_url("ws://127.0.0.1:8080")
#
func _on_ws_connection_established(_protocol):
pass
#
func _on_ws_data_received():
pass
protobuf:GDScript
! Godobuf proto- :
- , .
- . pressed
Send Rename . show_message
, Label VBoxContainer, .
- .
:
const GameProto = preload("res://game_proto.gd")
ClMessage Send/Rename:
# $Name
func _on_SetName_pressed():
var msg = GameProto.ClMessage.new()
var sn = msg.new_set_name()
sn.set_name(name_input.text)
send_msg(msg)
# $Message
func _on_SendMessage_pressed():
var msg = GameProto.ClMessage.new()
var scm = msg.new_send_chat_message()
scm.set_text(message_input.text)
message_input.clear()
send_msg(msg)
- send_msg. :
# ClMessage
func send_msg(msg: GameProto.ClMessage):
# ClMessage PoolByteArray ws
ws.get_peer(1).put_packet(msg.to_bytes())
to_bytes
( ClMessage
) godobuf - !
- . , - , .
#
func _process(_delta):
# ,
ws.poll()
#
func _on_ws_connection_established(_protocol):
show_message("Connection established!")
#
func _on_ws_data_received():
#
for i in range(ws.get_peer(1).get_available_packet_count()):
#
var bytes = ws.get_peer(1).get_packet()
var sv_msg = GameProto.SvMessage.new()
#
sv_msg.from_bytes(bytes)
#
_on_proto_msg_received(sv_msg)
#
func _on_proto_msg_received(msg: GameProto.SvMessage):
# .. oneof -
#
if msg.has_connected():
pass
elif msg.has_client_connected():
pass
elif msg.has_client_disconnected():
pass
elif msg.has_chat_message():
pass
elif msg.has_name_changed():
pass
elif msg.has_result():
pass
else:
push_warning("Received unknown message: %s" % msg.to_string())
poll
WebSocketClient
, . _process
- ID :
# ID
var own_id: int
# ID <>
var names = Dictionary()
:
# _on_proto_msg_received
if msg.has_connected():
var c = msg.get_connected()
own_id = c.get_id()
name_input.text = c.get_name()
show_message("Welcome! Your ID is %d and your assigned name is '%s'." % [c.get_id(), c.get_name()])
if/elif . GitHub: Main.gd
. - Kotlin Ktor. , GitHub - .
:
gradle- :
server - ;
proto - - :
com.google.protobuf, com.google.protobuf:protobuf-java ;
, / -.
- , broadcast- , .
Godot- , Linux/Windows/Android .. - .
. , :
Error handling (for example, passing a separate message
error
toClMessageResult
);
Connection loss / restoration handling;
Much more.
I hope this article was helpful and helped to understand Godot, websockets and protobuf.