Oculus Avatar

이 튜토리얼은 PUN에서 Oculus Avatar SDK의 사용법을 보여드립니다. 따라서 신규 유니티 프로젝트를 시작하고 다음의 패키지들을 임포트해주시기 바랍니다:

시작하기

임포트가 완료되었으면 기존의 컴포넌트 확장부터 시작할 수 있습니다. 첫 번째 단계는 'LocalAvatar' 와 'RemoteAvatar' 두 개의 프리팹이 있는 'Assets/OvrAvatar/Content/Prefabs' 로 가는 것 입니다: 다음 단계는 두 개의 프리팹을 사용하거나 복사본을 생성하는 것 입니다.

중요: 두 프리팹은 모두 'Resources' 폴더에 위치시켜야 합니다.
이 경우에 있어서는 각 프리팹을 'Assets/Resources' 에 위치해 놓습니다.

Back To Top

Avatar 동기화

다음 단계는 여러 개의 클라이언트 간에 동기화를 처리하는 PhotonView (우리는 나중에 붙일 것 입니다) 컴포넌트에 의해 관찰될 스크립트를 구현하는 것 입니다. 따라서 새로운 스크립트를 생성하여 PhotonAvatarView 라고 이름을 지어주고 코드에 다음 세 개의 레퍼런스를 추가 합니다:

    private PhotonView photonView;
    private OvrAvatar ovrAvatar;
    private OvrAvatarRemoteDriver remoteDriver;

또한 아바타의 데이터를 실제로 다른 클라이언트에 보내기 전에 데이터를 저장하기 위한 byte-array의 리스트가 필요합니다.

    private List<byte[]> packetData;

유니티의 Start 함수를 사용하여 이전의 모든 레퍼런스와 객체들을 설정할 수 있습니다.

    public void Start()
    {
        photonView = GetComponent<PhotonView>();

        if (photonView.IsMine)
        {
            ovrAvatar = GetComponent<OvrAvatar>();
            ovrAvatar.RecordPackets = true;
            ovrAvatar.PacketRecorded += OnLocalAvatarPacketRecorded;

            packetData = new List<byte[]>();
        }
        else
        {
            remoteDriver = GetComponent<OvrAvatarRemoteDriver>();
        }
    }

PhotonView 컴포넌트에 대한 레퍼런스를 얻은 후에 isMine 조건을 직접 사용하여 'Local'과 'Remote Avatar'사이를 명확하게 구분할 수 있습니다. 인스턴스화 된 객체가 우리의 객체라면 우리는 OvrAvatar 컴포넌트(다음 단계에서 이것을 사용)에 대한 레퍼런스를 얻고, 네트워크를 통해 이 데이터를 전송하기 전에 모든 아바타 관련 입력 이벤트를 저장하는 byte-array 리스트를 인스턴스화합니다. 객체가 다른 클라이언트에 속하면 OvrAvatarRemoteDriver 컴포넌트에 대한 레퍼런스를 얻어 이 컴포넌트를 나중에 다른 클라이언트가 제스처를 볼 수 있도록 입력을 모방하는 데 사용됩니다. 다음에는 Unity의 OnDisable 메소드를 통해 제스처의 기록 시작과 중지하는데 사용합니다.

    public void OnDisable()
    {
        if (photonView.IsMine)
        {
            ovrAvatar.RecordPackets = false;
            ovrAvatar.PacketRecorded -= OnLocalAvatarPacketRecorded;
        }
    }

또한 새로운 패킷이 기록 될 때 발생되는 이벤트 핸들러를 설정합니다. 다음 단계에서 이 패킷은 PUN에 의해 지원되는 byte-array로 직렬화됩니다. 그런 다음, 전에 생성된 리스트에 추가되어 네트워크를 통해 전송할 준비가 됩니다. 아래 확인:

    private int localSequence;

    public void OnLocalAvatarPacketRecorded(object sender, OvrAvatar.PacketEventArgs args)
    {
        if (!PhotonNetwork.InRoom || (PhotonNetwork.CurrentRoom.PlayerCount < 2))
        {
            return;
        }

        using (MemoryStream outputStream = new MemoryStream())
        {
            BinaryWriter writer = new BinaryWriter(outputStream);

            var size = Oculus.Avatar.CAPI.ovrAvatarPacket_GetSize(args.Packet.ovrNativePacket);
            byte[] data = new byte[size];
            Oculus.Avatar.CAPI.ovrAvatarPacket_Write(args.Packet.ovrNativePacket, size, data);

            writer.Write(localSequence++);
            writer.Write(size);
            writer.Write(data);

            packetData.Add(outputStream.ToArray());
        }
    }

이제 시리얼라이저가 있으므로 수신된 패킷을 비직렬화 할 디시리얼라이저가 필요합니다. 따라서 다음 작업은 이 기능을 구현하는 것 입니다. 예제에서는 DeserializeAndQueuePacketData 입니다:

    private void DeserializeAndQueuePacketData(byte[] data)
    {
        using (MemoryStream inputStream = new MemoryStream(data))
        {
            BinaryReader reader = new BinaryReader(inputStream);
            int remoteSequence = reader.ReadInt32();

            int size = reader.ReadInt32();
            byte[] sdkData = reader.ReadBytes(size);

            System.IntPtr packet = Oculus.Avatar.CAPI.ovrAvatarPacket_Read((System.UInt32)data.Length, sdkData);
            remoteDriver.QueuePacket(remoteSequence, new OvrAvatarPacket { ovrNativePacket = packet });
        }
    }

이 함수는 들어오는 byte-array를 비직렬화하고 제스처를 리플레이 하기 위해 OvrAvatarRemoteDriver 컴포넌트에 대기중인 패킷 데이터를 재 생성합니다. 이 부분의 마지막 코딩은 기록된 패킷 교환을 추가하는 것입니다. 이 경우 우리는 OnPhotonSerializeView 를 사용합니다. 왜냐하면 정기적으로 자동 호출되기 때문에 정기적인 업데이트와 자연스럽게 보이게 됩니다.

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.isWriting)
        {
            if (packetData.Count == 0)
            {
                return;
            }

            stream.SendNext(packetData.Count);

            foreach (byte[] b in packetData)
            {
                stream.SendNext(b);
            }

            packetData.Clear();
        }

        if (stream.isReading)
        {
            int num = (int)stream.ReceiveNext();

            for (int counter = 0; counter < num; ++counter)
            {
                byte[] data = (byte[])stream.ReceiveNext();

                DeserializeAndQueuePacketData(data);
            }
        }
    }

좀 더 이해를 돕기 위해서 두 부분으로 분리하였습니다. 첫 번째 파트는 게임 오브젝트의 소유자에 의해 실행 될 isWriting 조건입니다. 먼저 데이터를 보내야 하는지 확인합니다. 데이터를 보내야하는 경우 첫 번째로 보낸 값은 패킷 개수입니다. 이 정보는 수신측에서 중요한 것 입니다. 결국 우리는 더 이상 필요하지 않기 때문에 기록되고 직렬화 된 모든 패킷을 보내고 목록에 저장된 이전 패킷 데이터를 지웁니다. 마지막에는 기록되고 직렬화된 모든 패킷을 전송하고, 더 이상 필요가 없으므로 리스트에 저장된 이전 패킷 데이터를 삭제합니다.

그러나 isReading 조건은 객체를 소유하지 않은 원격 클라이언트에서만 실행됩니다. 먼저, 처리해야 하는 패킷 개수를 확인한 다음 이전에 구현된 함수를 호출하여 모든 패킷 데이터를 단계적으로 비직렬화 및 대기열에 넣습니다.

마지막 단계는 두 개의 프리팹에 PhotonView 컴포넌트와 구현 된 PhotonAvatarView 를 붙이는 것 입니다. PhotonView의 관찰되는 컴포넌트에 PhotonAvatarView 컴포넌트를 추가하는 것을 잊지 마세요.

Back To Top

Avatars 인스턴스화

네트워크 아바타를 인스턴스화 하기 위해서는, 불행하지만 PhotonNetwork.Instantiate 호출만으로는 되지 않습니다. 이유는 두 개의 다른 Avatar를 인스턴스화 해야 하기 때문입니다. 인스턴스를 생성하는 플레이어의 경우 'LocalAvatar' 이며 이외 다른 플레이어의 경우 'RemoteAvatar' 입니다. 따라서 수동 인스턴스화를 사용할 필요가 있습니다. 예제에서는 첫 번째로 Avatar용 'ViewId' 를 할당하고 다른 클라이언트들에게 알림을 주기 위한 캐싱을 사용하도록 하게하는 RaiseEventOptionsSendOptions 를 정의합니다. RaiseEventOptions을 사용하여, 커스텀 인스턴스화 이벤트가 룸의 캐시(나중에 참여하는 클라이언트들이 이 이벤트를 수신)내에 저장되도록 합니다. 그리고 이 이벤트를 로컬 클라이언트도 받도록 합니다. SendOptions 으로 커스텀 이벤트가 신뢰할 수 있는 것으로 전송되는 것을 확인합니다. 이후 룸에 있는 모든 클라이언트들에게 커스텀 인스턴스화 이벤트에게 RaiseEvent 함수를 사용합니다.

    public readonly byte InstantiateVrAvatarEventCode = 123;

    public void OnJoinedRoom()
    {
        int viewId = PhotonNetwork.AllocateViewID();

        RaiseEventOptions raiseEventOptions = new RaiseEventOptions
        {
            CachingOption = EventCaching.AddToRoomCache,
            Receivers = ReceiverGroup.All
        };

        SendOptions sendOptions = new SendOptions
        {
            Reliability = true
        };

        PhotonNetwork.RaiseEvent(InstantiateVrAvatarEventCode, viewId, raiseEventOptions, sendOptions);
    }

커스텀 이벤트를 수신하고 처리하기 위해 두 가지를 사용할 수 있습니다. 이 예제에서는 이들 중 하나를 시연하고 IOnEventCallback 인터페이스의 구현을 사용하여 계속하고 있습니다. 다른 옵션이 무엇인지 확인하려면 설명서에서 RPC 및 RaiseEvent 페이지를 볼 수 있습니다. IOnEventCallback 인터페이스를 구현한 튜토리얼은 다음과 같은 코드와 유사합니다.

public class MyClass : MonoBehaviour, IOnEventCallback
{
    public void OnEvent(EventData photonEvent) { }
}

이제 올바른 프리팹을 인스턴스화하는 OnEvent 콜백 핸들러에 몇 가지 로직을 추가해야 합니다. 따라서 보낸 사람의 ID와 로컬 클라이언트의 ID를 비교합니다. 같은 경우 이 클라이언트가 이벤트를 제기한 것으로 알고 있으며, 더 나아가 'LocalAvatar' 프리팹을 인스턴스화해야 합니다. ID가 같지 않으면 클라이언트는 'RemoteAvatar' 프리팹을 인스턴스화해야 합니다.

    public void OnEvent(EventData photonEvent)
    {
        if (photonEvent.Code == InstantiateVrAvatarEventCode)
        {
            GameObject go = null;

            if (PhotonNetwork.LocalPlayer.ActorNumber == photonEvent.Sender)
            {
                go = Instantiate(Resources.Load("LocalAvatar")) as GameObject;
            }
            else
            {
                go = Instantiate(Resources.Load("RemoteAvatar")) as GameObject;
            }

            if (go != null)
            {
                PhotonView pView = go.GetComponent<PhotonView>();

                if (pView != null)
                {
                    int viewId = (int)photonEvent.CustomData;

                    pView.ViewID = viewId;
                }
            }
        }
    }

이렇게 하면 클라이언트가 룸에 이미 참여했거나 나중에 참여하더라도 연결된 각 클라이언트에서 올바른 Avatar가 인스턴스화됩니다. 이제 우리는 우리의 커스텀 이벤트가 조금이라도 처리될 수 있도록 해야 합니다. 그러기 위해서는 기존에 구현된 OnEvent 콜백을 등록해야 합니다. 이 작업은 Unity의 OnEnable 및 OnDisable(이후 정리하고자 함) 기능을 통해 수행할 수 있습니다.

    public void OnEnable()
    {
        PhotonNetwork.AddCallbackTarget(this);
    }

    public void OnDisable()
    {
        PhotonNetwork.RemoveCallbackTarget(this);
    }

마지막으로, 플레이어가 게임을 종료할 때 각 클라이언트에서 아바타를 파괴해야 합니다. 또한 룸 캐시에서 저장된 이벤트를 제거해야 합니다.

Back To Top

테스팅

아바타의 동기화 테스트를 위한 두 가지 테스트 방법이 있습니다.

첫 번째 컴퓨터에는 각각 Oculus 장치(리프트 및 터치 컨트롤러)가 연결된 두 개 이상의 별도의 컴퓨터가 필요합니다. 유니티 에디터에서 게임을 시작하거나 빌드를 먼저 만든 다음 나중에 두 컴퓨터에서 모두 실행하여 제스처 동기화가 정상인지 확인할 수 있습니다.

또 다른 접근 방식은 두 번째 테스트 어플리케이션을 구축하는 것입니다. 이 프로젝트는 다소 'blank' 프로젝트일 수 있습니다(이 페이지 시작 부분에 언급된 플러그인은 여전히 필요함). 이 프로젝트에서는 RemoteAvatar가 인스턴스화되는 시점을 확인하기 위해 메인 카메라를 배치하고 회전하기만 하면 됩니다. 우리의 경우 카메라는 (0/0/0)을 봐야 합니다. Photon에 연결할 때 동일한 AppId 및 AppVersion을 사용하고 Instantiation 이벤트를 처리하기 위해 OnEvent 콜백을 구현 및 등록해야 합니다. 이 접근법의 장점은 Oculus 장치(리프트 및 터치 컨트롤러)가 하나 있는 컴퓨터 하나만 있으면 된다는 것입니다.

기술문서 TOP으로 돌아가기