> 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/unity-plugin-beta-overview/features/actions/writing-custom-executors.md).

# 编写自定义执行器

## 何时编写自定义执行器

Convai SDK 中包含的执行器覆盖了常见场景，但每个项目都有独特的行为需求。在以下情况编写自定义执行器：

* 你的游戏使用自定义移动系统（角色控制器、物理系统、寻路库）
* 你需要与 UI 系统、库存、物理对象或外部服务交互
* 你想通过条件逻辑组合多个行为
* 内置执行器无法满足你的 NPC 所需的精确行为

自定义执行器其实只是 **C# 类** ——一个实现了一个接口的 MonoBehaviour。除此之外没有任何 SDK 专属样板代码。

***

## IConvaiActionExecutor 接口

任何实现 `IConvaiActionExecutor` 的 MonoBehaviour 都可用作执行器。

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

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

该方法是 `异步` 的。你可以 `等待` 协程、任务或任何异步操作。

***

## 调用对象

`ConvaiActionInvocation` 会传入 `ExecuteAsync` ，并包含运行该行为所需的一切：

| 属性          | 类型                           | 描述                                                                 |
| ----------- | ---------------------------- | ------------------------------------------------------------------ |
| `命令`        | `ConvaiActionCommand`        | 来自后端的原始命令： `Command.Name` 和 `Command.Target` （尚未解析的目标名称字符串）。       |
| `定义`        | `ConvaiActionDefinition`     | 你的本地动作定义： `Definition.ActionName`, `Definition.TimeoutSeconds`，等等。 |
| `解析后的目标`    | `ConvaiResolvedActionTarget` | 匹配到的目标。 `ResolvedTarget.GameObjectReference` 会为你提供场景中的 GameObject。 |
| `Character` | `ConvaiCharacter`            | 执行此操作的角色。                                                          |
| `批次索引`      | int                          | 此次调用属于哪个批次（从 0 开始）。                                                |
| `步骤索引`      | int                          | 这是该批次中的第几步（从 0 开始）。                                                |

获取目标的场景 GameObject：

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

***

## 返回结果

请从以下工厂方法中返回其中之一： `ConvaiActionExecutionResult`:

| 方法                                                                 | 何时使用                                                          |
| ------------------------------------------------------------------ | ------------------------------------------------------------- |
| `ConvaiActionExecutionResult.Succeeded()`                          | 操作已成功完成。                                                      |
| `ConvaiActionExecutionResult.Failed(string message, Exception ex)` | 出现了问题（缺少组件、无效状态等）。消息和异常为可选项。                                  |
| `ConvaiActionExecutionResult.Unhandled(string message)`            | 此执行器无法处理给定的调用（例如，目标类型错误）。分发器会触发 `OnStepUnhandled`。消息为可选项。     |
| `ConvaiActionExecutionResult.Canceled()`                           | 该操作已取消（通常因为你检测到 `cancellationToken.IsCancellationRequested`). |
| `ConvaiActionExecutionResult.TimedOut()`                           | 当 `TimeoutSeconds` 超出时限时由分发器自动返回——你无需手动返回它。                   |

***

## 取消与超时

`ExecuteAsync` 接收一个 `CancellationToken`。当以下情况发生时，该令牌会被取消：

* 批次被取消（例如， `ReplaceCurrent` 策略接收到新批次）
* 该操作的 `TimeoutSeconds` 到期

如果你的执行器运行循环或等待长时间运行的操作， **请检查令牌** 以避免无限期阻塞：

```csharp
while (!arrived)
{
    if (cancellationToken.IsCancellationRequested)
        return ConvaiActionExecutionResult.Canceled();

    await Task.Yield();
}
```

或者直接将令牌与 `等待` 一起使用—— `OperationCanceledException` 会被分发器自动捕获：

```csharp
await Task.Delay(500, cancellationToken); // 如果被取消则抛出
```

***

## 分步：构建一个“高亮对象”执行器

本示例构建一个 `HighlightObjectExecutor` 它会在触发时为目标对象启用轮廓/高亮组件，等待三秒，然后将其禁用。

{% stepper %}
{% step %}
**创建脚本**

创建一个名为以下内容的新 C# 文件： `HighlightObjectExecutor.cs` 放到你的项目中：

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

[AddComponentMenu("My Game/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
        GameObject targetGo = invocation.ResolvedTarget?.GameObjectReference;
        if (targetGo == null)
            return ConvaiActionExecutionResult.Failed("未为高亮操作解析到目标。");

        // 2. 在目标上查找高亮组件
        Outline outline = targetGo.GetComponent<Outline>();
        if (outline == null)
            return ConvaiActionExecutionResult.Failed($"在 '{targetGo.name}' 上未找到 Outline 组件。");

        // 3. 启用高亮
        outline.enabled = true;

        // 4. 等待高亮持续时间（会响应取消）
        try
        {
            await Task.Delay(
                (int)(_highlightDuration * 1000),
                cancellationToken);
        }
        catch (TaskCanceledException)
        {
            outline.enabled = false;
            return ConvaiActionExecutionResult.Canceled();
        }

        // 5. 禁用高亮并返回成功
        outline.enabled = false;
        return ConvaiActionExecutionResult.Succeeded();
    }
}
```

{% endstep %}

{% step %}
**将组件添加到你的 NPC**

选中你的 NPC 的 GameObject，然后点击 **添加组件 → My Game → Highlight Object Executor**.

将 **高亮持续时间** 设置为你想要的值（默认：3 秒）。
{% endstep %}

{% step %}
**将其连接到动作定义**

在 `ConvaiActionConfigSource` 在同一个 NPC GameObject 上：

1. 在 **动作定义**.
2. 将 **动作名称** 设为 `高亮` （或你使用的任何名称）。
3. 将 **目标要求** 设为 `对象`.
4. 将 `HighlightObjectExecutor` 到 **执行器** 字段从另一个 GameObject 分配角色。
   {% endstep %}

{% step %}
**测试它**

按 **Play** 并对角色说：

> *“高亮灭火器。”*

灭火器上的 Outline 组件应启用三秒，然后禁用。
{% endstep %}
{% endstepper %}

***

## 更简单的示例：传送执行器

供参考，这是最小的自定义执行器模式——没有异步等待，只有同步操作：

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

public sealed class TeleportToTargetExecutor : MonoBehaviour, IConvaiActionExecutor
{
    [SerializeField] private Transform _moveRoot;

    public Task<ConvaiActionExecutionResult> ExecuteAsync(
        ConvaiActionInvocation invocation,
        CancellationToken cancellationToken)
    {
        GameObject targetGo = invocation.ResolvedTarget?.GameObjectReference;
        if (targetGo == null)
            return Task.FromResult(ConvaiActionExecutionResult.Failed("未解析到目标。"));

        Transform root = _moveRoot != null ? _moveRoot : transform;
        root.position = targetGo.transform.position;

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

注意使用 `Task.FromResult` ——对于同步执行器，请包装结果，而不是使用 `异步`/`等待`.

***

## 复合执行器模式

对于由多个游戏步骤组成的操作，请将整个序列放入一个 `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：播放动画
    if (_animator != null)
        _animator.SetTrigger(_triggerName);

    // 步骤 3：等待动画结束（会响应取消）
    await Task.Delay(TimeSpan.FromSeconds(_animationDuration), cancellationToken);

    // 步骤 4：将对象附加到角色的手上
    if (_attachPoint != null)
    {
        Transform targetTransform = invocation.ResolvedTarget.GameObjectReference.transform;
        targetTransform.SetParent(_attachPoint, worldPositionStays: false);
        targetTransform.localPosition = Vector3.zero;
        targetTransform.localRotation = Quaternion.identity;
    }

    return ConvaiActionExecutionResult.Succeeded();
}
```

{% hint style="info" %}
参见 `PickUpActionExecutor` 在 SDK 中查看该模式的完整参考实现。
{% endhint %}

***

## 提示与最佳实践

{% hint style="info" %}
**返回 `未处理` 当此执行器不是合适的任务处理器时。** 例如，如果你的执行器只处理生物目标，却收到了一个对象目标，请返回 `未处理` 而不是 `失败`。这样其他执行器（或回退方案）就可以处理该步骤，而不会被计为失败。
{% endhint %}

{% hint style="info" %}
**返回 `失败` 对于真正的错误** ——缺少组件、空引用、无效状态。请附上描述性消息，以便调试探针和控制台日志帮助你诊断问题。
{% endhint %}

{% hint style="warning" %}
**在任何循环或长时间 await 中，都要检查 CancellationToken。** 在其中未检查的循环 `ExecuteAsync` 会阻塞分发器，即使策略取消了当前批次，也会阻止新批次运行。
{% endhint %}

{% hint style="info" %}
**执行器运行在 Unity 主线程上。** 你可以安全地访问 `transform`, `GetComponent`, `Instantiate`以及其他 Unity API，而无需进行跨线程封送。
{% endhint %}

{% hint style="warning" %}
**使用 `invocation.ResolvedTarget` 和 `invocation.Definition` ——不要手动重新解析 `invocation.Command.Name` 或 `Command.Target` 手动解析。** 解析和规范化已经为你完成。自己解析原始字符串会绕过大小写不敏感匹配，并导致代码脆弱。
{% endhint %}

{% hint style="info" %}
**只要游戏玩法工作仍在进行，就让任务保持活跃。** 分发器会等待 `ExecuteAsync` 返回后才会开始下一步。对于寻路之类的长时间运行操作，请在执行器内部保持循环运行，直到工作真正完成。
{% endhint %}

***

## 结论

自定义执行器只是一个只有一个方法的 MonoBehaviour。实现 `IConvaiActionExecutor`，从以下位置获取目标： `invocation.ResolvedTarget.GameObjectReference`，完成你的工作，然后返回结果。对于长时间运行的行为，请使用 `异步`/`等待` 并检查 `CancellationToken` 以遵守超时和策略取消。


---

# 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/unity-plugin-beta-overview/features/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.
