13 KiB
13 KiB
结局动画系统(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.None和Cursor.visible = true以便玩家点击结算 UI 上的按钮。
- 开始序列时,系统调用
2. EndingDoorController.cs
专用于控制双开门平滑打开动画。
- 功能特性:
- 将开门拆分为“开小缝” -> “停顿” -> “完全打开”三个阶段,用于营造悬疑感。
- 支持左右两扇门自动镜像旋转(
mirrorRightDoor)。 - 支持通过 DOTween 驱动以获取更好的曲线(
Ease.InOutSine等)。
3. EndingLightController.cs
专用于在开门的同时,控制门后的灯光从全黑平滑渐亮。
- 机制:在
Awake()时自动将目标光源强度设为 0,当被EndingSequenceController调用时,在指定时间内渐变至目标强度。
4. EndingTrigger.cs
用于触发 EndingSequenceController.StartEndingSequence()。
- 支持方式:
- 碰撞触发:勾选
triggerOnCollision后,一旦带有指定PlayerTag 的物体进入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. 构建导演与标记物
- 在场景中创建一个空物体
_EndingSystem,重置位置为 (0,0,0)。 - 添加
EndingSequenceController脚本。 - 创建子物体
TargetPlayerPos,放到你希望黑屏后主角出现的位置,并朝向门。将它拖给Target Player Transform。
2. 准备屏幕渐变遮罩(Black & White)
- 在现有的 UI Canvas(或新建 Canvas,Sort Order 调高以覆盖其他 UI)下,创建两个全屏的 Image(Anchor = Stretch/Stretch,Left/Top/Right/Bottom = 0)。
- 将一个命名为
EndingBlackScreen(颜色纯黑),另一个命名为EndingWhiteScreen(颜色纯白)。 - 给它们分别添加
CanvasGroup组件,并取消勾选Interactable和Blocks Raycasts。 - 将它们拖给
EndingSequenceController中对应的Black Screen Canvas Group和White Screen Canvas Group槽位。
3. 准备最终结算 UI
- 在 Canvas 中准备好你的返回主菜单/退出游戏面板(如
EndingUIPanel),该面板不要使用自动拦截输入的逻辑,只需包含纯粹的 UI 元素和 Reach Button。 - 将其拖入
EndingSequenceController的Ending UI Panel槽位。序列开始时会自动将它隐藏,并在最终白屏完成后激活。 - 在
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 Button(Reach Button)或Return To Menu Button UGUI(Unity Button)其中一个槽位即可。
- 把“返回主菜单”按钮拖到
- UI
- 把 AI 总结文案的
TMP_Text拖到Summaray Text(仅用于显示 AI 总结/Debug 文案)。 - 把结局状态提示的
TMP_Text拖到Information Text(用于显示“成功逃离/倒下”等提示词)。
- 把 AI 总结文案的
- 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 流程。
- 预留
- Scene
4. 配置场景对象(门与光)
- 将门物体挂载
EndingDoorController并绑定左右两扇门。 - 在门后放置一盏 Light,挂载
EndingLightController并将灯光引用赋给它。 - 将这两个控制器拖拽到
EndingSequenceController中对应的Door Controller和Light Controller槽位。
5. 绑定玩家与相机
在 EndingSequenceController 脚本上:
- Player Body:拖入玩家预制体的最外层(带有
CharacterController的物体)。 - Player Camera Pivot:拖入用于上下旋转的相机支架(Camera 的父物体)。
- Player Camera:拖入主相机(带有 Camera 组件的物体)。
6. 放置触发区域
- 在场景内玩家走向结局的必经之路上放一个带
BoxCollider的空物体(开启Is Trigger)。 - 添加
EndingTrigger脚本。该脚本会自动找到场景里的EndingSequenceController。 - 如果是测试阶段,推荐在 UI 上建一个 Button,并绑定
EndingWinConditionDebugButton.UnlockEnding()来把 canWin 设为 true(而不是直接触发结局)。 - 如果你希望“必须先满足通关条件才触发”:
- 在
EndingTrigger上勾选requireWinConditionToTrigger - 确保你的流程在合适的时机调用一次
EndingWinConditionService.Instance.UnlockEnding()(或SetCanTriggerEnding(true))
- 在
7. 通关条件的典型接入点(推荐)
- 任务驱动:当关键任务状态变为 Completed 时,调用
EndingWinConditionService.Instance.UnlockEnding() - 解锁状态驱动:当出口门锁解除/钥匙使用成功时,调用
UnlockEnding() - 剧情节点驱动:当对话/动画结束回调触发时,调用
UnlockEnding(),然后让玩家走到出口触发器完成收尾演出
三、时序总结(DOTween/Coroutine 流程)
- 玩家触发结局,调用
StartEndingSequence(),锁死输入。 - 黑屏渐入(
fadeToBlackDuration)。 - 玩家被传送到
TargetPlayerPos,相机俯仰角强制回正。 - 稍作等待(
delayBeforeFadeFromBlack)后,黑屏渐出恢复画面。 - 并行执行:
- 镜头缓缓抬升(
cameraLookDuration,目标角度cameraLookUpAngle)。 - 画面慢慢放大,FOV变小聚焦到门上(
fovShrinkDuration,目标FOVtargetFOV)。
- 镜头缓缓抬升(
- 门缝动画启动:门开一条缝 -> 停顿 -> 大开。
- 灯光渐亮:门缝开启的同时门后强光亮起。
- 经过一段设定好的等待时间后(开门耗时 +
delayBeforeFadeToWhite),白屏渐入(fadeToWhiteDuration)。 - 彻底白屏后,系统激活
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.CanvasGroupAnimatorMichsky.UI.Reach.ImageFading- Unity
Animator(带动画曲线驱动 alpha)
解决:
- 不要把这些组件挂在结局遮罩上;或只保留一个系统负责控制 alpha。
- 当前脚本已做防御:结局开始时会自动禁用以上组件,避免抢写。
3) UI 排序问题(遮罩在 UI 里但被别的 Canvas 盖住)
解决(推荐做法):
- 给结局遮罩单独做一个 Overlay Canvas(或者确保它所在 Canvas 置顶)。
- 在 EndingSequenceController.cs 的
Overlay Safety中:Override Canvas Sorting勾选Overlay Sorting Order设为一个足够大的值(默认 5000)Bring Screen To Front勾选
4) “提示闪烁”脚本误绑定到遮罩 CanvasGroup
PressToContinue 之前存在兜底 FindObjectOfType<CanvasGroup>(true) 的逻辑,可能会误抓到全屏遮罩。
当前已修复:它只会使用 FeedNotification 自己身上的 CanvasGroup,不会再影响结局遮罩。