Que voulons-nous faire?
Synchronisation des actions des joueurs dans le jeu avec une architecture client-serveur. Il devrait être possible de jouer à partir du navigateur.
Par exemple, implémentons une salle de chat simple:
Lors de la connexion:
Le client reçoit un identifiant unique;
Le client reçoit des informations sur tous les autres joueurs (ID + nom);
Tous les autres joueurs reçoivent des informations sur le nouveau joueur (ID + nom par défaut);
Un message de connexion apparaît sur la console.
En cas de perte de connexion:
Tous les autres joueurs reçoivent des informations sur la sortie du joueur du serveur (ID);
Un message de sortie apparaît sur la console.
Lors du changement de nom:
Si le nom est déjà pris, le joueur reçoit une erreur;
Tous les joueurs sont informés du changement de nom;
Un message apparaît sur la console.
Lors de l'envoi d'un message pour discuter:
Tous les joueurs voient le message dans le journal / la console.
Remarque: rien ne vous empêche de mettre en œuvre un réseau plus complexe (par exemple, le mouvement des joueurs, certaines autres actions) - mais cela dépasse le cadre de cet article et est en soi un sujet assez complexe. Le chat est l'exemple le plus simple pour démontrer qu'une telle approche de transfert de données fonctionne en principe - et c'est le but de mon article.
Qu'est-il arrivé?
Le projet fini peut être étudié ici: https://github.com/ktori/godobuf-over-websocket-demo
Des captures d'écran se trouvent à la fin de l'article.
Qu'allons-nous utiliser?
Godot - moteur de jeu multiplateforme gratuit et 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 .. - .
. , :
Gestion des erreurs (par exemple, passer un message séparé
error
àClMessageResult
);
Gestion de la perte / restauration de connexion;
Beaucoup plus.
J'espère que cet article a été utile et a aidé à comprendre Godot, Websockets et protobuf.