Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 開發第四彈:圍住神經貓(簡單Demo)

Android 開發第四彈:圍住神經貓(簡單Demo)

編輯:關於Android編程

前言

如下圖所示,這篇文章要完成的就是這個簡單的示例,後續會繼續添加上動畫和聲音。這裡主要包含了游戲的一些簡單元素和邏輯。

在我的多次嘗試後發現想贏它還是挺難的……畢竟它的走法不是簡簡單單的Random而已。

這裡寫圖片描述

代碼已經上傳至Github,建議大家直接Fork而不是Download,畢竟開源的意義在於彼此分享代碼,而且這個太簡單了,後續肯定還會繼續更新的,所以……

游戲背景元素的定義

由於代碼還會繼續更新,所以博客中只是簡單的介紹一下,而且我都寫好了注釋。

如大家所見的,背景中有許多的點,這裡就定義一個Dot類即可。

關於Dot類,它顯然應該有坐標X、Y,還有一個狀態,那麼這個狀態具體有哪些呢?

1,點擊後開啟路障,這裡設為紅色
2,未點擊時路障為關閉狀態,這裡設為灰色
3,神經貓所在的位置自然也應該是一個狀態,畢竟這個位置玩家不能再去點擊了

然後設置相應的構造函數。

package myapplication.nomasp.com.catchcrazycat;

/**
 * Created by nomasp on 2015/09/25.
 */

public class Dot {
    int x, y;
    int status; // 灰色:貓可以走,紅色:以及被設置為路障,橘色:貓的位置
    public static final int STATUS_ON = 1;  // 開啟路障(紅色)
    public static final int STATUS_OFF = 0;   // 關閉路障(灰色)
    public static final int STATUS_IN = 9;   // 神經貓的位置

    public Dot(int x, int y) {
        super();
        this.x = x;
        this.y = y;
        status = STATUS_OFF;
    }

    public void setXY(int x, int y){
        this.x=x;
        this.y=y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }
}

初始化游戲

游戲的背景等我們全都放在Playground類中。

在初始化游戲之前,我們首先需要定義一個Dot類型的二維數組用於保存這些游戲中的原點,我這裡就稱之為游戲元素好了。

然後還需要為神經貓實例化,它本質上也是Dot類型的。

    private Dot matrix[][];  // 聲明數組matrix來保存游戲元素
    private Dot cat;    // 貓

當然了,行與列,還有路障,這些都可以設置為static且final的,如果你希望在游戲中設置不同的難度對應不同的行列和路障後續可以再修改。

    private static final int COL = 10;   // 每列有10個元素
    private static final int ROW = 10;   // 每行有10個元素
    private static final int BLOCKS = 15;   // 默認添加的路障數量

下面就是初始化游戲的方法,最後的輸出是測試使用的,為了方便我這種初學者的學習也就沒有刪掉。

   private void initGame(){
        for (int i = 0; i < ROW; i++){
            for (int j = 0; j < COL; j++){
                matrix[i][j].setStatus(Dot.STATUS_OFF);
            }
        }
        cat = new Dot(ROW/2,COL/2);   // 初始化貓的位置
        getDot(ROW / 2, COL / 2).setStatus(Dot.STATUS_IN);  // 初始化貓所在位置的狀態
        for(int i = 0; i < BLOCKS;){
            int x = (int)((Math.random()*1000)%COL);  // x為橫坐標,數組中的列
            int y = (int)((Math.random()*1000)%ROW);  // y為縱坐標,數組中的行
            if(getDot(x,y).getStatus() == Dot.STATUS_OFF){   // 如果當前Dot的狀態為OFF
                getDot(x,y).setStatus(Dot.STATUS_ON);        // 則將其打開,並讓i自增
                i++;
                //System.out.println(BLOCKS++i);
            }
        }
    }

元素的寬度這裡也可以一並聲明好:

private static int WIDTH;

關於寬度的設置有兩種方法:

方法一就是在MainActivity類中獲取屏幕的分辨率。

public static int screenWidth = 0;
// 使用Playground類中提到的根據屏幕設置元素寬度的方法一
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;

然後在Playground類中進行設置。

private static final int WIDTH = (int)(MainActivity.screenWidth*0.8)/10;

方法二到下文中繼續介紹。

重繪、回調以及觸控

首先為Playground類擴展SurfaceView以及實現OnTouchListener。

下面就是重繪背景的方法了。記得為paint設置抗鋸齒,後面根據不同狀態的Dot設置不同的顏色,最後在drawOval方法類實例化一個RectF即可。

   private void redraw(){
        Canvas c = getHolder().lockCanvas();  // 先上鎖
        c.drawColor(Color.LTGRAY);   // 為canvas設置為青色
        Paint paint = new Paint();  // 開始繪制到屏幕上
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);  // 抗鋸齒
        for (int i = 0; i < ROW; i++){
            int offset = 0;     // 設置偏移量
            if(i%2 != 0){   // i為奇數表示是第2,4,6……行(索引為1,3,5……)
                offset = WIDTH/2;   // 偏移量為元素的寬度的一半
            }
            for (int j = 0; j < COL; j++){
                Dot one = getDot(j,i);
                switch (one.getStatus()){
                    case Dot.STATUS_OFF:
                        paint.setColor(0xFFEEEEEE);
                        break;
                    case Dot.STATUS_IN:
                        paint.setColor(0xFFFF0000);
                        break;
                    case Dot.STATUS_ON:
                        paint.setColor(0xFFFFAA00);
                        break;
                    default:
                        break;
                }
                c.drawOval(new RectF(one.getX()*WIDTH+offset,
                        one.getY()*WIDTH,
                        (one.getX()+1)*WIDTH+offset,
                        (one.getY()+1)*WIDTH),paint);    // 大小由屏幕寬度決定
            }
        }
        getHolder().unlockCanvasAndPost(c);   // 解鎖
    }

然後就可以對它進行回調了。看,這裡就是前文中所說的設置WIDTH的第二種方法,巧妙的利用了surfaceChanged中傳入的width參數。

    Callback callback = new Callback() {    // 回調方法
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            redraw();  // 調用redraw進行重繪
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            WIDTH = width/(COL+1);
            redraw();   // 修改寬度後進行重繪
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {

        }
    };

以下就開始寫觸控個功能了,直接重載onTouch方法即可。

注釋已經寫的非常詳細了,和上面一樣,測試代碼為了學習方面也保留了。

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_UP){
            //Toast.makeText(getContext(),event.getX()+:+ event.getY(),
            //        Toast.LENGTH_SHORT).show();
            int x,y;    // 觸摸的X、Y坐標
            y = (int)(event.getY()/WIDTH);
            if(y%2 == 0){   // 表示第1、3、5……行元素(索引為0、2、4……)
                x = (int)(event.getX()/WIDTH);
            }else{
                x = (int)((event.getX()-WIDTH/2)/WIDTH);    // 事件獲取的X坐標減去左邊空余的WIDTH/2部分,再除以WIDTH
            }
            // 以下代碼對坐標進行保護,如果沒有這些代碼當點擊位置超出坐標范圍後也就是超出了數組索引
            if(x+1 > COL || y+1 > ROW){     // 超出邊界,當前點擊無效
                // 以下為測試代碼
//                System.out.println(------------------------);
//                getNrighbour(cat,k).setStatus(Dot.STATUS_IN);
//                k++;
//                redraw();
                initGame();
                // 以下為測試代碼
//                System.out.println(------------------------);
//                for(int i = 1;i < 7; i++){
//                    System.out.println(i+@+getDistance(cat,i));

            } else if(getDot(x,y).getStatus() == Dot.STATUS_OFF){  // 當這個點可用時
                getDot(x,y).setStatus(Dot.STATUS_ON);   // 點擊有效,開啟路障狀態
                move();
            }
            redraw();   // 重繪
        }
        return true;
    }

噢對了,前面用到的getDot方法還沒有說,這是由於上面的X、Y是觸控事件的X、Y坐標,但對應到數組中X就成了列,Y則成了行,而二維數組是行在前、列在後的,所以就為了方便起見就特意設置了一個方法。

   private Dot getDot(int x, int y){  // 傳入x,y返回matrix[y][x]
        return matrix[y][x];
    }

當然,你也可以直接寫成matrix[y][x]。

下面這個其實是一開始就寫的,只是其中用到的一些東西是逐個補充上去,所以這裡放在了後面。

   public Playground(Context context) {
        super(context);
        getHolder().addCallback(callback);  // 為getHolder設置回調
        matrix = new Dot[ROW][COL];   // 初始化
        for(int i = 0; i < ROW; i++){
            for(int j = 0; j < COL; j++){
                matrix[i][j]=new Dot(j,i);  // x表示行,y表示列,因此和數組的COL/ROW是錯開的
            }
        }
        setOnTouchListener(this);  // 該類已經實現了OnTouchListener接口,因此只用傳入this即可
        initGame();
    }

獲取距離等一些功能性方法

首先,游戲需要判斷神經貓等是否處於邊緣部分,作用不言而喻。它自然需要的是一個Dot類型的參數。

    private boolean isAtEdge(Dot d){
        if(d.getX()*d.getY() == 0 || d.getX()+1 == COL || d.getY()+1 == ROW){   // 此時處於游戲邊界
            return true;
        }
        return false;
    }

接下來這個就要好好理一理關系了,有了下面的圖片示例就很好理解了,按順時針計算就好。

這裡寫圖片描述

    private Dot getNeighbor(Dot d, int dir){ // 從水平相連的左側點開始為1,依次順時針計數
        switch (dir){
            case 1:
                return getDot(d.getX()-1,d.getY());
            case 2:
                if(d.getY()%2 == 0){
                    return getDot(d.getX()-1,d.getY()-1);
                }else{
                    return getDot(d.getX(),d.getY()-1);
                }
            case 3:
                if(d.getY()%2 == 0){
                    return getDot(d.getX(),d.getY()-1);
                }else{
                    return getDot(d.getX()+1,d.getY()-1);
                }
            case 4:
                return getDot(d.getX()+1,d.getY());
            case 5:
                if(d.getY()%2 == 0){
                    return getDot(d.getX(),d.getY()+1);
                }else{
                    return getDot(d.getX()+1,d.getY()+1);
                }
            case 6:
                if(d.getY()%2 == 0){
                    return getDot(d.getX()-1, d.getY()+1);
                }else{
                    return getDot(d.getX(),d.getY()+1);
                }
            default:
                return null;
        }
    }

以下這個方法用於計算距離:

如果距離某個方向路障中間有2個灰色(OFF)則返回-2;
如果緊挨該方向的路障,則返回0;
如果該方向沒有路障,則返回至邊界中間的灰色元素個數,為正數。
    private int getDistance(Dot d, int dir){
        int distance = 0;
        if(isAtEdge(d)){    // 如果該點已經在屏幕邊緣,那麼就不必繼續判斷直接返回distance即可
            return 1;
        }
        Dot ori = d,next;
        while (true) {
            next = getNrighbour(ori,dir);     // 將當前點d的某方向的鄰居賦值給next
            if(next.getStatus() == Dot.STATUS_ON){  // 碰到了路障,返回0或負數
                return distance*-1;
            }
            if(isAtEdge(next)){     // 抵達了場景邊緣,返回正數
                distance++;     // 說明下一個點也是可用的
                return distance;
            }
            distance++;     // 距離自增
            ori = next;     // 將next設置為當前參考點
        }
    }

勝負已分

由於這只是一個簡單的Demo,所以勝負就用Toast通知好了,具體的後續繼續完善,也歡迎大家貢獻代碼。

    private void lose(){    // 游戲失敗
        Toast.makeText(getContext(),Lose,Toast.LENGTH_SHORT).show();
    }

    private void win(){     // 游戲成功
        Toast.makeText(getContext(),Win,Toast.LENGTH_SHORT).show();
    }

負責神經貓移動的方法

好吧,我承認移動這一部分正在寫代碼的時候順序不在最後的,這是這裡為重點就放在最後介紹了。

按功能分為兩步,移動以及移動到,當然了move中肯定是要調用MoveTo的,只不過後者側重於神經貓的動作,前者側重於神經貓的走法。

private void MoveTo(Dot d){     // 移動神經貓到某個點
   d.setStatus(Dot.STATUS_IN);
   getDot(cat.getX(),cat.getY()).setStatus(Dot.STATUS_OFF);
   cat.setXY(d.getX(),d.getY());
}

下面就來寫move方法,為了方面學習,這裡按步驟進行。

1,判斷神經貓是否處於邊界,是則游戲失敗,不進行move,直接return。

if(isAtEdge(cat)){  // 判斷貓是否在場景邊界
    lose();     // 游戲失敗
    return;
}

而後判斷周圍的6個元素是否可以走(簡單的說就是:是否可用)。

for(int i = 1; i < 7; i++){
    Dot n = getNeighbor(cat, i);
    if(n.getStatus() == Dot.STATUS_OFF){    // 如果這鄰居(點)可用
         avaliable.add(n);      // 將其添加到avaliable中
         length.put(n,i);    // i為方向
         if(getDistance(n,i) > 0){   // 正數表示可以到達邊界
             positive.add(n);
         }
     }
}

上面實例化了一個n出來,當然你也可以直接寫成:

if(getNeighbor(cat,i).getStatus() == Dot.STATUS_OFF)

如果它的鄰居的狀態是關閉的,那麼也就意味著這一點是可用的,於是添加到avaliable中。在for循環之前就應該定義好它們:

Vector avaliable = new Vector<>();     // 當前點周圍6個點中的可用點
Vector positive = new Vector<>();  // 當前點到邊界的距離點
HashMap length = new HashMap();// 搭配positive,其用於記錄方向

如果沒有可用點,就意味著已經成功圍住了神經貓;如果只有一個可用點,就直接移動過去就好了。

if(avaliable.size() == 0){      // 沒有可用點
    win();      // 成功圍住神經貓
}else if(avaliable.size() == 1) {     // 只有一個可用點
    MoveTo(avaliable.get(0));
}

那麼最關鍵的部分來了,如果有多條路可以走怎麼辦?

else{      // 既不是第一次點擊,且有多條路可走
   // 根據到邊界edge的距離distance(包括路障等的計算)來決定走的方向
   Dot best = null;   // 最終決定要移動到的元素(點)
   if(positive.size() != 0){   // 含有可到達邊界的方向
      System.out.println(向前進);
      int min = 999;  // 用於記錄到達邊界的最小值,初始值為一個較大值
      for(int i = 0; i < positive.size(); i++){
         int tempDis = getDistance(positive.get(i),length.get(positive.get(i)));
         if(tempDis < min) {
              min = tempDis;
              best = positive.get(i);
         }
      }
   }else{  // 所有方向都有路障
      System.out.println(躲路障);
      int max = 0;    // 所有方向都有路障時,距離要麼為負數要麼為0
      for(int i = 0; i < avaliable.size(); i++){
         int tempDis = getDistance(avaliable.get(i),length.get(avaliable.get(i)));
         if(tempDis < max) {   // 因為tempDis是負數,所以用小於號
            max = tempDis;
            best = avaliable.get(i);
         }
      }
  }

如果各個方向都沒有路障,就選擇路徑最短的一個,如果有路障就要躲避這些路障。這裡分別用min和max來記錄最佳的路徑,最後就將其賦值給best:

best = avaliable.get(i);

為了防止best為空時不知道怎麼走,就為它設置一個隨機路徑好了。

if(best == null) {      // 當best為空時,隨機一個路徑
   int s = (int) ((Math.random() * 1000) % avaliable.size());
   MoveTo(avaliable.get(s));
}else{
   MoveTo(best);   // 移動到最合適的一點
}

歡迎大家pull request

我都會上傳到Github上,且都會有注釋,為了幫助到更多人,歡迎大家Fork和Star,更歡迎大家pull request,地址為:傳送門。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved