源码下载 第5章推箱子游戏 5.1推箱子游戏介绍 经典的推箱子游戏是一个来自日本的古老游戏,目的是训练玩家的逻辑思维能力。在一个狭小的仓库中,要求把箱子放到指定的位置,但一不小心就会出现箱子无法移动或者通道被堵住的情况,所以需要巧妙地利用有限的空间和通道合理地安排移动的次序和位置,这样才能顺利地完成任务。 推箱子游戏的规则如下: 游戏运行载入相应的地图,屏幕中出现一个推箱子的工人,其周围是围墙、人可以走的通道、几个可以移动的箱子和箱子放置的目的地。玩家通过按上、下、左、右键控制工人推箱子,当把箱子都推到了目的地之后出现过关信息,并显示下一关。如果推错,玩家可以右击撤销上次的移动,还可以按空格键重新玩这一关,直到过完全部关卡。 本章开发推箱子游戏,该游戏的效果如图51所示。 图51推箱子游戏界面 游戏中的图片资源如图52所示。 图52推箱子游戏的图片资源 其中,pic1为墙; pic2为箱子; pic3为在目的地的箱子; pic4为目的地; pic5为向下的人; pic6为向左的人; pic7为向右的人; pic8为向上的人; pic9为通道; pic10为站在目的地向下的人; pic11为站在目的地向左的人; pic12为站在目的地向右的人; pic13为站在目的地向上的人。 5.2程序设计的思路 首先确定开发难点。对工人的操作很简单,就是向4个方向移动,工人移动,箱子也移动,所以按键的处理也比较简单。当箱子到达目的地时,就会产生游戏过关事件,需要逻辑判断。仔细想一下,所有事件都发生在一张地图中,这张地图包括箱子的初始位置、箱子最终放置的位置和围墙障碍等。每一关地图都要更换,这些位置也要改变,所以每一关的地图数据是最关键的,它决定了每一关的不同场景和物体的位置。下面重点分析一下地图。 可以把地图想象成一个网格,每个格子就是工人每次移动的步长(这里为30像素),也是箱子移动的距离,这样问题就简化多了。首先设计一个mapRow×mapColumn的二维数组map,按照这样的框架来思考。对于格子的两个屏幕像素坐标(x、y),可以由二维数组下标(i、j)换算。 换算公式为leftX + j×30,leftY + i×30 每个格子的状态值分别用枚举类型值: //定义一些常量,对应地图的元素 final byte WALL=1,BOX=2,BOXONEND=3,END=4,MANDOWN=5, MANLEFT=6,MANRIGHT=7,MANUP=8,GRASS=9, MANDOWNONEND=10,MANLEFTONEND=11, MANRIGHTONEND=12,MANUPONEND=13; 其中,WALL(1)代表墙,BOX(2)代表箱子,BOXONEND(3)代表放到目的地的箱子,END(4)代表目的地,MANDOWN(5)代表向下的人,MANLEFT(6)代表向左的人,MANRIGHT(7)代表向右的人,MANUP(8)代表向上的人,GRASS(9)代表通道,MANDOWNONEND(10)代表站在目的地向下的人,MANLEFTONEND(11)代表站在目的地向左的人,MANRIGHTONEND(12)代表站在目的地向右的人,MANUPONEND(13)代表站在目的地向上的人。 图53初始位置 原始地图中格子的状态值数组采用相应的整数形式存储。 在玩家通过键盘控制工人推箱子的过程中,需要按游戏规则判断是否响应该按键指示。下面分析工人将会遇到什么情况,以便归纳出所有的规则和对应算法。为了描述方便,假设工人移动趋势方向为向右,其他方向的原理与之相同。p1、p2分别代表工人移动趋势方向前的两个方格,如图53所示。 1. 前方p1是围墙 如果工人前方是围墙(即阻挡工人的路线) { 退出规则判断,布局不做任何改变 } 2. 前方p1是通道(GRASS)或目的地(END) 如果工人前方是通道或目的地 { 工人可以进到p1方格,修改相关位置格子的状态值 } 3. 前方p1是箱子 在前面的两种情况中,只要根据前方p1处的物体就可以判断出工人是否可以移动,而在第3种情况中(如图54所示),需要根据箱子前方p2处的物体才能判断出工人是否可以移动。此时有以下几种可能: 图54前方p1是箱子 (1) p1处为箱子(BOX)或者放到目的地的箱子(BOXONEND),p2处为通道(GRASS); 工人可以进到p1方格; p2方格的状态为箱子。修改相关位置格子的状态值。 (2) p1处为箱子(BOX)或者放到目的地的箱子(BOXONEND),p2处为目的地(END); 工人可以进到p1方格; p2方格的状态为放置好的箱子。修改相关位置格子的状态值。 (3) p1处为箱子(BOX),p2处为墙(WALL)。 退出规则判断,布局不做任何改变。 综合前面的分析,可以设计出整个游戏的实现流程。 整个游戏的源文件说明如下。  GameFrame.java: 游戏界面视图。  Map.java: 封装游戏当前状态。  MapFactory.java: 提供地图数据。 5.3程序设计的步骤 5.3.1设计地图数据类 地图数据类(MapFactory)保存所有关卡的原始地图数据,每一关的数据为一个二维数组,所以此处map是三维数组。 import java.io.InputStream; 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,0,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; public static byte[][] getMap(int grade) {byte temp[][]; if(grade>=0 && grade 0) { //若要撤销,必须走过 Map priorMap=(Map) list.get(list.size() - 1); map=priorMap.getMap(); row=priorMap.getManX(); column=priorMap.getManY(); repaint(); list.remove(list.size() - 1); } else DisplayToast("不能再撤销!"); } else { DisplayToast("此关已完成,不能撤销!"); } } nextGrade()实现下一关的初始化及调用repaint()显示游戏界面: public void nextGrade() { if (grade >=MapFactory.getCount() - 1) { DisplayToast("恭喜你完成所有关卡!"); acceptKey=false; } else { grade++; initMap(); repaint(); acceptKey=true; } } priorGrade()实现上一关的初始化及调用repaint()显示游戏界面: public void priorGrade() { grade--; acceptKey=true; if (grade < 0) grade=0; initMap(); repaint(); } 在键盘事件keyPressed中根据用户的按键分别调用向4个方向移动的方法。 public void keyPressed(KeyEvent e)//键盘事件 { if(e.getKeyCode()==KeyEvent.VK_UP){ //向上 moveUp();} if(e.getKeyCode()==KeyEvent.VK_DOWN){ //向下 moveDown(); } if(e.getKeyCode()==KeyEvent.VK_LEFT){//向左 moveLeft(); } if(e.getKeyCode()==KeyEvent.VK_RIGHT){//向右 moveRight(); } repaint(); if (isFinished()) { //禁用按键 acceptKey=false; if(grade==10){JOptionPane.showMessageDialog(this,"恭喜通过最后一关");} else { //提示进入下一关 String msg="恭喜您通过第"+grade+"关!!!\n是否要进入下一关?"; int type=JOptionPane.YES_NO_OPTION; String title="过关"; int choice=0; choice=JOptionPane.showConfirmDialog(null,msg,title,type); if(choice==1)System.exit(0); else if(choice==0) { //进入下一关 acceptKey=true; nextGrade(); } } } } public void actionPerformed(ActionEvent arg0) { //TODO Auto-generated method stub } public void keyReleased(KeyEvent arg0) { //TODO Auto-generated method stub } public void keyTyped(KeyEvent arg0) { //TODO Auto-generated method stub } 鼠标事件的相关代码如下: public void mouseClicked(MouseEvent e) { //TODO Auto-generated method stub if (e.getButton()==MouseEvent.BUTTON3) //右击撤销移动 { undo();//撤销移动 } } main()方法(程序入口)实现一个GameFrame窗口: public static void main(String[] args) { new GameFrame(); } } 5.3.4设计播放背景音乐类 设计播放背景音乐类(Sound),用于播放背景音乐。 import javax.sound.midi.*; import java.io.File; class Sound//播放背景音乐类 { String path=new String("musics\\"); String file=new String("nor.mid"); Sequence seq; Sequencer midi; boolean sign; void loadSound() { try { seq=MidiSystem.getSequence(new File(path+file)); midi=MidiSystem.getSequencer(); midi.open(); midi.setSequence(seq); midi.start(); midi.setLoopCount(Sequencer.LOOP_CONTINUOUSLY); } catch (Exception ex) {ex.printStackTrace();} sign=true; } void mystop(){midi.stop();midi.close();sign=false;} boolean isplay(){return sign;} void setMusic(String e){file=e;} } 游戏动作的音效通常比较短小,其播放时间最多只有一两秒钟,而游戏背景音乐需要持续比较长的时间,少则十几秒,多则几分钟甚至更长时间。Java支持的背景音乐文件格式主要有CD、MP3和MIDI。这里使用的是MIDI格式音乐。 MIDI(Musical Instrument Digital Interface,乐器数字接口)是20世纪80年代初为解决电声乐器之间的通信问题而提出的。MIDI传输的不是声音信号,而是音符、控制参数等指令,它指示MIDI设备要做什么、怎么做,例如演奏哪个音符、多大音量等。它们被统一表示成MIDI消息(MIDI Message)。 Java提供了专门的包来处理和播放MIDI音乐,包名为javax.sound.midi,其中包括与MIDI相关的各个类及其方法。读取MIDI文件信息的主要步骤如下。 (1) 打开MIDI文件: Sequence sequence=MidiSystem.getSequence(new File(filename)); (2) 建立音频序列: Sequencer sequencer=MidiSystem.getSequencer(); (3) 打开音频序列: sequencer.open(); 在读取MIDI音频序列并初始化音频序列器之后,接下来便可以播放MIDI音乐了。播放MIDI音乐的步骤如下。 (1) 读取即将播放的音频序列: sequencer.setSequence(sequence); (2) 播放音频序列: sequencer.start(); 如果要循环播放MIDI音乐,则可以使用Sequencer的isRunning()方法进行判断,若该方法的返回值为false,说明音乐已经播放完毕,此时可以再次调用start()方法重新开始播放。