在设计完毕主窗体后,会发现主窗体载入了一个开始面板。在开始面板上:如果用户选择“开始游戏”功能,则本游戏会跳转到游戏面板,开始闯关;如果用户选择“关卡编辑器”功能,则本游戏会跳转到关卡编辑面板,设计自定义关卡。接下来,我们将讲解用于实现“开始游戏”功能的开始面板、游戏面板,以及用于实现“关卡编辑器”功能的关卡编辑面板。
开始面板是用户启动本游戏后看到的第一个面板。开始面板中显示了本游戏的标题和功能选项,用户可以通过键盘的上下键来控制蓝色方块,进而对选项进行选择,按Enter键即可确认选项。开始面板的效果如图1.7所示。
图1.7 开始面板效果
开始面板的设计过程如下:
(1)创建一个名为StarPanel的Java类,并让它继承自JPanel面板类,同时实现KeyListener键盘事件监听接口。在实现接口的同时要实现该接口中的三个抽象方法。这些方法的代码将在后续进行补充。StarPanel类的代码如下:
(2)在类中创建属性,分别用于记录主窗体对象、面板背景图片、背景图片的绘图对象以及选择图标的可用坐标,代码如下:
(3)在构造方法中,需要传入主窗体对象,以便能够为窗体添加键盘监听、修改窗体标题功能。构造方法中调用主窗体的setFocusable(true)方法是为了防止从关卡编辑器返回开始面板造成主窗体丢失焦点的问题。构造方法在最后定义了选择图标的y坐标,让图标停留在第一个选项上。构造方法的代码如下:
(4)image是开始面板中显示的图片,paintImage()方法用于在这张图片上绘制内容,其中包括绘制背景图片、绘制选项文字和选项图标。因为选项图标的纵坐标是变量,所以每次绘制时,图标都可能出现在不同的位置上。paintImage()方法的代码如下:
(5)绘制完image图片之后,需要将该图片显示到面板上。重写paint()绘图方法可以实现此功能。paint()方法是由面板类的父类(javax.swing.JComponent组件抽象类)提供的,该方法在展示面板时会自动调用。重写的paint()方法的代码如下:
(6)开始面板实现键盘事件中的keyPressed()方法,该方法会在用户按下键盘上的任意按键时被触发。在该方法中,使用KeyEvent键盘事件对象获取按下的按键的keyCode值。如果按下的是Enter键,则根据选择图标的y坐标判断用户选中的选项,进入对应的面板中;如果按下的是上箭头键或下箭头键,则更换选择图标的y坐标。接口中剩下的keyReleased()方法和keyTyped()方法不需要实现。keyPressed()方法的代码如下:
游戏面板是本游戏的核心面板,本游戏的所有算法均在此面板中实现。用户可以在游戏面板中通过键盘的上下左右箭头按键来控制玩家角色移动,玩家在移动的同时可以推动箱子一起移动,当所有的箱子都到达目的地之后,玩家还要进入下一个关卡。如果在游戏过程中出现将箱子推入死胡同的情况,用户可以按Esc键来重新开始当前关卡。
游戏面板中的模型图片与关卡编辑器中的模型图片有些许差别:关卡编辑器中的每个图片的大小都是20×20像素,而游戏面板中每个图片的大小都是30×30像素。
游戏面板的效果如图1.8所示。
图1.8 游戏面板效果
游戏面板的设计过程如下:
(1)创建一个名为GamePanel的Java类,让它继承自JPanel面板类,并实现KeyListener键盘事件监听接口,代码如下:
public class GamePanel extends JPanel implements KeyListener { }
(2)在类中创建属性,分别用于记录主窗体对象、关卡编号、关卡中的所有元素、关卡的主图片、玩家对象和箱子列表等,代码如下:
(3)编写GamePanel类的构造方法,该方法接收两个参数:frame是主窗体对象,level是关卡编号。根据此关卡编号读取对应的关卡对象。如果关卡编号小于1,则构造方法将读取自定义关卡对象。构造方法最后为主窗体添加键盘事件监听器。构造方法代码如下:
(4)游戏面板实现键盘事件中keyPressed()方法,该方法会在用户按下键盘上的任意按键时被触发。在keyPressed()方法中,使用KeyEvent键盘事件对象来获取被按下的按键的keyCode值,同时获取玩家角色当前所处的坐标索引。坐标索引表示玩家角色在二维数组中的索引位置。使用switch判断按键keyCode值:如果用户按下的是上下左右键,则调用本类的moveThePlayer()移动角色方法,根据按键的不同,传入不同的移动目标位置索引;如果用户按下的是Esc键,则调用本类的gotoAnotherLevel()跳关方法,重新开始当前关卡。
keyPressed()方法的代码如下:
(5)moveThePlayer()方法用来移动玩家角色和箱子。该方法接收四个参数:前两个参数表示玩家移动一步后所到达的目标位置的横坐标索引和纵坐标索引,后两个参数表示箱子被玩家移动一步后所到达的目标位置的横坐标索引和纵坐标索引。前两个参数用于判断玩家的移动路线是否被挡住,后两个参数用于判断箱子的移动路线是否被挡住。这个判断逻辑如图1.9所示。
图1.9 玩家移动时触发的逻辑
因为玩家可以在四个方向上移动,所以只需通过控制这四个参数就可以指定玩家下一步要移动到的位置。moveThePlayer()方法的代码如下:
(6)在确定关卡中所有元素的位置后,接下来就要绘制关卡,image主图片绘图对象就是用来绘制关卡的。paintImage()方法用于绘制主图片,该方法首先会绘制一个白色的实心矩形来填充整张图片,这样就得到了白色的背景;然后遍历用于保存关卡中所有元素的数组,绘制数组中不是null的模型对象;最后绘制箱子和玩家角色。paintImage()方法的代码如下:
(7)绘制完image图片之后,需要将该图片显示到面板上,这就需要重写paint()绘图方法。paint()方法除了要绘制图片,还要分析当前关卡的进程:如果所有箱子都到达了目的地,则进入下一关。paint()方法的代码如下:
(8)gotoAnotherLevel()方法用于进入其他关卡,该方法接收一个参数,即关卡数。如果传入的关卡数大于最大关卡数,则认为玩家已经完成了所有关卡,直接进入开始面板。如果传入的参数小于最大关卡数,则主窗体加载下一关的游戏面板。跳转代码被封装在一个Thread线程对象中,这样可以在跳转前添加0.5秒的延时,以避免切换场景过快导致用户反应不过来。gotoAnotherLevel()方法的代码如下:
关卡编辑器是本游戏的一大亮点,它允许用户自己绘制自定义关卡。这不仅能让本游戏保持新鲜感,避免枯燥乏味,还能激发用户的创造力和主动性,进而增强游戏的趣味性。
关卡编辑器的所有功能都集中在关卡编辑器面板中,在该面板中,用户可以在空白区域通过单击来绘制关卡中的元素。如果按住鼠标左键并拖曳则会将鼠标经过的区域全部覆盖上关卡中的元素。另外,如果在已经绘制好的元素上右击,则会擦除该元素;如果按住鼠标右键并拖拽,则会擦除鼠标经过的所有区域内的关卡元素。
在面板下方的按钮区域,用户可以选择绘制不同类型的元素,如墙块、玩家、箱子和目的地。用户如果对绘制的关卡元素不满意,可以单击“清空”按钮来清空所有元素。单击“保存”按钮后,用户可以将绘制的关卡保存为自定义关卡文件。用户在开始面板上选择“开始游戏”功能后,即可体验自己设计的关卡。用户如果单击“返回”按钮,将直接返回开始界面,且不会进行任何保存操作。关卡编辑器面板的效果如图1.10所示。
图1.10 关卡编辑器面板效果
关卡编辑器的设计过程如下:
(1)创建一个名为MapEditPanel的Java类,并让它继承自JPanel面板类,代码如下:
public class MapEditPanel extends JPanel { }
(2)在类中创建属性,分别记录主窗体对象、用于绘制关卡的主图片、各种按钮对象、用于保存关卡中所有元素的数组以及关卡图片在绘制面板上的坐标偏移量等。由于关卡图片并不是紧挨着面板左上角,而是在面板居中的位置,这会对鼠标坐标的计算产生影响,因此我们需要坐标偏移量参与鼠标的计算。
关卡编辑器中需要创建一个DrawMapPanel绘图面板类,该面板类用于提供鼠标绘图的空间。该类是一个成员内部类。
属性代码如下:
(3)在MapEditPanel类中,我们创建一个名为DrawMapPanel的成员内部类,这个内部类继承自JPanel面板类,同时实现MouseListener鼠标事件监听接口和MouseMotionListener鼠标移动事件监听接口。在DrawMapPanel类中,我们创建两个属性:paintFlag用于记录用户是否正在利用鼠标进行绘图,而clickButton用于记录用户按下了哪个鼠标按键。DrawMapPanel类的代码如下:
(4)由于DrawMapPanel类实现了两个鼠标事件监听接口,因此需要重写接口的抽象方法。本游戏使用鼠标按键按下、释放和拖曳操作,因此需要实现mousePressed()方法、mouseReleased()方法和mouseDragged()方法。
当鼠标按键被按下时,会触发mousePressed()方法,该方法首先将绘制paintFlag属性变为true,表示正在绘图,然后记录用户按下了哪个按键,最后调用类中的drawRigid()方法执行绘图。后续内容将详细介绍drawRigid()方法。mousePressed()方法的代码如下:
当鼠标按键被释放时,会触发mouseReleased()方法,该方法将paintFlag属性变为false,就表示停止了绘图操作。mouseReleased()方法的代码如下:
当鼠标被拖曳时,会触发mouseDragged()方法,该方法直接触发drawRigid()方法来绘制图片。mouseDragged()方法的代码如下:
(5)鼠标事件中调用的drawRigid()方法也是DrawMapPanel类中的一个方法,该方法可以在空白图片上绘制关卡中的元素。
drawRigid()方法接收一个MouseEvent参数,该参数是一个鼠标事件对象,用于获取当前鼠标在面板中的坐标。获得鼠标坐标之后,该方法首先需要判断鼠标是否在空白区域内,如果鼠标在空白区域之外,就不会绘制任何元素。如果鼠标处于空白区域内并且paintFlag绘图状态是true,那么该方法首先会获取鼠标坐标对应二维数组的索引,这个索引通过(鼠标坐标-偏移量)/模型宽度获得,例如第一个墙块的坐标为(0,0),横向第二个墙块的坐标应该是(20,0),当鼠标处在(0,0)~(20,20)的坐标范围时,该方法就认为鼠标选中的是第一个墙块。然后,该方法会判断用户按下了哪个鼠标键,左键则根据选中按钮在这个位置上创建对应的模型类,右键则将这个位置上的对象清空。最后,该方法会调用repaint()方法重新把关卡图片绘制到面板上。drawRigid()方法的代码如下:
(6)重写DrawMapPanel类的paint()绘图方法,以便将关卡图片绘制到面板中,这样就可以显示用户绘制完毕的关卡。paint()方法的代码如下:
(7)在DrawMapPanel类的paint()绘图方法中调用一个paintImage()方法,这个方法是写在外部类MapEditPanel中的。paintImage()方法负责解析用于保存关卡中所有元素的数组中的模型对象,将这些对象转化成图片并绘制相应的位置上。该方法中的g2是MapEditPanel类中的属性,它是关卡图片的绘图对象。首先,该方法在关卡中绘制一个白色的矩形以填充整个图片,从而为图片提供白色的背景。然后,该方法遍历关卡中的所有元素,如果取出的对象不是null,则调用模型对象的getImage()方法得到模型图片。最后,该方法将图片绘制到关卡图片中。不管模型图片多大,都按照宽和高都是20的正方形绘制。
paintImage()方法的代码如下:
(8)编写完与绘图相关的组件之后,接下来就需要初始化关卡编辑器中的其他组件了。init()方法用于为所有组件执行初始化操作,包括实例化关卡图片对象,实例化绘图面板对象以及各种按钮对象。init()方法的代码如下:
(9)addListener()方法用于为关卡编辑器添加监听事件。由于鼠标事件都被写在内部类DrawMapPanel类中,因此为主窗体添加鼠标事件时,只需直接传入已经创建好的DrawMapPanel类对象即可。清空按钮通过lambda表达式添加动作事件,单击此按钮时,重新创建用于保存关卡中所有元素的数组,原有的数据就被清除了。保存按钮通过lambda表达式添加动作事件,单击此按钮时,会将用于保存关卡中所有元素的数组交给关卡工具类以生成自定义关卡文件,最后触发返回按钮的单击事件。返回按钮通过lambda表达式添加动作事件,单击此按钮时,首先会删除之前交给主窗体的所有鼠标事件,然后跳转到开始面板。addListener()方法的代码如下:
(10)编写MapEditPanel类的构造方法,在构造方法中修改主窗体标题,同时初始化所有组件,并添加监听。构造方法的代码如下: