> 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/character-actions/writing-custom-executors.md).

# 编写自定义动作执行器

当内置执行器不符合你项目的移动系统、交互模式或游戏规则时，请实现 `IConvaiActionExecutor`。自定义执行器是一个标准的 C# `MonoBehaviour` ，带有一个异步方法。调度器会将其与任何内置执行器一视同仁——所有策略、事件和取消行为都会自动应用。

### 何时构建自定义执行器

在以下情况下构建自定义执行器：

* 你的项目使用自定义移动系统（根运动、 `CharacterController`、转向行为）
* 某个动作会修改库存、UI 状态、任务标记或物理对象
* 某个动作会调用外部服务或触发基于协程的动画系统
* 你需要条件逻辑——例如，一个会根据角色状态表现不同的动作

### IConvaiActionExecutor 接口

```csharp
public interface IConvaiActionExecutor
{
    Task<ConvaiActionExecutionResult> ExecuteAsync(
        ConvaiActionInvocation invocation,
        CancellationToken cancellationToken);
}
```

在任何 `MonoBehaviour`上实现此接口。调度器会调用 `ExecuteAsync` 对每个步骤执行并等待结果，然后再继续下一个步骤。请让任务保持存活，直到游戏逻辑工作完成——如果过早返回，即使动画或移动仍在进行，也会结束该步骤。

{% hint style="info" %}
执行器运行在 Unity 主线程上。你可以安全地调用 Unity API（`transform`, `GetComponent`, `Instantiate`等）在 `ExecuteAsync`中的任何地方。使用 `await Task.Yield()` 可以让出一帧而不离开主线程。
{% endhint %}

### ConvaiActionInvocation 对象

每个 `ExecuteAsync` 调用会接收一个 `ConvaiActionInvocation` ，其中包含执行该行为所需的一切：

| 属性      | 类型                           | 包含                                                                     |
| ------- | ---------------------------- | ---------------------------------------------------------------------- |
| `命令`    | `ConvaiActionCommand`        | 原始后端命令—— `名称`, `目标`, `有目标`                                             |
| `定义`    | `ConvaiActionDefinition`     | 本地定义—— `ActionName`, `TargetRequirement`, `Executor`, `TimeoutSeconds` |
| `已解析目标` | `ConvaiResolvedActionTarget` | 已解析的目标绑定—— `类型`, `名称`, `对象绑定`, `角色绑定`, `GameObjectReference`           |
| `角色`    | `ConvaiCharacter`            | 正在执行的 NPC                                                              |
| `批次索引`  | `int`                        | 在调度器生命周期内此批次的顺序索引                                                      |
| `步骤索引`  | `int`                        | 当前批次中此步骤的索引（从 0 开始）                                                    |

访问目标 `GameObject` ，带有：

```csharp
GameObject targetGo = invocation.ResolvedTarget?.GameObjectReference;
```

不要重新解析 `invocation.Command.Name` 或 `invocation.Command.Target` 来重新推导该做什么。使用 `invocation.Definition` 和 `invocation.ResolvedTarget` ——它们已经完成解析并验证。

### 执行结果类型

请从以下工厂方法之一返回 `ExecuteAsync`:

| 工厂方法                                                                                    | 何时使用                                          |
| --------------------------------------------------------------------------------------- | --------------------------------------------- |
| `ConvaiActionExecutionResult.Succeeded()`                                               | 行为已成功完成                                       |
| `ConvaiActionExecutionResult.Failed(string message = null, Exception exception = null)` | 发生了真实错误（缺少组件、状态无效、游戏流程失败）                     |
| `ConvaiActionExecutionResult.Unhandled(string message = null)`                          | 此执行器有意拒绝处理该调用（上下文不正确或目标类型不对）                  |
| `ConvaiActionExecutionResult.Canceled()`                                                | 该 `CancellationToken` 已发出信号——当你在循环中观察到取消时返回此值 |

{% hint style="danger" %}
不要 **上的初始动态信息文本** return `ConvaiActionExecutionResult.TimedOut()` 手动返回。调度器会返回 `超时` 会在……时自动 `TimeoutSeconds` 过期，并且 `CancellationToken` 被触发。如果你自己返回它，结果将变得模糊，并且会绕过调度器的超时跟踪。
{% endhint %}

**`失败` 与 `未处理`:** 使用 `失败` 当你尝试执行该行为但出现问题时使用。使用 `未处理` 当此执行器根本不应处理此特定调用时使用——例如目标类型不正确。调度器会触发 `OnStepFailed` 关于 `失败` 和 `OnStepUnhandled` 关于 `未处理`；两者都被视为对 `StopBatch` 失败策略。

### 取消

该 `CancellationToken` 会在以下情况下触发：

1. `BatchPolicy.ReplaceCurrent` 生效（新的批次会抢占当前批次）
2. `TimeoutSeconds` 在动作定义上过期
3. 调度器被禁用或销毁

请始终在任何循环中或每次 `await`:

```csharp
while (!arrived)
{
    cancellationToken.ThrowIfCancellationRequested();
    // 前进一步
    await Task.Yield();
}
```

如果你的代码捕获到 `OperationCanceledException`，则返回 `ConvaiActionExecutionResult.Canceled()` 立即：

```csharp
try
{
    await SomeAsyncOperation(cancellationToken);
}
catch (OperationCanceledException)
{
    return ConvaiActionExecutionResult.Canceled();
}
```

或者，让 `ThrowIfCancellationRequested` 向上冒泡。调度器会将你的 `ExecuteAsync` 包裹在 try/catch 中，并将未捕获的 `OperationCanceledException` 到 `已取消` 。

### 完整示例：高亮对象执行器

此执行器会在已解析目标上启用轮廓效果，等待三秒，然后将其禁用。

```csharp
using System.Threading;
using System.Threading.Tasks;
using Convai.Runtime.Actions;
using UnityEngine;

[AddComponentMenu("MyProject/Actions/高亮对象执行器")]
public sealed class HighlightObjectExecutor : MonoBehaviour, IConvaiActionExecutor
{
    [SerializeField] private float _highlightDuration = 3f;

    public async Task<ConvaiActionExecutionResult> ExecuteAsync(
        ConvaiActionInvocation invocation,
        CancellationToken cancellationToken)
    {
        // 1. 获取目标
        GameObject targetGo = invocation.ResolvedTarget?.GameObjectReference;
        if (targetGo == null)
            return ConvaiActionExecutionResult.Failed("高亮动作未解析到目标。");

        // 2. 查找所需组件
        var outline = targetGo.GetComponent<OutlineEffect>();
        if (outline == null)
            return ConvaiActionExecutionResult.Failed(
                $"目标 '{invocation.ResolvedTarget.Name}' 没有 OutlineEffect 组件。");

        // 3. 执行行为
        outline.enabled = true;

        try
        {
            // 4. 等待，同时尊重取消
            await Task.Delay(
                (int)(_highlightDuration * 1000),
                cancellationToken);
        }
        catch (OperationCanceledException)
        {
            // 在取消时清理
            if (outline != null)
                outline.enabled = false;

            return ConvaiActionExecutionResult.Canceled();
        }

        // 5. 清理并返回成功
        if (outline != null)
            outline.enabled = false;

        return ConvaiActionExecutionResult.Succeeded();
    }
}
```

### 复合动作

将整个游戏流程放入一个 `ExecuteAsync`。调度器会将一个动作定义视为不可分割——它会等待你的任务完成后才开始下一步。对于拾取、检查、先打开再拿取，或任何包含多个子行为的序列，这都是正确的模式。

```csharp
public async Task<ConvaiActionExecutionResult> ExecuteAsync(
    ConvaiActionInvocation invocation,
    CancellationToken cancellationToken)
{
    // 阶段 1：导航
    ConvaiActionExecutionResult moveResult =
        await _mover.ExecuteAsync(invocation, cancellationToken);
    if (moveResult.Status != ConvaiActionExecutionStatus.Succeeded)
        return moveResult;

    // 阶段 2：交互
    cancellationToken.ThrowIfCancellationRequested();
    _animator.SetTrigger("Interact");

    // 阶段 3：等待动画
    await Task.Delay(1200, cancellationToken);

    // 阶段 4：应用效果
    ApplyInteractionEffect(invocation.ResolvedTarget?.GameObjectReference);

    return ConvaiActionExecutionResult.Succeeded();
}
```

### 执行器设计规则

* **使用 `invocation.ResolvedTarget`，而不是 `invocation.Command.Target`.** 调度器已经将名称解析为一个 `GameObject` 绑定——不要重新解析原始字符串。
* **返回 `未处理` 当此执行器不合适时。** 单个执行器组件可以在多个动作定义之间共享。返回 `未处理` 会向调度器发出信号以触发 `OnStepUnhandled` ，而不会将其视为严重失败。
* **设置 `TimeoutSeconds` 在动作定义中。** 请使用超时机制，而不是在执行器内部实现你自己的截止时间逻辑。
* **在取消时清理。** 如果你的执行器启用了某个效果、移动了一个对象，或持有了某个资源，请在返回前释放它 `已取消`.
* **不要在多次调用之间保留状态。** 同一个执行器实例可能会在多个批次中针对不同目标被调用。不要假设上一次调用的状态仍然有效。

### 下一步

{% content-ref url="/pages/111bca064ba7041a987662d038af4d71d58a32cd" %}
[配置角色动作](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/character-actions/configuring-actions.md)
{% endcontent-ref %}

{% content-ref url="/pages/0341126fa4c492311dab4fb6aca6d0c64191016b" %}
[角色动作脚本参考](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/character-actions/actions-scripting-reference.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:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/character-actions/writing-custom-executors.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
