Java版赤色要塞源码分析

阅读更多
1.框架与环境搭建
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库位置。
Java版赤色要塞源码分析_第1张图片

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
该模式的使用可以不必等所有资源加载完毕,一开始就直接显示窗口,并提示用户加载进度。
如图
Java版赤色要塞源码分析_第2张图片

主要就是调用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是什么样地
Java版赤色要塞源码分析_第3张图片

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里
Java版赤色要塞源码分析_第4张图片

现在将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);
  }
}


最后拼合效果如下图
Java版赤色要塞源码分析_第5张图片

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



下图是地图的一隅
Java版赤色要塞源码分析_第6张图片

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

下图是第一关整个地图,用的 Picture Merge Genius工具合成的。
Java版赤色要塞源码分析_第7张图片

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


5. GameMode主程序

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

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

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


6.结尾
java很好很强大,降低了游戏制作的门槛,得以圆我们以前做游戏的梦。不过通过分析一个游戏我们也发现了,游戏要做好,程序、图片、声音、关卡设计一个都不能少。程序决定了游戏的可玩性高不高,所以还是很重要的。

  • Java版赤色要塞源码分析_第8张图片
  • 大小: 17 KB
  • Java版赤色要塞源码分析_第9张图片
  • 大小: 31.8 KB
  • jackal_chinese_comment.7z (64.7 KB)
  • 下载次数: 10
  • Java版赤色要塞源码分析_第10张图片
  • 大小: 25.3 KB
  • Java版赤色要塞源码分析_第11张图片
  • 大小: 8.7 KB
  • Java版赤色要塞源码分析_第12张图片
  • 大小: 23.1 KB
  • Java版赤色要塞源码分析_第13张图片
  • 大小: 26.7 KB
  • Java版赤色要塞源码分析_第14张图片
  • 大小: 733.3 KB
  • 查看图片附件

你可能感兴趣的:(游戏)