ROK Like沙盘地图菱形瓦片坐标转换实战:公式、服务端落地与调试

发布时间:2026/9/17 2:32:50
ROK Like沙盘地图菱形瓦片坐标转换实战:公式、服务端落地与调试 上个月联调沙盘地图功能时客户端同学跑过来问我“你们服务器下发的坐标到底是格子坐标还是画面坐标我直接用美术原图对上去差了半张地图。”这个场景我在几个 ROK Like 项目里都碰到过。问题根源在于地图在服务器里是一张二维网格显示在玩家屏幕上却是一张菱形瓦片拼出来的斜 45 度沙盘。菱形瓦片和笛卡尔坐标系之间并不是简单的等比缩放做转换时稍有不慎部队就会走到错误的地块上。这篇笔记想把 ROK Like 沙盘地图里最基础也最容易出错的菱形瓦片坐标转换讲透。内容包括等距投影公式的推导、服务器端协议/寻路/AOI 里的坐标落地策略以及一个可以直接打开的坐标调试工具。不管你是刚接手 SLG 服务端的新人还是被客户端坐标问题折磨过的老手这套东西拿去就能用。1. ROK Like 大地图为何需要单独搞定菱形瓦片转换1.1 一张菱形地图背后藏着两套坐标ROK万国觉醒以及一批 ROK Like 产品的沙盘地图视觉上都是菱形瓦片平铺出来的。美术切的每一块地表、每个资源点、每座蛮族营地都贴在菱形格子里玩家看过去整张地图是斜着铺开的。很多服务器开发第一次看到这种地图会下意识觉得“这只是表现层的事”但真正开始写玩法逻辑就会发现事情没那么简单。服务端的核心数据结构永远是一张二维数组。举个例子一张 1000x1000 的地图服务器里就是grid[1000][1000]下标从[0][0]到[999][999]地形、建筑、资源、NPC 全部挂在这个下标上下标就是格子的唯一身份证。但客户端拿到(col, row)之后要把它画到屏幕的哪个像素位置菱形格子的中心并不是简单把 col 和 row 乘以固定步长就能得到的因为斜向铺开的格子横向和纵向存在 45 度方向的耦合。于是就有了两套坐标系网格坐标逻辑格子下标(col, row)整数服务器只认这一套。世界坐标/屏幕坐标笛卡尔坐标系下的浮点坐标客户端渲染、点击判定、AOI 范围检测要用。两套坐标之间存在一个确定的数学映射。这个映射不整理清楚联调时就会出现格子和画面错位一整格、行军时间明显偏短或偏长、点击地块却选到隔壁格子的怪问题。1.2 服务器不处理转换会出什么问题有服务器同学会说“转换是客户端的事我反正只发(col, row)不就行了”我最初也这么想直到行军队列、集结、视野广播这些玩法开始联调才发现服务器根本躲不开坐标转换。先说一个最容易踩的坑行军时间计算。部队从 A 格走到 B 格路径长度是多少如果服务器只按网格下标差算一个“格子数”那这个格子数对应多少世界长度菱形地图里一个格子的宽度和高度是不一样的视觉上相邻两个格子的中心距离是sqrt(a*a b*b)这里 a 和 b 分别是菱形瓦片的水平半宽和垂直半高。如果把格子数和真实的笛卡尔距离搞混服务器算出来的行军时间和客户端表现会差得很离谱。再说 AOIArea of Interest兴趣区域广播。玩家周围一定范围内的其他玩家、建筑、资源点要同步给他范围判定通常用世界坐标画圆。这时服务器就必须把玩家的网格坐标转换成世界坐标做圆形范围过滤。反过来点击地面移动时客户端上报的如果是屏幕像素坐标服务器也要把它转换成网格坐标才能校验目标地块是否合法。协议设计也有讲究。我见过一个项目协议里直接传float x, float y世界坐标客户端收到后自己反推格子结果同样的坐标在不同机型上反推出不同的格子联调现场就吵起来了。后来改成协议里只传整数网格坐标世界坐标只在客户端渲染时用整个世界清净了。所以结论很明确ROK Like 服务器不是不需要坐标转换而是必须把转换规则定死并且自己手里要有调试工具。下面我把转换公式和实现完整讲一遍。2. 从网格坐标到世界坐标等距投影推导与 Lua 实现2.1 先统一坐标约定给公式之前必须把坐标系约定说死。我采用 SLG 项目里最常用的约定网格坐标(col, row)col 表示列向右增大row 表示行向下增大。世界坐标(x, y)x 轴向右y 轴向下单位与客户端渲染坐标一致。单个菱形瓦片水平半宽a tileWidth / 2垂直半高b tileHeight / 2。美术切出来的瓦片如果是正方形贴图旋转 45 度那a b但代码里不要依赖这个假设统一保留 a、b 两个参数更稳。一旦踩到美术用非正方形瓦片的项目公式里没有 a/b 的代码就直接废了。这个约定里网格坐标是整数、世界坐标是浮点服务器协议里只出现网格坐标。正变换网格到世界是基础几乎到处都要用逆变换世界到网格主要用于 AOI 和调试工具后面单独讲。2.2 正变换公式推导看网格坐标(col, row)的两个特殊方向就能把公式推出来。col 增加 1 时世界坐标 x 增加ay 增加b也就是列号增大的方向在屏幕上表现为“右下”row 增加 1 时x 减少ay 增加b也就是行号增大的方向表现为“左下”。当一个格子同时具有 col 和 row 时它相对原点格子的世界坐标就是把两个方向上的位移叠加x col * a row * (-a) (col - row) * a y col * b row * b (col row) * b这就是菱形瓦片等距投影的正变换公式。如果把a b 1代入会看到x col - rowy col row本质上就是把原来的方形网格水平旋转 45 度再做一个缩放。ROK 地图那种斜 45 度沙盘视角在数学上就是从这个变换来的。a 和 b 的作用只是按美术瓦片的真实宽高把旋转后的格子拉伸回来。这里有个容易混淆的点正变换得到的是格子的中心点坐标不是格子的左上角或者某个顶角坐标。为什么用中心点因为服务器逻辑里所有实体位置、路径点、攻击判定都以格子中心为锚点中心才算数。顶点坐标是美术渲染时关心的逻辑层用不上。2.3 Lua 实现与验证SLG 服务端常用 Lua配合 skynet 之类的框架。坐标转换这种纯函数建议单独成一个模块不要散落在各处。下面是一个可用的最小实现-- grid_coord.lua -- 菱形瓦片坐标工具服务器 Lua 侧 local M {} -- 美术瓦片参数按实际项目配置 local TILE_WIDTH 2 -- 菱形水平对角线长度 local TILE_HEIGHT 1 -- 菱形垂直对角线长度 local A TILE_WIDTH / 2 local B TILE_HEIGHT / 2 -- 网格坐标 - 世界坐标格子中心 function M.grid_to_world(col, row) local x (col - row) * A local y (col row) * B return x, y end -- 世界坐标 - 网格坐标 -- 返回离该世界坐标最近的格子下标 function M.world_to_grid(x, y) -- 四舍五入实现用 floor(v 0.5)在负数上表现更稳定 local col math.floor((x / A y / B) / 2 0.5) local row math.floor((y / B - x / A) / 2 0.5) return col, row end return M顺手写几组数据验证一下。设a b 1那么格(0, 0)中心在世界坐标(0, 0)格(1, 0)中心在(1, 1)也就是右下格(0, 1)中心在(-1, 1)也就是左下格(1, 1)中心在(0, 2)也就是正下方。注意最后一组(0, 0)和(1, 1)在地图上是竖直相邻的两个格子但它们在网格下标上相差(1, 1)而不是(0, 1)。这就是菱形坐标“斜着走”的体现也是很多新手写寻路时把邻居方向搞错的地方。ROK Like 的格子一共只有 4 个边邻居右下(col1, row)、左下(col, row1)、左上(col-1, row)、右上(col, row-1)。没有“正下方/正上方”的直接邻居想到达竖直方向的格子必须走两步。3. 逆变换取整陷阱中心锚点、边界归属和 round 选择3.1 逆变换的数学推导世界坐标反推网格坐标本质是解一个二元一次方程组。由x (col - row) * a y (col row) * b可以得到col (x / a y / b) / 2 row (y / b - x / a) / 2这两个式子算出来是浮点数。比如世界坐标(1.4, 0.6)在a b 1时算出来col 1.0row -0.4。接下来怎么做取整是全文最容易翻车的地方。3.2 为什么网上的资料说 floor我在 ROK Like 项目里却用 round网上很多等距瓦片教程会告诉你反解之后直接floor这和它们的锚点定义有关。那些教程往往把世界坐标定义在瓦片的左上角顶点或者使用的是另一种 2:1 交错坐标系此时floor是正确的。但我们在服务器逻辑里约定的是格子中心锚点情况就不同了。还是用刚才的例子。世界坐标(1.4, 0.6)最近的格子中心是哪个格(1, 0)的中心在(1, 1)L1 距离是|1.4-1| |0.6-1| 0.8格(1, -1)的中心在(2, 0)L1 距离是|1.4-2| |0.6-0| 1.2显然应该归到格(1, 0)。但逆变换算出col 1.0, row -0.4如果按网上的做法直接floor得到(1, -1)错了一格。用math.floor(row 0.5)也就是四舍五入得到(1, 0)才是对的。这个差异的本质是在你用正变换公式把格点映射到“格子中心”之后格子归属实际上是由世界坐标点到所有格子中心的 L1 距离决定的离哪个中心近就属于哪个格子。而 L1 度量的最近邻问题最优解法就是对每个连续坐标分量做四舍五入不是向下取整。网上那些用floor的资料多半用了不同的锚点约定照搬过来就会吃大亏。3.3 边界归属与负坐标处理四舍五入也不是银弹边界情况必须提前定规则。比如一个世界坐标恰好落在两个菱形格子共享的边上L1 距离相等四舍五入的行为在正负半轴上可能不一致。Lua 的math.floor(v 0.5)在正数区域是“四舍五入”在负数区域其实是对v 0.5向下取整比如-0.5会得到0-1.5会得到-1。这种行为总体可接受但规则要写进文档里不然后续接手的人被边界数据弄得一头雾水。我的做法是两条补充规则边界点一律归属到坐标值较大的格子。也就是两个格子共享边时统一算给 col/row 更大的那个。floor(v 0.5)配合整数偏移可以实现这个效果只要大家统一用同一个取整函数就行。服务器任何时候都不依赖客户端上报世界坐标反推格子。客户端点击后在自己本地换算成网格坐标上报给服务器的是(col, row)。这样边界歧义就只发生在客户端服务器永远拿整数格子做校验不会出现两边各执一词的情况。另外地图坐标系的原点通常在左上角但右下角以外的区域、负坐标区域也可能因为浮点误差计算出合法的格子下标。服务器做合法性校验时一定要再检查一遍“col 和 row 是否在地图范围内”不要只信转换函数的结果。4. 服务器实战落地协议、寻路、AOI 怎么用这套坐标4.1 协议层只传网格坐标不传世界坐标我前面提了一句“协议里只传网格坐标”这里展开说清楚优先级。ROK Like 的玩家操作比如移动、采集、攻击客户端发给服务器的最核心目标信息都应该是格子下标(col, row)或者更稳妥一点直接传目标实体 ID。服务器拿到目标实体 ID自己查表得到它的格子坐标这样连取整都不用做。如果某些玩法确实需要传坐标比如自由行军落点协议结构应该是message MoveRequest { int32 target_col 1; int32 target_row 2; }不要设计成float world_x; float world_y;这种带小数点的字段。浮点坐标有几个问题第一同样的数值在不同精度策略下可能反推出不同格子第二协议抓包时小数可读性差排查问题要多一道转换第三服务器一旦直接信任浮点坐标就需要在入口做合法性校验反而多写一堆代码。整数格子下标天生是离散的、可枚举的校验起来非常简单。4.2 寻路与距离计算在网格域操作别去世界域折腾ROK Like 的大地图寻路常规做法就是在网格坐标上跑 A*。格子只有 4 个边邻居方向数组很好写local DIRS { { 1, 0 }, -- 右下 { 0, 1 }, -- 左下 { -1, 0 }, -- 左上 { 0, -1 }, -- 右上 }启发函数用曼哈顿距离local function heuristic(col, row, target_col, target_row) return math.abs(col - target_col) math.abs(row - target_row) end这个曼哈顿距离看起来简单但它之所以正确正是因为菱形地图上一格一步的实际移动量刚好就是网格坐标差。不要试图把 A* 搬到世界坐标里去跑虽然也能跑通但边界判定、邻居生成、碰撞检测全部要重写得不偿失。行军时间的计算也建议基于格子数。路径格子数算出来后乘以“每格行军耗时”常量就得到总耗时。ROK Like 里不同地形往往有不同移速这个常量按地形类型拆开放地形表里但本质还是“格子数 × 单格耗时”。只有视野同步、技能范围这类真正需要世界距离的玩法才把网格坐标转成世界坐标做圆形范围判定。4.3 AOI 范围判定先粗筛再精筛AOI 是一个典型的混合坐标场景。玩家在某个格子视野半径是 R世界单位要把该半径内的所有实体同步给他。实现套路可以分两步。第一步把玩家所在网格坐标转换成世界坐标得到视野中心。第二步粗筛用一个足够大的矩形范围把候选格子框进来。矩形范围怎么定可以在网格域先估计一个范围。因为 col 和 row 与世界坐标的换算系数都是 a、b 量级候选格子范围可以保守取local K math.ceil(R / math.min(a, b)) 1 local min_col, max_col player_col - K, player_col K local min_row, max_row player_row - K, player_row K第三步精筛遍历候选格子里所有实体把实体格子转成世界坐标判断与视野中心的欧氏距离是否小于 R。小于则进入 AOI 同步列表。这个“粗筛 精筛”的策略比直接遍历全地图或者直接用网格距离代替世界距离都靠谱。直接用网格距离代替世界距离视野形状会变成一个菱形玩家会感觉左右两侧的视野比别人宽一截直接遍历全地图1000x1000 的地图在人数上来后就扛不住了。5. 坐标调试工具一张网页把整张菱形地图可视化5.1 为什么需要一个专用调试工具而不是抱着日志看坐标系 Bug 有个特点用日志排查效率特别低。因为问题往往出现在协议、取整规则、坐标约定三者的交叉点上直接看(col, row)数据看不出毛病必须把数值“画出来”才能发现规律。比如之前某个项目里所有实体都沿着地图对角线串位光看日志很难意识到是行列字段在序列化时反了但把点画在调试画布上一眼就看出所有点沿对角线镜像了。服务端排查问题常见选择是 Lua 命令行脚本、或附加到进程上的调试器。但坐标系这种几何问题真正有效的却是一张能画出菱形格子、能交互点击的调试面板。我自己在项目里养成了一个习惯所有涉及地图坐标的服务端模块都配套一个可视化调试工具坐标相关疑难杂症直接打开工具验证比抱着日志看半小时有用得多。调试工具选型可以按场景挑工具形态适用场景上手成本Lua 命令行脚本批量验证公式、处理线上日志里的坐标样本低Web Canvas 面板客户端/策划/服务端联调时直观可视化中协议日志解析脚本从线上协议 dump 里提取坐标并离线分析低我的诉求是“所有端都能直接打开”所以选择了 Web Canvas 方案。一张 HTML 文件双击就能跑不需要装任何依赖策划、客户端、服务端可以共用。5.2 Canvas 调试页核心实现一个最小可用的调试页核心就三块画菱形网格、网格转世界、世界转网格。画菱形网格的代码很简单function drawDiamond(ctx, cx, cy, a, b) { ctx.beginPath(); ctx.moveTo(cx, cy - b); ctx.lineTo(cx a, cy); ctx.lineTo(cx, cy b); ctx.lineTo(cx - a, cy); ctx.closePath(); }整个网格用双重循环遍历当前显示范围所有格子把每个格子的中心算出来再画菱形。坐标转换函数和 Lua 版保持一致function gridToWorld(col, row, a, b) { return { x: (col - row) * a, y: (col row) * b }; } function worldToGrid(x, y, a, b) { const col Math.floor((x / a y / b) / 2 0.5); const row Math.floor((y / b - x / a) / 2 0.5); return { col, row }; }页面里再加上两类交互鼠标在 Canvas 上移动时实时显示当前世界坐标和换算出的网格坐标输入(col, row)或(x, y)时在画布上高亮对应格子。有了这两个交互联调时最常见的三个问题都能快速验证给定网格坐标客户端画的位置对不对点击屏幕某个位置换算出的格子是不是预期格子批量日志里导出的坐标转来转去是否自洽。我实际项目里这个页面大概 150 行代码核心部分就是上面这些。完整代码我会放到文章最后给出来复制保存成 HTML 直接就能用。5.3 用调试工具当场定位一个转换 Bug说一个真实案例。某次联调客户端同学反馈“点击地图最右侧地块角色却移动到地图下方”。我用调试工具复现在工具里输入客户端点击位置解析出的世界坐标(398, 200)换算出来是格(299, -99)而手动输入格(299, 99)换算出的世界坐标是(200, 398)。两边数值看起来完全对不上但把坐标列出来就明白了客户端上报时把 x 和 y 填反了世界里 x 和 y 对应的是 col 和 row 的“和/差”关系一填反对角线方向就串位。这个场景如果没有调试画布靠看日志至少要多花半天。把点画出来规律是现成的。所以我很坚持一点坐标系这种事情不要靠脑子想画出来才是王道。6. 调试工具抓到的那些坐标系 Bug 与复盘6.1 “整体偏移半格”的经典问题联调中频繁出现的一类 Bug是地图上所有建筑、地块整体偏移半格。原因绝大多数出在锚点约定不统一。正变换算出来的是格子中心但美术导出的配置表里存的地块坐标可能是格子左上角顶点也可能是菱形左上角顶点。两者相差(a, b)或(0, b)之类的固定偏移。处理方法很简单配置表导入时统一做一次偏移校正把所有坐标都转成格子中心语义并用调试工具抽查几个已知格子验证。注意这类偏移 Bug 在格子总数大的时候不容易被发现因为整体看起来“差不多”只有点到具体地块时才会暴露。6.2 序列化字段顺序不一致引发的镜像问题前面提到的行列反转属于这一类。服务端和客户端对(col, row)的定义不一致一个认为 col 在前另一个认为 row 在前协议解析时字段顺序写反表现出来就是全地图沿对角线镜像。这个 Bug 好排查但容易反复。我的建议是协议定义文件里给字段注释写清楚“col 为列号、row 为行号”并且调试工具里把网格坐标的显示格式固定成(col, row)不要随手写成(row, col)。看似小事统一了能少吵很多架。6.3 浮点误差、边界取整和地图外坐标最后说浮点和边界。等距投影公式本身是线性变换不会累积误差但逆变换除以 a、b 再相加时浮点精度在极端坐标下可能产生偏差。比如a 0.3, b 0.3这类参数除出来的数可能带循环小数四舍五入时偶尔错一格。规避方式不是提高精度而是让服务器逻辑尽量少做“世界坐标转网格坐标”只在必要的入口比如 AOI 初始化、客户端上报世界坐标的兼容层做转换并且做完之后立即做边界合法性校验。地图外的坐标也要考虑。玩家点击地图边缘外侧worldToGrid照样会返回一个格子下标可能是负的也可能超出地图范围。服务器拿到这种下标要能识别并拒绝不要让它进入到寻路或 AOI 流程里。根据我个人的经验开发 ROK Like 服务器时坐标系这块最大的成本不是公式本身而是“约定统一”。公式十行代码就能写完但让服务端、客户端、策划、QA 四拨人在同一套坐标系语义下工作需要文档、调试工具和反复联调。把调试工具提前做好其实是在给自己省时间。后续如果项目里要加六边形地块、分层地图、或者跨服大地图这套工具稍作扩展也能继续用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询