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

