跨场景事件:没人聊但人人踩的持久化问题

news2026/4/8 17:44:04
目录根本矛盾静态事件幽灵订阅问题实例事件随场景消亡DontDestroyOnLoad 创可贴Bootstrap 场景模式多场景编辑让情况更糟生命周期问题GES 如何解决这个问题ScriptableObject 事件存在于场景之外Behavior Window自动生命周期管理Persistent Listener显式的跨场景存活场景切换时发生了什么逐步持久化的场景配置多数据库动态加载要注意的反模式更大的图景你的AudioManager播放背景音乐。它订阅了OnLevelStart在玩家进入新区域时切换曲目。你把AudioManager放在DontDestroyOnLoad对象上保持跨场景存活。开发期间一切正常因为你一直在同一个场景里测试。然后有人第一次从关卡 1 加载到关卡 2。音乐不切了。AudioManager还活着 ——DontDestroyOnLoad尽职了 —— 但事件订阅没有存活下来。或者更糟旧的订阅还在指向关卡 1 里已经被销毁的事件触发方下次有东西尝试调用它时你会在游戏中途收到一个MissingReferenceException。这就是持久化问题每个有多个场景的 Unity 项目迟早都会撞上。根本矛盾Unity 的场景系统和事件系统建立在对对象生命周期截然不同的假设上。场景是临时的。加载一个场景用它卸载它。场景里的对象与它同生共死。这很干净、可预测也符合玩家体验游戏的方式 —— 去到新区域离开旧区域。事件需要持久性。全局的数据分析系统需要听到每个场景的伤害事件。存档系统需要响应存档点事件不管玩家在哪一关。成就追踪器需要在整个游玩会话中累积数据。这两个模型是矛盾的。而 Unity 并没有给你好的工具来调和它们。静态事件幽灵订阅问题大多数开发者首先尝试的是静态事件publicstaticclassGameEvents{publicstaticeventActionOnLevelStart;publicstaticeventActionintOnPlayerDamaged;publicstaticeventActionOnPlayerDied;}静态事件跨场景加载持久化因为它们存在于类上而非对象上。问题解决了对吧没那么简单。静态事件持久化了但订阅它们的对象没有。场景卸载时场景里的每个 MonoBehaviour 都被销毁。如果其中一个 MonoBehaviour 订阅了静态事件而没在OnDisable或OnDestroy中取消订阅你就有了一个幽灵订阅 —— 一个指向已销毁对象的委托。下次事件触发时MissingReferenceException: The object of type EnemySpawner has been destroyed but you are still trying to access it.修复看似简单永远在OnDisable中取消订阅。但OnDisable在场景切换时有自己的问题后面会讲。而且即使你很自律一个脚本里漏掉一次取消订阅就会造成一个只在场景切换时才显现的 Bug —— 最难复现、最容易在测试中遗漏的那种。静态事件还造成了另一个架构问题一切都是全局的。没有这个事件属于这个场景或这个事件只在这个上下文中有意义的概念。整个项目中的每个系统都能看到并订阅每个事件。对于真正全局的事件如OnApplicationPause还行但对于场景特定的事件如OnDoorOpened或OnPuzzleSolved就一团糟了。实例事件随场景消亡相反的方案 —— MonoBehaviour 上的实例事件publicclassLevelManager:MonoBehaviour{publiceventActionOnLevelStart;publiceventActionOnLevelComplete;}干净且有作用域。只有引用了LevelManager的对象才能订阅。场景卸载时LevelManager被销毁所有订阅跟着消失。没有幽灵引用。但现在跨场景通信不可能了。你的AudioManager活在DontDestroyOnLoad的世界里需要当前场景中LevelManager的引用。怎么获得每次场景加载后FindObjectOfType静态注册表Service Locator每种方案都增加了复杂度和耦合 —— 恰恰是事件本应消除的东西。而且场景卸载后你的AudioManager还持有对已销毁LevelManager的引用。希望你做了 null 检查。DontDestroyOnLoad 创可贴“把事件系统放在DontDestroyOnLoad对象上就好了。”这是最常见的建议而且确实有点用。你创建一个持久化的EventManager持有所有事件标记DontDestroyOnLoad然后所有东西都订阅它。但关于DontDestroyOnLoad人们不会告诉你的是问题 1非 DDOL 对象在场景切换时会触发OnDisable。Unity 卸载场景时场景中的每个 MonoBehaviour 都收到OnDisable和OnDestroy。如果你的监听器在OnDisable中取消订阅理应如此它们就在场景切换期间取消订阅了。你的事件系统瞬间没有监听器了。如果这个窗口期内有东西触发事件没人听得到。问题 2切换期间的执行顺序不保证。新场景加载时OnEnable在所有新 MonoBehaviour 上触发。但什么顺序如果EnemySpawner.OnEnable在LevelManager.OnEnable之前触发而 Spawner 需要订阅 LevelManager 还没初始化的事件你就得到一个 null 引用。在你机器上能用Unity 恰好按正确顺序初始化了。在 QA 的机器上不行。问题 3重复的 DDOL 对象。如果你的持久化EventManager在一个被加载两次的场景里测试时从不同起始场景按 Play 很常见你就有两个EventManager。现在每个事件有两份。一半监听器订阅了一份另一半订阅了另一份。啥都不工作但 Inspector 里一切看起来正常。Bootstrap 场景模式有些团队用Bootstrap场景解决重复问题。游戏总是先加载一个 Bootstrap 场景创建所有持久化管理器然后以 Additive 方式加载实际的游戏场景。这能用但增加了实实在在的复杂度你不能从任何场景直接按 Play 了。必须从 Bootstrap 场景开始或者写编辑器工具在你的测试场景之前自动加载 Bootstrap。加载顺序变得关键。Bootstrap 必须在任何游戏场景访问其系统之前完成初始化。这通常意味着一个加载画面即使加载很快。场景管理变复杂了。你现在在管理 Additive 场景加载意味着要管理哪些场景已加载、哪些正在加载、哪些正在卸载 —— 同时进行。它能用。大量上线的游戏用这个模式。但这是纯粹为了绕过持久化问题而存在的基础设施。是管道工程不是游戏逻辑。多场景编辑让情况更糟Unity 的 Additive 场景加载对大世界很强大 —— 同时加载村庄场景、地形场景和 UI 场景。但它让持久化问题翻倍了。哪个场景拥有哪个事件如果OnShopOpened在村庄场景里OnInventoryChanged在玩家场景里村庄卸载时会怎样OnShopOpened消失了但仍然加载的玩家场景中的对象可能还在监听它。它们现在订阅了一个不存在的东西而且自己不知道。卸载场景本应是干净的。有了跨场景事件引用一点都不干净。生命周期问题让我们精确追踪场景切换时使用事件的完整过程SceneManager.LoadScene(Level2)被调用Unity 开始卸载当前场景当前场景所有 MonoBehaviour 触发OnDisable监听器取消订阅当前场景所有 MonoBehaviour 触发OnDestroy当前场景完全卸载新场景开始加载新场景所有 MonoBehaviour 触发Awake新场景所有 MonoBehaviour 触发OnEnable监听器重新订阅新场景所有 MonoBehaviour 触发Start问题在第 3 步和第 8 步之间的空隙。在这段时间里你的事件系统没有场景级别的监听器。任何 DDOL 对象在这个窗口期触发事件就是在对着虚空喊话。而第 8 步内部的顺序在不同机器或 Unity 版本间不确定。系统 A 可能需要订阅系统 B 初始化的事件。如果 B 的OnEnable在 A 之后执行你就得到一个表现为海森堡 Bug 的竞态条件。需要跨场景持久化的真实系统示例AudioManager—— 必须听到任何场景的OnLevelStart、OnBossFight、OnVictoryAnalyticsManager—— 必须追踪会话中每个场景的事件SaveSystem—— 必须响应OnCheckpointReached不管在哪个场景AchievementTracker—— 必须跨所有场景累积进度数据这些都是必须听到任何场景事件的系统。持久化问题不是学术讨论 —— 它阻挡了真实游戏中的真实功能。GES 如何解决这个问题GES 从架构层面解决持久化问题而不是用变通方案。ScriptableObject 事件存在于场景之外这是关键洞察。在 GES 中事件是 ScriptableObject 资产存在于项目的 Assets 文件夹里 —— 不在任何场景中。它们是项目级别的资源不是场景级别的对象。publicclassAudioManager:MonoBehaviour{[GameEventDropdown,SerializeField]privateSingleGameEventonLevelStart;[GameEventDropdown,SerializeField]privateSingleGameEventonBossFight;}关卡 1 卸载、关卡 2 加载时onLevelStart事件资产哪儿也不去。它不属于任何场景。它存在于项目级别独立于场景生命周期。你的AudioManagerDDOL保持对同一个事件资产的引用。新场景的LevelManager也获得对同一个事件资产的引用。通信就这么通了。不需要静态事件。不需要事件管理器单例。不需要 Bootstrap 场景。ScriptableObject 架构让跨场景通信成为事件存储方式的自然结果而不是你必须特意启用的特殊功能。Behavior Window自动生命周期管理GES 的 Behavior Window 可视化地处理订阅生命周期。当你通过 Behavior Window 绑定监听器时它在OnEnable中自动订阅、在OnDisable中自动取消订阅。不需要手写订阅代码。不可能忘记取消订阅。这意味着场景切换直接就能用旧场景卸载 ——OnDisable触发 —— Behavior Window 自动取消订阅旧监听器新场景加载 ——OnEnable触发 —— Behavior Window 自动订阅新监听器事件资产从未被销毁所以订阅无缝连接到同一个事件没有空隙。没有竞态条件。没有幽灵引用。Persistent Listener显式的跨场景存活对于真正需要跨场景加载持久化的系统 —— 你的AudioManager、你的AnalyticsManager—— GES 提供 Persistent Listener。在代码中使用AddPersistentListenerpublicclassAudioManager:MonoBehaviour{[GameEventDropdown,SerializeField]privateSingleGameEventonLevelStart;privatevoidOnEnable(){onLevelStart.AddPersistentListener(HandleLevelStart);}privatevoidOnDestroy(){onLevelStart.RemovePersistentListener(HandleLevelStart);}privatevoidHandleLevelStart(stringlevelName){// Change music based on level}}Persistent Listener 存储在与普通监听器分离的层中。它们能存活过场景切换因为事件是 ScriptableObject存在于场景之外监听器在 DDOL 对象上存活过切换Persistent 注册显式告诉事件系统跨加载保留这个在 Behavior Window 里有一个Persistent 复选框—— 就是AddPersistentListener的可视化等价物。勾上它该绑定就能存活过场景切换不需要任何代码。场景切换时发生了什么逐步之前同样的切换追踪但这次用 GESSceneManager.LoadScene(Level2)被调用Unity 开始卸载关卡 1关卡 1 MonoBehaviour 触发OnDisable—— Behavior Window 自动取消订阅它们的监听器关卡 1 MonoBehaviour 触发OnDestroy关卡 1 完全卸载事件资产完好无损—— 它们是 ScriptableObject不是场景对象Persistent Listener 完好无损—— 它们注册在 DDOL 对象上关卡 2 开始加载关卡 2 MonoBehaviour 触发OnEnable—— Behavior Window 自动订阅它们的监听器关卡 2 MonoBehaviour 触发Start关键区别第 5 步和第 9 步之间事件系统不是空的。Persistent Listener 仍然活跃。如果 DDOL 系统在加载期间触发事件Persistent Listener 能听到。场景特定的监听器消失了这是对的但全局系统从未丢失连接。持久化的场景配置场景配置很直观你的持久化管理器活在 DDOL 对象上使用 Persistent Listener 绑定。场景特定对象使用普通的 Behavior Window 绑定。事件资产放在任何场景都能访问的共享数据库中。多数据库动态加载对于有很多场景的大型项目GES 支持多个事件数据库。你可以按上下文组织事件核心数据库—— 启动时加载的全局事件OnApplicationPause、OnSaveRequested、OnAchievementUnlocked战斗数据库—— 战斗场景活跃时加载OnDamageDealt、OnEnemyDefeatedUI 数据库—— 随 UI 场景加载OnMenuOpened、OnSettingsChanged场景特定的数据库随场景一起加载和卸载。核心数据库始终保持加载。未加载数据库中的事件变为非活跃 —— 它们不会触发尝试 Raise 是空操作而不是报错。这给了你静态事件所缺乏的作用域“这个事件只在这个场景加载时存在”又没有实例事件的脆弱性“这个事件在这个对象死亡时消失”。要注意的反模式一个要避免的错误忘了在OnDestroy中移除 Persistent Listener。// BAD - persistent listener leaks if this object is destroyedprivatevoidOnEnable(){onLevelStart.AddPersistentListener(HandleLevelStart);}// GOOD - clean up in OnDestroy for DDOL objectsprivatevoidOnDestroy(){onLevelStart.RemovePersistentListener(HandleLevelStart);}普通监听器在OnDisable中取消订阅。Persistent Listener 应该在OnDestroy中取消订阅 —— 因为 Persistent Listener 的全部意义就是在场景切换时的OnDisable中存活下来。如果你把移除放在OnDisable就违背了初衷。GES 的 Runtime Monitor特别是 Warnings 标签页会标记注册在非DontDestroyOnLoad对象上的 Persistent Listener。这几乎总是 Bug —— 你告诉事件系统跨场景加载保留这个监听器但对象本身活不过加载。更大的图景跨场景持久化不只是一个技术问题 —— 它是一个影响整个项目结构的架构决策。错误的选择会级联成单例、Service Locator、Bootstrap 场景、加载顺序依赖、和散布在每个脚本中的防御性 null 检查。GES 的方案 —— ScriptableObject 事件加显式持久化控制 —— 意味着你不必在一切全局化和什么都不能跨场景之间二选一。事件存在于项目级别。监听器根据自身需求选择持久化方式。常见情况自动处理生命周期特殊情况显式控制。你的AudioManager用 Persistent Listener 订阅一次就能在整个会话中听到每个场景的事件。你的EnemySpawner通过 Behavior Window 订阅场景卸载时自动断开下一个场景自动重连。两种模式在同一个事件上共存。不需要特殊配置。不需要 Bootstrap 场景。没有竞态条件。 全球开发者服务矩阵 Game Event System(GES) 主页 国区开发者社区 Unity 中国资产商店 B站官方视频教程 高性能架构技术文档 国内技术交流群 (1071507578) 全球开发者社区 Unity Global Asset Store Discord 全球技术社区 YouTube 官方频道 Unity 官方论坛专贴 GitHub 官方主页欢迎感兴趣的小伙伴前来围观~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2493086.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…