> 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/update-the-roster.md).

# 角色加入与离开

了解 Convai 角色如何自动加入或离开已连接的 Unity 房间，以及何时应改为从代码中编辑成员列表。

一个 `ConvaiCharacter` 在房间已连接时出现在场景中的角色会无需重新连接就加入房间——通过实例化角色预制体或启用角色 `GameObject` 就是整个集成。使用此页来理解这种自动行为，并在需要时转而使用 `AddCharacterAsync` 或 `RemoveCharacterAsync` 仅当场景需要从脚本中有意编辑名册时才使用。

### 出现在场景中的角色会自动加入

无需调用任何东西。实例化第二个角色预制体，或启用一个 `ConvaiCharacter` `GameObject` 原本处于非激活状态的角色，SDK 会在连接接受该更改后立即将其添加到房间。控制台会确认：

```
“Sofia”无需重新连接就加入了房间。
```

角色一出现就会被发现、注入并准备就绪——无需再做任何设置它就能就座。它会以以下方式出现在名册中： `Starting` 直到 Convai 宣布它；参见 [房间就绪状态](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/readiness-and-partial-dispatch.md) 了解该状态的含义以及它通常持续多久。

### 禁用角色会保留其座位

`SetActive(false)` 不会将角色从房间中移除。它会保留成员资格并停止被寻址——目标选择会跳过它，而且无法与其交谈——直到再次启用；这无需往返，也无需等待。可在“在墙后”、“对象池中”或“暂时隐藏”时使用禁用；保留座位不需要任何成本。

只有在项目明确要求时，角色才会离开房间：被销毁，或失去所有权。无论哪种情况，控制台都会确认：

```
“Sofia”无需重新连接就离开了房间。
```

### 单角色房间无法扩展

以恰好一个角色连接的房间只为该角色单独打开，并且不携带名册，因此之后不会有任何东西能加入它——它会转而等待下一次连接。控制台会报告这一点，而不是保持沉默：

```
“James”无法加入此对话：该房间是为单个角色打开的，因此它没有
可供加入的名册。下次房间连接时它会被包含进去。若要让角色来来去去
在游戏过程中进行，请在房间连接之前让你希望在场景中处于激活状态的每个角色都就位——
处于存在但已禁用状态的角色不算，因为房间是为处于激活状态的
角色打开的。
```

若要让角色在游戏过程中来来去去，请让场景中有多个处于激活状态的角色 **之前** 在房间连接之前。连接时虽在场但未激活的角色不计入其中——房间会为当时处于激活状态的任何角色打开。

另外两种更改永远不会实时应用，因为它们决定的是房间如何创建，而不是里面是谁：更改的玩家，以及更改的 **初始角色**。任何一种都会改为排队进行重新连接。

### 从代码中显式编辑名册

当脚本需要有意添加或移除角色时使用此功能——例如，仅在某个其他条件满足后才生成角色，或者移除一个角色并在同一条命令中将对话交给特定替代者。检索 `IConvaiRoomConnectionService` 使用 `ConvaiManager.TryGetRoomConnectionService`.

#### 将角色添加到名册

调用 `AddCharacterAsync(IConvaiCharacterAgent character, string characterSessionId = null, CancellationToken cancellationToken = default)`。可选的 `characterSessionId` 会恢复该角色实例先前的对话，而不是开始新的对话。

两次添加同一个本地角色实例会抛出一个 `ArgumentException` 并显示消息 `此本地角色实例已是当前房间的成员。添加克隆时请使用另一个实例。`。若要添加房间中已存在角色的克隆，请实例化第二个 `ConvaiCharacter` 组件，并改为添加该实例——即使它共享一个 `CharacterId` 与原始对象相同的

该命令与以下操作共享一个名册变更门控： `RemoveCharacterAsync`，因此同一时间只能有一个名册更改在处理中，并且如果 `TimeoutException` 出错，携带 `等待角色名册更新确认时超时。` 如果 15 秒内没有收到确认，就会报错。

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

```csharp
using System;
using Convai.Runtime.Components;
using Convai.Runtime.Room;
using UnityEngine;

public class RosterAdder : MonoBehaviour
{
    public async void AddToRoom(IConvaiRoomConnectionService roomService, ConvaiCharacter character)
    {
        try
        {
            CharacterRosterUpdateResult result = await roomService.AddCharacterAsync(character);
            foreach (CharacterRoomMembership membership in result.Added)
                Debug.Log($"[MultiCharacter] 已将 {membership.CharacterId} 作为 {membership.MembershipId} 添加。");
        }
        catch (ArgumentException error)
        {
            Debug.LogError($"[MultiCharacter] {error.Message}");
        }
        catch (CharacterRosterUpdateException error)
        {
            Debug.LogError($"[MultiCharacter] 名册更新被拒绝（{error.Code}）：{error.Message}");
        }
        catch (InvalidOperationException error)
        {
            Debug.LogError($"[MultiCharacter] {error.Message}");
        }
        catch (TimeoutException error)
        {
            Debug.LogError($"[MultiCharacter] {error.Message}");
        }
    }
}
```

{% endcode %}

#### 从名册中移除角色

调用 `RemoveCharacterAsync(IConvaiCharacterAgent character, string replacementTargetMembershipId = null, CancellationToken cancellationToken = default)` 当你持有本地实例时，或者 `RemoveCharacterAsync(string membershipId, string replacementTargetMembershipId = null, CancellationToken cancellationToken = default)` 当你只有成员资格 ID 时。

`replacementTargetMembershipId` 是可选的，但只要你要移除的成员当前持有交互目标，就应传入一个——否则目标会清空为无，并且玩家输入会停止路由给任何人，直到你设置一个新目标。若你确实传入了一个，它必须指定一个当前在房间中的成员资格，并且不能是正在被移除的成员资格；任一违反都会抛出一个 `ArgumentException` 使用 `替换目标不是当前房间的一部分。` 或 `替换目标不能是正在移除的成员资格。`.

```csharp
try
{
    CharacterRosterUpdateResult result = await roomService.RemoveCharacterAsync(
        membershipId: assessorMembership.MembershipId,
        replacementTargetMembershipId: trainerMembership.MembershipId);
    Debug.Log($"[MultiCharacter] 当前活动目标现在是 {result.ActiveMembershipId}。");
}
catch (ArgumentException error)
{
    Debug.LogError($"[MultiCharacter] {error.Message}");
}
catch (CharacterRosterUpdateException error)
{
    Debug.LogError($"[MultiCharacter] 名册更新被拒绝（{error.Code}）：{error.Message}");
}
catch (InvalidOperationException error)
{
    Debug.LogError($"[MultiCharacter] {error.Message}");
}
catch (TimeoutException error)
{
    Debug.LogError($"[MultiCharacter] {error.Message}");
}
```

`CharacterRosterUpdateException` 会带有一个后端 `代码`。会确认两个值： `roster_epoch_mismatch`，当另一个已接受的命令先一步更改了名册时；以及 `unauthorized_sender`。将其他任何值视为未识别的后端拒绝，并同时记录 `代码` 和 `Message`.

### 名册限制

Convai 房间最多支持 50 个角色。SDK 会在进入名册的两条路径上强制执行这一客户端上限：房间连接时的完整角色阵容，以及稍后通过以下方式添加的单个角色 `AddCharacterAsync`。任一路径超出上限都会以相同的消息失败，消息中会写明请求的数量并指向 **Convai 管理器 > 角色加入房间**:

```
Convai 房间最多支持 50 个角色，而此处请求了 <count> 个。此
API 密钥可能允许的数量还更少；请使用 Convai 管理器 > 角色加入房间，只发送
此对话所需的角色。
```

在连接时，这会表现为一个 `ConvaiOperationException`；而从 `AddCharacterAsync` 这里会表现为一个普通的 `InvalidOperationException`，因为它是在向一个已被接受的名册添加一个成员资格，而不是验证整个连接请求。

名册不能为空。如果某次移除会使房间没有任何成员资格，Convai 会拒绝它，而不是接受一个空房间——请参见 [Live API 名册更新规则](/api-docs/zh/api-can-kao/core-api-reference/live-apis-beta/multi-character-sessions.md#update-the-roster) 以查看该规则的协议级说明。

### 故障排除

| 症状                                                           | 原因                                                            | 修复方法                                                   |
| ------------------------------------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------ |
| 在游戏过程中启用的角色永远不会加入                                            | 该房间是为单个角色打开的，并且不携带名册。请在它连接之前让你希望在场景中处于激活状态的每个角色都就位。           | 参见 [单角色房间无法扩展](#a-single-character-room-cant-grow) 上文。 |
| `InvalidOperationException`: `没有活动的多字符房间会话。`                 | 房间作为单角色房间连接，或者调用发生在连接完成之前。                                    | 检查 `CurrentMultiCharacterSession` 不是 `null` 在调用之前。     |
| `ArgumentException`: `该角色必须具有角色 ID。`                         | 该 `ConvaiCharacter` 传递给 `AddCharacterAsync` 具有一个空的 **角色 ID**. | 在添加角色之前设置该字段。                                          |
| `ArgumentException`: `此本地角色实例已是当前房间的成员。添加克隆时请使用另一个实例。`       | 同一个组件实例被传递给了 `AddCharacterAsync` 两次。                          | 使用第二个 `ConvaiCharacter` 实例来添加克隆。                       |
| `InvalidOperationException`：消息以 `Convai 房间最多支持 50 个角色`       | 名册（在连接时，或在此次添加之后）将超过 50 个角色。                                  | 减少角色阵容，或使用 **加入房间的角色** 只发送此对话所需的角色。                    |
| `CharacterRosterUpdateException` 代码为 `roster_epoch_mismatch` | 另一个已接受的命令先一步更改了名册。                                            | 读取 `session.RosterEpoch` 并重试该更改。                       |

### 下一步

{% 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/f9d36c1e08a6a79b4209c545683def235a26a8e5" %}
[处理房间事件](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/handle-roster-events.md)
{% endcontent-ref %}

{% content-ref url="/pages/9febe55e8fa6244952665a225b37a964cd41c375" %}
[房间就绪状态](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/readiness-and-partial-dispatch.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/update-the-roster.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.
