本文我們將逐步分享基于 JAVA UI 開發(fā)的“推箱子”小游戲這個(gè)項(xiàng)目的構(gòu)建流程。
實(shí)際上,筆者在進(jìn)行開發(fā)的過程中,并不是寫完一個(gè)界面的內(nèi)部邏輯,就開始對(duì)界面進(jìn)行美化,而是先讓所有的東西可以正常地跑起來,再談美化。
因此本系列文章前半部分會(huì)重點(diǎn)討論游戲以及界面之間的核心邏輯,后半部分則會(huì)分享美化界面的部分。
項(xiàng)目創(chuàng)建
打開 DevEco Studio,創(chuàng)建一個(gè)新項(xiàng)目,選擇 JAVA 作為開發(fā)語言,將項(xiàng)目保存至合適的位置。
根據(jù)上期分享的開發(fā)思路,先完成 UI 交互部分的框架。
可以看到,這里需要新建三個(gè) Slice(原本自帶一個(gè) MainAbilitySlice):
下面對(duì)四個(gè) UI 交互功能進(jìn)行講解。 MainAbilitySlice:打開應(yīng)用時(shí),首先顯示的界面,也就是用戶主界面。
SelectSlice:關(guān)卡選擇界面,用戶可以在這個(gè)界面選擇將要跳轉(zhuǎn)的關(guān)卡。
InitSlice:加載界面。當(dāng)用戶選擇關(guān)卡之后,會(huì)進(jìn)入加載界面,仿照游戲加載資源。(實(shí)際上啥都沒干)
GameSlice:最后一個(gè)界面,也就是這個(gè)游戲的核心界面,所有的游戲邏輯都將在這個(gè)頁面中進(jìn)行,因此它將是本篇文章的核心講解部分。
至此,我們可以簡(jiǎn)單地梳理一下四個(gè)界面以及他們包含的組件之間的關(guān)系:用戶進(jìn)入 MainAbilitySlice 之后,通過“開始游戲”按鍵進(jìn)入 SelectSlice。
在 SelectSlice 中有三個(gè)按鍵,會(huì)對(duì)應(yīng)跳轉(zhuǎn)到三個(gè)不同的關(guān)卡,但是進(jìn)入關(guān)卡之前會(huì)先進(jìn)入 InitSlice,加載過后在進(jìn)入最后的 GameSlice,從而開始游戲。以上就是開發(fā)的時(shí)候要理清楚的頁面跳轉(zhuǎn)關(guān)系。
核心代碼分析
①M(fèi)ainAbilitySlice
里面有四個(gè)按鈕,那可以簡(jiǎn)單的給他們分個(gè)類,例如:開始游戲的按鈕要實(shí)現(xiàn)的功能是頁面跳轉(zhuǎn),直接與其他界面關(guān)聯(lián),分為一類。
歷史記錄與關(guān)于游戲可以用彈出窗口來實(shí)現(xiàn),不需要額外界面,歸為一類;退出游戲按鈕直接結(jié)束應(yīng)用進(jìn)程,也是單獨(dú)一類。
明確之后,就可以給各個(gè)按鈕添加點(diǎn)擊事件了:
//開始游戲按鈕 startBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //頁面跳轉(zhuǎn) present(newSelectSlice(),newIntent()); } }); //歷史記錄按鈕 recordBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //歷史記錄彈窗 } }); //關(guān)于游戲按鈕 aboutBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //關(guān)于游戲彈窗 } }); //退出游戲按鈕 exitBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //退出游戲提示 CommonDialogcommonDialog=newCommonDialog(getContext()); commonDialog.setTitleText("提示"); commonDialog.setContentText("是否退出游戲"); commonDialog.setButton(1,"確定",newIDialog.ClickedListener(){ @Override publicvoidonClick(IDialogiDialog,inti){ terminateAbility(); } }); commonDialog.setButton(2,"取消",newIDialog.ClickedListener(){ @Override publicvoidonClick(IDialogiDialog,inti){ commonDialog.destroy(); } }); commonDialog.show(); } });在開發(fā)中,由于歷史記錄跟關(guān)于游戲這兩個(gè)功能并不是核心,因此,最開始也只是做個(gè)殼子放在這,以便自己能專注于游戲主邏輯的開發(fā),這也是我想分享的一種思路:先搭殼子再填東西。 因此,閱讀本系列時(shí),如果碰到代碼中只有注釋,沒有實(shí)現(xiàn)內(nèi)容時(shí),那是因?yàn)楫?dāng)時(shí)做到這一步的時(shí)候,并不會(huì)去關(guān)注具體如何實(shí)現(xiàn),只會(huì)想個(gè)大概,先放著。 到這里之后,實(shí)際上已經(jīng)完成了游戲的退出以及從 MainAbilitySlice 頁面到 SelectSlice 頁面的導(dǎo)航,便可進(jìn)行到我們的下一步。
②SelectSlice
這一個(gè)界面主要有三個(gè)按鈕,如何實(shí)現(xiàn)按下不同的按鈕,跳轉(zhuǎn)到同一個(gè)加載界面,但是加載完后又跳轉(zhuǎn)到不同的游戲界面?
這里使用 Intent 對(duì)跳轉(zhuǎn)時(shí)的數(shù)據(jù)進(jìn)行打包傳輸,具體實(shí)現(xiàn)如下:
firstBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("關(guān)卡",1); present(newInitSlice(),i); } }); secondBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("關(guān)卡",2); present(newInitSlice(),i); } }); thirdBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("關(guān)卡",3); present(newInitSlice(),i); } });這里 Intent 的 key 都是“關(guān)卡”,但是有不同的 value,對(duì)應(yīng)不同的關(guān)卡。實(shí)際上,到這里 SelectSlice 已經(jīng)完成了它的功能了。接下來進(jìn)入 InitSlice。 ③InitSlice 在加載界面中,我預(yù)想的是一個(gè)動(dòng)態(tài)的畫面,然后加上一個(gè)進(jìn)度條,因此我可能需要用到能夠播放 gif 的組件,以及進(jìn)度條組件。
那要怎么實(shí)現(xiàn)加載的時(shí)候進(jìn)度條跟進(jìn)?我的實(shí)現(xiàn)方式是使用兩個(gè)定時(shí)器(其實(shí)用一個(gè)也完全能搞定)
//onStart外定義 Timert1=newTimer(); Timert2=newTimer(); //onStart內(nèi) TimerTasktask1=newTimerTask(){ @Override publicvoidrun(){ //頁面跳轉(zhuǎn) present(newGameSlice(),intent); } }; TimerTasktask2=newTimerTask(){ @Override publicvoidrun(){ //進(jìn)度條更新 intvalue=progressBar.getProgress(); progressBar.setProgressValue(value+20); } }; t1.schedule(task1,5000); t2.schedule(task2,0,1000);關(guān)于如何播放 gif,本不應(yīng)該在此講解,因?yàn)榕c主線任務(wù)無關(guān),但是這里用到的第三方組件,后面游戲界面頻繁使用,因此在這進(jìn)行介紹。
這里用到了第三方組件 Glide,關(guān)于組件如何使用,可具體看這篇文章,只需幾行代碼即可完成 gif 的播放,十分方便。
https://ost.51cto.com/posts/8635
intimageResourceId=ResourceTable.Media_gifimg; Glide.with(this) .asGif() .load(imageResourceId) .into(draweeView);至此,從 InitSlice 跳轉(zhuǎn)到 GameSlice 的邏輯也寫好了,并且攜帶著從 SelectSlice 打包過來的數(shù)據(jù),接下來重點(diǎn)講解游戲界面的實(shí)現(xiàn)。 ④GameSlice 制作一個(gè)可以玩的游戲界面最首要的任務(wù)就是繪制地圖,因此定義了一個(gè) JAVA 類 GameMap 用于地圖的繪制。 在說明如何實(shí)現(xiàn) GameMap 之前,我想先簡(jiǎn)單闡述一下推箱子游戲的實(shí)現(xiàn)邏輯:實(shí)際上每一次操作的都是圖片,邏輯判斷依賴的是圖片所綁定的屬性值(只涉及加減運(yùn)算)。 舉個(gè)例子:我在程序中將路設(shè)置為 0,將墻體設(shè)置為 1,將寶可夢(mèng)設(shè)置為 2 和 3,將空球設(shè)置為 4,那收服之后的球應(yīng)該設(shè)置為:2+4=6、3+4=7,這樣就實(shí)現(xiàn)了你把空球推向?qū)毧蓧?mèng)時(shí),顯示的是已經(jīng)收復(fù)的球的狀態(tài)。 而如果將人設(shè)置為 8,那 8+2=10、8+3=11 也必須是人,這樣才能實(shí)現(xiàn)你移動(dòng)到寶可夢(mèng)上面時(shí),是以原人物的方式呈現(xiàn)。 這是在設(shè)置屬性值需要注意的,其他方面,例如怎么判斷墻體之類的,只需要 if 語句判斷即可。 還有一點(diǎn),如果我們要實(shí)現(xiàn)回退功能,就需要用到棧的一些相關(guān)操作。
核心代碼如下:
//GameMap繼承于PositionLayout布局,方便對(duì)圖片進(jìn)行渲染 publicclassGameMapextendsPositionLayout{ privatefinalstaticintsize=110; //用二維數(shù)組來存儲(chǔ)地圖 privateInteger[][]gameMap; //定義x,y坐標(biāo) privatePair至此,完成了地圖類的代碼,可以開始繪制 GameSlice 了。在預(yù)覽圖中看到,核心部分有很多:一個(gè)退出界面按鈕,一個(gè)設(shè)置按鈕,一個(gè)倒計(jì)時(shí)器,還有一張地圖,一個(gè)后退地圖操作的按鈕,按照我自己的想法先進(jìn)行分類。map_position; //標(biāo)識(shí)是否繪制過地圖(畫過一次后,后面所有的操作都只能是進(jìn)行刷新,防止重復(fù)生成對(duì)象) privateBooleanisDrew=Boolean.FALSE; //定義移動(dòng)方式枚舉,方便外部調(diào)用進(jìn)行選擇 publicenumMOVE_WAY{ MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT } //設(shè)置每種物體的屬性值 privatefinalstaticintROAD=0; privatefinalstaticintWALL=1; privatefinalstaticintLABA=2; privatefinalstaticintYIBU=3; privatefinalstaticintBOBO=4; privatefinalstaticintMINI=5; privatefinalstaticintMIAO=6; privatefinalstaticintBALL_EMPTY=7; privatefinalstaticintBALL_FULL1=9; privatefinalstaticintBALL_FULL2=10; privatefinalstaticintBALL_FULL3=11; privatefinalstaticintBALL_FULL4=12; privatefinalstaticintBALL_FULL5=13; privatefinalstaticintPEOPLE1=14; privatefinalstaticintPEOPLE2=16; privatefinalstaticintPEOPLE3=17; privatefinalstaticintPEOPLE4=18; privatefinalstaticintPEOPLE5=19; privatefinalstaticintPEOPLE6=20; //定義存儲(chǔ)地圖用的棧 privateStack stack; //可使用此構(gòu)造函數(shù)繪制不同大小的地圖,這個(gè)是預(yù)留的接口,項(xiàng)目中使用的是直接在xml文件中加入這個(gè)組件(因?yàn)槔^承了PositionLayout所以可以在xml文件中使用),當(dāng)然也可以直接用構(gòu)造函數(shù)創(chuàng)建,留給讀者自己發(fā)揮。 publicGameMap(Contextcontext,Integer[][]map,intx,inty){ super(context); gameMap=map; map_position=newPair<>(x,y); } //外部設(shè)置地圖接口 publicvoidsetMap(Integer[][]map){ gameMap=map; map_position=newPair<>(map.length,map[0].length); stack=newStack<>(); } //繪制地圖接口 publicvoiddrawMap(){ setHeight(size*map_position.s); setWidth(size*map_position.f); for(inti=0;igetMyPosition(){ for(inti=0;i(i,j); } } } returnnewPair<>(-1,-1); } //給地圖里任意一張圖設(shè)置對(duì)應(yīng)的值,移動(dòng)的時(shí)候需要此接口 protectedvoidsetValue(intx,inty,intvalue){ gameMap[x][y]+=value; } //判斷是否能夠移動(dòng) protectedBooleanisMove(inti,intj){ if(gameMap[i][j]==ROAD||gameMap[i][j]==LABA|| gameMap[i][j]==YIBU||gameMap[i][j]==BOBO|| gameMap[i][j]==MINI||gameMap[i][j]==MIAO) returnBoolean.TRUE; returnBoolean.FALSE; } //判斷是不是球 protectedBooleanisBall(inti,intj){ if(gameMap[i][j]==BALL_EMPTY||gameMap[i][j]==BALL_FULL1|| gameMap[i][j]==BALL_FULL2||gameMap[i][j]==BALL_FULL3|| gameMap[i][j]==BALL_FULL4||gameMap[i][j]==BALL_FULL5) { returntrue; } returnBoolean.FALSE; } //外部接口,每一次移動(dòng)完判斷游戲是否結(jié)束 publicBooleanisWin(){ for(inti=0;iposition=getMyPosition(); Integer[][]oldMap=newInteger[map_position.f][map_position.s]; for(inti=0;i=0)System.arraycopy(gameMap[i],0,oldMap[i],0,map_position.s); } stack.push(oldMap); switch(move_way){ caseMOVE_UP: if(this.isMove(position.f-1,position.s)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f-1,position.s,PEOPLE1); } if(this.isBall(position.f-1,position.s)) { if(this.isMove(position.f-2,position.s)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f-1,position.s,BALL_EMPTY); this.setValue(position.f-2,position.s,BALL_EMPTY); } } break; caseMOVE_DOWN: if(this.isMove(position.f+1,position.s)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f+1,position.s,PEOPLE1); } if(this.isBall(position.f+1,position.s)) { if(this.isMove(position.f+2,position.s)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f+1,position.s,BALL_EMPTY); this.setValue(position.f+2,position.s,BALL_EMPTY); } } break; caseMOVE_LEFT: if(this.isMove(position.f,position.s-1)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f,position.s-1,PEOPLE1); } if(this.isBall(position.f,position.s-1)) { if(this.isMove(position.f,position.s-2)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f,position.s-1,BALL_EMPTY); this.setValue(position.f,position.s-2,BALL_EMPTY); } } break; caseMOVE_RIGHT: if(this.isMove(position.f,position.s+1)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f,position.s+1,PEOPLE1); } if(this.isBall(position.f,position.s+1)) { if(this.isMove(position.f,position.s+2)) { this.setValue(position.f,position.s,-PEOPLE1); this.setValue(position.f,position.s+1,BALL_EMPTY); this.setValue(position.f,position.s+2,BALL_EMPTY); } } break; } flushMap(); } //外部回退原先地圖接口 publicvoidback(){ if(stack.empty())return; Integer[][]temp=stack.peek(); for(inti=0;i=0)System.arraycopy(temp[i],0,gameMap[i],0,map_position.s); } flushMap(); stack.pop(); } publicBooleangetIsDrew(){ returnisDrew; } publicGameMap(Contextcontext){ super(context); } publicGameMap(Contextcontext,AttrSetattrSet){ super(context,attrSet); } publicGameMap(Contextcontext,AttrSetattrSet,StringstyleName){ super(context,attrSet,styleName); } }
后退地圖操作的按鈕和地圖是剛需,優(yōu)先實(shí)現(xiàn),代碼如下:
//在外部定義變量 floatstart_x; floatstart_y; Integer[][]map; Integer[][]map1={ {1,1,1,1,1,1,1,1,1}, {1,0,0,0,1,0,0,1,1}, {1,0,1,0,1,7,2,1,1}, {1,0,0,0,0,7,3,1,1}, {1,0,1,0,1,7,4,1,1}, {1,0,0,0,1,0,0,1,1}, {1,1,1,1,1,0,14,1,1}, {1,1,1,1,1,1,1,1,1}, {1,1,1,1,1,1,1,1,1} }; Integer[][]map2={ {1,1,1,1,1,1,1,1,1}, {1,1,1,1,1,1,1,1,1}, {1,1,0,0,0,3,0,1,1}, {1,1,0,1,0,1,0,1,1}, {1,1,0,7,14,7,0,1,1}, {1,1,0,1,0,1,5,1,1}, {1,1,0,7,6,0,0,1,1}, {1,1,1,1,1,1,1,1,1}, {1,1,1,1,1,1,1,1,1} }; Integer[][]map3={ {1,1,1,1,1,1,1,1,1}, {1,1,1,1,1,1,1,1,1}, {1,0,2,0,7,0,1,1,1}, {1,1,0,7,6,7,0,1,1}, {1,3,4,5,1,0,5,1,1}, {1,0,1,7,0,7,0,1,1}, {1,0,7,0,1,2,7,1,1}, {1,0,14,0,0,0,0,1,1}, {1,1,1,1,1,1,1,1,1} }; //onStart方法內(nèi) switch(intent.getIntParam("關(guān)卡",0)) { case1: map=map1; break; case2: map=map2; break; case3: map=map3; break; } gameMap.setMap(map); if(!gameMap.getIsDrew())gameMap.drawMap(); elsegameMap.flushMap(); //滑動(dòng)屏幕移動(dòng)角色 gameMap.setTouchEventListener(newComponent.TouchEventListener(){ @Override publicbooleanonTouchEvent(Componentcomponent,TouchEventtouchEvent){ intaction=touchEvent.getAction(); switch(action){ caseTouchEvent.PRIMARY_POINT_DOWN: MmiPointstartPoint=touchEvent.getPointerPosition(0); start_x=startPoint.getX(); start_y=startPoint.getY(); break; caseTouchEvent.PRIMARY_POINT_UP: MmiPointendPoint=touchEvent.getPointerPosition(0); if(endPoint.getX()>start_x&&Math.abs(endPoint.getY()-start_y)100){//right ????????????????????????????gameMap.move(GameMap.MOVE_WAY.MOVE_RIGHT); ????????????????????????} ????????????????????????else?if(endPoint.getX()?start_y&&Math.abs(endPoint.getX()-start_x)100){//down ????????????????????????????gameMap.move(GameMap.MOVE_WAY.MOVE_DOWN); ????????????????????????} ????????????????????????if(gameMap.isWin()){ ????????????????????????????//贏了之后該干嘛 ????????????????????????} ????????????????????????break; ????????????????} ????????????????return?true; ????????????} ????????}); ????????stackBtn.setClickedListener(new?Component.ClickedListener()?{ ????????????@Override ????????????public?void?onClick(Component?component)?{ ????????????????gameMap.back(); ????????????} ????????});游戲整體框架(不包括數(shù)據(jù)存儲(chǔ))大概就是這些,至此,已經(jīng)實(shí)現(xiàn)了這個(gè)游戲最基礎(chǔ)的功能了,從開始界面到游戲界面,以及各種游戲操作,接下來的事情就是對(duì)這些零零散散的組件。
用一個(gè)漂亮的布局整合起來,再根據(jù)自己的興趣添加一些其他功能,下篇將重點(diǎn)給出美化 UI 交互界面的代碼,敬請(qǐng)期待!
審核編輯:湯梓紅
-
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104702 -
游戲
+關(guān)注
關(guān)注
2文章
742瀏覽量
26312 -
鴻蒙
+關(guān)注
關(guān)注
57文章
2339瀏覽量
42805
原文標(biāo)題:鴻蒙上開發(fā)“推箱子”小游戲
文章出處:【微信號(hào):gh_834c4b3d87fe,微信公眾號(hào):OpenHarmony技術(shù)社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論