React to roster and target changes
Subscribe to roster and interaction-target events in a shared Unity room, and rely on the order the SDK guarantees between them.
Subscribe to CharacterAdded, CharacterRemoved, CharacterStatusChanged, and InteractionTargetChanged on MultiCharacterRoomSession to react to a roster or target change as it happens. Use this page when your scene needs to update UI, logging, or gameplay state as characters come and go instead of polling the roster.
Prerequisites
A connected multi-character session. See Build your first multi-character session.
A reference to the current
MultiCharacterRoomSession, fromIConvaiRoomConnectionService.CurrentMultiCharacterSession.
Subscribe to the roster and target events
CharacterAdded
Action<CharacterRoomMembership>
A membership is added to the room after it was already connected.
CharacterRemoved
Action<CharacterRoomMembership>
A membership is removed from the room.
CharacterStatusChanged
Action<CharacterRoomMembership>
A membership transitions into Ready or Failed, or a new membership is inserted into the roster.
InteractionTargetChanged
Action<CharacterRoomMembership, CharacterRoomMembership>
The canonical active membership changes; the current membership is null when the target is cleared.
using Convai.Runtime.Room;
using UnityEngine;
public class MultiCharacterEventLogger : MonoBehaviour
{
private MultiCharacterRoomSession _session;
public void Attach(MultiCharacterRoomSession session)
{
Detach();
_session = session;
if (_session == null) return;
_session.CharacterAdded += HandleCharacterAdded;
_session.CharacterRemoved += HandleCharacterRemoved;
_session.CharacterStatusChanged += HandleCharacterStatusChanged;
_session.InteractionTargetChanged += HandleInteractionTargetChanged;
}
public void Detach()
{
if (_session == null) return;
_session.CharacterAdded -= HandleCharacterAdded;
_session.CharacterRemoved -= HandleCharacterRemoved;
_session.CharacterStatusChanged -= HandleCharacterStatusChanged;
_session.InteractionTargetChanged -= HandleInteractionTargetChanged;
_session = null;
}
private void HandleCharacterAdded(CharacterRoomMembership membership) =>
Debug.Log($"[MultiCharacter] Added {membership.CharacterId} ({membership.MembershipId}).");
private void HandleCharacterRemoved(CharacterRoomMembership membership) =>
Debug.Log($"[MultiCharacter] Removed {membership.CharacterId} ({membership.MembershipId}).");
private void HandleCharacterStatusChanged(CharacterRoomMembership membership) =>
Debug.Log($"[MultiCharacter] {membership.CharacterId} is now {membership.Status}.");
private void HandleInteractionTargetChanged(CharacterRoomMembership previous, CharacterRoomMembership current) =>
Debug.Log($"[MultiCharacter] Target changed from {previous?.MembershipId ?? "none"} to {current?.MembershipId ?? "none"}.");
private void OnDestroy() => Detach();
}The ordering guarantee when the active character is removed
Removing the membership that currently holds the interaction target produces two events in a fixed order: InteractionTargetChanged fires first, with the removed membership as previous and null as current, and CharacterRemoved fires second. Code that reacts to CharacterRemoved can rely on the interaction target already being cleared by the time it runs — no separate check of ActiveMembershipId is needed to avoid a stale read.
This cleared-target InteractionTargetChanged does not advance RouteEpoch. Removal clears ActiveMembershipId directly instead of going through the epoch-guarded target update, so a subscriber that reacts to target changes only by comparing RouteEpoch misses this event. Key any such logic off the event itself, not off a RouteEpoch change, if it must also react to a target cleared by removal.
Passing a replacement target does not suppress that first event. The SDK applies the removal before it applies the new target, so a removal with a replacement fires InteractionTargetChanged twice: once with null as current, then again with the replacement membership. Treat a null current target as a transition rather than a terminal state.
A related ordering guarantee applies to additions: when a new membership is inserted, CharacterAdded fires before CharacterStatusChanged for that same membership. CharacterAdded also fires exactly once for a given membership even when Convai's lifecycle message for it arrives before the roster-update acknowledgement does — the SDK deduplicates the two paths rather than raising the event twice.
Unsubscribe when the session ends
MultiCharacterRoomSession is replaced on every reconnect, so a handler attached to one session instance stops receiving events once that instance is discarded. Detach in OnDisable or OnDestroy, and reattach to the new CurrentMultiCharacterSession after IConvaiRoomConnectionService.Connected fires again.
Troubleshooting
CharacterAdded never fires for a character the scene started with
Those memberships were populated when the session object was created, not through the runtime-addition code path.
Read session.Characters right after connecting instead of waiting for CharacterAdded.
InteractionTargetChanged fires with current as null unexpectedly
The membership holding the target was removed. This event fires whether or not a replacement target was supplied.
Expected behavior. Pass replacementTargetMembershipId to Add and remove characters at runtime so a second event immediately restores a target, and treat the null as a transition.
An event you expected does not fire at all
The underlying acknowledgement was a stale or duplicate one and was discarded by the epoch guard.
Next steps
Switch the interaction targetAdd and remove characters at runtimeCharacter identity and addressingLast updated
Was this helpful?