Android使用SurfaceView开发《捉小猪》小游戏 (一)
先上效果圖:
哈哈, 說下實(shí)現(xiàn)思路:
我們可以把每一個(gè)樹樁, 小豬, 車廂都看成是一個(gè)Drawable, 這個(gè)Drawable里面保存了x, y坐標(biāo), 我們的SurfaceView在draw的時(shí)候, 就把這些Drawable draw出來.
那可能有的小伙伴就會問了:
1. 那小豬是怎么讓它跑起來, 并且腿部還不斷地在動呢?
2. 還有小豬是怎么找到出路的呢?
剛剛我們講過小豬是Drawable, 其實(shí)我們自定義的這個(gè)Drawable就是一個(gè)幀動畫, 它里面有一個(gè)Bitmap數(shù)組, 一個(gè)currentIndex(這個(gè)用來記錄當(dāng)前幀), 我們在子線程里面不斷更新這個(gè)currentIndex, 當(dāng)Drawable被調(diào)用draw的時(shí)候, 就根據(jù)currentIndex來從Bitmap數(shù)組里面取對應(yīng)的bitmap出來. 剛剛還講過Drawable里面保存了當(dāng)前x, y坐標(biāo), 我們的路徑動畫在播放的時(shí)候, 就不斷的更新里面的坐標(biāo), 另外, SurfaceView那邊也不斷的調(diào)用這些Drawable的draw方法, 把他們畫出來, 這樣小豬就可以邊移動, 邊播放奔跑的動畫了, 哈哈.
小豬找出路的話, 我們先看看這個(gè):
哈哈哈, 這樣思路是不是清晰了好多.
其實(shí)我們的SurfaceView里面有一個(gè)Rect二維數(shù)組, 用來存放這些矩形, 小豬離開手指之后, 就開始從小豬當(dāng)前所在的矩形,
用廣度優(yōu)先遍歷, 找到一條最短的路徑(比如: [5,5 5,4 5,3 5,2 5,1 5,0]這樣的), 然后再根據(jù)這條路徑在Rect數(shù)組中找到對應(yīng)的矩形, 最后根據(jù)這些對應(yīng)的矩形的坐標(biāo)來確定出Path.
哈哈, 有了Path小豬就可以跑了.
下面我們先來看看那個(gè)自定義的Drawable怎么寫 (下面的那個(gè)ThreadPool類就是我們自己封裝的一個(gè)單例的線程池):
/*** 自定義的Drawable,類似于AnimationDrawable*/ public class MyDrawable extends Drawable implements Cloneable {private final int mDelay;//幀延時(shí)private final byte[] mLock;//控制線程暫停的鎖private Semaphore mSemaphore;//來用控制線程更新問題private Bitmap[] mBitmaps;//幀private Paint mPaint;private int mCurrentIndex;//當(dāng)前幀索引private float x, y;//當(dāng)前坐標(biāo)private Future mTask;//幀動畫播放的任務(wù)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;}}@Overridepublic 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;}@Overridepublic int getIntrinsicWidth() {if (mBitmaps.length == 0) {return 0;}return mBitmaps[0].getWidth();}@Overridepublic int getIntrinsicHeight() {if (mBitmaps.length == 0) {return 0;}return mBitmaps[0].getHeight();}@Overridepublic void setAlpha(int alpha) {mPaint.setAlpha(alpha);}@Overridepublic void setColorFilter(ColorFilter cf) {mPaint.setColorFilter(cf);}@Overridepublic int getOpacity() {return PixelFormat.TRANSLUCENT;}@SuppressWarnings("MethodDoesntCallSuperMethod")public MyDrawable clone() {return new MyDrawable(0, mBitmaps[0]);} }start方法大概就是開啟一個(gè)子線程, 每次指定延時(shí)過后就更新currentIndex, currentIndex超出范圍就置0, 這樣就可以一直循環(huán)播放下去了, 哈哈.
mSemaphore是當(dāng)執(zhí)行draw的時(shí)候, 用來鎖定currentIndex不讓更新的.
好了, 現(xiàn)在有了Drawable, 我們再來看看Path是怎么播放的:
我們可以先獲取到Path上面的點(diǎn), 有了這些點(diǎn)接下來就非常簡單了.
獲取Path上面的點(diǎn)坐標(biāo)的方法大家應(yīng)該也很熟悉了吧, 代碼就不貼出來了,5.0及以上系統(tǒng)用Path的approximate方法, 5.0系統(tǒng)以下的用PathMeasure類. 具體代碼在SDK里面也可以找到.
播放Path的話, 我們可以自定義一個(gè)PathAnimation:
其實(shí)我們自定義的這個(gè)PathAnimation播放Path的邏輯也非常簡單:當(dāng)start方法執(zhí)行的時(shí)候,記錄一下開始時(shí)間,然后一個(gè)while循環(huán),條件就是: 當(dāng)前時(shí)間 - 開始時(shí)間 < 動畫時(shí)長, 然后根據(jù)當(dāng)前動畫已經(jīng)播放的時(shí)長和總動畫時(shí)長計(jì)算出當(dāng)前動畫的播放進(jìn)度, 然后我們就可以用這個(gè)progress來獲取Path上對應(yīng)的點(diǎn),看看完整的代碼:
我們通過setUpdateListener方法來監(jiān)聽動畫進(jìn)度, OnAnimationUpdateListener接口的onUpdate方法參數(shù)還有一個(gè)PointF, 這個(gè)PointF就是根據(jù)動畫當(dāng)前進(jìn)度從mPathKeyframes中獲取到Path所對應(yīng)的坐標(biāo)點(diǎn).
我們來寫一個(gè)demo來看看這個(gè)PathAnimation的效果:
哈哈, 可以了, 是我們想要的效果.
現(xiàn)在動畫什么的都準(zhǔn)備好了,就差怎么把出路變成Path了,我們先來看看怎么找出路:
上面說到,屏幕上都鋪滿了矩形,我們可以再創(chuàng)建一個(gè)int類型的二維數(shù)組,用來保存這些矩形的狀態(tài)(空閑:0,小豬占用:1,樹樁占用:2)
我們把這個(gè)數(shù)組打印出來是這樣的:
我們再看看這個(gè)數(shù)組:(空閑:0,小豬占用:1,樹樁占用:2)
0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 00 0 0 0 2 2 0 0 00 0 0 2 1 2 0 0 00 0 0 2 0 2 0 0 02 0 2 0 2 0 0 0 02 0 0 0 0 0 0 0 00 2 2 2 2 0 0 0 00 2 0 0 0 0 0 0 0這時(shí)候就要用到一個(gè) “廣度優(yōu)先遍歷” ,思路就是 (邏輯有點(diǎn)復(fù)雜,一次看不懂看多幾次就明白了):
先傳入這個(gè)狀態(tài)數(shù)組和當(dāng)前小豬的坐標(biāo);創(chuàng)建一個(gè)List<List<Point>>,存放出路,名字就叫做footprints吧;創(chuàng)建一個(gè)隊(duì)列,這個(gè)隊(duì)列存放待查找的點(diǎn);當(dāng)前小豬的坐標(biāo)先放進(jìn)footprints;當(dāng)前小豬的坐標(biāo)入隊(duì);標(biāo)記小豬坐標(biāo)已經(jīng)走過;進(jìn)入循環(huán) (循環(huán)條件就是隊(duì)列不為空){創(chuàng)建一個(gè)臨時(shí)的footprints;(因?yàn)樽疃嗫赡苡?span id="ozvdkddzhkzd" class="hljs-number">6個(gè)新的路徑)隊(duì)頭出隊(duì);尋找周圍6個(gè)方向(上,下,左,右,左上,右上,左下,右下)可以到達(dá)的位置 (不包括越界的、標(biāo)記過的、不是空閑的);遍歷這個(gè)可到達(dá)位置的數(shù)組{遍歷footprints{檢查隊(duì)頭的坐標(biāo)是否跟footprints的元素(List<Point>)的最后一個(gè)元素(Point)的位置(x,y)是一樣的(即可以鏈接)(比如: 現(xiàn)在footprints是[[(5,5), (5,4)], [(6,5), (6,4)]],隊(duì)頭的坐標(biāo)是(5,4), 那可達(dá)位置的數(shù)組就可能是[(5,3), (5,5), (4,4), (4,5), (6,4), (6,5)]){則創(chuàng)建一個(gè)新的List<Point>;add footprints的元素(比如: [(5,5), (5,4)]);再add可達(dá)位置的坐標(biāo)(比如: (5,3);臨時(shí)的footprints add 這個(gè)新的List (那臨時(shí)的footprints現(xiàn)在就是 [[(5,5), (5,4), (5,3)]]了);} }檢查本次可達(dá)位置的坐標(biāo)是否已經(jīng)是在邊界 (已經(jīng)找到出路){footprints add 臨時(shí)的footprints的元素;遍歷footprints{判斷footprints的元素的最后一位是否邊界位置{return 這個(gè)footprints的元素; (必然是最短的路徑);}}}隊(duì)列入隊(duì)本次可達(dá)位置的坐標(biāo);}(本次沒有找到出路) footprints addAll 臨時(shí)的footprints的元素,準(zhǔn)備下一輪循環(huán);}執(zhí)行到了這里, 即表示沒有出路, 如果footprints里面是空的話,我們直接返回null,如果不為空,就返回footprints最后一個(gè)元素,即能走的最長的一條路徑;好了,我們看看代碼是怎么寫的 (WayData等同于Point, 里面也保存有x, y坐標(biāo)點(diǎn)):
public static List<WayData> findWay(int[][] items, WayData currentPos) {//獲取數(shù)組的尺寸int verticalCount = items.length;int horizontalCount = items[0].length;//創(chuàng)建隊(duì)列Queue<WayData> way = new ArrayDeque<>();//出路List<List<WayData>> footprints = new ArrayList<>();//復(fù)制一個(gè)新的數(shù)組 (因?yàn)橐獦?biāo)記狀態(tài))int[][] pattern = new int[verticalCount][horizontalCount];for (int vertical = 0; vertical < verticalCount; vertical++) {System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount);}//當(dāng)前坐標(biāo)入隊(duì)way.offer(currentPos);//添加進(jìn)集合List<WayData> temp = new ArrayList<>();temp.add(currentPos);footprints.add(temp);//標(biāo)記狀態(tài) (已走過)pattern[currentPos.y][currentPos.x] = STATE_WALKED;while (!way.isEmpty()) {//隊(duì)頭出隊(duì)WayData header = way.poll();//以header為中心,獲取周圍可以到達(dá)的點(diǎn)(即未被占用,未標(biāo)記過的)(這個(gè)方法在獲取到可到達(dá)的點(diǎn)時(shí),會標(biāo)記這個(gè)點(diǎn)為: 已走過)List<WayData> directions = getCanArrivePos(pattern, header);//創(chuàng)建臨時(shí)的footprintsList<List<WayData>> footprintsTemp = new ArrayList<>();//遍歷可到達(dá)的點(diǎn)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);}}//檢查是否已達(dá)到邊界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;}}}//本次未找到出路,入隊(duì)這個(gè)可到達(dá)的點(diǎn)way.offer(direction);}//準(zhǔn)備下一輪循環(huán)if (!footprintsTemp.isEmpty()) {footprints.addAll(footprintsTemp);}}//沒有出路,返回能走的最長的一條路徑;return footprints.isEmpty() ? null : footprints.get(footprints.size() - 1);}getCanArrivePos方法:
/**尋找周圍6個(gè)方向可以到達(dá)的位置(不包括越界的,標(biāo)記過的,不是空閑的)*/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方法:
/**根據(jù)當(dāng)前方向獲取對應(yīng)的位置*/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;}我們執(zhí)行findWay方法,就會得到這個(gè)結(jié)果:
0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 00 0 0 0 2 2 0 0 00 0 0 2 1 2 0 0 00 0 0 2 * 2 0 0 02 0 2 * 2 0 0 0 02 * * * 0 0 0 0 0* 2 2 2 2 0 0 0 00 2 0 0 0 0 0 0 0哈哈,是不是很好玩, 我們將這條出路的坐標(biāo),分別獲取到對應(yīng)的Rect,再根據(jù)這個(gè)Rect的坐標(biāo)來連接成Path, 然后我們的小豬就可以跑啦.
本文到此結(jié)束,有錯(cuò)誤的地方請指出,謝謝大家!
Android使用SurfaceView開發(fā)《捉小豬》小游戲 (二)
完整代碼地址: https://github.com/wuyr/CatchPiggy
游戲主頁: https://wuyr.github.io/
總結(jié)
以上是生活随笔為你收集整理的Android使用SurfaceView开发《捉小猪》小游戏 (一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 纯js实现俄罗斯方块详解与源码
- 下一篇: Android文字跑马灯简单实现的三种方