推箱子小游戲——代碼分析


代碼組成

本項目主要分類三個Activity類:

  • MainActivity: 主活動類游戲初始界面
  • GameActivity:游戲界面
  • GameLevelActivity:關卡選擇界面

三個活動類對應的三個布局:

  • activity_main.xml: 主活動布局。
  • act_game_activity.xml:游戲活動布局。
  • act_xuan_guan_qia.xml: 選擇關卡布局

其他輔助類:

  • GameBitmaps: 用來加載圖片
  • GameLevels:用來存放關卡信息和返回關卡信息數組
  • GameState:用來使用StringBuffer存儲當前關卡狀態
  • GameView:自定義的View類,繪制游戲界面,監聽touch動作,對行為進行邏輯判斷
  • TCell: 自定義的類,用於表示旗幟位置

代碼調用關系

MainActivity

public class MainActivity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btnGameIntro = (Button) findViewById(R.id.btn_game_intro);
        btnGameIntro.setOnClickListener(
                new View.OnClickListener(){
                    @Override
                    public void onClick(View view){
                        Intent intent = new Intent(MainActivity.this, GameIntroActivity.class);
                        startActivity(intent);
                        Toast.makeText(MainActivity.this, "按了游戲簡介按鈕", Toast.LENGTH_SHORT).show();
                    }
                }
        );
        Button btnExitGame = (Button) findViewById(R.id.btn_exit);
        btnExitGame.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                finish();
            }
        });
        Button btnStartGame = (Button) findViewById(R.id.btn_start_game);
        btnStartGame.setOnClickListener(
                new View.OnClickListener(){
                    @Override
                    public void onClick(View view) {
                        Intent intent = new Intent(MainActivity.this, GameLevelActivity.class);
                        startActivity(intent);
                    }
                });
    }
}

在主活動類的onCreate方法中構建其布局layout文件,在通過三個Button類和findViewById方法與布局中三個Button綁定,並對三個Button建立Click監聽器。

其中id為start_game按鈕的監聽器回調函數是用來調用另一個GameLevelActivity。

GameLevelActivity

public class GameLevelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_xuan_guan_qia);

        GridView gv_levels = (GridView) findViewById(R.id.gv_levels);
        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, R.layout.gv_levels_item_textview, GameLevels.getLevelList());
        gv_levels.setAdapter(arrayAdapter);

        gv_levels.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Intent intent = new Intent(GameLevelActivity.this, GameActivity.class);
                intent.putExtra("Selected_level", position+1);
                startActivity(intent);
            }
        });
    }
}

首先構建act_xuan_guan_qia布局,每個關卡采用Grid網格布局,使用ArrayAdapter對每個Grid中的Textview內容進行賦值,最后對每個Grid建立監聽器,回調函數是啟動GameActivity活動類,並將所選關卡的值傳給GameActivity。

GameActivity

public class GameActivity extends Activity {
    public static Toast toast;
    public static Toast toast1;
    public static Toast toast2;
    public static final String KEY_SELECTED_LEVEL = "Selected_level";
    private GameState mCurrentState;
    private GameView mGameView;
    private int selected_level;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        selected_level = getIntent().getIntExtra(KEY_SELECTED_LEVEL,1);
        mCurrentState = new GameState(GameLevels.getLevel(selected_level));


        setContentView(R.layout.act_game_activity);

        toast = toast.makeText(this,"恭喜你,通關了!", toast.LENGTH_LONG);
        toast1 = toast1.makeText(this, "已經是第一關了!", toast1.LENGTH_SHORT);
        toast2 = toast2.makeText(this,"已經是最后一關了!",toast2.LENGTH_SHORT);

        mGameView = (GameView) findViewById(R.id.game_board);
        mGameView.setText(selected_level);

        Button mBtnPrvLevel = (Button) findViewById(R.id.btn_prv_level);

         GameLevels.OriginalLevels.size();
        mBtnPrvLevel.setOnClickListener(
                new View.OnClickListener(){
                    @Override
                    public void onClick(View view) {
                        if (selected_level == 1) {
                            toast1.show();
                        } else {
                            mGameView.goto_level(--selected_level);
                            mGameView.setText(selected_level);

                        }
                    }
                }
        );
        Button mBtnNextLevel = (Button) findViewById(R.id.btn_next_level);
        mBtnNextLevel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (selected_level == GameLevels.OriginalLevels.size()){
                    toast2.show();
                } else {
                    mGameView.goto_level(++selected_level);
                    mGameView.setText(selected_level);
                }
            }
        });

        Button mBtnReset = (Button) findViewById(R.id.btn_reset);
        mBtnReset.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mGameView.goto_level(selected_level);
                mGameView.setText(selected_level);
            }
        });

        Button btnExitGame = (Button) findViewById(R.id.btn_exit);
        btnExitGame.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                finish();
            }
        });
        
    }

    public GameState getCurrentState(){
        return mCurrentState;
    }
    public void setmCurrentState(int level){
        String[] test = GameLevels.getLevel(level);
        mCurrentState = new GameState(GameLevels.getLevel(level));
    }
}
  • 構建act_game_activity布局
  • GameState mCurrentState用來獲取所選關卡的字符串數組
  • GameView mGameView用於實例化自定義的View類,來調用其中的方法
  • 創建toast信息
  • 對四個Button建立監聽器,實現上下關切換和重置退出功能。

核心代碼分析

核心代碼都在GameView中,包含游戲界面的繪制和游戲操作的邏輯判斷。

public class GameView extends View {
    private SoundPool mSoundPool;
    private final int mSoundOneStep;
    private float mCellWidth;
    public static final int CELL_NUM_PER_LINE = 12;
    private float mVolume = (float) 0.5;
    private int mManRow;
    private int mManColumn;
    private int mBoxRow;
    private int mBoxColumn;
    public String text;
    private List<TCell> mFlagCells = new ArrayList<>();
    private GameActivity mGameActivity;

    public GameView(Context context, AttributeSet attrs) {
        super(context,attrs);
        mGameActivity = (GameActivity) context;
        mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
        mSoundOneStep = mSoundPool.load(mGameActivity, R.raw.onestep, 1);
       /* get_gongren_chushi_weizhi();
        get_XiangZi_ChuShi_WeiZhi();*/
        get_flag_weizhi();
        GameBitmaps.loadGameBitmaps(getResources());
    }
  • onDraw用來繪制游戲界面,首先通過兩個for循環繪制了一個12 x 12網格,再調用drawGameBoard繪制游戲板塊,即根據StringBuffer信息繪制圖片(使用switch case),再調用drawtt繪制一句話顯示當前關卡,當箱子都到達旗幟時,即B的case數為零,調用toast.show( )。
    //當GameView實例的尺寸發生變化,就會調用onSizeChanged
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCellWidth = w / CELL_NUM_PER_LINE;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //背景色
        Paint background = new Paint();
        background.setColor(getResources().getColor(R.color.ivory));
        canvas.drawRect(0, 0, getWidth(), getHeight(), background);

        //繪制游戲區域
        Paint linePaint = new Paint();
        linePaint.setColor(Color.BLACK);
        for (int r = 0; r <= CELL_NUM_PER_LINE; r++)
            canvas.drawLine(0, r * mCellWidth, getWidth(), r * mCellWidth, linePaint);
        for (int c = 0; c <= CELL_NUM_PER_LINE; c++)
            canvas.drawLine(c * mCellWidth, 0, c * mCellWidth, CELL_NUM_PER_LINE * mCellWidth, linePaint);

        drawGameBoard(canvas);
        drawtt(canvas);

    }

    private void drawGameBoard(Canvas canvas){
        int count =0;
        Rect srcRect;
        Rect destRect;
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        for (TCell tCell: mFlagCells) {
            if (labelInCells[tCell.row].charAt(tCell.column) == 'B')
                labelInCells[tCell.row].setCharAt(tCell.column,'R');
            srcRect = new Rect(0, 0, GameBitmaps.flagBitmap.getWidth(), GameBitmaps.flagBitmap.getHeight());
            destRect=getRect(tCell.row, tCell.column);
            canvas.drawBitmap(GameBitmaps.flagBitmap, srcRect, destRect, null);
        }

        for (int r=0;r<labelInCells.length;r++)
            for (int c=0;c<labelInCells[r].length();c++){
                destRect=getRect(r,c);
                switch (labelInCells[r].charAt(c)){
                    case 'W':
                        srcRect = new Rect(0,0,GameBitmaps.wallBitmap.getWidth(),GameBitmaps.wallBitmap.getHeight());
                        canvas.drawBitmap(GameBitmaps.wallBitmap, srcRect, destRect, null);
                        break;
                    case 'B':
                        srcRect = new Rect(0, 0, GameBitmaps.boxBitmap.getWidth(), GameBitmaps.boxBitmap.getHeight());
                        canvas.drawBitmap(GameBitmaps.boxBitmap, srcRect, destRect, null);
                        mBoxRow = r;mBoxColumn = c;
                        count++;
                        break;
                    case 'F':
                        srcRect = new Rect(0, 0, GameBitmaps.flagBitmap.getWidth(), GameBitmaps.flagBitmap.getHeight());
                        canvas.drawBitmap(GameBitmaps.flagBitmap, srcRect, destRect, null);
                        break;
                    case 'M':
                        srcRect = new Rect(0, 0, GameBitmaps.manBitmap.getWidth(), GameBitmaps.manBitmap.getHeight());
                        canvas.drawBitmap(GameBitmaps.manBitmap, srcRect, destRect, null);
                        mManRow = r; mManColumn = c;
                        break;
                    case 'R':
                        srcRect = new Rect(0, 0, GameBitmaps.boxBitmap.getWidth(), GameBitmaps.boxBitmap.getHeight());
                        canvas.drawBitmap(GameBitmaps.boxBitmap, srcRect, destRect, null);
                        break;
                }
            }
        if (count == 0) toast.show();



    }

    protected void drawtt(Canvas canvas) {
        Paint mPaint = new Paint();
        mPaint.setStrokeWidth(3);
        mPaint.setTextSize(100);
        mPaint.setColor(Color.BLUE);
        mPaint.setTextAlign(Paint.Align.LEFT);
        Rect bounds = new Rect();
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
        //int baseline = (getMeasuredHeight() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
        canvas.drawText(text,300, 1700, mPaint);
    }
    
    public void setText(int level){
        this.text = "當前的關卡為第"+level+"關";
    }
    private Rect getRect(int row, int column) {
        int left = (int)(column * mCellWidth);
        int top = (int) (row * mCellWidth);
        int right = (int)((column + 1) * mCellWidth);
        int bottom = (int)((row + 1) * mCellWidth);
        return new Rect(left, top, right, bottom);
    }

  • touch_blow_to_man 等四個方法:判斷觸摸點與小人的位置關系

    private boolean touch_blow_to_man(int touch_x, int touch_y, int manRow, int manColumn) {
        int belowRow = manRow + 1;
        Rect belowRect = getRect(belowRow, manColumn);
        return belowRect.contains(touch_x, touch_y);
    }

    private boolean touch_right_to_man(int touch_x, int touch_y, int manRow, int manColumn) {
        int rightColumn = manColumn + 1;
        Rect rightRect = getRect(manRow, rightColumn);
        return rightRect.contains(touch_x, touch_y);
    }

    private boolean touch_up_to_man(int touch_x, int touch_y, int manRow, int manColumn) {
        int upRow = manRow - 1;
        Rect upRect = getRect(upRow, manColumn);
        return upRect.contains(touch_x, touch_y);
    }

    private boolean touch_left_to_man(int touch_x, int touch_y, int manRow, int manColumn) {
        int leftColumn = manColumn - 1;
        Rect leftRect = getRect(manRow, leftColumn);
        return leftRect.contains(touch_x, touch_y);
    }
  • isBoxBlowMan等四個方法:判斷箱子位置與小人的關系
    private boolean isBoxBlowMan(){
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        if (labelInCells[mManRow+1].charAt(mManColumn) == 'B' || labelInCells[mManRow+1].charAt(mManColumn) == 'R'){
            mBoxRow = mManRow + 1;
            mBoxColumn = mManColumn;
            return true;
        }else return false;
    }
    private boolean isBoxUpMan(){
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        if (labelInCells[mManRow-1].charAt(mManColumn) == 'B' || labelInCells[mManRow-1].charAt(mManColumn) == 'R'){
            mBoxRow = mManRow - 1;
            mBoxColumn = mManColumn;
            return true;
        }else return false;
    }
    private boolean isBoxLeftMan(){
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        if (labelInCells[mManRow].charAt(mManColumn-1) == 'B' || labelInCells[mManRow].charAt(mManColumn-1) == 'R'){
            mBoxRow = mManRow;
            mBoxColumn = mManColumn-1;
            return true;
        }else return false;
    }
    private boolean isBoxRightMan(){
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        if (labelInCells[mManRow].charAt(mManColumn+1) == 'B' || labelInCells[mManRow].charAt(mManColumn+1) == 'R'){
            mBoxRow = mManRow;
            mBoxColumn = mManColumn+1;
            return true;
        }else return false;
    }
  • OnTouchEvent為觸摸監聽器,回調函數包含了邏輯判斷:如是否撞牆、修改數組和相關屬性記錄小人和箱子移動后的位置變化。每調用一次回調函數都要使當前界面無效,重新繪制界面。

    public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() != MotionEvent.ACTION_DOWN)
    return true;

      int touch_x = (int) event.getX();   //觸摸點的x坐標
      int touch_y = (int) event.getY();   //觸摸點的y坐標
    
      if (touch_up_to_man(touch_x, touch_y, mManRow, mManColumn)) {
          StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
          if (isBoxUpMan()){
              if (mBoxRow - 1 >= 0 && labelInCells[mBoxRow - 1].charAt(mBoxColumn) != 'W' && labelInCells[mBoxRow-1].charAt(mBoxColumn) != 'B') {
                  labelInCells[mBoxRow].setCharAt(mBoxColumn,'M');
                  labelInCells[mManRow].setCharAt(mManColumn,' ');
                  labelInCells[mBoxRow-1].setCharAt(mBoxColumn,'B');
                  mBoxRow--;
                  mManRow--;
                  mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
              }
          }else if (mManRow - 1 >= 0 && labelInCells[mManRow - 1].charAt(mManColumn) != 'W') {
              labelInCells[mManRow].setCharAt(mManColumn,' ');
              labelInCells[mManRow-1].setCharAt(mManColumn,'M');
              mManRow--;
              mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
          }
      }
    
          if (touch_blow_to_man(touch_x, touch_y, mManRow, mManColumn)) {
              StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
              if (isBoxBlowMan()) {
                  if (mBoxRow + 1 < CELL_NUM_PER_LINE && labelInCells[mBoxRow + 1].charAt(mBoxColumn) != 'W' && labelInCells[mBoxRow+1].charAt(mBoxColumn) != 'B') {
                      labelInCells[mBoxRow].setCharAt(mBoxColumn,'M');
                      labelInCells[mManRow].setCharAt(mManColumn,' ');
                      labelInCells[mBoxRow+1].setCharAt(mBoxColumn,'B');
                      mBoxRow++;
                      mManRow++;
                      mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
                  }
              }else if (mManRow + 1 < CELL_NUM_PER_LINE && labelInCells[mManRow + 1].charAt(mManColumn) != 'W'){
                  labelInCells[mManRow].setCharAt(mManColumn,' ');
                  labelInCells[mManRow+1].setCharAt(mManColumn,'M');
                  mManRow++;
                  mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
              }
          }
    
          if (touch_right_to_man(touch_x, touch_y, mManRow, mManColumn)){
              StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
              if (isBoxRightMan()){
                  if (mBoxColumn + 1 < CELL_NUM_PER_LINE && labelInCells[mBoxRow].charAt(mBoxColumn + 1) != 'W' && labelInCells[mBoxRow].charAt(mBoxColumn + 1) != 'B') {
                      labelInCells[mBoxRow].setCharAt(mBoxColumn,'M');
                      labelInCells[mManRow].setCharAt(mManColumn,' ');
                      labelInCells[mBoxRow].setCharAt(mBoxColumn+1,'B');
                      mBoxColumn++;
                      mManColumn++;
                      mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
                  }
              }else if (mManColumn + 1 < CELL_NUM_PER_LINE && labelInCells[mManRow].charAt(mManColumn + 1) != 'W'){
                  labelInCells[mManRow].setCharAt(mManColumn,' ');
                  labelInCells[mManRow].setCharAt(mManColumn+1,'M');
                  mManColumn++;
                  mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
              }
          }
    
          if (touch_left_to_man(touch_x, touch_y, mManRow, mManColumn)){
              StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
              if (isBoxLeftMan()){
                  if (mBoxColumn -1 >= 0 && labelInCells[mBoxRow].charAt(mBoxColumn - 1) != 'W' && labelInCells[mBoxRow].charAt(mBoxColumn - 1) != 'B'){
                      labelInCells[mBoxRow].setCharAt(mBoxColumn,'M');
                      labelInCells[mManRow].setCharAt(mManColumn,' ');
                      labelInCells[mBoxRow].setCharAt(mBoxColumn-1,'B');
                      mBoxColumn--;
                      mManColumn--;
                      mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
                  }
              }else if (mManColumn - 1 >= 0 && labelInCells[mManRow].charAt(mManColumn - 1) != 'W') {
                  labelInCells[mManRow].setCharAt(mManColumn,' ');
                  labelInCells[mManRow].setCharAt(mManColumn-1,'M');
                  mManColumn--;
                  mSoundPool.play(mSoundOneStep, mVolume, mVolume, 1, 0, 1f);
              }
          }
    
      postInvalidate();//使界面失效 引發onDraw方法執行
      return true;
    

    }

以下兩個方法用來獲取旗幟的位置,和進入另一個關卡。

    public void get_flag_weizhi(){
        mFlagCells.clear();
        StringBuffer[] labelInCells = mGameActivity.getCurrentState().getLabelInCells();
        for (int r = 0; r < GameView.CELL_NUM_PER_LINE; r++)
            for (int c = 0; c< GameView.CELL_NUM_PER_LINE;c++){
                if (labelInCells[r].charAt(c) == 'F'){
                    TCell tCell = new TCell();
                    tCell.row=r;
                    tCell.column=c;
                    mFlagCells.add(tCell);
                }
            }
    } 
    public void goto_level(int level){
        GameActivity activity = new GameActivity();
        activity.setmCurrentState(level);
        get_flag_weizhi();
        postInvalidate();
    }
}

GameLevels中的關卡信息表示

   public static final int DEFAULT_ROW_NUM = 12;
   public static final int DEFAULT_COLUMN_NUM = 12;
   //游戲區單元格放了什么
   public static final char NOTHING = ' ';         //該單元格啥也沒有
   public static final char BOX = 'B';             //該單元格放的是箱子
   public static final char FLAG = 'F';            //紅旗,表示箱子的目的地
   public static final char MAN = 'M';              //搬運工
   public static final char WALL = 'W';             //牆
   public static final char MAN_FLAG = 'R';        //搬運工 + 紅旗
   public static final char BOX_FLAG = 'X';        //箱子 + 紅旗

   public static final String [] LEVEL_1 = {
           "WWWWWWWWWWWW",
           "W         FW",
           "W          W",
           "W          W",
           "W WWWWWWWW W",
           "W          W",
           "W    B     W",
           "W    M     W",
           "W          W",
           "W          W",
           "W          W",
           "WWWWWWWWWWWW"
   };

自己實現的功能分析

整個項目功能都是我實現的,講一下遇到的問題。

  • 自定義的View類與layout中xml文件的結合,最后成功解決了。
  • 多個箱子的實現:因為箱子的個數是不確定的,不可能給每個箱子都定義兩個屬性來表示位置,但發現每次小人只能推一個箱子,所以在判斷小人旁有箱子的時候,將位置屬性附給該箱子。
  • 判斷是否勝利,提前用TCell數組獲取旗幟的位置信息,提前將旗幟繪制出來,判斷該位置上是否有箱子。
  • StringBuffer的使用。使用StringBuffer可以對字符串的單個字符進行操作,在切換關卡的時候,使用“=”將StringBuffer指向新關卡的字符串的地址,再重新繪制界面。
  • 位置判斷遇到了許多少考慮的情況,在調試中不斷修改。

總結

該項目整體比較簡單,功能也比較小,寫下來也幫助我熟悉了Android開發,紙上談兵終覺淺,本項目讓我對書上一些布局、監聽器等章節的內容有了深入的理解,也熟悉了Java中的一些細節知識,例如StringBuffer,如果再有一些時間,可以將這個小游戲做的更好更美觀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM