Java版赤色要塞源码分析

1.框架与环境搭建
1.1 本游戏使用了以下框架
[url=http://slick.ninjacave.com/]slick2d[/url]
[url=http://www.lwjgl.org/]lwjgl[/url]

首先去这两个网站分别将他们下载下来,(注意目前slick2d暂不支持lwjgl 3,所以需要下载lwjgl 2)然后建一个eclipse工程。
游戏源码(java,图片声音地图数据)可在我之前一篇博文中下载,本文文末代码只含有我加了中文注释的java代码。

1.2 lib目录下加入如下jar包
ibxm.jar
jinput.jar
jnlp.jar
jogg-0.0.7.jar
jorbis-0.0.15.jar
lwjgl.jar
slick.jar

1.3 native目录下加入如下本地库(本文以windows7 32位为例,其他类似)
jinput-dx8.dll
jinput-raw.dll
lwjgl.dll
OpenAL32.dll

1.4 关联native库
如图,展开lwjgl.jar,填上native库位置。
[img]http://dl2.iteye.com/upload/attachment/0105/3623/5fb2a0cb-e8cc-3a3f-bf35-eedac556db6b.png[/img]

1.5 解决编译错误
删除ApplicationGameContainer.java,ScalableGameContainer.java
(注:slick2d最后一次更新是2013年,貌似N久不维护了,有些API已经过期了,导致编译不过)
Main.java修改main函数,将ApplicationGameContainer替换为AppGameContainer
至此编译通过,环境搭建完毕。


2. 运行
要分析源码最好的方法就是先——玩游戏,熟悉一下功能。
游戏的功能:
原汁原味复刻FC版赤色要塞,但是画面分辨率提高了
2D卷轴
2种难度供选择
可以continue
玩家8方向移动
秘籍模式(输入上上下下左右左右ZX变30条命,且武器升级)
OGG音乐播放
加载百分比显示
多张图合成到一张图提高加载效率
瓷砖,地图,地形,方向,触发地图(敌人)


3.主类分析
3.1 Main

public class Main extends BasicGame {
public static final int DISPLAY_WIDTH = 1024;
public static final int DISPLAY_HEIGHT = 960;
public static final int SMALL_WIDTH = 512;
public static final int SMALL_HEIGHT = 480;
public IMode mode;
public void init(GameContainer gc) throws SlickException {
//......
try {
//载入进度条,字体,类
loadProgressBar();
loadFont();
loadClasses();
} catch(Throwable t) {
Log.error("Loading error", t);
}

//接受按键
input = new HumanInput(buttonMapping, gc);
//秘籍
konamiCode = new KonamiCode(this);
startPlayer();
resetNextFrameTime();
//进入loading模式
requestMode(Modes.LOADING, gc);
}
public void update(GameContainer gc, int delta) throws SlickException {
//......
int count = 0;
while(nextFrameTime <= Sys.getTime()) {
//全屏切换
fullScreenToggleCheck(gc);
//接收按键
input.snap();
//模式模型更新
mode.update(gc);
nextFrameTime += (int)((Sys.getTimerResolution() * 0.01f) + 0.5f);
if (++count == 8) {
resetNextFrameTime();
break;
}
}
}
public void render(GameContainer gc, Graphics g) throws SlickException {
//模式渲染
mode.render(gc, g);
//......
}
public static void main(String[] args) throws SlickException {
java.awt.Toolkit.getDefaultToolkit();
Main main = new Main();
AppGameContainer appGameContainer = new AppGameContainer(
new ScalableGame(main, DISPLAY_WIDTH, DISPLAY_HEIGHT, true),
SMALL_WIDTH, SMALL_HEIGHT, false);
try {
appGameContainer.setIcon("icons/32x32.png");
} catch(Throwable t) {
Log.error("Icon error", t);
}
appGameContainer.start();
}
}


由于用的slick2d框架,所以继承BasicGame,然后实现3个方法(init,update,render)即可。可看到,一般2d游戏框架都是这样的,程序员只需实现3个方法,init负责资源加载,update负责更新模型,render负责绘图。剩下的通用功能框架会负责考虑,如双缓冲绘图,FPS,跳帧。
其中update和render又转而交给IMode来处理

3.2 IMode


public interface IMode {
public void init(Main main, GameContainer gc) throws SlickException;
public void update(GameContainer gc) throws SlickException;
public void render(GameContainer gc, Graphics g) throws SlickException;
}


老规矩,IMode的抽象化是关键,可以避免冗长的if else分支判断现在处于哪种模式,使得扩展更简单,达到方便切换模式的作用。
Main的init函数最后进入了LoadingMode

4. 资源加载
4.1 LoadingMode
该模式的使用可以不必等所有资源加载完毕,一开始就直接显示窗口,并提示用户加载进度。
如图
[img]http://dl2.iteye.com/upload/attachment/0105/3628/197a944d-3938-37fc-93e8-bf89a7213d7d.png[/img]

主要就是调用main.loadNext()

  public float loadNext() throws Throwable {

switch(loadIndex) {
//加载声音
case 0:
bossIntro = new Music("music/boss_intro.ogg", Song.STREAMING);
break;
//37 加载精灵
case 37:
loadSprites();
break;
//38 大图片
case 38:
loadLargeImages();
break;
//39 精灵大小,貌似是用作碰撞检测
case 39:
loadSizes();
break;
//40 关卡
case 40:
loadStages(stages);
break;
case 41:
//全部加载完进入介绍模式
requestMode(Modes.INTRO, gc);
//requestMode(Modes.SUNSET, gc);
//setMode(new SunsetMode2(), gc);
//setMode(new StageViewMode(), gc);
break;
}

return ++loadIndex / 42f;
}


4.2 XMLPackedSheet使用
看一下如何加载精灵。用了slick2d的一个加载大图片的功能XMLPackedSheet
  private void loadSprites() throws Throwable {

XMLPackedSheet pack1 = new XMLPackedSheet(
"images/sprites-1.png", "images/sprites-1.xml");
}


看一下png和xml是什么样地
[img]http://dl2.iteye.com/upload/attachment/0105/3625/5b8843ff-0c2f-3c8b-9ce1-c54bbe7cfc92.png[/img]

xml里面存储了某个小图片的位置

<sheet>
<sprite name="explosion-2.png" x="1" y="1" width="128" height="124" />
<sprite name="gray-jeep-1.png" x="131" y="1" width="96" height="96" />
</sheet>


这样的好处就是多张图合成到一张图可以提高加载效率。
其实,制作这种图有工具可以使用,如slick2d自带的packulike(下载来的slick.zip的tool目录下),或者[url=http://homepage.ntlworld.com/config/imagepacker/]imagepacker[/url]

4.3 大图绘制
加载如下
  private void loadLargeImages() throws Throwable {
sunset = loadExtraLargeImage("sunset", "large-0", "large-1");
map = loadLargeImage("map", "large-1");
jeepYeah = loadExtraLargeImage("jeep-yeah", "large-2", "large-3");
}


如结尾日落图片,数据在sunset.dat里,tile则在large-0.png,large-1.png里
[img]http://dl2.iteye.com/upload/attachment/0105/3632/c670ca14-138b-3e8d-bfc6-2c620b6a345a.png[/img]

现在将Main里面的loadNext函数最后改为setMode(new SunsetMode2(), gc);

看下如何显示结尾日落图片

public class SunsetMode2 implements IMode {

public Main main;

@Override
public void init(Main main, GameContainer gc) throws SlickException {
this.main = main;
}

@Override
public void update(GameContainer gc) throws SlickException {
}

@Override
public void render(GameContainer gc, Graphics g) throws SlickException {
main.sunset.draw(0, 0);
//main.map.draw(0, 0);
//main.jeepYeah.draw(0, 0);
}
}


最后拼合效果如下图
[img]http://dl2.iteye.com/upload/attachment/0105/3630/4ebaac09-8bda-33b2-b4ca-27c11fe97b7f.png[/img]

4.4 地图加载

  private void loadStage(int index, Stage stage) throws Throwable {
//加载瓷砖,地图,地形,方向,触发地图(敌人)
loadTiles(index, stage);
loadMaps(index, stage);
loadTypes(index, stage);
loadDirections(index, stage);
loadTriggerMap(stage.mapHeight, triggerSizes, index, stage);
}


瓷砖,即tile,用过地图编辑器的都很熟悉了,地图由许多tile拼接而成。
地图,记录了每一格究竟用哪个tile
地形,每一格是固体,还是水。。。?
方向我没研究。
触发地图(敌人)就是敌人登场表,记录敌人在哪一个。

4.5 一个查看地图的小程序

现在将Main里面的loadNext函数最后改为setMode(new StageViewMode(), gc);
然后运行StageViewMode,注意程序会将地图以截屏的方式保存到d盘根目录。d:/jackal{$x}{$y}.png, x和y是递增的数字。注意执行前备份d盘根目录的同名png文件,以防文件被覆盖造成惨剧。

public class StageViewMode implements IMode {

Image copy;
GameMode gameMode;
int xTile;
int yTile;

public Main main;
public GameContainer gc;
public IInput input;

@Override
public void init(Main main, GameContainer gc) throws SlickException {
this.main = main;
this.gc = gc;
this.input = main.input;

gameMode = new GameMode();
gameMode.setStage(0, main.stages[0], false);
System.out.println("tilemap width=" +gameMode.tileMap.length+",height=" +gameMode.tileMap[0].length);
}

@Override
public void update(GameContainer gc) throws SlickException {
}

@Override
public void render(GameContainer gc, Graphics g) throws SlickException {
for (int y = 29; y >= 0; y--) {
for (int x = 31; x >= 0; x--) {
main.draw(gameMode.tiles[gameMode.tileMap[y + yTile*30][x + xTile*32]],
(x << 5) - 0, (y << 5) - 0);

int type = gameMode.typesMap[y + yTile*30][x + xTile*32];

Color originalColor = g.getColor();
g.setColor(Color.red);
g.drawRect(x << 5, y << 5, 32, 32);
if (type==GameMode.TYPE_SOLID) {
Color transparentColor = new Color(255, 0, 0, 0.5f);
g.setColor(transparentColor);
g.fillRect(x << 5, y << 5, 32, 32);
} else if (type==GameMode.TYPE_SHIELD) {
Color transparentColor = new Color(0, 255, 0, 0.5f);
g.setColor(transparentColor);
g.fillRect(x << 5, y << 5, 32, 32);
} else if (type==GameMode.TYPE_WATER) {
Color transparentColor = new Color(0, 255, 255, 0.5f);
g.setColor(transparentColor);
g.fillRect(x << 5, y << 5, 32, 32);
}
g.setColor(originalColor);


//System.out.println("triggerMap.length->"+gameMode.triggerMap.length);
int[][] triggers = gameMode.triggerMap[y + yTile*30];
for(int i = triggers.length - 1; i >= 0; i--) {
int[] trigger = triggers[i];
//System.out.println("trigger->"+trigger[0]);
int displatX = trigger[1];
boolean displayTrigger =false;
if (trigger[1] >= Main.DISPLAY_WIDTH && xTile == 1) {
displatX = trigger[1]- Main.DISPLAY_WIDTH;
displayTrigger= true;
} else if (trigger[1] < Main.DISPLAY_WIDTH && xTile == 0) {
displatX = trigger[1];
displayTrigger= true;
}
if (displayTrigger) {

if (trigger[0]==Triggers.SOLDIER_WALKER || trigger[0]==Triggers.SOLDIER_STATIONARY) {
main.draw((main.enemySoldiers)[1][1],
displatX, y <<5);
} else if (trigger[0]==Triggers.GREEN_BOAT) {
main.draw((main.greenBoats)[0],
displatX, y <<5);
}
}
}
}
}
copy = new Image(Main.SMALL_WIDTH, Main.SMALL_HEIGHT);
g.copyArea(copy, 0, 0);
ImageOut.write(copy, "d:/jackal" + yTile + xTile + ".png");

if (xTile == 1) {
xTile = 0;
yTile++;
} else {
xTile++;
}
if (yTile > 11) {
System.exit(0);
}
}
}



下图是地图的一隅
[img]http://dl2.iteye.com/upload/attachment/0105/3634/f05c45a8-47a0-3abd-afe2-3bc4b44f5fa8.png[/img]

游戏地图绘制大致就是以下流程:
先绘制底图,只是图片,没有碰撞检测
然后记录障碍物(固体,水。。。),控制玩家是否可以通过。
再加上敌人

下图是第一关整个地图,用的 Picture Merge Genius工具合成的。
[img]http://dl2.iteye.com/upload/attachment/0105/3636/d0ee4c13-4a33-37e4-bec5-a96bb968c365.jpg[/img]

本来想做个地图编辑器的,但是比较懒,没时间做了。


5. GameMode主程序

5.1 触发机关(敌人登场)
processTriggers函数判断镜头Y坐标是否达到敌人位置,若达到,则敌人出现(位置已由之前的loadTriggerMap载入)

敌人就不分析了,跟之前本博客分析的STG飞机游戏差不多。敌人都继承自一个共同的Enemy。Java语言就是这样,通过一层层抽象,实现了复杂的功能。

5.2 groupmap
游戏中有门,被炸开后会出现通路。还有房子被炸开后出现破房子,这些必须要改Stage.tileMap才行,是通过Stage.groupMap这个变量实现的。


6.结尾
java很好很强大,降低了游戏制作的门槛,得以圆我们以前做游戏的梦。不过通过分析一个游戏我们也发现了,游戏要做好,图片、声音、关卡设计其实占了大头,程序只是一个小头。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值