C++瓦片地圖坐標(biāo)轉(zhuǎn)換的實(shí)現(xiàn)詳解
一、前言
嚴(yán)格來說,瓦片的角度并不是45度。因?yàn)闉榱嗣佬g(shù)作圖方便,圖片的寬高比一般為2:1,如下圖所示,它的實(shí)際角度為arctan(1/2),不過這個(gè)數(shù)值對(duì)我們不重要。正如魚香肉絲沒有魚一般,叫它45度瓦片也無妨,由于它是一個(gè)菱形,所以這里我們稱它為菱形瓦片。

寬高比為2:1的菱形瓦片
或許有人認(rèn)為任意角度的瓦片都是可以的,其實(shí)不然,因?yàn)槲覀円紤]線條鋸齒的畫法,如果采用非整數(shù)比,則線條不是規(guī)律的(非像素游戲或許可以試試)。所以最常見的比例為2:1,其次是1:1。
還有一個(gè)問題,我們觀察菱形的四分之一部分,它將一個(gè)矩形一分為二。我們當(dāng)然期望它是平分的,然而這根本做不到,因?yàn)樗皇抢碚摰膶?duì)角線。對(duì)于正方形瓦片來說,邊緣是不會(huì)重疊的。而菱形瓦片不可避免的邊緣存在重疊。

邊緣必然重疊
二、定義
我們定義地圖上的一個(gè)點(diǎn)為世界(World)坐標(biāo),它是連續(xù)的,用浮點(diǎn)數(shù)表示。然后格子的索引叫地圖(Map)坐標(biāo),它是離散的,用有符號(hào)整數(shù)表示。不過這里地圖坐標(biāo)的取值未考慮負(fù)數(shù),如要使用負(fù)數(shù)的地圖坐標(biāo)則需要對(duì)代碼略微修改。
比如下圖的p點(diǎn),我們假設(shè)格子寬10像素。則其世界坐標(biāo)為(54,67),而地圖坐標(biāo)為(5,6)。

矩形瓦片示例
三、矩形瓦片
矩形瓦片的代碼很簡(jiǎn)單,如下:
//! 矩形瓦片地圖
template<Vector2 TILE_SIZE>
class Rectangle
{
public:
/**
* @brief 地圖坐標(biāo) -> 世界坐標(biāo)
*/
constexpr Vector2 Map2World(const Point& xy)
{
return toVector2(xy) * TILE_SIZE;
}
/**
* @brief 世界坐標(biāo) -> 地圖坐標(biāo)
*/
constexpr Point World2Map(const Vector2& pos)
{
return toPoint(pos / TILE_SIZE);
}
};四、菱形瓦片
1.斜菱形瓦片
這里的斜指的是,整個(gè)地圖拼出來是斜著的,也是一個(gè)菱形,如下圖所示(這是常用的算法):

斜菱形瓦片
我們令x'y'為地圖(格子)坐標(biāo),xy為世界(像素)坐標(biāo),其中wh為瓦片寬高,則有如下關(guān)系:

上面這個(gè)式子通過簡(jiǎn)單的變換,就可以得出:

轉(zhuǎn)換代碼如下,這里就體現(xiàn)出了將瓦片大?。═ILE_SIZE)作為模板的好處了,其中除2的操作會(huì)自動(dòng)合并為常量表達(dá)式,世界坐標(biāo)到地圖坐標(biāo)的轉(zhuǎn)換其中加了0.5,是為了四舍五入。
//! 斜45度瓦片地圖
template<Vector2 TILE_SIZE>
class DiamondSlant
{
public:
/**
* @brief 地圖坐標(biāo) -> 世界坐標(biāo)
*/
constexpr Vector2 Map2World(const Point& xy)
{
return { (xy[1] + xy[0]) * TILE_SIZE[0] / 2.0, (xy[1] - xy[0]) * TILE_SIZE[1] / 2.0};
}
/**
* @brief 世界坐標(biāo) -> 地圖坐標(biāo)
*/
constexpr Point World2Map(const Vector2& pos)
{
Vector2 xy_div = pos / TILE_SIZE;
return toPoint(Vector2{ xy_div[0] - xy_div[1] + 0.5, xy_div[0] + xy_div[1] - 0.5 });
}
};2.正菱形瓦片
下面這種整體也是一個(gè)矩形,它的特點(diǎn)是x軸移動(dòng)瓦片寬度,y軸只移動(dòng)半個(gè)瓦片高度,當(dāng)y為奇數(shù)時(shí),x再往右移動(dòng)半個(gè)瓦片寬度。(有些文章是y為偶數(shù)時(shí)x移動(dòng),原理相同)

正菱形瓦片
容易得到,從格子坐標(biāo)到世界坐標(biāo),如下:
當(dāng)y為偶數(shù)時(shí):

當(dāng)y為奇數(shù)時(shí):

這里出現(xiàn)和上面不一樣的事了,無法簡(jiǎn)單的逆推公式來表示x'y'。因?yàn)橥ㄟ^世界(像素)坐標(biāo)無法輕松得到它的地圖(格子)坐標(biāo)的y是奇數(shù)還是偶數(shù)。
從格子坐標(biāo)到世界坐標(biāo)的代碼如下:
/**
* @brief 地圖坐標(biāo) -> 世界坐標(biāo)
*/
constexpr Vector2 Map2World(const Point& xy)
{
Vector2 pos = { TILE_SIZE[0] * xy[0] , TILE_SIZE[1] / 2 * xy[1] };
if (xy[1] % 2 != 0)
{//奇數(shù)行向右偏移 w / 2
pos[0] += TILE_SIZE[0] / 2;
}
return pos;
}而從世界坐標(biāo)到格子坐標(biāo)則比較麻煩了,如下,我們劃分網(wǎng)格:

劃分網(wǎng)格
明顯格子大小為(w,h),記世界坐標(biāo)pos所在的格子為p,則有:

來看單個(gè)劃分網(wǎng)格內(nèi),如下:

單個(gè)劃分格子
設(shè)瓦片格子坐標(biāo)為xy,則當(dāng) pos在菱形內(nèi)時(shí),有:

當(dāng) pos在菱形外時(shí),四個(gè)角則分別判斷:右下角偏移(0,1);左下角偏移(-1,1);左上角偏移(-1,-1);右上角偏移(0,-1)。
所以最終實(shí)現(xiàn)代碼如下:
//! 平菱形瓦片地圖
template<Vector2 TILE_SIZE>
class DiamondFlat
{
public:
/**
* @brief 地圖坐標(biāo) -> 世界坐標(biāo)
*/
constexpr Vector2 Map2World(const Point& xy)
{
Vector2 pos = { TILE_SIZE[0] * xy[0] , TILE_SIZE[1] / 2 * xy[1] };
if (xy[1] % 2 != 0)
{//奇數(shù)行向右偏移 w / 2
pos[0] += TILE_SIZE[0] / 2;
}
return pos;
}
/**
* @brief 世界坐標(biāo) -> 地圖坐標(biāo)
*/
constexpr Point World2Map(const Vector2& pos)
{
constexpr Vector2 TILE_SIZE_HALF = TILE_SIZE / 2.0;
//四分之一矩形面積
constexpr real s = Each::AccumulateMul(TILE_SIZE_HALF);
//先計(jì)算矩形下標(biāo)
Point p = toPoint(pos / TILE_SIZE);
//在矩形內(nèi)坐標(biāo)
Vector2 p1 = pos - toVector2(p) * TILE_SIZE - TILE_SIZE_HALF;
//點(diǎn)圍成矩形面積
real sp = abs(p1[0] * TILE_SIZE_HALF[1]) + abs(p1[1] * TILE_SIZE_HALF[0]);
p[1] *= 2;
if (s < sp)
{
if (p1[0] > 0 && p1[1] > 0)
return p + Point{ 0, 1 };
else if (p1[0] < 0 && p1[1] > 0)
return p + Point{ -1, 1 };
else if (p1[0] < 0 && p1[1] < 0)
return p + Point{ -1, -1 };
else if (p1[0] > 0 && p1[1] < 0)
return p + Point{ 0, -1 };
else
return p;
}
else
{
return p;
}
}
};五、點(diǎn)在菱形內(nèi)判斷
如下圖所示,以菱形中心為原點(diǎn)建立坐標(biāo)系:

p在對(duì)角線上時(shí)
當(dāng)p點(diǎn)在菱形上時(shí),紅綠區(qū)域面積相等(對(duì)角線平分面積),所以:

(紅色區(qū)域加了兩次,將其中變成一個(gè)綠色區(qū)域)
則當(dāng)p點(diǎn)在菱形外時(shí),

;在菱形內(nèi)時(shí)

源碼位置:傳送門
到此這篇關(guān)于C++瓦片地圖坐標(biāo)轉(zhuǎn)換的實(shí)現(xiàn)詳解的文章就介紹到這了,更多相關(guān)C++坐標(biāo)轉(zhuǎn)換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C++ STL vector容量(capacity)和大小(size)的區(qū)別
這篇文章主要介紹了詳解C++ STL vector容量(capacity)和大小(size)的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
C++設(shè)計(jì)一個(gè)簡(jiǎn)單內(nèi)存池的全過程
利用C/C++開發(fā)大型應(yīng)用程序中,內(nèi)存的管理與分配是一個(gè)需要認(rèn)真考慮的部分,下面這篇文章主要給大家介紹了關(guān)于C++設(shè)計(jì)一個(gè)簡(jiǎn)單內(nèi)存池的全過程,需要的朋友可以參考下2021-09-09
c++中new一個(gè)結(jié)構(gòu)體初始化過程
這篇文章主要介紹了c++中new一個(gè)結(jié)構(gòu)體初始化過程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
C語言實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
詳解C++中的成員訪問運(yùn)算符和指針到成員運(yùn)算符
這篇文章主要介紹了C++中的成員訪問運(yùn)算符和指針到成員運(yùn)算符,即. 和 ->以及.* 和 ->*的使用方法,需要的朋友可以參考下2016-01-01
C++類與對(duì)象深入之構(gòu)造函數(shù)與析構(gòu)函數(shù)詳解
朋友們好,這篇播客我們繼續(xù)C++的初階學(xué)習(xí),現(xiàn)在對(duì)我們對(duì)C++非常重要的一個(gè)知識(shí)點(diǎn)做出總結(jié),整理出來一篇博客供我們一起復(fù)習(xí)和學(xué)習(xí),如果文章中有理解不當(dāng)?shù)牡胤?還希望朋友們?cè)谠u(píng)論區(qū)指出,我們相互學(xué)習(xí),共同進(jìn)步2022-06-06

