您的位置:首页 > 编程语言 > Java开发

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

2013-12-30 21:45 330 查看

一、游戏功能

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

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

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

在游戏中按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
[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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: