1.1 本游戏使用了以下框架
slick2d
lwjgl
首先去这两个网站分别将他们下载下来,(注意目前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库位置。
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
该模式的使用可以不必等所有资源加载完毕,一开始就直接显示窗口,并提示用户加载进度。
如图
主要就是调用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是什么样地
xml里面存储了某个小图片的位置
这样的好处就是多张图合成到一张图可以提高加载效率。
其实,制作这种图有工具可以使用,如slick2d自带的packulike(下载来的slick.zip的tool目录下),或者 imagepacker
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里
现在将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); } }
最后拼合效果如下图
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); } } }
下图是地图的一隅
游戏地图绘制大致就是以下流程:
先绘制底图,只是图片,没有碰撞检测
然后记录障碍物(固体,水。。。),控制玩家是否可以通过。
再加上敌人
下图是第一关整个地图,用的 Picture Merge Genius工具合成的。
本来想做个地图编辑器的,但是比较懒,没时间做了。
5. GameMode主程序
5.1 触发机关(敌人登场)
processTriggers函数判断镜头Y坐标是否达到敌人位置,若达到,则敌人出现(位置已由之前的loadTriggerMap载入)
敌人就不分析了,跟之前本博客分析的STG飞机游戏差不多。敌人都继承自一个共同的Enemy。Java语言就是这样,通过一层层抽象,实现了复杂的功能。
5.2 groupmap
游戏中有门,被炸开后会出现通路。还有房子被炸开后出现破房子,这些必须要改Stage.tileMap才行,是通过Stage.groupMap这个变量实现的。
6.结尾
java很好很强大,降低了游戏制作的门槛,得以圆我们以前做游戏的梦。不过通过分析一个游戏我们也发现了,游戏要做好,程序、图片、声音、关卡设计一个都不能少。程序决定了游戏的可玩性高不高,所以还是很重要的。