> 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` ，只有一个 async 方法。调度器会将其与任何内置执行器完全等同看待——所有策略、事件和取消行为都会自动适用。

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

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

* 你的项目使用自定义移动系统（根运动、 `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`        | 原始后端命令—— `名称`, `目标`, `HasTarget`                             |
| `定义`    | `ConvaiActionDefinition`     | 本地定义—— `ActionName`, `目标要求`, `执行器`, `超时时间（秒）`                |
| `已解析目标` | `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(string message = null)`                          | 行为已成功完成                                              |
| `ConvaiActionExecutionResult.Failed(string message = null, Exception exception = null)` | 发生了真实错误（缺少组件、状态无效、游戏逻辑失败）                            |
| `ConvaiActionExecutionResult.Unhandled(string message = null)`                          | 此执行器有意拒绝处理该调用（上下文或目标类型不匹配）                           |
| `ConvaiActionExecutionResult.Canceled()`                                                | 当调度器将未捕获的取消归类为非超时驱动时，会自动返回。不要手动返回它——让异常继续传播（见下文“取消”） |

{% hint style="danger" %}
不要 **不要** 返回 `ConvaiActionExecutionResult.TimedOut()` 手动。调度器会返回 `超时` 自动地 `超时时间（秒）` 到期并且 `CancellationToken` 被触发。如果你自己返回它，结果就会变得不明确，而且会绕过调度器的超时跟踪。
{% endhint %}

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

### 取消

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

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

在任何循环中，或每次 await 之后，都要检查令牌 `await`:

```csharp
while (!arrived)
{
    cancellationToken.ThrowIfCancellationRequested();
    // move one step
    await Task.Yield();
}
```

最好让 `OperationCanceledException` 继续传播，而不是自己捕获它。调度器会把你的 `ExecuteAsync` 包装在 try/catch 中，并对未捕获的 `OperationCanceledException` 为你分类：如果该步骤自身的 `超时时间（秒）` 已过期，则返回 `超时`；对于任何其他取消（`替换当前项`、禁用、销毁），则返回 `已取消`。如果你自己捕获该异常并无条件返回 `ConvaiActionExecutionResult.Canceled()`，你会把由超时驱动的取消误报为 `已取消` 而不是 `超时`.

如果你需要在取消时执行清理，请使用 `finally` 而不是 `catch` 以便异常仍继续传播，从而正确分类：

```csharp
try
{
    await SomeAsyncOperation(cancellationToken);
}
finally
{
    // 无论操作成功还是被取消，都必须执行的清理
}
```

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

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

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

[AddComponentMenu("MyProject/Actions/Highlight Object Executor")]
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("No target resolved for Highlight action.");

        // 2. 查找所需组件
        var outline = targetGo.GetComponent<OutlineEffect>();
        if (outline == null)
            return ConvaiActionExecutionResult.Failed(
                $"Target '{invocation.ResolvedTarget.Name}' has no OutlineEffect component.");

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

        try
        {
            // 4. 等待，并遵守取消
            await Task.Delay(
                (int)(_highlightDuration * 1000),
                cancellationToken);
        }
        finally
        {
            // 无论等待完成还是被取消，都进行清理
            if (outline != null)
                outline.enabled = false;
        }

        // 5. 让取消从上面的 try 块中继续传播，意味着
        // 调度器会正确地将其分类（TimedOut 而非 Canceled），而不是让这个
        // 执行器自行猜测——见上面的“取消”。
        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` 的信号，而不会将其视为硬性失败。
* **将 `超时时间（秒）` 在动作定义中。** 请使用超时机制，而不要在执行器内部实现你自己的截止时间逻辑。
* **在取消时进行清理。** 如果你的执行器启用了某个效果、移动了对象，或持有了某个资源，请在一个 `finally` 块中，在取消异常继续传播之前将其释放。
* **不要在多次调用之间保留状态。** 同一个执行器实例可能会在多个批次中针对不同目标被调用。不要假设上一次调用的状态仍然有效。

### 下一步

{% 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, and the optional `goal` 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>&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.
