于萬國(guó),胡宗森,隋麗娜,遲 劍,蔡永華,傅冬穎
(河北民族師范學(xué)院 數(shù)學(xué)與計(jì)算機(jī)科學(xué)學(xué)院,河北 承德 067000)
目前,市面上占有率領(lǐng)先的移動(dòng)游戲引擎有Cocos2d-x、Unity3D、Egret(白鷺)等。Unity3D最大的優(yōu)勢(shì)是腳本化和組件化,簡(jiǎn)化了游戲開發(fā)工作流中的場(chǎng)景編輯[1-2]。而Cocos2d-x是一個(gè)基于MIT協(xié)議的開源游戲框架,具備游戲開發(fā)快速、簡(jiǎn)易、跨平臺(tái)的特點(diǎn),基于Cocos2d-x的CocosCreator更是包含了游戲引擎、資源管理、場(chǎng)景編輯、游戲預(yù)覽和發(fā)布等游戲開發(fā)所需的全套功能,并將所有的功能和工具鏈整合在一個(gè)統(tǒng)一的應(yīng)用程序里[3-5]。
首先,它以數(shù)據(jù)驅(qū)動(dòng)和組件化作為核心的游戲開發(fā)方式,無縫融合了引擎成熟的JavaScript API體系,一方面能夠適應(yīng)Cocos系列引擎開發(fā)者習(xí)慣,另一方面為美術(shù)和策劃人員提供前所未有的內(nèi)容創(chuàng)作生產(chǎn)和即時(shí)預(yù)覽測(cè)試環(huán)境;其次,CocosCreator支持游戲的熱更新功能[6-7],引擎的C++代碼里,已經(jīng)留好了相應(yīng)的接口,開發(fā)者只需理解其更新流程,便可以在此基礎(chǔ)上自定義熱更新,不需花費(fèi)大量的精力重寫熱更新的功能;再次,CocosCreator構(gòu)建的native工程,與原cocos2d-js的工程基本相同,所以在手機(jī)端功能的拓展依舊很方便(比如接游戲的SDK等等)。因此CocosCreator逐步開始流行起來[8-9]。
該文從軟件工程專業(yè)實(shí)踐課程應(yīng)用典型案例庫(kù)的構(gòu)建角度出發(fā),利用Cocos2d-x游戲引擎的CocosCreator作為開發(fā)工具,設(shè)計(jì)了一款跨平臺(tái)(web、Android) APP—躲避刺豚君。
游戲服務(wù)端使用Tomcat搭建服務(wù)器,用Java語言編寫應(yīng)用,用MySQL作為存儲(chǔ)數(shù)據(jù)庫(kù)。游戲客戶端在UI方面使用CocosCreator,采用MVC模式進(jìn)行設(shè)計(jì),WebStorm進(jìn)行程序編輯;安裝Chrome瀏覽器進(jìn)行Web平臺(tái)的調(diào)試;安裝Python及原生平臺(tái)上安卓所需的NDK、SDK與ANT,用于安卓工程的構(gòu)建與編譯;使用ADT或Android Studio對(duì)安卓工程進(jìn)行管理。該游戲中的Data、Module、View模塊,分別對(duì)應(yīng)MVC模式中的Controller、Model與View模塊。具體實(shí)現(xiàn)方式如下:Data模塊在獲取到服務(wù)端的數(shù)據(jù)后,將數(shù)據(jù)進(jìn)行存儲(chǔ)。View模塊需要更新數(shù)據(jù)時(shí),通過Module模塊將Data模塊已存儲(chǔ)的數(shù)據(jù)進(jìn)行處理,處理結(jié)束后Module模塊將處理后的數(shù)據(jù)傳遞給View模塊。這種模塊劃分的優(yōu)點(diǎn)是能夠輕松地將游戲的邏輯與UI進(jìn)行分離,在改進(jìn)界面和改善用戶交互體驗(yàn)的同時(shí),不需要重新編寫業(yè)務(wù)邏輯[10]。
游戲服務(wù)端的功能有兩個(gè):一是玩家的注冊(cè)與登錄,二是在游戲結(jié)束后將玩家當(dāng)前的分?jǐn)?shù)進(jìn)行存儲(chǔ)。
客戶端分為四個(gè)場(chǎng)景:登錄場(chǎng)景、開始場(chǎng)景、主場(chǎng)景和結(jié)束場(chǎng)景。登錄場(chǎng)景中提供了玩家的注冊(cè)與登錄功能。開始場(chǎng)景中,玩家可以查看開發(fā)人員的一些信息,可以控制游戲的音樂和音效的開與關(guān),并且能夠正式開始游戲。主場(chǎng)景中,玩家可以點(diǎn)擊動(dòng)物獲得相應(yīng)的分?jǐn)?shù),界面上顯示了游戲的當(dāng)前剩余時(shí)間與當(dāng)前的分?jǐn)?shù),另外玩家可以控制游戲的暫停與繼續(xù),還可以返回游戲主菜單(即開始場(chǎng)景)。結(jié)束場(chǎng)景中顯示了游戲當(dāng)前分?jǐn)?shù)與歷史最高分?jǐn)?shù),玩家可以選擇重玩游戲或返回主菜單。
游戲功能的思維導(dǎo)圖如圖1所示。
圖1 游戲功能的思維導(dǎo)圖
開始游戲后,游戲的剩余時(shí)間和總分?jǐn)?shù)會(huì)在開始游戲的準(zhǔn)備動(dòng)畫后開始計(jì)算。在點(diǎn)擊兔子和熊的時(shí)候,游戲的分?jǐn)?shù)會(huì)增加一定的值,而且游戲的時(shí)間也會(huì)增加。但如果點(diǎn)擊到了刺豚就會(huì)進(jìn)行懲罰,游戲的分?jǐn)?shù)會(huì)扣除相應(yīng)的值,并扣除一定的游戲時(shí)間。在游戲時(shí)間為零后,游戲結(jié)束。
服務(wù)端設(shè)計(jì):本游戲采用B/S(瀏覽器/服務(wù)器)架構(gòu)[11]進(jìn)行設(shè)計(jì)。服務(wù)端使用Tomcat作為服務(wù)器。當(dāng)客戶端發(fā)出請(qǐng)求后,服務(wù)端收到請(qǐng)求并在GameServlet類的doPost方法中對(duì)請(qǐng)求的參數(shù)通過switch做篩選,然后調(diào)用DatabaseUtil類的方法將數(shù)據(jù)處理后,通過Writer類的一個(gè)實(shí)例化的對(duì)象將處理后的結(jié)果轉(zhuǎn)化為JSON結(jié)構(gòu)并返回給客戶端。用到的數(shù)據(jù)庫(kù)的User表如表1所示。
表1 User表
服務(wù)端實(shí)現(xiàn):游戲的服務(wù)端使用Tomcat作為服務(wù)器。當(dāng)客戶端發(fā)出請(qǐng)求后,服務(wù)端收到請(qǐng)求并在GameServlet類的doPost方法中對(duì)請(qǐng)求的參數(shù)通過switch做篩選,然后調(diào)用DatabaseUtil類的方法將數(shù)據(jù)處理后,通過Writer類的一個(gè)實(shí)例化的對(duì)象將處理后的結(jié)果轉(zhuǎn)化為JSON結(jié)構(gòu)并返回給客戶端。
客戶端游戲結(jié)構(gòu)例圖如圖2所示。
圖2 游戲結(jié)構(gòu)例圖
其中:
(1)network模塊中,Http.js與HttpAciton.js結(jié)合使用,用來往服務(wù)端發(fā)送請(qǐng)求,這里使用POST請(qǐng)求。具體實(shí)現(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模塊用于對(duì)數(shù)據(jù)的處理。BaseMoudle是基類,BaseData是派生類。
(3)data模塊用于接收服務(wù)器請(qǐng)求,BaseData是基類,PlayerData是派生類。具體實(shí)現(xiàn)為:在派生類實(shí)例化的時(shí)候,添加一個(gè)本地監(jiān)聽NOTI_DATA_UPDATE,在網(wǎng)絡(luò)請(qǐng)求收到信息后,便執(zhí)行這個(gè)監(jiān)聽,并將收到的信息存儲(chǔ)到PlayerData中。
(4)utils模塊中,Notification.js和NotifyName.js這兩個(gè)腳本都是一個(gè)對(duì)象,其中postNoti方法為發(fā)消息,addOb為給一個(gè)對(duì)象添加某消息的監(jiān)聽,removeOb便是給一個(gè)對(duì)象移除某條監(jiān)聽。
具體實(shí)現(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結(jié)合使用,用于動(dòng)態(tài)加載游戲中的Prefab和Sprite,并對(duì)Sprite進(jìn)行管理,在需要的時(shí)候進(jìn)行texture的清理。view模塊是游戲中各個(gè)具體功能的實(shí)現(xiàn)模塊,詳細(xì)介紹參見3.3節(jié)。
先解釋一下Prefab的加載原理。開發(fā)人員在層級(jí)管理器中里編輯好一個(gè)Prefab(預(yù)制件)后,將它拖動(dòng)到資源管理器,這時(shí)將會(huì)在PC的本地創(chuàng)建一個(gè)***.prefab和該預(yù)制件對(duì)應(yīng)的meta文件,在用編輯器打開***.prefab文件后,會(huì)發(fā)現(xiàn)它就是一個(gè)包含節(jié)點(diǎn)信息的json結(jié)構(gòu)。所以,加載預(yù)制件就是將本地的json結(jié)構(gòu)文件加載并且解析出來,然后形成了node的結(jié)構(gòu),再通過addChild將這個(gè)節(jié)點(diǎn)添加到自己想要的節(jié)點(diǎn)上,便完成了該節(jié)點(diǎn)的創(chuàng)建[12]。而meta文件中主要存儲(chǔ)該文件的uuid,在引用資源的時(shí)候,便通過這個(gè)uuid去查找對(duì)應(yīng)的文件,在內(nèi)存中該資源也是通過它的uuid作為Key存儲(chǔ)到相應(yīng)的map中[13]。
游戲客戶端實(shí)現(xiàn)用到了三個(gè)場(chǎng)景,開始場(chǎng)景、主場(chǎng)景、結(jié)束場(chǎng)景。各場(chǎng)景分別配有該場(chǎng)景的腳本。該種做法的目的是:能夠在新場(chǎng)景加載完畢后,優(yōu)先做一些處理。比如一些大型游戲項(xiàng)目,如果是新手進(jìn)入主場(chǎng)景,就要加載到該用戶當(dāng)前的引導(dǎo)界面,而老用戶則需要加載主場(chǎng)景的第一個(gè)界面。
(1)開始場(chǎng)景。
在StartScene.js這個(gè)腳本onLoad執(zhí)行的時(shí)候,先通過cc.loader.loadRes加載LoadView這個(gè)預(yù)制件,并在load成功后,將這個(gè)預(yù)制件存到ViewManger中。這樣的目的是在加載其他預(yù)制件時(shí),如果該預(yù)制件的節(jié)點(diǎn)過多,可能會(huì)很卡,也就是說消耗性能,這時(shí)再將這個(gè)預(yù)制件實(shí)例化出來,直接加載到場(chǎng)景上,相當(dāng)于一個(gè)過渡的作用。當(dāng)然開發(fā)人員也可以在這個(gè)預(yù)制件上,添加自定義的腳本,來做一些特殊處理。比如:如果想讓用戶在加載預(yù)制件的時(shí)候,不允許點(diǎn)擊屏幕上其他節(jié)點(diǎn)(防止造成誤操作),就可以在這個(gè)腳本里,給這個(gè)節(jié)點(diǎn)添加一個(gè)觸摸事件,并且阻止觸摸事件向下傳遞。存儲(chǔ)完之后,再加載StarView.prefab。
關(guān)鍵代碼如下:
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加載成功后,先添加一個(gè)監(jiān)聽。‘CLOSE_ABOUT_VIEW’,該監(jiān)聽的作用是在關(guān)閉“關(guān)于我們”界面后,將節(jié)點(diǎn)的透明度設(shè)置成最大值。接著調(diào)用toggleState這個(gè)方法,先初始化當(dāng)前的音樂與音效狀態(tài)。在該方法中使用cc.sys. localStorage存儲(chǔ)本地信息。像游戲中的音樂、音效的狀態(tài)通常是都需要保存的。但并不需要保存到服務(wù)端的數(shù)據(jù)庫(kù)中,這里用到了HTML5的本地存儲(chǔ)localStorage[7],如圖3所示。
圖3 localStorage的存儲(chǔ)實(shí)例
而native端則是分別調(diào)用ios和android設(shè)備原生讀寫文件的方法,將數(shù)據(jù)存儲(chǔ)到可讀寫區(qū)域。
關(guān)鍵代碼如下:
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;
}
}
界面上的聲音和音效的開關(guān)按鈕則是通過給該節(jié)點(diǎn)綁定一個(gè)Toggle組件,并添加checkEvent事件,在點(diǎn)擊該開關(guān)的時(shí)候再將對(duì)開關(guān)的狀態(tài)改變,并把開或關(guān)的狀態(tài)值再次存起來。
點(diǎn)擊按鈕后,先加載AboutView.prefab,加載成功后,在其對(duì)應(yīng)的腳本的onLoad里,去調(diào)用setContent方法。本游戲在該方法中將已存儲(chǔ)的開發(fā)者的個(gè)人信息的json文件讀取出來,并通過Label展示出來。
點(diǎn)擊開始游戲按鈕,則進(jìn)入主場(chǎng)景。
(2)主場(chǎng)景。
該場(chǎng)景中用到的組件類型的腳本有MainView.js、Center.js、Animal.js、PauseView.js,邏輯類型的腳本(類)有GameEngine.js、UnitController.js、Unit.js、Progress.js。
MainView的作用是用來管理整個(gè)場(chǎng)景上的UI顯示,GameEngine的作用是控制場(chǎng)景中的游戲邏輯,如:管理UnitController和Progress并完成向MainView發(fā)送消息。Center和UnitController是一一對(duì)應(yīng)關(guān)系,也就是UnitController是Center抽象出來的概念。Animal和Unit也是這樣的關(guān)系。UnitController用來管理它所屬的Unit,而Center對(duì)應(yīng)的節(jié)點(diǎn)又是Animal對(duì)應(yīng)節(jié)點(diǎn)的父節(jié)點(diǎn),這樣便使這四者的關(guān)系統(tǒng)一起來。Progress是用來計(jì)算游戲時(shí)間類,在游戲時(shí)間結(jié)束后,便通過GameEngine發(fā)送游戲結(jié)束的消息,在MainView接收到消息后,便跳轉(zhuǎn)到結(jié)束場(chǎng)景。
游戲主場(chǎng)景具體實(shí)現(xiàn)過程如下:
①在MainView.js的OnLoad執(zhí)行的時(shí)候,實(shí)例化一個(gè)GameEngine,并開始初始化GameEngine。也就是調(diào)用GameEngine的init方法(相當(dāng)于構(gòu)造方法),并在其中實(shí)例化Progress。在初始化結(jié)束后,便向MainView發(fā)消息,執(zhí)行MainView中的init方法,該方法的作用是播放Ready和Go這兩個(gè)動(dòng)畫。
②開始創(chuàng)建一組動(dòng)物。首先實(shí)例化一個(gè)UnitController,然后執(zhí)行它的init方法,然后創(chuàng)建若干個(gè)unit,并設(shè)置這些unit的信息,如:類型(狗熊、兔字、刺豚)、分值及時(shí)間獎(jiǎng)勵(lì)。創(chuàng)建完成后,引擎向view層發(fā)消息,view收到消息后,便通過對(duì)應(yīng)的prefab創(chuàng)建對(duì)應(yīng)的動(dòng)物節(jié)點(diǎn)(綁定Animal組件),并把這個(gè)節(jié)點(diǎn)添加到它們對(duì)應(yīng)的Center(該腳本中自定義了一個(gè)函數(shù),實(shí)現(xiàn)了加速度,使動(dòng)物上拋和下落更加真實(shí))上。這樣便實(shí)現(xiàn)了一組動(dòng)物中,能夠以相同的運(yùn)動(dòng)規(guī)律進(jìn)行運(yùn)動(dòng)。
③在玩家點(diǎn)擊動(dòng)物節(jié)點(diǎn)時(shí),該節(jié)點(diǎn)上的Animal.js中的點(diǎn)擊事件將會(huì)觸發(fā)。在點(diǎn)擊事件觸發(fā)后的回調(diào)方法中,將該Animal對(duì)應(yīng)的Unit的狀態(tài)進(jìn)行改變(包括計(jì)分、獎(jiǎng)懲游戲時(shí)間等等),并播放該節(jié)點(diǎn)相應(yīng)的動(dòng)畫。如果是狗熊或兔子,就從它們的父節(jié)點(diǎn)脫離出來,直接添加到MainView背景對(duì)應(yīng)的節(jié)點(diǎn)上,并開始向上飛,飛出屏幕后,移除該節(jié)點(diǎn)。而如果是刺豚的話,僅需要在執(zhí)行完點(diǎn)擊的動(dòng)畫后,移除即可。如果Center對(duì)應(yīng)的節(jié)點(diǎn),下落到屏幕外以后,再通過UnitController執(zhí)行GameEngine的創(chuàng)建動(dòng)物的方法,再創(chuàng)建新的一組。
④關(guān)于游戲的暫停方面,本游戲并沒有選擇使用cc.game.pause()這個(gè)引擎直接封裝好的用于暫停的方法。當(dāng)然相對(duì)于這個(gè)小項(xiàng)目,使用該方法就行。本游戲沒有使用它的原因是,當(dāng)使用這個(gè)方法的時(shí)候,整個(gè)項(xiàng)目中的游戲邏輯,渲染,事件處理,背景音樂和所有音效將會(huì)全部暫停,這樣導(dǎo)致的問題是:如果開發(fā)者想在暫停后彈出新的對(duì)話框,而該對(duì)話框中仍然需要有一些動(dòng)態(tài)的效果(如:使用ScrollView組件進(jìn)行滑動(dòng)等等操作)將不可能實(shí)現(xiàn)。所以這里通過在MainView定義一個(gè)字段isPause,進(jìn)而控制整個(gè)游戲過程中的動(dòng)態(tài)效果。這樣做也有一點(diǎn)小的缺陷,比如使用repeatForever的這些動(dòng)作,就必須使用AcitonMannager這個(gè)單例進(jìn)行單獨(dú)處理,將這個(gè)節(jié)點(diǎn)的動(dòng)作暫停,當(dāng)然在游戲繼續(xù)時(shí),還需要使用它將這個(gè)節(jié)點(diǎn)當(dāng)前已暫停的動(dòng)作繼續(xù)。
(3)結(jié)束場(chǎng)景。
在該場(chǎng)景中,主要是將本次游戲的分?jǐn)?shù)和歷史最高分?jǐn)?shù)通過Label顯示出來。其過程是客戶端計(jì)算本次的分?jǐn)?shù),在游戲結(jié)束時(shí),客戶端在將本次的分?jǐn)?shù)發(fā)給服務(wù)端后,服務(wù)端經(jīng)過分?jǐn)?shù)比較的運(yùn)算,將歷史最高分?jǐn)?shù)的數(shù)據(jù)返回給客戶端。
場(chǎng)景之間資源管理方面,資源的釋放是在切換場(chǎng)景的時(shí)候,在進(jìn)入下一個(gè)場(chǎng)景前,先釋放掉前場(chǎng)景所用到的資源,這對(duì)于一個(gè)小型的游戲項(xiàng)目來說就足夠了,不需要去花費(fèi)更多精力對(duì)游戲的資源進(jìn)行管理[14]。
躲避刺豚君游戲在Web和Native不同平臺(tái)進(jìn)行了調(diào)試。在Web平臺(tái),采用引擎的cc.log()的方法來輸出關(guān)鍵的變量并與使用Chrome進(jìn)行斷點(diǎn)調(diào)試的方法相結(jié)合進(jìn)行調(diào)試[13]。在Native(原生)平臺(tái),是在編譯項(xiàng)目時(shí)使用Debug模式,并配合程序的cc.log()方法來輸出關(guān)鍵的變量,從而將設(shè)備連接后,可以在Eclipse的控制臺(tái)顯示這些變量的值,進(jìn)而查找問題。
通過在兩個(gè)平臺(tái)的調(diào)試,發(fā)現(xiàn)兩個(gè)平臺(tái)其中的一些區(qū)別,具體如下:
首先,關(guān)于引擎的類中字段的權(quán)限問題。在web上,取到Sprite組件的spriteFrame對(duì)象之后,可以使用它的_name這個(gè)字段,而安卓調(diào)試時(shí)通過log輸出,發(fā)現(xiàn)該值為null,因此在使用該變量時(shí)就會(huì)報(bào)錯(cuò)。因?yàn)镃++的引擎中這個(gè)變量使用了private設(shè)置了它的訪問權(quán)限。因此需要謹(jǐn)慎地使用引擎內(nèi)部的帶有下劃線的私有變量。
其次,一個(gè)比較明顯的區(qū)別是在if的判斷上。在玩家首次打開游戲時(shí),因?yàn)橐O(shè)置音樂和音效開關(guān)的初始狀態(tài),所以就需要使用cc.sys.localStorage.getItem(arg)這個(gè)方法進(jìn)行判斷。通過log的輸出,發(fā)現(xiàn)如果沒有返回值時(shí),web端返回的是undefine而在安卓上返回值為null,因此需要區(qū)分開來進(jìn)行判斷。
調(diào)試經(jīng)驗(yàn)分享:
①在進(jìn)行Android調(diào)試的時(shí)候,最好的辦法是看C++源碼,緊接著和HTML5的源碼進(jìn)行對(duì)比,這樣才能更容易找到在web端沒有問題,但在native上卻出現(xiàn)問題的原因。
②在一個(gè)比較大的游戲開發(fā)過程中,開發(fā)人員應(yīng)該要時(shí)常去打包,檢查native上新開發(fā)的模塊有沒有問題,否則集中解決的時(shí)候就會(huì)消耗太多的精力。
從新工科背景下軟件工程專業(yè)實(shí)踐課程應(yīng)用典型案例庫(kù)的構(gòu)建角度出發(fā)[15],以應(yīng)用為導(dǎo)向,利用Cocos2d-x游戲引擎的最新開發(fā)工具CocosCreator,從游戲開發(fā)工具選用、開發(fā)環(huán)境、游戲功能、玩法介紹等入手,詳細(xì)介紹了實(shí)現(xiàn)跨平臺(tái)游戲—躲避刺豚君的設(shè)計(jì)和實(shí)現(xiàn)過程,經(jīng)測(cè)試,游戲跨平臺(tái)運(yùn)行通暢,功能完備。該游戲的開發(fā)對(duì)基于Cocos2d-x引擎的CocosCreator跨平臺(tái)游戲開發(fā)的設(shè)計(jì)和實(shí)現(xiàn)具有一定的參考價(jià)值。
計(jì)算機(jī)技術(shù)與發(fā)展2021年2期