> 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` | `已加入`, `已离开`，或 `已拒绝`.         |
| `MembershipId`         | `string`           | 受影响的房间成员身份。若某次加入从未获得成员身份，则为空。 |
| `CharacterId`          | `string`           | 与此相关角色的 Convai 角色 ID。         |
| `CharacterName`        | `string`           | 显示名称，用于日志和 UI。                |
| `RosterSize`           | `int`              | 此次更改后房间中有多少角色。                |
| `原因`                   | `string`           | 编辑为何被拒绝。除非 `更改` 是 `已拒绝`.      |

```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` 和 `开始` 和 `Awake` 都是可以订阅的安全位置——另一个组件的 `Awake` 并不安全，因为 Unity 不会对两个 `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>`                          | 某个成员身份转换为 `Ready` 或 `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 ?? \"无\"} 变为 {current?.MembershipId ?? \"无\"}。");

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

{% endcode %}

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

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

{% hint style="warning" %}
这个已清除目标的 `InteractionTargetChanged` 不会推进 `RouteEpoch`。移除会直接清除 `ActiveMembershipId` ，而不是经过受 epoch 保护的目标更新，因此只通过比较 `RouteEpoch` 来响应目标变化的订阅者会错过这个事件。此类逻辑应基于事件本身，而不是基于 `RouteEpoch` 变化，如果它还必须对因移除而被清除的目标做出响应。
{% 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` 视为一次过渡。 |
| 一个你预期会触发的事件根本没有触发                                            | 底层确认是过时或重复的确认，因此被丢弃，因为 `RosterEpoch` 或 `RouteEpoch` 已经越过了它。 | 读取 `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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
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=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
