多角色使用示例
实现视线指向称呼目标,以及脚本化名册切换,使 Unity 场景管理玩家在房间中对哪个角色进行称呼。
两个经过验证的模式展示了在房间连接后,应用程序如何驱动多角色会话:按玩家所看方向路由玩家输入,以及在场景进行中将一个角色替换为另一个角色,同时始终不让房间失去有效目标。这两种模式都是基于公开连接 API 编写的应用代码——都不属于 SDK 的一部分。
通过注视定向
上下文: 一个场景中有多个角色站在不同位置。玩家应当对当前正面对的那个角色说话,而不希望每次轻微转头都会在角色回答到一半时打断对话。
该模式所强制执行的规则
一个朝向摄像机前方的射线检测会识别玩家当前正面对的角色。短暂的宽限期会对结果进行防抖,因此短暂地把视线移开不会立刻改变交互目标。在切换之前,脚本会检查当前持有目标的角色是否仍在说话,并将切换推迟到该轮结束后。
实现
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. 在回答过程中若玩家把视线移到另一个角色身上,不会打断当前正在说话的那个角色——切换会等到 IsSpeaking 是 false 之后才执行。
场景进行中的脚本化成员替换
上下文: 一个训练场景中,某个角色会在中途离开,随后另一个角色接手,成为玩家正在对话的人——例如,主管离开,而安全培训员继续进行会话。
该模式所强制执行的规则
在发生其他变化之前,先将新进入的角色加入名单并给予时间到达 就绪 ,因此切换时绝不会把输入路由给一个还不能响应的角色。然后将离开的角色移除,并将 replacementTargetMembershipId 设为新进入角色的成员身份,因此移除和目标变更会在同一个命令中完成,房间也不会在任何时刻失去有效的交互目标。
实现
先添加新进入的角色,再移除离开的角色。先移除,即使在后续调用中排队了替代目标,也会在短暂时间内让名单中缺少你打算把对话交给的那个角色。
预期结果
AddCharacterAsync 返回新进入角色的新成员身份,并且 WaitForReadyAsync 在 CharacterStatusChanged 将其报告为 就绪 或 失败. RemoveCharacterAsync 然后移除离开的成员身份,并在同一个已确认的命令中,把 ActiveMembershipId 切换到新进入的成员身份—— CharacterRosterUpdateResult.ActiveMembershipId 会直接报告新的目标,无需单独调用 SetInteractionTargetAsync 。在整个切换过程中,名单始终至少保留一个成员。
从调用方的角度看,这个命令是原子的,但从事件订阅者的角度看并非如此。SDK 会先应用移除,再应用新的目标,因此 InteractionTargetChanged 会触发两次:一次是在 随后第二个事件触发。响应 当前 null 时,随后又在新进入的成员身份上触发一次。响应某个 null 目标的代码,例如通过调暗界面来处理,应当对其进行防抖,而不是把它当作对话结束。
下一步
在运行时添加和移除角色切换交互目标排查多角色会话问题最后更新于
这有帮助吗?