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

0.前言
本例子取自cping1982早期公开的一个STG源码,loon-simple-20090212,里面带了6个游戏。这次我们要分析的是STGSimple这个飞机游戏。截图如下:
[img]http://dl2.iteye.com/upload/attachment/0104/6933/3503532f-90ca-3860-ac53-634a58f85bf9.png[/img]

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

声明一下,这个程序不是我写的,是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的子类。
[img]http://dl2.iteye.com/upload/attachment/0104/6925/19cbb783-e1b9-3650-8470-3c10c41d50a0.png[/img]
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)
;
}
}
}



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

然后关键就是用双缓冲技术画图,防止屏幕闪烁。

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函数可以让敌机转圈。
[img]http://dl2.iteye.com/upload/attachment/0104/6927/bd0420e6-2bf7-3616-b9a9-131b9e59751b.png[/img]
[img]http://dl2.iteye.com/upload/attachment/0104/6929/eb039297-413e-31b2-abfa-092b1b55df57.png[/img]

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;
}
}


[img]http://dl2.iteye.com/upload/attachment/0104/6931/c1b2af7a-cd08-3027-8bea-cb0c15e59271.png[/img]
如图,数学公式就是先算出敌我之间的距离,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];
}


[img]http://dl2.iteye.com/upload/attachment/0104/6933/3503532f-90ca-3860-ac53-634a58f85bf9.png[/img]

9.总结
可以看到,用java做游戏并不难。这个例子可以作为新手做STG游戏(飞机大战,坦克大战)的一个起步。
当然这个例子是纯java,没有用任何框架。当代码膨胀了以后,可以考虑用一些游戏框架来解放我们的生产力。
另外,[url=http://blog.csdn.net/cping1982]原作者cping1982的博客[/url]相当好,有许多java游戏编程的资料值得参考。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值