SurfaceView開發(fā)[捉小豬]手機游戲 (二)
我們在上一回(Android使用SurfaceView開發(fā)《捉小豬》小游戲 (一))搞懂了這個模式的基本實現(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è)在配置一般的手機上面運行,需要1秒,那么在一些配置較高的手機上,可能0.2秒就完成了,試想一下我們的這個方法,如果運行在高配置的手機上,那偏移的速度,你懂的。
2. 因為每次只是向左偏移1個像素點,所以在屏幕分辨率較高的手機上面,移動的就會比小屏的手機慢,哈哈,當(dāng)然了,解決這個問題可以用動態(tài)調(diào)整偏移量的方法,比如現(xiàn)在在720*1280的手機上面,每次的偏移量是2,那么在1440*2560的手機上面就是4了,這樣的話,即使屏幕分辨率相差很遠(yuǎn),樹樁偏移的時間也差不多是一樣的。
3. 還記不記得多線程在單核cpu上面是怎么工作的?哈哈,雖然現(xiàn)在的手機都不是單核的,但是也會出現(xiàn)cpu滿載的情況,當(dāng)cpu比較忙碌時,可能一些優(yōu)先級比較低的線程,就會得不到照顧。想一下,因為我們用的是每次偏移一定距離的方法,也就是它每偏移一次,都是建立在線程爭取到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)離隊的(已放置),需要忽略距離
long intervalTime,//上次更新與現(xiàn)在的間隔時間
updateTime;//今次更新時間
while (isNeed) {
for (int i = 0; i < mProps.size(); i++) {
PropData prop = mProps.get(i);
//已離隊的不需要更新位置
if (mLeavedProps.contains(i)) {
continue;
}
//計算出總距離
distance = i * mPropSize + mLeftOffset;
//離隊樹樁數(shù)量
hitOffsetCount = 0;
for (int j = 0; j < mLeavedProps.size(); j++) {
//檢查是否有離隊的樹頭
if (mLeavedProps.get(j) < i) {
hitOffsetCount++;
}
}
//減去已離隊的樹樁占用的位置,得出真實的位置
distance -= mPropSize * hitOffsetCount;
//樹樁的x軸小于或等于實際的偏移距離,則認(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)在看到的效果是這樣的:
我們放置樹樁的時候,肯定不是每次都剛好落到格子的中心點的,所以當(dāng)手指松開的時候,還要我們?nèi)フ{(diào)整一下樹樁的位置,好讓它剛好落到中心點上,當(dāng)然了,我們還要判斷離它最近的格子上是不是空閑狀態(tài),如果不是,那就尋找下一個,直到找到空閑的格子為止。
我們先看看當(dāng)手指松開后,怎么確定樹樁的位置:
還記不記得我們的格子(Rect) 存放在一個二位數(shù)組里面?當(dāng)拖動樹樁的手指松開后,我們可以遍歷這個二維數(shù)組,然后逐個判斷event.getX和getY是否在該矩形里面,如果在,那就根據(jù)它的坐標(biāo)來確定樹樁的位置了,如果它是不可放置狀態(tài)(小豬占用或已有樹樁) 那就以它為起點尋找下一個空閑的格子,哈哈,這個還是用深度優(yōu)先遍歷來實現(xiàn):
/**
* 以currentPos為中心點,向周圍6個方向查找空閑的位置(廣度優(yōu)先遍歷)
* @param items 格子狀態(tài)
* @param ignorePos 需要忽略的格子
* @param currentPos 起始的格子(以這個格子為起點向四周查找)
* @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ù)組(因為要對數(shù)組元素值進(jìn)行修改,且不能影響原來的)
System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount);
}
way.offer(currentPos);//當(dāng)前pos先入隊
pattern[currentPos.y][currentPos.x] = STATE_WALKED;//狀態(tài)標(biāo)記(已走過)
while (!way.isEmpty()) {//隊列不為空
WayData header = way.poll();//隊頭出隊
List<WayData> directions = getCanArrivePosUnchecked(pattern, header);//獲取周圍6個方向的位置(不包括越界的)
//遍歷周邊的位置
for (int i = 0; i < directions.size(); i++) {
WayData direction = directions.get(i);
//判斷該位置是否空閑,如果是空閑則直接返回,如果不是空閑,則入隊,下次以它為中心,尋找周邊的元素
if (!currentPos.equals(direction) && items[direction.y][direction.x] == Item.STATE_UNSELECTED
&& !(ignorePos != null && ignorePos.contains(direction))) {
return direction;
} else {
way.offer(direction);
}
}
}
//隊列直至為空還沒返回,則找不到了
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 起點
* @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ù)組(因為要對數(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為起點
way.offer(currentPos);
//標(biāo)記狀態(tài)(已走過)
pattern[currentPos.y][currentPos.x] = STATE_WALKED;
if (items[currentPos.y][currentPos.x] != Item.STATE_SELECTED) {
//如果起點也是空閑狀態(tài),則算他一個
result.add(currentPos);
}
//開始廣度優(yōu)先遍歷
while (!way.isEmpty()) {
//隊頭出隊
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;
//重點來了
//現(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方法是怎么實現(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ā)[捉小豬]手機游戲 (二)的文章就介紹到這了,更多相關(guān)SurfaceView游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android通過XListView實現(xiàn)上拉加載下拉刷新功能
這篇文章主要為大家詳細(xì)介紹了Android通過XListView實現(xiàn)上拉加載下拉刷新功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12
Android控件之EditView常用屬性及應(yīng)用方法
本篇文章介紹了,Android控件之EditView常用屬性及應(yīng)用方法。需要的朋友參考下2013-04-04
Android開發(fā)仿QQ空間根據(jù)位置彈出PopupWindow顯示更多操作效果
我們打開QQ空間的時候有個箭頭按鈕點擊之后彈出PopupWindow會根據(jù)位置的變化顯示在箭頭的上方還是下方,比普通的PopupWindow彈在屏幕中間顯示好看的多,今天就給大家分享下實現(xiàn)代碼,需要的朋友參考下吧2016-12-12

