Multijogador multiplataforma sem dor em Godot

O que nós queremos fazer?

Sincronização das ações do jogador no jogo com uma arquitetura cliente-servidor. Deve ser possível jogar no navegador.





Por exemplo, vamos implementar uma sala de chat simples:





  1. Ao conectar:





    1. O cliente recebe um ID exclusivo;





    2. O cliente recebe informações sobre todos os outros jogadores (ID + nome);





    3. Todos os outros jogadores recebem informações sobre o novo jogador (ID + nome padrão);





    4. Uma mensagem de login é exibida no console.





  2. Na perda de conexão:





    1. Todos os outros jogadores recebem informações sobre a saída do jogador do servidor (ID);





    2. Uma mensagem de saída aparece no console.





  3. Ao mudar o nome:





    1. Se o nome já estiver em uso, o jogador recebe um erro;





    2. Todos os jogadores são notificados da mudança de nome;





    3. Uma mensagem aparece no console.





  4. Ao enviar uma mensagem para o bate-papo:





    1. Todos os jogadores veem a mensagem no log / console.





Nota: nada impede que você implemente redes mais complexas (por exemplo, a movimentação de jogadores, algumas outras ações) - mas isso está além do escopo deste artigo e é um tópico bastante complexo. O chat é o exemplo mais simples para demonstrar que tal abordagem para transferência de dados, em princípio, funciona - e este é o objetivo do meu artigo.





O que aconteceu?

O projeto concluído pode ser estudado aqui: https://github.com/ktori/godobuf-over-websocket-demo





As capturas de tela podem ser encontradas no final do artigo.





O que vamos usar?

  • Godot - motor de jogo multiplataforma gratuito e de código aberto;





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





Projeto após a instalação do addon godobuf
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- :





Janela Godobuf
Godobuf

- , .





 Cena

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





Cliente Nativo
Cliente WebSocket
WebSocket-

. , :





  • Tratamento de erros (por exemplo, passar uma mensagem separada error



    para ClMessageResult



    );





  • Tratamento de perda / restauração de conexão;





  • Muito mais.





Espero que este artigo tenha sido útil e ajudado a entender Godot, websockets e protobuf.








All Articles