Developers

Multiplayer

Add multiplayer avatar support to any Unity networking framework — Photon, Mirror, Netcode for GameObjects, FishNet, and more.

v1.0.0Works with any Unity networking layer

Overview

Ava-Twin avatars work in any Unity multiplayer game. The SDK provides a stateless, thread-safe API for loading avatars — there is no singleton, no global state, and no framework lock-in.

The integration pattern is simple:

  • 1. Each player stores an opaque avatar_id string (e.g. NWtPd1zoxDE).
  • 2. Transmit this string over your networking layer — Photon custom properties, Mirror SyncVars, Netcode NetworkVariables, or any other mechanism.
  • 3. Remote players call SDK.LoadAvatar(avatarId) to load the avatar locally.

No GLB URLs, no internal IDs, no framework-specific adapters — just a single opaque string per player.

Architecture

Here is how the avatar flows through a multiplayer session:

Local player: Customizer → save → receives opaque avatar_id (e.g. NWtPd1zoxDE)
Network publish: Local player publishes avatar_id + skin_tone to the network (Photon custom properties, Mirror SyncVar, Netcode NetworkVariable, etc.)
Remote clients: Receive avatar_id → call SDK.LoadAvatar(avatarId, skinTone)
SDK handles: Token mint → resolve → download → instantiate → materials → humanoid setup
Tip: The avatar_id is a short opaque alphanumeric string (e.g. NWtPd1zoxDE). It never expires. The same avatar combination always returns the same ID. Store it in your player profile database for persistence across sessions.

Quick Start

1. Save the avatar_id after customization

When a player finishes customizing their avatar, the SDK returns an AvatarId and SkinToneHex. Store these in your player profile or publish them to your networking layer.

csharp
var result = await SDK.OpenCustomizerAsync();
if (result != null)
{
    string avatarId = result.AvatarId;    // e.g. "NWtPd1zoxDE"
    string skinTone = result.SkinToneHex; // e.g. "#FFDFC4"

    // Store in your player profile / publish to network
    SaveToPlayerProfile(avatarId, skinTone);
}

2. Publish to your networking layer

Send the avatar_id and skin_tone to other players using your networking framework of choice.

Photon PUN
csharp
var props = new ExitGames.Client.Photon.Hashtable
{
    { "AvatarId", avatarId },
    { "SkinTone", skinTone }
};
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
Mirror
csharp
[SyncVar(hook = nameof(OnAvatarChanged))]
public string avatarId;

[SyncVar]
public string skinTone;

[Command]
void CmdSetAvatar(string id, string tone)
{
    avatarId = id;
    skinTone = tone;
}
Netcode for GameObjects
csharp
public NetworkVariable<FixedString64Bytes> avatarId = new();
public NetworkVariable<FixedString64Bytes> skinTone = new();

[ServerRpc]
void SetAvatarServerRpc(string id, string tone)
{
    avatarId.Value = id;
    skinTone.Value = tone;
}

3. Load remote player avatars

When a remote player's avatar data arrives, load it with SDK.LoadAvatar and parent it under your player hierarchy.

csharp
async Task LoadRemoteAvatar(string avatarId, string skinTone)
{
    var result = await SDK.LoadAvatar(avatarId, skinTone);
    if (result == null) return;

    // Parent the avatar under your player object
    result.Root.transform.SetParent(playerTransform);
    result.Root.transform.localPosition = Vector3.zero;
    result.Root.transform.localRotation = Quaternion.identity;

    // Get the humanoid Avatar for your Animator
    var humanoidAvatar = result.GetUnityHumanoidAvatar();
    if (humanoidAvatar != null)
    {
        animator.avatar = humanoidAvatar;
        animator.Rebind();
    }
}

AvatarResult Reference

The object returned by SDK.LoadAvatar contains everything you need to integrate the avatar into your player hierarchy and animation system.

Properties
result.Root (GameObject)
The instantiated avatar root. Parent it under your player hierarchy.
result.AvatarId (string)
The opaque avatar ID that was loaded.
result.SkinToneHex (string)
The skin tone hex color applied.
result.GetUnityHumanoidAvatar() (Avatar)
Returns the Unity humanoid Avatar for Animator setup. Use this to drive animations via Mecanim.

Handling Avatar Changes

Players can change their avatar mid-session (e.g. re-open the customizer during gameplay). Listen for network property changes and reload the avatar when the avatar_id changes.

Photon example
csharp
public void OnPlayerPropertiesUpdate(
    Photon.Realtime.Player target,
    ExitGames.Client.Photon.Hashtable changed)
{
    if (target == photonView.Owner && changed.ContainsKey("AvatarId"))
    {
        string newId = (string)changed["AvatarId"];
        string tone = target.CustomProperties.ContainsKey("SkinTone")
            ? (string)target.CustomProperties["SkinTone"]
            : null;
        _ = LoadRemoteAvatar(newId, tone);
    }
}
Warning: Always guard against concurrent loads. If a player changes their avatar rapidly, cancel or ignore in-flight loads to prevent race conditions.

Best Practices

Transmit the avatar_id, not the GLB URL
Avatar IDs are short, permanent, and don't expose your storage. GLB URLs are long, expire, and leak internal paths.
Cache is automatic
SDK.LoadAvatar caches GLB data in memory. If two players have the same avatar, the second load skips the network entirely.
Destroy old avatars
Before loading a new avatar for a player, destroy the previous one to prevent memory leaks.
Handle null results
SDK.LoadAvatar returns null on failure (network error, invalid ID). Always check and fall back to a default avatar.
Skin tone is optional
If you pass null for skinTone, the SDK uses the default tone for that avatar's head variant.
One avatar per player
Each call to SDK.LoadAvatar is independent. There's no global state or singleton involved in the loading pipeline.

Platform Notes

WebGL
Works identically. SDK.LoadAvatar is fully async and WebGL-compatible.
Mobile (iOS / Android)
No special handling needed. Same API, same behavior.
Unity Editor
Test multiplayer flows in the Unity Editor with multiple instances or ParrelSync.