高级多人游戏
高层与低层API
初始化网络 . 但是请确保稍后阅读其余部分! Godot始终支持通过UDP, TCP 和一些更高级别的协议(如SSL和HTTP)进行标准的低级网络连接. 这些协议非常灵活, 几乎可以用于任何事情. 然而, 使用它们来手动同步游戏状态可能需要大量的工作. 有时这种工作是无法避免的, 或者是值得的, 例如在后台使用自定义服务器实现时. 但在大多数情况下, 考虑Godot的高级网络API是值得的, 它牺牲了对低级网络的一些细粒度控制, 以获得更大的易用性. 这是由于低级协议的固有限制:
连接, 并针对通常不适合多人游戏等应用程序的目标进行优化. 包被缓冲成更大的批发送, 每包开销更少, 延迟更高. 这对于HTTP之类的东西可能很有用, 但对于游戏通常不太有用. 其中一些可以配置和禁用(例如禁用TCP连接的 “Nagle’s algorithm” ).- UDP 是一个更简单的协议, 它只发送数据包(没有 “连接” 的概念). 没有错误纠正使其非常快(低延迟), 但是包可能在过程中丢失或以错误的顺序接收. 此外,UDP 的 MTU (最大数据包大小)通常很低(只有几百字节), 因此传输更大的数据包意味着对它们进行分割, 重新组织它们, 并在部分失败时重试.
一般来说,TCP 可以被认为是可靠的, 有序的和缓慢的; UDP则是不可靠, 无序, 但是速度快. 由于性能上的巨大差异, 在避免不需要的部分(拥塞/流量控制特性, Nagle算法等)的同时, 重新构建游戏所需的TCP部分(可选的可靠性和包顺序)通常是合理的. 正因为如此, 大多数游戏引擎都带有这样的实现,Godot也不例外.
SceneTree 的高级API, 后者以通常以一种比较优化的方式在后台完成大部分繁重的工作.
注解
Godot支持的大多数平台都提供所有或大部分上述的高, 低网络功能. 但是, 由于网络在很大程度上依赖于硬件和操作系统, 一些特性可能会改变, 或者在某些目标平台上不可用. 最值得注意的是,HTML5平台目前只提供WebSocket支持, 缺乏一些高级功能, 以及对TCP和UDP等低级协议的原始访问.
注解
https://gafferongames.com/post/udp_vs_tcp/
这里 ), 包括全面的 游戏中的网络模型介绍 .
https://github.com/PerduGames/gdnet3
警告
在您的游戏中加入社交网络需要承担一定的责任. 如果做错了, 它会使您的应用程序很容易受到攻击, 并可能导致欺骗或利用. 它甚至可能允许攻击者破坏您的应用程序运行在的机器, 并使用您的服务器发送垃圾邮件, 攻击其他人或窃取您的用户数据, 如果他们玩您的游戏.
当涉及到网络而与Godot无关时, 情况总是如此. 当然, 您可以进行试验, 但是在发布网络应用程序时, 请始终注意任何可能的安全问题.
中间层的抽象
在讨论我们希望如何跨网络同步游戏之前, 了解用于同步的基本网络API是如何工作的可能会有所帮助. NetworkedMultiplayerPeer. 这个对象并不是直接创建的, 但经过设计, 以便多个C++实现可以提供它. PacketPeer, 因此它继承了所有用于序列化, 发送和接收数据的方法. 除此之外, 它还添加了设置节点, 传输模式等的方法. 它同时还包括当节点连接或断开时将通知您的信号. NetworkedMultiplayerEnet), 一个基于WebRTC的实现( WebRTCMultiplayer), 还有一个基于WebSocket的实现( WebSocketMultiplayerPeer),但这可以用来实现移动API(用于特设的WiFi, 蓝牙)或自定义设备/控制台特定的网络API。 大多数常见情况下, 不鼓励直接使用这个对象, 因为Godot提供了更高级别的网络使用. 只有当游戏对较低级别的API有特殊需求的情况下, 才使用它.初始化网络
SceneTree. 为了初始化高级网络, 必须向SceneTree提供一个NetworkedMultiplayerPeer对象. 要创建该对象, 首先必须将其初始化为服务器或客户端. 作为服务器初始化, 监听给定的端口, 指定最大节点的数量:
作为客户端初始化, 连接到给定的IP和端口:var peer = NetworkedMultiplayerENet.new()peer.create_server(SERVER_PORT, MAX_PLAYERS)get_tree().network_peer = peer
获取之前设置的网络客户端:var peer = NetworkedMultiplayerENet.new()peer.create_client(SERVER_IP, SERVER_PORT)get_tree().network_peer = peer
检查树是否被初始化为服务器或客户端:get_tree().get_network_peer()
停止联网功能:get_tree().is_network_server()
(更加合理的做法是, 首先发送消息让其他节点知道您正在离开, 而不是直接让连接关闭或让连接超时, 但这也取决于您的游戏设计.) 警告 当导出到 Android 时,在导出项目或使用一键部署之前,确保在 Android 导出预设中启用 [code]INTERNET[/code] 权限。否则,任何形式的网络通信都会被 Android 系统阻止。get_tree().network_peer = null
管理连接
SceneTree 的相关方法). 为了管理连接的节点,Godot在SceneTree中提供了以下信号: 服务器和客户端: network_peer_connected(int id)network_peer_disconnected(int id)SceneTree.get_network_unique_id() 检索到本地系统的ID. 这些ID主要对大厅管理有用, 一般来说应该被储存起来, 因为它们可以识别连接的同伴, 从而识别玩家. 你也可以使用ID只向某些对等体发送消息. 客户端:connected_to_serverconnection_failedserver_disconnected同样, 所有这些功能主要用于大厅管理或即时添加/删除玩家. 对于这些任务, 服务器显然必须作为一个服务工作, 你必须手动执行任务, 例如向新连接的玩家发送关于其他已经连接的玩家的信息(例如他们的名字, 统计信息等)./root/lobby.RPC
Node 的函数实现的:rpc("function_name", <optional_args>)rpc_id(<peer_id>,"function_name", <optional_args>)rpc_unreliable("function_name", <optional_args>)rpc_unreliable_id(<peer_id>, "function_name", <optional_args>)同步成员变量也是可能的:rset("variable", value)rset_id(<peer_id>, "variable", value)rset_unreliable("variable", value)rset_unreliable_id(<peer_id>, "variable", value)可以用两种方式来调用函数:- 可靠的: 函数调用无论如何都会到达, 但是可能需要更长的时间, 因为在发生故障时它将被重新发送.
- 不可靠: 如果函数调用没有到达, 它将不会被重新传送;但如果它到达了, 将迅速完成.
在大多数情况下, 需要可靠的调用. 当同步对象位置时, 不可靠的调用才很有用(因为同步必须持续发生, 如果包丢失, 这并不那么糟糕, 因为新的包最终会到达;同时包很可能会过时, 因为对象在此期间进一步移动了, 即使它被可靠地怨恨).
SceneTree中还有get_rpc_sender_id函数, 可以用来检查哪个对等体(或对等体ID)发送了RPC.回到大厅
让我们回到大厅. 想象一下, 连接到服务器的每个游戏角色都会将他的到来告诉其他每一个人.# Typical lobby implementation; imagine this being in /root/lobby.extends Node# Connect all functionsfunc _ready(): get_tree().connect("network_peer_connected", self, "_player_connected") get_tree().connect("network_peer_disconnected", self, "_player_disconnected") get_tree().connect("connected_to_server", self, "_connected_ok") get_tree().connect("connection_failed", self, "_connected_fail") get_tree().connect("server_disconnected", self, "_server_disconnected")# Player info, associate ID to datavar player_info = {}# Info we send to other playersvar my_info = { name = "Johnson Magenta", favorite_color = Color8(255, 0, 255) }func _player_connected(id): # Called on both clients and server when a peer connects. Send my info to it. rpc_id(id, "register_player", my_info)func _player_disconnected(id): player_info.erase(id) # Erase player from info.func _connected_ok(): pass # Only called on clients, not server. Will go unused; not useful here.func _server_disconnected(): pass # Server kicked us; show error and abort.func _connected_fail(): pass # Could not even connect to server; abort.remote func register_player(info): # Get the id of the RPC sender. var id = get_tree().get_rpc_sender_id() # Store the info player_info[id] = info # Call function to update lobby UI here
register_player函数上使用了remote关键字:
这个关键字有两个主要用途. 第一是让Godot知道这个函数可以从RPC调用. 如果不加任何关键字,Godot将阻止任何试图调用函数的行为, 以保证安全. 这使安全工作变得更加容易(所以一个客户端不能调用一个函数来删除另一个客户端系统上的文件). 第二个用途是指定如何通过RPC调用该函数. 这里又有四个不同的关键字:remote func register_player(info):
remoteremotesyncmasterpuppetremote关键字意味着rpc()调用将通过网络并远程执行.remotesync关键字意味着rpc()调用将通过网络并远程执行, 但也会在本地执行, 当做一个正常的函数调用.SceneTree上的get_rpc_sender_id函数来检查哪个对等体实际上对register_player进行了RPC调用. 有了这个, 大厅管理就应该或多或少的解释一下. 一旦你开始开发游戏, 很可能想增加一些额外的安全性, 以确保客户不做任何有趣的事情(只是不时地验证他们发送的信息, 或在游戏开始前验证). 为了简单起见, 并且因为每个游戏将分享不同的信息, 所以就不写这方面了.开始游戏
一旦有足够的游戏角色聚集在大厅时, 服务器应该开始游戏. 这本身没有什么特别的, 但是我们将解释一些很好的技巧, 这些技巧可以在这点上让您的生活更容易.游戏角色场景
一个场景 . 对于一个4人游戏, 每个客户端需要4个游戏角色节点实例. 那么, 如何命名这样的节点呢?在Godot中, 节点需要有一个独特的名字. 对于玩家来说, 也必须比较容易分辨出哪个节点代表每个玩家的ID. 实例化后的游戏角色场景的根节点命名为它的网络ID . 这样, 它们在每一个客户端中都是一样的,RPC调用也会很容易!下面是一个示例:
注解remote func pre_configure_game(): var selfPeerID = get_tree().get_network_unique_id() # Load world var world = load(which_level).instance() get_node("/root").add_child(world) # Load my player var my_player = preload("res://player.tscn").instance() my_player.set_name(str(selfPeerID)) my_player.set_network_master(selfPeerID) # Will be explained later get_node("/root/world/players").add_child(my_player) # Load other players for p in player_info: var player = preload("res://player.tscn").instance() player.set_name(str(p)) player.set_network_master(p) # Will be explained later get_node("/root/world/players").add_child(player) # Tell server (remember, server is always ID=1) that this peer is done pre-configuring. # The server can call get_tree().get_rpc_sender_id() to find out who said they were done. rpc_id(1, "done_preconfiguring")
add_child()的任何调用更改为通过call_deferred()进行延迟, 因为SceneTree在创建场景时被锁定(例如, 当_ready()被调用).同步游戏开始
由于延迟, 不同的硬件或其他原因, 设置游戏角色在每个客户端上花费的时间可能不同. 为了确保游戏会在每个人都准备好的时候真正开始, 有必要暂停游戏直到所有的游戏角色都准备好:
当服务器从所有客户端获得OK时, 它才告诉他们开始游戏, 例如:remote func pre_configure_game(): get_tree().set_pause(true) # Pre-pause # The rest is the same as in the code in the previous section (look above)
var players_done = []remote func done_preconfiguring(): var who = get_tree().get_rpc_sender_id() # Here are some checks you can do, for example assert(get_tree().is_network_server()) assert(who in player_info) # Exists assert(not who in players_done) # Was not added yet players_done.append(who) if players_done.size() == player_info.size(): rpc("post_configure_game")remote func post_configure_game(): # Only the server is allowed to tell a client to unpause if 1 == get_tree().get_rpc_sender_id(): get_tree().set_pause(false) # Game starts now!
同步游戏
在大多数游戏中, 多人联网的目标是让游戏在所有对等玩家身上同步运行. 除了提供RPC和远程成员变量集的实现,Godot还增加了网络主机的概念.网络主人
一个节点的网络主人是对该节点具有终极权限的客户端. 当没有明确设置时, 网络主控就会从父节点继承, 如果不改变的话, 父节点总是会成为服务器(ID 1). 因此, 服务器默认拥有对所有节点的权限. Node.set_network_master(id, recursive) 来设置网络主人(默认情况下recursive(递归)为true, 这意味着在节点的所有子节点上也递归地设置了网络主人). Node.is_network_master() 来检查客户端上的特定节点实例是否是该节点用于所有连接的客户端的网络主人. 这在服务器上执行时将返回true, 在所有客户端上将返回false. 如果你有留意前面的例子, 你有可能注意到了每个客户端(peer)都被设置为拥有自己玩家(节点)的网络主权限, 而不是服务器:
每当在客户端上执行这段代码时, 客户端就使得它控制的节点上成为主人, 同时其他所有节点仍然保持为傀儡(服务器是它们的网络主人). 轰炸机演示 :[...]# Load my playervar my_player = preload("res://player.tscn").instance()my_player.set_name(str(selfPeerID))my_player.set_network_master(selfPeerID) # The player belongs to this peer; it has the authority.get_node("/root/world/players").add_child(my_player)# Load other playersfor p in player_info: var player = preload("res://player.tscn").instance() player.set_name(str(p)) player.set_network_master(p) # Each other connected peer has authority over their own player. get_node("/root/world/players").add_child(player)[...]
主人和傀儡关键词
master/puppet关键字(或C#和Visual Script中的相应关键字)一起使用时. 与remote关键字类似, 函数也可以用它们来标记: 炸弹代码的示例:
游戏角色代码的示例:for p in bodies_in_area: if p.has_method("exploded"): p.rpc("exploded", bomb_owner)
puppet func stun(): stunned = truemaster func exploded(by_who): if stunned: return # Already stunned rpc("stun") # Stun this player instance for myself as well; could instead have used # the remotesync keyword above (in place of puppet) to achieve this. stun()
exploded的方法. 回顾一下, 每个对等体都有一套完整的玩家节点实例, 每个对等体(包括自己和主机)都有一个实例. 每个对等体都将自己设定为与自己对应的实例的主控, 并将其他每个实例的主控设定为不同的对等体.exploded方法的调用, 主机上的炸弹已经远程调用了该区域内所有拥有该方法的机体. 不过, 这个方法是在玩家节点中的, 而且有一个master关键词.exploded方法上的master关键字对于如何进行这种调用有两个意思. 第一, 从调用对等体(主机)的角度来看, 调用对等体只会尝试远程调用它设定为相关玩家节点网络主控的对等体上的方法. 其次, 从宿主发送调用的对等体的角度来看, 只有当对等体将自己设置为被调用方法的玩家节点的网络主控(该节点有master关键字)时, 它才会接受调用. 只要所有的对等体都同意谁是怎样的主控, 工作状态将最好.exploded方法告诉所有其他对等体, 它的玩家节点被眩晕了. 对等体通过远程调用该玩家节点的所有实例, 在其他对等体上的stun方法来实现. 因为stun方法有puppet关键字, 所以只有没有将自己设置为节点的网络主控的对等体才会调用它, 换句话说, 这些对等体由于不是该节点的网络主控而被设置为该节点的傀儡.stun的结果是让玩家在屏幕上看起来眩晕了所有对等体, 包括当前的网络主控(由于在rpc("stun")后本地调用stun). 炸弹的主人(主机)对区域内的每一个物体重复上述步骤, 这样, 炸弹区域内任何玩家的所有实例都会在所有对等体的屏幕上被眩晕.rpc_id(<id>, "exploded", bomb_owner)只向特定的玩家发送stun()消息. 这对于像炸弹这样的区域效果来说可能没有什么意义, 但在其他情况下可能会有意义, 比如单目标伤害.rpc_id(TARGET_PEER_ID, "stun") # Only stun the target peer
为专用服务器导出
为专用服务器导出 获取更多信息. 注解 这个页面上的代码样本并不是为了在专用服务器上运行而设计的. 必须修改它们, 使服务器不被认为是一个玩家, 还必须修改游戏启动机制, 使第一个加入的玩家可以启动游戏. 注解stun)的也正是玩家的网络主控。
