MapCalibrator 开发经验总结,直接作为 prompt 交给 AI 复刻

精华03/19100 浏览开发心得
NanoVG 地图编辑器/调校面板 — 架构模式速查一、核心架构:三文件分离MapData.lua        — 纯数据 + 坐标工具 + 持久化(不含任何绘制代码)
MapRenderer.lua    — 只负责渲染,从 MapData 读取数据驱动绘制
MapCalibrator.lua  — 编辑器 UI 叠加层,修改 MapData 中的数据
关键原则:渲染器不持有数据,编辑器不直接绘制游戏元素。三者通过 MapData 这个"单一数据源"解耦。二、数据表设计模式lua
复制-- 所有可编辑元素统一使用 zone-local 归一化坐标(0~1)
MapData.BUILDINGS = {
    { id = "唯一ID",        -- 持久化用,不可变
      name = "显示名",      -- UI 列表显示
      zone = "zoneName",    -- 所属区域名
      nx = 0.50, ny = 0.30, -- zone 内归一化坐标
      drawScale = 1.0,      -- 绘制缩放
      buildingType = "xxx", -- 类型分派键
      -- ...其他业务字段
    },
}
-- 第二类元素(景观/装饰)完全同构
MapData.LANDSCAPES = {
    { id = "唯一ID", name = "显示名", zone = "zoneName",
      nx = 0.30, ny = 0.65, drawScale = 1.0,
      landscapeType = "bamboo",        -- 类型分派键
      extra = { count = 9 },           -- 类型特有参数放 extra 表
    },
}
要点
  • nx/ny 是相对 zone 的 0~1 归一化值,不是绝对像素,方便 zone 整体移动/缩放
  • extra 表存放类型特有参数(竹子数量、花色、桥旋转角等),避免主表字段爆炸
  • id 用中文可读字符串,兼顾调试和持久化
复制-- 1. 归一化 → 绝对像素
function MapData.GetXxxPos(item)
    local z = MapData.ZONES[item.zone]
    return z.x + item.nx * z.w, z.y + item.ny * z.h
end
-- 2. 屏幕点 → 地图坐标
function MapData.ScreenToMap(sx, sy, cam, scrW, scrH)
    return (sx - scrW * 0.5) / cam.zoom + cam.x,
           (sy - scrH * 0.5) / cam.zoom + cam.y
end
-- 3. 命中检测(屏幕坐标输入)
function MapData.HitTestXxx(sx, sy, cam, scrW, scrH)
    local mx, my = MapData.ScreenToMap(sx, sy, cam, scrW, scrH)
    local hitR = 18 / cam.zoom  -- 命中半径随缩放自适应
    for i, item in ipairs(MapData.XXX) do
        local ix, iy = MapData.GetXxxPos(item)
        local dx, dy = mx - ix, my - iy
        if dx*dx + dy*dy <= hitR*hitR then return i end
    end
    return nil
end
-- 4. 实时更新单项
function MapData.UpdateXxx(index, fields)
    local item = MapData.XXX[index]
    if fields.nx then item.nx = fields.nx end
    if fields.ny then item.ny = fields.ny end
    if fields.drawScale then item.drawScale = fields.drawScale end
end
四、JSON 覆盖持久化模式lua
复制-- 设计思路:代码内保留默认值,JSON 只存用户修改的覆盖层
-- 好处:代码更新默认值不会被旧存档覆盖,新增元素自动使用默认值
function MapData.SaveLayout()
    -- 序列化所有元素的 id + nx + ny + drawScale
    local data = { version = 2, buildings = {...}, landscapes = {...} }
    local file = File("map_layout.json", FILE_WRITE)
    file:WriteString(cjson.encode(data))
    file:Close()
end
function MapData.LoadLayout()
    -- 反序列化后通过 id 索引表快速查找并覆盖
    -- 用 _buildingIdIndex[id] → 数组下标 的惰性索引加速
end
五、编辑器 UI 架构(MapCalibrator)5.1 状态设计lua
复制MapCalibrator.active = false          -- 模式开关
MapCalibrator.selectedType = nil      -- "building" | "landscape" | nil
MapCalibrator.selectedIdx = nil       -- 选中项在对应数组中的索引
MapCalibrator.dragging = false        -- 拖拽中
MapCalibrator.dragOffX/Y = 0          -- 拖拽偏移(防止元素跳到鼠标位置)
MapCalibrator.listScrollY = 0         -- 列表滚动
MapCalibrator.collapsedZones = {}     -- zone折叠状态 { [zoneName] = bool }
5.2 事件消费模式lua
复制-- 在 main.lua 的输入事件中:
if MapCalibrator.active then
    if MapCalibrator.HandleMouseDown(lx, ly, cam, w, h) then
        return  -- 消费了,不传给相机拖拽
    end
end
-- 未消费 → 正常相机拖拽逻辑
-- HandleMouseDown 内部返回规则:
--   UI 区域(工具栏/列表/面板)→ return true(总是消费)
--   地图区域命中元素 → return true(开始拖拽)
--   地图区域未命中 → return false(允许相机拖拽穿透)
5.3 拖拽实现(带偏移)lua
复制-- MouseDown 时记录偏移
local itemX, itemY = GetPos(item)
local mouseMapX, mouseMapY = ScreenToMap(lx, ly, cam, w, h)
dragOffX = itemX - mouseMapX   -- 关键:不是 0,而是差值
dragOffY = itemY - mouseMapY
-- MouseMove 时应用偏移
local mx, my = ScreenToMap(lx, ly, cam, w, h)
mx = mx + dragOffX  -- 加回偏移,元素不会跳
my = my + dragOffY
-- 反算回 zone-local 归一化坐标
local newNx = math.max(0.02, math.min(0.98, (mx - z.x) / z.w))
local newNy = math.max(0.02, math.min(0.98, (my - z.y) / z.h))
5.4 渲染分层lua
复制-- 在 MapRenderer.render() 中:
nvgSave(vg)
-- ...相机变换...
    -- 游戏内容渲染...
    MapCalibrator.RenderHighlight(vg, cam)  -- ① 相机空间内:虚线选框
nvgRestore(vg)
MapCalibrator.Render(vg, w, h, cam, fontSans) -- ② 屏幕空间:工具栏/列表/面板
5.5 折叠分组列表lua
复制-- 渲染逻辑:
for _, zoneName in ipairs(ZONE_ORDER) do
    -- 绘制 zone 标题(带 +/- 指示符 + 元素计数)
    -- 点击标题 → collapsedZones[zoneName] = not collapsedZones[zoneName]
    if not collapsed then
        -- 先渲染该 zone 的建筑(圆点标记 + 家族色)
        -- 再渲染该 zone 的景观(方块标记 + 绿色系)
    end
end
-- 命中检测逻辑必须与渲染逻辑完全同构(同样的遍历顺序和折叠跳过)
5.6 多类型选中的统一处理lua
复制-- 用 selectedType + selectedIdx 二元组标识选中项
-- 通过 getSelectedItem() 统一获取数据,避免到处 if/else
local function getSelectedItem()
    if selectedType == "building" then
        return MapData.BUILDINGS[selectedIdx], "building"
    elseif selectedType == "landscape" then
        return MapData.LANDSCAPES[selectedIdx], "landscape"
    end
end
-- 滚轮/面板/高亮等操作都通过这个统一接口
-- 只在最终调用 UpdateBuilding/UpdateLandscape 时分派
六、数据驱动渲染的分派表模式lua
复制-- 用类型字符串 → 绘制函数的映射表,替代 if/elseif 链
local LANDSCAPE_DRAW = {
    bamboo = function(vg, x, y, s, extra)
        drawBambooCluster(vg, x, y, s, extra and extra.count or 7)
    end,
    pineTree = function(vg, x, y, s) drawPineTree(vg, x, y, s) end,
    flowerTree = function(vg, x, y, s, extra)
        drawFlowerTree(vg, x, y, s, extra and extra.color)
    end,
    -- ...
}
-- 渲染时一行搞定
function drawLandscapeSprites(vg, zoneName)
    for _, ls in ipairs(MapData.LANDSCAPES) do
        if ls.zone == zoneName then
            local x, y = MapData.GetLandscapePos(ls)
            LANDSCAPE_DRAW[ls.landscapeType](vg, x, y, ls.drawScale, ls.extra)
        end
    end
end
七、给 AI 的一句话 Prompt 模板请为我的 NanoVG 2D 地图项目添加一个嵌入式调校面板(F1 切换),支持:
  1. 左侧按区域分组的元素列表,点区域名可折叠/展开
  2. 支持多种元素类型(建筑+景观),列表中用不同图标/颜色区分
  3. 在地图上拖拽调整元素位置(带偏移防跳),滚轮调缩放
  4. 右侧属性面板显示选中项的坐标和缩放值,带 +/- 按钮微调
  5. 关闭时自动 JSON 持久化,启动时加载覆盖默认值
  6. 采用三文件架构:数据层(MapData) / 渲染层(MapRenderer) / 编辑器层(MapCalibrator)
  7. 所有可编辑元素使用 zone-local 归一化坐标(nx/ny 0~1)
  8. 渲染层中的硬编码绘制调用全部改为数据驱动(分派表模式)
  9. 编辑器 UI 在屏幕空间绘制,选中高亮在相机空间绘制
输入事件使用消费模式:编辑器返回 true 消费,false 穿透给相机
horizontal linehorizontal line
以上就是完整的模式总结,涵盖数据设计、坐标系统、拖拽交互、折叠列表、持久化、渲染分层六大方面。下次新项目把这段贴给 AI 即可直接复刻。
猜你想搜
taptap 制造mapcalibrator开发经验
8
3
2