《Java游戏编程原理与实践教程》读书笔记(第5章——推箱子游戏)

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java游戏编程原理与实践教程》读书笔记(第5章——推箱子游戏)相关的知识,希望对你有一定的参考价值。

第5章 推箱子游戏

5.1 推箱子游戏介绍

运行游戏载人相应的地图,屏幕中出现一名推箱子的工人,其周围是围墙、人可以走的通道、几个可以移动的箱子盛和箱子放置的目的地。玩家通过按上下左右键控制工人推箱子,当所有箱子都推到了目的地后出现过关信息,并显示下一关。如果推错了,玩家通过单击鼠标右键可以撤销上次的移动操作,还可以按空格键重新玩这关,直到通过全部关卡。

游戏中用到的图片资源如下:

每张图片所表示的含义如下:

5.2 程序设计的思路

每一关的地图数据很关键,决定了每关的场景和物体位置。

把地图想象成一个网格,在Java中用一个二维数组来表示每个网格的内容,每个格子就是角色每次移动的步长(这里是30像素),也是箱子移动的举例。设计一个mapRowXmapColumn的二维数组map,格子的(x,y)的像素坐标,可以由二维数组下标(i,j)计算得到:

x=leftX+j*30;
y=leftY+i*30;
// 其中leftX和leftY表示屏幕左上角的起始坐标,可以是(0,0),但建议不要从(0,0)开始
// j表示列;i表示行,所以和x、y对应

在程序中很多参数都能常量来表示,所以存储原始地图中格子的状态值采用相对应的整数来表示。

    /* 常量,即游戏中的资源 */
    private final static int WALL = 1;// 墙
    private final static int BOX = 2;// 箱子
    private final static int BOX_ON_END = 3;// 放到目的地的箱子
    private final static int END = 4;// 目的地
    private final static int MAN_DOWN = 5;// 向下的人
    private final static int MAN_LEFT = 6;// 向左的人
    private final static int MAN_RIGHT = 7;// 向右的人
    private final static int MAN_UP = 8;// 向上的人
    private final static int GRASS = 9;// 通道
    private final static int MAN_DOWN_ON_END = 10;// 站在目的地向下的人
    private final static int MAN_LEFT_ON_END = 11;// 站在目的地向左的人
    private final static int MAN_RIGHT_ON_END = 12;// 站在目的地向右的人
    private final static int MAN_UP_ON_END = 13;// 站在目的地向上的人
    private final static int MOVE_PIXEL = 30;// 表示每次移动30像素

在玩家通关键盘控制工人推箱子的过程中,需要根据游戏规则判断是否响应按键指示,也就是说我们在写代码之前需要分析角色在移动过程中会遇到什么情况,来进行分情况处理。

下面以角色向右移动为例,来分析情况,而其他方向的原理都是一致的。如下(P1和P2分别代表角色移动趋势方向的两个方格):

  • 第一种情况:P1是墙(WALL),则角色不能向前移动,那么也就没有必要判断P2了。

if(P1==WALL)
{
    return;// 退出规则判断,布局不作任何改变
}
  • 第二种情况:P1是通道(GRASS)或终点(END),那么角色可以移动到P1位置,也没有判断P2的必要了。

if(P1==GRASS||P1==END){
    角色前进到P1格子;
    修改P1格子的状态值;
    修改角色原来位置的状态值;
}
  • 第三种情况:P1是箱子,那么就必须判断P2的情况了,因为只有确定P2的情况才能判断角色是否能够移动,此时有如下几种可能:

(1)P1处为箱子(BOX)或放到目的地的箱子(BOX_ON_END),P2处为通道(GRASS),那么角色可以推动箱子移动一步。

if(P1==BOX||P1==BOX_ON_END){
    if(P2==GRASS){
        角色前进到P1格子;
        原P1的箱子前进到P2格子;
        修改相关位置格子的状态值;
    }
}

(2)P1处为箱子(BOX)或放到目的地的箱子(BOX_ON_END),P2处为终点(END),角色也能推动箱子移动一步。

if(P1==BOX||P1==BOX_ON_END){
    if(P2==GRASS){
        角色前进到P1格子;
        P2格子状态为到达目的地的箱子
        修改相关位置格子的状态值;
    }
}

(3)P1处为箱子(BOX)或放到目的地的箱子(BOX_ON_END),P2处为墙(WALL),那么角色不能移动。

if(P1==BOX||P1==BOX_ON_END){
    if(P2==WALL){
        return;// 退出规则判断,布局不作任何改变
    }
}

整个游戏的源文件夹如下:

  • images:图片资源目录
  • sounds:音乐资源目录
  • GameFrame.java:游戏主界面
  • MainApp.java:启动主程序
  • Map.java:封装游戏当前状态
  • MapFactory.java:提供游戏的地图数据
  • SoundPlayerUtil:音乐播放方法

5.3 程序设计的步骤

5.3.1 设计地图数据类(MapFactory.java)

地图数据类MapFactory中保存了所有关卡的原始数据,每关数据为一个二维数组,因此此处的map是一个三维数组,保存所有关卡数据。

/**
 * @author lcl100
 * @create 2021-06-10 16:20
 * 地图数据类,保存所有关卡的原始地图数据,每关数据是一个二维数组,因此map是一个三维数组
 */
public class MapFactory {
    static byte map[][][] = {
            // 第一关
            {
                    {0, 0, 1, 1, 1, 0, 0, 0},
                    {0, 0, 1, 4, 1, 0, 0, 0},
                    {0, 0, 1, 9, 1, 1, 1, 1},
                    {1, 1, 1, 2, 9, 2, 4, 1},
                    {1, 4, 9, 2, 5, 1, 1, 1},
                    {1, 1, 1, 1, 2, 1, 0, 0},
                    {0, 0, 0, 1, 4, 1, 0, 0},
                    {0, 0, 0, 1, 1, 1, 0, 0}
            },
            // 第二关
            {
                    {1, 1, 1, 1, 1, 0, 0, 0, 0},
                    {1, 9, 9, 5, 1, 0, 0, 0, 0},
                    {1, 9, 2, 2, 1, 0, 1, 1, 1},
                    {1, 9, 2, 9, 1, 1, 1, 4, 1},
                    {1, 1, 1, 9, 1, 1, 1, 4, 1},
                    {0, 1, 1, 9, 9, 9, 9, 4, 1},
                    {0, 1, 9, 9, 9, 1, 9, 9, 1},
                    {0, 1, 9, 9, 9, 1, 1, 1, 1},
                    {0, 1, 1, 1, 1, 1, 0, 0, 0}
            }
            // 其他关卡数据
    };
    static int count = map.length;// 总的关卡数

    /**
     * 获取指定关卡的地图数据
     *
     * @param grade 指定关卡,从1开始传入
     * @return 返回该关卡的地图数据,就是一个二维数组
     */
    public static byte[][] getMap(int grade) {
        byte temp[][];// 保存关卡数据
        // 对传入的关卡数进行参数校验,不能是没有的关卡
        if (grade > 0 && grade <= count) {
            temp = map[grade - 1];// 获取指定关卡数据从map数组中,由于关卡从1开始,所以下标需要减去1
        } else {
            temp = map[0];// 如果传入的参数不合法,则显示第一关的数据
        }
//        return temp;
        // 获取已经获得关卡的行数和列数
        int row = temp.length;
        int column = temp[0].length;
        // 创建结果数组,将temp数组中的数据复制到result中返回
        byte[][] result = new byte[row][column];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < column; j++) {
                result[i][j] = temp[i][j];
            }
        }
        return result;
    }

    /**
     * 返回关卡总数
     *
     * @return
     */
    public static int getCount() {
        return count;
    }
}

 5.3.2 设计地图类(Map.java)

由于每移动一步,需要保存当前的游戏状态(当前角色所站的位置和地图数据),所以此处定义此地图类,保存人的位置和游戏地图的当前状态。撤销移动时,恢复地图的操作通过此类获取需要的人的位置、地图的当前状态和关卡数来实现。

/**
 * @author lcl100
 * @create 2021-06-10 16:29
 * 由于每移动一步,需要保存当前的游戏状态,所以此处定义此地图类,保存人的位置和游戏
 * 地图的当前状态。撤销移动时,恢复地图的操作通过此类获取需要的人的位置、地图的当前状态和关卡数来实现。
 */
public class Map {
    // 当前角色的x和y坐标
    int manX = 0, manY = 0;
    // 当前的地图数据
    byte[][] map;
    // 当前的关卡数
    int grade;

    /**
     * 此构造方法用于撤销操作,撤销操作只需要人的位置和地图的当前状态
     *
     * @param manX
     * @param manY
     * @param map
     */
    public Map(int manX, int manY, byte[][] map) {
        this.manX = manX;
        this.manY = manY;
//        this.map = map;// bug代码,不要使用
        // 将map数据直接复制到temp中,然后赋值给this.map
        int row = map.length;
        int column = map[0].length;
        byte[][] temp = new byte[row][column];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < column; j++) {
                temp[i][j] = map[i][j];
            }
        }
        this.map = temp;
    }

    /**
     * 此构造方法用于保存操作,恢复地图时需要人的位置、地图的当前状态和关卡数
     *
     * @param manX
     * @param manY
     * @param map
     * @param grade
     */
    public Map(int manX, int manY, byte[][] map, int grade) {
        this(manX, manY, map);
        this.grade = grade;
    }

    public int getManX() {
        return manX;
    }

    public void setManX(int manX) {
        this.manX = manX;
    }

    public int getManY() {
        return manY;
    }

    public void setManY(int manY) {
        this.manY = manY;
    }

    public byte[][] getMap() {
        return map;
    }

    public void setMap(byte[][] map) {
        this.map = map;
    }

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }
}

5.3.3 设计游戏面板类(GameFrame.java)

游戏面板类完成游戏的界面刷新显示,以及相应鼠标键盘的相关事件。

/**
 * @author lcl100
 * @create 2021-06-10 16:37
 * 游戏面板类完成游戏的界面刷新显示,以及相应鼠标键盘的相关事件
 */
public class GameFrame extends JFrame implements
        ActionListener, MouseListener, KeyListener {// 实现动作事件监听器、鼠标事件监听器、键盘事件监听器

    // 当前的关卡数,默认为第一关,从1开始计数
    private int grade = 1;
    // row,column记载人的位置,分别表示二维数组中的行号和列号,即map[row][column]确定人的位置
    private int row = 7, column = 7;
    // leftX,leftY记载左上角图片的位置,避免图片从(0,0)坐标开始,因为是图片填充,从(0,0)开始不行
    private int leftX = 50, leftY = 50;
    // 记载地图的总共有多少行、多少列
    private int mapRow = 0, mapColumn = 0;
    // 记载屏幕窗口的宽度和高度
    private int width = 0, height = 0;
    private boolean acceptKey = true;
    // 程序所需要用到的图片
    private Image pics[] = null;// 图片数据
    private byte[][] map = null;// 地图数据
    private ArrayList list = new ArrayList();
    private SoundPlayerUtil soundPlayer;// 播放声音工具类

    /* 常量,即游戏中的资源 */
    private final static int WALL = 1;// 墙
    private final static int BOX = 2;// 箱子
    private final static int BOX_ON_END = 3;// 放到目的地的箱子
    private final static int END = 4;// 目的地
    private final static int MAN_DOWN = 5;// 向下的人
    private final static int MAN_LEFT = 6;// 向左的人
    private final static int MAN_RIGHT = 7;// 向右的人
    private final static int MAN_UP = 8;// 向上的人
    private final static int GRASS = 9;// 通道
    private final static int MAN_DOWN_ON_END = 10;// 站在目的地向下的人
    private final static int MAN_LEFT_ON_END = 11;// 站在目的地向左的人
    private final static int MAN_RIGHT_ON_END = 12;// 站在目的地向右的人
    private final static int MAN_UP_ON_END = 13;// 站在目的地向上的人
    private final static int MOVE_PIXEL = 30;// 表示每次移动30像素

    /**
     * 在构造方法GameFrame0中,调用initMap()法来初始化本关grade游戏地图,清空悔棋信
     * 息列表list,同时播放MIDI背景音乐。
     */
    public GameFrame() {
        // 游戏窗口的一些基本设置
        setTitle("推箱子游戏");
        setSize(600, 600);
        setVisible(true);
        setLocation(300, 20);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container contentPane = getContentPane();
        contentPane.setLayout(null);
        contentPane.setBackground(Color.black);
        // 其他设置,初始化窗口的宽度和高度赋值给width和height
        this.width = getWidth();
        this.height = getHeight();
        // 初始化图片资源
        getPics();
        // 初始化地图数据
        initMap();
        // 注册事件监听器
        setFocusable(true);
        addKeyListener(this);
        addMouseListener(this);
        // 播放音乐
        initSound();
    }

    /**
     * initMap()方法的作用是初始化本关grade 游戏地图,清空悔棋信息列表list。 调用
     * getMapSizeAndPosition(方法获取游戏区域大小及显示游戏的左上角位置( leftX, leftY )。
     */
    public void initMap() {
        // 获取当前关卡的地图数据
        map = MapFactory.getMap(grade);
        // 清除上一关保存的回退地图数据,即清空list集合的内容
        list.clear();
        // 初始化地图行列数和左上角起始坐标位置
        getMapSizeAndPosition();
        // 获取角色的坐标位置
        getManPosition();
    }

    /**
     * getManPosition()方法的作用是获取工人的当前位置(row,column)。
     */
    public void getManPosition() {
        // 即遍历地图数组map中存在那个值等于MANXXX(MAN_DOWN表示向下的人;MAN_UP表示向上的人)的情况,即表示该位置是人站立的位置,这个由地图数据扫描得出
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                if (map[i][j] == MAN_DOWN || map[i][j] == MAN_DOWN_ON_END
                        || map[i][j] == MAN_UP || map[i][j] == MAN_UP_ON_END
                        || map[i][j] == MAN_LEFT || map[i][j] == MAN_LEFT_ON_END
                        || map[i][j] == MAN_RIGHT || map[i][j] == MAN_RIGHT) {
                    // 保存人的位置,i表示第几行,j表示第几列,而且是从0开始的
                    this.row = i;
                    this.column = j;
                    break;
                }
            }
        }
    }

    /**
     * getMapSizeAndPosition()方法用来获取游戏区域大小及显示游戏的左上角位置( lefX, leftY )。
     */
    private void getMapSizeAndPosition() {
        // 初始化mapRow和mapColumn,表示地图的行列数
        this.mapRow = map.length;
        this.mapColumn = map[0].length;
        // 初始化leftX和leftY,即计算左上角的位置,
        this.leftX = (width - map[0].length * MOVE_PIXEL) / 2;
        this.leftY = (height - map.length * MOVE_PIXEL) / 2;
    }

    /**
     * getPics()方法用来加载要显示的图片
     */
    public void getPics() {
        // 创建长度为13的数组,即有十三张图片
        pics = new Image[13];
        // 然后循环将每张图片读取保存到pics数组中
        for (int i = 0; i < 13; i++) {
            pics[i] = Toolkit.getDefaultToolkit().getImage("src\\\\images\\\\pic_" + (i + 1) + ".png");
        }
    }

    /**
     * 初始化播放的音乐
     */
    public void initSound() {
        // 调用SoundPlayerUtil类中的方法播放音乐
        soundPlayer = new SoundPlayerUtil();
        soundPlayer.loadSound("src\\\\sounds\\\\music.wav");
        soundPlayer.playSound(true);// 循环播放
    }

    /**
     * grassOrEnd()方法判断人所在的位置是通道GRASS还是目的地END
     *
     * @param man
     * @return
     */
    public byte grassOrEnd(byte man) {
        if (man == MAN_DOWN_ON_END || man == MAN_LEFT_ON_END || man == MAN_RIGHT_ON_END || man == MAN_UP_ON_END) {
            return END;
        }
        return GRASS;
    }

    /**
     * 人物向上移动
     */
    private void moveUp() {
        // 如果上一位是WALL,则不能移动
        if (map[row - 1][column] == WALL) {
            return;
        }
        // 如果上一位是BOX或BOX_ON_END,需要考虑上一位的上一位是什么情况
        if (map[row - 1][column] == BOX || map[row - 1][column] == BOX_ON_END) {
            // 那么就需要考虑上一位的上一位情况,若上上一位是END或GRASS,则向上一步,其他情况不用处理
            if (map[row - 2][column] == END || map[row - 2][column] == GRASS) {
                // 要保留当前信息,以便回退上一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte boxTemp = (byte) ((byte) map[row - 2][column] == END ? BOX_ON_END : BOX);
                byte manTemp = (byte) (map[row - 1][column] == BOX ? MAN_UP : MAN_UP_ON_END);
                // 箱子变成temp,箱子往前移动一步
                map[row - 2][column] = boxTemp;
                // 人变成MAN_UP,往上走一步
                map[row - 1][column] = manTemp;
                // 将人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                row--;
            }
        } else {
            // 上一位为GRASS或END,无需考虑上上一步,其他情况不用处理
            if (map[row - 1][column] == GRASS || map[row - 1][column] == END) {
                // 保留当前这一步的信息,以便回退上一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte temp = (byte) (map[row - 1][column] == END ? MAN_UP_ON_END : MAN_UP);
                // 人变成temp,人往上走一步
                map[row - 1][column] = temp;
                // 人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                row--;
            }
        }
    }

    /**
     * 人物向下移动,其中(row,column)是当前角色站的位置,而map[row,column]表示当前角色
     */
    private void moveDown() {
        // 如果下一位是WALL,则不能移动
        // 所以map[row+1][column]表示当前角色的下一步,也就是下一关图像块
        if (map[row + 1][column] == WALL) {
            return;
        }
        // 如果下一位是箱子BOX或放到目的地的箱子BOX_ON_END(即如果下一位是箱子,而不管是什么类型的箱子,都可以推动箱子),需要考虑下位的下一位是什么情况
        if (map[row + 1][column] == BOX || map[row + 1][column] == BOX_ON_END) {
            // 那么就需要考虑下一位的下一位情况(即箱子的下一位是什么,决定着箱子是否可以向前移动),若下下一位是目的地END或通道GRASS,则表示箱子可以向下移动一步,其他情况不用处理
            // map[row+2][column]表示箱子的下一步是什么
            if (map[row + 2][column] == END || map[row + 2][column] == GRASS) {
                // 下面的代码就是箱子向前移动一步,人移动原来箱子的位置
                // 要保留当前人和地图信息,以便回退下一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                // 判断箱子的下一步是否是目的地,如果是目的地,那么箱子的下一步就应该变成BOX_ON_END(放在目的地的箱子),如果不是目的地那应该还只是普通箱子BOX
                byte boxTemp = (byte) ((byte) map[row + 2][column] == END ? BOX_ON_END : BOX);
                // 判断人的下一步是否是箱子
                byte manTemp = (byte) (map[row + 1][column] == BOX ? MAN_DOWN : MAN_DOWN_ON_END);
                // 箱子变成temp,箱子往下移动一步
                map[row + 2][column] = boxTemp;
                // 人变成MAN_UP,往下走一步
                map[row + 1][column] = manTemp;
                // 将人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                row++;
            }
        } else {
            // 执行到这里,表示人的下一步不是箱子,那么要么是通道要么是终点
            // 下一位为GRASS或END,无需考虑下下一步,其他情况不用处理
            if (map[row + 1][column] == GRASS || map[row + 1][column] == END) {
                // 保留当前这一步的信息,以便回退下一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte temp = (byte) (map[row + 1][column] == END ? MAN_DOWN_ON_END : MAN_DOWN);
                // 人变成temp,人往下走一步
                map[row + 1][column] = temp;
                // 人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                row++;
            }
        }
    }

    /**
     * 人物向左移动
     */
    private void moveLeft() {
        // 如果左一位是WALL,则不能移动
        if (map[row][column - 1] == WALL) {
            return;
        }
        // 如果左一位是BOX或BOX_ON_END,需要考虑左一位的左一位是什么情况
        if (map[row][column - 1] == BOX || map[row][column - 1] == BOX_ON_END) {
            // 那么就需要考虑左一位的左一位情况,若左左一位是END或GRASS,则向左一步,其他情况不用处理
            if (map[row][column - 2] == END || map[row][column - 2] == GRASS) {
                // 要保留当前信息,以便回退左一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte boxTemp = (byte) ((byte) map[row][column - 2] == END ? BOX_ON_END : BOX);
                byte manTemp = (byte) (map[row][column - 1] == BOX ? MAN_LEFT : MAN_LEFT_ON_END);
                // 箱子变成temp,箱子往前移动一步
                map[row][column - 2] = boxTemp;
                // 人变成MAN_UP,往左走一步
                map[row][column - 1] = manTemp;
                // 将人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                column--;
            }
        } else {
            // 左一位为GRASS或END,无需考虑左左一步,其他情况不用处理
            if (map[row][column - 1] == GRASS || map[row][column - 1] == END) {
                // 保留当前这一步的信息,以便回退左一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte temp = (byte) (map[row][column - 1] == END ? MAN_LEFT_ON_END : MAN_LEFT);
                // 人变成temp,人往左走一步
                map[row][column - 1] = temp;
                // 人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                column--;
            }
        }
    }

    /**
     * 人物向右移动
     */
    private void moveRight() {
        // 如果右一位是WALL,则不能移动
        if (map[row][column + 1] == WALL) {
            return;
        }
        // 如果右一位是BOX或BOX_ON_END,需要考虑右位的右一位是什么情况
        if (map[row][column + 1] == BOX || map[row][column + 1] == BOX_ON_END) {
            // 那么就需要考虑右一位的右一位情况,若右右一位是END或GRASS,则向右一步,其他情况不用处理
            if (map[row][column + 2] == END || map[row][column + 2] == GRASS) {
                // 要保留当前信息,以便回退右一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte boxTemp = (byte) ((byte) map[row][column + 2] == END ? BOX_ON_END : BOX);
                byte manTemp = (byte) (map[row][column + 1] == BOX ? MAN_RIGHT : MAN_RIGHT_ON_END);
                // 箱子变成temp,箱子往右移动一步
                map[row][column + 2] = boxTemp;
                // 人变成MAN_UP,往右走一步
                map[row][column + 1] = manTemp;
                // 将人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                column++;
            }
        } else {
            // 右一位为GRASS或END,无需考虑右右一步,其他情况不用处理
            if (map[row][column + 1] == GRASS || map[row][column + 1] == END) {
                // 保留当前这一步的信息,以便回退右一步
                Map currMap = new Map(row, column, map);
                list.add(currMap);
                byte temp = (byte) (map[row][column + 1] == END ? MAN_RIGHT_ON_END : MAN_RIGHT);
                // 人变成temp,人往右走一步
                map[row][column + 1] = temp;
                // 人刚才站的地方变成GRASS或者END
                map[row][column] = grassOrEnd(map[row][column]);
                // 人离开后修改人的坐标
                column++;
            }
        }
    }

    /**
     * 验证玩家是否过关,如果有目的地END值或人直接站在目的地则没有成功
     *
     * @return 如果已经通关则返回true,否则返回false
     */
    public boolean isFinished() {
        for (int i = 0; i < mapRow; i++) {
            for (int j = 0; j < mapColumn; j++) {
                if (map[i][j] == END || map[i][j] == MAN_DOWN_ON_END || map[i][j] == MAN_UP_ON_END || map[i][j] == MAN_LEFT_ON_END || map[i][j] == MAN_RIGHT_ON_END) {
                    return false;
                }
            }
        }
        return true;
    }

    // 使用双缓冲技术解决动画闪烁问题
    private Image iBuffer;
    private Graphics gBuffer;

    /**
     * 重写绘制整个游戏区域的图形
     *
     * @param g
     */
    @Override
    public void paint(Graphics g) {
        if (iBuffer == null) {
            iBuffer = createImage(width, height);
            gBuffer = iBuffer.getGraphics();
        }
        // 清空屏幕原来的绘画
        gBuffer.setColor(getBackground());
        gBuffer.fillRect(0, 0, width, height);
        for (int i = 0; i < mapRow; i++) {
            for (int j = 0; j < mapColumn; j++) {
                // 画出地图,i表示行数,j表示列数
                if (map[i][j] != 0) {
                    // 这里要减1是因为图片的名称序号不对应,应该从0开始,但是从1开始的
                    gBuffer.drawImage(pics[map[i][j] - 1], leftX + j * MOVE_PIXEL, leftY + i * MOVE_PIXEL, 30, 30, this);
                }
            }
        }
        gBuffer.setColor(Color.RED);
        gBuffer.setFont(new Font("楷体_2312", Font.BOLD, 30));
        gBuffer.drawString("现在是第", 150, 140);
        gBuffer.drawString(String.valueOf(grade), 310, 140);
        gBuffer.drawString("关", 360, 140);
        g.drawImage(iBuffer, 0, 0, this);
        /* 下面的代码是未使用双缓冲技术会导致动画闪烁的代码 */
        /*g.clearRect(0, 0, width, height);
        for (int i = 0; i < mapRow; i++) {
            for (int j = 0; j < mapColumn; j++) {
                // 画出地图,i表示行数,j表示列数
                if (map[i][j] != 0) {
                    // 这里要减1是因为图片的名称序号不对应,应该从0开始,但是从1开始的
                    g.drawImage(pics[map[i][j] - 1], leftX + j * MOVE_PIXEL, leftY + i * MOVE_PIXEL, 30, 30, this);
                }
            }
        }
        g.setColor(Color.RED);
        g.setFont(new Font("楷体_2312", Font.BOLD, 30));
        g.drawString("现在是第", 150, 140);
        g.drawString(String.valueOf(grade), 310, 140);
        g.drawString("关", 360, 140);*/
    }

    @Override
    public void actionPerformed(ActionEvent e) {

    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    @Override
    public void keyPressed(KeyEvent e) {
        // 当按键盘上的按键时触发的事件
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:// 上方向键
                moveUp();// 向上移动
                break;
            case KeyEvent.VK_DOWN:// 下方向键
                moveDown();// 向下移动
                break;
            case KeyEvent.VK_LEFT:// 左方向键
                moveLeft();// 向左移动
                break;
            case KeyEvent.VK_RIGHT:// 右方向键
                moveRight();// 向右移动
                break;
        }
        // 然后重新绘制界面
        repaint();
        // 在移动完成后可能已经通关,所以需要判断是否通关
        if (isFinished()) {
            // 禁用按键
            acceptKey = false;
            // 判断是否是最后一关,如果是则直接提示,如果不是则询问是否要进入下一关
            if (grade == MapFactory.getCount()) {
                JOptionPane.showMessageDialog(this, "恭喜通过最后一关!");
            } else {
                // 提示进入下一关
                String msg = "恭喜通过第" + grade + "关!!!\\n是否要进入下一关?";
                int choice = JOptionPane.showConfirmDialog(null, msg, "过关", JOptionPane.YES_NO_OPTION);
                if (choice == 1) {
                    System.exit(0);
                } else if (choice == 0) {
                    // 进入下一关
                    acceptKey = true;
                    nextGrade();
                }
            }
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    @Override
    public void mouseClicked(MouseEvent e) {
        // MouseEvent.BUTTON3表示鼠标右键
        if (e.getButton() == MouseEvent.BUTTON3) {
            undo();
        }
    }

    @Override
    public void mousePressed(MouseEvent e) {

    }

    @Override
    public void mouseReleased(MouseEvent e) {

    }

    @Override
    public void mouseEntered(MouseEvent e) {

    }

    @Override
    public void mouseExited(MouseEvent e) {

    }

    /**
     * 返回当前人的位置用getManX()方法和getManY()方法
     *
     * @return
     */
    public int getManX() {
        return row;
    }

    public int getManY() {
        return column;
    }

    /**
     * 返回当前关卡数
     *
     * @return
     */
    public int getGrade() {
        return grade;
    }

    /**
     * 返回当前关卡的地图信息
     *
     * @return
     */
    public byte[][] getMap() {
        return MapFactory.getMap(grade);
    }

    /**
     * 显示提示信息对话框
     *
     * @param str
     */
    public void displayToast(String str) {
        JOptionPane.showMessageDialog(null, str, "提示", JOptionPane.ERROR_MESSAGE);
    }

    /**
     * 撤销移动操作
     */
    public void undo() {
        if (acceptKey) {
            if (list.size() > 0) {
                // 如果要撤销,必须要走过
                // 考虑用栈更合适
                Map priorMap = (Map) list.get(list.size() - 1);
                this.map = priorMap.getMap();
                this.row = priorMap.getManX();
                this.column = priorMap.getManY();
                repaint();// 重新画图
                list.remove(list.size() - 1);
            } else {
                displayToast("不能再撤销了!");
            }
        } else {
            displayToast("此关已完成,不能撤销!");
        }
    }

    /**
     * 实现下一关的初始化,并且调用repaint()方法显示游戏界面
     */
    public void nextGrade() {
        // 初始化下一关的数据
        if (grade >= MapFactory.getCount()) {
            displayToast("恭喜你完成所有关卡!");
            acceptKey = false;
        } else {
            // 关卡数加1
            grade++;
            // 初始化下一关的地图数据
            initMap();
            // 重新绘制画面
            repaint();
            acceptKey = true;
        }
    }

    /**
     * 实现上一关初始化并且调用repaint()发显示游戏界面
     */
    public void priorGrade() {
        grade--;
        acceptKey = true;
        if (grade < 0) {
            grade = 0;
        }
        initMap();
        repaint();
    }
}

在该类中最难的应该是moveUp()等移动方法,即当按了键盘的方向键后人物角色的移动逻辑,角色可以分为四个方向移动(上、下、左、右),推箱子游戏的特性是每次人物的移动会涉及到三个位置的逻辑判断处理,即人物角色的当前位置、角色要移动到的位置和箱子要到达的位置。

下面以人物向上移动为例说明情况:

向上移动涉及到的三个位置为人物当前位置(map[row][column])、角色向上一位的位置P1(map[row-1][column])和角色向上两位的位置P2(map[row-2][column])。注意,其中row表示行,也就是坐标系中的y坐标,所以向上移动是需要减1的。

首先判断P1处的图像类型,这里的图像类型可能是BOX、BOX_ON_END、WALL、GRASS或END。

(1)如果P1是墙WALL,那么角色不能移动,所以不用处理,直接返回。

(2)如果P1是BOX或BOX_ON_END,则表示可以推箱子,需要判断P2的图像类型,这里P2的图像类型可能是WALL、END或GRASS,如果是WALL则不能推动箱子不作任何处理,如果是END或GRASS,则需要进行特殊处理:

①先将当前游戏的地图信息和人物位置信息保存到List集合中,用于撤销操作。

②判断P2处是否为终点END,如果是终点则P2格子的状态为放在目的地的箱子BOX_ON_END,如果不是终点则是普通箱子。

byte boxTemp = (byte) ((byte) map[row - 2][column] == END ? BOX_ON_END : BOX);
// 箱子变成temp,箱子往前移动一步
map[row - 2][column] = boxTemp;

③在箱子移动后,角色也应该向上走一步,需要判断P1处是BOX_ON_END还是BOX,如果是BOX则P1格子的状态为MAN_UP,即人在P1的位置,否则改为MAN_UP_END,即人在目的地。

byte manTemp = (byte) (map[row - 1][column] == BOX ? MAN_UP : MAN_UP_ON_END);
// 人变成MAN_UP,往上走一步
map[row - 1][column] = manTemp;

④将人刚才站的地方变成GRASS或END

map[row][column] = grassOrEnd(map[row][column]);

⑤修改人的位置在map数组中的行坐标row,做自减操作

row--;

(3)如果P1的图像类型是GRASS或END,人可以直接移动一步,不需要判断P2类型。

①保存当前地图数据,用于撤销操作

// 保留当前这一步的信息,以便回退上一步
Map currMap = new Map(row, column, map);
list.add(currMap);

②判断P1处是否为目的地,如果是目的地则P1格子状态改为MAN_UP_ON_END,即表示人在目的地,否则P1格子状态改为MAN_UP,即人在P1位置。

byte temp = (byte) (map[row - 1][column] == END ? MAN_UP_ON_END : MAN_UP);
// 人变成temp,人往上走一步
map[row - 1][column] = temp;

③修改人的位置在map数组中的行坐标row,做自减操作

// 人刚才站的地方变成GRASS或者END
map[row][column] = grassOrEnd(map[row][column]);
// 人离开后修改人的坐标
row--;

由于会不断的重绘界面,所以用了双缓冲技术来解决闪烁问题,代码如下:

    // 使用双缓冲技术解决动画闪烁问题
    private Image iBuffer;
    private Graphics gBuffer;

    /**
     * 重写绘制整个游戏区域的图形
     *
     * @param g
     */
    @Override
    public void paint(Graphics g) {
        if (iBuffer == null) {
            iBuffer = createImage(width, height);
            gBuffer = iBuffer.getGraphics();
        }
        // 清空屏幕原来的绘画
        gBuffer.setColor(getBackground());
        gBuffer.fillRect(0, 0, width, height);
        for (int i = 0; i < mapRow; i++) {
            for (int j = 0; j < mapColumn; j++) {
                // 画出地图,i表示行数,j表示列数
                if (map[i][j] != 0) {
                    // 这里要减1是因为图片的名称序号不对应,应该从0开始,但是从1开始的
                    gBuffer.drawImage(pics[map[i][j] - 1], leftX + j * MOVE_PIXEL, leftY + i * MOVE_PIXEL, 30, 30, this);
                }
            }
        }
        gBuffer.setColor(Color.RED);
        gBuffer.setFont(new Font("楷体_2312", Font.BOLD, 30));
        gBuffer.drawString("现在是第", 150, 140);
        gBuffer.drawString(String.valueOf(grade), 310, 140);
        gBuffer.drawString("关", 360, 140);
        g.drawImage(iBuffer, 0, 0, this);
    }

5.3.4 播放音乐类(SoundPlayerUtil.java)

public class SoundPlayerUtil {
    public File file;
    public AudioInputStream stream;
    public AudioFormat format;
    DataLine.Info info;
    Clip clip;

    /**
     * 加载声音文件,支持wav、mp3等声音文件
     *
     * @param filePath 声音文件的路径
     */
    public void loadSound(String filePath) {
        file = new File(filePath);
        try {
            stream = Audiosystem.getAudioInputStream(file);
        } catch (UnsupportedAudioFileException | IOException e) {
            e.printStackTrace();
        }
        format = stream.getFormat();
    }

    /**
     * 播放音乐
     *
     * @param isLoop 表示是否循环播放音乐,如果传入的是true则表示循环播放
     */
    public void playSound(boolean isLoop) {
        info = new DataLine.Info(Clip.class, format);
        try {
            clip = (Clip) AudioSystem.getLine(info);
            clip.open(stream);
        } catch (LineUnavailableException | IOException e) {
            e.printStackTrace();
        }
        if (isLoop) {
            clip.loop(Clip.LOOP_CONTINUOUSLY);// 添加该句代码可以循环播放
        }
        clip.start();
    }

}

以上是关于《Java游戏编程原理与实践教程》读书笔记(第5章——推箱子游戏)的主要内容,如果未能解决你的问题,请参考以下文章

《Java游戏编程原理与实践教程》读书笔记(第4章——Java游戏程序的基本框架)

[读书笔记]Java编程思想

Java编程思想读书笔记--第14章类型信息

《深入理解Java虚拟机》读书笔记——第1章 走近Java

《python基础教程》第5章 条件循环和其他语句 读书笔记

《逆向工程核心原理》读书笔记——第7章 栈帧