Networking With Hand Data

Collaborating in XR is common and seeing each others hands adds to the experience.

This page will describe how you can make use of the utilities of the Ultraleap Unity Plugin to share your tracked hands with others through networking.


Requirements and acknowledgements

Ultraleap do not provide a complete networking solution, we provide the tools to send your hand data over an existing networking solution.

To get started with networking in Unity, consider some of the available solutions on the market and pick the solution that suits you:

Once you have chosen your networking solution, work through their sample projects to ensure you understand the process behind networking and have a project set up with clients able to connect to one another.


Adding Hands

If you have followed the guidance above, as well as our Your First Project Guide. You are ready to send the hand data across the network.

The Ultraleap Unity Plugin comes with a utility class called VectorHand. This utility has multiple functions, one of which is to encode a Hand to and from a VectorHand, another is to convert the basic elements of the VectorHand into a byte[].

Sending hand tracking data over a network can use a lot of bandwidth, so converting it into the smallest data possible will help to reduce lag across your application.

Make sure you understand how to get hand tracking data by following our Scripting Fundamentals.

If you are the owner of the tracking data, you should convert it into a byte[] like so (this uses pseudo code for simplicity, read on to see a full example):

// variables
Hand hand;
VectorHand vectorHand;
byte[] handBytes;

// method
vectorHand.Encode(hand);
vectorHand.FillBytes(handBytes);

Now that you have a byte[] you can send it over the network using your chosen method of networking. When your client receives the data, you will need to convert it back into a LeapHand to use with the Plugin components. To do this, you can use this approach (this uses pseudo code for simplicity, read on to see a full example):

// variables
Hand hand;
HandModelBase handModel
VectorHand vectorHand;
byte[] handBytes;

// method
vectorHand.ReadBytes(handBytes);
vectorHand.Decode(hand);
handModel.SetLeapHand(hand);
handModel.UpdateHand();

You will now have applied the hand data to a HandModelBase. If you wish to apply it to a different hand type, you can use the LeapHand directly after decoding it from the VectorHand.


Complete Example

Here is an example of using this to send hands over a network using Unitys Netcode for Gameobject:

using Leap;
using Leap.Encoding;

using UnityEngine;
using Unity.Netcode;

public class NetworkHands : NetworkBehaviour
{
    [SerializeField]
    private HandModelBase leftModel = null, rightModel = null;

    private LeapProvider leapProvider;

    private VectorHand leftVector = new VectorHand(), rightVector = new VectorHand();
    private Hand leftHand = new Hand(), rightHand = new Hand();

    private byte[] leftBytes = new byte[VectorHand.NUM_BYTES], rightBytes = new byte[VectorHand.NUM_BYTES];

    private bool leftTracked, rightTracked;

    private void Awake()
    {
        // Find the most suitable LeapProvider in the scene automatically
        leapProvider = Hands.Provider;
    }

    public override void OnNetworkSpawn()
    {
        if (IsOwner)
        {
            // We own the hands, so we will be sending the data across the network
            leapProvider.OnUpdateFrame += OnUpdateFrame;
            Destroy(leftModel?.gameObject);
            Destroy(rightModel?.gameObject);
        }
        else
        {
            // We are going to be sent hand data for these hands.
            // We should control the hands directly, not from a LeapProvider
            leftModel.leapProvider = null;
            rightModel.leapProvider = null;
        }
    }

    public override void OnNetworkDespawn()
    {
        if (IsOwner)
        {
            // We no longer need this event as we have disconnected from the network
            leapProvider.OnUpdateFrame -= OnUpdateFrame;
        }
    }

    private void OnUpdateFrame(Frame frame)
    {
        // Find the left hand index and use it if it exists
        int ind = frame.Hands.FindIndex(x => x.IsLeft);
        if(ind != -1)
        {
            // The left hand exists, encode the vector hand for it and fill the byte[] with data
            leftTracked = true;
            leftVector.Encode(frame.Hands[ind]);
            leftVector.FillBytes(leftBytes);
        }
        else
        {
            leftTracked = false;
        }

        ind = frame.Hands.FindIndex(x => !x.IsLeft);
        if(ind != -1)
        {
            // The right hand exists, encode the vector hand for it and fill the byte[] with data
            rightTracked = true;
            rightVector.Encode(frame.Hands[ind]);
            rightVector.FillBytes(rightBytes);
        }
        else
        {
            rightTracked = false;
        }

        // Send any data we have generated to the server to be disributed across the network
        UpdateHandServerRpc(NetworkManager.LocalClientId, leftTracked, rightTracked, leftBytes, rightBytes);
    }

    [ServerRpc]
    private void UpdateHandServerRpc(ulong clientId, bool leftTracked, bool rightTracked, byte[] leftHand, byte[] rightHand)
    {
        if (!IsServer) return;

        // As the server, we should directly Load the data we were given
        LoadHandsData(leftTracked, rightTracked, leftHand, rightHand);

        // Send the data on to all clients for use on their hands
        UpdateHandClientRpc(clientId, leftTracked, rightTracked, leftHand, rightHand);
    }

    [ClientRpc]
    private void UpdateHandClientRpc(ulong clientId, bool leftTracked, bool rightTracked, byte[] leftHand, byte[] rightHand)
    {
        // If we own this object, we do not need to load the hand data, we produced it!
        if (IsOwner) return;

        // Load the other client's hand data into our copy of their hands
        LoadHandsData(leftTracked, rightTracked, leftHand, rightHand);
    }

    private void LoadHandsData(bool leftTracked, bool rightTracked, byte[] leftHand, byte[] rightHand)
    {
        if (leftModel != null)
        {
            leftModel.gameObject.SetActive(leftTracked);

            if (leftTracked)
            {
                // Read the new data into the vector hand and then decode it into a Leap.Hand to be send to the hand model
                leftVector.ReadBytes(leftHand);
                leftVector.Decode(this.leftHand);
                leftModel?.SetLeapHand(this.leftHand);
                leftModel?.UpdateHand();
            }
        }

        if(rightModel != null)
        {
            rightModel.gameObject.SetActive(rightTracked);

            if (rightTracked)
            {
                // Read the new data into the vector hand and then decode it into a Leap.Hand to be send to the hand model
                rightVector.ReadBytes(rightHand);
                rightVector.Decode(this.rightHand);
                rightModel?.SetLeapHand(this.rightHand);
                rightModel?.UpdateHand();
            }
        }
    }
}