Skip to main content

Multiplayer Setup Tutorial

This tutorial will guide you through setting up a multiplayer XR experience using the QWR SDK. You'll learn how to create networked interactions, synchronize avatars, and implement voice communication.

Prerequisites

Before starting this tutorial, make sure you have:

  • Completed the Quick Start Tutorial
  • Basic understanding of networking concepts
  • QWR SDK properly installed in your Unity project

Step 1: Create a New Scene

  1. Create a new scene (File > New Scene)
  2. Save the scene as "MultiplayerDemo"
  3. Add a QWR XR Origin prefab to your scene (from Packages/QWR Core/Prefabs/QWR_XROrigin)
  4. Add a ground plane (3D Object > Plane) and position it at (0, 0, 0)

Step 2: Set Up Network Manager

  1. In the Hierarchy, select the QWR XR Origin GameObject
  2. Verify that it has a QWRNetworkManager component
  3. If not, add one (Add Component > QWR > Multiplayer > QWRNetworkManager)
  4. Configure the network settings:
    • Max Players: 8
    • Room Timeout: 300
    • Auto Reconnect: Enabled

Step 3: Create a Connection UI

  1. Create a UI Canvas in your scene

  2. Add a QWRUICanvas component to it

  3. Create a Panel with the following elements:

    • Input Field for Room Code
    • Button labeled "Create Room"
    • Button labeled "Join Room"
    • Text element for status messages
  4. Create a new C# script named "NetworkUIManager" with the following code:

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using QWR.Core.Multiplayer;
using QWR.Utilities.Logging;

public class NetworkUIManager : MonoBehaviour
{
[SerializeField] private TMP_InputField roomCodeInput;
[SerializeField] private Button createRoomButton;
[SerializeField] private Button joinRoomButton;
[SerializeField] private TextMeshProUGUI statusText;
[SerializeField] private GameObject connectionPanel;
[SerializeField] private GameObject gamePanel;

private QWRNetworkManager networkManager;

private void Start()
{
// Get network manager
networkManager = QWRNetworkManager.Instance;

if (networkManager == null)
{
QWRLogger.LogError("QWRNetworkManager not found!");
return;
}

// Set up UI events
if (createRoomButton != null)
createRoomButton.onClick.AddListener(CreateRoom);

if (joinRoomButton != null)
joinRoomButton.onClick.AddListener(JoinRoom);

// Subscribe to network events
networkManager.OnConnected += HandleConnected;
networkManager.OnDisconnected += HandleDisconnected;
networkManager.OnPlayerJoined += HandlePlayerJoined;
networkManager.OnPlayerLeft += HandlePlayerLeft;

// Initialize UI
if (connectionPanel != null)
connectionPanel.SetActive(true);

if (gamePanel != null)
gamePanel.SetActive(false);

if (statusText != null)
statusText.text = "Enter a room code to join, or create a new room.";
}

private void OnDestroy()
{
// Unsubscribe from events
if (networkManager != null)
{
networkManager.OnConnected -= HandleConnected;
networkManager.OnDisconnected -= HandleDisconnected;
networkManager.OnPlayerJoined -= HandlePlayerJoined;
networkManager.OnPlayerLeft -= HandlePlayerLeft;
}
}

public async void CreateRoom()
{
if (networkManager == null || statusText == null)
return;

// Update status
statusText.text = "Creating room...";

// Authenticate if needed
if (!networkManager.IsAuthenticated)
{
bool authSuccess = await networkManager.AuthenticateAsync();
if (!authSuccess)
{
statusText.text = "Authentication failed!";
return;
}
}

// Create a room
string roomName = "QWRRoom";
if (roomCodeInput != null && !string.IsNullOrEmpty(roomCodeInput.text))
roomName = roomCodeInput.text;

bool success = await networkManager.CreateRoomAsync(roomName, 8, false);

if (success)
{
statusText.text = $"Room created! Code: {networkManager.CurrentRoomCode}";
}
else
{
statusText.text = "Failed to create room!";
}
}

public async void JoinRoom()
{
if (networkManager == null || statusText == null || roomCodeInput == null)
return;

// Check room code
if (string.IsNullOrEmpty(roomCodeInput.text))
{
statusText.text = "Please enter a room code!";
return;
}

// Update status
statusText.text = "Joining room...";

// Authenticate if needed
if (!networkManager.IsAuthenticated)
{
bool authSuccess = await networkManager.AuthenticateAsync();
if (!authSuccess)
{
statusText.text = "Authentication failed!";
return;
}
}

// Join room
bool success = await networkManager.JoinRoomAsync(roomCodeInput.text);

if (success)
{
statusText.text = $"Joined room: {networkManager.CurrentRoomCode}";
}
else
{
statusText.text = "Failed to join room!";
}
}

private void HandleConnected()
{
// Update UI
if (connectionPanel != null)
connectionPanel.SetActive(false);

if (gamePanel != null)
gamePanel.SetActive(true);

QWRLogger.Log("Connected to room!");
}

private void HandleDisconnected()
{
// Update UI
if (connectionPanel != null)
connectionPanel.SetActive(true);

if (gamePanel != null)
gamePanel.SetActive(false);

if (statusText != null)
statusText.text = "Disconnected from room.";

QWRLogger.Log("Disconnected from room!");
}

private void HandlePlayerJoined(QWRNetworkPlayer player)
{
QWRLogger.Log($"Player joined: {player.PlayerName}");
}

private void HandlePlayerLeft(QWRNetworkPlayer player)
{
QWRLogger.Log($"Player left: {player.PlayerName}");
}
}
  1. Add the NetworkUIManager script to the Canvas
  2. Assign the UI elements to the corresponding fields in the Inspector

Step 4: Create Networked Objects

Let's create some objects that can be synchronized across the network:

  1. Create a cube (3D Object > Cube) and position it at (0, 1, 1)

  2. Add a Rigidbody component

  3. Add a QWRInteractable component with standard settings

  4. Add a QWRNetworkObject component

  5. Configure the QWRNetworkObject:

    • Sync Position: Enabled
    • Sync Rotation: Enabled
    • Sync Scale: Enabled
    • Sync Physics: Enabled
  6. Create a new C# script named "NetworkedInteractable" with the following code:

using UnityEngine;
using QWR.Core.Interaction;
using QWR.Core.Multiplayer;

public class NetworkedInteractable : MonoBehaviour
{
[SerializeField] private Material normalMaterial;
[SerializeField] private Material grabbedMaterial;

private QWRInteractable interactable;
private QWRNetworkObject networkObject;
private Renderer objectRenderer;

private void Awake()
{
interactable = GetComponent<QWRInteractable>();
networkObject = GetComponent<QWRNetworkObject>();
objectRenderer = GetComponent<Renderer>();

if (interactable == null || networkObject == null)
{
Debug.LogError("Required components missing!");
return;
}

// Subscribe to grab events
interactable.OnGrabbed += HandleGrabbed;
interactable.OnReleased += HandleReleased;
}

private void OnDestroy()
{
// Unsubscribe from events
if (interactable != null)
{
interactable.OnGrabbed -= HandleGrabbed;
interactable.OnReleased -= HandleReleased;
}
}

private void HandleGrabbed(QWRInteractor interactor)
{
// Request ownership when grabbed
if (!networkObject.IsOwner)
{
networkObject.RequestOwnership();
}

// Change material
if (objectRenderer != null && grabbedMaterial != null)
{
objectRenderer.material = grabbedMaterial;
}
}

private void HandleReleased(QWRInteractor interactor)
{
// Restore original material
if (objectRenderer != null && normalMaterial != null)
{
objectRenderer.material = normalMaterial;
}
}
}
  1. Add the NetworkedInteractable script to the cube

  2. Create two materials: one for normal state and one for grabbed state

  3. Assign the materials to the NetworkedInteractable component

  4. Duplicate the cube several times to create multiple networked objects

Step 5: Set Up Avatar Synchronization

The QWR XR Origin prefab already includes avatar synchronization components, but let's verify the setup:

  1. In the Hierarchy, select the QWR XR Origin GameObject
  2. Verify that it has a QWRNetworkAvatar component
  3. If not, add one (Add Component > QWR > Multiplayer > QWRNetworkAvatar)
  4. Configure the avatar settings:
    • Sync Hand Animations: Enabled
    • Sync Controller Inputs: Enabled
    • Update Rate: 30

Step 6: Set Up Voice Chat

  1. In the Hierarchy, select the QWR XR Origin GameObject

  2. Verify that it has a QWRVoiceChat component

  3. If not, add one (Add Component > QWR > Multiplayer > QWRVoiceChat)

  4. Configure the voice chat settings:

    • Enabled: True
    • Microphone Volume: 0.8
    • Speaker Volume: 1.0
    • Spatial Audio: Enabled
    • Voice Activation: Enabled
    • Activation Threshold: 0.02
  5. Create a new C# script named "VoiceChatManager" with the following code:

using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Multiplayer;

public class VoiceChatManager : MonoBehaviour
{
[SerializeField] private Toggle microphoneToggle;
[SerializeField] private Slider volumeSlider;

private QWRVoiceChat voiceChat;

private void Start()
{
// Get voice chat component
voiceChat = QWRNetworkManager.Instance?.VoiceChat;

if (voiceChat == null)
{
Debug.LogError("QWRVoiceChat not found!");
return;
}

// Set up UI events
if (microphoneToggle != null)
{
microphoneToggle.isOn = !voiceChat.IsMicrophoneMuted;
microphoneToggle.onValueChanged.AddListener(OnMicrophoneToggleChanged);
}

if (volumeSlider != null)
{
volumeSlider.value = voiceChat.SpeakerVolume;
volumeSlider.onValueChanged.AddListener(OnVolumeSliderChanged);
}
}

private void OnMicrophoneToggleChanged(bool isOn)
{
if (voiceChat != null)
{
voiceChat.MuteMicrophone(!isOn);
}
}

private void OnVolumeSliderChanged(float value)
{
if (voiceChat != null)
{
voiceChat.SpeakerVolume = value;
}
}
}
  1. Add a Toggle and Slider to your UI for microphone and volume control
  2. Add the VoiceChatManager script to the Canvas
  3. Assign the UI elements to the corresponding fields in the Inspector

Step 7: Create a Player List

  1. Create a new C# script named "PlayerListManager" with the following code:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Multiplayer;

public class PlayerListManager : MonoBehaviour
{
[SerializeField] private Transform playerListContainer;
[SerializeField] private GameObject playerEntryPrefab;

private QWRNetworkManager networkManager;
private Dictionary<string, GameObject> playerEntries = new Dictionary<string, GameObject>();

private void Start()
{
// Get network manager
networkManager = QWRNetworkManager.Instance;

if (networkManager == null)
{
Debug.LogError("QWRNetworkManager not found!");
return;
}

// Subscribe to player events
networkManager.OnPlayerJoined += HandlePlayerJoined;
networkManager.OnPlayerLeft += HandlePlayerLeft;

// Initialize player list
if (networkManager.IsConnected)
{
foreach (var player in networkManager.GetAllPlayers())
{
AddPlayerToList(player);
}
}
}

private void OnDestroy()
{
// Unsubscribe from events
if (networkManager != null)
{
networkManager.OnPlayerJoined -= HandlePlayerJoined;
networkManager.OnPlayerLeft -= HandlePlayerLeft;
}
}

private void HandlePlayerJoined(QWRNetworkPlayer player)
{
AddPlayerToList(player);
}

private void HandlePlayerLeft(QWRNetworkPlayer player)
{
RemovePlayerFromList(player);
}

private void AddPlayerToList(QWRNetworkPlayer player)
{
if (playerListContainer == null || playerEntryPrefab == null || player == null)
return;

// Check if player is already in list
if (playerEntries.ContainsKey(player.PlayerId))
return;

// Create player entry
GameObject entry = Instantiate(playerEntryPrefab, playerListContainer);
Text nameText = entry.GetComponentInChildren<Text>();

if (nameText != null)
{
nameText.text = player.PlayerName;
if (player.IsLocal)
nameText.text += " (You)";
if (player.IsHost)
nameText.text += " (Host)";
}

// Store entry
playerEntries[player.PlayerId] = entry;
}

private void RemovePlayerFromList(QWRNetworkPlayer player)
{
if (player == null)
return;

// Check if player is in list
if (playerEntries.TryGetValue(player.PlayerId, out GameObject entry))
{
// Remove entry
Destroy(entry);
playerEntries.Remove(player.PlayerId);
}
}
}
  1. Create a UI Panel to contain the player list
  2. Create a player entry prefab with a Text component
  3. Add the PlayerListManager script to the Canvas
  4. Assign the container and prefab to the corresponding fields in the Inspector

Step 8: Test Your Scene

  1. Save your scene
  2. Click the Play button to enter Play Mode
  3. Test the multiplayer functionality:
    • Create a room or join an existing one
    • Grab and manipulate the networked objects
    • Observe how the objects synchronize across the network

To test with multiple clients:

  1. Build the application for your target platform
  2. Run the build on one device
  3. Run in Play Mode in the Unity Editor on another device
  4. Join the same room on both devices
  5. Test the multiplayer interactions

Next Steps

Now that you've set up a basic multiplayer XR experience, you can:

  • Add more complex networked interactions
  • Implement custom network messages for game mechanics
  • Create a more sophisticated player avatar system
  • Add networked physics interactions
  • Implement a lobby system for matchmaking

Troubleshooting

Connection Issues

  • Ensure both devices are on the same network or have internet access
  • Check for firewall or NAT issues
  • Verify authentication settings
  • Ensure the room code is correct

Synchronization Problems

  • Check that objects have the QWRNetworkObject component
  • Verify ownership settings
  • Adjust synchronization update rates
  • Check for network latency issues

Voice Chat Issues

  • Ensure microphone permissions are granted
  • Check if the microphone is properly detected
  • Adjust microphone sensitivity settings
  • Verify that the voice chat service is properly initialized