Mirror Guide GameObjects - Child

4 minute read



Pickups, Drops, and Child Objects

Mirror에서는 오브젝트의 계층 내에서 복수의 Network Identity 컴포넌트를 지원할 수 없다. 따라서 플레이어 오브젝트는 무조건 Network Identity를 가져야하므로 그 자식들은 Network Identity를 가질 수 없다.

그렇다면 어떤 무기가 장착되어 있는지 또는 네트워크에 연결된 씬 오브젝트를 집어들거나 떨어뜨리는 등 모든 클라이언트가 알아야 하고 동기화해야할 필요가 있는 오브젝트는 어떤식으로 처리해야하는지 살펴본다.


Child Objects

우선 간단하게 플레이어의 팔 끝에 있는 손과 같이 플레이어 계층 아래 쪽에 있는 단일 연결 지점부터 살펴본다.

플레이어 프리펩의 NetworkBehaviour에서 상속된 스크립트에는 인스펙터 창에서 할당할이 가능한 게임 오브젝트 참조, 플레이어가 보유하고 있는 것을 다양하게 선택할 수 있는 SyncVar와 새로운 값에 따라 보유 중인 아이템의 기술을 아트를 변경할 수 있는 Hook이 있다.

주의 아이템 프리팹은 아트일 뿐이다. 스크립트도 없고 네트워킹 컴포넌트도 없어야한다. 물론 플레이어 프리팹의 ClientRpc에서 참조와 호출이 가능한 MonoBehaviour 기반의 스크립트는 가능하다.

예)

아래의 이미지에서 카일은 빈 게임 오브젝트인 RightHand를 손목에 추가하고 몇 개의 프리팹(Ball, Box, Cylinder)을 장착하고 이를 처리하기 위한 플레이어 장비 스크립트를 가지고 있다.

kyle


인스펙터는 Player Equipment 스크립트, Network Transform Child 컴포넌트 이 두 곳의 Target에 할당된 RightHand를 표시하므로 필요에 따라 모든 클라이언트에 대해 접속 포인트의 상대적인 위치를 조정할 수 있다.

다음은 장착된 아이템의 변경을 처리하기 위한 플레이어 장비 스크립트와 몇 가지 고려해야할 사항들이다.

  • 모든 아트 아이템을 디자인 타임에 첨부해 열거형을 근거로 활성화/ 비활성화할 수 있지만 이 방법은 수 많은 아이템에 대해서 잘 확장되지 않는다. 또한 애니메이션이나 특수 효과 등 게임에서의 동작에 관한 스크립트가 포함되어 있는 경우는 매우 빨리 볼 수 있기 때문에 이 예에서는 로컬로 인스턴스화와 파괴할 수 있다.

  • 항목과 부착점 사이의 위치 간격 띄우기를 다루지 않는다. 이 문제는 설계자가 설정할 수 있는 로컬 위치와 회전에 대한 public 필드 및 부모 부착점에 상대적인 로컬 좌표에 이러한 값을 적용하기 위한 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));
    }

    // Since Destroy is delayed to the end of the current frame, we use a coroutine
    // to clear out any child objects before instantiating the new one
    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;
    }
}


Dropping Items

이제 장착한 아이템을 네트워크 아이템으로 월드에 드롭할 수 있는 방법이 필요하다.

예)

먼저 위의 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()
    {
        // Instantiate the scene object on the server
        Vector3 pos = rightHand.transform.position;
        Quaternion rot = rightHand.transform.rotation;
        GameObject newSceneObject = Instantiate(sceneObjectPrefab, pos, rot);

        // set the RigidBody as non-kinematic on the server only (isKinematic = true in prefab)
        newSceneObject.GetComponent<Rigidbody>().isKinematic = false;

        SceneObject sceneObject = newSceneObject.GetComponent<SceneObject>();

        // set the child object on the server
        sceneObject.SetEquippedItem(equippedItem);

        // set the SyncVar on the scene object for clients
        sceneObject.equippedItem = equippedItem;

        // set the player's SyncVar to nothing so clients will destroy the equipped child item
        equippedItem = EquippedItem.nothing;

        // Spawn the scene object on the network for all to see
        NetworkServer.Spawn(newSceneObject);
    }   

위에서 본 이미지는 아이템 프리팹의 컨테이너로 기능하는 프리팹에 할당되어 있는 SceneObjectPrefab 필드가 있다. SceneObject 프리팹에는 Player Equip 스크립트와 같은 SyncVar, SetEmpled가 있는 SceneObject 스크립트가 있다. 공유 열거 값을 매개 변수로 사용하는 항목이다.

<br

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));
    }

    // Since Destroy is delayed to the end of the current frame, we use a coroutine
    // to clear out any child objects before instantiating the new one
    IEnumerator ChangeEquipment(EquippedItem newEquippedItem)
    {
        while (transform.childCount > 0)
        {
            Destroy(transform.GetChild(0).gameObject);
            yield return null;
        }

        // Use the new value, not the SyncVar property value
        SetEquippedItem(newEquippedItem);
    }

    // SetEquippedItem is called on the client from OnChangeEquipment (above),
    // and on the server from CmdDropItem in the PlayerEquip script.
    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은 RightHand 오브젝트에 연결되고 Box는 SceneObject에 연결되며 이는 인스펙터에 표시된다.

runtime


Pickup Items

이제 떨어진 상자를 줍는다. 이를 위해서 CmdPickupItem 메서드가 플레이어 장비 스크립트에 추가된다.

// CmdPickupItem is public because it's called from a script on the SceneObject
[Command]
public void CmdPickupItem(GameObject sceneObject)
{
    // set the player's SyncVar so clients can show the equipped item
    equippedItem = sceneObject.GetComponent<SceneObject>().equippedItem;

    // Destroy the scene object
    NetworkServer.Destroy(sceneObject);
}

이 메서드는 장면 오브젝트의 스크립트로 OnMouseDown에서 호출된다.

void OnMouseDown()
    {
        NetworkClient.localPlayer.GetComponent<PlayerEquip>().CmdPickupItem(gameObject);
    }

SceneObject는 네트워크에 연결되어 있으므로 플레이어 개체의 CmdPickItem으로 직접 전달하여 장착된 항목 SyncVar를 설정하고 장면 오브젝트를 파괴할 수 있다.

이 전체 예에서 플레이어 외에 Network Manager에 등록해야 하는 유일한 프리팹은 SceneObject프리팹이다.