> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/handle-roster-events.md).

# 处理房间事件

订阅共享 Unity 房间中的成员列表和交互目标事件，并依赖 SDK 保证的它们之间的顺序。

订阅 `ConvaiManager.Events.OnRoomRosterChanged` 用于对连接的房间中角色的加入或离开做出反应，包括拒绝的编辑及其原因。当你的场景需要在角色来来去去时更新 UI、日志或游戏状态，而不是轮询名册时，请使用此页面。

### 前提条件

* 一个已连接的多角色会话。参见 [构建你的第一个多角色会话](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/quick-start.md).
* 一个 `ConvaiManager` 参考，用于 `Events.OnRoomRosterChanged`.

### 订阅 OnRoomRosterChanged

`ConvaiManager.Events.OnRoomRosterChanged` 是 `Action<RoomRosterChanged>`，并且在连接的房间名册在运行时被编辑时都会触发——无论更改来自场景中角色的出现或消失，还是来自一个显式的 `AddCharacterAsync`/`RemoveCharacterAsync` 调用。

| `RoomRosterChanged` 字段 | 类型                 | 说明                            |
| ---------------------- | ------------------ | ----------------------------- |
| `变更`                   | `RoomRosterChange` | `加入`, `离开`，或 `拒绝`.            |
| `成员 ID`                | `字符串`              | 受影响的房间成员资格。对于从未获得成员资格的加入则为空。  |
| `角色 ID`                | `字符串`              | 此事件所指角色的 Convai Character ID。 |
| `CharacterName`        | `字符串`              | 显示名称，用于日志和 UI。                |
| `RosterSize`           | `整数`               | 此变更后房间包含多少角色。                 |
| `原因`                   | `字符串`              | 编辑被拒绝的原因。除非 `变更` 是 `拒绝`.      |

```csharp
private void OnEnable()
{
    ConvaiManager manager = ConvaiManager.ActiveManager;
    if (manager == null || !manager.IsInitialized) return;
    manager.Events.OnRoomRosterChanged += HandleRosterChanged;
}

private void OnDisable()
{
    ConvaiManager manager = ConvaiManager.ActiveManager;
    if (manager == null) return;
    manager.Events.OnRoomRosterChanged -= HandleRosterChanged;
}

private void HandleRosterChanged(RoomRosterChanged e)
{
    switch (e.Change)
    {
        case RoomRosterChange.Joined:
            Debug.Log($"[MultiCharacter] {e.CharacterName} 加入了。当前名册有 {e.RosterSize} 个成员。");
            break;
        case RoomRosterChange.Left:
            Debug.Log($"[MultiCharacter] {e.CharacterName} 离开了。当前名册有 {e.RosterSize} 个成员。");
            break;
        case RoomRosterChange.Refused:
            Debug.LogWarning($"[MultiCharacter] {e.CharacterName} 无法加入或离开：{e.Reason}");
            break;
    }
}
```

`ConvaiManager.Events` 在管理器仍在启动时抛出。它会在管理器自身的末尾变为可用 `Awake`，因此 `OnEnable` 和 `Start` 都是可以安全订阅的地方——另一个组件的 `Awake` 则不是，因为 Unity 不会保证两个 `Awake` 调用之间的顺序。请在 `ConvaiManager.IsInitialized` 上进行保护，尤其是在你无法控制顺序的地方。

角色加入实时房间是一个世界事件，而不是聊天事件——通常把它显示在角色上方的名牌上才是正确的位置，而不是聊天输入框里。 `ConvaiCharacter.RoomMembershipStatus` 会报告 `启动中` ，如果你需要在事件旁边获取逐角色状态，那么在给定角色仍在被服务通告期间也会报告它。

在以下内容上也相关： `ConvaiManager.Events`: `OnConversationTargetChanged` 报告对话在角色之间移动，包括其 `Requested`/`Confirmed`/`Failed` 阶段，以及 `OnConversationAvailabilityChanged` 报告被指向的角色是否已经能听到玩家。参见 [对话目标定位](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/conversation-targeting.md) 和 [对话可用性](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/conversation-availability.md).

### 更底层的会话事件

`MultiCharacterRoomSession` ——读取自 `IConvaiRoomConnectionService.CurrentMultiCharacterSession` ——以纯 C# 事件的形式暴露相同事实，比 `ConvaiManager.Events`低一级。只有当脚本已经持有一个 `MultiCharacterRoomSession` 引用并且需要 `CharacterRoomMembership` 直接使用对象，而不是使用 ID `OnRoomRosterChanged` 所报告的内容。

| 事件                         | 签名                                                         | 在……时触发                                    |
| -------------------------- | ---------------------------------------------------------- | ----------------------------------------- |
| `CharacterAdded`           | `Action<CharacterRoomMembership>`                          | 在已经连接之后，成员资格被添加到房间中。                      |
| `CharacterRemoved`         | `Action<CharacterRoomMembership>`                          | 成员资格被从房间中移除。                              |
| `CharacterStatusChanged`   | `Action<CharacterRoomMembership>`                          | 某个成员转变为 `就绪` 或 `Failed`，或者一个新成员被插入到花名册中时。 |
| `InteractionTargetChanged` | `Action<CharacterRoomMembership, CharacterRoomMembership>` | 规范的活动成员资格发生变化；当前成员资格为 `null` 当目标被清除时。     |

{% code title="Assets/Scripts/MultiCharacterEventLogger.cs" %}

```csharp
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] 添加了 {membership.CharacterId}（{membership.MembershipId}）。");

    private void HandleCharacterRemoved(CharacterRoomMembership membership) =>
        Debug.Log($"[MultiCharacter] 移除了 {membership.CharacterId}（{membership.MembershipId}）。");

    private void HandleCharacterStatusChanged(CharacterRoomMembership membership) =>
        Debug.Log($"[MultiCharacter] {membership.CharacterId} 现在是 {membership.Status}。");

    private void HandleInteractionTargetChanged(CharacterRoomMembership previous, CharacterRoomMembership current) =>
        Debug.Log($"[MultiCharacter] 目标已从 {previous?.MembershipId ?? \"none\"} 变为 {current?.MembershipId ?? \"none\"}。");

    private void OnDestroy() => Detach();
}
```

{% endcode %}

### 当活动角色被移除时的顺序保证

移除当前持有交互目标的成员资格会按固定顺序产生两个事件： `InteractionTargetChanged` 首先触发，移除的成员资格作为 `previous` 和 `null` 为 `current`以及 `CharacterRemoved` 随后触发。响应 `CharacterRemoved` 的代码可以依赖在它运行时交互目标已经被清除——无需单独检查 `当前成员 ID` 即可避免读取到过期值。

{% hint style="warning" %}
这个已清除目标的 `InteractionTargetChanged` 不会推进 `路由 epoch`。移除会直接清除 `当前成员 ID` ，而不是通过受 epoch 保护的目标更新流程，因此，如果订阅者仅通过比较 `路由 epoch` 来响应这一事件就会错过它。若这类逻辑也必须响应因移除而被清除的目标，请基于事件本身，而不是基于 `路由 epoch` 变化来触发。
{% endhint %}

传入替代目标不会抑制那个第一个事件。SDK 会先应用移除，再应用新目标，因此带有替代项的移除会触发 `InteractionTargetChanged` 两次：一次带着 `null` 为 `current`，然后再次带着替代成员资格。应将一个 `null` 当前目标视为一次过渡，而不是终态。

类似的顺序保证也适用于添加：当新增成员资格被插入时， `CharacterAdded` 会先触发 `CharacterStatusChanged` ，然后才触发该同一成员资格的 `CharacterAdded` 即使 Convai 的生命周期消息先于名册更新确认到达，它也会针对给定成员资格恰好触发一次——SDK 会对这两条路径去重，而不是触发两次事件。

{% hint style="info" %}
`CharacterAdded` 不会为房间首次连接时已存在的角色触发——这些成员资格在 `session.Characters` 会话对象存在时就已经位于 `CharacterAdded` 。请直接读取初始名册，而不要等待它的事件；该事件是为房间已经上线之后新增的角色准备的。
{% endhint %}

### 会话结束时取消订阅

`MultiCharacterRoomSession` 在每次重连时都会被替换，因此附加到某个会话实例上的处理程序在该实例被丢弃后就不再接收事件。请在 `OnDisable` 或 `OnDestroy`中分离，并重新附加到新的 `CurrentMultiCharacterSession` 在……之后 `IConvaiRoomConnectionService.Connected` 再次触发。

### 故障排查

| 症状                                                        | 原因                                                     | 修复方法                                                                                                                                                                                                                                  |
| --------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CharacterAdded` 对场景一开始就拥有的角色永远不会触发                       | 这些成员资格是在会话对象创建时填充的，而不是通过运行时添加代码路径。                     | 读取 `session.Characters` 连接后立即读取，而不是等待 `CharacterAdded`.                                                                                                                                                                               |
| `InteractionTargetChanged` 会触发，且带有 `current` 为 `null` 意外地 | 持有目标的成员资格已被移除。无论是否提供了替代目标，此事件都会触发。                     | 预期行为。传入 `replacementTargetMembershipId` 移动到 [角色的加入和离开](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/update-the-roster.md#remove-a-character-from-the-roster) 因此第二个事件会立即恢复目标，并将 `null` 视为一次过渡。 |
| 你期望的事件根本没有触发                                              | 底层确认是过期的或重复的，已被丢弃，因为 `花名册 epoch` 或 `路由 epoch` 已经推进超过它。 | 读取 `session.RosterEpoch` / `session.RouteEpoch` 并与你预期的状态进行比较，再判断该事件是否丢失。                                                                                                                                                              |

### 下一步

{% content-ref url="/pages/d8451a87588c6ea22a959d1a3ecd7ecc09422e8a" %}
[对话目标选择](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/conversation-targeting.md)
{% endcontent-ref %}

{% content-ref url="/pages/adffcd7e333fbd7f83aafb6f96412854dcf886f5" %}
[角色加入与离开](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/update-the-roster.md)
{% endcontent-ref %}

{% content-ref url="/pages/6481618ebe16f2ac14d3eecace53ed97c7f369d1" %}
[角色身份](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/character-identity.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/handle-roster-events.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
