Developer Documentation
Our new developer documentation is now available. Please check it out!

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.

1Join Room

The user navigates to the multiplayer lobby. Here, all players currently playing the game are connected to the same ODIN room so that they can figure out what to play. The application uses JoinRoom function of the OdinHandler instance to join the room. Please note: The server automatically creates the room if it does not exist. There is no need for bookkeeping on your side.

2RoomJoin

The OnRoomJoin event is triggered. It allows you to handle logic before the user actually joins the room. Please note: All upcoming PeerJoined and MediaAdded events are for users that were already connected to the room.

3PeerJoined

For each user connected to the same ODIN room you’ll receive an OnPeerJoined event that allows you to handle logic once a peer joined the room.

4MediaAdded

For each user connected to the same ODIN room which has a microphone stream enabled (some of them might only be spectators just listening) an OnMediaAdded event is triggered. This event needs to be handled by your application. In this callback, you basically use the AddPlaybackComponent member function of the OdinHandler singleton instance to create a PlaybackComponent that is attached to a GameObject in your scene, depending on your use case. Navigate to the event to learn more and to see some example code.

5RoomJoined

The OnRoomJoined event is triggered. This event allows you to handle logic after the user joined the room. All PeerJoined and MediaAdded events that come before RoomJoined event are for users that were already connected to the room. Events after RoomJoined event indicated changes to the room after the user connected.

6MediaRemoved

Whenever a user disconnects from the room or closes their microphone stream, an OnMediaRemoved event is triggered. It is your responsibility to clean up the PlaybackComponent components that were created earlier in the MediaAdded callback function.

7PeerLeft

Whenever a user disconnects from a room, this event is triggered. For example, you can show a push notification that a player has left the room. More info here: OnPeerLeft . Important notice:. Whenever a peer leaves a room, the media gets removed as well. So, aligned with this event, for each media of this peer, a MediaRemoved event will be triggered as well.

8Leave Room

You can use the member function LeaveRoom of the OdinHandler singleton instance to leave a room.

Important Notice: Due to the asynchronous nature of leaving a room operation, the current recommendation is to avoid invoking this function within OnDestroy if the scene is being unloaded. Scene unloading could occur when transitioning between scenes or shutting down an application.

Instead, the best practice is to call the LeaveRoom function and subsequently wait for the OnRoomLeft event to be triggered. Once this event has been triggered, it is then safe to perform further actions, such as calling LoadScene or Application.Quit.

9RoomLeave

The event OnRoomLeave is triggered to notify you, that the current user started to leave the room. You can use it to clean up your scene. You can either do that in this event or the next.

10RoomLeft

The event OnRoomLeft is triggered to notify you, that the current user has left the room. You need to listen to this event to do some cleanup work: You might have some PlaybackComponent components in your scene that you have created earlier. Some (or all of them) are linked to the room that the user just left. Use DestroyPlaybackComponents member function of the OdinHandler singleton instance to remove all PlaybackComponent elements linked to the left room.

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 not null 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 is false per default and is set to true in the RoomJoined event. In your PeerJoined event you can check if this property is true or not. Only show notifications if this property is set to true

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 listening to the direction their voice is coming from. We leverage Unitys built-in 3D positional audio by attaching the Voice playback 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 create this basic walkie-talkie functionality by listening to the following events: OnMediaAdded , OnMediaRemoved and OnRoomLeft .

Walkie-Talkie

This simple example works as follows:

  • 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 a walkieTalkie 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 achieve this is by creating a new script in Unity (in this example, we’ll call it OdinPeerManager) and implementing the callback functions within it. After that, you can either create an empty GameObject in your scene and attach the OdinPeerManager component to it, or directly attach the OdinPeerManager to the ODIN Manager prefab that’s already in your scene. Finally, use the inspector of the Odin Manager prefab to link the ODIN SDK events to your custom implementation.

Our OdinPeerManager added to our scene.

Our OdinPeerManager added to our scene.

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.

This is the Elena Soldier Model from the Unity Asset Store that we used in our demo

This is the Elena Soldier Model from the Unity Asset Store that we used in our demo

The final version of our OdinPeerManager implementing what we have defined above looks like this:

OdinPeerManager example
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.

Joining a room
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.

Switching channels
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:

Joining the world room
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’ve successfully joined the world room. However, at this point, all player voices are being routed to our walkie-talkie, which isn’t what we want. Instead, we want the other players’ walkie-talkies connected to ours, while their “world voice” should be attached to their corresponding avatars in the scene. This way, their voice positions will match their in-game positions, ensuring accurate spatial audio.

Our current implementation on OnMediaAdded looks like this:

Current OnMediaAdded implementation
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 already. But if it’s the world room, we need to attach the PlaybackComponent to the game object representing the player in the scene.

OdinPeerManager with positional audio
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 where the media was created is a “WalkieTalkie” room, we use the same implementation as 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, meaning Unity will automatically handle the 3D audio processing for us.

Info

This guide covered the basic and essential event handling. We didn’t dive into the specifics of integrating your multiplayer framework with ODIN, as the approach may vary depending on the framework you’re using. We provide a typical solution in our Mirror Networking guide and also have an open-source example available for Photon.

We also demonstrate how to integrate ODIN into an existing multiplayer game in our “ODIN for existing projects” guide.