Multiplayer
Add multiplayer avatar support to any Unity networking framework — Photon, Mirror, Netcode for GameObjects, FishNet, and more.
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_idstring (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:
avatar_id (e.g. NWtPd1zoxDE)avatar_id + skin_tone to the network (Photon custom properties, Mirror SyncVar, Netcode NetworkVariable, etc.)avatar_id → call SDK.LoadAvatar(avatarId, skinTone)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.
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.
var props = new ExitGames.Client.Photon.Hashtable
{
{ "AvatarId", avatarId },
{ "SkinTone", skinTone }
};
PhotonNetwork.LocalPlayer.SetCustomProperties(props);[SyncVar(hook = nameof(OnAvatarChanged))]
public string avatarId;
[SyncVar]
public string skinTone;
[Command]
void CmdSetAvatar(string id, string tone)
{
avatarId = id;
skinTone = tone;
}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.
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.
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.
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);
}
}