Cross-platform multiplayer on Godot without pain

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:





  1. When connecting:





    1. The client receives a unique ID;





    2. The client receives information about all other players (ID + name);





    3. All other players receive information about the new player (ID + default name);





    4. A login message appears on the console.





  2. On loss of connection:





    1. All other players receive information about the player's exit from the server (ID);





    2. An exit message appears on the console.





  3. When changing the name:





    1. If the name is already taken, the player receives an error;





    2. All players are notified of the name change;





    3. A message appears on the console.





  4. When sending a message to chat:





    1. 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 , , .





: game.proto





.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.





Project after installing godobuf addon
godobuf

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- :





Godobuf window
Godobuf

- , .





 Scene

- . 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 .. - .





Native client
WebSocket client
WebSocket-

. , :





  • Error handling (for example, passing a separate message error



    to ClMessageResult



    );





  • Connection loss / restoration handling;





  • Much more.





I hope this article was helpful and helped to understand Godot, websockets and protobuf.








All Articles