Godot Multiplayer RPC 实例演示
紧果呗
2023年02月06日 19:19
收录于文集
共40篇

### 🟠🟢🔵 Multiplayer RPC 实例演示

  • [Multiplayer Bomber Demo](https://godotengine.org/asset-library/asset/139)

  • [Pong Multiplayer](https://godotengine.org/asset-library/asset/138)

  • [Pong Non-Multiplayer](https://github.com/godotengine/godot-demo-projects/tree/master/2d/pong)

  • [Godot Multiplayer Networking Cheatsheet](https://blog.iancolwell.ca/godot-multiplayer-networking-cheatsheet)

  • [Godot Multiplayer RPC](https://github.com/jeangowhy/godot-tour/tree/4.x/multiplayer_rpc)

使用 Godot 4.x RPC API 进行网络游戏开发的工作流程概要:

  •  配置 MultiplayerPeer 启动相应的服务器/客户端;

  •  建立连接,记录 peer ID 并给相应的节点设置主控端 ID;

  •  配置 RPC API,根据需要设置 RPC 的调用方式,主要是:any_peer authority call_local call_remote;

  •  通过 is_network_master() 或 is_multiplayer_authority() 主动判断当前节点是否归属于主控;

  •  执行 rpc() 或 rpc_id() 等方法执行 RPC 调用远程方法,进行数据同步;

  •  处理网络连接,以下结束连接;

使用 RPC API 涉及到的数据同步,需要通过定义各种 RPC 方法来实现,比如,以下例子中用于记录所有客户连接标志的 connected_peer_ids,在服务器端总能记录到所有客户端的 ID。但是新加入的客户端,只知道自己与服务器的连接,其它客户端的连接需要通过查询(同步)服务器中的数据。所以,每当有新客户端发出连接请求时,服务端都需要执行一个 new_peer_broadcast 广播,远程调用所有客户端的方法,以创建相应的新玩家节点。同时,对于原有玩家,需要通过 rpc_id() 指定 ID 调用新玩家主机上的 API,同步创建与服务场景树一致的节点结构:

代码块
Python
自动换行
复制代码
var connected_peer_ids = []

func new_peer_connected(peer_id):
    await get_tree().create_timer(0.5).timeout
    # create player on all clients side.
    rpc("new_peer_broadcast", peer_id)
    rpc_id(peer_id, "old_peer_replication", connected_peer_ids)
    # create player on server side for client.
    create_player(peer_id)

func create_player(peer_id = 1):
    connected_peer_ids.append(peer_id)
    var player = preload("res://player.tscn").instantiate()
    player.set_multiplayer_authority(peer_id)
    add_child(player)
    rpc("refresh_peer_node_inf")
    if peer_id == multiplayer.get_unique_id():
        local_player = player

@rpc 
#@rpc("authority", "call_remote", "reliable", 1)
func new_peer_broadcast(new_peer_id):
    create_player(new_peer_id)

@rpc
#@rpc("authority", "call_remote", "reliable", 1)
func old_peer_replication(peer_ids):
    for peer_id in peer_ids:
        create_player(peer_id)
复制成功

注意,默认的 @rpc 标注是 call_remote 方式调用,不会在主控端运行,只在其它所有远程端执行数据同步。

因为连接信号是服务器端的行为,所以 @rpc 方式调用就是在所有客户端执行,由服务器发起 RPC 调用,但是不包括服务器。

使用 RPC API 最后阶段包含结束连接处理,当客户端或服务器端断开连接,就应该做相当的处理。客户端结束,整个连网系统还在运行,只是其中一个对等端的退出,需要将其占用的资源清理掉。而对于服务器结束,所以客户端都会失去连网功能,要么等待服务器重新启动再尝试连接,要么清理所有客户端相关资源。MultiplayerPeer 需要关闭,并且释放 multiplayer_peer = null,同时场景中也不能再调用相关的 RPC API。

通过 MultiplayerAPI.has_multiplayer_peer() 可以查询当前的连接状态,作为执行 RPC 的依据。

服务器端开启后就会进行侦听,等待客户端的连接请求。而客户端连接服务器则需要根据网络状态而定,有时可能因为网络不稳定,需要越长时间的才能建立连接。更坏的情况是,根本不能正常建立连接,经过一段时间尝试还不能建立连接,就会因为连接超时,进而触发 connetion_failed 信号。可以说,connected_to_server 是最不重要的连接信号之一。

服务器信号使用不恰当可能会导致服务器端在首个客户端连接时,重复出现 ID = 1 的 peer 连接。一般只需要将客户端的信号,与服务器端的信号分开连接使用。

Godot 3 关键字与 Godot 4 标注的等价关系参考:

代码块
C++
自动换行
复制代码
|      Godot 3.5 keywords     |                 Godot 4 @rpc annotations                |
|-----------------------------|---------------------------------------------------------|
| remote                      | @rpc("any_peer", "call_remote")                         |
| remotesync                  | @rpc("any_peer", "call_local")                          |
| master                      | @rpc("authority", "call_remote")                        |
| mastersync                  | @rpc("authority", "call_local")                         |
| rpc_unreliable()            | @rpc("unreliable")                                      |
| rpc_unreliable(master func) | @rpc or @rpc("authority", "call_remote", "reliable", 1) |
复制成功

这个表可以作为 Godot 3.5 与 Godot 4.x Multiplayer RPC 开发参考。

标注使用示范如下,假设通过一个 Area3D 的输入事件触发 RPC 调用:

  •  shake_me() 方法使用默认标注,即在主控执行远端同步,等同旧版本的 `master` 关键字;

  •  shake_all() 方法所有对等端都执行,并且执行远端同步,等同旧版本的 `remote` 关键字;

  •  clicked_all() 方法在所有对等端执行,并且执行本地与远端同步,等同旧版本的 `remotesync` 关键字。

总结来看,network master 也好,multiplayer authority 也好,就是一个主控概念。场景树中所有节点都可以归属给网络中的一个对等端,而默认归属为服务器端。比如,创建一个节点 Player,默认状态就归属于服务器,在所有联网主机中它应该都有一个同样的副本,那么:这个节点定义的 "authority&#​34; 方法,在服务器执行时,通过查询当下 MultiplayerPeer 所关联的 ID,即可以知道当下的 peer ID 与 Player 归属一致,所以方法可以执行。在其它客户端,同一 Player 方法就会因为主机所分配的 peer ID 值与 Player 节点归属不一致,而不会被执行。

而对于标注为 "any_peer&#​34; 的方法,相当于广播方法,假设在某一个客户中执行,会向所有主机通报:现在要执行这个 "any_peer&#​34; 标注过的方法。如果还配合 "call_local&#​34;,那么主动发起调用的这个客户机上也会执行这个方法,而不是单纯向其它主机发出 RPC 调用。

具体参数使用可以参考源代码 GDScriptParser::rpc_annotation() 方法中的定义:

代码块
C++
自动换行
复制代码
    // godot\master\modules\gdscript\gdscript_parser.cpp:3905
    for (int i = last; i >= 0; i--) {
        String mode = p_annotation->resolved_arguments[i].operator String();
        if (mode == "any_peer") {
            rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_ANY_PEER;
        } else if (mode == "authority") {
            rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY;
        } else if (mode == "call_local") {
            rpc_config["call_local"] = true;
        } else if (mode == "call_remote") {
            rpc_config["call_local"] = false;
        } else if (mode == "reliable") {
            rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_RELIABLE;
        } else if (mode == "unreliable") {
            rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE;
        } else if (mode == "unreliable_ordered") {
            rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE_ORDERED;
        } else {
            push_error(R"(Invalid RPC argument. Must be one of: 'call_local'/'call_remote' (local calls), 'any_peer'/'authority' (permission), 'reliable'/'unreliable'/'unreliable_ordered' (transfer mode).)", p_annotation);
        }
    }
复制成功

示范工程只包含两个场景:

  • player.tscn 玩家场景节点,提供玩家的基本角色行为和外观;

  • main.tscn 主场景,提供连接类型选择界面;

主场景使用的 main_rpc.gd 脚本参考如下:

代码块
Python
自动换行
复制代码
extends Node3D


const PORT = 4567
const HOST = "127.0.0.1"

var multiplayer_peer = ENetMultiplayerPeer.new()
var connected_peer_ids = []
var local_player

@onready var message_input = $MessageInput
@onready var menu = $Menu
@onready var host = $Menu/bg/vbox/Host
@onready var join = $Menu/bg/vbox/Join
@onready var network_side = $NetworkInfo/NetworkSide
@onready var peer_node_inf = $NetworkInfo/PeerNodeInfo


func _ready():
    # Start the server/client by pass "--server" or "--client" argument.
    if "--server" in OS.get_cmdline_args():
        # Signals for server
        multiplayer_peer.peer_disconnected.connect(peer_disconnected)
        multiplayer_peer.peer_connected.connect(new_peer_connected)
        start_server()
    elif "--client" in OS.get_cmdline_args():
        # Signals for clietns
        multiplayer.server_disconnected.connect(func(): destroy_peer(get_multiplayer_authority()))
        multiplayer.connected_to_server.connect(func(): print_debug("Connected to server: %s:%d" % [HOST, PORT]))
        multiplayer.connection_failed.connect(connection_failed)
        start_client()
    else:
        menu.visible = true


    host.pressed.connect(start_server)
    join.pressed.connect(start_client)

func start_server():
    message_input.text = "Starting a new server ..."
    var err = multiplayer_peer.create_server(PORT)
    if OK != err:
        print_debug("Can't create an ENet server on %s:%s" % [HOST, PORT])
        return
        
    multiplayer.multiplayer_peer = multiplayer_peer
    menu.visible = false
    network_side.text = "Server"
    message_input.text = ""
    print_debug("Start ENet Server on %s:%d." % [HOST, PORT])
    
    # create server side player
    create_player()


func start_client():
    message_input.text = "Starting a new client ..."
    var err = multiplayer_peer.create_client(HOST, PORT)
    if OK != err:
        print_debug("Can't create an ENet client on %s:%s" % [HOST, PORT])
        return
        
    multiplayer.multiplayer_peer = multiplayer_peer
    menu.visible = false
    network_side.text = "Client"
    message_input.text = ""
    print_debug("Start ENet Client on %s:%d." % [HOST, PORT])


func connection_failed():
    message_input.text = "Connection failed: %s:%d" % [HOST, PORT]
    await get_tree().create_timer(3).timeout
    menu.visible = true


func new_peer_connected(peer_id):
    print_debug("new peer connected: %d." % [peer_id])
    message_input.text = "new peer connected: %d." % [peer_id]
    await get_tree().create_timer(0.5).timeout
    message_input.text = ""
    # create player on all clients side.
    rpc("new_peer_broadcast", peer_id)
    rpc_id(peer_id, "old_peer_replication", connected_peer_ids)
    # create player on server side for client.
    create_player(peer_id)


func create_player(peer_id = 1):
    connected_peer_ids.append(peer_id)
    var player = preload("res://player.tscn").instantiate()
    player.set_multiplayer_authority(peer_id)
    add_child(player)
    rpc("refresh_peer_node_inf")
    if peer_id == multiplayer.get_unique_id():
        local_player = player


func peer_disconnected(peer_id):
    # Destroy player node on other peers
    rpc("destroy_peer_broadcast", peer_id)
    # Destroy player node on current peer node.
    destroy_peer(peer_id)


func destroy_peer(peer_id):
    if peer_id == 1:
        print_debug("Server closed!")
        multiplayer_peer.close()
        multiplayer.multiplayer_peer = null
        #get_tree().set_multiplayer_poll_enabled(false)
        #get_tree().multiplayer_poll = false
        clear_players()
        menu.visible = true
        return
    get_node(str(peer_id)).queue_free()
    connected_peer_ids.remove_at(connected_peer_ids.find(peer_id))
    rpc("refresh_peer_node_inf")

func clear_players():
    for id in connected_peer_ids:
        get_node(str(id)).queue_free()
    connected_peer_ids.clear()

@rpc("any_peer", "call_local")
func refresh_peer_node_inf():
    peer_node_inf.text = "#%d / %d" % [multiplayer.get_unique_id(), len(connected_peer_ids)]

@rpc 
#@rpc("authority", "call_remote", "reliable", 1)
func new_peer_broadcast(new_peer_id):
    create_player(new_peer_id)

@rpc 
#@rpc("authority", "call_remote", "reliable", 1)
func destroy_peer_broadcast(peer_id):
    destroy_peer(peer_id)


@rpc
#@rpc("authority", "call_remote", "reliable", 1)
func old_peer_replication(peer_ids):
    for peer_id in peer_ids:
        create_player(peer_id)

func _on_message_input_text_submitted(new_text):
    local_player.rpc("display_message", new_text)
    message_input.text = ""
    message_input.release_focus()
    message_input.accept_event();
复制成功

玩家场景使用的脚本参考:

代码块
Python
自动换行
复制代码
extends MeshInstance3D

var speed = 10

@onready var message = $Message
@onready var myname = $Name

func _ready():
    name = str(get_multiplayer_authority())
    myname.text = str(name)

func _physics_process(delta):
    if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
        var axis_x = int(Input.is_key_pressed(KEY_A)) - int(Input.is_key_pressed(KEY_D))
        var axis_z = int(Input.is_key_pressed(KEY_W)) - int(Input.is_key_pressed(KEY_S))
        var direction:Vector3 = Vector3(-axis_x, 0, -axis_z)
        
        if direction.length():
            # update my position by myself.
            global_position += direction.normalized() * delta * speed
           # refresh my position of all clients by RPC API.
            rpc("remote_set_position", global_position)

func _unhandled_input(event):
    if not event is InputEventMouseButton:
        return
    match [event.button_index, event.double_click, event.pressed]:
        [MOUSE_BUTTON_LEFT, true, true]:
            set_my_message("I'm myself!")

func set_my_message(msg):
    # This api works just for me
    #if not name != str(get_multiplayer_authority()):
    if not is_multiplayer_authority():
        return
    message.text = msg


@rpc("unreliable")
func remote_set_position(pos):
    print_debug("authority call remote: #%d - %s - " % [get_multiplayer_authority(), pos])
    global_position = pos

@rpc
#@rpc("authority", "call_remote", "reliable", 1)
func display_message(meg):
    message.text = str(message)

@rpc("authority", "call_remote", "reliable", 1)
func shake_me():
    message.text = "Shake me!"

@rpc("any_peer", "call_remote", "reliable", 1)
func shake_all():
    message.text = "Shake all!"

@rpc("any_peer", "call_local", "reliable", 1)
func clicked_by_player():
    message.text = str(multiplayer.get_remote_sender_id()) + " hit me!"


func _on_mouse_click_area_input_event(camera, event, position, normal, shape_idx):
    if not event is InputEventMouseButton:
        return
    match event.button_index:
        MOUSE_BUTTON_LEFT:
            rpc("clicked_by_player")
        MOUSE_BUTTON_RIGHT:
            rpc("shake_me")
        MOUSE_BUTTON_MIDDLE:
            rpc("shake_all")
复制成功