Android游戏开发之"抓住疯狂熊"(一)
??????? 刚接触android不久,自己根据网上的教程模仿了一个2014年的热门游戏“围住神经猫”,游戏方法非常简单,大家都玩过应该知道,只要将人物围住就可以获胜,若人物跑到地图边缘,则判定失败。
??????? 先上游戏界面:
?
????代码中有三个类:MainActivity、Playground、Dot
?
????Dot是格子的对象类,地图是由格子组成的,里面是基本的set、get方法。附代码:
public class Dot { int x,y; int status; public static final int STATUS_ON = 1;//设置障碍 public static final int STATUS_OFF = 0;//设置为空 public static final int STATUS_IN = 9;//神经猫的位置 public Dot(int x, int y) { super(); this.x = x; this.y = y; status = STATUS_OFF; } public int getX() { return x; } public int getY() { return y; } public int getStatus() { return status; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void setStatus(int status) { this.status = status; } public void setXY(int x,int y){ this.x = x; this.y = y; } }
?
?????? 这个游戏的主要实现方法在PlayGround类中,这个类继承了SurfaceView类。
?????? 下面我们先来了解一下surfaceView这个类:
????在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面。由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。
?????? 普通的Android控件,例如TextView、Button和CheckBox等,它们都是将自己的UI绘制在宿主窗口的绘图表面之上,这意味着它们的UI是在应用程序的主线程中进行绘制的。由于应用程序的主线程除了要绘制UI之外,还需要及时地响应用户输入,否则的话,系统就会认为应用程序没有响应了,因此就会弹出一个ANR对话框出来。对于一些游戏画面,或者摄像头预览、视频播放来说,它们的UI都比较复杂,而且要求能够进行高效的绘制,因此,它们的UI就不适合在应用程序的主线程中进行绘制。这时候就必须要给那些需要复杂而高效UI的视图生成一个独立的绘图表面,以及使用一个独立的线程来绘制这些视图的UI。
????SurfaceView及其宿主Activity窗口的绘图表面示意图
?
????? ?surfaceView是在一个新起的单独线程中可以重新绘制画面,而View必须在UI的主线程中更新画面。那么在UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要surfaceView中 thread处理,一般就需要有一个event queue的设计来保存touch event,这会稍稍复杂一点,因为涉及到线程同步。
所以基于以上,根据游戏特点,一般分成两类。
???? ??1、被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。
?????? 2、主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。
?
?
?????? 下面把Playground完整代码附上(已打注释):
public class Playground extends SurfaceView implements View.OnTouchListener { private static int WIDTH = 40;//定义格子的大小 private static final int ROW = 9;//定义行数 private static final int COL = 9;//定义列数 private static final int BLOCK = 10;//定义路障的数量 private int size = 0;//定义size 为格子与gif图片之间的间隔,又来调整gif的位置 private Canvas c;//定义画布 private long movieStart;//动图的开始 private Dot matrix[][];//声明地图对象 private Dot cat;//声明猫的对象 //构造函数 public Playground(Context context) { super(context); getHolder().addCallback(callback);//回调函数 matrix = new Dot[ROW][COL];//初始化地图 for (int i = 0;i < ROW; i++) for (int j = 0;j < COL; j++) matrix[i][j] = new Dot(j,i); setOnTouchListener(this);//添加监听器 initGame();//初始化游戏数据 } /* 得到格子对象 */ private Dot getDot(int x,int y){ return matrix[y][x]; } //判断点是否在边界 private boolean isAtEdge(Dot d){ if (d.getX()*d.getY()==0||d.getX()+1==COL||d.getY()+1==ROW) return true; return false; } //得到对象one附近的对象 private Dot getNeighbour(Dot one,int dir){ /* dir为周围对象的下标,1为one点的左上点,2为one点的右上点,3为one点的正右点,4为one点的右下点,5为one点的左下点,6为one点的正左点 */ switch (dir) { case 1: return getDot(one.getX()-1, one.getY()); case 2: if (one.getY()%2 == 0) { return getDot(one.getX()-1, one.getY()-1); }else { return getDot(one.getX(), one.getY()-1); } case 3: if (one.getY()%2 == 0) { return getDot(one.getX(), one.getY()-1); }else { return getDot(one.getX()+1, one.getY()-1); } case 4: return getDot(one.getX()+1, one.getY()); case 5: if (one.getY()%2 == 0) { return getDot(one.getX(), one.getY()+1); }else { return getDot(one.getX()+1, one.getY()+1); } case 6: if (one.getY()%2 == 0) { return getDot(one.getX()-1, one.getY()+1); }else { return getDot(one.getX(), one.getY()+1); } default: break; } return null; } private void MoveTo(Dot one){ one.setStatus(Dot.STATUS_IN);//设置猫的位置 getDot(cat.getX(),cat.getY()).setStatus(Dot.STATUS_OFF);//将猫原来的位置设为空状态 cat.setXY(one.getX(),one.getY());//设置猫的x,y坐标 } //点击一个点后的移动判断事件 private void move(){ if(isAtEdge(cat)){//如果神经猫逃离到边界,则判断游戏失败 lose(); return; } Vector<Dot> avaliable=new Vector<>();//用vector记录某个点的各个方向路线 Vector<Dot> positive=new Vector<>();//用vector记录某个点各个方向离边缘的距离 HashMap<Dot,Integer> al=new HashMap<Dot,Integer>(); for (int i=1;i<7;i++){//得到周围的所有点 Dot n = getNeighbour(cat,i); if (n.getStatus()==Dot.STATUS_OFF) { avaliable.add(n);//如果n点为空,添加到avaliable中 al.put(n, i);//将n和i记录到al中 if (getDistance(n,i)>0){ positive.add(n);//将存在到边缘的路线添加到positive中 } } } if(avaliable.size()==0)//one点周围没有空点 win(); else if (avaliable.size() == 1){ MoveTo(avaliable.get(0));//猫的移动方法 } else{ Dot best=null; if (positive.size()!=0){//存在可以直接到达屏幕边缘的走向 int min=999; for (int i=0;i<positive.size();i++){//遍历所有路线 int a = getDistance(positive.get(i),al.get(positive.get(i))); if (a<min) { min = a; best = positive.get(i);//得到最优路线 } } } else{//所有方向都有路障 int max = 1; for (int i=0;i<avaliable.size();i++){//遍历所有路线 int k = getDistance(avaliable.get(i),al.get(avaliable.get(i))); if (k<max){ max=k;//选择离障碍最远的路线,这样才会有更多的路线选择打到边缘 best=avaliable.get(i);//最优路线 } } } MoveTo(best);//选择最优路线进行移动 } } private int getDistance(Dot one, int dir) { int distance = 0; if (isAtEdge(one)) {//判断one点是否在格子边缘 return 1; } Dot ori = one,next; while (true) {//在一条直线上 // 如果下一个点遇到障碍点,distance返回值为负 // 如果下一个点为空值,则distance+1,继续进行循环 // 如果下一个点到了格子边缘,distance+1后返 next = getNeighbour(ori,dir); if (next.getStatus() == Dot.STATUS_ON) { return distance*-1; } if (isAtEdge(next)) {//判断下一个点是否在格子边缘 distance++;//距离增加 return distance; } distance++; ori = next; } } /* 失败方法 */ private void lose(){ GameDialog gameDialog = new GameDialog(getContext(),this);//生成一个游戏结束时的自定义Dialog对象 gameDialog.show(); // Toast.makeText(getContext(),"Lose",Toast.LENGTH_SHORT).show();//弹出"Lose"消息 } /* 获胜方法 */ private void win(){ Toast.makeText(getContext(),"Win",Toast.LENGTH_SHORT).show();//弹出"Win"消息 } /* 调整Bitmap的大小 */ public static Bitmap resizeBitmap(Bitmap bitmap, int w, int h) { if (bitmap != null) { int width = bitmap.getWidth();//得到图片的宽度 int height = bitmap.getHeight();//得到图片的高度 int newWidth = w;//你想调整的宽度 int newHeight = h;//你想调整的高度 float scaleWidth = ((float) newWidth) / width;//调整的宽度的比例 float scaleHeight = ((float) newHeight) / height;//调整的高度的比例 Matrix matrix = new Matrix();//实例化一个矩阵对象 matrix.postScale(scaleWidth, scaleHeight);//缩放变换 Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);//得到调整大小后的新Bitmap对象 return resizedBitmap;//返回Bitmap对象 } else { return null;//如果Bitmap不存在,返回空值 } } public void gifDraw(Canvas canvas,float x, float y) { Movie movie = Movie.decodeStream(getResources().openRawResource(R.raw.p));//将p.gif资源读入Movie中 long now = SystemClock.uptimeMillis();//从开机到现在的毫秒数(手机睡眠的时间不包括在内) if (movieStart == 0) { // first time movieStart = now; } if (movie != null) { int dur = movie.duration();//动画的持续时间 if (dur == 0) { dur = 1000; } int relTime = (int) ((now - movieStart) % dur);//得到帧数 movie.setTime(relTime);//设置movie的帧数 movie.draw(canvas, x, y);//显示在canvas上 invalidate();//界面刷新 } } public void redraw(){ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg); Bitmap xrd = BitmapFactory.decodeResource(getResources(), R.raw.p); c = getHolder().lockCanvas();//获取canvas对象 // c.drawColor(Color.LTGRAY); c.drawBitmap(resizeBitmap(bitmap,getWidth(),getHeight()),0,0,null);//设置背景图片 size = (ROW / 3) * (getWidth() / 9 - WIDTH);//将格子向下平移 Paint paint = new Paint();//实例化一个画笔对象 paint.setFlags(Paint.ANTI_ALIAS_FLAG);//消除锯齿 for (int i = 0;i < ROW; i++) { int offset = 0; if(i%2 != 0){//偶数行向右平移半个格子的距离 offset = WIDTH/2; } for (int j = 0; j < COL; j++) { Dot one = getDot(j, i); switch (one.getStatus()) { case Dot.STATUS_OFF://格子为空状态 paint.setColor(0x7fC0C0C0); // paint.setColor(0x7f040000); c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint); break; case Dot.STATUS_IN://猫所在的格子 paint.setColor(0xFFFFAA00); c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight() / 20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight() / 20), paint); gifDraw(c, one.getX() * WIDTH + offset + size - 40, one.getY() * WIDTH + 7 * getHeight() / 20 - 140); // c.drawBitmap(resizeBitmap(xrd,WIDTH * 3 / 2,WIDTH * 2),one.getX() * WIDTH + offset + size - WIDTH / 4, one.getY() * WIDTH + 7 * getHeight() / 20 - 3 * WIDTH / 2 , null); break; case Dot.STATUS_ON: paint.setColor(0x7fFF0000);//障碍所在的格子 c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint); break; default: break; } // c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint); } } getHolder().unlockCanvasAndPost(c);//结束锁定画图,并提交改变,将图形显示 } Callback callback = new Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { redraw(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { WIDTH=width/(COL+1); redraw(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }; //初始化游戏 public void initGame(){ for (int i = 0;i < ROW; i++) for (int j = 0;j < COL; j++) matrix[i][j].setStatus(Dot.STATUS_OFF); cat=new Dot(4,4);//初始神经猫的位置 getDot(4,4).setStatus(Dot.STATUS_IN); // cat.setStatus(Dot.STATUS_IN); for (int i = 0;i<BLOCK;){//随机设置障碍的位置 int x = (int) ((Math.random()*1000) % COL);//随机生成X坐标 int y = (int) ((Math.random()*1000) % ROW);//随机生成Y坐标 if (getDot(x,y).getStatus()==Dot.STATUS_OFF){//设置状态 getDot(x,y).setStatus(Dot.STATUS_ON); i++; } } } /* 屏幕的点击方法 */ public boolean onTouch(View v, MotionEvent e) { if(e.getAction()==MotionEvent.ACTION_UP){ int x,y=0; if(e.getY()>(7 * getHeight()/20))//判断点击范围是否在格子范围高度内 y= (int) ((e.getY()-7 * getHeight()/20)/WIDTH); else {//如若不是 initGame();//重新开始游戏 redraw();//重绘 return true; } if (y%2==0){//判断是否为偶数行 x= (int) ((e.getX() -size)/WIDTH);//得到x坐标 } else{ x= (int) ((e.getX()-WIDTH/2-size)/WIDTH);//同上 } if (x+1 > COL ||y+1>ROW) {//判断坐标是否大于边界 initGame(); } else if(getDot(x, y).getStatus() == Dot.STATUS_OFF){ getDot(x, y).setStatus(Dot.STATUS_ON);//设置障碍 move();//进行移动方法 } redraw();//重绘 } return true; } }
?
??????? 整个代码中最核心的部分是move(),人物移动的算法主要有两个:
?????? 1、人物的6个方向的直线上,只要有一条可以直通地图边缘的路线,则选择离边缘最短的那条走。
?????? 2、人物的6个方向的直线上,没有一条可以通往边缘的路线,即每个方向都有障碍物挡着,则选择离障碍物最远距离的那条路线走。
?
?
?????? 网上下载了一个图片想设为背景图片,可是没有一个函数可以调整图片的大小,找了一个自定义函数来实现:
public static Bitmap resizeBitmap(Bitmap bitmap, int w, int h) { if (bitmap != null) { int width = bitmap.getWidth();//得到图片的宽度 int height = bitmap.getHeight();//得到图片的高度 int newWidth = w;//你想调整的宽度 int newHeight = h;//你想调整的高度 float scaleWidth = ((float) newWidth) / width;//调整的宽度的比例 float scaleHeight = ((float) newHeight) / height;//调整的高度的比例 Matrix matrix = new Matrix();//实例化一个矩阵对象 matrix.postScale(scaleWidth, scaleHeight);//缩放变换 Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);//得到调整大小后的新Bitmap对象 return resizedBitmap;//返回Bitmap对象 } else { return null;//如果Bitmap不存在,返回空值 } }
?
?
???????本来还想实现的是通过一个gif来实现人物的动画,android内部类没有实现gif的功能,搜索了一下资料,android实现gif有三种方式:
?????? 第一 :GifView支持android播放gif,效果是先加载第一帧,然后慢慢加载完其他的针,这样效果视觉很不好,是从模糊到清晰的过程;
?????? 第二:是流行的把gif图片通过工具分拆成n帧,然后使用逐帧动画播放,很麻烦;
?????? 第三 :使用Movie提供的Movie.decodeStream()方法解析gif,然后通过文件流的方式播放,效果特别好 ,和原图片没差。
?????? 我试了一下第三种的方法,依旧没有实现,每次点击屏幕一次,动画才会跳一帧,等询问大神后再做修改。
public void gifDraw(Canvas canvas,float x, float y) { Movie movie = Movie.decodeStream(getResources().openRawResource(R.raw.p));//将p.gif资源读入Movie中 long now = SystemClock.uptimeMillis();//从开机到现在的毫秒数(手机睡眠的时间不包括在内) if (movieStart == 0) { // first time movieStart = now; } if (movie != null) { int dur = movie.duration();//动画的持续时间 if (dur == 0) { dur = 1000; } int relTime = (int) ((now - movieStart) % dur);//得到帧数 movie.setTime(relTime);//设置movie的帧数 movie.draw(canvas, x, y);//显示在canvas上 invalidate();//界面刷新 } }
?
?????? 暂时实现了这个游戏的核心功能,开始界面和结束界面过两天实现。
?????????
?
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。