游戏引擎mota-js-v3.0 施工记录
前言
mota-js是一款用于做出魔塔類型游戲的HTML5 2d游戲引擎(github項目地址),目前最新的版本是v2.66,由于原主力開發(fā)已經(jīng)工作,因此很長一段時間沒有大版本的更新。
最近在用樣板做一個游戲的時候,體會到了樣板的一些限制。主要有:1. 地圖尺寸受限,超過一定尺寸(30x30)會到達(dá)性能瓶頸,尤其是在手機(jī)上會很卡頓。2. 素材尺寸受限。貼圖種類被限制在32x32或32x48,尺寸更大做起來會很麻煩,沒有一個精靈系統(tǒng)。3. 實現(xiàn)一些特效很困難。樣板中大量使用了dom來分割地圖場景與狀態(tài)欄UI,一些聯(lián)動特效難以實現(xiàn),并且使用dom對做游戲開發(fā)而非前端開發(fā)的人來說是個噩夢。
去年針對這些問題改了一版pre3.0的運行時系統(tǒng),但現(xiàn)在回看感覺問題很多,首先是設(shè)計模式選取不當(dāng),造成理解困難,其次對原樣板的耦合太多,被原有框架所限制,導(dǎo)致渲染引擎的能力沒有發(fā)揮出來。因此這兩天決定重啟項目,解決之前的問題。
運行時系統(tǒng)簡介
這里主要施工的部分是運行時,編輯器有另外一位大佬在施工。游戲引擎的運行時系統(tǒng),如果是復(fù)雜的游戲,其系統(tǒng)規(guī)模將會是龐大的,如圖是《游戲引擎架構(gòu)》一書中對現(xiàn)代游戲引擎的系統(tǒng)架構(gòu)描述。
這樣的架構(gòu)在unity、ue4等引擎中有完整的體現(xiàn),但在我要施工的對象上不可能有這么多。
HTML5游戲是運行在瀏覽器上的,因此瀏覽器解決了A1-A3、B、C以及部分D的問題。其次因為是2d游戲,許多核心系統(tǒng)中的東西用不到。然后因為魔塔是棋盤類單機(jī)游戲,位置是確定的方格,因此也不需要碰撞檢測、骨骼動畫、在線多人等模塊。最后,因為是在原樣板基礎(chǔ)上進(jìn)行的二次開發(fā),很多游戲算法都已經(jīng)實現(xiàn)過了。因此算下來,借助成熟的第三方庫,工作量并不大,對預(yù)期的架構(gòu)圖修剪如下,預(yù)期工期在15天左右。
施工日志
4-19
調(diào)研第三方庫:
- 渲染引擎PIXI
- 動畫庫tweenjs
4-20
實現(xiàn)資產(chǎn)管理(AssetsManager)。
資產(chǎn)管理中存在的坑:
4-21
實現(xiàn)動畫管理(AnimationManager)、精靈管理(SpriteManager)的部分功能。
4-22
實現(xiàn)一個地圖的原型:瓦片地圖繪制與角色移動。
對系統(tǒng)框架的進(jìn)一步總結(jié):
4-23
- 實現(xiàn)一個UI的原型:對話框的繪制與控制消失
– 對話框基于窗口基類(WinBase),這是UI的基本單元,包含基本的坐標(biāo)、長寬屬性,并且可以有皮膚(WinSkin),具有聚合組件的功能。
– 組件(Components)是使能窗口的重要部分,可以接收消息控制窗口屬性,如,在對話框窗體注冊一個“點擊響應(yīng)”(tapScreen)組件,其功能為關(guān)閉當(dāng)前窗口。
– 對話框文字格式控制??刂谱址?#xff0c;比如"\r \n"之類的,對文字內(nèi)容進(jìn)行格式控制,重寫了原樣板的實現(xiàn),可以通過注冊實現(xiàn)任意格式控制,如字體變化、透明度變化等等。 - 實現(xiàn)尋路與路線繪制
– 尋路使用bfs,復(fù)用原樣板的函數(shù)。路線繪制重寫,功能性與原樣板一致。
– 對控制器(Controller)概念思考:一個控制器,響應(yīng)用戶操作,控制一個對象,對象可以是角色、UI,也可以沒有特定對象,表示實現(xiàn)某種操作(比如響應(yīng)點擊操作)。當(dāng)有特定對象時,被控制的對象是完全“受控”于控制器的。舉個例子:勇士在被控制移動時,使用了尋路功能,那么此時,尋路走動的過程中,會不斷調(diào)用的move,而這個move,是勇士自身的方法,還是控制器的方法?如果注意了控制器的概念,那么這里就應(yīng)該是調(diào)用控制器的方法,這樣做的好處是,把操作與對象的行為分離了,便于后續(xù)實現(xiàn)錄像系統(tǒng)——操作全部由控制系統(tǒng)管理,對象無需關(guān)心操作。
記錄一個坑:
ES6中的箭頭函數(shù)()=>{} 與 function有一個重要區(qū)別,那就是箭頭函數(shù)中的this綁定的是函數(shù)體外的,也就是說在聲明的時候就綁定了this,而不是像function一樣在調(diào)用的時候才綁定。
這兩部分需要后續(xù)補(bǔ)充的內(nèi)容:
4-24
今天主要實現(xiàn)事件系統(tǒng)和消息管理的部分架構(gòu)。
之前的進(jìn)度到了勇士能在地圖移動,但是沒辦法和地圖上的事件進(jìn)行交互,主要就對這塊進(jìn)行施工。
事件、角色的概念
事件,在傳統(tǒng)角色扮演類游戲中一般特指會產(chǎn)生一系列劇情、動作的觸發(fā)點或者npc,角色,通常指的是主角和npc一類會動會產(chǎn)生行為的對象。
在設(shè)計事件系統(tǒng)的架構(gòu)的過程中,理論上可以把地圖上能跑能動的有屬性的對象都當(dāng)成是角色,但純圖塊(block)、和角色(帶事件的塊如npc)和勇士(玩家控制的對象)顯然不屬于同一種,單明顯帶有一種遞增的關(guān)系:
block -> actor -> hero (具體的關(guān)系等之后施工完了再完善)
在實現(xiàn)中,如果把所有帶事件的點,都當(dāng)成是一個個“角色”,那么勇士觸發(fā)事件就可以當(dāng)作是與角色之間的【交互】,簡單的例子比如碰撞事件。
實現(xiàn)碰撞事件過程如下:
這個過程實現(xiàn)的是【碰撞】這類交互,但是實際游戲中,不止碰撞,比如有的點是空點,有的點是【戰(zhàn)后事件】、【拾取后事件】……對原樣板有的或沒有的總結(jié)有如下類型的事件:
事件類型:
其中打!的是在地圖上定義的事件,這些可能還不是全部,那么問題來了,要對所有事件單獨寫代碼去判斷嗎?原樣板是這么做的,感覺工作量會很恐怖,我怕工期趕不及,所以有了下面的消息管理。
消息管理
消息管理(MessageManager)是一個處理行為產(chǎn)生的消息的模塊。
這里定義一下“行為”的概念,一般來說,游戲中的對象都能產(chǎn)生行為,但不是所有行為都會產(chǎn)生消息。比如之前的控制器,是用戶行為,但由于控制器是同步的,即時控制勇士,沒有必要產(chǎn)生消息。但是勇士的行為會產(chǎn)生消息,因為勇士走出去觸碰地圖點,可能會產(chǎn)生諸如碰撞、到達(dá)、離開等一系列情況,這些情況是不由勇士自己處理的,必須交由消息中心處理。當(dāng)勇士發(fā)送消息時,是一個生產(chǎn)者,消息管理中心的任務(wù)是找到一個消費者處理這個消息。
比如前面11類事件,就是11種以上的消息,這些消息會在對應(yīng)的代碼執(zhí)行時,如果有消費者成功消費了這條消息,就會返回消費情況,讓生產(chǎn)者來進(jìn)行判斷下一步的情況——這個過程中,生產(chǎn)者不需要知道自己的消息被誰消費。這樣只需要在合適的地方加入消息生產(chǎn),就可以比較快速靈活地實現(xiàn)以上事件的處理。
移動事件
在原樣板中,事件與地圖點是綁定的,這在寫一些劇情的時候會很頭疼,npc不能頻繁移動,否則會滿地都是事件點,真·移動事件可以解決這個問題。
在上面的事件實現(xiàn)中,所有事件點都會成為角色(Actor),角色可以包含數(shù)據(jù),這個數(shù)據(jù)可以用于定位事件原點。這樣移動事件本質(zhì)上是移動角色,角色移動后,原點將不存在塊,勇士不再觸發(fā)事件,但觸發(fā)移動后的角色時,就會觸發(fā)其定位到原點的事件。
這一塊剛開始做,需要對事件系統(tǒng)進(jìn)行進(jìn)一步的施工。
存在的問題
- 同類消息多次注冊導(dǎo)致的沖突。當(dāng)一個生產(chǎn)者有多個消費者時會出現(xiàn)這種問題,同樣的問題也會出現(xiàn)在控制器,比如點擊綁定了屏幕響應(yīng)和勇者瞬移,一旦優(yōu)先級不當(dāng),就有可能出現(xiàn)事件觸發(fā)的瞬間就點下了響應(yīng)。只能從設(shè)計上避免。
- 移動事件需要存儲數(shù)據(jù),但只有移動過的事件才需要,如果全部成為角色,很有可能沒法區(qū)分,或者是需要一定開銷去區(qū)分誰的數(shù)據(jù)需要進(jìn)存檔。(也許這不是問題,需要進(jìn)一步測試)
4-25
首先處理兩個bug。
第一個,開啟新頁面時,一些sprite會加載不出來,經(jīng)過調(diào)試發(fā)現(xiàn)是PIXI的材質(zhì)緩存機(jī)制導(dǎo)致的,開啟strict模式即可。
第二個,在手機(jī)上出現(xiàn)點擊失效的情況。經(jīng)過調(diào)試發(fā)現(xiàn)是強(qiáng)制橫屏?xí)r,指針對象沒有做對應(yīng)的適配導(dǎo)致的,對tink.js的源碼進(jìn)行修改后修復(fù)。
然后基本完成移動事件和一個簡單的打字機(jī),然后做了一部分異步處理。
移動事件中移動事件點的關(guān)鍵在于對塊信息和事件信息及時修改,事件管理以及地圖管理訂閱事件移動時發(fā)出的leave和arrive消息,對事件和塊進(jìn)行重定位即可。
打字機(jī)之前就留了空,做起來也簡單了,做了一個動畫管理,申請一個【等待】的動畫對象,作用于對話框繪制中的【依次繪制每個字符】,然后處理好回調(diào)即可。另外實現(xiàn)了一個控制字符\w,可以控制說話的速度,比如下面圖就改變了兩次語速。
動畫這塊主要的問題在于解決異步,比如,如何實現(xiàn)說話過程中執(zhí)行下一個事件?目前還沒有好的思路。
(打字機(jī)演示圖因?qū)徍藛栴}刪去
4-26
最近事多,估計工期又要延后了……
今日主要做了兩個部分,一個是對資產(chǎn)管理進(jìn)行了一定的補(bǔ)充,之前是基于原樣板的圖集包裝的,為了測試sprite的優(yōu)先級變化,就用PIXI導(dǎo)入了其他類型的材質(zhì),然后在編輯器里做了一些改動,通過調(diào)整數(shù)據(jù)的材質(zhì)來改變貼圖
另一個是試圖解決異步的問題。據(jù)說有一種叫做promise的東西,我去找來看看大概明白了是怎么回事,但似乎原生的實現(xiàn)效率并不高,而且不一定適合引擎里的架構(gòu),就先在角色類和動畫類進(jìn)行實驗,因為這兩部分異步最多。
比如角色類,主要是移動,涉及到發(fā)送消息、移動動畫,如果不用promise,會出現(xiàn)大量回調(diào),可讀性很差。
用promise改進(jìn)后移動是這樣的:
其中涉及動畫的部分使用了一個onChange,用于移動過程中的優(yōu)先級變化。
這種形式相比于不停callback看上去好多了,當(dāng)然仍然沒有解決多個角色同時移動異步問題,因為這個還沒有實現(xiàn)Promise.all的效果。這個留到明天解決。
4-27
今天打算完成異步事件執(zhí)行的部分。
角色部分全部架構(gòu)基本完成,目前全部寫在ActorManager文件中,后續(xù)如果有增加新的角色部分可以考慮拆分,目前沒必要。
關(guān)于多角色異步執(zhí)行(即昨天提到的promise.all),本質(zhì)上和原樣板沒有區(qū)別,就是用一個唯一code掛在全局,執(zhí)行完畢后回調(diào)取消掉code。當(dāng)所有異步code都取消時,即為完成一次all。
以一個行走事件的異步過程為例。
角色消息中心事件管理地圖管理異步移動(Async)生成并記錄uid返回uid開始移動(Leave)Leave ActorLeave Actor在此期間可以執(zhí)行其他事件移動結(jié)束(Arrive)Arrive ActorArrive Actor異步結(jié)束(取消uid)撤銷uid,檢查等待事件角色消息中心事件管理地圖管理角色每次移動開始時和移動到達(dá)后都要發(fā)送消息,事件管理和地圖管理接收該消息,并對地圖和事件數(shù)據(jù)進(jìn)行修改(實現(xiàn)事件移動的方法),此外還需要在移動開始前和結(jié)束后對異步進(jìn)行記錄,如果在移動的過程中,執(zhí)行了等待全部異步事件(類似promise.all),則會在消息中心掛載一個一次性的回調(diào)函數(shù),等到全部執(zhí)行完畢后進(jìn)行調(diào)用。同樣也能實現(xiàn)競爭式如promise.race的效果。但目前暫未用到。目前有一個問題尚未解決——事件的碰撞,比如A要到B的位置,B要到C的位置,看上去能執(zhí)行成功,但發(fā)往事件管理和地圖管理的消息出現(xiàn)了競爭——無法確定誰先到達(dá),如果A先到,發(fā)現(xiàn)B處已經(jīng)有一個塊,就會發(fā)生重疊的錯誤情況。預(yù)想的解決辦法是同一個點碰撞后成為一個隊列,先進(jìn)先出,這樣一來,即使A到達(dá)B的時候,B還沒離開,在B離開時也能正確取出自己的事件。
最后做了一些關(guān)于地圖特效的實驗和接口,學(xué)習(xí)了一下PIXI包裝的filter,實現(xiàn)一個簡易的色調(diào)變化。但后來嘗試做光影但遇到了一些困難,這塊還是缺乏一些理論基礎(chǔ),有空了補(bǔ)一下。但特效畢竟不是目前引擎的重心,明天重點還是做核心的部分,至少要能執(zhí)行完一個魔塔游戲的基本流程。
明日施工計劃對象:事件系統(tǒng)(修bug,以及繼續(xù)做基本事件的補(bǔ)充),戰(zhàn)斗系統(tǒng)。
4-28
總結(jié)一下目前實現(xiàn)的結(jié)構(gòu)。
數(shù)據(jù)庫相關(guān)
AssetsManagerSpriteManagerAnimationManagerBattleManagerAssetsManager: 資產(chǎn)數(shù)據(jù)庫單例,管理包括材質(zhì)、敵人信息、道具信息、技能信息、事件信息、角色信息等原始靜態(tài)數(shù)據(jù)。只有加載這些數(shù)據(jù)后,才能初始化后續(xù)三部分。
SpriteManager:精靈管理單例,處理包括角色精靈、動畫精靈、窗口精靈等動態(tài)數(shù)據(jù)的基本管理。后續(xù)計劃做一個精靈緩沖池,防止進(jìn)行大量增刪行為(比如瀏覽地圖)帶來的開銷。
AnimationManager:動畫管理單例,實現(xiàn)各種特效的地方。提供動畫執(zhí)行單元實例的獲取接口。
BattleManager(施工中):戰(zhàn)斗管理單例。進(jìn)行戰(zhàn)斗數(shù)據(jù)管理,主要包括獲取敵人數(shù)據(jù)、戰(zhàn)斗傷害的計算。由于戰(zhàn)斗本身是屬于事件的部分,所以這部分純粹是作為一個API接口,如,輸入勇士信息,查詢敵人、獲取敵人信息并返回,不涉及到對實際運行數(shù)據(jù)產(chǎn)生的影響——但實際運行中的數(shù)據(jù)會影響到這里的計算結(jié)果。
游戲性相關(guān)
分發(fā)消息extendsextendsextendsextendsControlManagerMessageManagerListenerSceneManagerMapManagerEventManagerActorManagerControlManager:控制管理單例。包括兩部分:1. 用戶的輸入指令管理 2. 輸入指令后產(chǎn)生消息的管理。
MessageManager:消息處理單例。負(fù)責(zé)匯總各種消息來源的消息,并分發(fā)給各個監(jiān)聽者。
Listener:監(jiān)聽基類,相當(dāng)于是為MessageManager專門配的一個接收者。所有被動接收消息進(jìn)行處理的管理器都需要繼承此類。
SceneManager:場景管理單例。負(fù)責(zé)場景繪制,包括狀態(tài)欄、菜單欄、地圖界面、UI界面等。
MapManager:地圖管理單例。負(fù)責(zé)地圖的狀態(tài)存儲,包括地圖上的角色信息,地形信息等。
EventManager:事件管理單例。負(fù)責(zé)響應(yīng)事件消息,如移動事件、戰(zhàn)斗事件、自定義事件、轉(zhuǎn)場事件等。
今天首先做了一個Listener類,把之前的幾個消息接收者都?xì)w總了起來,使之具有高擴(kuò)展性。
使用的一個例子如下,增加一個新的戰(zhàn)前事件,改變角色的屬性:
理論上所有繼承了Listener的實例都能注冊接收這個消息然后進(jìn)行處理,也能work,但是不應(yīng)該這么做。因為在目前的實現(xiàn)中,如果消息接收者都進(jìn)行異步處理,那么將是一種偽并發(fā)狀態(tài),無法確定先后,而事實上,不同模塊的消息處理優(yōu)先級是不同的,所以后續(xù)可能會調(diào)整模塊消息接收的優(yōu)先級,部分模塊的消息處理很可能需要等其他模塊都完成后才進(jìn)行。
然后是戰(zhàn)斗系統(tǒng)。借用了一些原樣板的戰(zhàn)斗計算內(nèi)容,實現(xiàn)了技能的部分。
魔塔戰(zhàn)斗系統(tǒng)介紹
戰(zhàn)斗系統(tǒng)是魔塔類游戲的核心,屬于一種固定數(shù)值的回合制戰(zhàn)斗,即勇者、敵人每回合互相造成傷害,直到一方倒下,在沒有加特殊技能的情況下,其結(jié)果是能夠通過公式解析出來的。在傳統(tǒng)的三原塔里(4399的50層、新新、24層),是有戰(zhàn)斗動畫演示這個回合過程的,但在現(xiàn)代魔塔游戲里(以RM魔塔、H5魔塔為代表),這個戰(zhàn)斗動畫被基本取消了,玩家更多的重點關(guān)注在路線中,尤其以H5為甚,不僅取消了動畫,還引入了瞬移,以加快游戲節(jié)奏,此外,玩家還需要查看大量的傷害數(shù)據(jù),以及更高階的數(shù)據(jù)信息,包括臨界減傷表(加x點攻擊減少x點傷害)、防御減傷表(1防減少x點傷害)等。這使得戰(zhàn)斗系統(tǒng)有一定的計算負(fù)擔(dān),但是傳統(tǒng)的戰(zhàn)斗算法是有解析解的,所以問題不大。
但是,在一些藍(lán)海塔加入一些特殊技能后,比如“第x回合造成x點傷害”、“怪物每回合增加x點防御”、這種,很難有解析算法,再加上魔塔中勇者數(shù)據(jù)是變化很快的,到處都是引起屬性變化的寶石,使用緩存計算基本不太可能,很可能在計算一些大數(shù)據(jù)塔的過程中產(chǎn)生嚴(yán)重的卡頓。
因此關(guān)注戰(zhàn)斗系統(tǒng),首先就對技能進(jìn)行一定的關(guān)注。技能本質(zhì)上是一個影響戰(zhàn)斗進(jìn)程的特殊變量,正常的戰(zhàn)斗過程如下(感覺這個過程可以叫做戰(zhàn)斗管線了…):
戰(zhàn)斗基本信息勇者信息敵人信息傷害信息beforeBattlegetHeroInfogetEnemyInfo計算傷害afterBattle怪物技能可以繼承一個基類,基類的以上函數(shù)全部留空,即按默認(rèn)來。技能覆蓋對應(yīng)的函數(shù)后,可以對特定過程的數(shù)據(jù)流發(fā)生變化。
舉例來說,【硬化皮膚】技能1:怪物的防御力額外增加勇者50%攻擊的數(shù)值。那么就繼承g(shù)etEnemyInfo函數(shù),將敵人的防御數(shù)據(jù)修改即可。
但這么修改也有問題:修改不是線性的,多個技能時會發(fā)生沖突。比如有個技能2:【防御強(qiáng)化】怪物防御力增加20%。
因此每個函數(shù)添加一個修改單元,存放每個技能產(chǎn)生的修改結(jié)果,主要有兩種,一種是百分比(percentage)、一種是固定數(shù)值變化(hardchange),接下來的寫法就是:
一般來說這個模型對于大部分?jǐn)?shù)值類的技能是夠用的,但對于一些特定需求可能無法滿足,一方面,比如勇者的某種屬性依賴于怪物(比如勇者防御力增加怪物的某個屬性值),另一方面比如回合類的技能,其本質(zhì)是循環(huán),需要對傷害計算進(jìn)行大量修改,暫時不考慮。
此外一個優(yōu)化點,現(xiàn)代魔塔的地圖顯傷包含了上圖過程2、3、4的計算,而且互相獨立,據(jù)鹿神介紹可以用Worker實現(xiàn)異步計算,明天學(xué)習(xí)一下。
4-29
今天繼續(xù)完善戰(zhàn)斗系統(tǒng)。
昨天提到的Worker去看了,發(fā)現(xiàn)這個東西需要獨立的上下文環(huán)境進(jìn)行線程計算,和主線程之間只能通過通信進(jìn)行交互,這就很麻煩了。后來想到可以用空閑計算的方法進(jìn)行異步計算,但目前測試還沒有到性能瓶頸,先暫時放棄優(yōu)化這塊。
戰(zhàn)斗部分完善傷害計算,將顯傷加入地圖場景中。效果如下:
目前已經(jīng)能夠完成一個最簡單的游戲流程:打怪、撿寶物、切換地圖、對話,但還缺少一個重要的模塊:狀態(tài)管理。
這里的狀態(tài),指的是包括勇者的數(shù)值、游戲變量、錄像等實時信息,之所以要對這塊進(jìn)行管理,是因為涉及到一個重要功能:存讀檔。
SL大法玩過游戲的都知道,遇事不決就存檔是rpg中的常見操作,在魔塔中更甚,玩一座有一定難度的塔會產(chǎn)生大量的存檔,因為其中包含大量的路線分歧,經(jīng)常需要頻繁存讀檔,再加上H5有一個【自動存檔】功能,因此對存讀檔的性能有一定的需求。
就之前的經(jīng)驗看,當(dāng)塔層數(shù)較低(低于一百層)時,幾乎不會有卡頓,但在層數(shù)上升到一百多以上時,由于地圖數(shù)據(jù)讀檔和存檔過程中反復(fù)刷新,會產(chǎn)生一定的延遲。
因此,如果要用樣板制作大型的藍(lán)海塔,有必要對存讀檔進(jìn)行優(yōu)化。
明天進(jìn)行狀態(tài)管理部分的施工。
4-30
狀態(tài)管理參考了一些博文(存讀檔功能在unity3d的實現(xiàn) ),將游戲中狀態(tài)分為如下幾個部分:
- 勇士狀態(tài)(生命、攻擊、防御、道具……)
- 進(jìn)程(游戲變量、統(tǒng)計信息)
- 地圖(塊的設(shè)置和移除情況)
- 事件(位置、激活情況)
本質(zhì)上來說,這些都可以歸為變量,但在考慮到對狀態(tài)的存檔讀檔的時候,又有一些區(qū)別,其中勇士狀態(tài)和進(jìn)程在存讀檔時是需要完全存儲和加載的,不可分割,但是地圖和事件就不一定了。
舉例來說,一個游戲玩到第三關(guān),后面還有四關(guān)沒打,這時讀取第二關(guān)的存檔,那么存檔中關(guān)于后面四關(guān)的信息是無需加載的,存儲也一樣。這可以通過臟標(biāo)記來實現(xiàn)。但這樣還不夠。當(dāng)游戲進(jìn)行到中后期,已經(jīng)改動了很多的地圖狀態(tài),臟標(biāo)記已經(jīng)很難有優(yōu)化效果了,需要另外找辦法。
通過對原樣板的觀察測試發(fā)現(xiàn),此時存讀檔的主要開銷在于對全部地圖數(shù)據(jù)的記錄和讀取,測試中兩百層地圖的讀取大約需要200毫秒(5fps),這對逐幀繪制的一些特效會產(chǎn)生明顯卡頓,可以通過懶加載的方式避免:只對讀檔的目標(biāo)地圖進(jìn)行加載,其他的部分等到訪問時再進(jìn)行加載。
存檔相對高效一些,但是也只有10fps,在自動存檔時,也會影響一些需要高fps的畫面,存檔的優(yōu)化可以通過建立存檔樹來解決。存檔樹演示如下:
當(dāng)前x存檔1存檔2存檔3假設(shè)存檔3是之前試錯的一條路,通過讀檔回存檔1后,進(jìn)行另一個選擇,存儲了存檔2。注意到,無論是存檔3還是存檔2,都是在存檔1的基礎(chǔ)上進(jìn)行的改動,那就意味著:存儲的時候不必存儲全部內(nèi)容,只需要存儲相對于父存檔的改動即可。這個本質(zhì)上也是一種臟標(biāo)記的應(yīng)用,但是會在每一次存檔的時候,清除臟標(biāo)記,所以臟標(biāo)記會很少。
但是如果存檔進(jìn)行了這樣的改動,讀檔也必須與之匹配才行。如果只存儲相對改動,這無疑會增加讀檔的開銷:讀檔需要去找存檔樹的關(guān)系進(jìn)行拼接,不斷查詢存檔,這是很費時的,因為存檔不是全部都存在內(nèi)存中。
這就產(chǎn)生了矛盾:加速存檔,就會減慢讀檔,加快讀檔,就會減慢存檔,有沒有兩全其美的辦法,既能迅速讀檔也能迅速存檔?……很遺憾,暫時沒有,但可以優(yōu)先解決自動存檔,這樣就解決了大部分可能的卡頓。
在游戲過程中,觸發(fā)最為頻繁的是自動存檔,自動存檔指的是在進(jìn)行一些操作,如切換地圖、戰(zhàn)斗、開門等不可預(yù)知行為時進(jìn)行的存檔,相當(dāng)于上個保險,防止誤操作。目前的版本中支持一定步數(shù)的連續(xù)回檔,即自動存檔成一個隊列,可以回到前幾步的狀態(tài)。
自動存讀檔是典型的適合存檔樹+懶加載的優(yōu)化點,原因是每次存儲和讀檔都修改極小,而且存檔都在內(nèi)存中(自動存檔不會全部持久化),是一個天然的鏈?zhǔn)浇Y(jié)構(gòu),因此可以對其進(jìn)行著重優(yōu)化。
實現(xiàn)上,先實現(xiàn)一個最簡單的緩存基類,用于優(yōu)化自動存讀檔。包含方法有:
然后地圖管理包含一個繼承緩存基類的對象,切換樓層以及對圖塊增刪時進(jìn)行標(biāo)臟。
在戰(zhàn)斗后加上自動存檔,測試發(fā)現(xiàn)每次存儲量都很小(一張地圖),存取時間約20毫秒,基本不影響性能。
明天再考慮如何做手動存讀檔以及更復(fù)雜的樹形分支。
5-1
考慮以下三個基本功能:(規(guī)則1)
回退是一個之前沒考慮的新功能,就我個人來說,一般用于load手滑多退了一步的情況,但也有說能用來分析路線?不過這些不重要。
演示如下,狀態(tài)1~3是存儲的三個存檔,當(dāng)前狀態(tài)是未保存的進(jìn)度。
狀態(tài)1狀態(tài)2狀態(tài)3當(dāng)前狀態(tài)往回讀(load):
Xload狀態(tài)1狀態(tài)2狀態(tài)3當(dāng)前狀態(tài)此時無法使用back,原因是前一個【當(dāng)前狀態(tài)】并沒有存儲,已經(jīng)丟失(如果在讀取時進(jìn)行了存儲則另算,先不考慮)。
再次load:
此時可以通過back回到狀態(tài)3。
為了實現(xiàn)手動存檔讀檔,將在這個模型基礎(chǔ)上對save、load、back進(jìn)行第一次擴(kuò)展:(規(guī)則2)
如下:
load:index=2back:index=2狀態(tài)1狀態(tài)2狀態(tài)3當(dāng)前狀態(tài)這樣可以實現(xiàn)鏈?zhǔn)酱鏅n的手動存讀。但存在一個問題:如果在讀回狀態(tài)1后,進(jìn)行了新的狀態(tài)保存,就會變成:
狀態(tài)1狀態(tài)2狀態(tài)3狀態(tài)4當(dāng)前狀態(tài)此時之前的模型不能適用,將再次擴(kuò)充(規(guī)則3):
改動比較大的在load,演示如下,從狀態(tài)4讀到狀態(tài)3:
1.load2.back狀態(tài)1狀態(tài)2狀態(tài)3狀態(tài)4這樣實現(xiàn)的性能瓶頸在于查詢各個存儲的狀態(tài)然后進(jìn)行合并。據(jù)鹿神說并發(fā)讀取存檔開銷并不大,試了一下localforage,讀取900個存檔只用了70ms。因此存讀這塊并不是問題,難的是如何實現(xiàn)這一塊。有可能并不會需要這種LCA操作,一定深度后做一次全存是一個好的方法。
明天再嘗試進(jìn)行具體的實現(xiàn)施工…
5-2
存檔實現(xiàn)預(yù)期超過預(yù)期時間,放棄,改為懶加載優(yōu)化。
存檔時:如果有沒有修改過的樓層,就不必重新壓縮,直接存入。
讀檔時:讀取未解壓的存檔,只有訪問目標(biāo)樓層時,才解壓目標(biāo)樓層。
完善材質(zhì)類型,增加對tileset的支持。
調(diào)研自動元件的實現(xiàn):自動元件參考文章:RMXP的自動元件繪制原理
原樣板的繪制方法不再適合PIXI的框架,需要基于ActorSprite增加一種多模態(tài)的元件,情況略有些復(fù)雜,但原理不變。
5-3
自動元件竣工。
每個自動元件圖塊包含四個小sprite,通過放置在四個角落拼湊為一個完整的圖塊,這四個圖塊的模式一共47種情況,由九宮格的邊角決定。
上面的參考博文給出了“繪制情況-小元件”的映射表,但沒有給出“邊角-繪制情況”的映射,在此記錄如下:
// javascript let edge = {}; /*** 對mask符合filter的edge填充角落* @param value* @param filter*/ function fillCorner(filter, value, mask) {mask = mask || 0xf;for(let i = 0; i < (1<<8); i++){if((i & mask) == filter){edge[i] = value;}} } // 0 邊 fillCorner(0, 47); // 1 邊 fillCorner((1<<0), 42); // 下 fillCorner((1<<1), 43); // 右 fillCorner((1<<2), 44); // 上 fillCorner((1<<3), 45); // 左 // 2. 2邊 fillCorner((1<<0) + (1<<2), 32); // 下 + 上 fillCorner((1<<1) + (1<<3), 33); // 右 + 左 —— 對角無影響fillCorner((1<<1) + (1<<0), 35, 0xf | (1<<4)); // 右下* fillCorner((1<<1) + (1<<0) + (1<<4), 34, 0xf | (1<<4)); // 右下* —— 4fillCorner((1<<1) + (1<<2), 41, 0xf | (1<<5)); // 右上* fillCorner((1<<1) + (1<<2) + (1<<5), 40, 0xf | (1<<5)); // 右上* —— 5fillCorner((1<<3) + (1<<2), 39, 0xf | (1<<6)); // 左上* fillCorner((1<<3) + (1<<2) + (1<<6), 38, 0xf | (1<<6)); // 左上* —— 6fillCorner((1<<3) + (1<<0), 37, 0xf | (1<<7)); // 左下* fillCorner((1<<3) + (1<<0) + (1<<7), 36, 0xf | (1<<7)); // 左下* —— 7// 3. 3邊// 缺左 左角無影響 // 右滿 fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<4) + (1<<5), 16, 0xf | ((1<<4) + (1<<5))); // 右下 fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<4), 17, 0xf | ((1<<4) + (1<<5))); // 右上 fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<5), 18, 0xf | ((1<<4) + (1<<5))); // 無右 fillCorner((1<<0) + (1<<2) + (1<<1), 19, 0xf | ((1<<4) + (1<<5))); // 缺上 fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<4) + (1<<7), 20, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 下滿 fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<7), 21, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 左下 fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<4), 22, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 右下 fillCorner((1<<0) + (1<<1) + (1<<3), 23, 0xf | ((1<<4) + (1<<7))); // 缺 上 // 缺右 fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<6) + (1<<7), 24, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左滿 fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<6), 25, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左上 fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<7), 26, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左下 fillCorner((1<<0) + (1<<2) + (1<<3), 27, 0xf | ((1<<6) + (1<<7))); // 缺 右fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<5) + (1<<6), 28, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 上滿 (存疑 26 27 44 45 ?) fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<6), 30, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 左上 fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<5), 29, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 右上 fillCorner((1<<1) + (1<<2) + (1<<3), 31, 0xf | ((1<<5) + (1<<6))); // 缺 下// 4. 4邊 let four = (1<<0) + (1<<1) + (1<<2) + (1<<3); // -------- 右下 右上 左上 左下 ----------- edge[four + (1<<4) + (1<<5) + (1<<6) + (1<<7)] = 0; edge[four + (1<<4) + (1<<5) + (0<<6) + (1<<7)] = 1; // 缺左上 edge[four + (1<<4) + (0<<5) + (1<<6) + (1<<7)] = 2; // 缺右上 edge[four + (1<<4) + (0<<5) + (0<<6) + (1<<7)] = 3; // 缺左上 右上 edge[four + (0<<4) + (1<<5) + (1<<6) + (1<<7)] = 4; // 缺右下 edge[four + (0<<4) + (1<<5) + (0<<6) + (1<<7)] = 5; // 缺右下 左上 edge[four + (0<<4) + (0<<5) + (1<<6) + (1<<7)] = 6; // 缺右下 右上 edge[four + (0<<4) + (0<<5) + (0<<6) + (1<<7)] = 7; // 缺右下 右上 左上 edge[four + (1<<4) + (1<<5) + (1<<6) + (0<<7)] = 8; // 缺左下 edge[four + (1<<4) + (1<<5) + (0<<6) + (0<<7)] = 9; // 缺左上 左下 edge[four + (1<<4) + (0<<5) + (1<<6) + (0<<7)] = 10; // 缺左下 右上 edge[four + (1<<4) + (0<<5) + (0<<6) + (0<<7)] = 11; // 缺左上 左下 右上 edge[four + (0<<4) + (1<<5) + (1<<6) + (0<<7)] = 12; // 缺左下 右下 edge[four + (0<<4) + (1<<5) + (0<<6) + (0<<7)] = 13; // 缺左下 左上 右下 edge[four + (0<<4) + (0<<5) + (1<<6) + (0<<7)] = 14; // 缺左下 右上 右下 edge[four] = 15; // 都缺最后得到的一個edge,是一個邊角情況到繪制情況256-47的映射,這個可以作為繪制依據(jù)的查詢。
攝像機(jī)(Camera)的概念
地圖的進(jìn)一步完善是實現(xiàn)大地圖。當(dāng)前的地圖只能提供一定寬度的顯示,超過則會溢出到邊界,無法正常運行。要實現(xiàn)更大尺寸的地圖,在此先引入攝像機(jī)的概念。
Camera一般在3d游戲中使用比較多,因為涉及到成像透視等一系列需求,需要這個概念來幫助理解。在2d游戲中因為是平面的緣故,一般都是用畫布概念,動畫和游戲過程就是不斷繪制的過程。但涉及到遮擋的時候,畫布就無法幫助理解了。
2d攝像機(jī)本質(zhì)也是一個畫布,但它對應(yīng)著一個游戲?qū)嶓w,也即照射的場景(Scene),之前做場景管理,不僅管理場景中的數(shù)據(jù),還混入了渲染,而引入攝像機(jī)后,渲染的過程就應(yīng)放到攝像機(jī)中,其邏輯為:
數(shù)據(jù)渲染場景攝像機(jī)canvas攝像機(jī)要存儲視角(viewPoint),作為場景繪制的依據(jù),其次,還需要一個渲染區(qū)域(renderArea)作為繪制目標(biāo)——即畫到窗口的何處。當(dāng)繪制超出邊界時,需要進(jìn)行剪裁。最后,為了讓視角跟隨主角,需要有一個綁定對象到視角的方法。
目前實現(xiàn)能夠在電腦上基本能夠較為流暢地繪制一個52x52的大地圖,但在低性能手機(jī)上會嚴(yán)重的幀率下降(下降至20fps),推測是由于大量的sprite更新導(dǎo)致的,可以考慮進(jìn)行優(yōu)化——使用緩沖池,只對當(dāng)前畫面的一部分進(jìn)行刷新。
5-4
給大地圖加了緩動后基本竣工,留下兩個問題:1 手機(jī)上的性能優(yōu)化 2 讀檔優(yōu)化(大地圖讀檔會導(dǎo)致大量sprite申請和銷毀 這是沒必要的),等之后細(xì)節(jié)優(yōu)化再進(jìn)行吧,今天主要進(jìn)行事件部分的施工。
事件部分的改進(jìn)點如下:
后記
架構(gòu)的施工記錄到此基本接近尾聲,后面就是添磚加瓦充實游戲性系統(tǒng)、細(xì)節(jié)優(yōu)化以及找bug。對其中有價值的部分會專門開文章寫,這篇不會再更新了,主要最近又忙起來了,不知道咕到什么時候才能全部做完……就這樣吧。
總結(jié)
以上是生活随笔為你收集整理的游戏引擎mota-js-v3.0 施工记录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 调用scp命令_linux之s
- 下一篇: 百度云 x 中国联通 | 立标杆,中国联