多年来,在制作各种CET模组的过程中,我反复遇到同样的情况:反复编写同类型的基础功能模块。
生命周期管理、黑板轮询、载具状态追踪、距离检测、存储工具、日志封装……每个模组都需要类似的功能,而每个模组的实现又各有差异。单独来看都不算繁重,但在多个项目间重复堆放就开始显得杂乱了。
0引擎是我最终决定不再反复重写那些系统,而是好好把它们一次性建好。
它将状态缓存、事件分发、邻近检测、日志记录与存储整合到CET模组的共享运行时层。单纯玩游戏的话,除非你装了大量模组且它们开始依赖这个,否则不会有明显感知;但如果你在编写模组,多半会用到它。
更多关于模组的详细示例,请参阅我上传在可选文件中的 Readme.MD 文件。0-引擎运行单一托管更新循环,并据此构建中央状态快照。模组可订阅事件并读取缓存值,无需各自轮询。
状态快照包含:
• 玩家实体(安全生命周期处理)
• 位置 + 朝向 + 偏航角
• 菜单状态
• 战斗状态
• 载具状态
• 派生状态缓存(武器类型、时段、镜头俯仰角、区域等)
• 完整玩家状态机黑板(原始枚举 + 便捷标志)
开启方式:
local Engine = GetMod("0-Engine") local state = Engine.GetState()返回状态为深度只读。若试图修改该状态,将立即报错。
黑板数值每帧读取,但枚举解析和事件触发仅在数值变化时执行。原始数值位于 state.blackboard.psm.* 路径下,而便捷标记则分布在移动/战斗/身体/视野/载具/场景等模块中。
派生状态包含需要引擎调用的内容:武器类型(含记录ID缓存)、时段、玩家死亡状态、摄像机俯仰角(基于前向向量的反正弦值,已限制范围)、是否持有武器(通过交易系统的缓存TweakDBID进行插槽检测)、工作点侦测以及区域(由预防系统解析,每约30帧检测变化并限频处理)。
玩家生命周期采用防御性处理机制。引擎会追踪玩家实体标识,区分首次获取、失效及重建(重载/重生)三种状态,并相应触发PlayerReady(玩家就绪)、PlayerInvalidated(玩家失效)和PlayerRecreated(玩家重建)事件,同时原子化更新内部引用。
如果你只想要安全初始化:
在你的onInit函数中: local Engine = GetMod("0-Engine") 引擎就绪后执行(玩家入参) 引擎.日志("我的模组", "玩家就绪", "信息") 结束)从这里开始,预期模型采用过渡驱动逻辑,而非持续轮询:
Engine.Subscribe("SlidingChanged", function(isSliding) if isSliding then → 如果正在滑铲 Engine.Log("MyMod", "Sliding started", "debug") 结束 结束) 引擎.订阅("区域变更", function(区域) 引擎.日志("MyMod", "区域:" .. district) 结束)如果确实需要周期性逻辑,请使用帧率控制而非逐帧更新:
引擎.每帧执行(10, function(帧) local s = Engine.GetState() 如果 (s.inMenu) 则 返回 结束 -- 每10帧执行一次的轻量定时逻辑 结束)事件发射器设计得简单且安全:监听器受pcall保护,遍历时使用浅拷贝,回调中进行退订不会影响事件分发。
0号引擎提供多种距离检测模式。首先是基础模式,用于判定是否处于特定区域或检测小范围坐标集。其次是空间哈希模式,主要用于处理大规模坐标集。
距离检测使用缓存的玩家位置。IsNear()采用平方距离(无平方根)。区域支持独立限频及安全回调函数。
```lua
local zone = Engine.RegisterZone({
``` id = "afterlife_door", x = -1525.12,y = 1378.44,z = 32.0 radius = 8, throttle = 5, onEnter = function(dist)
当进入时 = function(距离) Engine.Log("MyMod", "已进入区域(" .. dist .. "米)") 结束了, onExit = function(dist)
退出时 = function(距离) 引擎.日志("我的模组", "已离开区域(" .. dist .. "米)") 结束 })区域通过帧模数节流进行检查,回调函数以pcall包裹,防止单个错误引发连锁崩溃。
基于网格的空间索引系统,适用于大规模坐标集。注册一次即可实现 O(1) 复杂度的最近点/范围内查询。可选的自动轮询功能,会在玩家进入、离开或接近已注册位置时触发回调。
local handle = Engine.RegisterSpatialSet("my_items", {
-- 翻译结果:
句柄 = 引擎.注册空间集合("我的物品", { { x = 100, y = 200, z = 10, id = "item_1" } { x = 500, y = 300, z = 15, id = "道具_2" } }, { gridSize = 100, -- 单元格大小(米,默认值:100) pollRadius = 60, -- 自动轮询半径(nil = 仅手动查询) pollThrottle = 30, -- 每次检测间隔帧数(默认30) onEnter = function(entry, distSq) print("附近:", entry.id) end, onExit = function(entry) print("离开:", entry.id) end, })更多示例、生命周期、移除等详细信息,请参见 readme.md。
大量 Mod 需要检测特定 NPC 何时生成——比如换装、自定义对话、行为变更。常规做法是挂钩 `NPCPuppet.OnGameAttached`,但这个事件会针对游戏中每个 NPC 触发。如果每个 Mod 都独立调用,意味着每个运行会话中,每个 Mod 都要重复执行数百次跨边界调用。这显然不太理想。
0-引擎将过滤逻辑移入RedScript。通过TweakDBID注册你关注的NPC,RedScript侧的哈希映射会处理其余工作。未匹配的生成体不会传入CET,匹配的生成体则排队分派至你的回调函数。
```yaml
handle = Engine.WatchNPC("Character.Panam", {
``` onSpawn = function(entityID, recordID) 引擎记录("我的模组", "帕南已生成!") 结束, onDespawn = function(entityID, recordID) 引擎日志("我的模组", "帕南已消失!") 结束 })全局公交事件(NPC生成/ NPC消失)同样会触发,若你更倾向于通过这种方式订阅。`skipCrowd` 默认开启,以避免人群重复生成。
Engine.Subscribe("NPCSpawned", function(entityID, recordID, tdbidString) -- 任意被注视的NPC触发 结束)没人盯着的时候,每个NPC刷新的额外开销就一次整数比较。所以……基本等于零。
表现加成,积少成多
• 黑板数据每帧读取一次
• 枚举名称仅在变更时解析
• 用于 IsNear() 的平方距离
• 区域限速
• 监听者呼叫守卫
• 热路径缓存TweakDBID
• 具备备份恢复的类原子写入存储
• 环形缓冲日志(256条记录)
没什么特别的,但加起来也不少,所以……
此模组需要你安装以下内容:
赛博引擎调校 代码仓库 redscript 1 ) 安装
《赛博引擎调整》 2 ) 安装
代码仓库 3 ) 安装
redscript 2 将下载的文件解压至游戏主目录,或使用模组管理器安装。
删除文件夹:\Cyberpunk 2077\bin\x64\plugins\cyber_engine_tweaks\mods\0-Engine
删除文件夹:\Cyberpunk 2077\r6\scripts\0-Engine
你的存档中没有固化任何信息,因此随时移除都是安全的。
当前版本:
0.17.3 v0.16.0 - 空间哈希 •
空间哈希模块 基于网格的空间索引(适用于100+坐标集)
• 注册已命名坐标组,支持O(1)邻近点与半径内点查询
• 自动轮询模式,支持自定义半径与触发间隔——自动触发进入/离开区域的回调
• 懒加载条目自动清理,满25%时自动压缩
• 动态分类批量重建(如已收集物品、已完成目标等)
• CET叠加面板:设置名称、条目数量、单元格数量、活动条目、网格尺寸
v0.16.1 - 加载稳定性优化 •
推迟引擎状态系统初始化 黑板监听器现在会在玩家附着后约0.2秒注册,以便引擎完成加载后再开始读取状态。
• 所有12个黑板监听器的立即触发丢弃事件,已替换为单次读取 SeedStateFromBlackboard()(加载期间无回调开销)
• 调整NPC观察者重新注册时机(改为玩家就绪后0.3秒,而非同一帧)
• 正确的延迟初始化清理在分离和快速会话切换时
v0.16.2 - 生命周期失效防御 •
更具防御性的生命周期 - 玩家无效化前的等待时间增加
v0.16.3 - 存档稳定性优化 •
生命周期预热 玩家就绪:发射前需连续有效18帧(约0.3秒),以确保玩家实体创建后引擎稳定。
• 分散“预备就绪”分发 - 回调每帧执行3次而非一次性全部执行(将30个模组分散至约10帧内完成)
• 将延迟初始化调整至0.4秒,为资源加载留出更多空间
v0.17.0 - 模组注册 •
引擎.注册(名称) 返回一个作用域句柄,包含所有引擎方法,并标记以用于追踪与批量控制。
• 作用域句柄方法:Mod.Subscribe、Mod.SetInterval、Mod.SetTimeout、Mod.SetNextTick、Mod.OnFrame、Mod.OnUpdate、Mod.WhenReady、Mod.RegisterZone、Mod.WatchNPC、Mod.RegisterSpatialSet
• 自动填充模组名称(用于Mod.Log、Mod.SetData、Mod.GetData、Mod.ClearData)
• Engine.DisableMod / Engine.EnableMod —— 运行时暂停/恢复某个模组的所有回调
• CET覆盖面板:“已注册模组”区域,包含各模组复选框开关及回调详情提示
• 准备就绪队列清空操作已重新调整至子系统状态读取之前
v0.17.1 - 就绪优先级与可靠性优化 •
就绪时优先级 - Engine.WhenReady和Mod.WhenReady支持可选的优先级设置(1-5,默认3)。优先级1最先触发,优先级5最后触发。错帧结算时,会按优先级从低到高的顺序依次处理。
•
自动清理处理 - 取消订阅或注销已追踪的句柄后,该句柄将自动从模组追踪列表中移除(防止悬浮窗中堆积无效句柄)
•
增强型已注册模组覆盖层 - 悬浮提示现会列出每个模组订阅的具体事件名称,而非仅显示数量
•
模组兼容的源标签 - 在调试覆盖层中,已注册的模组会正确显示归属信息,而不会全部显示为“0-引擎”
•
11 个漏洞修复 :清空就绪队列的前向引用(无声杀死整帧管线)、状态代理前向引用(代理缓存从未清除)、邻近更新提前返回(跳过了剩余区域)、一次性计时器句柄累积、引擎状态系统轮询弹性(轮询循环因原生方法错误崩溃)、空帧发射器清理、Mod.WhenReady 句柄泄漏、Mod.SetInterval 取消包装崩溃、邻近更新缩进修复、NPCWatcher 错误归属
v0.17.2 - NPC观察者会话安全 •
NPC观察者系统会话生命周期 - 现在拥有与 EngineStateSystem 相同的会话保护机制(包括 isReady 标志、OnPlayerAttach/OnPlayerDetach、IsPreGame 保护以及延迟初始化)
• 修复中途加载存档时的不稳定问题——过期的标记、缓存的刷新及事件不再跨游戏阶段残留
• 生成缓冲会在CET重新注册监听前捕获已加载的NPC,待监听就绪后重新播放其行为
• 会话即将结束时触发的消失事件会被静默丢弃(基于 isReady 判定)
• CET端现在会在处理事件前检查isReady状态,防止残留事件泄漏
• 每次状态转移时都会更新世代计数器,以便 CET 在会话边界间检测变化
v0.17.3 - 会话切换稳定性优化 •
投票重新安排移至回合结束 - 旧版“先重排”逻辑生成了“僵尸投票”,在会话销毁后残留,进入空星系时导致崩溃。
•
黑板链分裂 - 在会话过渡期间,GetBlackboardSystem() 可能返回空值;请使用空值检查来拆分链式调用
•
是否在延迟初始化中处于赛前守卫状态 - 在主菜单中延迟初始化触发的冗余安全措施
完整功能列表 :
• 模组注册(引擎注册、作用域句柄、运行时启用/禁用)
• 就绪时,按优先级(1-5)每帧分层扣减
• RedScript 引擎状态系统(基于世代计数的 PSM 缓存)
• RedScript NPC观察者系统(过滤NPC生成/消失追踪,完整会话生命周期)
• 全面的PSM黑板缓存,支持边缘过渡事件
• 衍生状态系统(武器、区域、掩体、摄影机俯仰、工作点等)
• 空间哈希(基于网格的批量邻近查询)
• NPC 监视者 API
• 生命周期处理(玩家就绪、玩家失效、玩家重建)
• 带有节流进入/离开/滴答回调的邻近区域
• 通过ImGui叠加层集中记录日志
• 持久化存储层
• 帧调度(OnUpdate、OnFrame、SetTimeout、SetInterval)
• CET 覆盖层,含各模组回调详情、事件列表及来源标注
使用 psiberx 库(Cron、GameSession、GameUI/GameHUD 辅助工具、Ref)及 Codeware。
感谢CET与redscript社区提供的工具与文档支持。
感谢 NightlyNow 提供的头图设计。