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

159 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 结局动画系统(Ending System)技术说明
目标:在游戏最后阶段,提供一套完全脱离玩家操作的序列化演出机制,实现黑屏传送、平滑抬升镜头、调整视野聚焦、控制特定对象(如开门与灯光)以及最后的白屏淡出和结算界面显示。
## 一、系统架构与组件清单
所有的系统代码均位于 `Assets/_Project/Scripts/EndingSystem` 目录下。
### 1. [EndingSequenceController.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/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.None``Cursor.visible = true` 以便玩家点击结算 UI 上的按钮。
### 2. [EndingDoorController.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/EndingDoorController.cs)
专用于控制双开门平滑打开动画。
- **功能特性:**
- 将开门拆分为“开小缝” -> “停顿” -> “完全打开”三个阶段,用于营造悬疑感。
- 支持左右两扇门自动镜像旋转(`mirrorRightDoor`)。
- 支持通过 DOTween 驱动以获取更好的曲线(`Ease.InOutSine` 等)。
### 3. [EndingLightController.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/EndingLightController.cs)
专用于在开门的同时,控制门后的灯光从全黑平滑渐亮。
- **机制**:在 `Awake()` 时自动将目标光源强度设为 0,当被 `EndingSequenceController` 调用时,在指定时间内渐变至目标强度。
### 4. [EndingTrigger.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/EndingTrigger.cs)
用于触发 `EndingSequenceController.StartEndingSequence()`
- **支持方式**
- **碰撞触发**:勾选 `triggerOnCollision` 后,一旦带有指定 `Player` Tag 的物体进入 `BoxCollider` 触发器便会自动调用。
- **手动触发(按钮)**:可通过按钮点击直接调用 `TriggerSequence()` 方法。
- **自适应绑定**:如果你忘记在 Inspector 拖拽 `EndingSequenceController` 引用,此脚本会在 `Awake/OnValidate` 时自动从父节点或当前场景中寻找对应的组件,避免空引用报错。
- **通关条件门禁(新增)**
- `requireWinConditionToTrigger` 勾选时:只有当 `EndingWinConditionService.CanTriggerEnding == true` 才允许“碰撞触发”启动结局演出
- `allowManualTriggerWhenLocked` 勾选时:`TriggerSequence()`(按钮/脚本手动调用)会绕过门禁,用于开发调试
### 5. [EndingWinConditionService.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/EndingWinConditionService.cs)
用于集中管理“是否允许触发通关结局”的全局变量(默认 false)。
- **目的**:把“玩家撞到出口触发器是否能通关”从硬编码变成可被任务/剧情/条件系统写入的状态。
- **关键 API**
- `EndingWinConditionService.EnsureInstance()`:确保服务存在(场景没放也会自动创建)
- `SetCanTriggerEnding(bool)` / `UnlockEnding()` / `LockEnding()`:从其它系统修改通关条件
- `CanTriggerEnding`:读取当前是否允许触发结局
### 6. [EndingWinConditionDebugButton.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/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` 组件,并取消勾选 `Interactable``Blocks Raycasts`
4. 将它们拖给 `EndingSequenceController` 中对应的 `Black Screen Canvas Group``White Screen Canvas Group` 槽位。
### 3. 准备最终结算 UI
1. 在 Canvas 中准备好你的返回主菜单/退出游戏面板(如 `EndingUIPanel`),该面板**不要**使用自动拦截输入的逻辑,只需包含纯粹的 UI 元素和 Reach Button。
2. 将其拖入 `EndingSequenceController``Ending UI Panel` 槽位。序列开始时会自动将它隐藏,并在最终白屏完成后激活。
3.`EndingUIPanel`(或其根节点)上挂载 [EndingUIScreenController.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/UI/EndingUIScreenController.cs)
- **Scene**
- `Main Menu Scene Build Index` 设为 `1`(你的主菜单场景 index=1)。
- `Begin Preload On Enable` 保持勾选:UI 激活时会后台异步预加载 index=1,并 `allowSceneActivation=false`,等待点击返回按钮才切换。
- **Buttons**
- 把“返回主菜单”按钮拖到 `Return To Menu Button`Reach Button)或 `Return To Menu Button UGUI`Unity 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 Enable`UI 激活时会按 `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 UGUI``Debug Summary String`:点击按钮可直接把预设字符串写进 `Summaray Text`,用于在未接入/未联网时调试 UI 流程。
### 4. 配置场景对象(门与光)
1. 将门物体挂载 `EndingDoorController` 并绑定左右两扇门。
2. 在门后放置一盏 Light,挂载 `EndingLightController` 并将灯光引用赋给它。
3. 将这两个控制器拖拽到 `EndingSequenceController` 中对应的 `Door Controller``Light 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](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/OpeningSystem/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.cs](file:///e:/Unity/Unity%20Program/Test_2022/Assets/_Project/Scripts/EndingSystem/EndingSequenceController.cs) 的 `Overlay Safety` 中:
- `Override Canvas Sorting` 勾选
- `Overlay Sorting Order` 设为一个足够大的值(默认 5000)
- `Bring Screen To Front` 勾选
### 4) “提示闪烁”脚本误绑定到遮罩 CanvasGroup
`PressToContinue` 之前存在兜底 `FindObjectOfType<CanvasGroup>(true)` 的逻辑,可能会误抓到全屏遮罩。
当前已修复:它只会使用 `FeedNotification` 自己身上的 CanvasGroup,不会再影响结局遮罩。