You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何将鼠标坐标转换为适配菱形瓦片的等距瓦片坐标?

鼠标坐标转菱形等距瓦片坐标的实现方法

首先,我们先拆解你提供的绘制代码里的瓦片坐标映射逻辑,再逆向推导鼠标坐标到瓦片(x,y)的转换公式——毕竟要转回去,得先搞懂正向是怎么算的。

第一步:还原无相机、无缩放的世界坐标

你的绘制代码里,瓦片最终的屏幕坐标经过了缩放相机偏移和z轴高度调整,所以第一步要把鼠标的屏幕坐标转换回原始世界坐标(去掉相机和缩放的影响):

假设你的鼠标屏幕坐标是mouseScreenXmouseScreenY,步骤如下:

  • 先抵消缩放影响:把鼠标坐标除以相机的缩放因子
  • 再抵消相机偏移:减去相机的X/Y偏移量
  • 最后还原z轴高度的偏移(你提到目前所有瓦片高度都是1,直接加回对应的偏移值即可)

对应的代码片段:

auto zoom = camera.GetZoomFactor();
// 转换为无缩放的坐标
double unzoomedX = static_cast<double>(mouseScreenX) / zoom;
double unzoomedY = static_cast<double>(mouseScreenY) / zoom;

// 抵消相机偏移
double worldX = unzoomedX - camera.GetXOffset();
double worldY = unzoomedY - camera.GetYOffset();

// 还原z=1的高度偏移(当前所有瓦片高度一致)
worldY += TILE_HEIGHT / 2.0;

第二步:逆向求解瓦片(x,y)坐标

从你的绘制代码里,我们可以提取出瓦片索引(x,y)到世界坐标的核心公式(忽略相机、缩放和z轴):

int64_t xPos = (x * TILE_WIDTH / 2) + (y * TILE_WIDTH / 2); // 等价于 (x + y) * (TILE_WIDTH / 2)
int64_t yPos = (y * TILE_HEIGHT / 2) - (x * TILE_HEIGHT / 2); // 等价于 (y - x) * (TILE_HEIGHT / 2)

我们把这个公式写成方程组形式,设halfWidth = TILE_WIDTH / 2.0halfHeight = TILE_HEIGHT / 2.0,那么:

worldX = (tileX + tileY) * halfWidth
worldY = (tileY - tileX) * halfHeight

现在解这个方程组得到tileXtileY

  1. 把第一个式子变形:tileX + tileY = worldX / halfWidth
  2. 把第二个式子变形:tileY - tileX = worldY / halfHeight

将两个式子相加/相减,就能分别解出tileYtileX

tileY = ( (worldX / halfWidth) + (worldY / halfHeight) ) / 2.0
tileX = ( (worldX / halfWidth) - (worldY / halfHeight) ) / 2.0

第三步:处理整数瓦片索引与边界验证

计算出来的tileXtileY是浮点数,我们需要取整得到瓦片的索引。不过要注意:直接取整可能会误判边缘的相邻瓦片,所以最好先转成整数候选,再验证鼠标点是否在该瓦片的菱形范围内(追求精准的话建议加上这一步,不需要极致精准可以省略)。

另外,要确保最终的tileXtileY在地图尺寸范围内(0 ≤ tileX < mapSizeX0 ≤ tileY < mapSizeY),超出范围就说明鼠标在地图外。

完整的转换函数示例:

#include <cmath>

// 假设MousePos是包含x和y的结构体,比如struct { int x; int y; };
std::pair<std::size_t, std::size_t> ScreenToIsometricTile(const MousePos& mousePos, const Camera& camera, std::size_t mapSizeX, std::size_t mapSizeY) {
    const double halfWidth = TILE_WIDTH / 2.0;
    const double halfHeight = TILE_HEIGHT / 2.0;
    auto zoom = camera.GetZoomFactor();

    // 步骤1:转换为世界坐标
    double unzoomedX = static_cast<double>(mousePos.x) / zoom;
    double unzoomedY = static_cast<double>(mousePos.y) / zoom;
    double worldX = unzoomedX - camera.GetXOffset();
    double worldY = unzoomedY - camera.GetYOffset();
    worldY += halfHeight; // 还原z=1的高度偏移

    // 步骤2:求解瓦片坐标
    double tileX = ( (worldX / halfWidth) - (worldY / halfHeight) ) / 2.0;
    double tileY = ( (worldX / halfWidth) + (worldY / halfHeight) ) / 2.0;

    // 四舍五入得到整数候选索引(比直接截断更准确)
    std::size_t candidateX = static_cast<std::size_t>(std::round(tileX));
    std::size_t candidateY = static_cast<std::size_t>(std::round(tileY));

    // 步骤3:边界验证
    if (candidateX >= mapSizeX || candidateY >= mapSizeY) {
        // 返回无效坐标,用地图尺寸表示超出范围
        return {mapSizeX, mapSizeY};
    }

    // 可选:验证点是否在瓦片菱形内部(避免斜边缘误判)
    // 计算候选瓦片的世界坐标范围,判断鼠标点是否在其中
    // 示例逻辑(简化版):
    // auto tileWorldX = (candidateX + candidateY) * halfWidth;
    // auto tileWorldY = (candidateY - candidateX) * halfHeight;
    // double dx = std::abs(worldX - tileWorldX) / halfWidth;
    // double dy = std::abs(worldY - tileWorldY) / halfHeight;
    // if (dx + dy > 1.0) {
    //     // 不在当前瓦片内,需要检查相邻瓦片
    // }

    return {candidateX, candidateY};
}

补充说明

  • 如果以后瓦片有不同的z值(高度),步骤1里的worldY调整就需要动态处理了——因为你不知道目标瓦片的z值,这时候可以先按z=0计算候选瓦片,再遍历候选瓦片及其相邻的几个瓦片,验证哪个瓦片的菱形区域包含鼠标点。
  • 你的绘制循环里x是从mapSizeX-1倒序遍历的,这只是绘制顺序的问题,瓦片的索引(x,y)逻辑还是一致的,转换后的坐标可以直接用来调用GetTile(candidateX, candidateY)

内容的提问来源于stack exchange,提问作者D.G. Redd

火山引擎 最新活动