交互系统:先校验,再执行
玩家按键后,系统通过 Raycast 命中交互层物体,收集目标上的 IInteractable,并在真正调用 Interact() 前检查父级所有条件组件。任一条件失败,交互中止,失败原因可以进入 UI 或日志。
命中可交互物体。
逐一执行 CanInteract,包括道具需求、解锁状态和门禁条件。
通过后调用目标行为,例如开门、切换机关或拾取物品。
成功后触发消耗道具、解锁状态、播放旁白或写入 Facts。
这条链路让 AI 即使提出“开门”,也必须走到同样的系统门槛中,而不是绕过玩家交互规则。
真实代码节选整理:玩家按下交互键后,射线命中对象,先收集交互组件,再逐个检查父级条件,全部通过后才调用 Interact()。
private void PerformInteract()
{
if (cameraTransform == null) return;
Ray ray = new Ray(cameraTransform.position, cameraTransform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, interactRange, interactLayer))
{
var interactables = new List<IInteractable>(8);
var unique = new HashSet<IInteractable>();
foreach (var it in hit.collider.GetComponents<IInteractable>())
if (it != null && unique.Add(it)) interactables.Add(it);
foreach (var it in hit.collider.GetComponentsInParent<IInteractable>())
if (it != null && unique.Add(it)) interactables.Add(it);
if (interactables.Count <= 0) return;
var conditions = hit.collider.GetComponentsInParent<InteractConditionBehaviour>();
for (int i = 0; i < conditions.Length; i++)
{
if (conditions[i] != null
&& !conditions[i].CanInteract(gameObject, out string failReason))
{
var failedTriggers =
hit.collider.GetComponentsInParent<Core.Triggers.InteractFailedTrigger>();
for (int t = 0; t < failedTriggers.Length; t++)
failedTriggers[t]?.OnFailed(failReason);
return;
}
}
for (int i = 0; i < interactables.Count; i++)
interactables[i]?.Interact();
for (int i = 0; i < conditions.Length; i++)
conditions[i]?.OnInteractSucceeded(gameObject);
}
}
任务系统:把叙事阶段变成可见目标
TaskSystem 使用 TaskData 定义静态任务,用 TaskEntry 保存运行时状态。任务状态包括 Todo、Completed 和 Expired,TaskService 负责添加、更新、移除与广播变化。
任务 ID、短标题、描述文本,适合做成 ScriptableObject 资产。
运行时状态、加入时间、超时时间与计划移除时间。
监听 TaskService 事件,刷新任务列表与详情 UI。
AI 的命令负责提出任务更新请求,实际状态变化仍由 TaskService 统一处理。UI、存档和剧情触发器因此读到同一个状态源。
真实代码节选:TaskCommandReceiver 不直接改 UI,而是把命令统一落到 TaskService。
public override bool TryExecute(LLMCommand command)
{
if (command == null) return false;
if (!string.Equals(command.type, "Task", System.StringComparison.OrdinalIgnoreCase))
return false;
TaskService service = taskService != null ? taskService : TaskService.Instance;
if (service == null) return false;
string taskId = string.IsNullOrWhiteSpace(command.payload)
? command.targetId
: command.payload;
if (string.IsNullOrWhiteSpace(taskId)) return false;
if (string.Equals(command.action, "Remove", System.StringComparison.OrdinalIgnoreCase))
return service.RemoveTask(taskId);
if (string.Equals(command.action, "Todo", System.StringComparison.OrdinalIgnoreCase))
return service.SetTaskStatus(taskId, TaskStatus.Todo);
if (string.Equals(command.action, "Expired", System.StringComparison.OrdinalIgnoreCase))
return service.SetTaskStatus(taskId, TaskStatus.Expired);
return service.SetTaskStatus(taskId, TaskStatus.Completed);
}
旁白系统:给剧情一个可控窗口
旁白系统用 ScriptableObject 保存段落式叙事内容,每一页都可以绑定进入事件或继续事件。播放期间可以锁定玩家控制,让关键剧情节点有稳定的呈现时机。
在《回声》中,旁白承担两类工作:一是醒来、恢复供电、接近真相等阶段性叙事;二是在关键交互后呈现那些不应完全交给 ECHO-7 解释的信息。它和 LLM 的关系不是替代,而是分工:AI 负责动态回应,旁白负责确定节拍。
结局系统:门槛裁决和演出分离
结局系统由触发器、门槛服务和演出控制器组成。玩家是否能触发结局,不由碰撞区域单独决定,而由 EndingWinConditionService 维护的状态判断。
任务、对话或剧情节点调用 UnlockEnding,开放通关门槛。
玩家进入出口触发区,系统检查当前是否允许触发。
锁定输入,黑屏传送,控制门与光,最后展示结算 UI。
这让“说服 ECHO-7”不会停留在台词层面。只有当任务状态、Facts 和门槛条件真正满足,系统才会把流程收束到结局演出。
真实代码节选:触发器只负责接收碰撞,是否进入结局由 EndingWinConditionService 裁决。
private void OnTriggerEnter(Collider other)
{
if (hasTriggered) return;
if (other.CompareTag(playerTag))
{
TriggerSequence();
}
}
public void TriggerSequence()
{
if (hasTriggered) return;
hasTriggered = true;
if (!allowManualTriggerWhenLocked && requireWinConditionToTrigger)
{
var wc = EndingWinConditionService.EnsureInstance();
if (wc != null && !wc.CanTriggerEnding)
{
hasTriggered = false;
return;
}
}
sequenceController?.StartEndingSequence();
}
public void SetCanTriggerEnding(bool value)
{
if (canTriggerEnding == value) { return; }
canTriggerEnding = value;
CanTriggerEndingChanged?.Invoke(canTriggerEnding);
}