譯介丨構建一個通用的輕量級 2d 遊戲引擎(part 1)


3樓貓 發佈時間:2022-05-06 07:51:10 作者:mnikn Language

  • 原文鏈接:點擊跳轉
  • 作者:Sébastien Bénard,《死亡細胞》的主創
  • 譯者:mnikn

1. 簡介

如果你對要做什麼事情沒什麼想法的話,是很難去構建一個 2d 的平臺類遊戲引擎的。因此第一原則是從簡單入手。你知道 KISS 原則嗎?Keep It Short and Simple:這就是我們接下來要做的事情。
我的大部分遊戲都是從一個簡單的畫布開始的,最終成長為一個 2d platformer 或者 top-down 風格的遊戲。即使是《死亡細胞》底層也是用同樣的引擎。順帶一提,platformer 和 top-down 的遊戲引擎區別只在於,platformer 相較於 top-down 多添加了重力的因素而已。
在這篇文章我使用 Haxe 來實現代碼:如果你不知道 Haxe 是什麼的話,簡單來說 Haxe 是一種跨平臺語言,能夠編譯生成多個平臺的包,包括 Flash、C 和 iOS/Android。不過 Haxe 的基本語法很簡單並且通用,所以你可以很容易把這些代碼轉換成任意語言。
我會先創造一個簡易輕量的 Entity 類作為基礎,隨後再慢慢拓展。傳統的做法,不過其中也會有一些 tricky。
第一版本的 Entity 類代碼如下:
class Entity { // 圖形對象 public var sprite : YourSpriteClass; ​ // 基礎座標 public var cx : Int; public var cy : Int; public var xr : Float; public var yr : Float; ​ // 結果座標 public var xx : Float; public var yy : Float; ​ // 移動相關 public var dx : Float; public var dy : Float; ​ public function new() { //... } ​ public function update() { //... } }

2. 座標系統

首先我會嘗試去構建一個專注於易用性的座標系統。
通常我會基於網格(grid)去構建系統: 例如關卡就是一系列空單元格(player 可以行走的單元格) 帶上一些牆壁單元格。
其中 cx,cy 是當前單元格的網格座標,xr,yr
表示在單元格內的位置(按比例表示,0 到 1.0)。xx,yy 就是根據 cx,cy + xr,yr 算出來的位置結果值。
有了這樣一個系統做事情就方便了很多。例如,檢查一個 Entity 右邊的碰撞情況變得很簡單:檢查下 cx+1,cy 座標就好。同樣你可以使用 xr 來檢測當前單元格右方是否有 Entity。
為了簡化這一過程,我們抽離出一個方法 hasCollision(cx,cy),當對於參數上有碰撞時返回 true,否則為 false。
if( hasCollision(cx+1,cy) && xr>=0.7 ) { xr = 0.7; // 給 xr 設置上限 // ... }
xx,yy 結果值只會在 update 循環函數結束時更新。
注意: 有時候更新 sprite.x 和 sprite.y 會有細微的性能損耗:當你改變這些值時,大量的東西會根據自身內部邏輯去做更新。例如座標一更新,對象又要重新渲染。所以有時候你不會想直接操作 sprite.x,出於性能考慮我會用結果變量 xx
同時這會讓跨平臺開發變得更加容易,Entity 更多專注於邏輯而不是圖形。
// 假設網格中每個單元格的大小是 16px xx = Std.int( (cx+xr) * 16 ); yy = Std.int( (cy+yr) * 16 ); sprite.x = xx; sprite.y = yy;
同時有時候你想要根據 xx,yy 來初始化 cx,cyxr,yr
public function setCoordinates(x,y) { xx = x; yy = y; cx = Std.int(xx/16); cy = Std.int(yy/16); xr = (xx-cx*16) / 16; yr = (yy-cy*16) / 16; }

3. 在 X 座標上移動

每一幀中 xr 都會添加上 dx 的值。
如果 xr 大於 1 或者小於 0(例如,Entity 已經超出了它當前單元格的邊界),那麼 cx 座標會對應進行更新。
while( xr>1 ) { xr --; cx ++;} while( xr<0 ) { xr ++; cx --;}
為了讓移動更加順滑,你應該要在 dx 上添加摩擦係數(效果會比簡單加個 if
判斷要好)。
dx *= 0.96;
在主循環代碼裡面,當對應的事件觸發時(按鈕觸發或者其他情況),你只需要簡單改下 dx 的值,就能讓 entity 移動。
// hero Entity hero.dx = 0.1; // 或者 hero.dx += 0.05;

4. X 座標的碰撞檢測

基於這個系統,檢測和管理碰撞變得非常簡單:
if( hasCollision(cx+1,cy) && xr>=0.7 ) { xr = 0.7; dx = 0; // stop movement } if( hasCollision(cx-1,cy) && xr<=0.3 ) { xr = 0.3; dx = 0; }

5. X 座標上的處理完成!

以下是處理 X 座標的完整代碼,這已經是極簡版了:)
xr+=dx; dx*=0.96; if( hasCollision(cx+1,cy) && xr>=0.7 ) { xr = 0.7; dx = 0; } if( hasCollision(cx-1,cy) && xr<=0.3 ) { xr = 0.3; dx = 0; }

6. Y 座標又要怎麼處理?

大部分情況下都是 CV 大法。可能處理方式和 X 座標有點不同,具體要看你的遊戲類型是什麼。例如 platformer 來說,當發生 Entity 腳下檢測到碰撞時,你可能希望 yr 的上限值是 0.5 而不是 0.7。
yr+=dy; dy+=0.05; dy*=0.96; ​ if( hasCollision(cx,cy-1) && yr<=0.3 ) { dy = 0; yr = 0.3; } if( hasCollision(cx,cy+1) && yr>=0.5 ) { dy = 0; yr = 0.5; } ​ while( yr>1 ) { cy++; yr--;} while( yr<0 ) { cy--; yr++;}

譯者的話

翻譯水平有限可能會有錯漏,有問題的話麻煩指出。這篇文章裡面的思路,無論是打算自己從零開始,還是使用一個通用的遊戲引擎,都很有參考價值。而且作者的官網裡面都是寶藏!他做了一些免費開源的工具,在我看來這些工具質量比市面上大多數收費的工具都要好,建議關注一波。

© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com