英文原文:
https://mirror-networking.gitbook.io/docs/guides/gameobjects/pickups-drops-and-child-objects
经常出现的问题是,如何处理作为玩家预制件的子对象,所有的客户端都需要知道并同步,比如哪种武器被装备了,拾取联网的场景对象,以及玩家将对象丢入场景。
Mirror不能支持对象层次结构中的多个网络身份组件。由于 Player 对象必须有一个网络标识,它的子层级对象却不能有网络标识。
子对象
让我们从一个简单的情况开始,即单个附着点位于我们的 Player 层次结构中的某个位置,例如手臂末端的一只手。在从 Player Prefab 上的 NetworkBehaviour 继承的脚本中,我们将有一个 GameObject 引用,其中可以在 Inspector 中分配附着点,一个 SyncVar 枚举,其中包含玩家持有的各种选择,以及一个用于 SyncVar 的 Hook根据新值换出持有物品的美术。
在下图中,Kyle 在手腕上添加了一个空的游戏对象 RightHand,以及一些要装备的预制件(Ball、Box、Cylinder),以及一个用于处理它们的 Player Equip 脚本。
注意:item预制件只是美术…它们没有脚本,并且它们不能有网络组件。当然,它们可以有基于单一行为的脚本,可以从player预制件上的 ClientRpc 中引用和调用。
Inspector 显示分配在 2 个位置的 RightHand、Player Equip 脚本以及 Network Transform Child 组件的目标,因此我们可以根据需要调整所有客户端的附着点(不是美术)的相对位置。
下面是玩家装备脚本来处理装备物品的变化,以及一些注意事项:
-
虽然我们可以在设计时附加所有美术item并根据枚举启用/禁用它们,但这并不能很好地扩展到很多项目,如果它们上有脚本来说明它们在游戏中的行为,例如动画、特殊效果等。它可能会很快变得丑陋,所以这个例子在本地实例化和销毁,而不是作为设计选择。
-
该示例不努力处理item和附加点之间的位置偏移,例如使物品的把手或把手与手对齐。最好在具有可在设计器中设置的本地位置和旋转的公共字段的项目的单一行为脚本中处理此问题,并在 Start 中使用一些代码以相对于父附加点在本地坐标中应用这些值。
using UnityEngine;
using System.Collections;
using Mirror;
public enum EquippedItem : byte
{
nothing,
ball,
box,
cylinder
}
public class PlayerEquip : NetworkBehaviour
{
public GameObject sceneObjectPrefab;
public GameObject rightHand;
public GameObject ballPrefab;
public GameObject boxPrefab;
public GameObject cylinderPrefab;
[SyncVar(hook = nameof(OnChangeEquipment))]
public EquippedItem equippedItem;
void OnChangeEquipment(EquippedItem oldEquippedItem, EquippedItem newEquippedItem)
{
StartCoroutine(ChangeEquipment(newEquippedItem));
}
// 由于Destroy被延迟到当前帧的末尾,
// 我们在实例化新对象之前使用一个coroutine来清除任何子对象
IEnumerator ChangeEquipment(EquippedItem newEquippedItem)
{
while (rightHand.transform.childCount > 0)
{
Destroy(rightHand.transform.GetChild(0).gameObject);
yield return null;
}
switch (newEquippedItem)
{
case EquippedItem.ball:
Instantiate(ballPrefab, rightHand.transform);
break;
case EquippedItem.box:
Instantiate(boxPrefab, rightHand.transform);
break;
case EquippedItem.cylinder:
Instantiate(cylinderPrefab, rightHand.transform);
break;
}
}
void Update()
{
if (!isLocalPlayer) return;
if (Input.GetKeyDown(KeyCode.Alpha0) && equippedItem != EquippedItem.nothing)
CmdChangeEquippedItem(EquippedItem.nothing);
if (Input.GetKeyDown(KeyCode.Alpha1) && equippedItem != EquippedItem.ball)
CmdChangeEquippedItem(EquippedItem.ball);
if (Input.GetKeyDown(KeyCode.Alpha2) && equippedItem != EquippedItem.box)
CmdChangeEquippedItem(EquippedItem.box);
if (Input.GetKeyDown(KeyCode.Alpha3) && equippedItem != EquippedItem.cylinder)
CmdChangeEquippedItem(EquippedItem.cylinder);
}
[Command]
void CmdChangeEquippedItem(EquippedItem selectedItem)
{
equippedItem = selectedItem;
}
}
掉落物品
现在我们可以装备这些物品,我们需要一种方法将当前物品作为联网物品放到世界中。请记住,作为子美术,item预制件上根本没有网络组件。
首先,让我们在上面的 Update 方法中再添加一个 Input,以及一个 CmdDropItem 方法:
void Update()
{
if (!isLocalPlayer) return;
if (Input.GetKeyDown(KeyCode.Alpha0) && equippedItem != EquippedItem.nothing)
CmdChangeEquippedItem(EquippedItem.nothing);
if (Input.GetKeyDown(KeyCode.Alpha1) && equippedItem != EquippedItem.ball)
CmdChangeEquippedItem(EquippedItem.ball);
if (Input.GetKeyDown(KeyCode.Alpha2) && equippedItem != EquippedItem.box)
CmdChangeEquippedItem(EquippedItem.box);
if (Input.GetKeyDown(KeyCode.Alpha3) && equippedItem != EquippedItem.cylinder)
CmdChangeEquippedItem(EquippedItem.cylinder);
if (Input.GetKeyDown(KeyCode.X) && equippedItem != EquippedItem.nothing)
CmdDropItem();
}
[Command]
void CmdDropItem()
{
// 在服务器上实例化场景对象
Vector3 pos = rightHand.transform.position;
Quaternion rot = rightHand.transform.rotation;
GameObject newSceneObject = Instantiate(sceneObjectPrefab, pos, rot);
// 仅在服务器上将 RigidBody 设置为非运动学(预制件中的 isKinematic = true)
newSceneObject.GetComponent<Rigidbody>().isKinematic = false;
SceneObject sceneObject = newSceneObject.GetComponent<SceneObject>();
// 在服务器上设置子对象
sceneObject.SetEquippedItem(equippedItem);
// 为客户端设置场景对象上的 SyncVar
sceneObject.equippedItem = equippedItem;
// 将玩家的 SyncVar 设置为空,这样客户端将销毁装备的子项目
equippedItem = EquippedItem.nothing;
// 在网络上生成场景对象以供所有人查看
NetworkServer.Spawn(newSceneObject);
}
在上图中,有一个 sceneObjectPrefab 字段分配给一个预制件,该预制件将充当我们项目预制件的容器。 SceneObject 预制件有一个带有 SyncVar 的 SceneObject 脚本,类似于 Player Equip 脚本,以及一个将共享枚举值作为参数的 SetEquippedItem 方法。
using UnityEngine;
using System.Collections;
using Mirror;
public class SceneObject : NetworkBehaviour
{
[SyncVar(hook = nameof(OnChangeEquipment))]
public EquippedItem equippedItem;
public GameObject ballPrefab;
public GameObject boxPrefab;
public GameObject cylinderPrefab;
void OnChangeEquipment(EquippedItem oldEquippedItem, EquippedItem newEquippedItem)
{
StartCoroutine(ChangeEquipment(newEquippedItem));
}
// 由于Destroy被延迟到当前帧的末尾,
// 我们在实例化新对象之前使用一个coroutine来清除任何子对象
IEnumerator ChangeEquipment(EquippedItem newEquippedItem)
{
while (transform.childCount > 0)
{
Destroy(transform.GetChild(0).gameObject);
yield return null;
}
// 使用新值,而不是 SyncVar 属性值
SetEquippedItem(newEquippedItem);
}
// 从 OnChangeEquipment(上图)在客户端调用 SetEquippedItem,
// 在服务器上来自 PlayerEquip 脚本中的 CmdDropItem。
public void SetEquippedItem(EquippedItem newEquippedItem)
{
switch (newEquippedItem)
{
case EquippedItem.ball:
Instantiate(ballPrefab, transform);
break;
case EquippedItem.box:
Instantiate(boxPrefab, transform);
break;
case EquippedItem.cylinder:
Instantiate(cylinderPrefab, transform);
break;
}
}
}
在下面的运行时图像中,Ball(Clone) 附加到 RightHand 对象,Box(Clone) 附加到 SceneObject(Clone),这在 inspector 中显示。
美术预制件上有简单的碰撞器(球体、盒子、胶囊)。如果您的美术item具有网格碰撞器,则必须将其标记为凸面(Convex)才能与 SceneObject 容器上的 RigidBody 一起使用。
拾取物品
现在我们在场景中丢了一个盒子,我们需要重新捡起它。为此,将 CmdPickupItem 方法添加到 Player Equip 脚本中:
// CmdPickupItem 是公开的,因为它是从 SceneObject 上的脚本调用的
[Command]
public void CmdPickupItem(GameObject sceneObject)
{
// 设置玩家的 SyncVar 以便客户端可以显示装备的物品
equippedItem = sceneObject.GetComponent<SceneObject>().equippedItem;
// 销毁场景对象
NetworkServer.Destroy(sceneObject);
}
这个方法简单地从场景对象脚本中的 OnMouseDown 调用:
void OnMouseDown()
{
NetworkClient.localPlayer.GetComponent<PlayerEquip>().CmdPickupItem(gameObject);
}
由于SceneObject(Clone)是联网的,我们可以直接通过玩家对象上的CmdPickupItem来设置装备物品SyncVar,销毁场景对象。
对于整个示例,除了 Player 之外,唯一需要向 Network Manager 注册的预制件是 SceneObject 预制件。