从无到有的Java小游戏开发练习(一)---推箱子

来源:互联网 发布:纵横造价软件视频教程 编辑:程序博客网 时间:2024/05/16 07:45

一、游戏功能

游戏由障碍、空地、箱子、终点与玩家组成。

通过上下左右控制玩家推动箱子。当箱子的推动方向没有障碍时,向前移动到新的位置,玩家也向前移动一步。

当所有箱子都处于终点时,游戏胜利,按回车键进入下一关。当完成所有关卡时,按回车键结束游戏。

在游戏中按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 一次性显示。

// 双缓冲技术重载paintpublic 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);}// 重载updatepublic 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

1 0
原创粉丝点击