前言

代码演示环境

  • 软件环境:Windows 10
  • 开发工具:Visual Studio Code
  • JDK版本:OpenJDK 15
Java 2D单人游戏

创建基于Title的地图

在2D游戏中,地图是整体结构,或者我们叫做游戏地图(game map),通常是几个屏幕的宽度表示。有些游戏地图是屏幕的20倍;甚至是100位以上,主要特点是跨屏幕之后,让地图滚动显示,这种类型的游戏又叫做2D平台游戏(2D platform game)。所以平台游戏是指玩家从一个平台跑到另外一平台,在其中需要跑、跳等动作,除此之外,还要避开敌人,以及采血、加体力等动作。本章我们介绍怎样创建基本的地图、地图文件、碰撞侦测、加体力、简单的敌人,以及生成背景的视差滚动效果等。

如果在游戏中如果巨幅图片,这种策略不是最好的解决方案,因为这会产生大量的内存消耗,可能会导致不装载图片。另外,巨幅图片不能说明玩家哪个地图可以使用,哪些地图不可以使用。解决这个问题的一般策略是使用基于title的图片。tile-base地图是把地图分解决成表格,每个单元格包含一小块的图片,或者没有图片。如下图示:

Java游戏编程不完全详解-5_游戏开发

基于tile的地力点有点像使用预制块来创建游戏,不是同的就是这些块的颜色不,并且可以无限制使用颜色。Tile地力的包含的引用属于表格的每个单元格(cell)所有,这样,我们只需要一些小图片就可以实现整个tile的画面显示,并且我们可以根据游戏的需求无限制创建背景画面,而不担心内存的约束问题。大多数游戏都使用16或者32位的图片来表示单元格,我们这里使用是64位的图片,如下图所示:

Java游戏编程不完全详解-5_游戏开发_02

以上就是基于tile的地图,它有九个块效果可以很容易决定哪些是“solid”部分,哪些是”empty”的地图,这样,你可以知道哪部分地图玩家可以跳,哪部分玩家可以穿墙。下面我们首先实现这个Tile地图。

TileMap类

package com.funfree.arklis.engine.tilegame;
import java.awt.Image;
import java.util.LinkedList;
import java.util.Iterator;
import com.funfree.arklis.graphic.*;

/**
	功能:书写一个类用来表示Tile地图
	备注:该类包含的数据用来实现地图、包括小怪。每个tile引用一个图片。当然这些图片会被
		使用多次。
	*/
public class TileMap{
	private Image[][] tiles; //表示地图
	private LinkedList sprites; //表示游戏中的小怪
	private Sprite player; //表示玩家
	
	/**
		初始化成员变量时指定地图的宽和高
		*/
	public TileMap(int width, int height){
		tiles = new Image[width][height];
		sprites = new LinkedList();
	}
	
	/**
		返回地图的宽度
		*/
	public int getWidth(){
		return tiles.length;
	}
	
	/**
		获取地图的高度
		*/
	public int getHeight(){
		return tiles[0].length;
	}
	
	/**
		根据指定的坐标来获取相应的tile。如果返回null值,那么表示地图越界了。
		*/
	public Image getTile(int x, int y){
		if(x < 0 || x >= getWidth() ||
		   y < 0 || y >= getHeight()){
		   	   //那么回返null值
			return null;
		}else{
			//否则返回tile
			return tiles[x][y];
		}
	}
	
	
	/**
		根据指定的坐标和指定的图片来更换tile
		*/
	public void setTile(int x, int y, Image tile){
		tiles[x][y] = tile;
	}
	
	/**
		返回玩家角色
		*/
	public Sprite getPlayer(){
		return player;
	}
	
	
	/**
		指定玩家角色
		*/
	public void setPlayer(Sprite player){
		this.player = player;
	}
	
	/**
		添加小怪地图中
		*/
	public void addSprite(Sprite sprite){
		sprites.add(sprite);
	}
	
	/**
		删除该地图中的小怪
		*/
	public void removeSprite(Sprite sprite){
		sprites.remove(sprite);
	}
	
	/**
		迭代出该地图中所有的小怪,除了玩家本身之外。
		*/
	public Iterator getSprites(){
		return sprites.iterator();
	}
}

除了地图,TileMap还包含在游戏中的小怪,小怪可以地图中任何位置,并且没有边界的限制。如下图:

Java游戏编程不完全详解-5_游戏开发_03

装载Title的地图

下面我们需要有一个地方来保存该地图,然后在恰当的时候实际创建该地图。Tile地图游戏总是有多个级别的地图,该示例也不例外。如果我们可以很轻松的方式来创建多个地图,那么玩家可以在完成一个地图之后,然后开始下一个地图的游戏情节。我们创建地图时呼叫TileMap的addTile()方法和addSprite()方法,该方法的灵活性非常好,但是,这样编辑地图的级别比较困难,并且代码本身也不是很优雅。所以,大多数的tile游戏有自己的地图编辑器来创建地图。这个地图编辑器是可视化添加tile和小怪到游戏中,这样做的方式是非常简捷的方式。一般把地图保存到中介地图文件中,而这个文件是可以让游戏解析的。这样,我们可定义一个基于文本地图文件,这样我们可以编辑地图,因为tile是被定义在一个表格中的,所以文本文件中的每个字符可以表示一个tile或者是一个小怪/玩家,如下图:

Java游戏编程不完全详解-5_游戏开发_04

其中”#”表示注释,而其它的表示tile的row。该地图是固定的,所以可以我们可让地图变量,并且可能添加更多的line或者让line更长。那么解析地图的步骤有三步:

  • 读取每一行,忽略注释行,然后把每行放到一个集合中
  • 创建一个TileMap对象,TileMap的宽度就是集合中最长元素的长度值,而高度就是集合中的line的数量
  • 解析每一line中的每个字符,根据该字符添加相应的tile或者sprite到地图中去。

完成这个工作是ResourceManager类。需要注意的是:添加sprite到TileMap中去时,开始,我们需要创建不同的Sprite对象,这样,我们可根据这些“主”怪来克隆小怪;第二,每个sprite不需要尺寸与tile的尺寸一样,所以,我们需要保证每个sprite在tile中的中央,这些事件都在addSprite()方法完成。本章以前的Sprite的位置相同的屏幕,但是在本章示例中,sprite的位置是相同到tile地图。我们使用TileMapRender的静态方法titlesToPixels()来转换tile位置到地图的位置。该函数乘以tile的数值,

int pixelSize = numTiles * TITLE_SIZE;

以上公式可以让sprite移动到地图上的任意一个位置,并且不需要调整tile的边界。也就是说,我们有一个灵活的方式来创建地图和解析它们,以及创建一个TileMap对象。在示例中,所有的地图都在map文件夹中(map1.txt和map2.txt)等等。如果我们需要下一个地图,只需要让代码去寻找下一个地图文件即可;如果没有找到,代码回装载第一个地图。也就是说,我们不需要新地图,只需要在这个目录中删除地图文件即可,也不需要告诉游戏有多少个地图存在。

ResourceManager类

package com.funfree.arklis.engine.tilegame;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.io.*;
import java.util.ArrayList;
import javax.swing.ImageIcon;

import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;


/**
    功能:书写一个ResourceManager类用来装载和管理tile图片和“主”怪。游戏中的
    	小怪是从“主”怪克隆而来。
   	备注:该类有第四章GameCore所有功能。
*/
public class ResourceManager {
    private ArrayList tiles; //保存文字地图的集合
    private int currentMap; //当前地图
    private GraphicsConfiguration gc; //显示卡

    // 用来被克隆的主怪
    private Sprite playerSprite;
    private Sprite musicSprite;
    private Sprite coinSprite;
    private Sprite goalSprite;
    private Sprite grubSprite;
    private Sprite flySprite;

    /**
        使用指定的显卡来创建资源管理器对象
    */
    public ResourceManager(GraphicsConfiguration gc) {
        this.gc = gc;
        loadTileImages();
        loadCreatureSprites();
        loadPowerUpSprites();
    }


    /**
        从images目录获取图片
    */
    public Image loadImage(String name) {
        String filename = "images/" + name;
        return new ImageIcon(filename).getImage();
    }
	
	//获取Mirror图片
    public Image getMirrorImage(Image image) {
        return getScaledImage(image, -1, 1);
    }
	
	//获取反转后的图片
    public Image getFlippedImage(Image image) {
        return getScaledImage(image, 1, -1);
    }
	
	/**
		完成图片的转换功能
		*/
    private Image getScaledImage(Image image, float x, float y) {

        // 设置一个图片转换对象
        AffineTransform transform = new AffineTransform();
        transform.scale(x, y);
        transform.translate(
            (x-1) * image.getWidth(null) / 2,
            (y-1) * image.getHeight(null) / 2);

        // 创建透明的图片(不同半透明)
        Image newImage = gc.createCompatibleImage(
            image.getWidth(null),
            image.getHeight(null),
            Transparency.BITMASK);

        // 绘制透明图片
        Graphics2D g = (Graphics2D)newImage.getGraphics();
        g.drawImage(image, transform, null);
        g.dispose();

        return newImage;
    }

	/**
		从maps目录中装载下一下地图
		*/
    public TileMap loadNextMap() {
        TileMap map = null;
        while (map == null) {
            currentMap++;
            try {
                map = loadMap(
                    "maps/map" + currentMap + ".txt");
            }
            catch (IOException ex) {
                if (currentMap == 1) {
                    // 无装载的地图,返回null值!
                    return null;
                }
                currentMap = 0;
                map = null;
            }
        }
		
        return map;
    }
	
	/**
		重新装载maps目录下的地图文本
		*/
    public TileMap reloadMap() {
        try {
            return loadMap(
                "maps/map" + currentMap + ".txt");
        }
        catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }
	
	/**
		完成装载地图的核心方法
		*/
    private TileMap loadMap(String filename)throws IOException{
        ArrayList lines = new ArrayList();
        int width = 0;
        int height = 0;
		
        // 读取文本文件中的每一行内容到集合中保存
        BufferedReader reader = new BufferedReader(
            new FileReader(filename));
        for(;;) {
            String line = reader.readLine();
            // 没有内容可读取了
            if (line == null) {
                reader.close();
                break;
            }

            // 添加每一行记录,除了注释
            if (!line.startsWith("#")) {
                lines.add(line);
                width = Math.max(width, line.length());
            }
        }

        // 解析每一行,以便创建TileEngine对象
        height = lines.size();
        TileMap newMap = new TileMap(width, height);
        for (int y = 0; y < height; y++) {
        	//获取集合中的字符串对象
            String line = (String)lines.get(y);
            //把每个字符中的字符取出来
            for (int x = 0; x < line.length(); x++) {
                char ch = line.charAt(x);

                // 检查字符是否为A,B,C等字符
                int tile = ch - 'A';
                //如果是字符A
                if (tile >= 0 && tile < tiles.size()) {
                	//那么根据tile值来创建地图元素--这里地图实现的核心方法
                    newMap.setTile(x, y, (Image)tiles.get(tile));
                }
                // 如果字符是表示小怪的,比如0, !或者*,那么分别添加主怪到集合中
                else if (ch == 'o') {
                    addSprite(newMap, coinSprite, x, y);
                }else if (ch == '!') {
                    addSprite(newMap, musicSprite, x, y);
                }else if (ch == '*') {
                    addSprite(newMap, goalSprite, x, y);
                }else if (ch == '1') {
                    addSprite(newMap, grubSprite, x, y);
                }else if (ch == '2') {
                    addSprite(newMap, flySprite, x, y);
                }
            }
        }
		
        // 添加玩家到地图中去
        Sprite player = (Sprite)playerSprite.clone();
        player.setX(TileMapRenderer.tilesToPixels(3));
        player.setY(0);
        newMap.setPlayer(player);
		//返回新的tile地图对象
        return newMap;
    }
	
	/**
		添加小怪到地图中去,并且是指定是的位置。
		*/
    private void addSprite(TileMap map,Sprite hostSprite, int tileX, int tileY){
        if (hostSprite != null) {
            // 从“主”怪克隆小怪
            Sprite sprite = (Sprite)hostSprite.clone();
			
            // 把小怪置中
            sprite.setX(
                TileMapRenderer.tilesToPixels(tileX) +
	                (TileMapRenderer.tilesToPixels(1) -
	                sprite.getWidth()) / 2);
			
            // 在底部调试该小怪
            sprite.setY(
                TileMapRenderer.tilesToPixels(tileY + 1) -
                sprite.getHeight());
			
            // 添加该小怪到地图中去
            map.addSprite(sprite);
        }
    }
	
	
    // -----------------------------------------------------------
    // 实现装载小怪和图片的代码
    // -----------------------------------------------------------
	
	
    public void loadTileImages() {
        //保存查找A,B,C等字符,这样可以非常方便的删除images目录下的tiles
        tiles = new ArrayList();
        char ch = 'A';
        while (true) {
            String name = "tile_" + ch + ".png";
            File file = new File("images/" + name);
            if (!file.exists()) {
                break;
            }
            tiles.add(loadImage(name));
            ch++;
        }
    }
	
	
    public void loadCreatureSprites() {
		//声明一个图片至少保存4个图片的数组
        Image[][] images = new Image[4][];
		
        // 装载左边朝向的图片
        images[0] = new Image[] {
        	//装载玩家图片
            loadImage("player1.png"),
            loadImage("player2.png"),
            loadImage("player3.png"),
            //装载苍蝇图片
            loadImage("fly1.png"),
            loadImage("fly2.png"),
            loadImage("fly3.png"),
            //装载蠕虫图片
            loadImage("grub1.png"),
            loadImage("grub2.png"),
        };
		
        images[1] = new Image[images[0].length];
        images[2] = new Image[images[0].length];
        images[3] = new Image[images[0].length];
        for (int i = 0; i < images[0].length; i++) {
            // 装载右朝向的图片
            images[1][i] = getMirrorImage(images[0][i]);
            // 装载左朝向“死亡”图片
            images[2][i] = getFlippedImage(images[0][i]);
            // 装载右朝向“死亡”图片
            images[3][i] = getFlippedImage(images[1][i]);
        }
		
        // 创建creature动画对象
        Animation[] playerAnim = new Animation[4];
        Animation[] flyAnim = new Animation[4];
        Animation[] grubAnim = new Animation[4];
        for (int i = 0; i < 4; i++) {
            playerAnim[i] = createPlayerAnim(
                images[i][0], images[i][1], images[i][2]);
            flyAnim[i] = createFlyAnim(
                images[i][3], images[i][4], images[i][5]);
            grubAnim[i] = createGrubAnim(
                images[i][6], images[i][7]);
        }
		
        // 创建creature小怪(包括玩家)
        playerSprite = new Player(playerAnim[0], playerAnim[1],
            playerAnim[2], playerAnim[3]);
        flySprite = new Fly(flyAnim[0], flyAnim[1],
            flyAnim[2], flyAnim[3]);
        grubSprite = new Grub(grubAnim[0], grubAnim[1],
            grubAnim[2], grubAnim[3]);
    }
	
	//根据指定的图片来创建玩家动画对象
    private Animation createPlayerAnim(Image player1,Image player2, Image player3){
        Animation anim = new Animation();
        anim.addFrame(player1, 250);
        anim.addFrame(player2, 150);
        anim.addFrame(player1, 150);
        anim.addFrame(player2, 150);
        anim.addFrame(player3, 200);
        anim.addFrame(player2, 150);
        return anim;
    }
	
	//根据指定的图片来创建苍蝇动画对象
    private Animation createFlyAnim(Image img1, Image img2,Image img3){
        Animation anim = new Animation();
        anim.addFrame(img1, 50);
        anim.addFrame(img2, 50);
        anim.addFrame(img3, 50);
        anim.addFrame(img2, 50);
        return anim;
    }
	
	//根据指定的图片来创建蠕虫动画对象
    private Animation createGrubAnim(Image img1, Image img2) {
        Animation anim = new Animation();
        anim.addFrame(img1, 250);
        anim.addFrame(img2, 250);
        return anim;
    }
	
	
    private void loadPowerUpSprites() {
        // 创建“心”怪用来加体力
        Animation anim = new Animation();
        anim.addFrame(loadImage("heart1.png"), 150);
        anim.addFrame(loadImage("heart2.png"), 150);
        anim.addFrame(loadImage("heart3.png"), 150);
        anim.addFrame(loadImage("heart2.png"), 150);
        goalSprite = new PowerUp.Goal(anim);
		
        // 创建 "星"怪,用来加分
        anim = new Animation();
        anim.addFrame(loadImage("star1.png"), 100);
        anim.addFrame(loadImage("star2.png"), 100);
        anim.addFrame(loadImage("star3.png"), 100);
        anim.addFrame(loadImage("star4.png"), 100);
        coinSprite = new PowerUp.Star(anim);
		
        // 创建“音乐”怪用来加速玩家
        anim = new Animation();
        anim.addFrame(loadImage("music1.png"), 150);
        anim.addFrame(loadImage("music2.png"), 150);
        anim.addFrame(loadImage("music3.png"), 150);
        anim.addFrame(loadImage("music2.png"), 150);
        musicSprite = new PowerUp.Music(anim);
    }
}

绘制Tile地图

我们知道tile地图大于屏幕,所以只有一部分地图同一时间在屏幕上显示。玩家的移动的原理是:地图滚动来保持玩家在屏幕的中央位置如下图所示:

Java游戏编程不完全详解-5_游戏开发_05

它的计算公式如下:

int offsetX = screenWidth / 2 –	Math.round(player.getX()) – TITLE_SIZE;

该公式把屏幕的水平位置赋值给offsetX变量,这个公式不复杂,所以我们需要确保玩家在离开左边边缘到地图右边边缘时,地图滚动必须停止,这样地图的边缘不会被显示在屏幕上。于是,我们需要这样限制:

int mapWidth = titlesToPixels(map.getWidth());
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);
int offsetY = screenHeight – titlesToPixels(map.getHeight());

完整的计算公式如下图所示:

Java游戏编程不完全详解-5_游戏开发_06

完整的代码参见TileMapRenderer类。

TileMapRenderer类

package com.funfree.arklis.engine.tilegame;
import java.awt.*;
import java.util.Iterator;
import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;

/**
    功能:该类用来绘制TileMap到屏幕中,是一个核心工具类。它绘制所有tile、小怪(包括玩家),以及可选的
    	背景图片到玩家的位置。如果背景图片宽度小于tile地图的宽度,那么背景图片会慢慢移动出现,从而产生
    	视差效果给玩家。有三个静态方法用来转换像素到tile的位置,返回既然。注意:该类使用tile的尺寸是64位
    备注:该类是核心类,它完成tile的呈现功能。
*/
public class TileMapRenderer {
	//指定tile的固定大小
    private static final int TILE_SIZE = 64;
    //使用bit为单位表示tile的尺寸 Math.pow(2,TILE_SIZE_BITS) == TILE_SIZE
    private static final int TILE_SIZE_BITS = 6;
	
    private Image background;
	
    /**
        转换像素的位置为tile的位置
    */
    public static int pixelsToTiles(float pixels) {
        return pixelsToTiles(Math.round(pixels));
    }


    /**
        转换像素的位置为tile的位置
    */
    public static int pixelsToTiles(int pixels) {
        //把正确的值转换成负值像素
        return pixels >> TILE_SIZE_BITS;
		
        //或者使用floor方法,而不是power函数计算tile的尺寸
        //return (int)Math.floor((float)pixels / TILE_SIZE);
    }


    /**
        转换tile的位置为像素的位置
    */
    public static int tilesToPixels(int numTiles) {
        //该方法移位的目标是加速作用,但是实际上对于现代的处理器效果不大
        return numTiles << TILE_SIZE_BITS;
		//或者使用乘法运算,而不使用power方法计算tile的尺寸 
        //return numTiles * TILE_SIZE;
    }


    /**
        设置背景图片
    */
    public void setBackground(Image background) {
        this.background = background;
    }


    /**
        绘制指定的TileMap
    */
    public void draw(Graphics2D g, TileMap map,int screenWidth, int screenHeight){
    	//取得玩家角色
        Sprite player = map.getPlayer();
        //获取的宽度
        int mapWidth = tilesToPixels(map.getWidth());
		
        //根据玩家当前的位置获取地图滚动的位置(屏幕宽度一半减去当前玩家X坐标减去TILE_SIZE)
        int offsetX = screenWidth / 2 -
            Math.round(player.getX()) - TILE_SIZE;
        offsetX = Math.min(offsetX, 0);
        offsetX = Math.max(offsetX, screenWidth - mapWidth);
		
        // 取得y的偏移量,然后绘制所有sprites和tiles
        int offsetY = screenHeight -
            tilesToPixels(map.getHeight());
		
        // 绘制黑色背景
        if (background == null ||
            screenHeight > background.getHeight(null)){
            g.setColor(Color.black);
            g.fillRect(0, 0, screenWidth, screenHeight);
        }
		
        // 绘制视觉差效果的背景图片
        if (background != null) {
            int x = offsetX *
                (screenWidth - background.getWidth(null)) /
                (screenWidth - mapWidth);
            int y = screenHeight - background.getHeight(null);
			
            g.drawImage(background, x, y, null);
        }
		
        // 绘制可视化的tiles
        int firstTileX = pixelsToTiles(-offsetX);
        int lastTileX = firstTileX + pixelsToTiles(screenWidth) + 1;
        for (int y = 0; y < map.getHeight(); y++) {
            for (int x = firstTileX; x <= lastTileX; x++) {
                Image image = map.getTile(x, y);
                if (image != null) {
                    g.drawImage(image,
                        tilesToPixels(x) + offsetX,
                        tilesToPixels(y) + offsetY,
                        null);
                }
            }
        }
		
        // 绘制玩家
        g.drawImage(player.getImage(),
            Math.round(player.getX()) + offsetX,
            Math.round(player.getY()) + offsetY,
            null);
        
        // 绘制小怪
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite sprite = (Sprite)i.next();
            int x = Math.round(sprite.getX()) + offsetX;
            int y = Math.round(sprite.getY()) + offsetY;
            g.drawImage(sprite.getImage(), x, y, null);
			
            // 当sprite对象在屏幕上时需要唤醒creature对象
            if (sprite instanceof Creature &&
                x >= 0 && x < screenWidth){
                ((Creature)sprite).wakeUp();
            }
        }
    }
}

绘制Sprites

在绘制tile之后,我们需要绘制sprite图形。这里我们分开来绘制sprite对象,它的思路如下:

  • 区分sprite与屏幕尺寸的区域,只在屏幕中可视部分绘制sprite对象。当sprite移动时,它们被存贮在确定的区域
  • 保存sprite在一个有序的列表中,保存的顺序是sprite的从左到右水平位置。跟踪列表中的第一个可视化的sprite对象,当sprite移动时,确保该列表是被保存过的
  • 实现列表中的每个sprite的run方法,检查它们是可视的

前两个思路不用置疑的,因为在地图有许多sprite对象,但是,不是每张地图都有很多sprite对象,所以你可brute-force方法来检查每个sprite是否可视。也就是遍历列表,绘制出每个sprite对象:

Iterator i = map.getSprites();
while(i.hasNext()){
	Sprite sprite = (Sprite)i.next();
	int x = Math.round(sprite.getX()) + offsetX;
	int y = Math.round(sprite.getY()) + offsetY;
	g.drawImage(sprite.getImage(),x,y,null);
}

视差滚动

现在我们已经绘制了tile和sprite对象,接下面我们绘制背景。当我们绘制背景时,我们需要怎样把背景合成为地图,实现的策略如下:

  • 保持背景不动,所以我们不需要在滚动地图滚动背景图片
  • 使用与地图滚动的相同速率来滚动背景地图
  • 滚地背景的速率比滚动地图的速率小一些,那么可以让背景出现远去的效果

第三种方式我们叫”parallax scrolling”(视觉差滚动),视差出现的原理是:从不同的视点让对象在不同的位置出现。比如,我们讲过不会使用巨幅图片表示背景,如果我们要创建背景地图是屏幕的两倍,背景从屏幕的第一个屏幕到第二个屏幕

Java游戏编程不完全详解-5_游戏开发_07

Java游戏编程不完全详解-5_游戏开发_08

Java游戏编程不完全详解-5_游戏开发_09

GameManager类

package com.funfree.arklis.engine.tilegame;
import java.awt.*;
import java.awt.event.*;
import java.util.Iterator;
import static java.lang.System.*;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.sampled.AudioFormat;
import com.funfree.arklis.sounds.*;
import javax.swing.border.*;
import javax.swing.*;
import com.funfree.arklis.engine.tilegame.*;
import com.funfree.arklis.engine.*;
import com.funfree.arklis.input.*;
import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;


/**
    功能:该类管理游戏中所有方面,包括碰撞侦测等等。
*/
public class GameManager extends GameCore {
	
    // 示压缩4400Hz、16位、单声道little-endian顺序
    private static final AudioFormat PLAYBACK_FORMAT =
        new AudioFormat(44100, 16, 1, true, false);
	
    private static final int DRUM_TRACK = 1;
	
    public static final float GRAVITY = 0.002f;
	
    private Point pointCache = new Point();
    private TileMap map;
    private MidiPlayer midiPlayer;
    private SoundManager soundManager;
    private ResourceManager resourceManager;
    private Sound prizeSound;
    private Sound boopSound;
    private InputManager inputManager;
    private TileMapRenderer renderer;
    
	//玩家的四个游戏行为
    private GameAction moveLeft;
    private GameAction moveRight;
    private GameAction jump;
    private GameAction exit;
	
	
    public void init() {
        super.init();
		
        // 设置输入管理器
        initInput();
		
        // 启动资源管理器
        resourceManager = new ResourceManager(
        screen.getFullScreenWindow().getGraphicsConfiguration());
		
        // 装载资源
        renderer = new TileMapRenderer();
        renderer.setBackground(
            resourceManager.loadImage("background.jpg"));
		
        // 载入第一个地图
        map = resourceManager.loadNextMap();
		
        // 载入声音
        soundManager = new SoundManager(PLAYBACK_FORMAT);
        prizeSound = soundManager.getSound("sounds/prize.wav");
        boopSound = soundManager.getSound("sounds/boop2.wav");
		
        // 播放声音
        midiPlayer = new MidiPlayer();
        Sequence sequence =
            midiPlayer.getSequence("sounds/music.midi");
        midiPlayer.play(sequence, true);
        toggleDrumPlayback();
    }
	
    
    /**
		放一个接口方法,以便关联类使用,比如InputComponent类使用。
		*/
	public InputManager getInputManager(){
		return inputManager;
	}
    
    /**
		添加有游戏行为的名称,以便让InputComponent使用这些文字来修改被影射的键
		*/
	private void addActionConfig(JPanel configPanel, GameAction action){
		JLabel label = new JLabel(action.getName(), JLabel.RIGHT);
		com.funfree.arklis.engine.InputComponent input = 
			new com.funfree.arklis.engine.InputComponent(action,this);
		configPanel.add(label);
		configPanel.add(input);
		getList().add(input);//放到集合中保存起来
	}
	
	
	
    /**
        关闭所有的被使用的资源
    */
    public void stop() {
        super.stop();
        midiPlayer.close();
        soundManager.close();
    }
	
	
    private void initInput() {
        moveLeft = new GameAction("左移");
        moveRight = new GameAction("右移");
        jump = new GameAction("跳",
            GameAction.DETECT_INITIAL_PRESS_ONLY);
        exit = new GameAction("退出",
            GameAction.DETECT_INITIAL_PRESS_ONLY);
	
        inputManager = new InputManager(
            screen.getFullScreenWindow());
        inputManager.setCursor(InputManager.INVISIBLE_CURSOR);
	
        inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT);
        inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT);
        inputManager.mapToKey(jump, KeyEvent.VK_SPACE);
        inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);
    }
	
	/**
		检查输入
		*/
    private void checkInput(long elapsedTime) {
		
        if (exit.isPressed()) {
            stop();
        }
		
        Player player = (Player)map.getPlayer();
        if (player.isAlive()) {
            float velocityX = 0;
            if (moveLeft.isPressed()) {
                velocityX -= player.getMaxSpeed();
            }
            if (moveRight.isPressed()) {
                velocityX += player.getMaxSpeed();
            }
            if (jump.isPressed()) {
                player.jump(false);
            }
            player.setVelocityX(velocityX);
        }
    }
	
	//这里最关键的部分是使用呈现器TileMapRenderer类来封装所有复杂的呈现过程。
    public void draw(Graphics2D g) {
        renderer.draw(g, map,
            screen.getWidth(), screen.getHeight());
    }
	
	
    /**
        获取当前的地图
    */
    public TileMap getMap() {
        return map;
    }
	
	
    /**
        打开/关闭背景midi音乐
    */
    public void toggleDrumPlayback() {
        Sequencer sequencer = midiPlayer.getSequencer();
        if (sequencer != null) {
            sequencer.setTrackMute(DRUM_TRACK,
                !sequencer.getTrackMute(DRUM_TRACK));
        }
    }
	
	
    /**
        获取Sprite碰撞的tile对象。只需要Sprite的x和y值需要被修改,但是不是同时修改。
        如果返回null表示没有侦测到Sprite的碰撞。该方法是实现游戏的核心方法!
    */
    public Point getTileCollision(Sprite sprite,float newX, float newY){
        float fromX = Math.min(sprite.getX(), newX);
        float fromY = Math.min(sprite.getY(), newY);
        float toX = Math.max(sprite.getX(), newX);
        float toY = Math.max(sprite.getY(), newY);
		
        // 获取tile的位置
        int fromTileX = TileMapRenderer.pixelsToTiles(fromX);
        int fromTileY = TileMapRenderer.pixelsToTiles(fromY);
        int toTileX = TileMapRenderer.pixelsToTiles(
            toX + sprite.getWidth() - 1);
        int toTileY = TileMapRenderer.pixelsToTiles(
            toY + sprite.getHeight() - 1);
		
        // 检查是否有碰撞的title
        for (int x = fromTileX; x <= toTileX; x++) {
            for (int y = fromTileY; y <= toTileY; y++) {
                if (x < 0 || x >= map.getWidth() ||
                    map.getTile(x, y) != null){
                    // 碰撞发现了,返回碰撞的tile
                    pointCache.setLocation(x, y);
                    return pointCache;
                }
            }
        }
        // 没有碰撞
        return null;
    }
	
	
    /**
        检查两个Sprite是否发生了碰撞。如果两个对象同一种类,那么返回false值。如果
        一个Sprites是Creatue类并且是死的,那么也返回false值。
    */
    public boolean isCollision(Sprite s1, Sprite s2) {
        // 如果两个Sprite是同一对象,那么返回false值
        if (s1 == s2) {
            return false;
        }
		
        // 如果有一个Sprite是死的,那么返回false值
        if (s1 instanceof Creature && !((Creature)s1).isAlive()) {
            return false;
        }
        if (s2 instanceof Creature && !((Creature)s2).isAlive()) {
            return false;
        }
		
        // 否则获取Sprite的像素位置
        int s1x = Math.round(s1.getX());
        int s1y = Math.round(s1.getY());
        int s2x = Math.round(s2.getX());
        int s2y = Math.round(s2.getY());
		
        // 然后检查两个sprite的边界是否交叉
        return (s1x < s2x + s2.getWidth() &&
            s2x < s1x + s1.getWidth() && 
            s1y < s2y + s2.getHeight() && 
            s2y < s1y + s1.getHeight());
    }
	
	
    /**
        获取与指定Sprite碰撞的Sprite对象,如果返回null值,那么表示Sprite没有与指定的Sprite碰撞。
    */
    public Sprite getSpriteCollision(Sprite sprite) {
		
        // 遍历Sprite列表
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite otherSprite = (Sprite)i.next();
            if (isCollision(sprite, otherSprite)) {
                // 如果发现碰撞,那么返回该Sprite对象
                return otherSprite;
            }
        }
		
        // 否则没有碰撞发生
        return null;
    }
	
	
    /**
        更新当前地图中的所有Sprite的Animation、position和速率。
    */
    public void update(long elapsedTime) {
        Creature player = (Creature)map.getPlayer();
		
        // 如果玩家死亡!那么重新启动地图
        if (player.getState() == Creature.STATE_DEAD) {
            map = resourceManager.reloadMap();
            return;
        }
		
        // 获取键盘/鼠标的输入
        checkInput(elapsedTime);
		
        // 更新玩家
        updateCreature(player, elapsedTime);
        player.update(elapsedTime);
		
        // 更新其它的sprite对象
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite sprite = (Sprite)i.next();
            if (sprite instanceof Creature) {
                Creature creature = (Creature)sprite;
                if (creature.getState() == Creature.STATE_DEAD) {
                    i.remove();
                }else {
                    updateCreature(creature, elapsedTime);
                }
            }
            // 普通更新
            sprite.update(elapsedTime);
        }
    }
	
	
    /**
        更新create对象,让所有creaute没有飞行的下降,然后检查它们是否有碰撞
    */
    private void updateCreature(Creature creature,long elapsedTime){
		
        // 应用加速度
        if (!creature.isFlying()) {
            creature.setVelocityY(creature.getVelocityY() +
                GRAVITY * elapsedTime);
        }
		
        // 修改x值
        float dx = creature.getVelocityX();
        float oldX = creature.getX();
        float newX = oldX + dx * elapsedTime;
        Point tile = getTileCollision(creature, newX, creature.getY());
        if (tile == null) {
            creature.setX(newX);
        }else {
            // 画出tile边界
            if (dx > 0) {
                creature.setX(
                    TileMapRenderer.tilesToPixels(tile.x) -
                    creature.getWidth());
            }else if (dx < 0) {
                creature.setX(
                    TileMapRenderer.tilesToPixels(tile.x + 1));
            }
            creature.collideHorizontal();
        }
        if (creature instanceof Player) {
            checkPlayerCollision((Player)creature, false);
        }
		
        // 修改y值
        float dy = creature.getVelocityY();
        float oldY = creature.getY();
        float newY = oldY + dy * elapsedTime;
        tile = getTileCollision(creature, creature.getX(), newY);
        if (tile == null) {
            creature.setY(newY);
        }else {
            // 画出tile的边界
            if (dy > 0) {
                creature.setY(
                    TileMapRenderer.tilesToPixels(tile.y) -
                    creature.getHeight());
            }else if (dy < 0) {
                creature.setY(
                    TileMapRenderer.tilesToPixels(tile.y + 1));
            }
            creature.collideVertical();
        }
        if (creature instanceof Player) {
            boolean canKill = (oldY < creature.getY());
            checkPlayerCollision((Player)creature, canKill);
        }
	
    }
	
	
    /**
        检查玩家是否与其它的Sprite发生碰撞。如果可以玩家Sprite对象,那么返回true值。
    */
    public void checkPlayerCollision(Player player,boolean canKill){
    	//如果玩家死亡
        if (!player.isAlive()) {
            return;//那么不做为
        }
		
        // 检查玩家是否与其它的sprite发生碰撞
        Sprite collisionSprite = getSpriteCollision(player);
        if (collisionSprite instanceof PowerUp) {
            acquirePowerUp((PowerUp)collisionSprite);
        }
        else if (collisionSprite instanceof Creature) {
            Creature badguy = (Creature)collisionSprite;
            if (canKill) {
                // 杀死小怪,然后让玩家弹起
                soundManager.play(boopSound);
                badguy.setState(Creature.STATE_DYING);
                player.setY(badguy.getY() - player.getHeight());
                player.jump(true);
            }
            else {
                // 否则玩家死亡!
                player.setState(Creature.STATE_DYING);
            }
        }
    }
	
	
    /**
        给玩家加技能、分、体力,然后从地图上移除这些元素。
    */
    public void acquirePowerUp(PowerUp powerUp) {
        // 从地图移除poswerUp
        map.removeSprite(powerUp);
	
        if (powerUp instanceof PowerUp.Star) {
            // 给玩家加点
            soundManager.play(prizeSound);
        }else if (powerUp instanceof PowerUp.Music) {
            // 修改音乐
            soundManager.play(prizeSound);
            toggleDrumPlayback();
        }else if (powerUp instanceof PowerUp.Goal) {
            // 进入到下一张地图
            soundManager.play(prizeSound,
                new EchoFilter(2000, .7f), false);
            map = resourceManager.loadNextMap();
        }
    }
    
    /**
		功能:该方法是一个非常重要的辅助方法--用来创建游戏的菜单
		*/
	private JButton createButton(String name, String toolTip){
		//装载图片
		String imagePath = "images/menu/" + name + ".png";
		ImageIcon iconRollover = new ImageIcon(imagePath);
		int w = iconRollover.getIconWidth();
		int h = iconRollover.getIconHeight();
		//给当前按钮设置光标的样式
		Cursor cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
		//让默认的图片透明
		Image image = screen.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
		Graphics2D g = (Graphics2D)image.getGraphics();
		Composite alpha = AlphaComposite.getInstance(AlphaComposite.DST_OVER, .5f);
		g.setComposite(alpha);//设置透明度
		g.drawImage(iconRollover.getImage(),0,0,null);
		g.dispose();
		ImageIcon iconDefault = new ImageIcon(image);
		//显示被按下的图片
		image = screen.createCompatibleImage(w,h,Transparency.TRANSLUCENT);
		g = (Graphics2D)image.getGraphics();
		g.drawImage(iconRollover.getImage(),2,2,null);
		g.dispose();
		ImageIcon iconPressed = new ImageIcon(image);
		//创建按钮对象
		JButton button = new JButton();
		button.addActionListener(this);
		button.setIgnoreRepaint(true);
		button.setFocusable(false);
		button.setToolTipText(toolTip);
		button.setBorder(null);
		button.setContentAreaFilled(false);
		button.setCursor(cursor);
		button.setIcon(iconDefault);
		button.setRolloverIcon(iconRollover);
		button.setPressedIcon(iconPressed);
		
		return button;
	}
}

在我们计算了offsetX—它表示地图在屏幕中的位置,所以我们需要一个公式把offsetX转换成backgroundX的值。offsetX的范围是从0(地图的左半部分开始)到screenWidth-mapWidth(地图的右半部分),这样匹配backgroundX的范围(从0到screenWidth – backgound.getWidth(null))的值。这样我们可插入点:

int backgroundX = offsetX *
	(screenWidth – background.getWidth(null)) /
	(screenWidth - mapWidth);
g.drawImage(background, backgroundX,0,null);

使用以上公式的条件是:

  • 背景图片的宽度大于屏幕的点阵宽度
  • 地图的宽度大背景图片的宽度

最后件事件就是:我们可以不使用图片使用背景,我们可以使用另外一个TileMap来自由绘制背景,它甚至可以与地图一样大,所以我们不必设置滚动的背景,而只需要把两个滚动速率调不一致即可,并且让前景地图是透明的,那么背景就可以看见了。如果我们需要使用背景作为tile图片,那么需要确保右边界匹配左边界,这样才会出无缝滚动效果。

Power-Ups

有一个事件我们必须做,那么就是使用close方法实现游戏中所有的sprite对象,PowerUp类个通用的clone()方法,用来反射克隆的对象,包含它的子类。在该方法中,它选择第一个构造方法来创建新的对象的实例。注意:PowerUp的子类基本不做实际的事件,它们只是一个占位符而忆,这些子类可以实现的功能如下:

  • 当玩家需要一个星、一个声音时,但是不需要其它的行为发生
  • 当玩家需要玩家播放音乐时,背景音乐可以开和关
  • 最后,当玩家需要“goal”时,装载下一个地图

Java游戏编程不完全详解-5_游戏开发_10

在示例游戏中有两种坏蛋,一个苍蝇和蠕虫。它们一直运行,直到碰到墙为止。在实现坏蛋之前,我们来看一下原图玩家与坏蛋都是面朝左的。所以,当我们需要让玩家向右移动时,那么玩家必须面向右,这就需要我们动态创建玩家面朝向右。

Java游戏编程不完全详解-5_游戏开发_11

参见前面的ResourceManager类。

Creature类

在游戏中每个坏蛋都四种不同的游戏行为:

  • 左移
  • 右移
  • 面朝左死亡
  • 面朝右死亡

这样,我们通过动画类Animation来修改sprite的不同方向和死亡的样子。玩家有三种状态:STATE_NORMAL、STATE_DYING和STATE_DEAD状态。从死亡到已死亡只有一秒钟的时间。该类向Sprite添加了如下功能:

  1. wakeUp()方法在坏蛋第一次出现在屏幕中时被呼叫,这时,该方法呼叫setVelocityX(-getMaxSpeed())来开始一个坏蛋的移动,如果玩家没有看见坏蛋时,坏蛋不会移动的。
  2. isAlive()和isFlying方法可以方便检查坏蛋的当前状态。比如坏蛋死亡、是否伤害到玩家,正在飞行的坏蛋不能下降等
  3. 最后collideVertical()和collideHorizontal方法当坏蛋碰到一个tile时被呼叫。如果垂直碰撞了,那么坏蛋的垂直速率设置为0,水平碰撞一样。
package com.funfree.arklis.engine.tilegame.sprites;
import java.lang.reflect.Constructor;
import com.funfree.arklis.graphic.*;

/**
    功能:该类是一个Sprite类,用来表示下降或者死亡的Sprite对象。它有游戏行为四个:
    	左移、右移、左移死亡和右移死亡
*/
public abstract class Creature extends Sprite {

    /**
        从Dying到DEAD的时间:
    */
    private static final int DIE_TIME = 1000;
	
    public static final int STATE_NORMAL = 0;
    public static final int STATE_DYING = 1;
    public static final int STATE_DEAD = 2;
	//指定游戏行为
    private Animation left;
    private Animation right;
    private Animation deadLeft;
    private Animation deadRight;
    private int state;
    private long stateTime;

    /**
        使用指定的游戏行为创建Creature对象
    */
    public Creature(Animation left, Animation right,
        Animation deadLeft, Animation deadRight){
        super(right);
        this.left = left;
        this.right = right;
        this.deadLeft = deadLeft;
        this.deadRight = deadRight;
        state = STATE_NORMAL;
    }
    //针对Player和InputManagerTest类修订构造方法--可能无意义:为让编译通过!(版本ver 1.0.1)
    public Creature(Animation action){
    	super(action);
    }
	
	/**
		从主怪克隆一个副本
		*/
    public Object clone() {
        // 使用反射技术来创建一个正确的子类对象(呼叫上面的构造方法)
        Constructor constructor = getClass().getConstructors()[0];
        try {
            return constructor.newInstance(new Object[] {
                (Animation)left.clone(),
                (Animation)right.clone(),
                (Animation)deadLeft.clone(),
                (Animation)deadRight.clone()
            });
        }catch (Exception ex) {
            // 应该不会出现,如果出现返回null值
            ex.printStackTrace();
            return null;
        }
    }


    /**
        获取该Creature的最大速度
    */
    public float getMaxSpeed() {
        return 0;
    }
	
	
    /**
        当Creature第一次出现在屏幕上时唤醒该creature对象,一般creature开始向左移动
    */
    public void wakeUp() {
        if (getState() == STATE_NORMAL && getVelocityX() == 0) {
            setVelocityX(-getMaxSpeed());
        }
    }
	
	
    /**
        得到该Creature的状态:正常STATE_NORMAL、正在死亡STATE_DYING,或者已死亡STATE_DEAD。
    */
    public int getState() {
        return state;
    }
	
	
    /**
        设置该Creature的状态:STATE_NORMAL,STATE_DYING或者STATE_DYING
    */
    public void setState(int state) {
        if (this.state != state) {
            this.state = state;
            stateTime = 0;
            if (state == STATE_DYING) {
                setVelocityX(0);
                setVelocityY(0);
            }
        }
    }
	
	
    /**
        检查该creature是否还活着
    */
    public boolean isAlive() {
        return (state == STATE_NORMAL);
    }


    /**
        检查该creature是否正在飞行
    */
    public boolean isFlying() {
        return false;
    }
	
	
    /**
        在update()方法之前呼叫,如果该creature与水平的tile发生碰撞情况的话。
    */
    public void collideHorizontal() {
        setVelocityX(-getVelocityX());
    }
	
	
    /**
        在update()方法之前呼叫,如果出现垂直碰撞的话
    */
    public void collideVertical() {
        setVelocityY(0);
    }
	
	
    /**
        更新该creature对象
    */
    public void update(long elapsedTime) {
        // 选择正确的游戏行为
        Animation newAnim = anim;
        if (getVelocityX() < 0) {
            newAnim = left;
        }else if (getVelocityX() > 0) {
            newAnim = right;
        }if (state == STATE_DYING && newAnim == left) {
            newAnim = deadLeft;
        }else if (state == STATE_DYING && newAnim == right) {
            newAnim = deadRight;
        }
		
        // 更新游戏行为
        if (anim != newAnim) {
            anim = newAnim;
            anim.start();
        }else {
            anim.update(elapsedTime);
        }
		
        // 变更死亡状态
        stateTime += elapsedTime;
        if (state == STATE_DYING && stateTime >= DIE_TIME) {
            setState(STATE_DEAD);
        }
    }
}

Player类

该类向Creature类添加了跳(jump)的功能。大多数情况下,我们希望玩家跳,如果玩家不在地上,所以重写setY()和方法collideVertical()方法可以追踪该玩家是否在地上。如果玩家在地上,那么该玩家可以跳,另外我们可强制玩家跳(jump(true))。

package com.funfree.arklis.engine.tilegame.sprites;
import com.funfree.arklis.graphic.*;

/**
    功能:这是一个玩家
    备注:根据第四章的Player来修订的类。
*/
public class Player extends Creature {
    private static final float JUMP_SPEED = -.95f; //设置跳的速度
    //版本ver 1.0.1添加开始
    public static final int STATE_NORMAL = 0;
	public static final int STATE_JUMPING = 1;
	public static final float SPEED = .3F;
	public static final float GRAVITY = .002F;
    private boolean onGround; //标识是否在地上
   	
   	private int floorY;
	private int state;
	//版本ver 1.0.1添加结束
	
    public Player(Animation left, Animation right,
        Animation deadLeft, Animation deadRight){
        super(left, right, deadLeft, deadRight);
    }
    
    //版本ver 1.0.1添加开始
    public Player(Animation animation){
		super(animation);
		state = STATE_NORMAL;
	}
	
	
	public int getState(){
		return state;
	}
	
	public void setState(int state){
		this.state = state;
	}
	
	
	
	/**
		设置floor的位置,不管玩家是否开始跳,或者已经着陆
		*/
	public void setFloorY(int floorY){
		this.floorY = floorY;
		setY(floorY);
	}
	
	/**
		让玩家产生的跳的动作
		*/
	public void jump(){
		setVelocityY(-1);
		state = STATE_JUMPING;
	}
	//版本ver 1.0.1添加结束
	
	
    public void collideHorizontal() {
        setVelocityX(0);
    }
	
	
    public void collideVertical() {
        // 检查是否碰撞到地上
        if (getVelocityY() > 0) {
            onGround = true;
        }
        setVelocityY(0);
    }
	
	
    public void setY(float y) {
        // 检查是否落下
        if (Math.round(y) > Math.round(getY())) {
            onGround = false;
        }
        super.setY(y);
    }
	
	
    public void wakeUp() {
        // do nothing
    }
	
	
    /**
        如果玩家在地下,或者强制中是true时,那么让玩家跳
    */
    public void jump(boolean forceJump) {
        if (onGround || forceJump) {
            onGround = false;
            setVelocityY(JUMP_SPEED);
        }
    }
	
	//获取最大的移动速度
    public float getMaxSpeed() {
        return 0.5f;
    }
    
    
	/**
		更新玩家的位置和动作,也可以设置玩家的状态为NORMAL状态,如果玩家已经着陆了
		在这里可能无意义,因为需要让编译通过。版本ver 1.0.1添加方法
		*/
	public void update(long elapsedTime){
		//设置水平位置的速率(下降效果)
		if(getState() == STATE_JUMPING){
			setVelocityY(getVelocityY() + GRAVITY * elapsedTime);
		}
		//移动玩家
		super.update(elapsedTime);
		//检查玩家是否着陆
		if(getState() == STATE_JUMPING && getY() >= floorY){
			setVelocityY(0);
			setY(floorY);
			setState(STATE_NORMAL);
		}
	}
}

碰撞侦测

游戏中我们必须确保玩家和坏蛋不能穿墙而过,但可以平台上跳。每次我们移动玩家或者坏蛋时,我们需要检查该creature是否与其它的tile发生了碰撞;如果是,那么我们必须调整相应的位置。因为我们使用基于tile的地图,所以碰撞侦测技术比较容易实现。理论上说,一个sprite可以一次跨多个tile,并且可一次可以定位在四个不同tile上。所以, 需要不断检查当前tile是否有sprite占用,并且每个sprite将要占用的下一下tile对象。在GameManager类的getTileCollision()方法就中完成该任务的。它检查一个sprite从原来的位置到新的位置是否跨跳了任何solid tile对象,如果是这样的情况,那么返回与sprite碰撞的tile的位置,否则返回null值。另外,该方法可以处理地图左边界或者右边界是否与create发生了碰撞,以保证creature对象在地图。

注意:该方法在处理一个sprite在多个帧之间跨跳多个tile时不是很完美,需要使用第十一章的sprite-to-environment碰撞侦测来完美,但是该代码可以处理大多数的碰撞情况了。

处理一个碰撞

Java游戏编程不完全详解-5_游戏开发_12如果sprite往下移,然后再右移时,我们会侦测到sprite会被碰撞tile的情况。同时,上面的sprite右移也会碰撞到tile,直观看很容易解决这些问题:让两个sprite左移一点就可以了。但是怎样计算左移、不上、不下和右移偏移量?

Java游戏编程不完全详解-5_游戏开发_13为解决这个问题,首先我们把sprite的移支分解成两个部分:水平移动和垂直移动。所以,我们首先解决水平移动的碰撞侦测。

Java游戏编程不完全详解-5_游戏开发_14如果一个碰撞被侦测到,那么只需要让sprite沿原路修正一下即可,从而限制了该sprite与title的边缘。

Java游戏编程不完全详解-5_游戏开发_15Sprite的水平移动碰撞解决之后,那么同样对于sprite的垂直移动问题,也是一样的解决方式。代码参见GameManager类的updateCreature方法。

updateCreature()方法也可以用于没有的飞行的creature的下降碰撞处理。因为下降总是会影响creature的,但是如果creature站在title上,那么该效果不明显,因为creature与tile之间是标准的碰撞。当一个碰撞被侦测到并且被校正之后,我们会呼叫creature的collideVertical()和collideHorizontal()方法。通过这些方法用来修改或者保存该creature的速率,防止再次发生碰撞。对于sprite的碰撞,如果sprite是一个player(玩家),那么它与其它sprite碰撞时,比如power-up和坏蛋在此示例游戏程序中,我们忽略这些碰撞,只是调整玩家的侦测碰撞,这样可以我们看到哪个玩家的sprite边界与另外一个sprite的边界发生了重叠。完成该功能的方法是GameManager类中的isCollision()方法。因为TileMap包含了所有的sprite列表,所以我们可以从这个列表中检查它们与否与玩家发生了碰撞。如下图

Java游戏编程不完全详解-5_游戏开发_16完整代码参见GameManager类中的getSpriteCollision方法。

当玩家与一个sprite发生完全碰撞时,如果该sprite是power-up,那么你可给玩家加点、播放声音,或者所有power-up支持的技能。如果sprite是一个坏蛋,那么我们可杀死坏蛋或者玩家。这时玩家落下,或者跳下时,换句话说,玩家的垂直运行是增加的:

 boolean canKill = (oldY < player.getY());

当玩家行走时碰撞到坏蛋,那么玩家死亡。

现在我们已经把创建了所有的游戏的元素,比如键盘输入、声音、音乐等这些基本元素。

完成游戏

  1. GameManager类处理键盘处理、更新sprite,提供碰撞侦测,以及播放声音和音乐
  2. TileMapRenderer类绘制地图、视觉差背景和sprite对象
  3. ResourceManager类装载图片,创建动画和sprite图形。

在控制台中运行命令:java –jar bee.jar

或者双击bee.jar即可运行程序!

总结

完成Java 2D游戏比较简单,我们只要完成三个核心类的书写,那么就有具备一个游戏引擎的功能,剩下的就是研究、扩展我们的Player类和Creature类就中完成各种游戏中的人物、NPC的功能。

图片来源:http://www.lyouxi.com/  游戏盒子