SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (一)
先上效果圖:
哈哈, 說下實(shí)現(xiàn)思路:
我們可以把每一個樹樁, 小豬, 車廂都看成是一個Drawable, 這個Drawable里面保存了x, y坐標(biāo), 我們的SurfaceView在draw的時候, 就把這些Drawable draw出來.
那可能有的小伙伴就會問了:
1. 那小豬是怎么讓它跑起來, 并且腿部還不斷地在動呢?
2. 還有小豬是怎么找到出路的呢?
剛剛我們講過小豬是Drawable, 其實(shí)我們自定義的這個Drawable就是一個幀動畫, 它里面有一個Bitmap數(shù)組, 一個currentIndex(這個用來記錄當(dāng)前幀), 我們在子線程里面不斷更新這個currentIndex, 當(dāng)Drawable被調(diào)用draw的時候, 就根據(jù)currentIndex來從Bitmap數(shù)組里面取對應(yīng)的bitmap出來. 剛剛還講過Drawable里面保存了當(dāng)前x, y坐標(biāo), 我們的路徑動畫在播放的時候, 就不斷的更新里面的坐標(biāo), 另外, SurfaceView那邊也不斷的調(diào)用這些Drawable的draw方法, 把他們畫出來, 這樣小豬就可以邊移動, 邊播放奔跑的動畫了, 哈哈.
小豬找出路的話, 我們先看看這個:
哈哈哈, 這樣思路是不是清晰了好多.
其實(shí)我們的SurfaceView里面有一個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小豬就可以跑了.
下面我們先來看看那個自定義的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;//當(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; } } @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, 這樣就可以一直循環(huán)播放下去了, 哈哈.
mSemaphore是當(dāng)執(zhí)行draw的時候, 用來鎖定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的話, 我們可以自定義一個PathAnimation:
其實(shí)我們自定義的這個PathAnimation播放Path的邏輯也非常簡單:當(dāng)start方法執(zhí)行的時候,記錄一下開始時間,然后一個while循環(huán),條件就是: 當(dāng)前時間 - 開始時間 < 動畫時長, 然后根據(jù)當(dāng)前動畫已經(jīng)播放的時長和總動畫時長計算出當(dāng)前動畫的播放進(jìn)度, 然后我們就可以用這個progress來獲取Path上對應(yīng)的點(diǎn),看看完整的代碼:
public class PathAnimation { private Keyframes mPathKeyframes;//關(guān)鍵幀 private long mAnimationDuration;//動畫時長 private OnAnimationUpdateListener mOnAnimationUpdateListener;//動畫更新監(jiān)聽 private AnimationListener mAnimationListener;//動畫事件監(jiān)聽 private volatile boolean isAnimationRepeat, //反復(fù)播放的動畫 isAnimationStopped,//已停止 isAnimationCanceled, //已取消 (停止和取消的區(qū)別: 取消是在動畫播放完之前主動取消的, 停止是動畫播放完,自動停止的) isAnimationEndListenerCalled;//動畫已取消的監(jiān)聽已經(jīng)回調(diào) PathAnimation(MyPath path) { updatePath(path); } public PathAnimation setDuration(long duration) { mAnimationDuration = duration; return this; } void updatePath(MyPath path) { //根據(jù)系統(tǒng)版本選擇更合適的關(guān)鍵幀類 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; } /** * 設(shè)置動畫是否重復(fù)播放 */ 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;//當(dāng)前動畫已經(jīng)播放的時長 if (mAnimationListener != null) { mAnimationListener.onAnimationStart(); } while ((currentPlayedDuration = SystemClock.uptimeMillis() - startTime) < mAnimationDuration) { //如果動畫被打斷則跳出循環(huán) if (isAnimationInterrupted()) { break; } //根據(jù)當(dāng)前動畫已經(jīng)播放的時長和總動畫時長計算出當(dāng)前動畫的播放進(jìn)度 float progress = (float) currentPlayedDuration / (float) mAnimationDuration; if (mOnAnimationUpdateListener != null) { if (!isAnimationInterrupted()) { mOnAnimationUpdateListener.onUpdate(progress, mPathKeyframes.getValue(progress)); } } } if (isAnimationRepeat && !isAnimationInterrupted()) { //如果是設(shè)置了重復(fù)并且還沒有被取消,則重復(fù)播放動畫 mPathKeyframes.reverse(); if (mAnimationListener != null) { mAnimationListener.onAnimationRepeat(); } start(); } else { isAnimationStopped = true; if (mAnimationListener != null) { //判斷應(yīng)該回調(diào)哪一個接口 if (isAnimationCanceled) { mAnimationListener.onAnimationCanceled(); } else { mAnimationListener.onAnimationEnd(); } } //標(biāo)記接口已回調(diào) 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();//動畫結(jié)束 void onAnimationCanceled();//動畫取消 void onAnimationRepeat();//動畫重復(fù)播放 } }
我們通過setUpdateListener方法來監(jiān)聽動畫進(jìn)度, OnAnimationUpdateListener接口的onUpdate方法參數(shù)還有一個PointF, 這個PointF就是根據(jù)動畫當(dāng)前進(jìn)度從mPathKeyframes中獲取到Path所對應(yīng)的坐標(biāo)點(diǎn).
我們來寫一個demo來看看這個PathAnimation的效果:
哈哈, 可以了, 是我們想要的效果.
現(xiàn)在動畫什么的都準(zhǔn)備好了,就差怎么把出路變成Path了,我們先來看看怎么找出路:
上面說到,屏幕上都鋪滿了矩形,我們可以再創(chuàng)建一個int類型的二維數(shù)組,用來保存這些矩形的狀態(tài)(空閑:0,小豬占用:1,樹樁占用:2)
我們把這個數(shù)組打印出來是這樣的:
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
我們再看看這個數(shù)組:(空閑: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
這時候就要用到一個 “廣度優(yōu)先遍歷” ,思路就是 (邏輯有點(diǎn)復(fù)雜,一次看不懂看多幾次就明白了):
先傳入這個狀態(tài)數(shù)組和當(dāng)前小豬的坐標(biāo); 創(chuàng)建一個List<List<Point>>,存放出路,名字就叫做footprints吧; 創(chuàng)建一個隊(duì)列,這個隊(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)建一個臨時的footprints;(因?yàn)樽疃嗫赡苡?個新的路徑) 隊(duì)頭出隊(duì); 尋找周圍6個方向(上,下,左,右,左上,右上,左下,右下)可以到達(dá)的位置 (不包括越界的、標(biāo)記過的、不是空閑的); 遍歷這個可到達(dá)位置的數(shù)組{ 遍歷footprints{ 檢查隊(duì)頭的坐標(biāo)是否跟footprints的元素(List<Point>)的最后一個元素(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)建一個新的List<Point>; add footprints的元素(比如: [(5,5), (5,4)]); 再add可達(dá)位置的坐標(biāo)(比如: (5,3); 臨時的footprints add 這個新的List (那臨時的footprints現(xiàn)在就是 [[(5,5), (5,4), (5,3)]]了); } } 檢查本次可達(dá)位置的坐標(biāo)是否已經(jīng)是在邊界 (已經(jīng)找到出路){ footprints add 臨時的footprints的元素; 遍歷footprints{ 判斷footprints的元素的最后一位是否邊界位置{ return 這個footprints的元素; (必然是最短的路徑); } } } 隊(duì)列入隊(duì)本次可達(dá)位置的坐標(biāo); } (本次沒有找到出路) footprints addAll 臨時的footprints的元素,準(zhǔn)備下一輪循環(huán); } 執(zhí)行到了這里, 即表示沒有出路, 如果footprints里面是空的話,我們直接返回null,如果不為空,就返回footprints最后一個元素,即能走的最長的一條路徑;
好了,我們看看代碼是怎么寫的 (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ù)制一個新的數(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)記過的)(這個方法在獲取到可到達(dá)的點(diǎn)時,會標(biāo)記這個點(diǎn)為: 已走過) List<WayData> directions = getCanArrivePos(pattern, header); //創(chuàng)建臨時的footprints List<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ì)這個可到達(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個方向可以到達(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方法,就會得到這個結(jié)果:
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
哈哈,是不是很好玩, 我們將這條出路的坐標(biāo),分別獲取到對應(yīng)的Rect,再根據(jù)這個Rect的坐標(biāo)來連接成Path, 然后我們的小豬就可以跑啦.
本文到此結(jié)束,有錯誤的地方請指出,謝謝大家!
SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (二)
完整代碼地址: https://github.com/wuyr/CatchPiggy
游戲主頁: https://wuyr.github.io/
到此這篇關(guān)于SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (一)的文章就介紹到這了,更多相關(guān)SurfaceView游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
android自定義波浪加載動畫的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了android自定義波浪加載動畫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-11-11Android 勇闖高階性能優(yōu)化之啟動優(yōu)化篇
在移動端程序中,用戶希望的是應(yīng)用能夠快速打開。啟動時間過長的應(yīng)用不能滿足這個期望,并且可能會令用戶失望。輕則鄙視你,重則直接卸載你的應(yīng)用2021-10-10android圖像繪制(四)自定義一個SurfaceView控件
自定義控件(類似按鈕等)的使用,自定義一個SurfaceView。如某一塊的動態(tài)圖(自定義相應(yīng)),或者類似UC瀏覽器下面的工具欄,感興趣的朋友可以了解下2013-01-01android studio 新手入門教程(三)Github( ignore忽略規(guī)則)的使用教程圖解
這篇文章主要介紹了android studio 新手入門教程(三)Github( ignore忽略規(guī)則)的使用教程圖解,需要的朋友可以參考下2017-12-12Android框架Volley使用之Post請求實(shí)現(xiàn)方法
這篇文章主要介紹了Android框架Volley使用之Post請求實(shí)現(xiàn)方法,,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05Android實(shí)現(xiàn)檢測手機(jī)多點(diǎn)觸摸點(diǎn)數(shù)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)檢測手機(jī)多點(diǎn)觸摸點(diǎn)數(shù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05Android實(shí)現(xiàn)購物車整體頁面邏輯詳解
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)購物車的整體頁面邏輯,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-11-11android 加載本地聯(lián)系人實(shí)現(xiàn)方法
在android開發(fā)過程中,有些功能需要訪問本地聯(lián)系人列表,本人搜集整理了一番,拿出來和大家分享一下,希望可以幫助你們2012-12-12Android開發(fā)Jetpack組件ViewModel與LiveData使用講解
Jetpack是一個由多個技術(shù)庫組成的套件,可幫助開發(fā)者遵循最佳做法,減少樣板代碼并編寫可在各種Android版本和設(shè)備中一致運(yùn)行的代碼,讓開發(fā)者精力集中編寫重要的代碼2022-09-09