从无到有的Java小游戏开发练习(一)---推箱子
一、游戏功能
游戏由障碍、空地、箱子、终点与玩家组成。
通过上下左右控制玩家推动箱子。当箱子的推动方向没有障碍时,向前移动到新的位置,玩家也向前移动一步。
当所有箱子都处于终点时,游戏胜利,按回车键进入下一关。当完成所有关卡时,按回车键结束游戏。
在游戏中按R建重新开始本关。
二、素材准备
从网上下载推箱子游戏的地图素材与背景音乐。
三、游戏的大致框架
首先最容易想到的是一个管理地图信息的 Map 类,其中应该包括一个关卡地图中的所有信息。
其次应该有一个 DataManager 类来从文件中读取地图、读取图片,并能根据读入的地图文件与关卡编号创造出所需的 Map 类的对象。
还需要有一个 SoundManager 类来播放音乐。
游戏中最不能缺少的是 GameManager 类,用于管理游戏的所有逻辑。
最后是一个窗口,用于综合所有的管理类,将输入传入 GameManager 类以及显示游戏画面。
四、地图类的设计
因此设计出Map类,其中有4个私有成员:二维数组 byte map[ ][ ] 储存地图上的元素,int level 储存当前地图的等级,manX、manY 表示玩家当前所在的位置。
private int manX,manY;// 主角所在位置的坐标 private byte map[][];// 二维地图元素数组 private int level;// 当前地图的等级
对于每一种地图元素,我们都需要用一个数字来表示。因此我们定义一些 byte 类型的常量。
/** 地图元素含义表 */ public final static byte WALL = 1, BOX = 2, BOX_ON_END = 3, END = 4, MAN_DOWN = 5, MAN_LEFT = 6, MAN_RIGHT = 7, MAN_UP = 8, GRASS = 9, MAN_DOWN_ON_END = 10, MAN_LEFT_ON_END = 11,MAN_RIGHT_ON_END = 12, MAN_UP_ON_END = 13;
考虑到进入下一个关卡与重置本关都要新建一个Map对象,因此构造方法有两种,一种传入level,一种则不需要。
/** 构造一个地图对象,不设定等级 */ public Map(byte map[][]){ this.init(map); } /** 构造一个地图对象并指定等级 */ public Map(byte map[][],int level) { this.init(map); this.level = level; }
构造Map时,我们只需要传入表示地图元素的二维数组与等级即可,玩家的位置可以由地图计算得到。
这里没有判断地图的合法性,即主角是否只有一个、箱子与终点是否对应以及谜题是否有解。因为这里的地图是事先写入文件中的,在写入时就应该保证合法性。
/** 初始化一个地图对象 */ public void init(byte map[][]){ this.map = new byte[map.length][map[0].length]; for (int i=0;i<map.length;i++){ for (int j=0;j<map[0].length;j++){ this.map[i][j] = map[i][j]; } } findMan(); } // 判断类型k是否为主角 private boolean isMan(byte k){ boolean res = false; if (k>=5&&k<=13&&k!=9) res = true; return res; } /** 计算主角在地图中的位置 */ public void findMan(){ bk:for (int i=0;i<map.length;i++){ for (int j=0;j<map[i].length;j++){ if (isMan(map[i][j])){ manX = i; manY = j; break bk; } } } }
在实际使用中,我们需要有公有方法来获得地图的一些信息。
/** 获取地图的行数 */ public int getRow(){ return map.length; } /** 获取地图的列数 */ public int getColumn(){ return map[0].length; } /** 设置主角的位置 */ public void setMan(int x, int y){ manX = x; manY = y; } /** 获取主角在地图中的X坐标 */ public int getManX(){ return manX; } /** 获取主角在地图中的y坐标 */ public int getMaxY(){ return manY; } /** 获取(i,j)在地图中的元素 */ public byte getMap(int i,int j){ return map[i][j]; } /** 设置(i,j)的元素类型 */ public void setMap(int i,int j,byte t){ map[i][j]=t; } /** 获取当前等级 */ public int getLevel(){ return level; } /** 判断(i,j)是否为空地 */ public boolean isGrassOrEnd(int i,int j){ if (map[i][j]==4||map[i][j]==9) return true; return false; } /** 判断(i,j)为箱子 */ public boolean isBox(int x,int y){ if (map[x][y]==2||map[x][y]==3) return true; return false; } /** 判断(i,j)是否在地图上 */ public boolean inMap(int x,int y){ if (x>=0&&x<map.length&&y>=0&&y<map[x].length&&map[x][y]>0) return true; return false; }
此时,游戏基础的地图类就完成了。
五、游戏管理器类
GameManager 是游戏中最重要的类,它负责管理游戏中的所有行为,是一个游戏的核心。
类中首先要有一个Map对象,然后还要有一个方法能够接受新的Map对象创建新游戏。
private Map map;// 地图类 /** 构造函数 */ public GameManager(){} /** 初始化游戏为地图map */ public void init(Map map){ this.map = map; }
接下来是游戏的操作,玩家按下上下左右四个方向键,能够向四个方向移动或推箱子。
对于这个功能,我们没有必要写4个方法。只需要一个能接受方向变量的方法即可。
定义4个方向及含义。
public final static int UP = 0, RIGHT = 1, DOWN = 2, LEFT = 3;// 方向 private final int direct[][] = { {-1,0}, {0,1}, {1,0}, {0,-1} };// 方向常量
移动的过程分两种情况讨论,前方为空地、前方为箱子且能推动。
/** 向dir方向移动主角 */ public boolean manMoveTo(int dir){ if (!canMove()) return false; int dx = map.getManX()+direct[dir][0]; int dy = map.getMaxY()+direct[dir][1]; if (!map.inMap(dx, dy)) return false; if (map.isGrassOrEnd(dx, dy)){ manOut(map.getManX(),map.getMaxY()); manIn(dx,dy,dir); } else if (map.isBox(dx, dy)){ int ddx = dx + direct[dir][0]; int ddy = dy + direct[dir][1]; if (!map.inMap(ddx, ddy)) return false; if (map.isGrassOrEnd(ddx, ddy)){ BoxOut(dx,dy); BoxIn(ddx,ddy); manOut(map.getManX(),map.getMaxY()); manIn(dx,dy,dir); } } return true; } // 箱子离开(x,y) private void BoxOut(int x, int y) { byte tp = map.getMap(x,y); if (tp == Map.BOX) map.setMap(x, y, Map.GRASS); if (tp == Map.BOX_ON_END) map.setMap(x, y, Map.END); } // 箱子进入(x,y) private void BoxIn(int x, int y) { byte tp = map.getMap(x,y); if (tp == Map.GRASS) map.setMap(x, y, Map.BOX); if (tp == Map.END) map.setMap(x, y, Map.BOX_ON_END); } //角色离开此地(x,y) private void manOut(int x,int y){ byte tp = map.getMap(x, y); if (tp>=5 && tp<=8) map.setMap(x, y, Map.GRASS); if (tp>=10 && tp<=13) map.setMap(x, y, Map.END); } //角色以dir方向进入此地(x,y) private void manIn(int x,int y,int dir){ byte tp = map.getMap(x, y); if (tp == Map.END) { switch(dir){ case UP: map.setMap(x, y, Map.MAN_UP_ON_END); break; case RIGHT: map.setMap(x, y, Map.MAN_RIGHT_ON_END); break; case DOWN: map.setMap(x, y, Map.MAN_DOWN_ON_END); break; case LEFT: map.setMap(x, y, Map.MAN_LEFT_ON_END); break; } } if (tp == Map.GRASS){ switch(dir){ case UP: map.setMap(x, y, Map.MAN_UP); break; case RIGHT: map.setMap(x, y, Map.MAN_RIGHT); break; case DOWN: map.setMap(x, y, Map.MAN_DOWN); break; case LEFT: map.setMap(x, y, Map.MAN_LEFT); break; } } map.setMan(x, y); }
如此一来游戏的主逻辑就构建完成了。
最后是一些传递信息的方法。
private boolean gameOn = true;// 游戏是否可操作 /** 判断是否胜利 */ public boolean isWin(){ for (int i=0;i<map.getRow();i++){ for (int j=0;j<map.getColumn();j++){ if (map.getMap(i, j)==Map.END||map.getMap(i, j)>=10&&map.getMap(i, j)<=13) return false; } } return true; } /** 获取游戏是否可操作 */ public boolean canMove(){ return gameOn; } /** 设置游戏是否可操作 */ public void setGame(boolean ok){ gameOn = ok; } /** 获取地图类 */ public Map getMap(){ return map; }
六、管理数据的类
DataManager 要做的很简单,从文件中读取数据即可。
读取地图:
/** 读取文件中的地图数据 */ public static byte[][][] loadMap(){ byte[][][] map = null; File file = new File("data/map.mp"); if (file.exists()){ try { Scanner scan = new Scanner(file); int len = scan.nextInt(); System.out.println(len); map = new byte[len][][]; for (int k=0;k<len;k++){ int n = scan.nextInt(); int m = scan.nextInt(); System.out.println(n+" "+m); map[k] = new byte[n][m]; for (int i=0;i<n;i++){ for (int j=0;j<m;j++){ map[k][i][j] = scan.nextByte(); System.out.print(map[k][i][j]); } System.out.println(); } System.out.println(); } scan.close(); } catch (Exception e){ System.out.println("地图数据读取出错!!!\n"+e.toString()); } } return map; }
读取图片:
/** 从文件中加载Image */ public Image[] getPic(){ Image pic[] = new Image[14]; for (int i=0;i<=13;i++){ File f = new File("images\\pic"+i+".JPG"); try { pic[i] = ImageIO.read(f); } catch (IOException e) { e.printStackTrace(); } } return pic; }
对于地图,仅仅读取文件中的数据还是不够的,还要能返回一个 Map 对象。
// 获取等级为level的地图的一个副本 private byte[][] getMap(int level){ if (level < 0) level = 0; if (level >= maxLevel) level = maxLevel - 1; byte res[][] = new byte[map[level].length][map[level][0].length]; for (int i=0;i<res.length;i++){ for (int j=0;j<res[i].length;j++){ res[i][j] = map[level][i][j]; } } return res; }
/** 创造一个等级为level的地图对象 */ public Map createMap(int level){ if (level < 0) level = 0; if (level >= maxLevel) level = maxLevel - 1; Map mp = new Map(getMap(level),level); return mp; }
在读取地图文件时还要用一个变量来记录关卡总数。
private int maxLevel;// 地图总数即最大关卡数 /** 获取最大关卡数 */ public int getMaxLevel(){ maxLevel = map.length; return maxLevel; }
七、音乐管理类
由于本游戏只需要一个固定背景音乐,不需要音效,所以 SoundManager 任务很简单。
String path = new String("audio\\"); String file = new String("bgm.mid"); Sequence seq; Sequencer midi; boolean sign; public SoundManager() {} public 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 e){ System.out.println(e.toString()); } sign = true; }
八、界面类
界面类 GameFrame 需要继承 JFrame 并有 KeyListener 接口便于接受玩家的按键。
以下是该类中的一些私有变量。
// 管理器 private GameManager gm; private DataManager dm; private SoundManager sm; // 双缓冲技术 private Image iBuffer; private Graphics gBuffer; // 窗体信息 private String title = "推箱子"; private int leftX = 0, leftY = 0; private int width = 0, height = 0; private int mapRow = 0, mapColumn = 0; // 贴图数据 private Image pic[] = null;
初始化 GameFrame 时,需要新建三个管理器的对象,添加监听器。
/** 构造一个游戏窗体 */ public GameFrame() { init(); } /** 初始化窗体 */ public void init(){ dm = new DataManager(); gm = new GameManager(); sm = new SoundManager(); this.setTitle(title); this.setSize(600,600); this.setLocation(300, 20); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setFocusable(true); pic = dm.getPic(); sm.loadSound(); width = this.getWidth(); height = this.getHeight(); this.addKeyListener(this); newGame(0); }
创建新游戏时,用 DataManager 的对象获取一个新地图用于初始化 GameManager 。
为了获取贴图的坐标,还要更新坐标信息。
// 从第level关开始新游戏 private void newGame(int level){ gm.init(dm.createMap(level)); gm.setGame(true); getMapSizeAndPosition(); repaint(); } // 更新地图信息与贴图位置 private void getMapSizeAndPosition(){ mapRow = gm.getMap().getRow(); mapColumn = gm.getMap().getColumn(); leftX = (width - mapColumn * 30) / 2; leftY = (height - mapRow * 30) / 2; System.out.println("左上坐标: "+leftX+" "+leftY+" 行列数: "+mapRow+" "+mapColumn); }
当用户有按键操作时,根据不同的输入进行不同的处理。
由于操作后画面有可能变化,所以要调用repaint()重绘画面。
若按键结束后游戏胜利,则设置游戏状态为 false 。
public void keyPressed(KeyEvent e) { switch (e.getKeyCode()){ case KeyEvent.VK_ENTER: if (!gm.canMove()){ if (dm.getMaxLevel()-1==gm.getMap().getLevel()) System.exit(0); else newGame(gm.getMap().getLevel()+1); } break; case KeyEvent.VK_R: newGame(gm.getMap().getLevel()); break; case KeyEvent.VK_UP: gm.manMoveTo(GameManager.UP); break; case KeyEvent.VK_DOWN: gm.manMoveTo(GameManager.DOWN); break; case KeyEvent.VK_LEFT: gm.manMoveTo(GameManager.LEFT); break; case KeyEvent.VK_RIGHT: gm.manMoveTo(GameManager.RIGHT); break; } repaint(); if (gm.isWin()) gm.setGame(false); }
为了防止屏幕闪烁,采用双缓冲技术。
获取一个与屏幕等大的 Image 类的对象 iBuffer,用 Graphics 类的对象 gBuffer 对 iBuffer 进行绘图,最后将 iBuffer 一次性显示。
// 双缓冲技术重载paint public void paint(Graphics g){ if (iBuffer == null){ iBuffer = createImage(this.getSize().width, this.getSize().height); gBuffer = iBuffer.getGraphics(); } gBuffer.setColor(getBackground()); gBuffer.fillRect(0, 0, this.getSize().width, this.getSize().height); for (int i=0;i<mapRow;i++){ for (int j=0;j<mapColumn;j++){ byte tp = gm.getMap().getMap(i, j); if (tp>0){ gBuffer.drawImage(pic[tp], leftX+j*30, leftY+i*30, this); } } } gBuffer.setColor(Color.red); gBuffer.setFont(new Font("楷体_2312", Font.BOLD, 30)); gBuffer.drawString("按R键重新开始本关", 100, 60); gBuffer.drawString("现在是第", 100, 100); gBuffer.drawString(String.valueOf(gm.getMap().getLevel()+1), 260, 100); gBuffer.drawString("关", 310, 100); if (!gm.canMove()) { if (dm.getMaxLevel()-1==gm.getMap().getLevel()) gBuffer.drawString("恭喜你通关了! 按回车键退出游戏!", 100, 140); else gBuffer.drawString("按回车键进入下一关", 100, 140); } g.drawImage(iBuffer,0,0,this); } // 重载update public void update(Graphics g){ paint(g); }
至此,一个简单的推箱子游戏就完成了。
⑨、调试与运行
public class GameMain { public static void main(String[] args) { GameFrame f = new GameFrame(); f.setVisible(true); } }
关键:推箱子的逻辑、双缓冲绘图。
代码下载:http://download.csdn.net/detail/cyendra/6796841