开发日记:(正面俯视角瓦片)地图的房间旋转实现(逻辑)
修改于05/2884 浏览开发日记

需求背景做一个像素风俯视角经营游戏,房间是 9x9 的瓦片方块,有一扇门。玩家建造时需要选择门朝哪个方向(下、左、上、右),这样可以灵活规划道路和布局。核心问题:一套墙壁素材,怎么实现四个方向的旋转?



思路:不旋转图片,旋转"摆放逻辑"最初想法是把贴图旋转 90/180/270 度——但俯视角瓦片游戏里,墙壁贴图有透视效果(顶部石头、底部草地),旋转图片会穿帮。最终方案:贴图不旋转,通过偏移实现视觉旋转。把一个房间拆成三层:层1: 地基(草地铺满,永不变化)
层2: 墙体(固定一张图,整体偏移)
层3: 门(4个方向各画一张,按方向切换)
素材设计一共只需要 6 张图:素材说明building_foundation.png9x9 草地纹理(铺满整个建筑区域)building_wall.png墙体,画在"门朝下"的默认位置door_bottom.png门朝下时的门door_top.png门朝上时的门door_left.png门朝左时的门door_right.png门朝右时的门关键设计:墙体图只画一张(默认 bottom 朝向),其他方向通过位移实现。墙体图的特征:
- 上边缘紧贴图片顶部(后墙)
- 下边缘留出 2 格绿化区空间(门口前方)
- 左右各留 1 格绿化区
旋转的核心:Layout 重新生成 + 贴图偏移第一层:逻辑层(Layout 数组)房间的每个格子有类型:wall(墙)、floor(地板)、door(门)、deco(绿化装饰区)。旋转时不是"把数组转 90 度",而是根据门朝向重新生成整个 layout:lua
复制-- 根据门朝向确定各方向的绿化厚度
if doorSide == "bottom" then
decoTop = 0; decoBottom = 2; decoLeft = 1; decoRight = 1
elseif doorSide == "top" then
decoTop = 2; decoBottom = 0; decoLeft = 1; decoRight = 1
elseif doorSide == "left" then
decoTop = 1; decoBottom = 1; decoLeft = 2; decoRight = 0
elseif doorSide == "right" then
decoTop = 1; decoBottom = 1; decoLeft = 0; decoRight = 2
end
规则很简单:门那一侧留 2 格绿化,对面留 0 格,左右各 1 格。墙壁紧贴绿化区内侧。这决定了碰撞体、寻路、放置检测等逻辑层行为。第二层:渲染层(贴图偏移)墙体素材只有一张(画的是 bottom 默认版本),其他方向通过整体平移来对齐:lua
复制-- 墙体偏移表(单位:格数)
local ds = building.doorSide
local dxTile, dyTile = 0, 0
if ds == "left" then
dxTile, dyTile = 1, 1 -- 右移1格,下移1格
elseif ds == "top" then
dxTile, dyTile = 0, 2 -- 下移2格
elseif ds == "right" then
dxTile, dyTile = -1, 1 -- 左移1格,下移1格
-- bottom 是默认位置,偏移为 (0,0)
end
原理:默认图的墙壁上边缘贴顶、下边缘留 2 格空间。当门朝上时,需要上面留空、下面贴底——只要把整张图往下推 2 格就行了。第三层:门贴图直接切换门比较特殊,4 个方向的形态差异大(横门 vs 竖门),所以直接准备 4 张小图,按方向选用:lua
复制-- 加载 4 张门图
for _, side in ipairs({"bottom", "top", "left", "right"}) do
buildingImgs[key].door[side] = nvgCreateImage(vg, "image/door_" .. side .. ".png")
end
-- 渲染时选对应方向
local doorImg = buildingImgs[key].door[doorSide]
旋转交互:NanoVG 按钮直接跟随建筑操作按钮(旋转/确认/取消)直接在 NanoVG 渲染层画,跟建筑预览同一个坐标系:lua
复制-- 按钮锚定在建筑右上角
local anchorX = ((previewX + width) * tileSize - camera.x) * zoom
local anchorY = ((previewY - 1) * tileSize - camera.y) * zoom
-- 画圆形按钮
nvgBeginPath(vg)
nvgCircle(vg, btnX, btnY, 20)
nvgFillColor(vg, nvgRGBA(60, 80, 120, 220))
nvgFill(vg)
点击检测用简单的圆形碰撞:lua
复制function BuildSystem:HitTestButtons(screenX, screenY, camera)
for _, btn in ipairs(buttons) do
local dx = screenX - btn.cx
local dy = screenY - btn.cy
if dx * dx + dy * dy <= radius * radius then
return btn.action
end
end
end
好处:不走 UI 层,不需要坐标转换,按钮天然跟着建筑走。
建筑实例存储旋转信息放置建筑时,把旋转相关信息存进实例:lua
复制local building = {
id = nextId,
templateKey = "hero_house",
rotation = 90, -- 旋转角度 (0/90/180/270)
doorSide = "left", -- 门朝向
layout = actualLayout, -- 实际使用的 layout(已旋转版本)
originX = 5, originY = 3,
}
为什么要存 layout?因为拆除时需要知道当初实际占了哪些格子。如果只存 templateKey 去查 Config,拿到的永远是默认 bottom 版本的 layout,旋转过的建筑拆除就会清错格子。
总结层次旋转方式说明逻辑层 (layout)重新生成根据 doorSide 计算绿化/墙壁/门的位置渲染层 (墙体)整体偏移一张图 + 4组偏移量,无需旋转图片渲染层 (门)切换图片4个方向各一张门图数据层 (实例)存储旋转建筑实例记录 rotation + layout 副本核心取巧点:俯视角瓦片游戏的"旋转"其实是"平移"。墙壁的视觉结构在上下左右是对称的(三面墙+一面开口),只需要把开口挪到对应方向——而"挪开口"在渲染上就是平移整张贴图的绘制起点。



