从无到有的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("地图数据读取出错!!!
"+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

原文地址:https://www.cnblogs.com/cyendra/p/3681539.html