9.3 KiB
编辑器构件模块
脚本入口: eve.Editor()
引擎不附带完整 3D 场景编辑器或 2D 地图编辑器,而是提供组装自定义编辑器所需的构件。新的工具协议不在核心中枚举“瓦片笔刷、地形隆起、摆放单位”等工具类型;任何实现 IEditorTool 的代码都能进入同一个会话。
推荐把编辑器拆成五类可替换组件:
| 协议 | 职责 |
|---|---|
IEditorTool |
生命周期、输入与手势;原生或 Squirrel 工具一视同仁 |
IEditableTarget + capability |
被编辑对象;工具只查询自己需要的能力 |
IBrushKernel / IFieldBrushOperation |
笔刷形状、衰减和“画什么”分别组合 |
IEditCommand / IEditConstraint |
通用撤销事务与项目美术/玩法限制 |
IEditorOverlay / IEditorInspector |
与渲染器和 UI 框架无关的预览、属性呈现 |
因此红警 2 / 帝国时代 2 风格的格子地图、魔兽 3 风格的 3D 地图和连续高度场不需要三套会话。它们分别提供目标 capability、工具和视口坐标转换即可。
设计参考了 Three.js TransformControls、Babylon.js GizmoManager、ImGuizmo、Unity GridBrushBase 与 Godot 编辑器插件的常见 API 形态;详见编辑器模块设计。
基本用法
local editor = eve.Editor();
// 3D 操作框
local gizmo = editor.newGizmo();
gizmo.setMode("translate"); // translate | rotate | scale | bound
gizmo.setSpace("world"); // local | world
gizmo.setPosition(0, 1, 0);
gizmo.setSize(1.0);
// 每帧:用相机射线拾取并拖拽
local axis = gizmo.pick(rayOx, rayOy, rayOz, rayDx, rayDy, rayDz);
if (axis != "" && mouseDown) {
gizmo.beginDrag(axis, rayOx, rayOy, rayOz, rayDx, rayDy, rayDz);
}
if (gizmo.isDragging()) {
gizmo.updateDrag(rayOx, rayOy, rayOz, rayDx, rayDy, rayDz);
// 把 gizmo.getPosition* / getRotation* / getScale* 写回场景节点
}
if (mouseUp) gizmo.endDrag();
// 绘制:遍历 getPart* 用调试线 / 网格画出轴、环、平面
for (local i = 0; i < gizmo.getPartCount(); i++) {
local kind = gizmo.getPartKind(i); // axis | plane | ring | box | center | handle
// ...
}
// 高度图地形网格(配合 procgen.generateHeightmap / procgen.newHeightmap)。
// Smooth 变体用高度场梯度生成平滑顶点法线,坑/坡连续着色(无平直三角片)。
local mesh = editor.newHeightmapMeshSmooth(hm, 0.5, 3.2);
terrainEnt.setMesh(mesh);
// 原生圆形线性衰减笔刷;strength 为正时升高,为负时降低,返回实际修改的采样数。
local changed = editor.applyHeightmapBrush(hm, cellX, cellY, radius, strength);
// 高度图编辑后原地更新(复用 GPU 缓冲,指针不变)
if (changed > 0)
editor.updateHeightmapMeshSmooth(mesh, gfx, hm, 0.5, 3.2);
接口式工具会话(C++)
EditorSession session;
TileBufferTarget target("ground", &tiles);
ConstantBrushFalloff hardEdge;
CircleBrushKernel circle(&hardEdge);
PaintIntFieldOperation paintGrass(17);
FieldBrushTool paint("paint-grass", "Paint Grass", &circle, &paintGrass);
session.bindTarget(&target);
session.addTool(&paint); // 接受任意 IEditorTool
session.activateTool("paint-grass");
EditorPointerEvent down;
down.phase = EditorPointerEvent::Phase::Down;
down.x = tileX;
down.y = tileY;
session.dispatchPointer(down); // 自动开启一次可撤销 stroke
将 TileBufferTarget 换成 HeightmapTarget、把操作换成 AddScalarFieldOperation,同一个 FieldBrushTool 就成为带衰减的地形升降笔刷。项目也可以实现新的 IEditableTarget capability(对象放置、道路、区域、体素等)以及对应操作;无需修改 EditorSession。
项目限制
实现 IEditConstraint::evaluate() 并注册到 session.constraints()。约束可以允许、给出警告或拒绝任意 IEditCommand,例如锁定水岸坡度、限定可用 tile、吸附建筑朝向、阻止穿过地图边界。所有命令都经 EditorContext::execute() 进入约束和事务,拒绝的命令不会污染撤销栈。
自定义呈现
视口实现 IEditorOverlay,Inspector 实现 IEditorInspector。工具只输出圆、线、矩形、文本和属性意图,所以可同时接入 2.5D 正交视口、3D 透视视口、ImGui 或游戏自己的 UI。
Squirrel 自定义工具
脚本工具也实现相同的 IEditorTool 协议。回调返回位标志:1 表示已处理,2 表示捕获指针,4 表示释放指针。
local editor = eve.Editor();
local session = editor.newSession();
local road = editor.newScriptTool("road", "Road");
road.setActivateCallback(function() { previewRoad(); });
road.setPointerCallback(function(phase, pointerId, button, x, y, dx, dy,
pressure, shift, control, alt) {
if (phase == 0) { beginRoad(x, y); return 1 | 2; } // Down
if (phase == 1) { updateRoad(x, y); return 1; } // Move
if (phase == 2) { finishRoad(); return 1 | 4; } // Up
cancelRoad(); return 1 | 4;
});
session.addTool(road);
session.activateTool("road");
// 视口负责把屏幕/射线坐标变换到工具坐标后转发。
session.dispatchPointer(0, 0, 0, mapX, mapY, 0.0, 0.0, 1.0);
EditorSession 和工具都是非拥有关系;脚本必须像上例一样持有 road,直到从会话移除。
地图笔刷
旧的 Brush / EditorHistory API 为兼容已有脚本保留。新编辑器优先使用上面的协议式会话;旧 API 适合很小的纯 tile 工具。
local buf = editor.newTileBuffer(64, 64);
local brush = editor.newBrush();
brush.setTool("paint"); // paint | erase | fill | line | rect | stamp
brush.setTile(3);
brush.setSize(3);
brush.setShape("circle");
brush.paintAt(buf, tx, ty);
// 同步到 map.TileLayer(示例)
for (local y = 0; y < buf.getHeight(); y++)
for (local x = 0; x < buf.getWidth(); x++)
layer.setTile(x, y, buf.getGid(x, y));
编辑器壳
local dock = editor.newDock();
dock.setRegionSize("left", 200);
dock.layout(config.width, config.height);
// 用 dock.getRegionX/Y/W/H("center"|"left"|...) 放置 ui 窗口
local toolbar = editor.newToolbar();
toolbar.addTool("move", "Move");
toolbar.addTool("paint", "Paint");
toolbar.setShortcut("move", "W");
local insp = editor.newInspector();
insp.addChoice("mode", "Mode", "translate,rotate,scale", "translate");
insp.addFloat3("pos", "Position", 0, 0, 0);
对象关系
| 类型 | 职责 |
|---|---|
TransformGizmo |
单对象 TRS/包围盒手柄、射线交互、绘制部件 |
GizmoManager |
多 mode 开关 + attach,转发 pick/drag(Babylon 风格) |
TileBuffer |
与 map 解耦的 GID 网格 |
Brush |
笔刷工具;产出 preview / change 列表 |
EditorHistory |
撤销栈;瓦片分组可 applyLastToBuffer |
| Toolbar / Inspector / Dock | 仅状态与矩形,由 ui 渲染 |
目标导向指南
给场景节点加平移旋转缩放
创建 GizmoManager,attach() 后按需 setPositionEnabled / setRotationEnabled / setScaleEnabled;把相机 screenToRay 交给 pick / updateDrag,再写回 scene 节点 local TRS。
做简易 tile 地图编辑器
TileBuffer + Brush + EditorHistory:每次 paintAt 后把 getChange* 记入 beginGroup/recordTile/endGroup;Undo 时 applyLastToBuffer。用 EditorDock 划分调色板与视口,用 EditorToolbar 切换 paint/erase/fill。
常见问题
- 期望引擎弹出完整编辑器窗口——本模块只提供构件,需自行用
ui/graphics组装。 - 笔刷直接改
TileLayer——请先画在TileBuffer,再同步,避免模块硬耦合。 getPart*在 mode/TRS 变化后过期——调用rebuildParts()或任意 set*(多数 set 已自动重建)。
API 快查
- 模块:
newSession/newScriptTool/newGizmo/newGizmoManager/newTileBuffer/newBrush/newToolbar/newInspector/newDock/newHistory/applyHeightmapBrush/newHeightmapMesh/updateHeightmapMesh/newHeightmapMeshSmooth/updateHeightmapMeshSmooth - 会话:
addTool/removeTool/clearTools/activateTool/getActiveToolId/dispatchPointer/hasPointerCapture/update/cancelActiveTool/undo/redo - 脚本工具:
setShortcut/setActivateCallback/setDeactivateCallback/setPointerCallback/setKeyCallback/setUpdateCallback/setCancelCallback - Gizmo:
setMode/setSpace/setPosition/setRotationEuler/setScale/setBounds/setSnap*/pick/beginDrag/updateDrag/endDrag/getPart* - Manager:
set*Enabled/attach/detach/getGizmo/pick/beginDrag/updateDrag - Brush:
setTool/setSize/setShape/setTile/paintAt/eraseAt/floodFill/paintLine/paintRect/preview*/getChange* - History:
push/beginGroup/recordTile/endGroup/undo/redo/applyLastToBuffer
使用要点
- 模块与辅助对象应长期持有;不要每帧
newGizmo()。 - 枚举一律 string,非法值抛异常。
- 绘制与输入由宿主完成;本模块不做 GPU 提交。
源码: src/modules/editor/
设计文档: docs/dev/编辑器模块设计.md
相关测试: test/editor.cpp