> 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` 在房间已连接时出现在场景中的角色会无需重新连接就加入——实例化一个角色预制体或启用一个角色 `游戏对象` 就是完整的集成。使用本页来理解这种自动行为，并在需要时使用 `AddCharacterAsync` 或 `RemoveCharacterAsync` 仅当场景需要从脚本中有意编辑名册时。

### 出现的角色会自动加入

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

```
'Sofia' 未重新连接就加入了房间。
```

角色一出现就会被发现、注入并准备就绪——无需再进行任何设置即可入座。它会以以下方式出现在名册中： `启动中` 直到 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` 组件，并改为添加该实例——尽管它与原始对象共享一个 `角色 ID` ，但它会成为一个可独立寻址的成员。

该命令与以下操作共享一个名册变更门控： `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`。将任何其他值视为无法识别的后端拒绝，并同时记录 `代码` 和 `消息`.

### 名册限制

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

```
一个 Convai 房间最多支持 50 个角色，而此处请求了 <count> 个。该 Convai 套餐对这个
API 密钥可能允许的数量还更少；请使用 Convai Manager > Characters Joining the Room 只发送
此对话所需的角色。
```

在连接时，这会表现为一个 `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`: `该角色必须具有 character ID。`                  | “ `ConvaiCharacter` 传递给 `AddCharacterAsync` 具有空的 **Character 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 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/update-the-roster.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.
