Voice Chat
Relays opaque game-encoded audio frames by proximity (World interest) or voice channel; the framework never decodes — codec + playback are the game's.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IVoiceService
Relays opaque, game-encoded audio frames. Two routes, chosen per frame by Channel: proximity (Channel 0 → whoever can see the speaker in the world, interest-managed via the World piece) and channel (Channel > 0 → everyone joined to that voice channel, a Core room). The piece NEVER decodes or interprets the audio — it is entirely the game's vocabulary. Frames relay unreliably (voice is a loss-tolerant real-time stream); each frame is serialized once and the bytes reused for every recipient, like the movement/emote broadcast. Spatial-optional. The channel route is pure Core rooms — it works with no World piece, so a non-spatial game (voice channels / party chat, no world) gets full channel voice. The proximity route is inherently spatial: with no World composed it is simply unavailable (a Channel-0 frame is dropped), since "who can hear me nearby" has no meaning without a world.
-
void Relay(ISession session, VoiceFrameRequest request)Validates and relays a frame to the speaker's proximity observers or a channel.
-
void SetChannel(ISession session, byte channel, bool join)Joins or leaves a voice channel (to receive/stop receiving its frames).
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
VoiceOptions
Configurable voice-relay parameters (defaults here; never inlined in logic).
-
bool EchoToSelf { get; set; }Echo the speaker's own frames back to them. Off by default — you don't hear yourself; a game doing local monitoring turns it on.
-
int MaxAudioBytes { get; set; }Maximum size of an opaque audio frame, in bytes. Sized for one codec packet (~20-60 ms of Opus is well under 1 KiB); anything larger is dropped as malformed/abusive. Per-speaker flood control is the kernel's per-message-type rate limiting, configured by the game. Default 1024; raise only if the game's codec emits larger packets.
-
int MaxChannel { get; set; }Highest voice-channel number a client may use (1..MaxChannel). 0 is proximity. Default 32; size it to the number of concurrent channels the game needs (party / team / global).
-
bool RequireChannelMembershipToSpeak { get; set; }Require the speaker to have joined a channel before relaying to it. On by default (a stray frame to a channel you never joined is dropped); off lets a game push to channels freely.
Wire messages 3
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
VoiceFrameRequest Client → server: an opaque encoded audio frame to relay. Audio is a game-encoded blob — the framework never decodes it (Opus, PCM, whatever the game chose). Channel 0 = proximity (relayed to whoever can see the speaker in the world); a value > 0 = a voice channel (relayed to everyone joined to that channel). Sent unreliably — voice is a loss-tolerant real-time stream, not a discrete event.
EntityVoiceFrame Server → recipients: a relayed audio frame from a speaker (unreliable). Clients decode Audio with their own codec and play it positionally (proximity) or flat (channel) — the framework doesn't care.
VoiceChannelRequest Client → server: join or leave a voice channel (to receive/stop receiving its frames).
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IVoiceClient
The Voice puzzle piece, client side. Capture audio with your own codec and push opaque frames — proximity (channel 0) or a voice channel (> 0); receive EntityVoiceFrame events to decode + play however you like (positional for proximity, flat for channel). The framework never decodes the audio — codec and playback are entirely the game's. Remove this package and voice simply stops; nothing else breaks.
-
event Action<EntityVoiceFrame> FrameReceivedA frame arrived from a speaker you can hear (a nearby entity, or a channel you're in).
-
void Send(byte channel, byte[] audio)Sends an opaque audio frame. Channel 0 = proximity; > 0 = a voice channel. Unreliable.
-
void JoinChannel(byte channel)Joins a voice channel (to receive/send on it).
-
void LeaveChannel(byte channel)Leaves a voice channel.
UI views you can replace 1
Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.
ICrossplayVoiceView
Narrow contract the presenter drives. UI-only: render calls in, user intents out.