SurfaceView開發(fā)[捉小豬]手機(jī)游戲 (二)
我們在上一回(Android使用SurfaceView開發(fā)《捉小豬》小游戲 (一))搞懂了這個模式的基本實(shí)現(xiàn)思路,小豬如何找出最短的逃跑路線和如何播放路徑動畫. 還封裝了我們自己的PathAnimation和Drawable。
還差下面樹樁出現(xiàn)的效果:
哈哈,記得植物大戰(zhàn)僵尸里面有個關(guān)卡的道具出現(xiàn)也是這種效果的。
本來做這個效果的時候,想著用一個方便快捷的方法:一個新線程中,不斷遍歷已出現(xiàn)的樹樁,然后判斷是否已到達(dá)目標(biāo)位置,如果未到達(dá)就直接 x - -
后來發(fā)現(xiàn),用這個方法存在三個問題:
1. 某個任務(wù),假設(shè)在配置一般的手機(jī)上面運(yùn)行,需要1秒,那么在一些配置較高的手機(jī)上,可能0.2秒就完成了,試想一下我們的這個方法,如果運(yùn)行在高配置的手機(jī)上,那偏移的速度,你懂的。
2. 因?yàn)槊看沃皇窍蜃笃?個像素點(diǎn),所以在屏幕分辨率較高的手機(jī)上面,移動的就會比小屏的手機(jī)慢,哈哈,當(dāng)然了,解決這個問題可以用動態(tài)調(diào)整偏移量的方法,比如現(xiàn)在在720*1280的手機(jī)上面,每次的偏移量是2,那么在1440*2560的手機(jī)上面就是4了,這樣的話,即使屏幕分辨率相差很遠(yuǎn),樹樁偏移的時間也差不多是一樣的。
3. 還記不記得多線程在單核cpu上面是怎么工作的?哈哈,雖然現(xiàn)在的手機(jī)都不是單核的,但是也會出現(xiàn)cpu滿載的情況,當(dāng)cpu比較忙碌時,可能一些優(yōu)先級比較低的線程,就會得不到照顧。想一下,因?yàn)槲覀冇玫氖敲看纹埔欢ň嚯x的方法,也就是它每偏移一次,都是建立在線程爭取到cpu時間片的基礎(chǔ)上,才能更新位置,當(dāng)cpu任務(wù)較多時,線程獲取到時間片的周期也會變長,周期一長,那么樹樁的位置更新,也會變慢。
所以這種方法不可取,那么我們用哪種方法呢?
記不記得我們在上一回中,自己封裝了個動畫類,動畫進(jìn)度的更新,是根據(jù)當(dāng)前動畫已執(zhí)行時間和動畫時長來計算的。
我們也可以用這個方法來做樹樁的偏移動畫,不過首先,肯定不能每個樹樁對應(yīng)一個線程的,這樣無疑會增大cpu的開銷,正確的方法應(yīng)該只開一個線程來控制全部的樹樁。
我們來新建一個輔助類PropOffsetHelper:
里面維護(hù)一個PropData的list,這個PropData里面有一個我們自定義的drawable, 還有記錄上一次更新的時間:
public class PropData { public MyDrawable drawable; public long lastUpdateTime; public PropData(MyDrawable drawable) { this.drawable = drawable; lastUpdateTime = SystemClock.uptimeMillis(); } public void draw(Canvas canvas) { drawable.draw(canvas); } public float getX() { return drawable.getX(); } public void setX(float x) { drawable.setX(x); } public float getY() { return drawable.getY(); } public void setY(float y) { drawable.setY(y); } public void release() { if (drawable != null) { drawable.release(); } } @Override public String toString() { return String.format(Locale.getDefault(), "%f, %f", drawable.getX(), drawable.getY()); } }
我們的PropOffsetHelper聲明以下成員變量:
private float mPropOffsetSpeed;//樹頭的移動速度 private MyDrawable mPropDrawable;//樹頭的圖片 private List<PropData> mProps;//全部樹頭的數(shù)據(jù) private List<Integer> mLeavedProps;//已放置的樹頭(索引) private float mStartX, mStartY;//樹頭一開始的位置 private int mPropSize;//樹頭尺寸 private Future mUpdateTask;//更新位置的線程 private float mLeftOffset;//左邊的偏移量 private volatile boolean isNeed;//是否需要更新位置 private long mLastStopTime;//上一次暫停的時間
下面我們來看最重要的方法:
/** * 開始更新樹樁的位置 */ public void startComputeOffset() { updatePropGenerateTime(); isNeed = true; //更新樹頭位置線程 mUpdateTask = ThreadPool.getInstance().execute(() -> { boolean isFinished;//樹頭是否已經(jīng)到對應(yīng)的位置 float distance,//需要偏移的路程 offset;//本次更新的偏移量 int hitOffsetCount;//排在該樹頭前面的,并且已經(jīng)離隊(duì)的(已放置),需要忽略距離 long intervalTime,//上次更新與現(xiàn)在的間隔時間 updateTime;//今次更新時間 while (isNeed) { for (int i = 0; i < mProps.size(); i++) { PropData prop = mProps.get(i); //已離隊(duì)的不需要更新位置 if (mLeavedProps.contains(i)) { continue; } //計算出總距離 distance = i * mPropSize + mLeftOffset; //離隊(duì)樹樁數(shù)量 hitOffsetCount = 0; for (int j = 0; j < mLeavedProps.size(); j++) { //檢查是否有離隊(duì)的樹頭 if (mLeavedProps.get(j) < i) { hitOffsetCount++; } } //減去已離隊(duì)的樹樁占用的位置,得出真實(shí)的位置 distance -= mPropSize * hitOffsetCount; //樹樁的x軸小于或等于實(shí)際的偏移距離,則認(rèn)為已經(jīng)偏移完成,不需要繼續(xù)更新位置 isFinished = prop.getX() <= distance; updateTime = SystemClock.uptimeMillis(); if (!isFinished) { //計算間隔時間 intervalTime = updateTime - prop.lastUpdateTime; //路程 = 時間 * 速度 offset = intervalTime * mPropOffsetSpeed; //更新x軸位置 prop.setX(prop.getX() - offset); } //刷新上一次的更新時間 prop.lastUpdateTime = updateTime; } } }); } /** * 更新線程停止后又重新開始,需要加上停止的這段時間 */ private void updatePropGenerateTime() { if (mLastStopTime > 0) { //總停止時間 = 當(dāng)前時間 - 上次更新時間 long totalStoppedTime = SystemClock.uptimeMillis() - mLastStopTime; mLastStopTime = 0; for (int i = 0; i < mProps.size(); i++) { //加上這段時間 mProps.get(i).lastUpdateTime += totalStoppedTime; } } }
這次我們樹樁的位置是根據(jù)時間來更新的, 這樣就算在cpu滿載的時候,也不會出現(xiàn)偏移很慢的情況,只是屏幕刷新頻率慢了(掉幀)最多只是偏移的動畫不是那么流暢而已。
好了,現(xiàn)在我們的準(zhǔn)備工作都已經(jīng)差不多了,下面我們來將它們拼到一起。
我們先看這張圖:
有沒有發(fā)現(xiàn),一個小格子只能容納1樣?xùn)|西(小豬或者樹樁),如果格子上面已經(jīng)有東西了的話,再放東西上去,是會自動偏移到離他最近的一個空閑的格子上的。好吧,我們先搞定格子的位置吧:
聲明兩個二維數(shù)組(一個存放格子坐標(biāo),一個記錄格子狀態(tài):小豬占用、樹樁占用、空閑):
private Rect[][] mItems;//矩形二維數(shù)組 private volatile int[][] mItemStatus;//用來保存對應(yīng)的矩形狀態(tài)(小豬占用,木頭占用,空閑)
下面我們來看看怎么初始化格子的坐標(biāo):
private void initItems() { mItems = new Rect[VERTICAL_COUNT][HORIZONTAL_COUNT]; mItemStatus = new int[VERTICAL_COUNT][HORIZONTAL_COUNT]; int currentX, currentY; int childrenY = (getHeight() - mItemSize * VERTICAL_COUNT - mItemSize) / 2 + mItemSize / 2, childrenX = (getWidth() - mItemSize * HORIZONTAL_COUNT - mItemSize) / 2 + mItemSize / 2; //初始化矩形二維數(shù)組, 用單雙行交錯的方式排列 for (int vertical = 0; vertical < VERTICAL_COUNT; vertical++) { currentY = mItemSize * vertical; for (int horizontal = 0; horizontal < HORIZONTAL_COUNT; horizontal++) { //如果行數(shù)是雙數(shù),則向右偏移半個格子 currentX = mItemSize * horizontal + (vertical % 2 == 0 ? mItemSize / 2 : 0); Rect rect = new Rect(childrenX + currentX, childrenY + currentY, childrenX + currentX + mItemSize, childrenY + currentY + mItemSize); mItems[vertical][horizontal] = rect; changeItemStatus(vertical, horizontal, Item.STATE_UNSELECTED); } } }
我們現(xiàn)在看到的效果是這樣的:
我們放置樹樁的時候,肯定不是每次都剛好落到格子的中心點(diǎn)的,所以當(dāng)手指松開的時候,還要我們?nèi)フ{(diào)整一下樹樁的位置,好讓它剛好落到中心點(diǎn)上,當(dāng)然了,我們還要判斷離它最近的格子上是不是空閑狀態(tài),如果不是,那就尋找下一個,直到找到空閑的格子為止。
我們先看看當(dāng)手指松開后,怎么確定樹樁的位置:
還記不記得我們的格子(Rect) 存放在一個二位數(shù)組里面?當(dāng)拖動樹樁的手指松開后,我們可以遍歷這個二維數(shù)組,然后逐個判斷event.getX和getY是否在該矩形里面,如果在,那就根據(jù)它的坐標(biāo)來確定樹樁的位置了,如果它是不可放置狀態(tài)(小豬占用或已有樹樁) 那就以它為起點(diǎn)尋找下一個空閑的格子,哈哈,這個還是用深度優(yōu)先遍歷來實(shí)現(xiàn):
/** * 以currentPos為中心點(diǎn),向周圍6個方向查找空閑的位置(廣度優(yōu)先遍歷) * @param items 格子狀態(tài) * @param ignorePos 需要忽略的格子 * @param currentPos 起始的格子(以這個格子為起點(diǎn)向四周查找) * @return 空閑的格子 */ public static WayData findNextUnSelected(int[][] items, List<WayData> ignorePos, WayData currentPos) { int verticalCount = items.length; int horizontalCount = items[0].length; Queue<WayData> way = new ArrayDeque<>(); int[][] pattern = new int[verticalCount][horizontalCount]; for (int vertical = 0; vertical < verticalCount; vertical++) { //復(fù)制數(shù)組(因?yàn)橐獙?shù)組元素值進(jìn)行修改,且不能影響原來的) System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount); } way.offer(currentPos);//當(dāng)前pos先入隊(duì) pattern[currentPos.y][currentPos.x] = STATE_WALKED;//狀態(tài)標(biāo)記(已走過) while (!way.isEmpty()) {//隊(duì)列不為空 WayData header = way.poll();//隊(duì)頭出隊(duì) List<WayData> directions = getCanArrivePosUnchecked(pattern, header);//獲取周圍6個方向的位置(不包括越界的) //遍歷周邊的位置 for (int i = 0; i < directions.size(); i++) { WayData direction = directions.get(i); //判斷該位置是否空閑,如果是空閑則直接返回,如果不是空閑,則入隊(duì),下次以它為中心,尋找周邊的元素 if (!currentPos.equals(direction) && items[direction.y][direction.x] == Item.STATE_UNSELECTED && !(ignorePos != null && ignorePos.contains(direction))) { return direction; } else { way.offer(direction); } } } //隊(duì)列直至為空還沒返回,則找不到了 return null; }
我們找到這個空閑的格子之后,更新格子狀態(tài),然后再檢測當(dāng)前小豬的路徑動畫中,是否經(jīng)過這個格子,如果經(jīng)過這個格子的話,需要重新找路徑(不能在樹樁上面走過):
/** * 通知有新的樹頭放下, 有逃跑路徑在這個新占用位置上的小豬,都要重新計算新的逃跑路線(舊的已經(jīng)無效了) */ private void positionOccupied(int vertical, int horizontal) { for (int i = 0; i < PIGGY_COUNT; i++) { Pig pig = mPiggies[i]; List<WayData> pathData = pig.getPathData(); if (pathData == null || pig.getState() != Pig.STATE_RUNNING) { continue; } int currentIndex = -1; if (pig.isRepeatAnimation()) { currentIndex = 0; } else { for (int j = 0; j < pathData.size(); j++) { WayData pos = pig.getPosition(); if (pathData.get(j).equals(pos)) { currentIndex = j; break; } } } if (currentIndex != -1) { for (int k = currentIndex; k < pathData.size(); k++) { if (pathData.get(k).x == horizontal && pathData.get(k).y == vertical) { stopTask(pig); pig.setState(Pig.STATE_STANDING); initRunAnimation(pig, true); break; } } } } }
我們再來看一下這張圖:
有沒有發(fā)現(xiàn),四個格子里面剛好放進(jìn)了四只小豬,第五只怎么放也放不下,盡管有時候最下面的那個格子沒有小豬。
如果小豬的位置不會變,是固定的話,那還好辦,但是現(xiàn)在的小豬位置是不斷的在變的,而且小豬之間還會重疊,這樣一來,如果是直接根據(jù)小豬當(dāng)前位置去判斷的話,肯定是不行的,那應(yīng)該要怎么做呢?
哈哈,看這個:
/** * 判斷當(dāng)前位置是否能放置樹樁或小豬(如果在一個封閉的圈子里面,則連小豬當(dāng)前位置也要計算)例如:(0表示樹頭 .表示小豬) * * * * * * * * * 0 0 0 * * * * 0 . . 0 * * * 0 * 0 * * * * * 0 0 * * * * * * * * * * 計算出來空閑的結(jié)果是1,也即是可以放置,如果再多一個小豬在里面,則不可放置 * @param items 格子狀態(tài) * @param occupiedPos 小豬們的所在位置 * @param currentPos 起點(diǎn) * @param result 空閑的格子 * @return 圈子內(nèi)能否放置 */ public static boolean isCurrentPositionCanSet(int[][] items, WayData[] occupiedPos, WayData currentPos, List<WayData> result) { int verticalCount = items.length; int horizontalCount = items[0].length; Queue<WayData> way = new ArrayDeque<>(); int[][] pattern = new int[verticalCount][horizontalCount]; for (int vertical = 0; vertical < verticalCount; vertical++) { //復(fù)制數(shù)組(因?yàn)橐獙?shù)組元素值進(jìn)行修改,且不能影響原來的) System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount); } for (WayData tmp : occupiedPos) { if (tmp != null) { //先將小豬們占用的位置標(biāo)記未未選中 pattern[tmp.y][tmp.x] = Item.STATE_UNSELECTED; } } //以currentPos為起點(diǎn) way.offer(currentPos); //標(biāo)記狀態(tài)(已走過) pattern[currentPos.y][currentPos.x] = STATE_WALKED; if (items[currentPos.y][currentPos.x] != Item.STATE_SELECTED) { //如果起點(diǎn)也是空閑狀態(tài),則算他一個 result.add(currentPos); } //開始廣度優(yōu)先遍歷 while (!way.isEmpty()) { //隊(duì)頭出隊(duì) WayData header = way.poll(); //尋找周圍6個方向可以到達(dá)的位置(不包括越界的,標(biāo)記過的,不是空閑的)也就是空閑的格子 List<WayData> directions = getCanArrivePos(pattern, header); for (int i = 0; i < directions.size(); i++) { WayData direction = directions.get(i); //將這些位置添加進(jìn)去 result.add(direction); way.offer(direction); } } int count = 0; //重點(diǎn)來了 //現(xiàn)在result里面保存的位置,都是忽略了小豬的坐標(biāo)的,所以現(xiàn)在要重新計算一下 //遍歷小豬們當(dāng)前所在位置,是否在result中,如果在,記錄一下 for (WayData tmp : occupiedPos) { if (tmp != null && result.contains(tmp)) { count++; } } //最后,如果空閑格子內(nèi)的小豬數(shù) < 總的空閑格子數(shù),則認(rèn)為這個圈內(nèi)還能放得下,反之 return count < result.size(); }
有的小伙伴看完可能就會有疑惑,為什么用List的contains方法判斷也可以呢?它們的內(nèi)存地址可能都不相同的啊,
哈哈,這個,我們先來看一下ArrayList中contains方法是怎么實(shí)現(xiàn)的:
public boolean contains(Object o) { return indexOf(o) >= 0; }
調(diào)用了indexOf方法,那我們再看看indexOf:
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
哈哈,有沒有發(fā)現(xiàn),如果我們傳進(jìn)去的對象不為空,那么它就會調(diào)用這個對象的equals方法,看到這個方法,我們大多數(shù)時候,都是只用來判斷字符串是否一樣是吧,這個方法在Object類中,是直接返回 this == obj的,那么我們可以在WayData中重寫equals方法,然后再判斷它們的x和y是否相等就行了,嘻嘻:
@Override public boolean equals(Object obj) { return obj instanceof WayData ? x == ((WayData) obj).x && y == ((WayData) obj).y : this == obj; }
好了,本篇文章到此結(jié)束,有錯誤的地方請指出,謝謝大家!
完整代碼地址: 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通過XListView實(shí)現(xiàn)上拉加載下拉刷新功能
這篇文章主要為大家詳細(xì)介紹了Android通過XListView實(shí)現(xiàn)上拉加載下拉刷新功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12Android控件之EditView常用屬性及應(yīng)用方法
本篇文章介紹了,Android控件之EditView常用屬性及應(yīng)用方法。需要的朋友參考下2013-04-04Android實(shí)現(xiàn)動態(tài)高斯模糊效果
在Android開發(fā)中常常會用到高斯模糊,但有的時候我們可能會需要一個圖片以不同的模糊程度展現(xiàn)出來,那如何實(shí)現(xiàn)呢,一起通過本文來學(xué)習(xí)學(xué)習(xí)吧。2016-08-08Android開發(fā)仿QQ空間根據(jù)位置彈出PopupWindow顯示更多操作效果
我們打開QQ空間的時候有個箭頭按鈕點(diǎn)擊之后彈出PopupWindow會根據(jù)位置的變化顯示在箭頭的上方還是下方,比普通的PopupWindow彈在屏幕中間顯示好看的多,今天就給大家分享下實(shí)現(xiàn)代碼,需要的朋友參考下吧2016-12-12Android實(shí)現(xiàn)View拖拽跟隨手指移動效果
這篇文章主要介紹了Android實(shí)現(xiàn)View拖拽跟隨手指移動效果,主要使用setTranslationX() 和setTranslationY() 屬性方法實(shí)現(xiàn)的,需要的朋友參考下吧2017-08-08