For the complete documentation index, see llms.txt. This page is also available as Markdown.

多角色使用示例

实现视线指向称呼目标,以及脚本化名册切换,使 Unity 场景管理玩家在房间中对哪个角色进行称呼。

两个经过验证的模式展示了在房间连接后,应用程序如何驱动多角色会话:按玩家所看方向路由玩家输入,以及在场景进行中将一个角色替换为另一个角色,同时始终不让房间失去有效目标。这两种模式都是基于公开连接 API 编写的应用代码——都不属于 SDK 的一部分。

两个示例都假定已连接的多角色会话中具有 IConvaiRoomConnectionService.CurrentMultiCharacterSession 已填充。参见 构建你的第一个多角色会话 如果房间尚未连接。

通过注视定向

上下文: 一个场景中有多个角色站在不同位置。玩家应当对当前正面对的那个角色说话,而不希望每次轻微转头都会在角色回答到一半时打断对话。

该模式所强制执行的规则

一个朝向摄像机前方的射线检测会识别玩家当前正面对的角色。短暂的宽限期会对结果进行防抖,因此短暂地把视线移开不会立刻改变交互目标。在切换之前,脚本会检查当前持有目标的角色是否仍在说话,并将切换推迟到该轮结束后。

实现

Assets/Scripts/LookToAddressTargeting.cs
using System;
using System.Threading;
using Convai.Runtime.Components;
using Convai.Runtime.Room;
using UnityEngine;

public class LookToAddressTargeting : MonoBehaviour
{
    [SerializeField] private Camera _lookCamera;
    [SerializeField] private LayerMask _characterLayerMask;
    [SerializeField] private float _maxLookDistance = 10f;
    [SerializeField] private float _lookAwayGracePeriod = 1.5f;

    private IConvaiRoomConnectionService _roomService;
    private ConvaiCharacter _addressedCharacter;
    private ConvaiCharacter _candidateCharacter;
    private float _candidateStableSince;
    private bool _switchInFlight;

    private readonly CancellationTokenSource _lifetime = new();

    public void Attach(IConvaiRoomConnectionService roomService, ConvaiCharacter initialTarget)
    {
        _roomService = roomService;
        _addressedCharacter = initialTarget;
    }

    private void Update()
    {
        if (_roomService?.CurrentMultiCharacterSession == null) return;

        ConvaiCharacter looked = ResolveLookedAtCharacter();
        if (looked != _candidateCharacter)
        {
            _candidateCharacter = looked;
            _candidateStableSince = Time.time;
        }

        if (_candidateCharacter == null || _candidateCharacter == _addressedCharacter) return;
        if (Time.time - _candidateStableSince < _lookAwayGracePeriod) return;

        TrySwitchTarget(_candidateCharacter);
    }

    private ConvaiCharacter ResolveLookedAtCharacter()
    {
        if (!Physics.Raycast(_lookCamera.transform.position, _lookCamera.transform.forward,
                out RaycastHit hit, _maxLookDistance, _characterLayerMask))
            return null;

        return hit.collider.GetComponentInParent<ConvaiCharacter>();
    }

    private async void TrySwitchTarget(ConvaiCharacter target)
    {
        if (_switchInFlight) return;
        if (_addressedCharacter != null && _addressedCharacter.IsSpeaking) return;

        _switchInFlight = true;
        try
        {
            InteractionTargetResult result = await _roomService.SetInteractionTargetAsync(target, _lifetime.Token);
            if (result.Changed) _addressedCharacter = target;
        }
        catch (InvalidOperationException error)
        {
            Debug.LogError($"[MultiCharacter] Could not switch target: {error.Message}");
        }
        catch (ArgumentException error)
        {
            Debug.LogError($"[MultiCharacter] {error.Message}");
        }
        catch (TimeoutException error)
        {
            Debug.LogError($"[MultiCharacter] {error.Message}");
        }
        finally
        {
            _switchInFlight = false;
        }
    }

    private void OnDestroy()
    {
        _lifetime.Cancel();
        _lifetime.Dispose();
    }
}

ConvaiCharacter.IsSpeaking 以及 OnSpeechStarted/OnSpeechStopped 这些事件的作用域限定为 SDK 将语音事件解析到的那个角色实例,因此检查 IsSpeaking 这里,即使房间里有它的两个克隆体,也能读取到正确的角色。 Update 每一帧都会重新评估,因此一旦被指向的角色停止说话,下一帧的检查就会通过,并且对新候选者的待处理切换会在无需额外连线的情况下继续执行。

预期结果

当玩家持续注视某个角色达到 _lookAwayGracePeriod 秒时, SetInteractionTargetAsync 会把交互目标移动到该角色,并 InteractionTargetResult.Changed 返回 true. 在回答过程中若玩家把视线移到另一个角色身上,不会打断当前正在说话的那个角色——切换会等到 IsSpeakingfalse 之后才执行。

场景进行中的脚本化成员替换

上下文: 一个训练场景中,某个角色会在中途离开,随后另一个角色接手,成为玩家正在对话的人——例如,主管离开,而安全培训员继续进行会话。

该模式所强制执行的规则

在发生其他变化之前,先将新进入的角色加入名单并给予时间到达 就绪 ,因此切换时绝不会把输入路由给一个还不能响应的角色。然后将离开的角色移除,并将 replacementTargetMembershipId 设为新进入角色的成员身份,因此移除和目标变更会在同一个命令中完成,房间也不会在任何时刻失去有效的交互目标。

实现

预期结果

AddCharacterAsync 返回新进入角色的新成员身份,并且 WaitForReadyAsyncCharacterStatusChanged 将其报告为 就绪失败. RemoveCharacterAsync 然后移除离开的成员身份,并在同一个已确认的命令中,把 ActiveMembershipId 切换到新进入的成员身份—— CharacterRosterUpdateResult.ActiveMembershipId 会直接报告新的目标,无需单独调用 SetInteractionTargetAsync 。在整个切换过程中,名单始终至少保留一个成员。

从调用方的角度看,这个命令是原子的,但从事件订阅者的角度看并非如此。SDK 会先应用移除,再应用新的目标,因此 InteractionTargetChanged 会触发两次:一次是在 随后第二个事件触发。响应 当前 null 时,随后又在新进入的成员身份上触发一次。响应某个 null 目标的代码,例如通过调暗界面来处理,应当对其进行防抖,而不是把它当作对话结束。

下一步

在运行时添加和移除角色切换交互目标排查多角色会话问题

最后更新于

这有帮助吗?