在这第22个小时,该是处理网络的时候了。 同步世界各地的玩家以使他们感到自己在同一时间玩同一游戏一直是一项艰巨的任务。 在游戏开发过程的早期就必须考虑具有多人游戏模式功能,以避免长时间且痛苦地重写核心组件。幸运的是,Godot再一次挽救了局面,这要感谢它闪亮的高级网络界面!
TCP,UDP及其重要性
首先,当谈到网络协议时,首先映入脑海的是TCP/IP协议栈,它为整个互联网提供了基本的支持。
这背后的原因是很清楚的:TCP在复杂的互联网上工作得很好(记住,它不是无缘无故地被称为“网络”的!),并让你觉得一切都很简单:
-
它是基于连接的,所以你的服务器不会混合多个客户端发送的请求。
-
它保证了可靠性,所以如果请求不能到达目的地,你一定会得到通知。
-
它保证有序接收。
-
请求的大小几乎可以随您的需要而定,因为TCP会将它们分割成正确大小的包。
这些点都是很厉害的,但是它们是有代价的:延迟。当一个数据包丢失时,TCP会再次询问发送方,当数据包n到达时,如果数据包n-1还在网络中,TPC就无法处理它。这对于互联网上的大多数应用来说,并不是什么大问题(你只要多等一会儿就可以让网页加载了),但是对于游戏这样的实时应用来说,就不一样了!因为游戏中的数据包都是实时的。
UDP是一种更为简单的协议,它将数据包发送到远程计算机:数据包丢失、排序、发送者身份以及对相同数据包的多次接收都与它无关。您可以使用UDP来微调处理网络。这允许您以复杂性为代价获得更快的速度。
这就是Godot的高级网络系统的作用:它为我们处理了UDP的复杂性,并将其整合到了它的Scene系统内部,使多人游戏看起来就像单人游戏一样,你可以设置每个节点与远程玩家同步,或者将其当前状态的同步信息发送给其他玩家。
注意
使用原始UDP / TCP
除了高级网络系统外,Godot还通过StreamPeerTCP、TCP_Server和PacketPeerUDP类提供了经典的UDP和TCP客户端/服务器功能。如果你想编写你的自定义游戏服务器或连接到第三方API,这很有用。
管理连接
理论就到此为止吧! 要连接多个Godot实例,我们需要将其中一个配置为服务器,并将其他实例作为客户端连接到它(清单22.1)。
LISTING 22.1 建立连接
# 对于服务器
func host_game(port):
var host = NetworkedMultiplayerENet.new()
host.create_server(port)
get_tree().set_network_peer(host)
# 对于客户端
func join_game(ip, port):
var host = NetworkedMultiplayerENet.new()
host.create_client(ip, port)
get_tree().set_network_peer(host)
我们创建一个NetworkedMultiplayerENet
实例,将其配置为服务器或客户端,并将其定义为我们的Scene Tree
使用的实例。请注意,Godot网络API是在抽象的NetworkedMultiplayerPeer
类中定义的,NetworkedMultiplayerENet
是使用ENet库实现的。
一旦我们的网络对等体在每个Godot实例上被定义,我们就会通过信号通知我们网络活动:
-
network_peer_connected
: 一个新的Godot实例加入服务器。这个信号在每个对等体上都会被触发(不仅仅是在服务器上),新连接的对等体会多次收到这个信号(每个对等体在到达之前已经连接了一个)。 -
network_peer_disconnected
: 当有人离开时在每个对等体上触发(除了离开的对等体)。 -
connected_to_server
:一旦建立连接,就会在新连接的对等体上触发。 -
connection_failed
: 当与服务器的连接失败时触发。 -
server_disconnected
: 与服务器的连接已经丢失,所以其他对等体得到一个network_peer_disconnected
信号。
NOTE
当心防火墙
在为游戏选择端口时要小心! 虽然这很容易记住,但低数字端口(如HTTP使用的TCP 80)大多数时候只有具有高级权限的用户(如管理员)才能访问。所以,你应该选择一个高于1023的端口,并确保它没有被其他应用程序使用。
最后,当你想关闭连接时,只需从场景树中删除网络对等体即可(清单22.2)。
LISTING 22.2 关闭连接
func finish_game(port):
get_tree().set_network_peer(null)
请注意,这对客户端和服务器端都是一样的。
远程过程调用
让我们的多个Godot实例进行对话吧!为此,我们可以使用所谓的RPC(远程过程调用),它包括从一个对等体触发对一个或多个对等体的过程调用。要注意这里说的是 "过程 "而不是 “函数”,因为过程不会向调用者返回任何值(所以它是一个发送便忘记(fire-and-forget)调用)。Godot提供了以下RPC方法:
-
Node.rpc: 常规的RPC调用;使用它来调用节点的一个过程。
-
Node.rset: 和RPC一样,但用于远程设置节点的属性。
-
Node.rpc_id/rset_id:我们可以使用这些方法,而不是向所有的对等体发送过程,通过提供网络ID,只向某个对等体发送过程。
-
Node.rpc_unreliable/rset_unreliable/rpc_unreliable_id/rset_unreliab 最后,这些是前面方法的不可靠版本。这意味着你的过程可能根本不会被接收到(还记得UDP是如何工作的吗?)
远程和同步关键词
话虽如此,但如果控制不好,RPC可能是一个危险的工具:如果你允许一个完全陌生的人从全球各地调用你的Godot实例的任何函数,会发生什么?还记得文件系统章节吗,当时你可以从Godot访问整个用户的文件系统?为了防止类似的事情发生,必须用关键字显式标记一个函数(或一个属性)允许被RPC调用(清单22.3)。
sync
: 该过程被所有对等体(包括调用者)调用。
remote
: 该过程仅在远程对等体上调用,而不在调用者上调用。
LISTING 22.3 使用使用远程和同步关键字
var speed = Vector2(0, -200)
sync var alive = true
remote func update_pos_for_remotes(new_pos):
position = new_pos
func _process(delta):
if is_network_master():
position += speed * delta
if position.y < 0:
position.y = 0
rset("alive", false)
rpc_unreliable("update_pos_for_remotes", position)
注意网络拥塞。
请记住,使用RPC,即使是不可靠的(例如rpc_unreliable),在每一帧上调用是昂贵的,并且不能很好地扩展到本地网络或几个玩家游戏之外,所以使用固定的计时器来完成这样的任务。
清单22.3说明了如何使用这些关键字:我们有一个节点倒下了,当它落地时,它被杀死了。每一帧,进程会被调用一次,以更新负责它的对等体上的节点的状态(这就是is_network_master()
告诉你的,但后面会更多)。
现在,这个对等体需要把这个节点的新位置告诉其他的对等体。因此,它使用了 update_pos_for_remotes
上的 RPC 调用,它更新了除了自己以外的所有对等体。需要注意的是,我们这里使用的是rpc_unreliable
,因为这个调用是针对每一帧进行的,因此丢失一个帧并不是太大的问题。
最后,当节点掉到地上时,我们需要将alive
属性设置为false
。 这次,即使是负责该节点的对等体也必须更新此属性,因此我们将其标记为sync
,并让rset
调用同时更新每个对等体,包括我们自己。
主从
即使远程和同步很酷,但它们在某些方面还是有不足:漏洞保护。考虑到任何对等体都可以执行RPC 调用remote 或sync过程,这将影响所有其他对等体,因此客户机很容易模拟服务器并欺骗游戏。
在我们前面的例子中,我们可以让一个恶意的客户端使用rset(alive,false)来杀死一个节点,即使它不是负责这节点。
为了解决这个问题,Godot有一个master/slave概念,该概念设置在每个对等体的每个节点上。还记得最后一段中的is_network_master()吗?如果对等体在负责执行代码的当前节点(即master)上,则返回true
默认情况下,服务器被设置为Scene Tree
中所有节点的master。稍后,当客户端开始连接时,他们可以同意某个节点由特定的对等体管理。这意味着你应该确保一个节点只有一个对等体被声明为master,否则,奇怪的事情就会发生。
另外需要注意的是,默认的set_network_master()
会配置一个节点的slave/master属性(清单22.4)以及它在场景树中的所有子节点,但你可以通过设置false作为第二个参数来禁用这个行为。
LISTING 22.4 声明一个节点的对等体Master
var player_scene = preload("res://scenes/player.tscn")
func peer_joined(peer_id)
var player = player_scene.instance()
get_tree().get_root().add_child(player)
player.set_network_master(peer_id)
现在你可以使用is_network_master()
(清单22.5)来分离只应该在master上运行的任务,通常是处理玩家的用户输入。
LISTING 22.5 根据节点的Master属性过滤输入处理
func _update(delta)
if is_network_master():
if Input.is_action_pressed("ui_up"):
# Do something
混合sync / remote和is_network_master()可能很麻烦,因此已经创建了master和slave关键字来简化操作。 从它们的名称可以猜出,标记为master的过程仅在声明为该节点的master的对等体上执行。 另一方面,slave在其他对等点上执行(图22.1)。
FIGURE 22.1
每个对等体的典型实时场景树视图,有master 节点(绿色)和slave 节点(红色)。
这提供了真正强大的同步模式,master从slaves那里得到通知(后者在master procedure上使用RPC),并向他们发送同步命令(在slave procedure上使用RPC),同时确保没有恶意的slave可以冒充自己(在slave上的slave procedure使用RPC只会被自己调用!)。
在非竞争性或合作游戏中,漏洞利用是一个小问题。 根据您的用例,考虑到任何slave如何使用它们,并且不提供防止恶意使用的保护,则有必要避免使用remote和sync关键字。
LISTING 22.6 用Slave代替Sync
slave func update_score(new_score)
score = new_score
func _on_player_killed():
if is_network_master(): # Score should be handled only by the master# 分数只能由master处理
rpc("update_score", score + 100)
update_score(score + 100)
替换sync的一种常见模式是将您的过程声明为slave(清单22.6),然后将其同时调用RPC(以同步所有slave对等体)和直接调用为普通函数(在本地执行函数)。这样,当从master 触发时,就可以在任何地方调用过程,而不会有被恶意的slave使用时被利用的风险。
测试同步
了解网络的一个很好的方法是在一个真正简单的项目上进行尝试。
1.用几个按钮和一个标签创建一个非常基本的GUI。见图22.2。
FIGURE 22.2
一个基本的图形用户界面
2.将清单22.1中提供的函数连接到客户端/服务器按钮。
- 创建一个
_update_text
函数,在Label
上添加一行日志,并使用多个函数调用此_update_text
,同时使用不同的网络属性进行标记。 参见图22.3。
FIGURE 22.3
在内容浏览器中寻找静态网格资产。
4.将ping按钮连接到一个使rpc
、rpc_id
或直接调用我们网络函数的函数上。
5.最后,启动你的项目,用多种配置进行测试。乱七八糟地看看会发生什么,真的很有参考价值(比如有多个服务器或设置多个对等体作为master)。
Visual Script
正如你刚才所看到的,联网在很大程度上是关于脚本的:你必须动态配置谁是 slave和 master(鉴于事先不知道对等体),然后使用RPC作为过程调用脚本函数。到目前为止,我们只展示了使用GDScript的例子,因为它是使用Godot进行脚本的最常见方式,但这并不意味着你不能使用VisualScript进行联网(图22.4)。
FIGURE 22.4
使用VisualScript的网络。
如图22.4所示,VisualScript提供了一个RPC下拉菜单来配置函数上的网络属性。要调用程序,可以使用RPC方法上配置的CallSelf框(这与用GDScript调用Node.rpc完全一样)。
总结
在这一小时中,您了解了网络以及Godot如何通过其高级网络系统简化该领域。 您了解了如何配置和连接客户端和服务器。 然后,您学习了如何使用RPC在网络对等方之间进行同步。 最后,您学习了如何使用关键字sync,remote,master和slave来装饰您的过程,并通过使用按对等和按节点的master / slave属性来使用更强大的同步模式。 网络是一个复杂的庞然大物,有很多问题(通常设计程序来避免受到恶意客户端的利用,或者保持少量数据以进行同步并避免延迟),但是您已经对Godot提供的功能有了一个很好的概述。
Q&A
Q. 有没有办法提前确定对等体的网络ID?
A. 服务器ID总是1,所以它可以安全地硬编码在你的游戏中;然而,客户端在连接时得到唯一的、随机生成的ID。
Q. 如何判断当前节点是master或 slave?
A. 你可以使用Node.is_network_master()方法,该方法返回一个布尔值。
Q. 我可以用Godot为我的游戏建立一个专用服务器吗?
A. 默认情况下,Godot是带有2D / 3D渲染器和音频引擎的完整游戏引擎。 当玩家使用它时,这是完全正常的,但是当您想使其在无GPU的无头服务器上运行,而这只是现代游戏PC的CPU和RAM的一小部分时,这会变得很烦人。
但是,官方网站上提供了Godot的服务器版本(当前支持Linux 64位,这是服务器的绝佳选择)。