javascript高仿熱血傳奇游戲?qū)崿F(xiàn)代碼
前言
游戲的第一個(gè)版本開發(fā)于14年,瀏覽器端使用html+css+js,服務(wù)端使用asp+php,通訊采用ajax,數(shù)據(jù)存儲(chǔ)使用access+mySql。不過由于一些問題(當(dāng)時(shí)還不會(huì)用node,用asp寫復(fù)雜的邏輯真的會(huì)寫吐;當(dāng)時(shí)對(duì)canvas寫的也少,dom渲染很容易達(dá)到性能瓶頸),已經(jīng)廢棄。后來用canvas重制了一版。本文寫于18年。
1.開發(fā)前的準(zhǔn)備
為什么要用Javascript來實(shí)現(xiàn)一款比較復(fù)雜的PC端游戲
1.js實(shí)現(xiàn)PC端網(wǎng)游是可行的。隨著PC、手機(jī)硬件配置的升級(jí)和瀏覽器的更新?lián)Q代,以及H5各種庫的發(fā)展,js實(shí)現(xiàn)一款網(wǎng)游的難度越來越低。這里的難度主要是兩方面:瀏覽器的性能;js代碼是否足夠易于擴(kuò)展,以滿足于一款邏輯極其復(fù)雜的游戲的迭代。
2.現(xiàn)階段的js游戲里,很少有規(guī)模較大的可供參考。涉及到多人聯(lián)機(jī)、服務(wù)端數(shù)據(jù)存儲(chǔ)、復(fù)雜交互的游戲,大多數(shù)(幾乎全部)都是用flash開發(fā)的。但是flash畢竟在衰落,而js發(fā)展迅速,并且只要有瀏覽器就可以運(yùn)行。
為什么選擇了一款2001年的熱血傳奇游戲
第一個(gè)原因是對(duì)老游戲的情懷; 當(dāng)然更重要的另一個(gè)原因是,別的游戲要么我不會(huì)玩、要么我會(huì)玩但沒有素材(圖片、音效等)。花很大精力去收集一個(gè)游戲的地圖、人物怪物模型、物品和裝備圖,然后去處理、解析一遍再用于js開發(fā),我覺得是浪費(fèi)時(shí)間。
由于我以前搜集了一些傳奇游戲的素材,并且幸運(yùn)地找到了提取熱血傳奇客戶端資源文件的方法( github地址 ),所以可以直接開始寫碼,省去了一些準(zhǔn)備時(shí)間。
可能的困難
1.瀏覽器的運(yùn)行性能:這個(gè)應(yīng)該是最困難的一點(diǎn)。假如游戲要保持40幀,那么每幀只有25ms的時(shí)間留給js計(jì)算。并且由于渲染通常比計(jì)算耗性能,實(shí)際上留給js的時(shí)間只有10毫秒左右。
2.防作弊:如何避免用戶直接調(diào)用接口或者篡改網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)?由于目標(biāo)是用js實(shí)現(xiàn)比較復(fù)雜的游戲,并且任何網(wǎng)絡(luò)游戲都需要考慮這一點(diǎn),一定會(huì)有相對(duì)成熟的方案。此處不是本文重點(diǎn)。
2.整體設(shè)計(jì)
瀏覽器端
畫面渲染使用canvas。
相比dom(div)+css,canvas可以處理比較復(fù)雜的場(chǎng)景渲染和事件管理,例如下面這個(gè)場(chǎng)景,涉及了四張圖片:玩家、動(dòng)物、地上的物品、最下層的地圖圖片。(實(shí)際還有地上的影子,鼠標(biāo)指向人物、動(dòng)物、物品時(shí)出現(xiàn)的相應(yīng)名稱,以及地面上的陰影。為了方便讀懂,先不考慮這么多內(nèi)容。)
這時(shí),如果希望實(shí)現(xiàn)“點(diǎn)擊動(dòng)物、攻擊動(dòng)物;點(diǎn)擊物品、撿起物品”的效果,那么需要對(duì)動(dòng)物和物品進(jìn)行事件監(jiān)聽。如果采用dom的方式,那么會(huì)出現(xiàn)幾點(diǎn)難于處理的問題:
a.渲染的順序和事件處理的順序不同(有時(shí)候z-index小的需要先處理事件),需要額外處理。例如這個(gè)上面的例子里:點(diǎn)擊怪物、點(diǎn)擊物品的時(shí)候也容易點(diǎn)到人物,那么需要給人物做“點(diǎn)擊事件穿透”的處理。而且事件處理的順序不固定:假如我有一個(gè)技能(例如游戲里的治療)需要點(diǎn)人物才可以釋放,那么這時(shí)人物又需要有事件監(jiān)聽。所以一個(gè)元素是否需要處理事件、處理事件的先后,是隨著游戲狀態(tài)的不同而變化的,而 dom的事件綁定已經(jīng)不能滿足需要 。
b.有關(guān)聯(lián)的元素難以放在同一個(gè)dom節(jié)點(diǎn)中:例如玩家的模型、玩家的名字和玩家身上的技能畫效,理想情況下是放在一個(gè) <div> 或者 <section> 容器里,便于管理(這樣幾個(gè)元素的定位就可以繼承父元素,不用分別處理位置了)。但是這樣,z-index會(huì)很難處理。例如玩家A在玩家B的上面,那么A會(huì)被B遮擋,因此需要A的z-index小一些,但是又需要讓玩家A的名字不會(huì)被B的名字或者影子遮擋,就無法實(shí)現(xiàn)。簡(jiǎn)單點(diǎn)說, dom結(jié)構(gòu)的可維護(hù)性會(huì)犧牲畫面展示的效果,反之亦然 。
c.性能問題。即使?fàn)奚诵Ч胐om渲染,勢(shì)必出現(xiàn)很多嵌套關(guān)系,所有元素的style都在頻繁變化,連續(xù)觸發(fā)瀏覽器的repaint甚至reflow。
canvas渲染邏輯與項(xiàng)目邏輯分離
如果將canvas的各種渲染操作(如 drawImage 、 fillText 等)與項(xiàng)目代碼放在一起,那么勢(shì)必導(dǎo)致項(xiàng)目后期無法維護(hù)。翻了一下幾款現(xiàn)有的canvas庫,結(jié)合vue的數(shù)據(jù)綁定+調(diào)試工具的方式,搞了一個(gè)全新的canvas庫Easycanvas( github地址 ),并且像vue一樣支持通過一個(gè)插件來調(diào)試canvas中的元素。
這樣,整個(gè)游戲的渲染部分就容易很多,只需要管理游戲當(dāng)前的狀態(tài)、并且根據(jù)服務(wù)端從socket傳回來的數(shù)據(jù)去更新數(shù)據(jù)就可以。 “數(shù)據(jù)的變化引起視圖的變化”這個(gè)環(huán)節(jié)由Easycanvas負(fù)責(zé) 。例如下圖的玩家包裹物品的實(shí)現(xiàn),我們只需要給出包裹容器的位置、背包里每個(gè)元素的排布規(guī)則,然后將每個(gè)包裹的物品綁定到一個(gè)array上,然后去管理這個(gè)array即可(數(shù)據(jù)映射到畫面的過程由Easycanvas負(fù)責(zé))。
例如,5行8列共計(jì)40個(gè)物品的樣式可以通過如下的形式傳遞給Easycanvas(index為物品索引,物品x方向間距36,y方向間距32)。而這個(gè)邏輯是一成不變的,無論物品的數(shù)組怎樣變化、包裹被拖拽到什么位置,每個(gè)物品的相對(duì)位置都是固定的。至于canvas上的渲染則完全不需要項(xiàng)目本身來考慮,所以可維護(hù)性較好。
style: { tw: 30, th: 30, tx: function () { return 40 + index % 8 * 36; }, ty: function () { return 31 + Math.floor(index / 8) * 32; } }
canvas分層渲染
假設(shè):游戲需要保持40幀,瀏覽器寬800高600,面積48萬(后面稱48萬為1個(gè)屏幕面積)。
如果用同一個(gè)canvas來呈現(xiàn),那么這個(gè)canvas的幀數(shù)40,每秒至少需要繪制40個(gè)屏幕面積。但是同一個(gè)坐標(biāo)點(diǎn)很可能出現(xiàn)多個(gè)元素重疊的情況,例如底部的UI、血條、按鈕就是重疊放置,他們又共同遮擋了場(chǎng)景地圖。所以這些加在一起,每秒瀏覽器的繪制量很容易達(dá)到100個(gè)屏幕面積以上。
這個(gè)繪制是很難優(yōu)化的,因?yàn)檎麄€(gè)canvas畫布的任何一處都在進(jìn)行視圖的更新:可能是玩家和動(dòng)物的移動(dòng)、可能是按鈕的特效、可能是某個(gè)技能效果的變化。這樣的話,即使玩家不動(dòng),由于衣服“隨風(fēng)飄飄”的效果(其實(shí)是精靈動(dòng)畫播放到下一張圖),或者是地面上出現(xiàn)了一瓶藥水,都要引起整個(gè)canvas的重繪。因?yàn)?游戲中幾乎不可能出現(xiàn)某一幀的畫面與上一幀毫無區(qū)別的情況,即使是游戲畫面的一個(gè)局部,也很難保持不變 。整個(gè)游戲的畫面永遠(yuǎn)在更新。
因?yàn)?游戲中幾乎不可能出現(xiàn)某一幀的畫面與上一幀毫無區(qū)別的情況 ,畫面永遠(yuǎn)在更新。
因此,這次我采用了3個(gè)canvas重疊排布的方式。由于Easycanvas的事件處理支持傳遞,因此即使點(diǎn)到了最上面的canvas,如果沒有任何元素結(jié)束了某一次點(diǎn)擊,后面的canvas也可以接到這次事件。3個(gè)canvas分別負(fù)責(zé)UI、地面(地圖)、精靈(人物、動(dòng)物、技能特效等):
這樣分層的好處是,每層最大幀數(shù)可以根據(jù)需要來調(diào)整:
例如UI層,因?yàn)楹芏郩I平時(shí)是不動(dòng)的,即使動(dòng)也不會(huì)需要太精密的繪制,所以可以適當(dāng)降低幀數(shù),例如降低到20。這樣假如玩家的體力從100降低到20,那么可以在50ms內(nèi)更新視圖,而50ms的切換是玩家感覺不出來的。因?yàn)橄耋w力這種 UI層數(shù)據(jù)的變化很難在很短的時(shí)間內(nèi)連續(xù)變化多次,而50ms的延遲是人很難感知的,所以不需要頻繁的繪制 。假如我們每秒節(jié)約了20幀,那么很可能可以節(jié)約10個(gè)屏幕面積的繪制。
再如地面,只有玩家移動(dòng)的時(shí)候,地圖才會(huì)變化。這樣,如果玩家不動(dòng),那么每幀可以省去1個(gè)屏幕面積。由于需要保證玩家移動(dòng)時(shí)的流暢感,地面的最大幀數(shù)不宜太低。假如地面為30幀,那么玩家不動(dòng)時(shí),每秒就可以節(jié)約30個(gè)屏幕面積的繪制(這個(gè)項(xiàng)目中,地圖是幾乎繪滿屏幕的)。而且其它玩家、動(dòng)物的移動(dòng)不會(huì)改變地面,也不需要重繪地面這一層。
精靈層最大幀數(shù)不能降低,這層會(huì)展示游戲的人物動(dòng)作等核心部分,所以最大幀數(shù)設(shè)置為40.
這樣,每秒繪制的面積,玩家移動(dòng)時(shí)可能是80~100個(gè)屏幕面積,而玩家不移動(dòng)時(shí)可能只有50個(gè)屏幕面積。游戲中,玩家停下來打怪、打字、整理物品、釋放技能都是站立不動(dòng)的,因此 大量的時(shí)間里都不會(huì)觸發(fā)地面的繪制,對(duì)性能的節(jié)約很大 。
服務(wù)器端
由于目標(biāo)是js實(shí)現(xiàn)一款多人網(wǎng)游,所以服務(wù)端使用Node,使用socket與瀏覽器通訊。這樣做還有一個(gè)好處,就是一些 公用的邏輯可以在兩端復(fù)用 ,例如判斷地圖上某個(gè)坐標(biāo)點(diǎn)是否存在障礙物。
Node端的玩家、場(chǎng)景等游戲相關(guān)數(shù)據(jù)全部存儲(chǔ)與內(nèi)存中,定期同步至文件。每次Node服務(wù)啟動(dòng)時(shí),將數(shù)據(jù)從文件讀取至內(nèi)存。這樣可以玩家較多時(shí),文件讀寫的頻率成指數(shù)級(jí)上升,從而引發(fā)的性能問題。(后來為了提高穩(wěn)定,為文件讀寫增加了一個(gè)緩沖,“內(nèi)存-文件-備份”的方式,以免讀寫過程中服務(wù)器重啟導(dǎo)致的文件損壞)。
Node端分接口、數(shù)據(jù)、實(shí)例等多層。“接口”負(fù)責(zé)和瀏覽器端交互?!皵?shù)據(jù)”是一些靜態(tài)數(shù)據(jù),例如某個(gè)藥品的名稱和效果、某個(gè)怪物的速度和體力,是游戲規(guī)則的一部分?!皩?shí)例”是游戲中的當(dāng)前狀態(tài),例如某個(gè)玩家身上的一個(gè)藥品,就是“藥品數(shù)據(jù)”的一個(gè)實(shí)例。再舉個(gè)例子,“鹿的實(shí)例”擁有“當(dāng)前血量”這個(gè)屬性,鹿A可能是10,鹿B可能是14,而“鹿”本身只有“初始血量”。
3.場(chǎng)景地圖的實(shí)現(xiàn)
地圖場(chǎng)景
下面開始介紹地圖場(chǎng)景部分,仍然是依賴 Easycanvas 進(jìn)行渲染。
思考
由于玩家是始終固定在屏幕中心的,所以玩家的移動(dòng),實(shí)際上是地圖的移動(dòng)。例如玩家像左跑,地圖就向右平移即可。剛才已經(jīng)提到,玩家處于3個(gè)canvas中的中間一層,而地圖屬于底層,因此玩家一定遮擋地圖。
這樣看起來是合理的,但是假如地圖中有一棵樹,那么“玩家的層次始終高于樹”就不對(duì)了。這時(shí),有2種大的解決方案:
地圖分層,“地面”與“地上”拆開。將玩家處于兩層之間,例如下圖,左側(cè)是地上、右側(cè)是地面,然后重疊繪制,把人物夾在中間:
這樣看似解決了問題,其實(shí)引入了2個(gè)新的問題:第一個(gè)是,玩家有時(shí)可能會(huì)被“地上”的東西遮擋(例如一棵樹),有時(shí)又需要能夠遮擋“地上”的東西(例如站在這棵樹的下方,頭部會(huì)遮擋住樹)。另一個(gè)問題是渲染的性能消耗會(huì)增加。由于玩家是時(shí)刻在變的,“地上”這一層需要頻繁重繪。這樣做也打破了最初的設(shè)計(jì)——盡量節(jié)約地面大地圖的渲染,從而導(dǎo)致canvas的分層更加復(fù)雜。
地圖不分層,“地面”與“地上”在一起繪制。當(dāng)玩家處于樹后的時(shí)候,將玩家的透明度設(shè)置為0.5,例如下圖:
這樣做只有一個(gè)壞處:玩家的身體要么都不透明、要么都半透明(怪物在地圖上行走也會(huì)有這個(gè)效果),不會(huì)完全真實(shí)。因?yàn)槔硐氲男Ч谴嬖谕婕业纳眢w被遮擋住一部分的場(chǎng)景的。但是這樣做對(duì)性能友好,并且代碼易于維護(hù),目前我也采用了這個(gè)方案。
那么如何判斷“地圖”這張圖片哪些地方是樹呢?游戲通常會(huì)有一個(gè)大的地圖描述文件(其實(shí)就是一個(gè)Array),通過0、1、2這樣的數(shù)字來標(biāo)識(shí)哪些地方可以通過、哪些地方存在障礙物、哪些地方是傳送點(diǎn)等等。熱血傳奇中的這個(gè)“描述文件”就是48x32為最小單位進(jìn)行描述的,所以玩家在傳奇中的行動(dòng)會(huì)有一種“棋盤”的感覺。單位越小越流暢,但是占用的體積越大、生成這個(gè)描述的過程也就越耗時(shí)。
下面開始正題。
實(shí)現(xiàn)
我找了一個(gè)朋友幫我導(dǎo)出熱血傳奇客戶端中“比奇省”的地圖,寬33600、高22400,是我電腦的幾百倍大。為了避免電腦爆炸,需要拆分成多塊加載。由于傳奇的最小單元是48x32,我們以480x320將地圖拆成了4900(70x70)個(gè)圖片文件。
canvas的尺寸我們?cè)O(shè)定為800x600,這樣玩家只需要加載3x3共計(jì)9張圖片就可以鋪滿整個(gè)畫布。800/480=1.67,那么為什么不是2x2?因?yàn)橛锌赡芡婕耶?dāng)前的位置正好導(dǎo)致有的圖片只展示了一部分。如下圖:
總結(jié)
以上所述是小編給大家介紹的javascript高仿熱血傳奇游戲?qū)崿F(xiàn)代碼,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
javascript實(shí)現(xiàn)C語言經(jīng)典程序題
這篇文章主要介紹了javascript實(shí)現(xiàn)C語言經(jīng)典程序題的解題思路,感興趣的小伙伴們可以參考一下2015-11-11JavaScript數(shù)值千分位格式化的兩種簡(jiǎn)單實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄狫avaScript數(shù)值千分位格式化的兩種簡(jiǎn)單實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08js實(shí)現(xiàn)tab選項(xiàng)卡函數(shù)代碼
js實(shí)現(xiàn)tab選項(xiàng)卡函數(shù)代碼,需要的朋友可以參考下,這樣的代碼也是比較常用的,相當(dāng)原理也比較簡(jiǎn)單。2010-04-04JS實(shí)現(xiàn)獲取時(shí)間已經(jīng)時(shí)間與時(shí)間戳轉(zhuǎn)換
這篇文章主要為大家提供了用JavaScript編寫的獲取時(shí)間的類,以及時(shí)間戳轉(zhuǎn)時(shí)間的三種格式,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-03-03JS實(shí)現(xiàn)簡(jiǎn)單的tab切換選項(xiàng)卡效果
這篇文章主要介紹了JS實(shí)現(xiàn)簡(jiǎn)單的tab切換選項(xiàng)卡效果,涉及javascript結(jié)合鼠標(biāo)事件對(duì)頁面元素屬性動(dòng)態(tài)操作的相關(guān)技巧,需要的朋友可以參考下2016-09-09webpack打包js文件及部署的實(shí)現(xiàn)方法
這篇文章主要介紹了webpack打包js文件的方法及webpack打包后的JS文件如何部署,需要的朋友可以參考下2017-12-12JavaScript輪播停留效果的實(shí)現(xiàn)思路
輪播停留與無線滾動(dòng)十分類似,都是利用屬性及變量控制移動(dòng)實(shí)現(xiàn)輪播。下面通過本文給大家分享JavaScript輪播停留效果的實(shí)現(xiàn)思路,感興趣的朋友一起看看吧2018-05-05