Imported from NormiStyxia/visual-level-editor (
SKILL.md). Install upstream withnpx skills add NormiStyxia/visual-level-editor. Copyright stays with the author.
可视化关卡编辑器 — 完整架构指南
使用前确认 README
开始使用本 skill 处理编辑器迁移、复用或架构参考任务时,先询问用户:
你是否已经阅读
@normi_visual-level-editor/README.md?
如果用户尚未阅读,先提醒其阅读 README 中的迁移风险说明:该编辑器不是通用 SDK,直接复制源码可能遇到空接口、字段差异、Schema 不一致、宿主能力缺失、预览依赖缺失等问题,需要自行 Debug。
如果用户正在迁移、复用或调试已有编辑器实现,还应提醒其阅读 @normi_visual-level-editor/editor-migration-troubleshooting.md。该文档是 README 的补充排障指南,覆盖 Host 安装、预览系统、物理碰撞、策略节点/触发器、渲染动效、UI 交互、数据同步/导入导出等常见迁移问题。
推荐引导用户采用“参考架构,而不是直接复制源码”的方式:请用户提供当前项目的数据结构、对象类型、配置字段、运行时逻辑和具体需求,然后基于当前项目重新设计适配方案。
一、项目概述
2D 横版动作平台跳跃游戏 + 内置可视化关卡编辑器,引擎 UrhoX。
编辑器运行在 Web Sandbox 预览中(不能直接写本地文件),核心数据通过 JSON 序列化/反序列化。
二、文件架构
当前项目已完成一轮解耦重构,整体形成:
入口层 main / Renderer / TitleMenu
-> 渲染层 render/*
-> 宿主桥接层 editor-host/*
-> 编辑器业务层 editor/*
-> 菜单 / 对话 / 动效 / 节点编辑子系统
注意:当前实际不存在
scripts/editor/titlemenu/目录。
与 TitleMenu 绑定相关的拆分模块位于scripts/editor-host/titlemenu/。
2.0 当前同步状态(2026-07-03)
本 skill 的 scripts/ 代码包已同步到当前项目编辑器实现。
| 项 | Lua 文件数 | 说明 |
|---|---|---|
| skill 同步前 | 109 | 缺少部分拆分后的 UI / Preview 模块 |
| 当前项目编辑器相关范围 | 117 | editor、editor-host、dialog、effects、menu、render、NodeCanvas、Strategy、TitleMenu、Renderer、main |
| skill 同步后 | 117 | 已补齐当前项目文件 |
本次新增/补齐文件:
scripts/editor/preview/PreviewContact.lua
scripts/editor/preview/PreviewLifecycle.lua
scripts/editor/preview/PreviewRender.lua
scripts/editor/preview/PreviewRuntimeMotion.lua
scripts/editor/preview/PreviewStrategy.lua
scripts/editor/preview/PreviewUpdate.lua
scripts/editor/ui/EditorGuidePanel.lua
scripts/editor/ui/JsonImportPanel.lua
当前功能状态:
- EventBus 覆盖物件拖拽、背景图层拖拽、物件贴图层拖拽/缩放。
- Prefab 使用
levelEditor.customPrefabs持久化,保存/删除进入 EventBus/Patch/AI Sync。 - 顶部“预制体”工具支持画布多选保存;第一个选中的物件作为放置锚点。
EditorPreview.lua已拆为editor/preview/*多模块;迁移时不要遗漏子目录。
2.1 顶层入口与 Facade
| 文件 | 职责 |
|---|---|
scripts/main.lua |
游戏总入口,启动脚本与事件转发入口。 |
scripts/Renderer.lua |
NanoVG 主渲染入口,负责调用游戏渲染与 overlay 渲染。 |
scripts/TitleMenu.lua |
菜单与编辑器总 facade,负责组装 menu、editor、editor-host、NodeCanvas、OverlayBridge 等模块。 |
2.2 render 渲染层
| 文件 | 职责 |
|---|---|
scripts/render/GameRenderer.lua |
游戏主体 NanoVG 绘制,包含背景、平台、玩家、HUD 等游戏画面。 |
scripts/render/OverlayBridge.lua |
渲染层与菜单/编辑器 overlay 的解耦桥。Renderer.lua 不直接依赖 TitleMenu.lua,而是通过这里注册/调用 overlay 回调。 |
2.3 editor-host 宿主桥接层
| 文件 | 职责 |
|---|---|
scripts/editor-host/EditorHost.lua |
编辑器宿主 Port。editor/* 统一通过该模块访问宿主能力,避免直接 require TitleMenu.lua。 |
scripts/editor-host/EditorHostAdapterImpl.lua |
默认宿主实现,把 EditorHost 调用转发回 TitleMenu.lua / menu/MenuFlow.lua / GameState / LevelDataService。 |
TitleMenu 绑定拆分模块
| 文件 | 职责 |
|---|---|
scripts/editor-host/titlemenu/EditorCoreBridge.lua |
TitleMenu 与编辑器核心流程的绑定。 |
scripts/editor-host/titlemenu/EditorLevelFlow.lua |
关卡进入、退出、章节/关卡流转相关逻辑。 |
scripts/editor-host/titlemenu/EditorAssetActions.lua |
素材导入、贴图、背景图层等资源操作绑定。 |
scripts/editor-host/titlemenu/EditorLoopActions.lua |
编辑器循环更新、UI 刷新、运行时循环相关动作。 |
scripts/editor-host/titlemenu/EditorPersistenceActions.lua |
关卡导入导出、保存、持久化相关动作。 |
scripts/editor-host/titlemenu/EditorAISyncActions.lua |
AI Sync、Diff、Debug Log、预览 Bundle 等同步相关动作。 |
2.4 editor 编辑器业务层
核心状态与工具
| 文件 | 职责 |
|---|---|
scripts/editor/EditorState.lua |
编辑器状态总入口,聚合 editor/state/*,提供画布状态、选择状态、预览状态、贴图库、关卡元信息等。 |
scripts/editor/LevelDataService.lua |
关卡数据服务,统一管理章节/关卡数据读写入口。 |
scripts/editor/EventBus.lua |
append-only 操作事件记录,供 AI Sync / Undo / Replay 使用。 |
scripts/editor/EventFixer.lua |
事件修复、标准化、补全。 |
scripts/editor/EditorChange.lua |
编辑器数据变更辅助,封装常见变更入口。 |
scripts/editor/ObjIdUtil.lua |
稳定 objId 工具,按 objId 查找/分配对象。 |
scripts/editor/Prefab.lua |
预制体逻辑。 |
状态分区 scripts/editor/state/
| 文件 | 职责 |
|---|---|
CoreState.lua |
编辑器核心默认状态。 |
CanvasState.lua |
画布缩放、平移、坐标转换相关状态。 |
SelectionState.lua |
当前选中对象、选中图层、选择工具状态。 |
PreviewState.lua |
预览模式状态。 |
TextureLibState.lua |
贴图库、自定义素材库状态。 |
MetaState.lua |
关卡元信息、镜头边界等状态。 |
UI 与属性面板
| 文件 | 职责 |
|---|---|
scripts/editor/LevelEditorUI.lua |
编辑器 UI 组装入口,依赖 editor-host.EditorHost 获取宿主能力。 |
scripts/editor/ui/EditorToolbar.lua |
编辑器工具栏。 |
scripts/editor/ui/EditorCanvasSection.lua |
编辑器画布区域。 |
scripts/editor/ui/EditorCanvasObjectsSection.lua |
画布物件列表/对象区域。 |
scripts/editor/ui/props/LevelMetaSection.lua |
关卡基础信息属性区。 |
scripts/editor/ui/props/BgLayerSection.lua |
背景图层属性区。 |
scripts/editor/ui/props/ObjectListSection.lua |
物件列表区。 |
scripts/editor/ui/props/ObjectDetailsSection.lua |
物件基础属性区。 |
scripts/editor/ui/props/ObjectMappingSection.lua |
触发器与执行器映射区。 |
scripts/editor/ui/props/ObjectBehaviorSection.lua |
物件行为/策略配置区。 |
scripts/editor/ui/props/ObjectTextureLayersSection.lua |
物件贴图图层区。 |
scripts/editor/ui/props/PrefabSection.lua |
预制体区。 |
scripts/editor/ui/props/TextureLibrarySection.lua |
贴图库区。 |
画布渲染、预览、导入导出
| 文件 | 职责 |
|---|---|
scripts/editor/EditorRenderer.lua |
编辑器画布渲染,负责网格、物件、贴图层、背景图层、镜头框等。 |
scripts/editor/EditorPreview.lua |
预览模式门面,整合 editor/preview/*。 |
scripts/editor/EditorImport.lua |
关卡 JSON 导入。 |
scripts/editor/EditorExport.lua |
关卡 JSON 导出/保存。 |
scripts/editor/EditorEnterFlow.lua |
编辑器进入流程。 |
scripts/editor/LevelMigration.lua |
关卡版本迁移。 |
scripts/editor/LevelBackup.lua |
关卡备份。 |
scripts/editor/LevelSummary.lua |
关卡摘要。 |
交互系统(Pipeline Pattern)
| 文件 | 职责 |
|---|---|
scripts/editor/EditorInteractionPipeline.lua |
输入处理管线,按 Interaction 链式分发。 |
scripts/editor/EditorUpdateGateInteraction.lua |
更新入口门控,预览模式激活时跳过编辑交互。 |
scripts/editor/EditorScrollAndOverlayGateInteraction.lua |
滚轮缩放与覆盖层检测。 |
scripts/editor/EditorShortcutInteraction.lua |
快捷键,例如撤销。 |
scripts/editor/EditorCanvasInputContext.lua |
构建画布输入上下文。 |
scripts/editor/EditorCanvasNavigationInteraction.lua |
画布导航。 |
scripts/editor/EditorCanvasPanPotentialInteraction.lua |
平移潜在状态判定。 |
scripts/editor/EditorCanvasPanDragInteraction.lua |
画布平移拖拽执行。 |
scripts/editor/EditorObjectDragInteraction.lua |
物件拖拽移动。 |
scripts/editor/EditorMappingClickInteraction.lua |
触发器-执行器映射点击。 |
scripts/editor/EditorBgLayerHitInteraction.lua |
背景图层命中检测。 |
scripts/editor/EditorBgLayerDragInteraction.lua |
背景图层拖拽。 |
scripts/editor/EditorTextureToolInteraction.lua |
贴图工具交互。 |
scripts/editor/EditorTextureToolObjectClickInteraction.lua |
贴图工具物件点击。 |
scripts/editor/EditorSelectDeletePrecheckInteraction.lua |
删除前检查。 |
scripts/editor/EditorSelectDeleteInteraction.lua |
选择/删除物件。 |
scripts/editor/EditorMouseReleaseInteraction.lua |
鼠标释放处理。 |
scripts/editor/EditorMouseCoordLabelUpdate.lua |
鼠标坐标标签更新。 |
scripts/editor/EditorCamBoundsInteraction.lua |
镜头范围框拖拽。 |
预览模式 scripts/editor/preview/
| 文件 | 职责 |
|---|---|
PreviewLifecycle.lua |
预览启动/退出。 |
PreviewUpdate.lua |
预览帧更新与独立输入处理。 |
PreviewRender.lua |
预览渲染。 |
PreviewContact.lua |
Box2D 碰撞与触发器回调。 |
PreviewStrategy.lua |
策略树运行时执行。 |
PreviewRuntime.lua |
预览运行时上下文/公共运行时逻辑。 |
PreviewRuntimeMotion.lua |
move_obj 等路径/插值运动动画。 |
Patch / Undo / Replay / AI Sync
| 文件 | 职责 |
|---|---|
scripts/editor/EditorUndoState.lua |
撤销状态快照。 |
scripts/editor/EditorUndoAction.lua |
撤销操作执行。 |
scripts/editor/PatchGenerator.lua |
Patch 生成。 |
scripts/editor/PatchCompiler.lua |
Patch 编译。 |
scripts/editor/PatchCoalescer.lua |
Patch 合并压缩。 |
scripts/editor/PatchMerger.lua |
Patch 合并。 |
scripts/editor/PatchPreview.lua |
可读 Diff / Patch 预览。 |
scripts/editor/LevelPatch.lua |
Patch 应用。 |
scripts/editor/ReplayEngine.lua |
Event/Intent/Patch 回放管线。 |
scripts/editor/IntentClassifier.lua |
Event -> Intent 语义分类。 |
scripts/editor/IntentMerger.lua |
Intent LWW 合并、create/delete 对消。 |
scripts/editor/IntentPatchCompiler.lua |
Intent -> 可执行 Patch。 |
scripts/editor/ExportService.lua |
AI Sync / Diff / 全量导出服务。 |
scripts/editor/AISyncCodec.lua |
AI Sync 数据编码/解码。 |
scripts/editor/AISyncPanel.lua |
AI Sync 面板 UI。 |
scripts/editor/AISyncPipelineHarness.lua |
AI Sync 管线测试/验证辅助。 |
2.5 menu 菜单系统
| 文件 | 职责 |
|---|---|
scripts/menu/MenuFlow.lua |
菜单流程 facade,汇总标题页、主菜单、章节选择、任务面板等。 |
scripts/menu/TitleScreen.lua |
标题页。 |
scripts/menu/MainMenuView.lua |
主菜单视图。 |
scripts/menu/MainMenuSpine.lua |
主菜单 Spine 表现。 |
scripts/menu/LayerEditor.lua |
图层编辑相关菜单 UI。 |
scripts/menu/TaskPanel.lua |
任务面板。 |
scripts/menu/ChapterSelect.lua |
章节选择。 |
scripts/menu/BackButton.lua |
返回按钮。 |
scripts/menu/ChapterBg.lua |
章节背景。 |
scripts/menu/ChapterIconEditor.lua |
章节图标编辑。 |
2.6 NodeCanvas / Strategy 节点编辑系统
| 文件 | 职责 |
|---|---|
scripts/StrategyNode.lua |
节点类型定义、端口、Inspector 字段、求值引擎、序列化。 |
scripts/StrategyEditor.lua |
策略编辑器入口,连接对象属性面板与 NodeCanvas。 |
scripts/NodeCanvas.lua |
可视化节点编辑器 facade。 |
scripts/NodeCanvasCore.lua |
节点画布核心状态/通用逻辑。 |
scripts/NodeCanvasGraph.lua |
节点图、连线、端口与图结构逻辑。 |
scripts/NodeCanvasEditorBridge.lua |
NodeCanvas 与编辑器属性/对话/变更记录的桥接。 |
2.7 dialog 对话系统
| 文件 | 职责 |
|---|---|
scripts/dialog/DialogEditor.lua |
对话可视化编辑。 |
scripts/dialog/DialogManager.lua |
运行时队列管理。 |
scripts/dialog/DialogView.lua |
对话展示、打字机动画与点击推进。 |
scripts/dialog/DialogRenderer.lua |
NanoVG 底层渲染。 |
scripts/dialog/DialogConfig.lua |
预设配置。 |
2.8 effects 动效系统
| 文件 | 职责 |
|---|---|
scripts/effects/EffectRegistry.lua |
注册式动效引擎。 |
scripts/effects/builtin.lua |
内置效果集。 |
三、路径依赖情况
3.1 总体依赖原则
当前重构后的依赖方向以“编辑器业务层不直接依赖宿主实现”为核心:
editor/*
-> editor-host.EditorHost
-> editor-host.EditorHostAdapterImpl
-> TitleMenu / MenuFlow / GameState / LevelDataService
渲染层也通过 overlay bridge 解耦:
Renderer
-> render.GameRenderer
-> render.OverlayBridge
-> TitleMenu 注册的 overlay 回调
关键目标:
editor/*不直接 requireTitleMenu.lua。Renderer.lua不直接 requireTitleMenu.lua。TitleMenu.lua作为组装层可以依赖 editor/menu/editor-host/render/NodeCanvas 等模块。editor-host/titlemenu/*是从 TitleMenu 拆出的绑定模块,仍属于宿主侧,不属于 editor 业务层。
3.2 主入口依赖链
main.lua
-> Renderer.lua
-> render/GameRenderer.lua
-> render/OverlayBridge.lua
说明:
main.lua是脚本入口。Renderer.lua负责 NanoVG 主渲染调度。- 游戏主体绘制进入
render/GameRenderer.lua。 - 菜单/编辑器 overlay 通过
render/OverlayBridge.lua注册与调用。
3.3 TitleMenu 组装链
TitleMenu.lua
-> editor-host/titlemenu/*
-> editor-host.EditorHost
-> editor-host.EditorHostAdapterImpl
-> editor.EditorState / LevelEditorUI / EditorPreview / EditorRenderer / LevelDataService / EventBus
-> menu.MenuFlow / menu.*
-> NodeCanvas / StrategyEditor / StrategyNode
-> render.OverlayBridge
说明:
TitleMenu.lua是当前菜单与编辑器的总 facade。- 它负责安装
EditorHost,注册OverlayBridge,并绑定拆分出去的 editor-host/titlemenu action 模块。 - 后续继续重构时,应优先从 TitleMenu 中继续抽离功能到
editor-host/titlemenu/*或menu/*,避免 TitleMenu 再次膨胀。
3.4 editor-host 依赖链
editor/*
-> editor-host.EditorHost
-> EditorHostAdapterImpl
-> TitleMenu / MenuFlow / GameState / LevelDataService
说明:
EditorHost.lua是 Port,只定义宿主接口。EditorHostAdapterImpl.lua是 Adapter,把接口调用转回宿主实现。editor/*只能调用EditorHost暴露的方法,不应直接 requireTitleMenu.lua、MenuFlow.lua或GameState.lua。
3.5 editor 内部依赖链
EditorState
-> editor/state/*
LevelEditorUI
-> editor-host.EditorHost
-> EditorState / LevelDataService / EditorChange
-> editor/ui/*
-> editor/ui/props/*
EditorPreview
-> editor-host.EditorHost
-> EditorState
-> dialog.DialogManager
-> effects.builtin / EffectRegistry
-> editor/preview/*
EditorRenderer
-> EditorState
-> NodeCanvas
-> effects.builtin / EffectRegistry
说明:
EditorState.lua是状态门面,内部聚合editor/state/*。LevelEditorUI.lua是 UI 门面,内部组合editor/ui/*和editor/ui/props/*。EditorPreview.lua是预览门面,内部组合editor/preview/*。EditorRenderer.lua是画布渲染门面。
3.6 menu 依赖链
menu/MenuFlow.lua
-> menu.TitleScreen
-> menu.MainMenuView
-> menu.MainMenuSpine
-> menu.LayerEditor
-> menu.TaskPanel
-> menu.ChapterSelect
-> TitleMenu / Player(懒加载访问)
说明:
MenuFlow.lua汇总菜单页面与流程。- 与编辑器/玩家相关的重依赖应保持懒加载,避免启动阶段循环依赖。
3.7 dialog / effects 依赖链
dialog/DialogManager.lua
-> dialog.DialogConfig
-> dialog.DialogView
dialog/DialogView.lua
-> effects.EffectRegistry
dialog/DialogRenderer.lua
-> dialog.DialogManager
-> effects.EffectRegistry
dialog/DialogEditor.lua
-> editor.EditorState
-> editor.EditorChange
effects/builtin.lua
-> effects.EffectRegistry
说明:
dialog/*已独立成对话域,编辑器通过DialogEditor复用对话编辑能力。effects/*是注册式动效系统,新增效果优先只改effects/builtin.lua。
3.8 NodeCanvas / Strategy 依赖链
StrategyEditor.lua
-> StrategyNode
-> NodeCanvas
-> editor.EditorChange
NodeCanvas.lua
-> NodeCanvasCore
-> NodeCanvasGraph
-> NodeCanvasEditorBridge
NodeCanvasGraph.lua
-> editor.EditorState
-> editor.ObjIdUtil
NodeCanvasEditorBridge.lua
-> dialog.DialogEditor
-> editor.EditorChange
-> StrategyNode
说明:
NodeCanvas*.lua是策略节点图编辑子系统。NodeCanvasEditorBridge.lua专门处理 NodeCanvas 与编辑器域之间的桥接,避免所有桥接逻辑堆回NodeCanvas.lua。StrategyNode.lua是节点类型、默认值、端口、序列化与求值的权威来源。
3.9 依赖边界检查清单
后续修改编辑器时优先检查:
- 新增
editor/*模块是否直接 require 了TitleMenu.lua?如果是,应改走editor-host.EditorHost。 - 新增渲染逻辑是否让
Renderer.lua直接依赖菜单/编辑器?如果是,应改走render.OverlayBridge。 - 新增菜单流程是否继续塞进
TitleMenu.lua?如果是,优先放到menu/*或editor-host/titlemenu/*。 - 新增预览逻辑是否直接访问正式游戏输入?预览应优先走
editor/preview/*的独立输入与运行时状态。 - 新增策略节点默认值是否只维护在
StrategyNode.NODE_DEFAULTS?不要在 AI Sync / Patch / Preview 中维护第二份默认值。
四、工程复用前置依赖与阻碍
当前编辑器已经完成主要解耦,但还不是完全零依赖的独立包。复用时需要区分:
- 通用编辑器核心依赖:目标工程必须提供。
- 当前游戏专属依赖:短期可以随项目一起迁移,长期应抽象为 Host Port / Adapter。
- 宿主侧依赖:只应留在
editor-host/*或具体游戏工程中,不应进入editor/*核心。
4.1 通用前置依赖
| 依赖 | 使用位置 | 复用判断 |
|---|---|---|
urhox-libs/UI |
LevelEditorUI、editor/ui/*、NodeCanvas*、DialogEditor、AISyncPanel 等 |
必需。编辑器 UI、Inspector、面板控件都依赖它。 |
cjson |
AISyncCodec、LevelPatch、PatchPreview、Prefab |
必需。关卡 JSON、Prefab、Patch、AI Sync 编解码依赖它。 |
复用目标工程需要保证:
- 已能
require("urhox-libs/UI")。 - 已能
require("cjson")。 - UI 初始化流程由宿主工程完成。
4.2 当前仍未完全解耦的游戏专属依赖
| 依赖 | 当前使用位置 | 阻碍等级 | 说明 |
|---|---|---|---|
GameConfig |
editor-host/EditorHostAdapterImpl.lua |
已隔离 | 已通过 EditorHost.GetEditorConfig() 注入,editor/* 不再直接 require。 |
Animation |
editor-host/PreviewServicesAdapter.lua |
已隔离 | 已通过 EditorHost.GetPreviewServices().animation 注入,editor/* 不再直接 require。 |
Combat |
editor-host/PreviewServicesAdapter.lua |
已隔离 | 已通过 EditorHost.GetPreviewServices().combat 注入,editor/* 不再直接 require。 |
Targetable |
editor-host/PreviewServicesAdapter.lua |
已隔离 | 已通过 EditorHost.GetPreviewServices().target 注入,editor/* 不再直接 require。 |
menu.BackButton |
editor-host/EditorHostAdapterImpl.lua |
已隔离 | 已通过 EditorHost.CreateBackButton() 注入,editor-host/titlemenu/* 不再直接 require。 |
4.3 当前可接受的宿主侧依赖
这些依赖不属于通用编辑器核心,但可以留在当前项目宿主层中:
TitleMenu.lua
menu/*
GameState
Player
Renderer.lua
render/*
WorldMap
Enemy / BatEnemy / CastleEnemies
GMConsole
SpriteEditor
bootstrap/*
约束:
editor/*不应直接依赖这些模块。- 如果
editor/*需要能力,应通过editor-host.EditorHost暴露接口。 editor-host/EditorHostAdapterImpl.lua可以将接口转发给当前项目的TitleMenu/GameState/MenuFlow。
4.4 推荐最小复用包
通用编辑器核心包
scripts/editor/
scripts/editor-host/EditorHost.lua
scripts/editor-host/NullPreviewServices.lua
scripts/dialog/
scripts/effects/
scripts/NodeCanvas.lua
scripts/NodeCanvasCore.lua
scripts/NodeCanvasGraph.lua
scripts/NodeCanvasEditorBridge.lua
scripts/StrategyNode.lua
scripts/StrategyEditor.lua
前置依赖:
urhox-libs/UI
cjson
当前项目宿主适配包
scripts/editor-host/EditorHostAdapterImpl.lua
scripts/editor-host/titlemenu/*
scripts/render/OverlayBridge.lua
TitleMenu.lua 中的 EditorHost.Install / OverlayBridge.Register 逻辑
当前游戏专属预览依赖
scripts/GameConfig.lua
scripts/Animation.lua
scripts/Combat.lua
scripts/Targetable.lua
这层不建议长期作为通用编辑器核心,应在后续 M5 中替换为 Adapter。
4.5 复用前检查清单
- 目标工程是否可用
urhox-libs/UI? - 目标工程是否可用
cjson? - 是否提供了
EditorHost的宿主实现? - 是否替换或携带了
GameConfig? - 是否替换或携带了
Animation/Combat/Targetable? - 是否避免
editor/*直接 requireTitleMenu/GameState/Player? - 是否使用
render.OverlayBridge连接编辑器 overlay,而不是让Renderer.lua直接依赖菜单/编辑器?
4.6 M5 已实施:EditorConfig / PreviewServices / BackButton Port
M5 已将当前游戏专属依赖隔离到 editor-host 宿主适配层。核心结构如下:
EditorHost暴露复用 Port:
-- editor-host.EditorHost 中暴露
GetEditorConfig()
GetPreviewServices()
CreateBackButton()
- 当前项目在
EditorHostAdapterImpl中返回项目实现:
return {
getEditorConfig = function() return require("GameConfig") end,
getPreviewAnimationAdapter = function() return require("Animation") end,
getPreviewCombatAdapter = function() return require("Combat") end,
getPreviewTargetAdapter = function() return require("Targetable") end,
}
editor/EditorPreview.lua、editor/EditorRenderer.lua与editor/preview/*不再直接 require 游戏模块,改为:
local EditorHost = require("editor-host.EditorHost")
local config = EditorHost.GetEditorConfig()
local animation = EditorHost.GetPreviewAnimationAdapter()
local combat = EditorHost.GetPreviewCombatAdapter()
local targetable = EditorHost.GetPreviewTargetAdapter()
- 为通用复用提供空实现:
local NullPreviewAdapter = {
updateAnimation = function() end,
handleAttack = function() end,
registerTarget = function() end,
unregisterTarget = function() end,
}
- 迁移顺序建议:
GameConfig -> EditorConfig / GetEditorConfig
Targetable -> PreviewTargetAdapter
Combat -> PreviewCombatAdapter
Animation -> PreviewAnimationAdapter
当前代码已完成上述隔离。后续迁移到其它项目时,只需要替换 EditorHostAdapterImpl.lua 或提供新的宿主 Adapter;通用编辑器核心可使用 NullPreviewServices 作为无战斗/无动画的默认空实现。
4.7 当前迁移结论(2026-07-03)
当前编辑器可以迁移,但不应按“复制后零适配”的 SDK 使用。
推荐方式:
复制 editor 核心包
+ 复制 NodeCanvas / Strategy / dialog / effects
+ 复制 editor-host/EditorHost.lua 与 NullPreviewServices.lua
+ 为目标项目重写 EditorHostAdapterImpl.lua
+ 按目标项目数据结构适配 Import / Export / LevelPatch / UI 属性面板
不推荐直接复用当前项目的 TitleMenu.lua / main.lua 作为目标项目宿主;它们只作为接入参考。
五、物件系统
3.1 工具类型
TOOLS = {
{ id = "select", name = "选择" }, -- 选中/移动/调整
{ id = "platform", name = "平台" }, -- 可站立平台
{ id = "obstacle", name = "障碍" }, -- 伤害区域
{ id = "trigger", name = "触发器" }, -- 事件触发区
{ id = "executor", name = "执行器" }, -- 场景执行器
}
3.2 物件数据结构
{
objId = 1, -- 稳定唯一ID(nextObjId 递增分配,永不复用)
type = "platform", -- "platform" | "obstacle" | "trigger" | "executor" | "ground"
name = "platform1",
x = 5.0, y = 3.0, -- 世界坐标(米,Y-up 左手系)
w = 4.0, h = 0.5, -- 尺寸(米)
texLayers = {...}, -- 贴图图层列表(可选)
effects = {...}, -- 动效列表(可选)
-- trigger 专属:
triggerMethod = "touch",-- "touch" | "interact" | "attack"
triggerStrategy = {...},-- 策略树(StrategyNode.Serialize 格式)
triggerRepeat = false, -- 是否可重复触发
mappedExecutors = {3}, -- 关联的执行器 objId 列表
-- executor 无额外字段(由 trigger 的策略树驱动)
}
3.3 objId 规则(极重要)
- objId 是稳定引用:创建时由
nextObjId递增分配 - 永远不要用数组索引引用物件(排序/删除会导致索引错位)
- 策略节点中
targetObjIdx字段存储 objId(命名历史原因,实际是 ID) ObjIdUtil.FindByObjId(objects, id)用于按 ID 查找
六、贴图图层系统(texLayers)
4.1 数据结构
每个物件可叠加多个贴图图层:
{
path = "image/地图素材/平台-花.png", -- 资源路径
name = "花", -- 显示名
visible = true,
opacity = 1.0, -- 0~1
scaleW = 1.4, -- 宽度缩放倍数
scaleH = 3.5, -- 高度缩放倍数
offsetX = 0, -- X偏移(像素)
offsetY = 0, -- Y偏移(像素)
rotation = 0, -- 旋转角度(度)
lockAspect = false, -- 锁定宽高比
}
4.2 渲染逻辑
- 图层按数组顺序从下到上叠加渲染
- 支持锚点拖拽调整缩放/旋转(EditorTextureToolInteraction)
- NanoVG 中通过
nvgImagePattern绘制
4.3 素材库
customTextures = {
{ path = "image/背景/sky.png", name = "天空", cat = "bg" },
{ path = "image/地图素材/石砖.png", name = "石砖", cat = "tile" },
-- cat: "bg" | "tile" | "seq" | "solid" | "other" | "dlg_portrait" | "dlg_bg"
}
七、背景图层系统(bgLayers)
5.1 数据结构
{
path = "image/背景/sky.png",
name = "天空",
x = 0, y = 0, -- 世界坐标位置
w = 30, h = 17.5, -- 世界尺寸(米)
opacity = 1.0,
depth = 0.5, -- 视差深度:0=最远背景(不动),1=前景(同步相机)
visible = true,
lockAspect = true,
effects = {...}, -- 背景图层也支持动效
}
5.2 交互
- 编辑器中可拖拽移动、四角缩放
- 支持图层顺序调整(UI 列表拖拽)
- depth 值决定预览时的视差滚动速率
八、动效引擎(Effects)
6.1 注册式架构
-- effects/EffectRegistry.lua — 核心
Registry.Register("effect_id", {
name = "显示名",
params_schema = {
{ key = "amp", label = "振幅", default = 0.3, min = 0.05, max = 2.0 },
},
apply = function(t, params)
-- 返回: dx, dy, scale, angle, alpha, renderCtx
return 0, dy, nil, nil, nil
end,
})
6.2 内置效果
| ID | 名称 | 返回 | 参数 |
|---|---|---|---|
float |
浮动 | dy | amp, speed |
pulse |
脉冲缩放 | scale | min, max, speed |
rotate |
旋转 | angle | speed(度/秒) |
blink |
闪烁 | alpha | min, speed |
shake |
摇晃 | dx | amp, speed |
6.3 应用方式
物件/背景图层的 effects 数组,每帧调用 EffectRegistry.Apply(effects, t) 叠加所有效果的变换。
九、策略节点系统(StrategyNode + NodeCanvas)
7.1 节点类型完整列表
入口: event 数据: value, string, param, read_item 条件: compare, logic 运算: math, concat 流程: branch, sequence, random, delay, repeat_n, break_flow 动作: spawn, move_obj, set_var, play_fx, dialog, damage, win_level, camera_zoom, modify_item, set_ability, destroy_self, teleport_player, reset_trigger
7.2 端口类型与颜色
| 类型 | 颜色 | 说明 |
|---|---|---|
flow |
白色 {240,240,240} |
执行流 |
number |
绿色 {100,200,140} |
数值 |
boolean |
橙色 {220,140,80} |
布尔 |
string |
紫色 {180,130,220} |
文本 |
7.3 新增节点类型的步骤(OCP 扩展)
在 StrategyNode.lua 中同步添加四处:
- NODE_TYPES — 添加
{ label, color, category, desc, icon } - PORT_DEFS — 定义 inputs/outputs 端口
- INSPECTOR_FIELDS — 定义可编辑属性(支持 showWhen/hideWhen)
- NODE_DEFAULTS — 所有字段的默认值
如果节点有运行时行为,还需在 PreviewStrategy.lua 中添加执行逻辑。
7.4 Inspector 字段类型
| type | 控件 | 说明 |
|---|---|---|
float |
+/- 按钮 + 输入框 | min/max/step |
int |
+/- 按钮 + 输入框 | 整数 |
text |
TextField | 文本 |
bool |
切换按钮 | 布尔 |
select |
选项列表 | options 引用 M.XXX_OPS |
obj_select |
物件列表 | 选择关卡物件(存 objId) |
tex_select |
贴图列表 | 按 cat_filter 过滤 |
audio_select |
音效列表 | 已导入音效 |
path_editor |
路径点编辑器 | 根据 pathType 动态切换 |
action_button |
动作按钮 | 触发子编辑器 |
layer_opacity_list |
图层透明度 | 按目标物件图层动态生成 |
7.5 条件字段显示
{ key = "zoomCenterX", showWhen = { zoomUsePan = true } } -- 仅 zoomUsePan=true 显示
{ key = "pathPoints", hideWhen = { pathType = "none" } } -- pathType="none" 时隐藏
7.6 策略树序列化格式
{
"rootId": 3,
"params": [{ "name": "custom1", "value": 0, "label": "自定义1" }],
"nodes": [
{ "id": 3, "type": "event", "x": 100, "y": 200, "outputNode": 4 },
{ "id": 4, "type": "camera_zoom", "x": 337, "y": 119, "zoomScale": 0.5, ... }
]
}
7.7 求值引擎
- 数据节点(value/string/param/read_item/compare/math/logic/concat)立即求值返回结果
- 动作节点(spawn/move_obj/dialog/...)被
_collectActions收集为 action 列表 - 流程节点(branch/sequence/random/repeat_n)控制执行路径
- 运行时通过
M.Execute(tree, runtimeParams)获取 action 列表
十、对话系统
8.1 dialog 节点字段(完整)
行为配置:
dlgDisableMove(bool) — 禁用玩家输入dlgDurationMode("timed" | "click") — 持续模式dlgDuration(float) — 定时关闭秒数
整体底图: dlgWholeTexture, dlgWholeOffsetX/Y, dlgWholeOpacity, dlgWholeWidth/Height 背景框: dlgBgTexture, dlgBgOffsetX/Y, dlgBgOpacity, dlgBgWidth/Height 立绘: dlgPortraitTexture, dlgPortraitOffsetX/Y, dlgPortraitOpacity, dlgPortraitWidth/Height 名称文字: dlgSpeaker(内容), dlgNameOffsetX/Y, dlgNameOpacity, dlgNameFontSize, dlgNameFontColor, dlgNameStrokeW/Color 正文: dialogText(内容), dlgTextOffsetX/Y, dlgTextOpacity, dlgTextFontSize, dlgTextFontColor, dlgTextStrokeW/Color, dlgTextAnim, dlgTextAnimSpeed
8.2 运行时行为
- 队列机制 — 同一时间只能有一个活跃对话,
DialogManager._queue管理等待队列 - 两阶段点击 — 动画播放中点击 → 跳过动画显示全文;全文显示后点击 → 关闭对话
- 禁用移动 —
dlgDisableMove=true仅屏蔽方向/跳跃/攻击输入,不冻结物理和动画 - InputLocked 传递 — Player.lua 检查
GameState.movementLocked,PreviewUpdate.lua 单独检查
十一、镜头系统
9.1 cameraBounds(关卡级别配置)
cameraBounds = {
enabled = true,
x = -6.04, y = 12.28, -- 左上角世界坐标(Y-up)
w = 77.95, h = 36.30, -- 宽高(米)
}
编辑器中可拖拽四角调整范围框。
9.2 camera_zoom 策略节点
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| zoomScale | float | 1.0 | <1 缩小,>1 放大 |
| zoomUsePan | bool | false | 是否平移到指定中心 |
| zoomCenterX/Y | float | 15/8.75 | 平移目标世界坐标 |
| zoomDuration | float | 0.5 | 缩放过渡时间 |
| zoomEase | select | "easeOut" | 缓动类型 |
| zoomDisableMove | bool | false | 缩放期间禁用移动 |
| zoomAutoRestore | bool | false | 是否自动恢复 |
| zoomHoldDuration | float | 3.0 | 保持时间(autoRestore=true) |
| zoomRestoreDuration | float | 0.5 | 恢复过渡时间 |
| zoomRestoreEase | select | "easeOut" | 恢复缓动 |
9.3 DisableMove 实现
- Player.lua:
if ShouldLockMovement() then只屏蔽方向/跳跃/攻击输入 - PreviewUpdate.lua:
inputLocked = movementLocked or cameraLockMove独立检查 - PreviewRuntimeMotion.lua: 设置
GameState.cameraZoomLockMove
十二、预览模式
10.1 关键特征
- 预览模式有独立的输入处理(PreviewUpdate.lua),不走 Player.lua
- Box2D 物理引擎运行(平台碰撞、触发器检测)
- 策略树异步执行(delay 用协程/定时器,move_obj 用插值动画)
- PreviewRuntimeMotion 处理 move_obj 的路径动画(linear/bezier/circle/custom)
10.2 触发器执行流程
玩家进入触发区 → PreviewContact 检测碰撞
→ triggerMethod 校验(touch 直接触发 / interact 需要 F 键 / attack 需要攻击)
→ PreviewStrategy.ExecuteTrigger(objId)
→ StrategyNode.Execute(tree, runtimeParams) 收集 actions
→ 按序执行 actions(含异步等待 delay/dialog/move_obj)
十三、关卡 JSON 格式
文件路径: assets/levels/{chapter}_{level}.json
{
"version": 1,
"chapter": 1, "level": 1,
"chapterName": "序章", "levelName": "觉醒",
"worldW": 30, "worldH": 17.5,
"playerStartX": 2.0, "playerStartY": 5.0,
"playerOffsetY": 0, "playerRenderScale": 1.0,
"nextObjId": 29,
"cameraBounds": { "enabled": true, "x": ..., "y": ..., "w": ..., "h": ... },
"objects": [ ... ],
"bgLayers": [ ... ],
"customTextures": [ { "path": "...", "name": "...", "cat": "..." } ],
"levelMeta": { ... }
}
十四、EventBus 记录规范
所有编辑器操作必须通过 EventBus 记录(供 AI Sync 追踪):
local EventBus = require("editor.EventBus")
EventBus._appendLog({
type = "<操作类型>",
objId = <目标物件ID>,
-- ... 操作特定字段
}, { applied = 1, skipped = 0, errors = {} }, nil)
已覆盖的事件类型:
- 物件: obj_add, obj_delete, obj_move, obj_resize, obj_property_change
- 贴图: tex_layer_add, tex_layer_remove, tex_layer_change
- 背景: bg_layer_add, bg_layer_remove, bg_layer_change
- 策略: strategy_node_add, strategy_node_remove, strategy_node_change
- 镜头: camera_bounds_change
- 映射: mapping_change
- 参数: strategy_params_change
十五、新增功能检查清单
新增策略节点
-
StrategyNode.lua→ NODE_TYPES 加元数据 -
StrategyNode.lua→ PORT_DEFS 加端口定义 -
StrategyNode.lua→ INSPECTOR_FIELDS 加编辑字段 -
StrategyNode.lua→ NODE_DEFAULTS 加默认值 -
PreviewStrategy.lua→ 添加运行时执行逻辑 - EventBus 自动覆盖(NodeCanvas.applyField 已集成)
新增动效
-
effects/builtin.lua→ Registry.Register(...) 一行搞定 - 无需改动其他文件(OCP)
新增编辑器工具
-
EditorState.lua→ TOOLS 表添加条目 -
LevelEditorUI.lua→ 工具栏按钮 -
EditorInteractionPipeline.lua→ 对应的 Interaction 文件 - EventBus 记录操作
修改对话系统
-
DialogEditor.lua→ 图层编辑 UI -
DialogRenderer.lua→ NanoVG 渲染 -
DialogView.lua→ 运行时展示 - 注意队列机制和两阶段点击逻辑
十六、坐标系统
编辑器坐标
- 世界坐标: Y-up 左手系(Y+ = 向上),单位米
- 画布像素: 左上角为原点(Y+ = 向下),通过 worldW/worldH 映射
- 转换:
screenToWorld(sx, sy)/worldToScreen(wx, wy)(考虑 zoom/pan)
物件定位
obj.x, obj.y= 物件中心的世界坐标obj.w, obj.h= 物件尺寸(米)- 碰撞体在预览中自动根据 type 生成
NodeCanvas 坐标
- 节点位置
node.x, node.y是节点画布内部坐标(像素,非世界坐标) - 单独的
panX/panY/zoom控制视口
十七、常见修改场景
场景 A: 给 move_obj 新增一个参数
StrategyNode.lua→ INSPECTOR_FIELDS.move_obj 添加字段StrategyNode.lua→ NODE_DEFAULTS.move_obj 添加默认值PreviewRuntimeMotion.lua→ 使用新参数
场景 B: 新增一种触发方式
StrategyNode.lua→ TRIGGER_METHODS 添加选项PreviewContact.lua→ 添加新触发方式的检测逻辑- EditorState 物件属性面板 → triggerMethod 选项自动更新(引用 M.TRIGGER_METHODS)
场景 C: 新增背景图层效果
effects/builtin.lua→Registry.Register("new_effect", {...})- 完毕(编辑器 UI 自动从 Registry 读取可用效果列表)
场景 D: 修改预览模式输入
PreviewUpdate.lua→ 修改输入处理逻辑- 注意: 这里不走 Player.lua,是独立的输入系统
- 检查
inputLocked/cameraZoomLockMove状态
Known Issue:move_obj 默认值裁剪陷阱(2026-06)
问题
AI Sync grouped v4 会裁剪与 NODE_DEFAULTS 相同的字段。
对于 move_obj:
opacityTarget = 1.0
属于默认值,因此正常导出时会被省略。
如果接收端、离线脚本或 Patch 应用器没有正确合并默认值,最终会出现:
{
"type": "move_obj",
"opacityDuration": 2.2
}
而缺失:
"opacityTarget": 1
导致渐显动画失效。
典型症状
- 编辑器属性面板显示正常
- JSON 缺少
opacityTarget - 预览中
0 → 1渐显不执行 1 → 0渐隐正常
原因是:
opacityTarget = 0
属于非默认值,会被保留。
根因
协议允许裁剪默认值:
ExportService.stripDefaults
但接收端未正确补回:
NODE_DEFAULTS.move_obj
导致:
导出裁剪
+
落盘未补默认
+
预览依赖字段存在
=
渐显失效
协议约束
任何 AI Sync 接收端必须保证:
createNode
setNode
Deserialize
Patch Apply
与:
StrategyNode.NODE_DEFAULTS
使用同一份默认值定义。
禁止维护第二份手写默认值表。
推荐实现
创建节点时:
node = mergeDefaults(
StrategyNode.GetNodeDefaults(type),
node
)
而不是:
node = patchData
直接落盘。
排查顺序
遇到动画或节点行为异常时:
- 检查 JSON 是否缺少被裁剪字段
- 检查 NODE_DEFAULTS 是否一致
- 检查 Deserialize 是否补默认值
- 检查 Patch Apply 是否补默认值
- 检查 Preview 是否依赖字段存在
经验总结
AI Sync 可以安全裁剪默认值。
前提是:
默认值只允许存在一个权威来源(Single Source of Truth)
否则压缩协议会暴露实现层默认值不一致的问题。
参考文件
本 skill 文件夹内保留以下配套文档:
| 文件 | 用途 |
|---|---|
reusable-package-checklist.md |
最小可复用包清单,说明迁移时必须复制、可选复制和不建议复制的文件。 |
adapter-template.md |
目标项目 EditorHostAdapterImpl.lua 模板,包含必需接口、推荐接口、PreviewServices 模板。 |
demo-integration-example.md |
Demo 项目接入示例,说明如何安装 Host、初始化 UI、接入渲染和预览服务。 |
compression-safety-classification.md |
AI Sync 默认值裁剪安全模型,必须保留,用于处理 grouped v4 / Patch Apply 的默认值一致性问题。 |
当用户询问“如何迁移编辑器到其他项目”、“编辑器如何工程复用”、“Adapter 怎么写”、“Demo 怎么接入”时,优先阅读对应文档,而不是只依赖 SKILL.md 主体。