于萬國,胡宗森,隋麗娜,遲 劍,蔡永華,傅冬穎
(河北民族師范學院 數(shù)學與計算機科學學院,河北 承德 067000)
目前,市面上占有率領先的移動游戲引擎有Cocos2d-x、Unity3D、Egret(白鷺)等。Unity3D最大的優(yōu)勢是腳本化和組件化,簡化了游戲開發(fā)工作流中的場景編輯[1-2]。而Cocos2d-x是一個基于MIT協(xié)議的開源游戲框架,具備游戲開發(fā)快速、簡易、跨平臺的特點,基于Cocos2d-x的CocosCreator更是包含了游戲引擎、資源管理、場景編輯、游戲預覽和發(fā)布等游戲開發(fā)所需的全套功能,并將所有的功能和工具鏈整合在一個統(tǒng)一的應用程序里[3-5]。
首先,它以數(shù)據(jù)驅(qū)動和組件化作為核心的游戲開發(fā)方式,無縫融合了引擎成熟的JavaScript API體系,一方面能夠適應Cocos系列引擎開發(fā)者習慣,另一方面為美術和策劃人員提供前所未有的內(nèi)容創(chuàng)作生產(chǎn)和即時預覽測試環(huán)境;其次,CocosCreator支持游戲的熱更新功能[6-7],引擎的C++代碼里,已經(jīng)留好了相應的接口,開發(fā)者只需理解其更新流程,便可以在此基礎上自定義熱更新,不需花費大量的精力重寫熱更新的功能;再次,CocosCreator構建的native工程,與原cocos2d-js的工程基本相同,所以在手機端功能的拓展依舊很方便(比如接游戲的SDK等等)。因此CocosCreator逐步開始流行起來[8-9]。
該文從軟件工程專業(yè)實踐課程應用典型案例庫的構建角度出發(fā),利用Cocos2d-x游戲引擎的CocosCreator作為開發(fā)工具,設計了一款跨平臺(web、Android) APP—躲避刺豚君。
游戲服務端使用Tomcat搭建服務器,用Java語言編寫應用,用MySQL作為存儲數(shù)據(jù)庫。游戲客戶端在UI方面使用CocosCreator,采用MVC模式進行設計,WebStorm進行程序編輯;安裝Chrome瀏覽器進行Web平臺的調(diào)試;安裝Python及原生平臺上安卓所需的NDK、SDK與ANT,用于安卓工程的構建與編譯;使用ADT或Android Studio對安卓工程進行管理。該游戲中的Data、Module、View模塊,分別對應MVC模式中的Controller、Model與View模塊。具體實現(xiàn)方式如下:Data模塊在獲取到服務端的數(shù)據(jù)后,將數(shù)據(jù)進行存儲。View模塊需要更新數(shù)據(jù)時,通過Module模塊將Data模塊已存儲的數(shù)據(jù)進行處理,處理結束后Module模塊將處理后的數(shù)據(jù)傳遞給View模塊。這種模塊劃分的優(yōu)點是能夠輕松地將游戲的邏輯與UI進行分離,在改進界面和改善用戶交互體驗的同時,不需要重新編寫業(yè)務邏輯[10]。
游戲服務端的功能有兩個:一是玩家的注冊與登錄,二是在游戲結束后將玩家當前的分數(shù)進行存儲。
客戶端分為四個場景:登錄場景、開始場景、主場景和結束場景。登錄場景中提供了玩家的注冊與登錄功能。開始場景中,玩家可以查看開發(fā)人員的一些信息,可以控制游戲的音樂和音效的開與關,并且能夠正式開始游戲。主場景中,玩家可以點擊動物獲得相應的分數(shù),界面上顯示了游戲的當前剩余時間與當前的分數(shù),另外玩家可以控制游戲的暫停與繼續(xù),還可以返回游戲主菜單(即開始場景)。結束場景中顯示了游戲當前分數(shù)與歷史最高分數(shù),玩家可以選擇重玩游戲或返回主菜單。
游戲功能的思維導圖如圖1所示。

圖1 游戲功能的思維導圖
開始游戲后,游戲的剩余時間和總分數(shù)會在開始游戲的準備動畫后開始計算。在點擊兔子和熊的時候,游戲的分數(shù)會增加一定的值,而且游戲的時間也會增加。但如果點擊到了刺豚就會進行懲罰,游戲的分數(shù)會扣除相應的值,并扣除一定的游戲時間。在游戲時間為零后,游戲結束。
服務端設計:本游戲采用B/S(瀏覽器/服務器)架構[11]進行設計。服務端使用Tomcat作為服務器。當客戶端發(fā)出請求后,服務端收到請求并在GameServlet類的doPost方法中對請求的參數(shù)通過switch做篩選,然后調(diào)用DatabaseUtil類的方法將數(shù)據(jù)處理后,通過Writer類的一個實例化的對象將處理后的結果轉(zhuǎn)化為JSON結構并返回給客戶端。用到的數(shù)據(jù)庫的User表如表1所示。

表1 User表
服務端實現(xiàn):游戲的服務端使用Tomcat作為服務器。當客戶端發(fā)出請求后,服務端收到請求并在GameServlet類的doPost方法中對請求的參數(shù)通過switch做篩選,然后調(diào)用DatabaseUtil類的方法將數(shù)據(jù)處理后,通過Writer類的一個實例化的對象將處理后的結果轉(zhuǎn)化為JSON結構并返回給客戶端。
客戶端游戲結構例圖如圖2所示。

圖2 游戲結構例圖
其中:
(1)network模塊中,Http.js與HttpAciton.js結合使用,用來往服務端發(fā)送請求,這里使用POST請求。具體實現(xiàn)的代碼為:
sendRequest(succ,fail) {
var xhr=new XMLHttpRequest();
xhr.timeout=this.timeout;
this.url=encodeURI(this.url);
xhr.open("POST", this.url, true);
['abort' ,'error','timeout'].forEach(function(eventname){
xhr[`on${eventname}`]=function(){
fail(-1,'network error');
}
})
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.onreadystatechange=function(){
if(xhr.readyState===4 && (xhr.status>=200 && xhr.status<=207)){
let data=xhr.responseText;
let json;
try {
json=JSON.parse(data);
} catch(e){
} finally{
json=null;
}}}
xhr.send();
}
(2)module模塊用于對數(shù)據(jù)的處理。BaseMoudle是基類,BaseData是派生類。
(3)data模塊用于接收服務器請求,BaseData是基類,PlayerData是派生類。具體實現(xiàn)為:在派生類實例化的時候,添加一個本地監(jiān)聽NOTI_DATA_UPDATE,在網(wǎng)絡請求收到信息后,便執(zhí)行這個監(jiān)聽,并將收到的信息存儲到PlayerData中。
(4)utils模塊中,Notification.js和NotifyName.js這兩個腳本都是一個對象,其中postNoti方法為發(fā)消息,addOb為給一個對象添加某消息的監(jiān)聽,removeOb便是給一個對象移除某條監(jiān)聽。
具體實現(xiàn)的代碼為:
postNoti(name, msg) {
const cbs=this.noti[name];
if (!cbs) {
return;
}
for (let i=0;i const dic=cbs[i]; const obj=dic.obj; const cb=dic.cb; obj[cb].call(obj, msg); } }, addOb(name,cb,obj) { let cbs=this.noti[name]; if (!cbs) { cbs=[{cb:cb, obj:obj}]; this.noti[name]=cbs; } else { for (let i=0;i let dic=cbs[i]; if (dic.cb===cb && dic.obj===obj) { return; } } cbs.push({cb:cb, obj:obj}); } } TextureManager.js和ViewMannager.js結合使用,用于動態(tài)加載游戲中的Prefab和Sprite,并對Sprite進行管理,在需要的時候進行texture的清理。view模塊是游戲中各個具體功能的實現(xiàn)模塊,詳細介紹參見3.3節(jié)。 先解釋一下Prefab的加載原理。開發(fā)人員在層級管理器中里編輯好一個Prefab(預制件)后,將它拖動到資源管理器,這時將會在PC的本地創(chuàng)建一個***.prefab和該預制件對應的meta文件,在用編輯器打開***.prefab文件后,會發(fā)現(xiàn)它就是一個包含節(jié)點信息的json結構。所以,加載預制件就是將本地的json結構文件加載并且解析出來,然后形成了node的結構,再通過addChild將這個節(jié)點添加到自己想要的節(jié)點上,便完成了該節(jié)點的創(chuàng)建[12]。而meta文件中主要存儲該文件的uuid,在引用資源的時候,便通過這個uuid去查找對應的文件,在內(nèi)存中該資源也是通過它的uuid作為Key存儲到相應的map中[13]。 游戲客戶端實現(xiàn)用到了三個場景,開始場景、主場景、結束場景。各場景分別配有該場景的腳本。該種做法的目的是:能夠在新場景加載完畢后,優(yōu)先做一些處理。比如一些大型游戲項目,如果是新手進入主場景,就要加載到該用戶當前的引導界面,而老用戶則需要加載主場景的第一個界面。 (1)開始場景。 在StartScene.js這個腳本onLoad執(zhí)行的時候,先通過cc.loader.loadRes加載LoadView這個預制件,并在load成功后,將這個預制件存到ViewManger中。這樣的目的是在加載其他預制件時,如果該預制件的節(jié)點過多,可能會很卡,也就是說消耗性能,這時再將這個預制件實例化出來,直接加載到場景上,相當于一個過渡的作用。當然開發(fā)人員也可以在這個預制件上,添加自定義的腳本,來做一些特殊處理。比如:如果想讓用戶在加載預制件的時候,不允許點擊屏幕上其他節(jié)點(防止造成誤操作),就可以在這個腳本里,給這個節(jié)點添加一個觸摸事件,并且阻止觸摸事件向下傳遞。存儲完之后,再加載StarView.prefab。 關鍵代碼如下: cc.loader.loadRes('/Prefab/LoadView', function (err, loadViewPrefab) { ViewManager._loadViewPrefab=loadViewPrefab; ViewManager.createPrefabNode('/Prefab/Start/StartView', 'StartView', function (node) { this.node.addChild(node); }.bind(this)); }.bind(this)) StartView.prfab加載成功后,先添加一個監(jiān)聽?!瓹LOSE_ABOUT_VIEW’,該監(jiān)聽的作用是在關閉“關于我們”界面后,將節(jié)點的透明度設置成最大值。接著調(diào)用toggleState這個方法,先初始化當前的音樂與音效狀態(tài)。在該方法中使用cc.sys. localStorage存儲本地信息。像游戲中的音樂、音效的狀態(tài)通常是都需要保存的。但并不需要保存到服務端的數(shù)據(jù)庫中,這里用到了HTML5的本地存儲localStorage[7],如圖3所示。 圖3 localStorage的存儲實例 而native端則是分別調(diào)用ios和android設備原生讀寫文件的方法,將數(shù)據(jù)存儲到可讀寫區(qū)域。 關鍵代碼如下: toggleState() { let ls=cc.sys.localStorage; if (ls.getItem('Music')=='ON') { this.musicToggle.isChecked=true; Common.playMusic('Start'); } else if (ls.getItem('Music')=='OFF') { this.musicToggle.isChecked=false; } else { ls.setItem('Music', 'ON'); Common.playMusic('Start'); this.musicToggle.isChecked=true; } if (ls.getItem('Audio')=='ON') { this.audioToggle.isChecked=true; } else if (ls.getItem('Audio')=='OFF') { this.audioToggle.isChecked=false; } else { ls.setItem('Audio', 'ON'); this.audioToggle.isChecked=true; } } 界面上的聲音和音效的開關按鈕則是通過給該節(jié)點綁定一個Toggle組件,并添加checkEvent事件,在點擊該開關的時候再將對開關的狀態(tài)改變,并把開或關的狀態(tài)值再次存起來。 點擊按鈕后,先加載AboutView.prefab,加載成功后,在其對應的腳本的onLoad里,去調(diào)用setContent方法。本游戲在該方法中將已存儲的開發(fā)者的個人信息的json文件讀取出來,并通過Label展示出來。 點擊開始游戲按鈕,則進入主場景。 (2)主場景。 該場景中用到的組件類型的腳本有MainView.js、Center.js、Animal.js、PauseView.js,邏輯類型的腳本(類)有GameEngine.js、UnitController.js、Unit.js、Progress.js。 MainView的作用是用來管理整個場景上的UI顯示,GameEngine的作用是控制場景中的游戲邏輯,如:管理UnitController和Progress并完成向MainView發(fā)送消息。Center和UnitController是一一對應關系,也就是UnitController是Center抽象出來的概念。Animal和Unit也是這樣的關系。UnitController用來管理它所屬的Unit,而Center對應的節(jié)點又是Animal對應節(jié)點的父節(jié)點,這樣便使這四者的關系統(tǒng)一起來。Progress是用來計算游戲時間類,在游戲時間結束后,便通過GameEngine發(fā)送游戲結束的消息,在MainView接收到消息后,便跳轉(zhuǎn)到結束場景。 游戲主場景具體實現(xiàn)過程如下: ①在MainView.js的OnLoad執(zhí)行的時候,實例化一個GameEngine,并開始初始化GameEngine。也就是調(diào)用GameEngine的init方法(相當于構造方法),并在其中實例化Progress。在初始化結束后,便向MainView發(fā)消息,執(zhí)行MainView中的init方法,該方法的作用是播放Ready和Go這兩個動畫。 ②開始創(chuàng)建一組動物。首先實例化一個UnitController,然后執(zhí)行它的init方法,然后創(chuàng)建若干個unit,并設置這些unit的信息,如:類型(狗熊、兔字、刺豚)、分值及時間獎勵。創(chuàng)建完成后,引擎向view層發(fā)消息,view收到消息后,便通過對應的prefab創(chuàng)建對應的動物節(jié)點(綁定Animal組件),并把這個節(jié)點添加到它們對應的Center(該腳本中自定義了一個函數(shù),實現(xiàn)了加速度,使動物上拋和下落更加真實)上。這樣便實現(xiàn)了一組動物中,能夠以相同的運動規(guī)律進行運動。 ③在玩家點擊動物節(jié)點時,該節(jié)點上的Animal.js中的點擊事件將會觸發(fā)。在點擊事件觸發(fā)后的回調(diào)方法中,將該Animal對應的Unit的狀態(tài)進行改變(包括計分、獎懲游戲時間等等),并播放該節(jié)點相應的動畫。如果是狗熊或兔子,就從它們的父節(jié)點脫離出來,直接添加到MainView背景對應的節(jié)點上,并開始向上飛,飛出屏幕后,移除該節(jié)點。而如果是刺豚的話,僅需要在執(zhí)行完點擊的動畫后,移除即可。如果Center對應的節(jié)點,下落到屏幕外以后,再通過UnitController執(zhí)行GameEngine的創(chuàng)建動物的方法,再創(chuàng)建新的一組。 ④關于游戲的暫停方面,本游戲并沒有選擇使用cc.game.pause()這個引擎直接封裝好的用于暫停的方法。當然相對于這個小項目,使用該方法就行。本游戲沒有使用它的原因是,當使用這個方法的時候,整個項目中的游戲邏輯,渲染,事件處理,背景音樂和所有音效將會全部暫停,這樣導致的問題是:如果開發(fā)者想在暫停后彈出新的對話框,而該對話框中仍然需要有一些動態(tài)的效果(如:使用ScrollView組件進行滑動等等操作)將不可能實現(xiàn)。所以這里通過在MainView定義一個字段isPause,進而控制整個游戲過程中的動態(tài)效果。這樣做也有一點小的缺陷,比如使用repeatForever的這些動作,就必須使用AcitonMannager這個單例進行單獨處理,將這個節(jié)點的動作暫停,當然在游戲繼續(xù)時,還需要使用它將這個節(jié)點當前已暫停的動作繼續(xù)。 (3)結束場景。 在該場景中,主要是將本次游戲的分數(shù)和歷史最高分數(shù)通過Label顯示出來。其過程是客戶端計算本次的分數(shù),在游戲結束時,客戶端在將本次的分數(shù)發(fā)給服務端后,服務端經(jīng)過分數(shù)比較的運算,將歷史最高分數(shù)的數(shù)據(jù)返回給客戶端。 場景之間資源管理方面,資源的釋放是在切換場景的時候,在進入下一個場景前,先釋放掉前場景所用到的資源,這對于一個小型的游戲項目來說就足夠了,不需要去花費更多精力對游戲的資源進行管理[14]。 躲避刺豚君游戲在Web和Native不同平臺進行了調(diào)試。在Web平臺,采用引擎的cc.log()的方法來輸出關鍵的變量并與使用Chrome進行斷點調(diào)試的方法相結合進行調(diào)試[13]。在Native(原生)平臺,是在編譯項目時使用Debug模式,并配合程序的cc.log()方法來輸出關鍵的變量,從而將設備連接后,可以在Eclipse的控制臺顯示這些變量的值,進而查找問題。 通過在兩個平臺的調(diào)試,發(fā)現(xiàn)兩個平臺其中的一些區(qū)別,具體如下: 首先,關于引擎的類中字段的權限問題。在web上,取到Sprite組件的spriteFrame對象之后,可以使用它的_name這個字段,而安卓調(diào)試時通過log輸出,發(fā)現(xiàn)該值為null,因此在使用該變量時就會報錯。因為C++的引擎中這個變量使用了private設置了它的訪問權限。因此需要謹慎地使用引擎內(nèi)部的帶有下劃線的私有變量。 其次,一個比較明顯的區(qū)別是在if的判斷上。在玩家首次打開游戲時,因為要設置音樂和音效開關的初始狀態(tài),所以就需要使用cc.sys.localStorage.getItem(arg)這個方法進行判斷。通過log的輸出,發(fā)現(xiàn)如果沒有返回值時,web端返回的是undefine而在安卓上返回值為null,因此需要區(qū)分開來進行判斷。 調(diào)試經(jīng)驗分享: ①在進行Android調(diào)試的時候,最好的辦法是看C++源碼,緊接著和HTML5的源碼進行對比,這樣才能更容易找到在web端沒有問題,但在native上卻出現(xiàn)問題的原因。 ②在一個比較大的游戲開發(fā)過程中,開發(fā)人員應該要時常去打包,檢查native上新開發(fā)的模塊有沒有問題,否則集中解決的時候就會消耗太多的精力。 從新工科背景下軟件工程專業(yè)實踐課程應用典型案例庫的構建角度出發(fā)[15],以應用為導向,利用Cocos2d-x游戲引擎的最新開發(fā)工具CocosCreator,從游戲開發(fā)工具選用、開發(fā)環(huán)境、游戲功能、玩法介紹等入手,詳細介紹了實現(xiàn)跨平臺游戲—躲避刺豚君的設計和實現(xiàn)過程,經(jīng)測試,游戲跨平臺運行通暢,功能完備。該游戲的開發(fā)對基于Cocos2d-x引擎的CocosCreator跨平臺游戲開發(fā)的設計和實現(xiàn)具有一定的參考價值。3.3 游戲客戶端的具體實現(xiàn)

4 游戲的調(diào)試
5 結束語