`
xpenxpen
  • 浏览: 703533 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

雷霆行动(STG飞机游戏)源码分析

阅读更多
0.前言
本例子取自cping1982早期公开的一个STG源码,loon-simple-20090212,里面带了6个游戏。这次我们要分析的是STGSimple这个飞机游戏。截图如下:


出处请参见上半年私人计划简略及Java桌面游戏开发入门示例并源码集合
下载地址:http://code.google.com/p/loon-simple/downloads/list

声明一下,这个程序不是我写的,是cping1982写的。本人在这里斗胆分析一下高手5年前写的代码,一来是提高自己,二来也是给众多小白以信心和勇气,分析完源码你会发现用java写一个飞机或者坦克的游戏还是不难的。
如果你无法访问google code,也可在本文文末下载我已经加上了注释的版本。
下面进入正题。

1. Role 所有角色的基类

这个类是做STG游戏的关键,所有画面上的东东都是“角色”,包括玩家飞机,敌机,老板,子弹等等。于是把他抽象出来。三个方法move,checkHit,draw是关键。

public abstract class Role {
	protected static GamePanel app;
	protected Image img; //图片
	protected float x;
	protected float y;
	protected float WIDTH;
	protected float HEIGHT;
	private boolean dead; //是否死亡

        //移动
	abstract void move();

	//碰撞检测
	protected boolean checkHit(Role chara) {
		return x > chara.x - WIDTH && x < chara.x + chara.WIDTH
				&& y > chara.y - HEIGHT && y < chara.y + chara.HEIGHT;
	}

	//把自己画出来
	public void draw(Graphics g) {
		g.drawImage(img, (int) x, (int) y, app);
	}
}


以下是Role类的层次结构图,可以看到,一切东东都是Role的子类。

Battle是玩家飞机。
Enemy是敌人。
Hero是子弹。
当然类的命名好坏我们就不去评判了,意思到就行了。

2. Battle 玩家飞机

public class Battle extends Role {
	//tamaIntCount防止子弹放的太快,要限制频率
	private int tamaIntCount;
	//速度(按一次方向键移动的距离)
	private float speed;
	//3枚子弹的速度
	//第1枚,(-1,-7),x少许向左,y快速向上
	//第2枚,(0,-8),x不变,y快速向上
	//第3枚,(1,-7),x少许向右,y快速向上
	private float tv[] = { -1F, -7F, 0.0F, -8F, 1.0F, -7F };
	//当前血量
	public int power;
	//最大血量
	public int powerMax;

	public void move() {
		if (Key.left) {
			//如果按下x键,减速移动,否则全速移动,如果移动超出屏幕,不让移出。上下左右分别重复处理
			if (Key.xkey)
				x -= (double) speed / 4D;
			else
				x -= speed;
			if (x <= 0.0F)
				x = 0.0F;
		}
		//......
		//以上是左键处理,其他3个方向类似,此处省略
		//......

		//如果按下z键,放出3枚子弹,tamaIntCount防止子弹放的太快,要限制频率
		if (Key.zkey && tamaIntCount <= 0) {
			for (int i = 0; i < tv.length; i += 2) {
				GamePanel.addList(new BattleBasic(x + WIDTH / 2.0F, y, tv[i],
						tv[i + 1]));
				tamaIntCount = 8;
			}
		}
		//如果按下x键,放出1枚激光
		if (Key.xkey && !Key.zkey && tamaIntCount <= 0) {
			GamePanel.addList(new BattleBeam(x + WIDTH / 2.0F, y, 0.0F, -8F));
			tamaIntCount = 2;
		}
	}
}


move方法,根据按下的方向键移动。
z,x来生成子弹(BattleBasic)或激光(BattleBeam)。

checkHit判断如果和别的角色碰撞,则扣减血,如果血为0,则死亡

	public boolean checkHit(Role chara) {
		//此处判断并处理与敌机(EnemyA,B,C)或子弹(EnemyShot)碰撞的逻辑
		if ((chara instanceof EnemyA) || (chara instanceof EnemyB)
				|| (chara instanceof EnemyC) || (chara instanceof EnemyShot)) {
			if ((x + WIDTH) - 14F > chara.x && x + 14F < chara.x + chara.WIDTH
					&& (y + HEIGHT) - 12F > chara.y
					&& y + 12F < chara.y + chara.HEIGHT) {
				chara.dead();
				power -= 50;
				if (power <= 0) {
					dead();
					GamePanel.burst = new Burst(x, y);
				}
				return true;
			}
		}
		//......
		//此处省略与其他角色碰撞的逻辑,比如boss等
		//......
		return false;
	}


3.Hero 子弹

public class Hero extends Role {
	//xy上的移动速度(带方向)
	protected float vx;
	protected float vy;

	public void move() {
		//先移动
		x += vx;
		y += vy;
		//如果移出屏幕,则死亡,一般是子弹这类
		if (x + WIDTH < 0.0F || x > (float) app.getWidth() || y + HEIGHT < 0.0F
				|| y > (float) app.getHeight())
			dead();
	}
}



Hero
|----BattleShot 玩家子弹
|    |----BattleBasic 普通子弹
|    |----BattleBeam 激光
|----EnemyShot  敌人子弹
     |----CircleBullets 圆圈子弹
     |----MoveAimingBullet 向玩家发射的子弹
     |----......


4. 入口点 Main

public class Main {
	public static void main(String args[]) {
		java.awt.EventQueue.invokeLater(new Runnable() {
			public void run() {
				new STGFrame();
			}
		});
	}
}


STGFrame代码省略,就是嵌套一个GamePanel

5.GamePanel (骨架代码)

这个是整个飞机游戏的核心代码。

public class GamePanel extends Panel implements Runnable {
	//游戏核心线程
	private Thread gameThread;
	//模式
	public static int gameMode;
	//后台Image(目的是缓冲,防画面闪烁)
	private Image offImage;
	//后台Graphics
	private Graphics g_off;
	//玩家飞机
	public static Image heroImage;
	//所有角色列表
	public static LinkedList<Role> list;
	//临时角色列表
	public static LinkedList<Role> listTmp;
	//玩家
	Battle battle;
}


5.1 初始化
public GamePanel() {
	list = new LinkedList<Role>();
	listTmp = new LinkedList<Role>();
	//载入所有图片
	heroImage = Utility.loadImage("image/this.gif");
	enemyImageA = Utility.loadImage("image/enemyA.gif");
	//......
	//此处省略其他图片
	//......

	gameMode = 0;

	//焦点聚焦,接收键盘输入
	addKeyListener(new Key());
	setFocusable(true);
	requestFocus();

	setBackground(Color.black);
	setForeground(Color.white);

	//启动核心线程
	gameThread = new Thread(this);
	gameThread.start();
}


5.2 核心线程
public void run() {
	while (gameThread == Thread.currentThread()) {
		//绘制打底图
		gameRender();
		if (g_off != null) {
			long RefreshTime = System.currentTimeMillis();
			try {
				Thread.sleep(2L);
			} catch (InterruptedException e) {
			}
			switch (gameMode) {
			case 0:
				title();
				break;

			case 1:
				stage1();
				break;

			case 12:
				ready();
				break;
			//......
			//此处省略其他模式
			//......
			}
			//前面都是在内存中画,现在一次性画到屏幕上
			paintScreen();
			//以下防止电脑太快,适当的休息一下(其实是空转cpu,不太好)
			while (System.currentTimeMillis() - RefreshTime < 13L)
				;
		}
	}
}



可以看到核心线程就是一个死循环,根据不同的模式执行不同的逻辑。
模式表
编号方法含义
0title标题画面
1stage1第1关
2stage2第2关
3stage3第3关
10congratulation恭喜画面
11gameOver游戏结束画面
12ready准备阶段
13appearingAnime玩家飞机登场画面
14crear打败boss阶段
15disappearing关卡通关画面
16bossDeathAnimeboss死亡画面


然后关键就是用双缓冲技术画图,防止屏幕闪烁。
private void gameRender() {
	if (offImage == null) {
		offImage = createImage(450, 500);
		if (offImage == null)
			return;
		g_off = offImage.getGraphics();
	}
	g_off.setColor(Color.BLACK);
	g_off.fillRect(0, 0, 450, 500);
}
public void paintScreen() {
	try {
		Graphics g = getGraphics();
		if (g != null && offImage != null)
			g.drawImage(offImage, 0, 0, null);
		Toolkit.getDefaultToolkit().sync();
		if (g != null)
			g.dispose();
	} catch (Exception e) {
		e.printStackTrace();
	}
}


5.3 主程序
看一下stage1里面调用主程序gameMain
private void stage1() {
    StageA.start();
    gameMain();
}

private void gameMain() {
	//角色碰撞检测,注意这里很通用,不论玩家飞机还是敌机都存在于这个list中,等待碰撞检测
	for (int i = 0; i < list.size(); i++) {
		Role chara1 = list.get(i);
		for (int j = 0; j < list.size(); j++) {
			//2重嵌套循环,判断角色之间的两两碰撞,性能会受影响,可考虑《算法第四版》一书中的碰撞检测算法来优化
			Role chara2 = list.get(j);
			chara1.checkHit(chara2);
		}

	}
	//移动角色,并绘画
	for (int i = list.size() - 1; i >= 0; i--) {
		Role chara1 = (Role) list.get(i);
		chara1.move();
		chara1.draw(g_off);
	}
	//listTmp加入list
	for (int i = 0; i < listTmp.size(); i++)
		list.add(listTmp.get(i));

	//玩家如果死亡,进入11模式
	if (battle.isDead()) {
		gameMode = 11;
	}
	listTmp.clear();
}

这里的碰撞检测便是关键了,2重嵌套循环,判断角色之间的两两碰撞。接下来每个角色移动。
可以说它便是许多游戏实现的关键所在。有了这个骨架代码,只要实现不同Role的checkHit和move还有draw方法就可以了,游戏的扩展性变得简单。


6. EnemyTable 敌人登场表
敌人什么时候出现,为了避免硬编码,于是定了一张表格,这个便是表格类。
public class EnemyTable {
	//登场时间
	public int time;
	//登场xy
	public float x;
	public float y;
	//种类(0,1,2,3,4)
	public int enemyKind;
	//形式
	public int pattern;
}


7. 关卡设计
public class StageA {
	//依次出现:(时间以50为单位)
	//2个A,2个A,6个A+1个C
	static EnemyTable stageA[] = {
			new EnemyTable(0, 0, 25F, -50F, 0),
			new EnemyTable(0, 0, 250F, -50F, 1),
			
			new EnemyTable(0, 1, 25F, -50F, 0),
			new EnemyTable(0, 1, 300F, -50F, 1),
			
			new EnemyTable(0, 2, 30F, -50F, 0),
			new EnemyTable(0, 2, 20F, -50F, 2),
			new EnemyTable(0, 2, 100F, -50F, 2),
			new EnemyTable(0, 2, 200F, -50F, 2),
			new EnemyTable(0, 2, 300F, -50F, 2),
			new EnemyTable(0, 2, 330F, -50F, 1),
			new EnemyTable(2, 2, 20F, -50F, 0),
			//............
			};

	public static void start() {
		for (int i = 0; i < stageA.length; i++) {
			//如果时间到了(以50为单位),则敌人登场
			if ((double) stageA[i].time == (double) GamePanel.time / 50D)
				if (stageA[i].enemyKind == 0)
					GamePanel.addList(new EnemyA(stageA[i].x, stageA[i].y,
							_battle, stageA[i].pattern));
				else if (stageA[i].enemyKind == 1)
					GamePanel.addList(new EnemyB(stageA[i].x, stageA[i].y,
							_battle, stageA[i].pattern));
			//............
		}

	}

}


看到了吗,这样改关卡就变得容易多了,只要改最上面那个表格就ok了。其实做的更好的话可以提取到一个配置文件里。


8.各种敌人,子弹的设计
接下来就是开动你的想象力的时间了。结合各种数学,物理知识,做出千变万化的角色。

这里分析几个例子

8.1 EnemyA
public class EnemyA extends Enemy {
	public void move() {
	//............
	} else if (pattern == 2) {
		//sin函数可以展现弧线的运动轨迹
		x += Math.sin((3.1415926535897931D * (double) counter) / 40D) * 5D;
		y += 1.5D;
	} else if (pattern == 3) {
		//转圈
		if (counter < 50)
			y += 2.5D;
		else if (counter >= 100) {
			x += Math.sin((3.1415926535897931D * ((double) counter - 100D)) / 160D) * 2.5D;
			y += Mathh.sin((3.1415926535897931D * ((double) counter - 20D)) / 160D) * 2.5D;
		}
	}
}

下图分别是pattern 2 和pattern 3的运行轨迹,可以看到pattern2使用一个sin函数可以展现弧线的运动轨迹。而pattern3使用2个sin函数可以让敌机转圈。



8.2 MoveAimingBullet
这个类不错,敌人会朝着玩家发射出子弹,而不是乱放空枪,使得敌人少许有了一些AI。

public MoveAimingBullet(float x, float y, Battle ziki) {
	super(x, y);
	speed = 2.0F;
	x_ziki = ziki.x;
	y_ziki = ziki.y;
	x_enemy = x;
	y_enemy = y;
	distance = (float) Math
			.sqrt((((double) x_ziki + (double) ziki.WIDTH / 2D) - (double) x_enemy)
					* (((double) x_ziki + (double) ziki.WIDTH / 2D) - (double) x_enemy)
					+ (double) ((y_ziki - y_enemy) * (y_ziki - y_enemy)));
	if (distance != 0.0F) {
		vx = (float) (((((double) x_ziki + (double) ziki.WIDTH / 2D) - (double) x_enemy) / (double) distance) * (double) speed);
		vy = ((y_ziki - y_enemy) / distance) * speed;
	} else {
		vx = 0.0F;
		vy = speed;
	}
}



如图,数学公式就是先算出敌我之间的距离,distance=sqrt(x^2+y^2)
然后放出的子弹的速度就是
vx=x/distance
vy=y/distance


8.3 CircleBullets
老版更厉害,可以放出一个圆圈的子弹。

public CircleBullets(float x, float y, boolean flag) {
	super(x, y);
	speed = 1.0F;
	//老版一次放24枚子弹
	tamaNum = 24;
	vxCircle = new float[tamaNum];
	vyCircle = new float[tamaNum];
	float rad_step = (float) (6.2831853071795862D / (double) tamaNum);
	float rad;
	if (flag)
		rad = 0.0F;
	else
		rad = (float) ((double) rad_step / 2D);
	for (int i = 0; i < tamaNum;) {
		vxCircle[i] = (float) (Math.cos(rad) * (double) speed);
		vyCircle[i] = (float) (Math.sin(rad) * (double) speed);
		GamePanel.addList(new EnemyShot(x, y, vxCircle[i], vyCircle[i]));
		i++;
		rad += rad_step;
	}

	vx = vxCircle[0];
	vy = vyCircle[0];
}




9.总结
可以看到,用java做游戏并不难。这个例子可以作为新手做STG游戏(飞机大战,坦克大战)的一个起步。
当然这个例子是纯java,没有用任何框架。当代码膨胀了以后,可以考虑用一些游戏框架来解放我们的生产力。
另外,原作者cping1982的博客相当好,有许多java游戏编程的资料值得参考。
  • 大小: 19.4 KB
  • 大小: 44.1 KB
  • 大小: 43.9 KB
  • 大小: 49.5 KB
  • 大小: 46.4 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics