可能缺少Unity UNET,无论您喜欢它还是讨厌它,都缺少文档和示例。 根据我的经验,我发现有些事情与直觉有些背道而驰。 本文旨在重点介绍其中一些,但我发现它们的文档或说明不充分。 我们将从最基础的开始,然后从那里开始。

制作仅限本地游戏的游戏时,使用Instantiate
函数将GameObjects作为保存的预制件带入世界。 但是,这是仅本地功能。 这意味着,如果客户端实例GameObject
,则不会在任何其他客户端(包括服务器)上创建该游戏对象。 您至少还有更多步骤: Spawn
功能。 Spawn函数告诉运行游戏的服务器在所有客户端和服务器本身上实例化预制件。
在UNET中,每个玩家(不是网络连接)都会获得一个GameObject来代表玩家。 每个玩家都拥有相同的Player GameObject,但是该GameObject的属性可以更改,以使其对玩家来说是唯一的。
但是,这里的重点是,玩家的GameObject
是特殊的,并由UNET Server专门对待。 首先,玩家对象具有hasLocalAuthority
,这意味着它可以用其他游戏对象无法与服务器进行通信。 它负责执行播放器的动作,并将其中继到服务器,服务器将接受它们。 服务器不会接受来自其他GameObjects
更新,因为它们没有本地权限。
当您调用Spawn
函数时,会发生这种情况,但是,诸如位置之类的事情不会在服务器和客户端之间同步。 您可以使用SyncVar
或其他某种机制来确保所有客户端和服务器保持同步。 对于诸如位置之类的简单事物,UNET提供了一个称为Network Transform的组件,该组件可以自动处理此问题。
重要的是要记住,默认情况下,UNET会实现服务器权限协议。 这意味着服务器始终对任何游戏对象的同步值拥有最终决定权。 这可以防止人们作弊,但会产生深远的影响。 例如,如果要更改syncVar
属性的值,则必须有权执行此操作。 服务器可以执行此操作,或者具有本地权限的对象可以执行此操作,但是其他对象则不能。 本地游戏对象不能影响其他玩家游戏对象的位置,因为它们没有本地权限。
此外,游戏对象若要调用具有Command
装饰器的函数,则必须具有本地权限。 这意味着必须在直接附加到本地游戏对象的脚本上调用它。 如果您调用附加到另一个游戏对象的函数,则服务器将拒绝您的请求。
所有对象代码始终同时在所有实例(客户端和服务器)上运行。 服务器可能会告诉客户端要做什么(特别是使用ClientRPC
属性函数),而客户端可能会告诉服务器要做什么(通常使用SyncVars
或Command
属性函数),但是所有脚本代码将始终在所有客户端上运行。
因此,重要的是通过在要只为当前连接的本地播放器执行的代码周围放置条件语句(例如isLocalPlayer
来保护代码段。 例:
一个简单的例子就是运动。 所有输入控件都应包含一个isLocalPlayer
检查,因为我们只希望为当前玩家的游戏对象输入输入。 然后,对于其他播放器的位置,我们希望通过服务器进行同步。 接受输入并操纵其他游戏对象的位置是没有意义的(并且行不通的)。 这样的效果是,当您设置一个新位置并有权这样做时,服务器将尊重该位置,而所有其他客户端将对其进行更新。 但是,当其他客户端更改其位置时,其游戏对象将自动更新,不受您的输入例程的束缚,这将在您无权更改它们的情况下向您的控制台抛出警告/错误。 别忘了: 用于控制玩家的相同代码将同时执行,本地游戏实例上的所有玩家也将使用相同的代码。 isLocalPlayer
检查可以正确识别您想要在玩家游戏对象上发生的事情,而不是其他玩家游戏对象。
全面考虑始终在所有播放器上运行代码是一件困难的事情。 您必须勤于了解由谁执行的操作。 永远不要以为您是机器上唯一执行代码的游戏对象。
专家提示 :有几种功能装饰器也可以帮助解决此问题。 例如, [Server]
装饰器将仅在服务器上执行代码。 以下是快速备忘单列表:
-
[Server]
/[ServerCallback]
*-仅在服务器上执行 -
[Client]
/[ClientCallback]*
-仅在客户端上执行 -
[ClientRPC]
-从服务器上的所有客户端上调用特定的RPC函数(稍后将对此进行详细说明) -
[TargetRPC]
-在特定的网络连接上调用特定的RPC函数。 类似于ClientRPC,但并非所有客户端都将执行RPC代码,只有一个与客户端连接匹配的代码。 -
[Command]
-在从客户端调用的服务器上调用特定功能。 通常是在您的spawn函数被调用的地方。 请注意,只能从玩家游戏对象或具有本地权限的对象调用此对象,而不能从另一个对象调用。
注意 :客户端和服务器回调函数实际上执行相同的操作,但是它们在以下方面有所不同(与文档不同):
“此自定义属性与[Server]自定义属性相同,不同之处在于,如果在客户端上调用,它不会在控制台中生成警告。 这对于避免向控制台发送针对引擎将要调用的功能(例如Update()或物理回调)的垃圾邮件非常有用。”
当专门针对客户端的功能为何在服务器上被调用? 如果您发现这个作者,也想知道。
让我们定义几件事:
- 当前游戏实例-一台计算机上正在运行的游戏实例。 用UNET术语来说,这实际上转换为特定的netId。
- 本地玩家对象—服务器识别为当前玩家的游戏对象的特殊GameObject。 这由NetworkManager管理。
想象一下:在那里,正在努力编写出色的游戏。 您遇到了一个棘手的逻辑,并决定最好仅在服务器上运行该逻辑。 也许它会检查有关当前世界状况的信息,或者将事件通知特定的玩家。 因此,将其包装在if(isServer)
并继续工作。
在测试期间,您使用一台自托管服务器,并注意到应该仅在该服务器上运行的代码正在您的一个客户端上运行! 是什么赋予了? 答案:在自托管服务器上,主机既是服务器又是客户端,这意味着isServer
和 isServer
都可以为true。
好吧,那很臭,似乎没有意义,但是isClient
呢? 您可以快速重新处理代码以检查if (!isClient)
。
“那应该教他们!”你自己想。
您再次启动代码以仍然看到相同的行为。 我勒个去? 这里发生了什么? 答案:您忘记了自己是自己服务器的客户端。 这意味着isClient
isLocalPlayer
和isServer
可以在完全相同的时间全部为true。
没错,仅因为您要托管服务器就意味着您不仅是服务器,而且还是客户端,反之亦然。 更糟糕的是,您也是自己服务器的客户端。 这意味着您所看到的就是服务器的渲染图。 那么这些值实际上定义了什么?
isLocalPlayer
—如果在代表本地玩家的isLocalPlayer
测试, isLocalPlayer
True
。 这绝不会检查您是服务器还是客户端。 此外,对于在您的计算机上渲染的所有对象都是播放器,但不是您的特定播放器实例,则为false
。 请记住:所有对象始终在游戏的所有实例上渲染。 因此,每个人都将自己与isLocalPlayer
为true,而每个其他播放器对象将isLocalPlayer
为false。 此外,从正在运行的专用服务器的角度来看,所有正在运行的游戏对象都将将此值设置为false
因为它们都不是本地的。
isClient
—如果正在执行的游戏实例是服务器的客户端,则为true
。 这并不涉及游戏中的任何特定对象,而是更广泛地谈论当前游戏实例。 对于自托管服务器,当您作为客户端连接到自己时,这仍然适用,这意味着isServer
也将同时适用。 isClient
为true
且isServer
为false
的唯一时间是您未托管服务器,或者所连接的服务器是没有本地连接的客户端的专用服务器。
isServer
—如果检查isServer
属性的游戏实例实际上是托管游戏的服务器(作为自托管或专用服务器),则为true
。 这并不涉及游戏中的任何特定对象,即使玩家的游戏对象对其进行了检查。 尽管使用此功能来检查以防止本地播放器执行某些代码可能很诱人,但由于isServer
和isLocalPlayer
可以同时为true,因此不能被该应用程序信任。 实际上,在自托管服务器的情况下, isServer
, isLocalPlayer
和isClient
都可以同时为true。 执行自托管服务器的播放器将使所有这些值均为true
。 稍后我们将看到几个示例,说明这一点。
让我们看一些示例代码。 我们将此脚本附加到玩家的游戏对象上,并为每个生成的玩家运行一次。 请记住:这将在游戏所有实例上的所有玩家对象上执行。
您可以放心地忽略playerDetails.playerName
及其工作方式。 只是知道在我的设置中,这为我提供了玩家的可显示名称。 您可以使用netId
进行此netId
。
下表是我们正在讨论的UNET HLAPI布尔值的表格。 第一张表是从运行自托管服务器的Player1的角度来看的。 Player2已连接。您所看到的是Player1认为这些布尔值对他本人和Player2都适用。
从Player1的角度来看,Player2的isServer
值设置为true。 这是因为就Player2的游戏对象而言,它实际上是在当前计算机上本地渲染的。 请记住, isServer
与谁是服务器无关。 如果您是服务器,则所有游戏对象都将isServer
设置为true
。
此外,请注意Player1是客户端和服务器。 如前所述,Player1不仅是服务器,还是游戏同一实例中的客户端。
现在,让我们从Player2的角度来看相同的设置。 此示例与托管游戏的Player1和连接的Player2完全相同。唯一的区别是,从Player2的角度来看,我们将看到这些布尔值的外观。
同样,我们可能无法获得期望的结果! 首先,我们没有将Player1视为服务器。 相反, isServer
为false
,并且isClient
设置为true
。 您可能会想“是的,Player1是他自己的服务器的客户端,因此这很有意义”,但您会错了。 isClient
和 isServer
不会告诉特定玩家是服务器还是客户端,而是一个更全局的变量,它确定游戏的特定实例是服务器还是客户端。 对于该实例上的 所有 NetworkBehaviour
对象 都是全局的 。
那么,当我们使用专用服务器时,这会如何变化? 现在,我们对isServer
和isClient
有了一个更全局的了解,这可能更有意义。
上表是从专用服务器的角度得出的。 在这种情况下,它不会将播放器视为客户端,而会将其视为服务器。 这是因为这是考虑isServer
的错误方法。 我们为每个播放器看到了isServer
,因为如上所述, isServer
指的是调试后的应用程序(专用服务器)实际上是服务器。 无论您在哪里测量isClient
或isServer
,它都将引用充当服务器或客户端的游戏实例。
下面的两个表格从连接到专用服务器的两个游戏客户端的角度显示了相同的设置,从而反映了这一点。