> 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/vision/usage-examples.md).

# 视觉使用示例

这些示例涵盖了最常见的 Vision 集成模式。每个示例都是独立的——复制相关脚本，将其附加到相应的 GameObject，并在 Inspector 中配置序列化字段。

### 在安全培训中监控物体放置

一个安全培训应用程序，其中一个 Convai 角色监控用户是否将设备放置在正确区域，并提供语音反馈。该角色使用实时场景摄像头画面来观察放置情况。

**预期结果：** 当玩家移动物体时，角色会描述其位置，并确认放置是否正确，或标记出安全问题。

```csharp
using Convai.Modules.Vision;
using Convai.Runtime.Vision.Publishing;
using UnityEngine;

/// <summary>
/// 在培训序列开始时启用视觉，在完成时禁用。
/// 附加到与 ConvaiVisionPublisher 相同的 GameObject 上。
/// </summary>
public class SafetyTrainingVisionController : MonoBehaviour
{
    [SerializeField] private ConvaiVisionPublisher _publisher;

    void Awake()
    {
        // 以 Manual 模式开始，以便视觉仅在主动培训期间捕获
        _publisher.SetPublishPolicy(VisionPublishPolicy.Manual);
    }

    public void BeginTrainingSequence()
    {
        _publisher.SetPublishPolicy(VisionPublishPolicy.HighResponsiveness);
        _publisher.EnablePublishing(true);
    }

    public void EndTrainingSequence()
    {
        _publisher.EnablePublishing(false);
        _publisher.SetPublishPolicy(VisionPublishPolicy.Manual);
    }
}
```

### 在运行时选择摄像头设备

一个桌面入门应用程序，用户在会话开始前选择要使用的物理摄像头。当用户的工作站有多个摄像头（内置摄像头、USB 摄像头等）时非常有用。

**预期结果：** 下拉列表会在 Start 时填充所有检测到的摄像头名称。选择一个摄像头名称并点击 **Switch** 即可在不停止会话的情况下切换采集设备。

```csharp
using System.Collections.Generic;
using Convai.Runtime.Vision.Sources;
using TMPro;
using UnityEngine;

/// <summary>
/// 使用可用的摄像头名称填充 TMP_Dropdown，并按需切换设备。
/// 需要场景中存在 WebcamVisionFrameSource。
/// </summary>
public class WebcamSelectorUI : MonoBehaviour
{
    [SerializeField] private WebcamVisionFrameSource _webcamSource;
    [SerializeField] private TMP_Dropdown _deviceDropdown;

    private List<string> _deviceNames = new();

    async void Start()
    {
        _deviceNames = new List<string>(WebcamVisionFrameSource.GetAvailableDeviceNames());
        _deviceDropdown.ClearOptions();
        _deviceDropdown.AddOptions(_deviceNames);

        _deviceDropdown.onValueChanged.AddListener(async index =>
        {
            if (index >= 0 && index < _deviceNames.Count)
                await _webcamSource.SwitchWebcamAsync(_deviceNames[index]);
        });
    }
}
```

{% hint style="info" %}
`TMP_Dropdown` 需要 TextMeshPro 包。如果你的项目使用旧版 `UnityEngine.UI.Dropdown`，请替换 `TMP_Dropdown` 中，且 `Dropdown` — `AddOptions(List<string>)` ，其行为完全相同。
{% endhint %}

### 流式传输一个俯视安全摄像头

一个建筑导览应用程序，其中一个俯视安全摄像头监控整个平面图。发布器使用 `LowOverhead` 策略，因为场景变化较慢，带宽必须为音频保留。

**预期结果：** 当被询问时，角色会描述俯视图中可见的内容——家具布局、占用情况或危险。

将俯视摄像头分配给 **Target Camera** 字段，在 `CameraVisionFrameSource` 组件的 Inspector 中。 `CameraVisionFrameSource.TargetCamera` 有一个 private setter，因此脚本不能在运行时将帧源重新指向另一台摄像头——Inspector 字段是选择其读取哪台摄像头的唯一受支持方式。

```csharp
using Convai.Modules.Vision;
using Convai.Runtime.Vision.Publishing;
using UnityEngine;

/// <summary>
/// 为一个场景配置发布策略，其中俯视安全摄像头
/// 已在 Inspector 中分配到 CameraVisionFrameSource 的 Target Camera 字段。
/// 附加到 ConvaiVisionRoot GameObject。
/// </summary>
public class SecurityCameraVisionSetup : MonoBehaviour
{
    [SerializeField] private ConvaiVisionPublisher _publisher;

    void Awake()
    {
        // Low-overhead 策略：5 fps，350 kbps——适用于缓慢变化的场景
        _publisher.SetPublishPolicy(VisionPublishPolicy.LowOverhead);
    }
}
```

### 在玩家注视时激活发布

连续流式传输视觉内容成本很高。此模式仅在玩家看向特定物体（例如一台机械设备）时激活发布，否则暂停。

**预期结果：** 只有当玩家看着物体时，角色才会对其状态作出响应。当玩家移开视线时，网络和 GPU 开销为零。

```csharp
using Convai.Modules.Vision;
using Convai.Runtime.Vision.Publishing;
using UnityEngine;

/// <summary>
/// 当玩家看向指定物体时激活视觉发布。
/// 附加到目标物体。ConvaiVisionPublisher 必须设置为 Manual 策略。
/// </summary>
public class LookAtVisionTrigger : MonoBehaviour
{
    [SerializeField] private ConvaiVisionPublisher _publisher;
    [SerializeField] private Camera _playerCamera;
    [SerializeField] private float _maxViewAngle = 15f;

    void Awake()
    {
        _publisher.SetPublishPolicy(VisionPublishPolicy.Manual);
    }

    void Update()
    {
        Vector3 directionToObject = (transform.position - _playerCamera.transform.position).normalized;
        float angle = Vector3.Angle(_playerCamera.transform.forward, directionToObject);
        bool isLooking = angle < _maxViewAngle;

        if (isLooking && !_publisher.IsPublishing)
            _publisher.EnablePublishing(true);
        else if (!isLooking && _publisher.IsPublishing)
            _publisher.EnablePublishing(false);
    }
}
```

### 为 WebGL 配置 Vision

在 WebGL 中，不需要帧源组件。 `ConvaiVisionPublisher` 会通过 `canvas.captureStream()`自动捕获浏览器画布。设置 **Connection Type** 到 **Video** 以及发布策略即可——其余一切都会自动完成。

**预期结果：** 角色会接收到浏览器画布的实时画面。场景中没有帧源组件。

```csharp
using Convai.Modules.Vision;
using Convai.Runtime.Vision.Publishing;
using UnityEngine;

/// <summary>
/// WebGL 专用设置。无需帧源——发布器使用 canvas.captureStream()。
/// 附加到与 ConvaiVisionPublisher 相同的 GameObject 上。
/// 在 Play 之前将 ConvaiRoomManager.ConnectionType 设置为 Video。
/// </summary>
public class WebGLVisionSetup : MonoBehaviour
{
    [SerializeField] private ConvaiVisionPublisher _publisher;

    void Awake()
    {
        // LowOverhead 适用于 WebGL——画布捕获上限为 15 fps
        _publisher.SetPublishPolicy(VisionPublishPolicy.LowOverhead);
    }
}
```

{% hint style="danger" %}
**WebGL 上需要 HTTPS。** 当前交互目标的 `canvas.captureStream()` 浏览器会阻止非 HTTPS 来源上的 API。在生产环境中测试 Vision 之前，请将你的 WebGL 构建部署到 HTTPS 主机。 `http://localhost` 是唯一的例外。
{% endhint %}

### 按需触发视觉并调整响应模式

前面的示例都通过 `ConvaiVisionPublisher`持续发布帧。 `IConvaiRoomConnectionService` 在

`RequestVisionStatus()` 会要求后端报告会话帧缓冲区的状态。 `TriggerVision(ConvaiVisionTriggerRequest)` 会要求后端将缓冲帧附加到下一轮，并根据请求的响应模式调用模型。 `UpdateRespondMode(ConvaiRespondModeLane, ConvaiRespondMode)` 会改变整个输入通道在本次会话剩余时间里如何影响角色的发言。这三者都会通过领域事件异步确认，而不是通过返回值——参见 [Vision 脚本 API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/vision/scripting-api.md) 以获取完整的方法签名、请求字段和事件负载。

**预期结果：** 调用 `RequestLookNow()` 会记录缓冲区状态，然后角色会描述发生了什么变化并说出答案。调用 `SilenceVisionUntilAsked()` 会阻止角色对新的视觉帧做出反应，直到 `RequestLookNow()` 再次被调用。

```csharp
using Convai.Domain.DomainEvents.Vision;
using Convai.Domain.EventSystem;
using Convai.Runtime;
using Convai.Runtime.Components;
using Convai.Runtime.Room;
using Convai.Runtime.Vision.Context;
using UnityEngine;

/// <summary>
/// 在发布器常规帧节奏之外按需请求一次视觉检查，
/// 然后报告缓冲区状态和触发结果。附加到场景中的任意位置。
/// </summary>
public class OnDemandVisionInspector : MonoBehaviour
{
    private IConvaiRoomConnectionService _roomService;
    private SubscriptionToken _statusToken;
    private SubscriptionToken _triggerToken;

    void OnEnable()
    {
        ConvaiManager manager = ConvaiManager.ActiveManager;
        if (manager == null || !manager.TryGetRoomConnectionService(out _roomService))
            return;

        if (!manager.TryGetEventHub(out IEventHub hub))
            return;

        _statusToken = hub.Subscribe<VisionContextStatusReceived>(OnVisionStatus, EventDeliveryPolicy.MainThread);
        _triggerToken = hub.Subscribe<VisionContextTriggerReceived>(OnVisionTriggerAck, EventDeliveryPolicy.MainThread);
    }

    void OnDisable()
    {
        if (ConvaiManager.ActiveManager == null || !ConvaiManager.ActiveManager.TryGetEventHub(out IEventHub hub))
            return;

        hub.Unsubscribe(_statusToken);
        hub.Unsubscribe(_triggerToken);
    }

    // 从“立即查看”UI 按钮调用
    public void RequestLookNow()
    {
        _roomService?.RequestVisionStatus();
        _roomService?.TriggerVision(new ConvaiVisionTriggerRequest
        {
            Text = "自上次查看后工作台上有什么变化？",
            RespondMode = ConvaiRespondMode.MustRespond
        });
    }

    // 调用一次，例如当玩家进入一个视觉响应会造成干扰的区域时
    public void SilenceVisionUntilAsked()
    {
        _roomService?.UpdateRespondMode(ConvaiRespondModeLane.Vision, ConvaiRespondMode.Silent);
    }

    private void OnVisionStatus(VisionContextStatusReceived status)
    {
        Debug.Log($"[Vision] {status.Outcome} — 上一帧年龄：{status.LastFrameAgeMs} ms（来源：{status.ActiveSourceLabel}）");
    }

    private void OnVisionTriggerAck(VisionContextTriggerReceived ack)
    {
        if (ack.Downgraded)
            Debug.Log($"[Vision] 触发降级为 {ack.ActualRespondMode}：{ack.DowngradeReason}");
        else if (ack.LlmTriggered)
            Debug.Log($"[Vision] 角色使用了 {ack.FramesAttached} 帧进行响应。");
    }
}
```

`RequestLookNow()` 上面的代码构建了一个新的 `ConvaiVisionTriggerRequest` ，没有显式 `UpdateId` ，因此在一次确认丢失后再次调用会发送一个不同的触发，而不是安全重放——每次调用都会获得自己生成的 ID。要让重试具备幂等性，请预先生成一个 `UpdateId` ，将其传递给构造函数，并在重试之间复用相同的值；然后后端会针对该 ID 重放原始确认，而不是再次触发。参见 [Vision 脚本 API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/vision/scripting-api.md#convaivisiontriggerrequest) 以了解构造函数签名。

### 下一步

{% content-ref url="/pages/9ee175b12da238182d707abd778abdc3a8706c80" %}
[视觉脚本 API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/vision/scripting-api.md)
{% endcontent-ref %}

{% content-ref url="/pages/86612bc613c5a299a468b71b3fb8a40e625ee1fe" %}
[排查视觉问题](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/vision/troubleshooting-and-diagnostics.md)
{% endcontent-ref %}

{% content-ref url="/pages/cb7758289bc48a93210bce51933fd819fd05e245" %}
[自定义帧源](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/vision/custom-frame-sources.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/vision/usage-examples.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.
