159 lines
13 KiB
Markdown
159 lines
13 KiB
Markdown
|
|
# 结局动画系统(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(或新建 Canvas,Sort Order 调高以覆盖其他 UI)下,创建两个全屏的 Image(Anchor = Stretch/Stretch,Left/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,不会再影响结局遮罩。
|