這次帶給大家JS仿熱血傳奇遊戲,JS仿熱血傳奇遊戲的注意事項有哪些,以下就是實戰案例,一起來看一下。
前言
遊戲的第一個版本開發於14年,瀏覽器端使用html+css+js,服務端使用asp +php,通訊採用ajax,資料儲存使用access+mySql。不過由於一些問題(當時還不會用node,用asp寫複雜的邏輯真的會寫吐;當時對canvas寫的也少,dom渲染很容易達到效能瓶頸),已經廢棄。後來用canvas重製了一版。本文寫於18年。
1.開發前的準備
為什麼要用Javascript來實作一款比較複雜的PC端遊戲
1.js實作PC端網遊是可行的。隨著PC、手機硬體配置的升級和瀏覽器的更新換代,以及H5各種庫的發展,js實現一款網遊的難度越來越低。這裡的難度主要是兩方面:瀏覽器的效能;js程式碼是否足夠易於擴展,以滿足於一款邏輯極其複雜的遊戲的迭代。
2.現階段的js遊戲裡,很少有規模較大的可供參考。涉及到多人連線、服務端資料儲存、複雜互動的遊戲,大多數(幾乎全部)都是用flash開發的。但是flash畢竟在衰落,而js發展迅速,而且只要有瀏覽器就可以運作。
為什麼選擇了一款2001年的熱血傳奇遊戲
#第一個原因是對舊遊戲的情懷; 當然更重要的另一個原因是,別的遊戲要嘛我不會玩、要嘛我會玩但沒有素材(圖片、音效等)。花很大精力去收集一個遊戲的地圖、人物怪物模型、物品和裝備圖,然後去處理、解析一遍再用於js開發,我覺得是浪費時間。
由於我以前蒐集了一些傳奇遊戲的素材,並且幸運地找到了提取熱血傳奇客戶端資源檔案的方法( github位址 ),所以可以直接開始寫碼,省去了一些準備時間。
可能的困難
1.瀏覽器的運作效能:這應該是最困難的一點。假如遊戲要維持40幀,那麼每幀只有25ms的時間留給js計算。且由於渲染通常比計算耗性能,實際上留給js的時間只有10毫秒左右。
2.防作弊:如何避免使用者直接呼叫介面或竄改網路請求資料?由於目標是用js實現比較複雜的遊戲,並且任何網路遊戲都需要考慮這一點,一定會有相對成熟的方案。此處不是本文重點。
2.整體設計
瀏覽器端
畫面渲染使用canvas。
比起dom(p)+css,canvas可以處理比較複雜的場景渲染和事件管理,例如下面這個場景,涉及了四張圖片:玩家、動物、地上的物品、最下層的地圖圖片。 (實際上還有地上的影子,滑鼠指向人物、動物、物品時出現的對應名稱,以及地面上的陰影。為了方便讀懂,先不考慮這麼多內容。)
這時,如果希望實現「點擊動物、攻擊動物;點擊物品、撿起物品」的效果,那麼需要對動物和物品進行事件監聽。如果採用dom的方式,那麼會出現幾點難於處理的問題:
a.渲染的順序和事件處理的順序不同(有時z-index小的需要先處理事件),需要額外處理。例如這個上面的例子裡:點擊怪物、點擊物品的時候也容易點到人物,那麼需要給人物做「點擊事件穿透」的處理。而且事件處理的順序不固定:假如我有一個技能(例如遊戲裡的治療)需要點人物才可以釋放,那麼這時人物又需要有事件監聽。所以一個元素是否需要處理事件、處理事件的先後,是隨著遊戲狀態的不同而改變的,而 dom的事件綁定已經無法滿足需求 。
b.有關聯的元素難以放在同一個dom節點中:例如玩家的模型、玩家的名字和玩家身上的技能畫效,理想情況下是放在一個
容器裡,方便管理(這樣幾個元素的定位就可以繼承父元素,不用分別處理位置了)。但是這樣,z-index會很難處理。例如玩家A在玩家B的上面,那麼A會被B遮擋,因此需要A的z-index小一些,但是又需要讓玩家A的名字不會被B的名字或影子遮擋,就無法實現。簡單說, dom結構的可維護性會犧牲畫面所展示的效果,反之亦然 。
c.效能問題。即使犧牲了效果,用dom渲染,勢必出現很多嵌套關係,所有元素的style都在頻繁變化,連續觸發瀏覽器的repaint甚至reflow。
canvas渲染邏輯與專案邏輯分離
如果將canvas的各種渲染操作(如 drawImage 、 fillText 等)與專案程式碼放在一起,勢必會導致專案後期無法維護。翻了一下幾款現有的canvas庫,結合vue的資料綁定+調試工具的方式,搞了一個全新的canvas庫Easycanvas( github地址),並且像vue一樣支持通過一個插件來調試canvas中的元素。
這樣,整個遊戲的渲染部分就容易很多,只需要管理遊戲當前的狀態、並且根據服務端從socket傳回來的資料去更新資料就可以。 「資料的變化造成視圖的變化」這個環節由Easycanvas負責 。例如下圖的玩家包裹物品的實現,我們只需要給出包裹容器的位置、背包裡每個元素的排布規則,然後將每個包裹的物品綁定到一個array上,然後去管理這個array即可(資料映射到畫面的過程由Easycanvas負責)。
例如,5行8列共40個物品的樣式可以透過如下的形式傳遞給Easycanvas(index為物品索引,物品x方向間距36,y方向間距32)。而這個邏輯是一成不變的,無論物品的陣列怎樣變化、包裹被拖曳到什麼位置,每個物品的相對位置都是固定的。至於canvas上的渲染則完全不需要專案本身來考慮,所以可維護性較好。
style: { tw: 30, th: 30, tx: function () { return 40 + index % 8 * 36; }, ty: function () { return 31 + Math.floor(index / 8) * 32; } }
canvas分層渲染
假設:遊戲需要保持40幀,瀏覽器寬800高600,面積48萬(後面稱48萬為1個螢幕面積)。
如果用同一個canvas來呈現,那麼這個canvas的幀數40,每秒至少需要繪製40個螢幕面積。但是同一個座標點很可能出現多個元素重疊的情況,例如底部的UI、血條、按鈕就是重疊放置,他們又共同遮擋了場景地圖。所以這些加在一起,每秒瀏覽器的繪製量很容易達到100個螢幕面積以上。
這個繪製是很難優化的,因為整個canvas畫布的任何一個地方都在進行視圖的更新:可能是玩家和動物的移動、可能是按鈕的特效、可能是某個技能效果的變化。這樣的話,即使玩家不動,由於衣服「隨風飄飄」的效果(其實是精靈動畫播放到下一張圖),或者是地面上出現了一瓶藥水,都要引起整個canvas的重繪。因為 遊戲中幾乎不可能出現某一幀的畫面與上一幀毫無區別的情況,即使是遊戲畫面的一個局部,也很難保持不變 。整個遊戲的畫面永遠在更新。
因為 遊戲中幾乎不可能出現某一幀的畫面與上一幀毫無區別的情況 ,畫面永遠在更新。
因此,這次我採用了3個canvas重疊排布的方式。由於Easycanvas的事件處理支援傳遞,因此即使點到了最上面的canvas,如果沒有任何元素結束了某一次點擊,後面的canvas也可以接到這次事件。 3個canvas分別負責UI、地面(地圖)、精靈(人物、動物、技能特效等):
這樣分層的好處是,每層最大幀數可以根據需要來調整:
例如UI層,因為很多UI平常是不動的,即使動也不會需要太精密的繪製,所以可以適當降低幀數,例如降低到20。這樣假如玩家的體力從100降到20,那麼可以在50ms內更新視圖,而50ms的切換是玩家感覺不出來的。因為像體力這種 UI層資料的變化很難在很短的時間內連續變化多次,而50ms的延遲是人很難感知的,所以不需要頻繁的繪製 。假如我們每秒節約了20幀,那麼很可能可以節省10個螢幕面積的繪製。
再如地面,只有玩家移動的時候,地圖才會改變。這樣,如果玩家不動,那麼每個畫面可以省去1個螢幕面積。由於需要確保玩家移動時的流暢感,地面的最大格數不宜太低。假如地面為30幀,那麼玩家不動時,每秒就可以節約30個螢幕面積的繪製(這個項目中,地圖是幾乎繪滿螢幕的)。而且其它玩家、動物的移動不會改變地面,也不需要重繪地面這一層。
精靈層最大幀數不能降低,這層會展示遊戲的人物動作等核心部分,所以最大幀數設定為40.
#這樣,每秒繪製的面積,玩家移動時可能是80~100個螢幕面積,而玩家不移動時可能只有50個螢幕面積。遊戲中,玩家停下來打怪、打字、整理物品、釋放技能都是站立不動的,因此 大量的時間裡都不會觸發地面的繪製,對性能的節約很大 。
伺服器端
由於目標是js實作一款多人網遊,所以服務端使用Node,使用socket與瀏覽器通訊。這樣做還有一個好處,就是一些 公用的邏輯可以在兩端重複使用 ,例如判斷地圖上某個座標點是否有障礙物。
Node端的玩家、場景等遊戲相關資料全部儲存與記憶體中,定期同步至檔案。每次Node服務啟動時,將資料從檔案讀取至記憶體。這樣可以玩家較多時,文件讀寫的頻率成指數上升,進而引發的效能問題。 (後來為了提高穩定,為檔案讀寫增加了一個緩衝,「記憶體-檔案-備份」的方式,以免讀寫過程中伺服器重新啟動導致的檔案損壞)。
Node端分介面、資料、實例等多層。 「介面」負責和瀏覽器端互動。 「數據」是一些靜態數據,例如某個藥品的名稱和效果、某個怪物的速度和體力,是遊戲規則的一部分。 「實例」是遊戲中的當前狀態,例如某個玩家身上的藥品,就是「藥品數據」的一個實例。再舉個例子,「鹿的實例」擁有「目前血量」這個屬性,鹿A可能是10,鹿B可能是14,而「鹿」本身只有「初始血量」。
3.場景地圖的實作
地圖場景
#下面開始介紹地圖場景部分,仍然是依賴 Easycanvas 來渲染。
思考
由於玩家是永遠固定在螢幕中心的,所以玩家的移動,其實就是地圖的移動。例如玩家像左跑,地圖就向右平移即可。剛才已經提到,玩家處於3個canvas中的中間一層,而地圖屬於底層,因此玩家一定遮住地圖。
這樣看起來是合理的,但是假如地圖中有一棵樹,那麼「玩家的層次始終高於樹」就不對了。這時,有2種大的解決方案:
地圖分層,「地面」與「地上」拆開。將玩家處於兩層之間,例如下圖,左邊是地上、右邊是地面,然後重疊繪製,把人物夾在中間:
##
這樣看似解決了問題,其實引入了2個新的問題:第一個是,玩家有時可能會被「地上」的東西遮擋(例如一棵樹),有時又需要能夠遮擋「地上」的東西(例如站在這棵樹的下方,頭部會遮住樹木)。另一個問題是渲染的效能消耗會增加。由於玩家是時時刻刻在變的,「地上」這一層需要頻繁重繪。這樣做也打破了最初的設計——盡量節省地面大地圖的渲染,導致canvas的分層更加複雜。 地圖不分層,「地面」與「地上」在一起繪製。當玩家處於樹後的時候,將玩家的透明度設為0.5,例如下圖:
這樣做只有一個壞處:玩家的身體不是都不透明、就是半透明(怪物在地圖上行走也會有這個效果),不會完全真實。因為理想的效果是存在玩家的身體被遮住一部分的場景的。但是這樣做對效能友好,而且程式碼易於維護,目前我也採用了這個方案。
那麼要如何判斷「地圖」這張圖片哪些地方是樹呢?遊戲通常會有一個大的地圖描述檔(其實就是一個Array),透過0、1、2這樣的數字來標識哪些地方可以通過、哪些地方有障礙物、哪些地方是傳送點等等。熱血傳奇中的這個「描述文件」就是48x32為最小單位進行描述的,所以玩家在傳奇中的行動會有一種「棋盤」的感覺。單位越小越流暢,但是佔用的體積越大、產生這個描述的過程就越耗時。
下面開始正題。
實作
我找了一個朋友幫我匯出熱血傳奇客戶端中「比奇省」的地圖,寬33600、高22400,是我電腦的幾百倍大。為了避免電腦爆炸,需要拆分成多塊載入。由於傳奇的最小單元是48x32,我們以480x320將地圖拆成了4900(70x70)個圖片檔。
canvas的尺寸我們設定為800x600,這樣玩家只需要載入3x3共9張圖片就可以鋪滿整個畫布。 800/480=1.67,那為什麼不是2x2?因為有可能玩家目前的位置剛好導致有的圖片只展示了一部分。如下圖:
#相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
以上是JS仿經典傳奇遊戲的詳細內容。更多資訊請關注PHP中文網其他相關文章!