
### 🟠🟢🔵 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,同步创建与服务场景树一致的节点结构:
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 标注的等价关系参考:
| 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" 方法,在服务器执行时,通过查询当下 MultiplayerPeer 所关联的 ID,即可以知道当下的 peer ID 与 Player 归属一致,所以方法可以执行。在其它客户端,同一 Player 方法就会因为主机所分配的 peer ID 值与 Player 节点归属不一致,而不会被执行。
而对于标注为 "any_peer" 的方法,相当于广播方法,假设在某一个客户中执行,会向所有主机通报:现在要执行这个 "any_peer" 标注过的方法。如果还配合 "call_local",那么主动发起调用的这个客户机上也会执行这个方法,而不是单纯向其它主机发出 RPC 调用。
具体参数使用可以参考源代码 GDScriptParser::rpc_annotation() 方法中的定义:
// 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 脚本参考如下:
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(); 玩家场景使用的脚本参考:
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")