前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java游戏编程不完全详解-5

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

作者头像
老九君
发布2021-10-26 16:32:23
1.7K0
发布2021-10-26 16:32:23
举报
文章被收录于专栏:老九学堂
前言

代码演示环境

  • 软件环境:Windows 10
  • 开发工具:Visual Studio Code
  • JDK版本:OpenJDK 15

Java 2D单人游戏

创建基于Title的地图

在2D游戏中,地图是整体结构,或者我们叫做游戏地图(game map),通常是几个屏幕的宽度表示。有些游戏地图是屏幕的20倍;甚至是100位以上,主要特点是跨屏幕之后,让地图滚动显示,这种类型的游戏又叫做2D平台游戏(2D platform game)。

所以平台游戏是指玩家从一个平台跑到另外一平台,在其中需要跑、跳等动作,除此之外,还要避开敌人,以及采血、加体力等动作。本章我们介绍怎样创建基本的地图、地图文件、碰撞侦测、加体力、简单的敌人,以及生成背景的视差滚动效果等。

如果在游戏中如果巨幅图片,这种策略不是最好的解决方案,因为这会产生大量的内存消耗,可能会导致不装载图片。

另外,巨幅图片不能说明玩家哪个地图可以使用,哪些地图不可以使用。解决这个问题的一般策略是使用基于title的图片。tile-base地图是把地图分解决成表格,每个单元格包含一小块的图片,或者没有图片。如下图示:

基于tile的地力点有点像使用预制块来创建游戏,不是同的就是这些块的颜色不,并且可以无限制使用颜色。

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

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

TileMap类

代码语言:javascript
复制
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还包含在游戏中的小怪,小怪可以地图中任何位置,并且没有边界的限制。如下图:

装载Title的地图

下面我们需要有一个地方来保存该地图,然后在恰当的时候实际创建该地图。Tile地图游戏总是有多个级别的地图,该示例也不例外。如果我们可以很轻松的方式来创建多个地图,那么玩家可以在完成一个地图之后,然后开始下一个地图的游戏情节。

我们创建地图时呼叫TileMap的addTile()方法和addSprite()方法,该方法的灵活性非常好,但是,这样编辑地图的级别比较困难,并且代码本身也不是很优雅。所以,大多数的tile游戏有自己的地图编辑器来创建地图。这个地图编辑器是可视化添加tile和小怪到游戏中,这样做的方式是非常简捷的方式。

一般把地图保存到中介地图文件中,而这个文件是可以让游戏解析的。这样,我们可定义一个基于文本地图文件,这样我们可以编辑地图,因为tile是被定义在一个表格中的,所以文本文件中的每个字符可以表示一个tile或者是一个小怪/玩家,如下图:

其中”#”表示注释,而其它的表示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的数值,

代码语言:javascript
复制
int pixelSize = numTiles * TITLE_SIZE;

以上公式可以让sprite移动到地图上的任意一个位置,并且不需要调整tile的边界。也就是说,我们有一个灵活的方式来创建地图和解析它们,以及创建一个TileMap对象。

在示例中,所有的地图都在map文件夹中(map1.txt和map2.txt)等等。如果我们需要下一个地图,只需要让代码去寻找下一个地图文件即可;如果没有找到,代码回装载第一个地图。也就是说,我们不需要新地图,只需要在这个目录中删除地图文件即可,也不需要告诉游戏有多少个地图存在。

ResourceManager类

代码语言:javascript
复制
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地图大于屏幕,所以只有一部分地图同一时间在屏幕上显示。玩家的移动的原理是:地图滚动来保持玩家在屏幕的中央位置如下图所示:

它的计算公式如下:

代码语言:javascript
复制
int offsetX = screenWidth / 2 –  Math.round(player.getX()) – TITLE_SIZE;

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

代码语言:javascript
复制
int mapWidth = titlesToPixels(map.getWidth());
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);
int offsetY = screenHeight – titlesToPixels(map.getHeight());

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

完整的代码参见TileMapRenderer类。

TileMapRenderer类

代码语言:javascript
复制
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对象:

代码语言:javascript
复制
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”(视觉差滚动),视差出现的原理是:从不同的视点让对象在不同的位置出现。比如,我们讲过不会使用巨幅图片表示背景,如果我们要创建背景地图是屏幕的两倍,背景从屏幕的第一个屏幕到第二个屏幕

GameManager类

代码语言:javascript
复制
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))的值。这样我们可插入点:

代码语言:javascript
复制
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”时,装载下一个地图

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

参见前面的ResourceManager类。

Creature类

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

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

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

  1. wakeUp()方法在坏蛋第一次出现在屏幕中时被呼叫,这时,该方法呼叫setVelocityX(-getMaxSpeed())来开始一个坏蛋的移动,如果玩家没有看见坏蛋时,坏蛋不会移动的。
  2. isAlive()和isFlying方法可以方便检查坏蛋的当前状态。比如坏蛋死亡、是否伤害到玩家,正在飞行的坏蛋不能下降等
  3. 最后collideVertical()和collideHorizontal方法当坏蛋碰到一个tile时被呼叫。如果垂直碰撞了,那么坏蛋的垂直速率设置为0,水平碰撞一样。
代码语言:javascript
复制
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))。

代码语言:javascript
复制
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碰撞侦测来完美,但是该代码可以处理大多数的碰撞情况了。

处理一个碰撞

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

为解决这个问题,首先我们把sprite的移支分解成两个部分:水平移动和垂直移动。所以,我们首先解决水平移动的碰撞侦测。

如果一个碰撞被侦测到,那么只需要让sprite沿原路修正一下即可,从而限制了该sprite与title的边缘。

Sprite的水平移动碰撞解决之后,那么同样对于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列表,所以我们可以从这个列表中检查它们与否与玩家发生了碰撞。如下图

完整代码参见GameManager类中的getSpriteCollision方法。

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

代码语言:javascript
复制
 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的功能。

如果大家感兴趣,想要完整的代码,那么使用下面的联系方式

QQ咨询:胖达QQ:3038443845 微信加:laojiujun 老九君

记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老九学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java 2D单人游戏
    • 创建基于Title的地图
      • TileMap类
    • 装载Title的地图
      • ResourceManager类
    • 绘制Tile地图
      • TileMapRenderer类
    • 绘制Sprites
      • 视差滚动
        • GameManager类
      • Power-Ups
        • Creature类
          • Player类
            • 碰撞侦测
              • 处理一个碰撞
            • 完成游戏
            • 总结
            相关产品与服务
            全球应用加速
            全球应用加速(Global Application Acceleration Platform,GAAP)基于全球部署的节点和线路,通过高速通道、智能路由及安全防护技术,实现数据高速、稳定、安全的跨地域传输,帮助业务解决全球用户访问卡顿或者延迟过高的问题。通过图形化配置界面,只需几分钟,即可通过高速通道访问您的业务源站,并通过控制台查看通道的运行情况。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档