a

2011年3月1日 星期二

讓 2D Server 變成偽 3D Server

Server 會使用數格子的原因其來有自, 不過數格子的方式已不太能滿足現今 3D MMORPG 的需求, 不但功能被限制住了, 很多可以做的很精彩的 Gameplay 也被綁死, 在數據精細度上也不夠, 因此如果要讓 Gameplay 豐富一點的話, Server 就必須做一些改變.

如果 Server 2D 數格子的架構, 並且也沒有浮點數的情況下, 該如何讓 2D Server 變成3D Server ? 下面將詳細的解說做法, 雖然沒有實作, 不過應該是可行的, 而且是在 Server 不需大修改, 只要小小的增加新功能的情況下即可完成, 這方法也適用在所有的 2D Server.

所謂的數格子就是如同很久以前的 2D RPG 遊戲一樣, 走一步就是一格, 打怪的距離是用格子計算, 施法的距離也是用格子計算, 這些在 2D MMORPG 中都算正常, 但是在 3D MMORPG 中可就不一樣了.



如上圖, 這是一張由上往下看的俯視圖, 紅色代表敵人, 藍色代表玩家, 兩個都是近戰系的攻擊方式, 假設攻擊距離是 2, 敵人可以打到玩家, 玩家也可以打到敵人, 看起來是很正常.



如上圖, 當視角切換到側面的時候觀看, 一個在山頂, 一個在山谷, 使用目測就可以很清楚的判斷兩者的距離絕對是超過攻擊距離, 但是為何彼此還能夠互打呢? 這就是 2D Server 直接拿來做成 3D MMORPG 所產生的結果.


這個 Bug 可以避免, 比如說把敵人種遠一點, 或是中間有阻檔牆等等都可以輕易解決, 但是這樣的結果卻大大了限制住 3D MMORPG 應該可以有的 Gameplay.

現在有一個可以彌補這個缺陷的方式, 就是我們讓 2D Server 變成3D Server, 會稱為偽 3D是因為它並不是真正的 3D, 而是我們透過一些方法來讓它真的很像 3D;



想像一下 2D Server, 角色是存在於一格一格的格子之中, 就如同一個棋盤一般





現在把這格子給 3D , 角色存在的位置就代表一個小立方體.





再想像一下, 如果角色是存在於一個 n x n 格的立方體陣中呢? 或是你可以想像成一個 nxn 格的魔術方塊中.

這就是為什麼要稱為偽 3D Server的原因, 因為角色的 Z 軸也是使用數格子的方式來計算.

為了樣讓 Server Z 軸資料, 所以場景編輯器在輸出場景資料的時候也必須輸出一個高度圖 (Height Map), 這個高度圖是一個 2維陣列,

HeightMap[n][n] = { {5, 2, 5, 2, 5....} …. }
陣列裡每個值就是代表每一格格子的高度.

除了地表的高度之外, 阻檔物的高度也必須計算在內, 因為除了距離之外, 我們還可以讓 Gameplay 更豐富一點 (後面會提到), 有一點必須注意, 因為高度是使用格子的方式在計算, 所以在某些情況下高度要使用四捨五入的方式來計算.



如上圖, 當高度在格子的一半以下時則以不足的條件捨去, 反之若大於一半則無條件增加一格, 會這樣做的原因是因為格子很大, 難免會有高度落於格子的之間, 會使用四捨五入的方式是因為可以讓玩家在視覺上的誤差減到最小, 如此就可以避免感覺距離夠卻打不到, 或是距離明明有差卻還被打到的感覺.

Server 有了高度圖後就要開始處理計算的問題, 在任何的距離運算都必須把高度納入運算之中, 有兩種方法, 一種是每次運算時依角色的 x,y 去取得當時的高度, 另一種則是角色的資料本身就有代表高度的 z .

在此建議使用後者, 為角色新增一個 z , 因為前者的方法每次計算距離時都要去取得高度, 而後者則只有在角色移動的時候才需要去取得高度更新至 z , 之後計算距離時只要把 z 值直接拿來使用即可, 當計算一多時, 兩者之間效能上就會有很大的差異.

假設有一個 Vertor3 class, 裡面有 x,y,z , 細部就不說明了, 只列出兩個重要的函式.

inline int Vector3::Length()
{
 return x * x + y * y + z * z;
}

inline int Vector3::Distance(const Vector3 &kPos)
{
 return (*this - kPos).Length();
}

求得 A B 的距離 nDistance, 這個 nDistance 代表的是 A B 共有幾格的距離,
int nDistance = A.Distance(B);

為了運算速度考量, Length 中並沒有開平方根, 因此攻擊距離 nAttackRange 必須相乘.
if ( nDistance <= nAttackRange * nAttackRange)
{
 // 攻擊.
}

有了 z 軸後就不會有低形高低落差時可以互打的情況, 也為 Gameplay 增色不少


既然有了高度圖, 如果只拿來當作距離運算的話豈不太浪費了, 高度圖能做的還不止如此.





如上圖, 2D Server 角色走一步後就可從山谷瞬間移至山頂, 所以在中山頂與山谷之間必須要有阻檔牆, 或是完全交由 Client , 高度圖在此還有一個功能, 就是可以當做高低落差來使用.

當現在的格子高度與下一步的高度差太多時, 上坡時就可以限制無法移動, 但是下坡時依然可以移動, 而且也省去了做阻檔牆的工時, 除此之外, 這高度也不限於只能在地形上使用, 地上若有障礙物 (如石塊, 桌子..etc) 一樣適用, 甚至還可以做到和 3D Server 一樣的效果, 就是角色可以跳至障礙物上, 而且在障礙物上依然可以進行戰鬥.

這個除了可以拿來給玩家角色使用外, 還可以當作敵人在做路徑搜尋時的碰撞使用, 就不會產生敵人從山谷瞬間移至山頂的情況, 此時只要判斷 z 的差距即可.

此外高度圖還另有妙用, 可以做出媲美 3D Server 的視覺阻檔效果.


如上圖, 在這些地形下其實是不應該被攻擊的, 或者說攻擊不到彼此, 因為地形或地物阻礙了攻擊, 雖然說這效果一般都是以浮點向量運算來達成, 不過數格子的偽 3D一樣可以做到.

先求得 A B 的距離, 這個 nDistance 代表的是 A B 共有幾格的距離,
int nDistance = Sqrt(A.Distance(B)); // 這裡的 Distance 必須要開根號.

有了距離就可以求得 A B 的斜率 S, 但是若 Server 沒有浮點數的話, 斜率就會變得不精準, 在此就要使用定點浮點數的技巧 (不要使用乘法, 速度較慢).

X0000000 00000000 00000000 0fffffff

Server 的數值型態為Int 32 bit 有號數, 扣除最左邊一個正負號 X還有 31 bits, 為了求稍微精準一點, 我們小數 f 取兩位, 也就是 0.99 = 7 bits, 最後剩餘的 0 共有 24 bits = -16777214 ~ 16777215 這個數值區間對於 2D Server 來說是夠用了, 因此

Vector3 A1 = A << 7; // 除了空出右邊 7 bits 之外, 也順便將左邊一位正負號去除, 若有需要請自行加回來.
Vector3 B1 = B << 7;
Vector3 S = (B1 – A1) / (nDistance << 7); // nDistance 要先求出然後才能轉換成定點浮點數.

為什麼 nDistance 要先求出然後才能轉換成定點浮點數? 因為效能問題, nDistance 所代表的是 A B 點有幾格, Server是數格子的, Server 的資料也是以格子計算, 所以一次只要數一格就好, 不需要細到數半格或是 0.2 格之類的.

int nAttack = 1; // 0 = 不攻擊, 1 = 攻擊.
Vector3 kPos = A1;
Vector3 kNewPos;

for (i = 0; i < nDistance; i ++)
{
 kPos += S; // 加上斜率.
 kNewPos = kPos >> 7; // 定點浮點數轉回長整數.

 nHeight = GetHeightMap(kNewPos .x, kNewPos.y); 
 // 角色攻擊高度應該要高於角色原點(腳底)的高度甚至可以依身高而有所不同.
 if (角色所在高度 + 角色攻擊高度 < nHeight) // 小於 nHeight 表示該點是在障礙物之中.
 {
  nAttack = 0;
  break;
 }
}

if (1 == nAttack)
{
 // 攻擊.
}

高度圖為 2D Server 完成了幾件事,

1. 解決了高低落差時的攻擊距離問題.
2. 解決了障礙物高低落差時的角色行走問題, 甚至可以跳躍, 也可以摔死.
3. 增加了角色攻擊的障礙物判定.

1, 2 都是解決問題, 3 則是增加了更豐富的變化,

1 意見:

  1. 看到你這篇,想起當年做鮮膜X就有向程式提出這種理論。
    實驗都很順利,上下交叉道路(橋上&橋下)、螺旋狀深谷都實作成功。
    可惜最後到了在做御劍飛行的玩法時產生破綻,不是因為可以向上一直飛本身有問題,而是飛高了會看到場景破綻,結果最後御劍飛行只能從地表提高少單位的高度,變得好像在玩磁浮滑板...Orz
    當年若是能實現無接縫場景,鮮膜X就能在完美世界之前實現地vs空、空vs空的打鬥了。
    殘念...

    回覆刪除