Networking Integration
VoiceReact provides generic networking hooks that work with any Unity networking library.
Architecture
VoiceReact does not implement networking. Instead, it provides:
IVoiceNetworkAdapterinterface for networking integrationVoiceNetworkDatastruct for voice event serialization- Hooks to send/receive voice events
You’re responsible for:
- Implementing the adapter for your networking library
- Serializing/deserializing VoiceNetworkData
- Transmitting data over network
- Authority and ownership logic
Supported Networking Libraries
VoiceReact works with:
- Netcode for GameObjects (Unity’s official solution)
- Mirror
- Photon (PUN2, Fusion)
- Fish-Net
- Any custom networking solution
Quick Start
1. Enable Networking
using Peek.VoiceReact;
[SerializeField] private VoiceReactController playerVoice;
void Start()
{
playerVoice.enableNetworking = true;
}2. Create Network Adapter
Implement IVoiceNetworkAdapter for your networking library:
using UnityEngine;
using Peek.VoiceReact.Network;
public class MyNetworkAdapter : MonoBehaviour, IVoiceNetworkAdapter
{
public void SendVoiceEvent(VoiceNetworkData data)
{
// Serialize and send using your networking library
MyNetworkManager.Instance.SendToServer(data);
}
public void OnRemoteVoiceEvent(VoiceNetworkData data)
{
// Handle voice event from remote player
// (usually not needed - VoiceReact handles this internally)
}
}3. Assign Adapter
void Start()
{
var adapter = GetComponent<MyNetworkAdapter>();
playerVoice.SetNetworkAdapter(adapter);
}That’s it! VoiceReactController will now send voice events through your adapter.
VoiceNetworkData Structure
Voice events are sent as VoiceNetworkData:
public struct VoiceNetworkData
{
public bool isPlayerSpeaking; // Is player currently speaking?
public float decibelLevel; // Current dB level
public Vector3 playerPosition; // Player's 3D position
public string transcription; // STT result (if enabled)
public float timestamp; // Event timestamp
}This struct is small and efficient for network transmission.
Netcode for GameObjects Example
Complete example using Unity’s official Netcode:
using Unity.Netcode;
using UnityEngine;
using Peek.VoiceReact;
using Peek.VoiceReact.Network;
public class NetcodeVoiceAdapter : NetworkBehaviour, IVoiceNetworkAdapter
{
private VoiceReactController voiceController;
void Start()
{
voiceController = GetComponent<VoiceReactController>();
if (IsOwner)
{
// Only local player sends voice events
voiceController.SetNetworkAdapter(this);
}
}
public void SendVoiceEvent(VoiceNetworkData data)
{
if (!IsOwner) return;
// Send to server using ServerRpc
SendVoiceEventServerRpc(
data.isPlayerSpeaking,
data.decibelLevel,
data.playerPosition,
data.timestamp
);
}
[ServerRpc]
void SendVoiceEventServerRpc(
bool isSpeaking,
float db,
Vector3 position,
float timestamp)
{
// Broadcast to all clients except sender
SendVoiceEventClientRpc(isSpeaking, db, position, timestamp);
}
[ClientRpc]
void SendVoiceEventClientRpc(
bool isSpeaking,
float db,
Vector3 position,
float timestamp)
{
if (IsOwner) return; // Don't process own events
// Notify local VoiceListeners about remote player voice
NotifyLocalListeners(isSpeaking, db, position);
}
void NotifyLocalListeners(bool isSpeaking, float db, Vector3 position)
{
if (!isSpeaking) return;
// Calculate normalized volume for ProcessRemotePlayerVoice
float normalizedVolume = Mathf.InverseLerp(-60f, 0f, db);
// Find all local VoiceListeners
var listeners = FindObjectsByType<VoiceListener>(FindObjectsSortMode.None);
foreach (var listener in listeners)
{
float distance = Vector3.Distance(listener.transform.position, position);
listener.ProcessRemotePlayerVoice(normalizedVolume, distance);
}
}
public void OnRemoteVoiceEvent(VoiceNetworkData data)
{
// Not needed for this implementation
}
}Mirror Example
Integration with Mirror networking:
using Mirror;
using UnityEngine;
using Peek.VoiceReact;
using Peek.VoiceReact.Network;
public class MirrorVoiceAdapter : NetworkBehaviour, IVoiceNetworkAdapter
{
private VoiceReactController voiceController;
void Start()
{
voiceController = GetComponent<VoiceReactController>();
if (isLocalPlayer)
{
voiceController.SetNetworkAdapter(this);
}
}
public void SendVoiceEvent(VoiceNetworkData data)
{
if (!isLocalPlayer) return;
CmdSendVoiceEvent(
data.isPlayerSpeaking,
data.decibelLevel,
data.playerPosition,
data.timestamp
);
}
[Command]
void CmdSendVoiceEvent(bool isSpeaking, float db, Vector3 position, float timestamp)
{
RpcBroadcastVoiceEvent(isSpeaking, db, position, timestamp);
}
[ClientRpc]
void RpcBroadcastVoiceEvent(bool isSpeaking, float db, Vector3 position, float timestamp)
{
if (isLocalPlayer) return;
// Process remote voice event
ProcessRemoteVoice(isSpeaking, db, position);
}
void ProcessRemoteVoice(bool isSpeaking, float db, Vector3 position)
{
if (!isSpeaking) return;
float normalizedVolume = Mathf.InverseLerp(-60f, 0f, db);
var listeners = FindObjectsByType<VoiceListener>(FindObjectsSortMode.None);
foreach (var listener in listeners)
{
float distance = Vector3.Distance(listener.transform.position, position);
listener.ProcessRemotePlayerVoice(normalizedVolume, distance);
}
}
public void OnRemoteVoiceEvent(VoiceNetworkData data)
{
// Not needed for this implementation
}
}Photon PUN2 Example
Integration with Photon PUN2:
using Photon.Pun;
using UnityEngine;
using Peek.VoiceReact;
using Peek.VoiceReact.Network;
public class PhotonVoiceAdapter : MonoBehaviourPun, IVoiceNetworkAdapter
{
private VoiceReactController voiceController;
void Start()
{
voiceController = GetComponent<VoiceReactController>();
if (photonView.IsMine)
{
voiceController.SetNetworkAdapter(this);
}
}
public void SendVoiceEvent(VoiceNetworkData data)
{
if (!photonView.IsMine) return;
// Send via RPC
photonView.RPC(
nameof(RPC_VoiceEvent),
RpcTarget.Others,
data.isPlayerSpeaking,
data.decibelLevel,
data.playerPosition,
data.timestamp
);
}
[PunRPC]
void RPC_VoiceEvent(bool isSpeaking, float db, Vector3 position, float timestamp)
{
// Process remote player voice
if (!isSpeaking) return;
float normalizedVolume = Mathf.InverseLerp(-60f, 0f, db);
var listeners = FindObjectsByType<VoiceListener>(FindObjectsSortMode.None);
foreach (var listener in listeners)
{
float distance = Vector3.Distance(listener.transform.position, position);
listener.ProcessRemotePlayerVoice(normalizedVolume, distance);
}
}
public void OnRemoteVoiceEvent(VoiceNetworkData data)
{
// Not needed for this implementation
}
}Remote Voice Processing
VoiceListener provides ProcessRemotePlayerVoice() for network voice events:
public class NetworkVoiceReceiver : MonoBehaviour
{
void OnReceivedNetworkVoiceData(float volume, Vector3 remotePlayerPosition)
{
// Find all local listeners
var listeners = FindObjectsByType<VoiceListener>(FindObjectsSortMode.None);
foreach (var listener in listeners)
{
// Calculate distance to remote player
float distance = Vector3.Distance(
listener.transform.position,
remotePlayerPosition
);
// Let listener process remote voice
listener.ProcessRemotePlayerVoice(volume, distance);
}
}
}This triggers the same OnPlayerHeard/OnPlayerLost events as local voice detection.
Speech-to-Text Networking
Include STT transcriptions in network data:
public class STTNetworkAdapter : NetworkBehaviour, IVoiceNetworkAdapter
{
private VoiceReactController voiceController;
void Start()
{
voiceController = GetComponent<VoiceReactController>();
if (IsOwner)
{
voiceController.SetNetworkAdapter(this);
// Subscribe to transcription events
voiceController.OnTranscriptionReady.AddListener(OnTranscription);
}
}
void OnTranscription(string text)
{
// Send transcription to other players
SendTranscriptionServerRpc(text);
}
[ServerRpc]
void SendTranscriptionServerRpc(string text)
{
// Broadcast transcription
ReceiveTranscriptionClientRpc(text);
}
[ClientRpc]
void ReceiveTranscriptionClientRpc(string text)
{
if (IsOwner) return;
Debug.Log($"Remote player said: {text}");
DisplaySubtitle(text);
}
public void SendVoiceEvent(VoiceNetworkData data)
{
// Standard voice event handling
// Transcription is sent separately via OnTranscription
}
public void OnRemoteVoiceEvent(VoiceNetworkData data)
{
// Not needed
}
}Optimization Strategies
Event Throttling
Limit network traffic by throttling voice events:
public class ThrottledNetworkAdapter : IVoiceNetworkAdapter
{
private float lastSendTime = 0f;
private float sendInterval = 0.1f; // 10 Hz
public void SendVoiceEvent(VoiceNetworkData data)
{
// Only send every 100ms
if (Time.time - lastSendTime < sendInterval)
return;
lastSendTime = Time.time;
// Send to network
SendToNetwork(data);
}
}Only Send on State Change
Only send when speaking state changes:
private bool lastSpeakingState = false;
public void SendVoiceEvent(VoiceNetworkData data)
{
// Only send on speaking state change
if (data.isPlayerSpeaking != lastSpeakingState)
{
lastSpeakingState = data.isPlayerSpeaking;
SendToNetwork(data);
}
}Spatial Relevance
Only send to nearby players:
[ServerRpc]
void SendVoiceEventServerRpc(bool isSpeaking, float db, Vector3 position)
{
// Find players in range
var nearbyPlayers = GetPlayersInRange(position, maxHearingRange);
foreach (var player in nearbyPlayers)
{
// Send only to relevant clients
SendToClient(player.clientId, isSpeaking, db, position);
}
}Latency Compensation
Account for network latency:
[ClientRpc]
void ReceiveVoiceEventClientRpc(bool isSpeaking, float db, Vector3 position, float timestamp)
{
// Calculate latency
float latency = Time.time - timestamp;
if (latency > 0.5f)
{
// Discard old events
return;
}
// Process voice event
ProcessRemoteVoice(isSpeaking, db, position);
}Authority Validation
Ensure only the owner sends voice events:
public void SendVoiceEvent(VoiceNetworkData data)
{
// Validate authority
if (!HasAuthority())
{
Debug.LogWarning("Non-owner tried to send voice event!");
return;
}
SendToNetwork(data);
}Common Patterns
Voice Activity Indicator
Show which players are speaking:
public class VoiceActivityUI : MonoBehaviour
{
private Dictionary<ulong, bool> playerSpeakingStates = new();
[ClientRpc]
void UpdateVoiceActivityClientRpc(ulong playerId, bool isSpeaking)
{
playerSpeakingStates[playerId] = isSpeaking;
UpdateUI();
}
void UpdateUI()
{
foreach (var kvp in playerSpeakingStates)
{
var indicator = GetIndicator(kvp.Key);
indicator.SetActive(kvp.Value);
}
}
}Proximity Voice Chat
Implement spatial voice chat:
void ProcessRemoteVoice(Vector3 remotePosition, float db, float maxRange)
{
var listeners = FindObjectsByType<VoiceListener>(FindObjectsSortMode.None);
foreach (var listener in listeners)
{
float distance = Vector3.Distance(listener.transform.position, remotePosition);
if (distance <= maxRange)
{
// Player is in range - process voice
float normalizedVolume = Mathf.InverseLerp(-60f, 0f, db);
listener.ProcessRemotePlayerVoice(normalizedVolume, distance);
}
}
}Debugging
Enable logging for network voice events:
public void SendVoiceEvent(VoiceNetworkData data)
{
Debug.Log($"[Network] Sending voice event: speaking={data.isPlayerSpeaking}, dB={data.decibelLevel}");
SendToNetwork(data);
}
[ClientRpc]
void ReceiveVoiceEventClientRpc(bool isSpeaking, float db, Vector3 position)
{
Debug.Log($"[Network] Received voice event: speaking={isSpeaking}, dB={db}, pos={position}");
ProcessRemoteVoice(isSpeaking, db, position);
}Best Practices
- Only send on state changes - Don’t spam the network with redundant data
- Throttle updates - Limit send rate (10-20 Hz is usually sufficient)
- Validate authority - Ensure only owners send voice events
- Use spatial relevance - Only send to players in hearing range
- Handle latency - Discard or interpolate old events
- Test with latency - Use Unity’s Network Simulator