推箱子小游戏——代码分析

代码组成

本项目主要分类三个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,如果再有一些时间,可以将这个小游戏做的更好更美观。

原文地址:https://www.cnblogs.com/20189210mujian/p/10878319.html