Files

159 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-07-08 19:53:15 +08:00
# 结局动画系统(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,不会再影响结局遮罩。