zoukankan      html  css  js  c++  java
  • 从无到有的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

  • 相关阅读:
    JavaScript文本框焦点事件
    JavaScript实现图片切换
    自定义搭建PHP开发环境
    python基础整理1
    python基础整理2——Linux
    雪花算法(03)生成时间
    雪花算法(02)算法中的位运算
    雪花算法(01)介绍
    ASP.NET控件Repeter的使用
    DNS解析详细过程
  • 原文地址:https://www.cnblogs.com/cyendra/p/3681539.html
Copyright © 2011-2022 走看看