Files
Echo/Assets/_Project/Docs/EndingSystem_Technical_Reference.md
2026-07-08 20:42:51 +08:00

13 KiB
Raw Permalink Blame History

结局动画系统(Ending System)技术说明

目标:在游戏最后阶段,提供一套完全脱离玩家操作的序列化演出机制,实现黑屏传送、平滑抬升镜头、调整视野聚焦、控制特定对象(如开门与灯光)以及最后的白屏淡出和结算界面显示。

一、系统架构与组件清单

所有的系统代码均位于 Assets/_Project/Scripts/EndingSystem 目录下。

1. EndingSequenceController.cs

核心导演脚本,负责控制整个结局动画的时间线与并行动作。

  • 依赖引用:
    • Black Screen / White Screen CanvasGroup:用于控制屏幕淡出淡入的 UI 层。
    • Player Body / Camera Pivot / Camera:用于移动玩家、抬头旋转(利用 Pivot 防止与身体冲突)、控制 FOV。
    • Target Player Transform:黑屏最暗时玩家被传送的目的地标识。
    • Ending UI Panel:序列最后激活的结算/返回主菜单面板。
  • DOTween 增强:如果项目内定义了 DOTWEEN,脚本将自动采用 DG.Tweening.Sequence 来保证多轨动画的同步执行与曲线平滑度;否则退回到原生的 Coroutine
  • 关键机制:锁定输入
    • 开始序列时,系统调用 PlayerControlLockService.Instance.Lock(this) 禁用玩家走动、跳跃、按键交互与鼠标旋转。
    • 结束序列时(激活 UI 时),调用 Unlock(this) 并在代码中强制设置 Cursor.lockState = CursorLockMode.NoneCursor.visible = true 以便玩家点击结算 UI 上的按钮。

2. EndingDoorController.cs

专用于控制双开门平滑打开动画。

  • 功能特性:
    • 将开门拆分为“开小缝” -> “停顿” -> “完全打开”三个阶段,用于营造悬疑感。
    • 支持左右两扇门自动镜像旋转(mirrorRightDoor)。
    • 支持通过 DOTween 驱动以获取更好的曲线(Ease.InOutSine 等)。

3. EndingLightController.cs

专用于在开门的同时,控制门后的灯光从全黑平滑渐亮。

  • 机制:在 Awake() 时自动将目标光源强度设为 0,当被 EndingSequenceController 调用时,在指定时间内渐变至目标强度。

4. EndingTrigger.cs

用于触发 EndingSequenceController.StartEndingSequence()

  • 支持方式
    • 碰撞触发:勾选 triggerOnCollision 后,一旦带有指定 Player Tag 的物体进入 BoxCollider 触发器便会自动调用。
    • 手动触发(按钮):可通过按钮点击直接调用 TriggerSequence() 方法。
  • 自适应绑定:如果你忘记在 Inspector 拖拽 EndingSequenceController 引用,此脚本会在 Awake/OnValidate 时自动从父节点或当前场景中寻找对应的组件,避免空引用报错。
  • 通关条件门禁(新增)
    • requireWinConditionToTrigger 勾选时:只有当 EndingWinConditionService.CanTriggerEnding == true 才允许“碰撞触发”启动结局演出
    • allowManualTriggerWhenLocked 勾选时:TriggerSequence()(按钮/脚本手动调用)会绕过门禁,用于开发调试

5. EndingWinConditionService.cs

用于集中管理“是否允许触发通关结局”的全局变量(默认 false)。

  • 目的:把“玩家撞到出口触发器是否能通关”从硬编码变成可被任务/剧情/条件系统写入的状态。
  • 关键 API
    • EndingWinConditionService.EnsureInstance():确保服务存在(场景没放也会自动创建)
    • SetCanTriggerEnding(bool) / UnlockEnding() / LockEnding():从其它系统修改通关条件
    • CanTriggerEnding:读取当前是否允许触发结局

6. EndingWinConditionDebugButton.cs

用于“调试按钮”把 canWin 置为 true/false(不负责触发结局,只负责改变量)。

  • 用途:开发期快速解锁出口门禁;后期删除该 Button(或整个 Debug UI)不会影响通关逻辑,也不会报错。
  • 用法:把该脚本挂在任意 GameObject(通常就是你的 Debug Button 对象),在 Button OnClick 里绑定:
    • EndingWinConditionDebugButton.UnlockEnding()(解锁通关)
    • EndingWinConditionDebugButton.LockEnding()(重新上锁)

二、场景配置与接入步骤

1. 构建导演与标记物

  1. 在场景中创建一个空物体 _EndingSystem,重置位置为 (0,0,0)。
  2. 添加 EndingSequenceController 脚本。
  3. 创建子物体 TargetPlayerPos,放到你希望黑屏后主角出现的位置,并朝向门。将它拖给 Target Player Transform

2. 准备屏幕渐变遮罩(Black & White

  1. 在现有的 UI Canvas(或新建 CanvasSort Order 调高以覆盖其他 UI)下,创建两个全屏的 ImageAnchor = Stretch/StretchLeft/Top/Right/Bottom = 0)。
  2. 将一个命名为 EndingBlackScreen(颜色纯黑),另一个命名为 EndingWhiteScreen(颜色纯白)。
  3. 给它们分别添加 CanvasGroup 组件,并取消勾选 InteractableBlocks Raycasts
  4. 将它们拖给 EndingSequenceController 中对应的 Black Screen Canvas GroupWhite Screen Canvas Group 槽位。

3. 准备最终结算 UI

  1. 在 Canvas 中准备好你的返回主菜单/退出游戏面板(如 EndingUIPanel),该面板不要使用自动拦截输入的逻辑,只需包含纯粹的 UI 元素和 Reach Button。
  2. 将其拖入 EndingSequenceControllerEnding UI Panel 槽位。序列开始时会自动将它隐藏,并在最终白屏完成后激活。
  3. EndingUIPanel(或其根节点)上挂载 EndingUIScreenController.cs
    • Scene
      • Main Menu Scene Build Index 设为 1(你的主菜单场景 index=1)。
      • Begin Preload On Enable 保持勾选:UI 激活时会后台异步预加载 index=1,并 allowSceneActivation=false,等待点击返回按钮才切换。
    • Buttons
      • 把“返回主菜单”按钮拖到 Return To Menu ButtonReach Button)或 Return To Menu Button UGUIUnity Button)其中一个槽位即可。
    • UI
      • 把 AI 总结文案的 TMP_Text 拖到 Summaray Text(仅用于显示 AI 总结/Debug 文案)。
      • 把结局状态提示的 TMP_Text 拖到 Information Text(用于显示“成功逃离/倒下”等提示词)。
    • Information Text
      • Outcome 选择结局类型:Escaped(成功逃离)或 Fainted(倒下/昏倒)。
      • Escaped Information String / Fainted Information String 分别填写两套提示词。
      • 勾选 Apply Outcome Text On EnableUI 激活时会按 Outcome 自动刷新 Information Text
    • AI Summary
      • Request AI Summary On Enable 勾选:UI 激活时会用当前激活的 LLM 服务发起一次“无状态总结”请求,并把结果填入 Summaray Text
      • Chat Manager 可不填(会在场景里自动查找 LLMChatManager),但建议显式拖拽,避免运行时找错对象。
      • Max History Messages 控制发给 AI 的历史对话条数上限;Include Known Facts From Context Memory 会把记忆系统的 facts 一并塞进 Prompt。
    • AI Summary Debug
      • 预留 Debug Summary Button / Debug Summary Button UGUIDebug Summary String:点击按钮可直接把预设字符串写进 Summaray Text,用于在未接入/未联网时调试 UI 流程。

4. 配置场景对象(门与光)

  1. 将门物体挂载 EndingDoorController 并绑定左右两扇门。
  2. 在门后放置一盏 Light,挂载 EndingLightController 并将灯光引用赋给它。
  3. 将这两个控制器拖拽到 EndingSequenceController 中对应的 Door ControllerLight Controller 槽位。

5. 绑定玩家与相机

EndingSequenceController 脚本上:

  • Player Body:拖入玩家预制体的最外层(带有 CharacterController 的物体)。
  • Player Camera Pivot:拖入用于上下旋转的相机支架(Camera 的父物体)。
  • Player Camera:拖入主相机(带有 Camera 组件的物体)。

6. 放置触发区域

  1. 在场景内玩家走向结局的必经之路上放一个带 BoxCollider 的空物体(开启 Is Trigger)。
  2. 添加 EndingTrigger 脚本。该脚本会自动找到场景里的 EndingSequenceController
  3. 如果是测试阶段,推荐在 UI 上建一个 Button,并绑定 EndingWinConditionDebugButton.UnlockEnding() 来把 canWin 设为 true(而不是直接触发结局)。
  4. 如果你希望“必须先满足通关条件才触发”:
    • EndingTrigger 上勾选 requireWinConditionToTrigger
    • 确保你的流程在合适的时机调用一次 EndingWinConditionService.Instance.UnlockEnding()(或 SetCanTriggerEnding(true)

7. 通关条件的典型接入点(推荐)

  • 任务驱动:当关键任务状态变为 Completed 时,调用 EndingWinConditionService.Instance.UnlockEnding()
  • 解锁状态驱动:当出口门锁解除/钥匙使用成功时,调用 UnlockEnding()
  • 剧情节点驱动:当对话/动画结束回调触发时,调用 UnlockEnding(),然后让玩家走到出口触发器完成收尾演出

三、时序总结(DOTween/Coroutine 流程)

  1. 玩家触发结局,调用 StartEndingSequence(),锁死输入。
  2. 黑屏渐入fadeToBlackDuration)。
  3. 玩家被传送TargetPlayerPos,相机俯仰角强制回正。
  4. 稍作等待(delayBeforeFadeFromBlack)后,黑屏渐出恢复画面。
  5. 并行执行
    • 镜头缓缓抬升(cameraLookDuration,目标角度 cameraLookUpAngle)。
    • 画面慢慢放大,FOV变小聚焦到门上(fovShrinkDuration,目标FOV targetFOV)。
  6. 门缝动画启动:门开一条缝 -> 停顿 -> 大开。
  7. 灯光渐亮:门缝开启的同时门后强光亮起。
  8. 经过一段设定好的等待时间后(开门耗时 + delayBeforeFadeToWhite),白屏渐入fadeToWhiteDuration)。
  9. 彻底白屏后,系统激活 Ending UI Panel,并解除输入锁定、显式释放出鼠标指针供玩家点击界面;结算 UI 内部可通过 EndingUIScreenController 预加载并返回主菜单,以及生成/调试结局总结文案。

四、黑屏/白屏不生效排错清单(重要)

你描述的“黑屏遮罩似乎没效果”,常见原因不是代码逻辑,而是场景里有别的系统在抢写 alpha / 直接 SetActive(false) / UI 排序被盖住

1) 遮罩对象被别的系统关掉(SetActive(false)

  • 工程内的开场苏醒系统 AwakeningSequenceController.cs 在“继续游戏”分支会把它引用的黑屏对象 SetActive(false)
  • 解决:
    • 最推荐:开场与结局使用不同的全屏遮罩对象(不要共用同一个 CanvasGroup)。
    • 当前脚本已做防御:结局开始时会强制把遮罩 SetActive(true),并把它置顶。

2) 你给遮罩挂了第三方 Fade 组件导致抢写 alpha

如果 EndingBlackScreen/EndingWhiteScreen 上同时挂了以下组件,它们会在协程里持续改 alpha,从而和结局 tween 拉扯:

  • Michsky.UI.Reach.CanvasGroupAnimator
  • Michsky.UI.Reach.ImageFading
  • Unity Animator(带动画曲线驱动 alpha

解决:

  • 不要把这些组件挂在结局遮罩上;或只保留一个系统负责控制 alpha。
  • 当前脚本已做防御:结局开始时会自动禁用以上组件,避免抢写。

3) UI 排序问题(遮罩在 UI 里但被别的 Canvas 盖住)

解决(推荐做法):

  1. 给结局遮罩单独做一个 Overlay Canvas(或者确保它所在 Canvas 置顶)。
  2. EndingSequenceController.csOverlay Safety 中:
    • Override Canvas Sorting 勾选
    • Overlay Sorting Order 设为一个足够大的值(默认 5000
    • Bring Screen To Front 勾选

4) “提示闪烁”脚本误绑定到遮罩 CanvasGroup

PressToContinue 之前存在兜底 FindObjectOfType<CanvasGroup>(true) 的逻辑,可能会误抓到全屏遮罩。 当前已修复:它只会使用 FeedNotification 自己身上的 CanvasGroup,不会再影响结局遮罩。