Event Handling
Events in ODIN allow you to quickly customize the implementation of voice in your game or application.
Basic application flow
Have a look at this application flow of a very basic lobby application. Events that you need to implement are highlighted in red.
Handling Notifications to users
Many applications notify users that other users have left or joined the room. If you show these notifications
whenever a PeerJoined
event is incoming, you’ll show a notification for every user that is already connected to
the user. If you just want to notify users of changes after they have connected the room you have two options:
- The property Self
of the Room
is set in the
RoomJoined
event. If this property is notnull
then you can be sure, that the event indicates a change after the user connected - You can set a local
Boolean
property in your class that isfalse
per default and is set totrue
in theRoomJoined
event. In yourPeerJoined
event you can check if this property is true or not. Only show notifications if this property is set totrue
Example Implementation
We have created a simple showcase demo in Unity. The idea is, that you need to find other players in a foggy environment just by their voice. We leverage ODINs built-in 3D positional audio that typically attaches the voice to player game objects so that their voice represents their location in 3D space - they are louder if close, and you might not hear them if they are far away. If they don’t find each other they can use a walkie-talkie like functionality to talk to other players independently of their 3D location.
In the first step, we implement that basic walkie-talkie functionality by implementing these events: OnMediaAdded , OnMediaRemoved and OnRoomLeft .
Walkie-Talkie
This simple example works like that:
- All users are connected to the same ODIN room named “WalkieTalkie1”
- We provide an AudioMixerGroup with some audio effects added to the voice of other users, so they sound a bit distorted
- In our example, the local player object is controlled by a
PlayerController
script that has awalkieTalkie
member variable that references a walkie-talkie mesh of the player character (see image below). - A singleton
GameManager
instance handles creation of player objects and manages them. We use this class instance to get hold of our local player object to get a reference to the walkie-talkie game object that we need in the next step. - Whenever another user connects a room, we handle the OnMediaAdded event and attach a PlaybackComponent to this walkie talkie mesh. Therefore, all audio sent by other players voice is coming out of this walkie talkie mesh.
The simplest way to do that is to create a new class in Unity (in our example we name it OdinPeerManager
) and to
implement that callback functions there. Then, either create an empty GameObject in your scene and attach the
OdinPeerManager
component to it or attach the OdinPeerManager
directly to the ODIN Manager prefab that you
already have in your scene and use inspector of the Odin Manager prefab to link the events of the ODIN SDK to your own implementation.
This is the player mesh we used in our ODIN example showcase. It’s Elena from the Unity Asset Store that looks stunning and is very easy to use. Highlighted is the walkie-talkie game object that is used to realistically attach all other players voice to this object. Therefore, other players will hear walkie-talkie sound coming out of this players walkie-talkie as you would in real life.
The final implementation of our OdinPeerManager
implementing what we have defined above looks like this:
using OdinNative.Odin.Room;
using OdinNative.Unity.Audio;
using UnityEngine;
using UnityEngine.Audio;
public class OdinPeerManager : MonoBehaviour
{
[ToolTip("Set to an audio mixer for radio effects")]
public AudioMixerGroup walkieTalkieAudioMixerGroup;
private void AttachWalkieTalkiePlayback(GameObject gameObject, Room room, ulong peerId, ushort mediaId)
{
// Attach the playback component from the other player to our local walkie talkie game object
PlaybackComponent playback = OdinHandler.Instance.AddPlaybackComponent(gameObject, room.Config.Name, peerId, mediaId);
// Set the spatialBlend to 1 for full 3D audio. Set it to 0 if you want to have a steady volume independent of 3D position
playback.PlaybackSource.spatialBlend = 0.5f; // set AudioSource to half 3D
playback.PlaybackSource.outputAudioMixerGroup = walkieTalkieAudioMixerGroup;
}
public void OnRoomLeft(RoomLeftEventArgs eventArgs)
{
Debug.Log($"Room {eventArgs.RoomName} left, remove all playback components");
// Remove all Playback Components linked to this room
OdinHandler.Instance.DestroyPlaybackComponents(eventArgs.RoomName);
}
public void OnMediaRemoved(object sender, MediaRemovedEventArgs eventArgs)
{
Room room = sender as Room;
Debug.Log($"ODIN MEDIA REMOVED. Room: {room.Config.Name}, MediaId: {eventArgs.Media.Id}, UserData: {eventArgs.Peer.UserData.ToString()}");
// Remove all playback components linked to this media id
OdinHandler.Instance.DestroyPlaybackComponents(eventArgs.Media.Id);
}
public void OnMediaAdded(object sender, MediaAddedEventArgs eventArgs)
{
Room room = sender as Room;
Debug.Log($"ODIN MEDIA ADDED. Room: {room.Config.Name}, PeerId: {eventArgs.PeerId}, MediaId: {eventArgs.Media.Id}, UserData: {eventArgs.Peer.UserData.ToString()}");
// Another player connected the room. Find the local player object and add a PlaybackComponent to it.
// In multiplayer games, player objects are often not available at runtime. The GameManager instance handles
// that for us. You need to replace this code with your own
var localPlayerController = GameManager.Instance.GetLocalPlayerController();
if (localPlayerController && localPlayerController.walkieTalkie)
{
AttachWalkieTalkiePlayback(localPlayerController.walkieTalkie, room, eventArgs.PeerId, eventArgs.Media.Id);
}
}
}
What’s left is that we need to join the room once the game starts. We do that in our PlayerController
script.
public class PlayerController : MonoBehaviour
{
// Join the room when the script starts (i.e. the player is instantiated)
void Start()
{
OdinHandler.Instance.JoinRoom("WalkieTalkie1");
}
// Leave the room once the player object gets destroyed
void OnDestroy()
{
OdinHandler.Instance.LeaveRoom("WalkieTalkie1");
}
}
Switching channels
Walkie-talkies allow users to choose a channel so not everyone is talking on the same channel. We can add this functionality with a couple lines of code. The only thing we need to do is to leave the current room representing a channel and to join another room. That’s it.
ODIN rooms are represented by its name. Nothing more. There is no bookkeeping required. Choose a name that makes sense for your application and join that room.
public class PlayerController : MonoBehaviour
{
// The current walkie-talkie channel
public int channelId = 1;
// Create a room name of a channel
string GetOdinRoomNameForChannel(int channel)
{
return $"WalkieTalkie{channel}";
}
// Join the room when the script starts (i.e. the player is instantiated)
void Start()
{
UpdateOdinChannel(channelId);
}
// Leave the room once the player object gets destroyed
void OnDestroy()
{
OdinHandler.Instance.LeaveRoom(GetOdinRoomNameForChannel(channelId));
}
// Leave and join the corresponding channel
private void UpdateOdinChannel(int newChannel, int oldChannel = 0)
{
if (oldChannel != 0)
{
OdinHandler.Instance.LeaveRoom(GetOdinRoomNameForChannel(oldChannel));
}
OdinHandler.Instance.JoinRoom(GetOdinRoomNameForChannel(newChannel));
}
// Check for key presses and change the channel
void Update()
{
if (Input.GetKeyUp(KeyCode.R))
{
int newChannel = channelId + 1;
if (newChannel > 9) newChannel = 1;
UpdateOdinChannel(newChannel, channelId);
}
if (Input.GetKeyUp(KeyCode.F))
{
int newChannel = channelId - 1;
if (newChannel < 1) newChannel = 9;
UpdateOdinChannel(newChannel, channelId);
}
}
}
That’s it. You don’t need to change anything in the OdinPeerManager
as we already handle everything. If we switch
the room, we first leave the current room, which triggers the OnRoomLeft
event. As we implemented
that event callback we just remove all PlaybackComponent
objects linked to this room - i.e. there
will be no PlaybackComponent
objects anymore linked to our walkie-talkie game object.
Next, we join the other room. For every player that is sending audio in this channel. we’ll receive the OnMediaAdded event which will again create PlaybackComponent objects to our walkie-talkie game object.
3D Positional Audio
As described above, in our example we have two layers of voice: walkie-talkie that we have just implemented and 3D positional audio for each player.
Adding the second layer requires two things:
- Joining another room when the game starts. Yes, with ODIN you can join multiple rooms at once and our SDK and servers handle everything automatically for you.
- Changing the OnMediaAdded callback to handle 3D rooms differently than Walkie-Talkie rooms.
Joining the world room
All players running around in our scene join the same room, we simply call it “World”. So, we adjust our current
Start
implementation in PlayerController
:
public class PlayerController : MonoBehaviour
{
// ...
// Join the room when the script starts (i.e. the player is instantiated)
void Start()
{
UpdateOdinChannel(channelId);
// Join the world room for positional audio
OdinHandler.Instance.JoinRoom("World");
}
// Leave the room once the player object gets destroyed
void OnDestroy()
{
OdinHandler.Instance.LeaveRoom(GetOdinRoomNameForChannel(channelId));
// Leave the world room
OdinHandler.Instance.LeaveRoom("World");
}
// ...
}
Adjusting OnMediaAdded
That’s it. We now have joined the world room. What happens now is, that all players voice is attached to our walkie-talkie. Which is not what we want. We want to have the other players walkie-talkies attached to our walkie-talkie, but the other players “world-voice” we want to attach to the corresponding players in the scene so that their voice position is identical to their position in the scene.
Our current implementation on OnMediaAdded looks like this:
public class OdinPeerManager : MonoBehaviour
{
// ...
public void OnMediaAdded(object sender, MediaAddedEventArgs eventArgs)
{
Room room = sender as Room;
Debug.Log($"ODIN MEDIA ADDED. Room: {room.Config.Name}, PeerId: {eventArgs.PeerId}, MediaId: {eventArgs.Media.Id}, UserData: {eventArgs.Peer.UserData.ToString()}");
// Another player connected the room. Find the local player object and add a PlaybackComponent to it.
// In multiplayer games, player objects are often not available at runtime. The GameManager instance handles
// that for us. You need to replace this code with your own
var localPlayerController = GameManager.Instance.GetLocalPlayerController();
if (localPlayerController && localPlayerController.walkieTalkie)
{
AttachWalkieTalkiePlayback(localPlayerController.walkieTalkie, room, eventArgs.PeerId, eventArgs.Media.Id);
}
}
// ...
}
Depending on the room where the media is added we need to handle things differently. If it’s a walkie-talkie room, we add the PlaybackComponent representing the other players voice to the local players walkie-talkie. This is what we have implemented today. But if it’s the world room, we need to attach the PlaybackComponent to the game object representing the player in the scene.
public class OdinPeerManager : MonoBehaviour
{
// ...
// Create and add a PlaybackComponent to the other player game object
private void AttachOdinPlaybackToPlayer(PlayerController player, Room room, ulong peerId, ushort mediaId)
{
PlaybackComponent playback = OdinHandler.Instance.AddPlaybackComponent(player.gameObject, room.Config.Name, peerId, mediaId);
// Set the spatialBlend to 1 for full 3D audio. Set it to 0 if you want to have a steady volume independent of 3D position
playback.PlaybackSource.spatialBlend = 1.0f; // set AudioSource to full 3D
}
// Our new OnMediaAdded callback handling rooms differently
public void OnMediaAdded(object sender, MediaAddedEventArgs eventArgs)
{
Room room = sender as Room;
Debug.Log($"ODIN MEDIA ADDED. Room: {room.Config.Name}, PeerId: {eventArgs.PeerId}, MediaId: {eventArgs.Media.Id}, UserData: {eventArgs.Peer.UserData.ToString()}");
// Check if this is 3D sound or Walkie Talkie
if (room.Config.Name.StartsWith("WalkieTalkie"))
{
// A player connected Walkie Talkie. Attach to the local players Walkie Talkie
var localPlayerController = GameManager.Instance.GetLocalPlayerController();
if (localPlayerController && localPlayerController.walkieTalkie)
{
AttachWalkieTalkiePlayback(localPlayerController, room, eventArgs.PeerId, eventArgs.Media.Id);
}
}
else if (room.Config.Name == "World")
{
// This is 3D sound, find the local player object for this stream and attach the Audio Source to this player
PlayerUserDataJsonFormat userData = PlayerUserDataJsonFormat.FromUserData(eventArgs.Peer.UserData);
PlayerController player = GameManager.Instance.GetPlayerForOdinPeer(userData);
if (player)
{
AttachOdinPlaybackToPlayer(player, room, eventArgs.PeerId, eventArgs.Media.Id);
}
}
}
// ...
}
If the room a media is added is a “WalkieTalkie” room, we use the same implementation used before. However, if it’s
the world room, we need to find the corresponding player game object in the scene and attach the PlaybackComponent
to it. We also set spatialBlend
to 1.0
to activate 3D positional audio,
that is Unity will handle 3D audio for us automatically.
This guide should show you how to do the basic and required event handling. We don’t go into much detail how to merge your multiplayer framework with ODIN. Depending on the multiplayer framework there are different solutions on how to do that. We show a typical solution in our Mirror Networking guide and have an open-source example available for Photon.