SurfaceView游戏 SurfaceView开发[捉小猪]手机游戏 (一)
陈小缘 人气:0先上效果图:
哈哈, 说下实现思路:
我们可以把每一个树桩, 小猪, 车厢都看成是一个Drawable, 这个Drawable里面保存了x, y坐标, 我们的SurfaceView在draw的时候, 就把这些Drawable draw出来.
那可能有的小伙伴就会问了:
1. 那小猪是怎么让它跑起来, 并且腿部还不断地在动呢?
2. 还有小猪是怎么找到出路的呢?
刚刚我们讲过小猪是Drawable, 其实我们自定义的这个Drawable就是一个帧动画, 它里面有一个Bitmap数组, 一个currentIndex(这个用来记录当前帧), 我们在子线程里面不断更新这个currentIndex, 当Drawable被调用draw的时候, 就根据currentIndex来从Bitmap数组里面取对应的bitmap出来. 刚刚还讲过Drawable里面保存了当前x, y坐标, 我们的路径动画在播放的时候, 就不断的更新里面的坐标, 另外, SurfaceView那边也不断的调用这些Drawable的draw方法, 把他们画出来, 这样小猪就可以边移动, 边播放奔跑的动画了, 哈哈.
小猪找出路的话, 我们先看看这个:
哈哈哈, 这样思路是不是清晰了好多.
其实我们的SurfaceView里面有一个Rect二维数组, 用来存放这些矩形, 小猪离开手指之后, 就开始从小猪当前所在的矩形,
用广度优先遍历, 找到一条最短的路径(比如: [5,5 5,4 5,3 5,2 5,1 5,0]这样的), 然后再根据这条路径在Rect数组中找到对应的矩形, 最后根据这些对应的矩形的坐标来确定出Path.
哈哈, 有了Path小猪就可以跑了.
下面我们先来看看那个自定义的Drawable怎么写 (下面的那个ThreadPool类就是我们自己封装的一个单例的线程池):
/** * 自定义的Drawable,类似于AnimationDrawable */ public class MyDrawable extends Drawable implements Cloneable { private final int mDelay;//帧延时 private final byte[] mLock;//控制线程暂停的锁 private Semaphore mSemaphore;//来用控制线程更新问题 private Bitmap[] mBitmaps;//帧 private Paint mPaint; private int mCurrentIndex;//当前帧索引 private float x, y;//当前坐标 private Future mTask;//帧动画播放的任务 private volatile boolean isPaused;//已暂停 public MyDrawable(int delay, Bitmap... bitmaps) { mSemaphore = new Semaphore(1); mBitmaps = bitmaps; mDelay = delay; mPaint = new Paint(); mPaint.setAntiAlias(true); mLock = new byte[0]; } public void start() { stop(); mTask = ThreadPool.getInstance().execute(() -> { while (true) { synchronized (mLock) { while (isPaused) { try { mLock.wait(); } catch (InterruptedException e) { return; } } } try { Thread.sleep(mDelay); } catch (InterruptedException e) { return; } try { mSemaphore.acquire(); } catch (InterruptedException e) { return; } mCurrentIndex++; if (mCurrentIndex == mBitmaps.length) { mCurrentIndex = 0; } mSemaphore.release(); } }); } void pause() { isPaused = true; } void resume() { isPaused = false; synchronized (mLock) { mLock.notifyAll(); } } private void stop() { if (mTask != null) { mTask.cancel(true); mTask = null; mCurrentIndex = 0; } } @Override public void draw(@NonNull Canvas canvas) { try { mSemaphore.acquire(); } catch (InterruptedException e) { return; } canvas.drawBitmap(mBitmaps[mCurrentIndex], x, y, mPaint); mSemaphore.release(); } public void release() { stop(); if (mBitmaps != null) { for (Bitmap bitmap : mBitmaps) { if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } } } mBitmaps = null; mPaint = null; mTask = null; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } public Bitmap getBitmap() { Bitmap result = null; if (mBitmaps != null && mBitmaps.length > 0) { result = mBitmaps[0]; } return result; } @Override public int getIntrinsicWidth() { if (mBitmaps.length == 0) { return 0; } return mBitmaps[0].getWidth(); } @Override public int getIntrinsicHeight() { if (mBitmaps.length == 0) { return 0; } return mBitmaps[0].getHeight(); } @Override public void setAlpha(int alpha) { mPaint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { mPaint.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @SuppressWarnings("MethodDoesntCallSuperMethod") public MyDrawable clone() { return new MyDrawable(0, mBitmaps[0]); } }
start方法大概就是开启一个子线程, 每次指定延时过后就更新currentIndex, currentIndex超出范围就置0, 这样就可以一直循环播放下去了, 哈哈.
mSemaphore是当执行draw的时候, 用来锁定currentIndex不让更新的.
好了, 现在有了Drawable, 我们再来看看Path是怎么播放的:
我们可以先获取到Path上面的点, 有了这些点接下来就非常简单了.
获取Path上面的点坐标的方法大家应该也很熟悉了吧, 代码就不贴出来了,5.0及以上系统用Path的approximate方法, 5.0系统以下的用PathMeasure类. 具体代码在SDK里面也可以找到.
播放Path的话, 我们可以自定义一个PathAnimation:
其实我们自定义的这个PathAnimation播放Path的逻辑也非常简单:当start方法执行的时候,记录一下开始时间,然后一个while循环,条件就是: 当前时间 - 开始时间 < 动画时长, 然后根据当前动画已经播放的时长和总动画时长计算出当前动画的播放进度, 然后我们就可以用这个progress来获取Path上对应的点,看看完整的代码:
public class PathAnimation { private Keyframes mPathKeyframes;//关键帧 private long mAnimationDuration;//动画时长 private OnAnimationUpdateListener mOnAnimationUpdateListener;//动画更新监听 private AnimationListener mAnimationListener;//动画事件监听 private volatile boolean isAnimationRepeat, //反复播放的动画 isAnimationStopped,//已停止 isAnimationCanceled, //已取消 (停止和取消的区别: 取消是在动画播放完之前主动取消的, 停止是动画播放完,自动停止的) isAnimationEndListenerCalled;//动画已取消的监听已经回调 PathAnimation(MyPath path) { updatePath(path); } public PathAnimation setDuration(long duration) { mAnimationDuration = duration; return this; } void updatePath(MyPath path) { //根据系统版本选择更合适的关键帧类 mPathKeyframes = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new PathKeyframes(path) : new PathKeyframesSupport(path); } OnAnimationUpdateListener getUpdateListener() { return mOnAnimationUpdateListener; } void setUpdateListener(OnAnimationUpdateListener listener) { mOnAnimationUpdateListener = listener; } /** * 设置动画是否重复播放 */ public PathAnimation setRepeat(boolean isAnimationRepeat) { this.isAnimationRepeat = isAnimationRepeat; return this; } boolean isAnimationRepeat() { return isAnimationRepeat; } AnimationListener getAnimationListener() { return mAnimationListener; } void setAnimationListener(AnimationListener listener) { mAnimationListener = listener; } void start() { if (mAnimationDuration > 0) { ThreadPool.getInstance().execute(() -> { isAnimationStopped = false; isAnimationCanceled = false; isAnimationEndListenerCalled = false; final long startTime = SystemClock.uptimeMillis(); long currentPlayedDuration;//当前动画已经播放的时长 if (mAnimationListener != null) { mAnimationListener.onAnimationStart(); } while ((currentPlayedDuration = SystemClock.uptimeMillis() - startTime) < mAnimationDuration) { //如果动画被打断则跳出循环 if (isAnimationInterrupted()) { break; } //根据当前动画已经播放的时长和总动画时长计算出当前动画的播放进度 float progress = (float) currentPlayedDuration / (float) mAnimationDuration; if (mOnAnimationUpdateListener != null) { if (!isAnimationInterrupted()) { mOnAnimationUpdateListener.onUpdate(progress, mPathKeyframes.getValue(progress)); } } } if (isAnimationRepeat && !isAnimationInterrupted()) { //如果是设置了重复并且还没有被取消,则重复播放动画 mPathKeyframes.reverse(); if (mAnimationListener != null) { mAnimationListener.onAnimationRepeat(); } start(); } else { isAnimationStopped = true; if (mAnimationListener != null) { //判断应该回调哪一个接口 if (isAnimationCanceled) { mAnimationListener.onAnimationCanceled(); } else { mAnimationListener.onAnimationEnd(); } } //标记接口已回调 isAnimationEndListenerCalled = true; } }); } } /** 会阻塞,直到动画真正停止才返回 */ private void waitStopped() { isAnimationStopped = true; //noinspection StatementWithEmptyBody while (!isAnimationEndListenerCalled) { } } /** 会阻塞,直到动画真正取消才返回 */ private void waitCancel(){ isAnimationCanceled = true; //noinspection StatementWithEmptyBody while (!isAnimationEndListenerCalled) { } } void stop() { waitStopped(); } void cancel() { waitCancel(); } /** 动画被打断 */ private boolean isAnimationInterrupted() { return isAnimationCanceled || isAnimationStopped; } public interface OnAnimationUpdateListener { void onUpdate(float currentProgress, PointF position); } public interface AnimationListener { void onAnimationStart();//动画开始 void onAnimationEnd();//动画结束 void onAnimationCanceled();//动画取消 void onAnimationRepeat();//动画重复播放 } }
我们通过setUpdateListener方法来监听动画进度, OnAnimationUpdateListener接口的onUpdate方法参数还有一个PointF, 这个PointF就是根据动画当前进度从mPathKeyframes中获取到Path所对应的坐标点.
我们来写一个demo来看看这个PathAnimation的效果:
哈哈, 可以了, 是我们想要的效果.
现在动画什么的都准备好了,就差怎么把出路变成Path了,我们先来看看怎么找出路:
上面说到,屏幕上都铺满了矩形,我们可以再创建一个int类型的二维数组,用来保存这些矩形的状态(空闲:0,小猪占用:1,树桩占用:2)
我们把这个数组打印出来是这样的:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
我们再看看这个数组:(空闲:0,小猪占用:1,树桩占用:2)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 0 0 0 2 1 2 0 0 0 0 0 0 2 0 2 0 0 0 2 0 2 0 2 0 0 0 0 2 0 0 0 0 0 0 0 0 0 2 2 2 2 0 0 0 0 0 2 0 0 0 0 0 0 0
这时候就要用到一个 “广度优先遍历” ,思路就是 (逻辑有点复杂,一次看不懂看多几次就明白了):
先传入这个状态数组和当前小猪的坐标; 创建一个List<List<Point>>,存放出路,名字就叫做footprints吧; 创建一个队列,这个队列存放待查找的点; 当前小猪的坐标先放进footprints; 当前小猪的坐标入队; 标记小猪坐标已经走过; 进入循环 (循环条件就是队列不为空){ 创建一个临时的footprints;(因为最多可能有6个新的路径) 队头出队; 寻找周围6个方向(上,下,左,右,左上,右上,左下,右下)可以到达的位置 (不包括越界的、标记过的、不是空闲的); 遍历这个可到达位置的数组{ 遍历footprints{ 检查队头的坐标是否跟footprints的元素(List<Point>)的最后一个元素(Point)的位置(x,y)是一样的(即可以链接) (比如: 现在footprints是[[(5,5), (5,4)], [(6,5), (6,4)]],队头的坐标是(5,4), 那可达位置的数组就可能是[(5,3), (5,5), (4,4), (4,5), (6,4), (6,5)]){ 则创建一个新的List<Point>; add footprints的元素(比如: [(5,5), (5,4)]); 再add可达位置的坐标(比如: (5,3); 临时的footprints add 这个新的List (那临时的footprints现在就是 [[(5,5), (5,4), (5,3)]]了); } } 检查本次可达位置的坐标是否已经是在边界 (已经找到出路){ footprints add 临时的footprints的元素; 遍历footprints{ 判断footprints的元素的最后一位是否边界位置{ return 这个footprints的元素; (必然是最短的路径); } } } 队列入队本次可达位置的坐标; } (本次没有找到出路) footprints addAll 临时的footprints的元素,准备下一轮循环; } 执行到了这里, 即表示没有出路, 如果footprints里面是空的话,我们直接返回null,如果不为空,就返回footprints最后一个元素,即能走的最长的一条路径;
好了,我们看看代码是怎么写的 (WayData等同于Point, 里面也保存有x, y坐标点):
public static List<WayData> findWay(int[][] items, WayData currentPos) { //获取数组的尺寸 int verticalCount = items.length; int horizontalCount = items[0].length; //创建队列 Queue<WayData> way = new ArrayDeque<>(); //出路 List<List<WayData>> footprints = new ArrayList<>(); //复制一个新的数组 (因为要标记状态) int[][] pattern = new int[verticalCount][horizontalCount]; for (int vertical = 0; vertical < verticalCount; vertical++) { System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount); } //当前坐标入队 way.offer(currentPos); //添加进集合 List<WayData> temp = new ArrayList<>(); temp.add(currentPos); footprints.add(temp); //标记状态 (已走过) pattern[currentPos.y][currentPos.x] = STATE_WALKED; while (!way.isEmpty()) { //队头出队 WayData header = way.poll(); //以header为中心,获取周围可以到达的点(即未被占用,未标记过的)(这个方法在获取到可到达的点时,会标记这个点为: 已走过) List<WayData> directions = getCanArrivePos(pattern, header); //创建临时的footprints List<List<WayData>> footprintsTemp = new ArrayList<>(); //遍历可到达的点 for (int i = 0; i < directions.size(); i++) { WayData direction = directions.get(i); for (List<WayData> tmp : footprints) { //检查是否可以链接 if (canLinks(header, tmp)) { List<WayData> list = new ArrayList<>(); list.addAll(tmp); list.add(direction); footprintsTemp.add(list); } } //检查是否已达到边界 if (isEdge(verticalCount, horizontalCount, direction)) { if (!footprintsTemp.isEmpty()) { footprints.addAll(footprintsTemp); } //返回最短的出路 for (List<WayData> tmp : footprints) { if (!tmp.isEmpty() && isEdge2(verticalCount, horizontalCount, tmp)) { return tmp; } } } //本次未找到出路,入队这个可到达的点 way.offer(direction); } //准备下一轮循环 if (!footprintsTemp.isEmpty()) { footprints.addAll(footprintsTemp); } } //没有出路,返回能走的最长的一条路径; return footprints.isEmpty() ? null : footprints.get(footprints.size() - 1); }
getCanArrivePos方法:
/** 寻找周围6个方向可以到达的位置(不包括越界的,标记过的,不是空闲的) */ public static List<WayData> getCanArrivePos(int[][] items, WayData currentPos) { int verticalCount = items.length; int horizontalCount = items[0].length; List<WayData> result = new ArrayList<>(); int offset = currentPos.y % 2 == 0 ? 0 : 1, offset2 = currentPos.y % 2 == 0 ? 1 : 0; for (int i = 0; i < 6; i++) { WayData tmp = getNextPosition(currentPos, offset, offset2, i); if ((tmp.x > -1 && tmp.x < horizontalCount) && (tmp.y > -1 && tmp.y < verticalCount)) { if (items[tmp.y][tmp.x] != Item.STATE_SELECTED && items[tmp.y][tmp.x] != Item.STATE_OCCUPIED && items[tmp.y][tmp.x] != STATE_WALKED) { result.add(tmp); items[tmp.y][tmp.x] = STATE_WALKED; } } } //打乱它,为了让方向顺序不一样, 即每次都不同 Collections.shuffle(result); return result; }
getNextPosition方法:
/** 根据当前方向获取对应的位置 */ private static WayData getNextPosition(WayData currentPos, int offset, int offset2, int direction) { WayData result = new WayData(currentPos.x, currentPos.y); switch (direction) { case 0: //左 result.x -= 1; break; case 1: //左上 result.x -= offset; result.y -= 1; break; case 2: //左下 result.x -= offset; result.y += 1; break; case 3: //右 result.x += 1; break; case 4: //右上 result.x += offset2; result.y -= 1; break; case 5: //右下 result.x += offset2; result.y += 1; break; } return result; }
我们执行findWay方法,就会得到这个结果:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 0 0 0 2 1 2 0 0 0 0 0 0 2 * 2 0 0 0 2 0 2 * 2 0 0 0 0 2 * * * 0 0 0 0 0 * 2 2 2 2 0 0 0 0 0 2 0 0 0 0 0 0 0
哈哈,是不是很好玩, 我们将这条出路的坐标,分别获取到对应的Rect,再根据这个Rect的坐标来连接成Path, 然后我们的小猪就可以跑啦.
本文到此结束,有错误的地方请指出,谢谢大家!
完整代码地址: https://github.com/wuyr/CatchPiggy
游戏主页: https://wuyr.github.io/
加载全部内容