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
- Create a new scene (File > New Scene)
- Save the scene as "MultiplayerDemo"
- Add a QWR XR Origin prefab to your scene (from
Packages/QWR Core/Prefabs/QWR_XROrigin) - Add a ground plane (3D Object > Plane) and position it at (0, 0, 0)
Step 2: Set Up Network Manager
- In the Hierarchy, select the QWR XR Origin GameObject
- Verify that it has a QWRNetworkManager component
- If not, add one (Add Component > QWR > Multiplayer > QWRNetworkManager)
- Configure the network settings:
- Max Players: 8
- Room Timeout: 300
- Auto Reconnect: Enabled
Step 3: Create a Connection UI
-
Create a UI Canvas in your scene
-
Add a QWRUICanvas component to it
-
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
-
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}");
}
}
- Add the NetworkUIManager script to the Canvas
- 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:
-
Create a cube (3D Object > Cube) and position it at (0, 1, 1)
-
Add a Rigidbody component
-
Add a QWRInteractable component with standard settings
-
Add a QWRNetworkObject component
-
Configure the QWRNetworkObject:
- Sync Position: Enabled
- Sync Rotation: Enabled
- Sync Scale: Enabled
- Sync Physics: Enabled
-
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;
}
}
}
-
Add the NetworkedInteractable script to the cube
-
Create two materials: one for normal state and one for grabbed state
-
Assign the materials to the NetworkedInteractable component
-
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:
- In the Hierarchy, select the QWR XR Origin GameObject
- Verify that it has a QWRNetworkAvatar component
- If not, add one (Add Component > QWR > Multiplayer > QWRNetworkAvatar)
- Configure the avatar settings:
- Sync Hand Animations: Enabled
- Sync Controller Inputs: Enabled
- Update Rate: 30
Step 6: Set Up Voice Chat
-
In the Hierarchy, select the QWR XR Origin GameObject
-
Verify that it has a QWRVoiceChat component
-
If not, add one (Add Component > QWR > Multiplayer > QWRVoiceChat)
-
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
-
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;
}
}
}
- Add a Toggle and Slider to your UI for microphone and volume control
- Add the VoiceChatManager script to the Canvas
- Assign the UI elements to the corresponding fields in the Inspector
Step 7: Create a Player List
- 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);
}
}
}
- Create a UI Panel to contain the player list
- Create a player entry prefab with a Text component
- Add the PlayerListManager script to the Canvas
- Assign the container and prefab to the corresponding fields in the Inspector
Step 8: Test Your Scene
- Save your scene
- Click the Play button to enter Play Mode
- 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:
- Build the application for your target platform
- Run the build on one device
- Run in Play Mode in the Unity Editor on another device
- Join the same room on both devices
- 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