本文我们将逐步分享基于 JAVA UI 开发的“推箱子”小游戏这个项目的构建流程。
实际上,笔者在进行开发的过程中,并不是写完一个界面的内部逻辑,就开始对界面进行美化,而是先让所有的东西可以正常地跑起来,再谈美化。
因此本系列文章前半部分会重点讨论游戏以及界面之间的核心逻辑,后半部分则会分享美化界面的部分。
项目创建
打开 DevEco Studio,创建一个新项目,选择 JAVA 作为开发语言,将项目保存至合适的位置。
根据上期分享的开发思路,先完成 UI 交互部分的框架。
可以看到,这里需要新建三个 Slice(原本自带一个 MainAbilitySlice):
下面对四个 UI 交互功能进行讲解。 MainAbilitySlice:打开应用时,首先显示的界面,也就是用户主界面。
SelectSlice:关卡选择界面,用户可以在这个界面选择将要跳转的关卡。
InitSlice:加载界面。当用户选择关卡之后,会进入加载界面,仿照游戏加载资源。(实际上啥都没干)
GameSlice:最后一个界面,也就是这个游戏的核心界面,所有的游戏逻辑都将在这个页面中进行,因此它将是本篇文章的核心讲解部分。
至此,我们可以简单地梳理一下四个界面以及他们包含的组件之间的关系:用户进入 MainAbilitySlice 之后,通过“开始游戏”按键进入 SelectSlice。
在 SelectSlice 中有三个按键,会对应跳转到三个不同的关卡,但是进入关卡之前会先进入 InitSlice,加载过后在进入最后的 GameSlice,从而开始游戏。以上就是开发的时候要理清楚的页面跳转关系。
核心代码分析
①MainAbilitySlice
里面有四个按钮,那可以简单的给他们分个类,例如:开始游戏的按钮要实现的功能是页面跳转,直接与其他界面关联,分为一类。
历史记录与关于游戏可以用弹出窗口来实现,不需要额外界面,归为一类;退出游戏按钮直接结束应用进程,也是单独一类。
明确之后,就可以给各个按钮添加点击事件了:
//开始游戏按钮 startBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //页面跳转 present(newSelectSlice(),newIntent()); } }); //历史记录按钮 recordBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //历史记录弹窗 } }); //关于游戏按钮 aboutBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ //关于游戏弹窗 } }); //退出游戏按钮 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(); } });在开发中,由于历史记录跟关于游戏这两个功能并不是核心,因此,最开始也只是做个壳子放在这,以便自己能专注于游戏主逻辑的开发,这也是我想分享的一种思路:先搭壳子再填东西。 因此,阅读本系列时,如果碰到代码中只有注释,没有实现内容时,那是因为当时做到这一步的时候,并不会去关注具体如何实现,只会想个大概,先放着。 到这里之后,实际上已经完成了游戏的退出以及从 MainAbilitySlice 页面到 SelectSlice 页面的导航,便可进行到我们的下一步。
②SelectSlice
这一个界面主要有三个按钮,如何实现按下不同的按钮,跳转到同一个加载界面,但是加载完后又跳转到不同的游戏界面?
这里使用 Intent 对跳转时的数据进行打包传输,具体实现如下:
firstBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("关卡",1); present(newInitSlice(),i); } }); secondBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("关卡",2); present(newInitSlice(),i); } }); thirdBtn.setClickedListener(newComponent.ClickedListener(){ @Override publicvoidonClick(Componentcomponent){ Intenti=newIntent(); i.setParam("关卡",3); present(newInitSlice(),i); } });这里 Intent 的 key 都是“关卡”,但是有不同的 value,对应不同的关卡。实际上,到这里 SelectSlice 已经完成了它的功能了。接下来进入 InitSlice。 ③InitSlice 在加载界面中,我预想的是一个动态的画面,然后加上一个进度条,因此我可能需要用到能够播放 gif 的组件,以及进度条组件。
那要怎么实现加载的时候进度条跟进?我的实现方式是使用两个定时器(其实用一个也完全能搞定)
//onStart外定义 Timert1=newTimer(); Timert2=newTimer(); //onStart内 TimerTasktask1=newTimerTask(){ @Override publicvoidrun(){ //页面跳转 present(newGameSlice(),intent); } }; TimerTasktask2=newTimerTask(){ @Override publicvoidrun(){ //进度条更新 intvalue=progressBar.getProgress(); progressBar.setProgressValue(value+20); } }; t1.schedule(task1,5000); t2.schedule(task2,0,1000);关于如何播放 gif,本不应该在此讲解,因为与主线任务无关,但是这里用到的第三方组件,后面游戏界面频繁使用,因此在这进行介绍。
这里用到了第三方组件 Glide,关于组件如何使用,可具体看这篇文章,只需几行代码即可完成 gif 的播放,十分方便。
https://ost.51cto.com/posts/8635
intimageResourceId=ResourceTable.Media_gifimg; Glide.with(this) .asGif() .load(imageResourceId) .into(draweeView);至此,从 InitSlice 跳转到 GameSlice 的逻辑也写好了,并且携带着从 SelectSlice 打包过来的数据,接下来重点讲解游戏界面的实现。 ④GameSlice 制作一个可以玩的游戏界面最首要的任务就是绘制地图,因此定义了一个 JAVA 类 GameMap 用于地图的绘制。 在说明如何实现 GameMap 之前,我想先简单阐述一下推箱子游戏的实现逻辑:实际上每一次操作的都是图片,逻辑判断依赖的是图片所绑定的属性值(只涉及加减运算)。 举个例子:我在程序中将路设置为 0,将墙体设置为 1,将宝可梦设置为 2 和 3,将空球设置为 4,那收服之后的球应该设置为:2+4=6、3+4=7,这样就实现了你把空球推向宝可梦时,显示的是已经收复的球的状态。 而如果将人设置为 8,那 8+2=10、8+3=11 也必须是人,这样才能实现你移动到宝可梦上面时,是以原人物的方式呈现。 这是在设置属性值需要注意的,其他方面,例如怎么判断墙体之类的,只需要 if 语句判断即可。 还有一点,如果我们要实现回退功能,就需要用到栈的一些相关操作。
核心代码如下:
//GameMap继承于PositionLayout布局,方便对图片进行渲染 publicclassGameMapextendsPositionLayout{ privatefinalstaticintsize=110; //用二维数组来存储地图 privateInteger[][]gameMap; //定义x,y坐标 privatePair至此,完成了地图类的代码,可以开始绘制 GameSlice 了。在预览图中看到,核心部分有很多:一个退出界面按钮,一个设置按钮,一个倒计时器,还有一张地图,一个后退地图操作的按钮,按照我自己的想法先进行分类。map_position; //标识是否绘制过地图(画过一次后,后面所有的操作都只能是进行刷新,防止重复生成对象) privateBooleanisDrew=Boolean.FALSE; //定义移动方式枚举,方便外部调用进行选择 publicenumMOVE_WAY{ MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT } //设置每种物体的属性值 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; //定义存储地图用的栈 privateStack stack; //可使用此构造函数绘制不同大小的地图,这个是预留的接口,项目中使用的是直接在xml文件中加入这个组件(因为继承了PositionLayout所以可以在xml文件中使用),当然也可以直接用构造函数创建,留给读者自己发挥。 publicGameMap(Contextcontext,Integer[][]map,intx,inty){ super(context); gameMap=map; map_position=newPair<>(x,y); } //外部设置地图接口 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;i< map_position.f; i++) { for (int j = 0; j < map_position.s; j++) { DraweeView draweeView = new DraweeView(getContext()); draweeView.setComponentSize(size,size); draweeView.setContentPosition(size * j, size * i); int index = gameMap[i][j]; switch (index) { case ROAD: Glide.with(getContext()) .load(ResourceTable.Media_road) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case WALL: Glide.with(getContext()) .load(ResourceTable.Media_wall) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case LABA: Glide.with(getContext()) .load(ResourceTable.Media_laba) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case YIBU: Glide.with(getContext()) .load(ResourceTable.Media_yibu) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BOBO: Glide.with(getContext()) .load(ResourceTable.Media_bobo) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case MINI: Glide.with(getContext()) .load(ResourceTable.Media_mini) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case MIAO: Glide.with(getContext()) .load(ResourceTable.Media_miao) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BALL_EMPTY: Glide.with(getContext()) .load(ResourceTable.Media_ballEmpty) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BALL_FULL1: case BALL_FULL2: case BALL_FULL3: case BALL_FULL4: case BALL_FULL5: Glide.with(getContext()) .load(ResourceTable.Media_ballFull) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case PEOPLE1: case PEOPLE2: case PEOPLE3: case PEOPLE4: case PEOPLE5: case PEOPLE6: Glide.with(getContext()) .load(ResourceTable.Media_people) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; } this.addComponent(draweeView); } } isDrew = Boolean.TRUE; } //外部刷新地图接口 public void flushMap(){ for (int i = 0; i < map_position.f; i++) { for (int j = 0; j < map_position.s; j++) { DraweeView draweeView = (DraweeView) getComponentAt(i * map_position.s + j); draweeView.setComponentSize(size,size); draweeView.setContentPosition(size * j, size * i); int index = gameMap[i][j]; switch (index) { case ROAD: Glide.with(getContext()) .load(ResourceTable.Media_road) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case WALL: Glide.with(getContext()) .load(ResourceTable.Media_wall) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case LABA: Glide.with(getContext()) .load(ResourceTable.Media_laba) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case YIBU: Glide.with(getContext()) .load(ResourceTable.Media_yibu) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BOBO: Glide.with(getContext()) .load(ResourceTable.Media_bobo) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case MINI: Glide.with(getContext()) .load(ResourceTable.Media_mini) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case MIAO: Glide.with(getContext()) .load(ResourceTable.Media_miao) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BALL_EMPTY: Glide.with(getContext()) .load(ResourceTable.Media_ballEmpty) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case BALL_FULL1: case BALL_FULL2: case BALL_FULL3: case BALL_FULL4: case BALL_FULL5: Glide.with(getContext()) .load(ResourceTable.Media_ballFull) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; case PEOPLE1: case PEOPLE2: case PEOPLE3: case PEOPLE4: case PEOPLE5: case PEOPLE6: Glide.with(getContext()) .load(ResourceTable.Media_people) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(draweeView); break; } } } } //获取当前操作的人物坐标 public Pair getMyPosition(){ for(inti=0;i< map_position.f; i++) { for (int j = 0; j < map_position.s; j++) { if (gameMap[i][j] == PEOPLE1 || gameMap[i][j] == PEOPLE2 || gameMap[i][j] == PEOPLE3 || gameMap[i][j] == PEOPLE4 || gameMap[i][j] == PEOPLE5 || gameMap[i][j] == PEOPLE6) { return new Pair<>(i,j); } } } returnnewPair<>(-1,-1); } //给地图里任意一张图设置对应的值,移动的时候需要此接口 protectedvoidsetValue(intx,inty,intvalue){ gameMap[x][y]+=value; } //判断是否能够移动 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; } //外部接口,每一次移动完判断游戏是否结束 publicBooleanisWin(){ for(inti=0;i< map_position.f; i++) { for (int j = 0; j < map_position.s; j++) { if (gameMap[i][j] == LABA || gameMap[i][j] == YIBU || gameMap[i][j] == BOBO || gameMap[i][j] == MINI || gameMap[i][j] == MIAO) return Boolean.FALSE; } } return Boolean.TRUE; } //外部移动接口 public void move(@NotNull MOVE_WAY move_way){ Pair position=getMyPosition(); Integer[][]oldMap=newInteger[map_position.f][map_position.s]; for(inti=0;i< map_position.f; i++) { if (map_position.s >=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< map_position.f; i++) { if (map_position.s >=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); } }
后退地图操作的按钮和地图是刚需,优先实现,代码如下:
//在外部定义变量 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方法内 switch(intent.getIntParam("关卡",0)) { case1: map=map1; break; case2: map=map2; break; case3: map=map3; break; } gameMap.setMap(map); if(!gameMap.getIsDrew())gameMap.drawMap(); elsegameMap.flushMap(); //滑动屏幕移动角色 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_x && Math.abs(endPoint.getY() - start_y) < 100){//left gameMap.move(GameMap.MOVE_WAY.MOVE_LEFT); } else if(endPoint.getY() < start_y && Math.abs(endPoint.getX() - start_x) < 100){//up gameMap.move(GameMap.MOVE_WAY.MOVE_UP); } else if(endPoint.getY() >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(); } });游戏整体框架(不包括数据存储)大概就是这些,至此,已经实现了这个游戏最基础的功能了,从开始界面到游戏界面,以及各种游戏操作,接下来的事情就是对这些零零散散的组件。
用一个漂亮的布局整合起来,再根据自己的兴趣添加一些其他功能,下篇将重点给出美化 UI 交互界面的代码,敬请期待!
审核编辑:汤梓红
-
JAVA
+关注
关注
19文章
2966浏览量
104701 -
游戏
+关注
关注
2文章
742浏览量
26312 -
鸿蒙
+关注
关注
57文章
2339浏览量
42805
原文标题:鸿蒙上开发“推箱子”小游戏
文章出处:【微信号:gh_834c4b3d87fe,微信公众号:OpenHarmony技术社区】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论