Skip to main content
New Participant
January 14, 2022

Meta avatar 2 multiplayer

Hello everyone. 

I'm trying to use the new meta avatars in multiplayer but I struggle with implementation. I tried with Photon Pun 2 and the sample scene "network loopback" but I'm unable to achieve proper networking. 

Has anyone achieved multiplayer with the new avatar sdk or know a documentation or tutorial I can use to make it work?

Thanks a lot.

This topic has been closed for replies.

42 replies

New Participant
January 19, 2022

I have the same problem.
Here is my code

using Oculus.Avatar2;
using Photon.Pun;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using static Oculus.Avatar2.OvrAvatarEntity;

public class MirrorTest : MonoBehaviourPun, IPunObservable
{
    [SerializeField] private OvrAvatarEntity avatarEntity = null;
    private readonly float[] _streamLodSnapshotElapsedTime = new float[OvrAvatarEntity.StreamLODCount];
    private static readonly float[] StreamLodSnapshotIntervalSeconds = new float[OvrAvatarEntity.StreamLODCount] { 1f / 72, 2f / 72, 3f / 72, 4f / 72 };
    
    private const int MAX_PACKETS_PER_FRAME = 3;
    

    private void Awake()
    {
        PhotonNetwork.AddCallbackTarget(this);

        avatarEntity.SetNewActiveView(photonView.IsMine?CAPI.ovrAvatar2EntityViewFlags.FirstPerson
                                                        : CAPI.ovrAvatar2EntityViewFlags.ThirdPerson);
        DataHolder test = FindObjectOfType<DataHolder >();
        avatarEntity.SetIsLocal(photonView.IsMine);

        avatarEntity.SetBodyTracking(photonView.IsMine? test.ovrAvatarManager : null);
        avatarEntity.SetLipSync(photonView.IsMine ? test.lipSyncBehavior : null);
    }

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

    List<byte[]> GetData()
    {
        //if (!_localAvatar.HasJoints) { return; }
        List<byte[]> fullData = new List<byte[]>();
        for (int streamLod = (int)StreamLOD.High; streamLod <= (int)StreamLOD.Low; ++streamLod)
        {
            _streamLodSnapshotElapsedTime[streamLod] += Time.unscaledDeltaTime;

            fullData.Add(avatarEntity.RecordStreamData((StreamLOD)streamLod));
        }

        return fullData;
    }

    private void ApplyData(List<byte[]> data)
    {
        for (int i = 0; i < data.Count; i++)
        {
            avatarEntity.ApplyStreamData(data[i]);
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsReading)
        {
            if (avatarEntity.HasJoints)
            {
                stream.SendNext(GetData());
            }
        }
        else if (stream.IsWriting)
        {
            ApplyData((List<byte[]>) stream.ReceiveNext());
        }
    }
}

I test it locally and it works fine, can someone help me?

Inspiring
February 2, 2022

I got it to work! Here's the key to the problem:

  1. ApplyStreamData() will only work if the avatar has been configured as IsLocal = false and if the Creation Info has been set to preset_remote.
  2. However you've got to apply these configurations BEFORE CreateEntity() is called which happens in Awake() otherwise it ain't gonna work.

My problem was that I had to configure these programatically and not in the editor because I had to instantiate a generic prefab using Photon. As anyone would with Photon. Here's how the relevant parts of my code look now:

protected override void Awake()
    {
        ConfigureAvatarEntity();
        base.Awake();
    }


void ConfigureAvatarEntity()
    {
        m_photonView = GetComponent<PhotonView>();
        if (m_photonView.IsMine)
        {
            SetIsLocal(true);
            _creationInfo.features = Oculus.Avatar2.CAPI.ovrAvatar2EntityFeatures.Preset_Default;
            SampleInputManager sampleInputManager = OvrAvatarManager.Instance.gameObject.GetComponent<SampleInputManager>();
            SetBodyTracking(sampleInputManager);
            gameObject.name = "MyAvatar";
        }
        else
        {
            SetIsLocal(false);
            _creationInfo.features = Oculus.Avatar2.CAPI.ovrAvatar2EntityFeatures.Preset_Remote;
            gameObject.name = "OtherAvatar";
        }
    }
j0schm03
Participant
February 3, 2022

You just saved me! I've spent so many hours scratching my head trying to get this to work even with a local avatar. I finally go it working but I have a bool on the AvatarEntity script right now instead of looking at whether the photonview IsMine bool. 

 

Where did you place your script you referenced? I was going to add this to my AvatarEntity script and add the configure function right before create Entity there. BUT I cannot add using Photon.PUN in this script because it cannot find the namespace due to file structure. I just cannot figure out how to fix this issue.

 

So where did you add this code to ensure the configure gets called before the AvatarEntity Awake? Thanks!!

 

One question, 

Inspiring
January 31, 2022

I've got the same problem, let me know if you find a solution

Participant
April 23, 2022

Hi!
I followed all the above steps. In editor with quest2 it works as it should be, but in quest2 standalone build nothing appears from character. No hands no avatar. Edit: Even the example files are not showing Avatar in builds. I made the test people admin roles.
Please help.

Other question: If I would like to put collisions on the index finger, how would I do that?
Thank you in advance 🙂

Up and Coming
April 24, 2022

Hi Muszti, Did you ever figure out how to add colliders/rigidbodies to the hands of meta avatarts?

Participant
April 24, 2022

Hi cazforshort!
I just instantiate my custom hands prefabs. Those has collisions. But I'm experiencing a significant FPS loss.

Participant
June 29, 2022

Here is the $1,000,000 follow-up question. How do you use the new avatars in Fusion. I feel like there has to be a super optimized built in method I'm not seeing to handle the byte[] recordings / snapshots.

Up and Coming
June 30, 2022

Now I read the warning that PUN and Bolt will be maintained but not improved. Looks like I should study Fusion, but I'm not sure how reliable it is. It seems to provide some examples related to VR.
https://doc.photonengine.com/en-us/fusion/current/samples/game-samples/fusion-dragonhunters-vr

Up and Coming
July 13, 2022

I use Normcore. It seems easier than PUN, but meta avatar usage is difficult no matter how you slice it.

Inspiring
July 11, 2022

Yes, read through the replies.

New Participant
July 11, 2022

Hi Olaysia - thanks for sharing this super useful code. I was trying to reply to Evans_Taylor_Digital as I'm having the same problem - using your code like-for-like but no game objects instantiating inside the remote avatar.  Everything looks like it's happening as it should - ConfigureAvatarEntity() is setting the isLocal flag and the creation info is being properly set to the remote preset before Awake in the base class runs, but nothing is created inside the OtherAvatar game object.  I had partial success using ForceEnableFeatures() to set UseDefaultAnimHierarchy - doing that fixed the spawning of the children inside the OtherAvatar game object and I could see them moving around over the network, but no meshes were visible. I'll continue trying things and add this to the thread if I get it working. It sounds like most people got this working with the code you posted so it's obviously something I'm doing wrong.

 

 

Inspiring
July 11, 2022

Oh, my bad. I thought this was a reply to one of my earlier posts.

It sounds like your other avatar prefab is instantiating but its failing to load its meta avatar. How are you instantiating the other avatar? Are you passing it a user id to load? Because if it doesn't have a user id it won't be able to load its avatar.

New Participant
July 11, 2022

I think you're right. I am using your code:

Int64 userId = Convert.ToInt64(m_userId);
        object[] objects = new object[1] { userId };
        GameObject myAvatar = PhotonNetwork.Instantiate("NetworkAvatar", spawnPos, Quaternion.identity, 0, objects);

 

But I think m_userId is null because I'm not getting the value properly from the other script.  It's really helpful to know that if I'm not passing the value it won't spawn any mesh. Kind of obvious now you pointed it out - thank you!

Inspiring
July 11, 2022

Yes! You're going to need to fetch a user id before you photon instantiate that avatar: Here's how I did it:

public class LogInManager : MonoBehaviourPunCallbacks
{
    const string ROOM_NAME = "Meta Avatar Sdk Test Room";
    const float SPAWN_POS_Z = 1f;
    const string AVATAR_PREFAB_NAME = "GGMetaAvatarEntity";

    [SerializeField] Text m_screenText;
    [SerializeField] Camera m_camera;
    [SerializeField] float m_minSpawnPos_x = -5f;
    [SerializeField] float m_maxSpawnPos_x = 5f;
    [SerializeField] float m_minSpawnPos_z = -5f;
    [SerializeField] float m_maxSpawnPos_z = 5f;
    [SerializeField] ulong m_userId;

    //Singleton implementation
    private static LogInManager m_instance;
    public static LogInManager Instance
    {
        get
        {
            return m_instance;
        }
    }
    private void Awake()
    {
        if (m_instance == null)
        {
            m_instance = this;
        }
        else
        {
            Destroy(this.gameObject);
        }
    }

    void Start()
    {
        StartCoroutine(SetUserIdFromLoggedInUser());
        StartCoroutine(ConnectToPhotonRoomOnceUserIdIsFound());
        StartCoroutine(InstantiateNetworkedAvatarOnceInRoom());
    }

    IEnumerator SetUserIdFromLoggedInUser()
    {
        if (OvrPlatformInit.status == OvrPlatformInitStatus.NotStarted)
        {
            OvrPlatformInit.InitializeOvrPlatform();
        }

        while (OvrPlatformInit.status != OvrPlatformInitStatus.Succeeded)
        {
            if (OvrPlatformInit.status == OvrPlatformInitStatus.Failed)
            {
                Debug.LogError("OVR Platform failed to initialise");
                m_screenText.text = "OVR Platform failed to initialise";
                yield break;
            }
            yield return null;
        }

        Users.GetLoggedInUser().OnComplete(message =>
        {
            if (message.IsError)
            {
                Debug.LogError("Getting Logged in user error " + message.GetError());
            }
            else
            {
                m_userId = message.Data.ID;
            }
        });
    }

    IEnumerator ConnectToPhotonRoomOnceUserIdIsFound()
    {
        while (m_userId == 0)
        {
            Debug.Log("Waiting for User id to be set before connecting to room");  
            yield return null;
        }
        ConnectToPhotonRoom();
    }

    void ConnectToPhotonRoom()
    {
        PhotonNetwork.ConnectUsingSettings();
        m_screenText.text = "Connecting to Server";
    }

    public override void OnConnectedToMaster()
    {
        PhotonNetwork.JoinLobby();
        m_screenText.text = "Connecting to Lobby";
    }

    public override void OnJoinedLobby()
    {
        m_screenText.text = "Creating Room";
        PhotonNetwork.JoinOrCreateRoom(ROOM_NAME, null, null);
    }

    public override void OnJoinedRoom()
    {
        string roomName = PhotonNetwork.CurrentRoom.Name;
        m_screenText.text = "Joined room with name " + roomName;
    }

    IEnumerator InstantiateNetworkedAvatarOnceInRoom()
    {
        while (PhotonNetwork.InRoom == false)
        {
            Debug.Log("Waiting to be in room before intantiating avatar");
            yield return null;
        }
        InstantiateNetworkedAvatar();
    }

    void InstantiateNetworkedAvatar()
    {
        float rand_x = UnityEngine.Random.Range(m_minSpawnPos_x, m_maxSpawnPos_x);
        float rand_z = UnityEngine.Random.Range(m_minSpawnPos_z, m_maxSpawnPos_z);
        Vector3 spawnPos = new Vector3(rand_x, SPAWN_POS_Z, rand_z);
        Int64 userId = Convert.ToInt64(m_userId);
        object[] objects = new object[1] { userId };
        GameObject myAvatar = PhotonNetwork.Instantiate(AVATAR_PREFAB_NAME, spawnPos, Quaternion.identity, 0, objects);
        m_camera.transform.SetParent(myAvatar.transform);
        m_camera.transform.localPosition = Vector3.zero;
        m_camera.transform.localRotation = Quaternion.identity;
    }

}
New Participant
July 11, 2022

Thanks Olaysia - this worked perfectly - remote and local avatars showing up with the correct users. By looking through the code I now understand how this is working too. This whole thread is fantastic and will help a lot of people figure this out!

Participant
December 8, 2022

It's the local variable that holds the result of the message requested.

m_userId = message.Data.ID;
New Participant
August 7, 2023

I am trying to use Meta Avatars to create a multi-player experience, but I am well over my head. Where are the official tutorial videos? If they don't exist, where should I begin in order to understand PUN / Meta Avatar SDK functionality? Those links to 'documentation' just don't do it.