安卓游戏编程示例-三-

107 阅读27分钟

安卓游戏编程示例(三)

原文:zh.annas-archive.org/md5/B228CC957519C7ABCD7559EDEA0B426A

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:平台游戏 - 枪支、生命、金钱和敌人

在本章中,我们将做很多事情。首先,我们将构建一个可变射速的机枪,让它射击子弹。然后,我们将引入拾取物或收藏品。这些给玩家在尝试逃到下一个关卡时提供了搜寻的目标。

然后,就在 Bob 开始认为他的生活是充满草丛和收藏品的幸福生活时,我们将为他构建两个对手,让他智取或消灭。一个追踪无人机和一个巡逻的守卫。我们可以轻松地将所有这些事物添加到我们的关卡设计中。

准备,瞄准,开火。

现在,我们可以给我们的英雄一把枪,稍后,我们可以给他敌人射击。我们将创建一个MachineGun类来完成所有工作,以及一个Bullet类来表示它发射的炮弹。Player类将控制MachineGun类,而MachineGun类将控制和跟踪它发射的所有Bullet对象。

创建一个新的 Java 类,将其命名为Bullet。子弹并不复杂。我们的子弹需要有一个xy的位置,一个水平速度和一个方向,以帮助计算速度。

这意味着以下简单的类、构造函数以及一堆的 getter 和 setter:

public class Bullet  {

    private float x;
    private float y;
    private float xVelocity;
    private int direction;

    Bullet(float x, float y, int speed, int direction){
        this.direction = direction;
        this.x = x;
        this.y = y;
        this.xVelocity = speed * direction;
    }

    public int getDirection(){
        return direction;
    }

    public void update(long fps, float gravity){
        x += xVelocity / fps;
    }

    public void hideBullet(){
        this.x = -100;
        this.xVelocity = 0;
    }

    public float getX(){
        return x;
    }

    public float getY(){
        return y;
    }

}

现在,让我们实现MachineGun类。

创建一个新的 Java 类,将其命名为MachineGun。首先,我们添加一些成员。maxBullets变量不是玩家拥有的射击次数,那是无限的,它是MachineGun类可以拥有的子弹对象数量。对于非常快速射击的枪来说,10 个就足够了,正如我们将看到的。成员numBulletsnextBullet帮助类跟踪其 10 个子弹。rateOfFire变量控制玩家能够多快地按下射击按钮,lastShotTime通过跟踪上次发射子弹的系统时间来帮助执行rateOfFire。射速将是武器可升级的方面。

输入我们讨论过的代码如下。

import java.util.concurrent.CopyOnWriteArrayList;

public class MachineGun extends GameObject{
    private int maxBullets = 10;
    private int numBullets;
    private int nextBullet;
    private int rateOfFire = 1;//bullets per second
    private long lastShotTime;

    private CopyOnWriteArrayList<Bullet> bullets;

    int speed = 25;

注意

对于功能性目的,我们可以将存储我们子弹的CopyOnWriteArrayList bullets视为一个普通的ArrayList对象。我们使用这个更复杂且稍慢的类,因为它线程安全,当玩家点击射击按钮时,子弹可能会同时从 UI 线程以及我们自己的线程中被访问。这篇文章解释了CopyOnWriteArrayList,如果你想知道更多,请访问:

如何处理并发修改异常

我们有一个构造函数,它只是初始化子弹,lastShotTimenextBullet

MachineGun(){
   bullets = new CopyOnWriteArrayList<Bullet>();
   lastShotTime = -1;
   nextBullet = -1;
}

在这里,我们通过调用每个子弹的bullet.update方法,更新枪支控制的所有Bullet对象。

public void update(long fps, float gravity){
        //update all the bullets
        for(Bullet bullet: bullets){
            bullet.update(fps, gravity);
        }
    }

接下来,我们有一些 getter,它们将让我们了解有关我们的枪及其子弹的信息,以便进行像碰撞检测和绘制子弹等操作。

public int getRateOfFire(){
  return rateOfFire;
}

public void setFireRate(int rate){
  rateOfFire = rate;
}

public int getNumBullets(){
  //tell the view how many bullets there are
  return numBullets;
}

public float getBulletX(int bulletIndex){
  if(bullets != null && bulletIndex < numBullets) {
       return bullets.get(bulletIndex).getX();
    }

  return -1f;
}

public float getBulletY(int bulletIndex){
  if(bullets != null) {
       return bullets.get(bulletIndex).getY();
     }
     return -1f;
}

我们还有一个快速帮助方法,当我们想要停止绘制子弹时使用。我们在shoot方法中将其隐藏,直到准备好重新分配。

public void hideBullet(int index){
  bullets.get(index).hideBullet();
}

一个返回旅行方向的 getter:

public int getDirection(int index){
  return bullets.get(index).getDirection();
}

现在,我们添加一个更全面的方法,该方法实际射出一颗子弹。该方法将上一次射击的时间与当前的rateOfFire进行比较。然后继续增加nextBullet并在允许的情况下创建一个新的Bullet对象。子弹以 Bob 面向的同一方向飞速射出。请注意,如果成功发射了子弹,该方法将返回true。这样,InputController类可以播放与玩家按钮按下相对应的声音效果。

public boolean shoot(float ownerX, float ownerY, 
    int ownerFacing, float ownerHeight){

    boolean shotFired = false;
    if(System.currentTimeMillis() - lastShotTime  >                          
      1000/rateOfFire){

        //spawn another bullet;
        nextBullet ++;

        if(numBullets >= maxBullets){
            numBullets = maxBullets;
        }

        if(nextBullet == maxBullets){
            nextBullet = 0;
        }

        lastShotTime = System.currentTimeMillis();
        bullets.add(nextBullet, 
                new Bullet(ownerX, 
                (ownerY+ ownerHeight/3), speed, ownerFacing));

        shotFired = true;
        numBullets++;
    }
    return shotFired;
}

最后,我们有一个方法,当玩家找到机枪升级包时调用。我们将在本章后面看到更多相关内容。在这里,我们只是增加了rateOfFire,这使得玩家可以更猛烈地敲击开火按钮,并且仍然能够得到效果。

public void upgradeRateOfFire(){
  rateOfFire += 2;
}
}// End of MachineGun class

现在,我们将修改Player类以携带一把MachineGun。给Player一个类型为MachineGun的成员变量。

public MachineGun bfg;

接下来,在Player构造函数中,添加一行代码来初始化我们的新MachineGun对象:

bfg = new MachineGun();

Player类的update方法中,在我们为玩家调用move()之前,添加对MachineGun类的update方法的调用。如下所示突出:

bfg.update(fps, gravity);

// Let's go!
this.move(fps);

Player类添加一个方法,这样我们的InputController就可以访问虚拟触发器。正如我们所见,如果成功射击,该方法将返回true,这样InputController类就知道是否播放射击声音。

public boolean pullTrigger() {
        //Try and fire a shot
        return bfg.shoot(this.getWorldLocation().x,  
           this.getWorldLocation().y, 
           getFacing(), getHeight());
}

现在,我们可以在InputController类中做一些小的添加,让玩家能够开火。要添加的代码在现有代码中突出显示:

} else if (jump.contains(x, y)) {
  l.player.startJump(sound);

} else if (shoot.contains(x, y)) {
 if (l.player.pullTrigger()) {
 sound.playSound("shoot");
 }

} else if (pause.contains(x, y)) {
  l.switchPlayingStatus();

}

不要忘记我们新的控制系统的工作方式,我们还需要在InputController类的MotionEvent.ACTION_POINTER_DOWN情况下的更下方添加同样的额外代码。像往常一样,这里是有很多上下文背景的突出代码:

} else if (jump.contains(x, y)) {
  l.player.startJump(sound);

} else if (shoot.contains(x, y)) {
 if (l.player.pullTrigger()) {
 sound.playSound("shoot");
}

} else if (pause.contains(x, y)) {
  l.switchPlayingStatus();
}

现在我们有了一把枪,它已装填好,我们知道如何扣动扳机。我们只需要绘制子弹。

draw方法中添加新代码,在我们绘制调试文本之前,如下所示:

//draw the bullets
paint.setColor(Color.argb(255, 255, 255, 255));
for (int i = 0; i < lm.player.bfg.getNumBullets(); i++) {
   // Pass in the x and y coords as usual
   // then .25 and .05 for the bullet width and height
   toScreen2d.set(vp.worldToScreen
            (lm.player.bfg.getBulletX(i),
            lm.player.bfg.getBulletY(i),
            .25f,
            .05f));

        canvas.drawRect(toScreen2d, paint);
}

// Text for debugging
if (debugging) {
// etc

我们现在将发射一些子弹。请注意,开火速率令人不满意且缓慢。我们将添加一些收集品,玩家可以获得这些收集品以增加他的枪的开火速率。

收集品

收集品是玩家可以收集的游戏对象。它们包括像升级包、额外生命、金钱等。我们现在将实现其中每一个收集品。由于我们的游戏引擎是这样设置的,这将出奇地简单。

我们首先要创建一个类来保存当前玩家的状态。我们想要监控收集到的金钱、机枪的火力以及剩余的生命。我们将其称为PlayerState。创建一个新的 Java 类,并将其命名为PlayerState

除了我们刚才讨论的那些变量之外,我们还希望PlayerState类记住一个xy位置,以便在玩家失去生命时进行重生。输入这些成员变量和简单的构造函数:

import android.graphics.PointF;

public class PlayerState {

    private int numCredits;
    private int mgFireRate;
    private int lives;
    private float restartX;
    private float restartY;

    PlayerState() {
        lives = 3;
        mgFireRate = 1;
        numCredits = 0;
    }

现在,我们需要一个方法,我们可以调用它来初始化重生位置。我们稍后会用到这个方法。此外,我们还需要一个方法来重新加载位置。这是PlayerState类的接下来两个方法:

public void saveLocation(PointF location) {
   // The location saves each time the player uses a teleport
     restartX = location.x;
     restartY = location.y;
}

public PointF loadLocation() {
   // Used every time the player loses a life
   return new PointF(restartX, restartY);
}

我们只需要一堆 getter 和 setter,以便访问这个类的成员:

public int getLives(){
  return lives;
}

public int getFireRate(){
  return mgFireRate;
}

public void increaseFireRate(){
  mgFireRate += 2;
}

public void gotCredit(){
  numCredits ++;
}

public int getCredits(){
  return numCredits;
}

public void loseLife(){
  lives--;
}

public void addLife(){
  lives++;
}

public void resetLives(){
  lives = 3;
}
public void resetCredits(){
  lives = 0;
}

}// End PlayerState class

接下来,在PlatformView类中声明一个PlayerState类型的成员对象:

// Our new engine classes
private LevelManager lm;
private Viewport vp;
InputController ic;
SoundManager sm;
private PlayerState ps;

PlatformView构造函数中初始化它:

vp = new Viewport(screenWidth, screenHeight);
sm = new SoundManager();
sm.loadSound(context);
ps = new PlayerState();

loadLevel("LevelCave", 10, 2);

现在,在loadLevel方法中,创建一个RectF对象,保存玩家的起始位置,并将其传递给PlayerState对象ps以便妥善保存。每次玩家死亡时,都可以使用这个位置进行重生。

ic = new InputController(vp.getScreenWidth(), vp.getScreenHeight());

PointF location = new PointF(px, py);
ps.saveLocation(location);

//set the players location as the world centre of the viewport

现在,我们将创建三个类,分别对应我们的三种收集物。这些类非常简单。它们扩展了GameObject,设置了位图,具有碰撞箱和在世界中的位置。还要注意,它们在构造函数中都接收一个类型,并使用setType()存储这个值。我们很快就会看到如何使用它们的类型来处理玩家“收集它们”时会发生的事情。创建三个新的 Java 类:CoinExtraLifeMachineGunUpgrade。注意,收集物比平台稍小一些,这可能正如我们所预期的。依次输入它们的代码。

以下是Coin的代码:

public class Coin extends GameObject{

    Coin(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = .5f;
        final float WIDTH = .5f;

        setHeight(HEIGHT); 
        setWidth(WIDTH); 

        setType(type);

        // Choose a Bitmap
        setBitmapName("coin");

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity){}
}

现在,对于ExtraLife

public class ExtraLife extends GameObject{

    ExtraLife(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = .8f;
        final float WIDTH = .65f;

        setHeight(HEIGHT); 
        setWidth(WIDTH); 

        setType(type);

        // Choose a Bitmap

        setBitmapName("life");

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity){}
}

最后,MachineGunUpgrade类:

public class MachineGunUpgrade extends GameObject{
    MachineGunUpgrade(float worldStartX, 
        float worldStartY, 
        char type) {

        final float HEIGHT = .5f;
        final float WIDTH = .5f;

        setHeight(HEIGHT); 
        setWidth(WIDTH); 

        setType(type);

        // Choose a Bitmap

        setBitmapName("clip");

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity){}
}

现在,更新LevelManager类,使其能够处理我们的关卡设计中这三个新对象,并将它们添加到GameObjectsArrayList中。为此,我们需要在三个地方更新LevelManager类:getBitmap()getBitmapIndex()loadMapData()。以下是这些小更新的内容,新代码在现有代码中突出显示。

getBitmap()进行以下添加:

case 'p':
  index = 2;
  break;

case 'c':
 index = 3;
 break;

case 'u':
 index = 4;
 break;

case 'e':
 index = 5;
 break;

default:
  index = 0;
  break;

进行相同的添加,但这次是在getBitmapIndex()中:

case 'p':
  index = 2;
  break;

case 'c':
 index = 3;
 break;

case 'u':
 index = 4;
 break;

case 'e':
 index = 5;
 break;

default:
  index = 0;
  break;

LevelManager中进行最后的修改,对loadMapData()进行以下添加:

case 'p':// a player
    // Add a player to the gameObjects
    gameObjects.add(new Player(context, px, py, pixelsPerMetre));
    // We want the index of the player
    playerIndex = currentIndex;
    // We want a reference to the player object
    player = (Player) gameObjects.get(playerIndex);
    break;

case 'c':
 // Add a coin to the gameObjects
 gameObjects.add(new Coin(j, i, c));
 break;

case 'u':
 // Add a machine gun upgrade to the gameObjects
 gameObjects.add(new MachineGunUpgrade(j, i, c));
 break;

case 'e':
 // Add an extra life to the gameObjects
 gameObjects.add(new ExtraLife(j, i, c));
 break;
}

现在,我们可以将三个适当命名的图形添加到 drawable 文件夹中,并开始将它们添加到我们的LevelCave设计中。继续从下载捆绑包中的Chapter7/drawables文件夹复制clip.pngcoin.pnglife.png到你的 Android Studio 项目的drawable文件夹中。

添加一系列注释,标识所有游戏对象类型。我们将在项目过程中添加这些注释,以及它们在关卡设计中的字母数字代码。将以下注释添加到LevelData类中:

// Tile types
// . = no tile
// 1 = Grass
// 2 = Snow
// 3 = Brick
// 4 = Coal
// 5 = Concrete
// 6 = Scorched
// 7 = Stone

//Active objects
// g = guard
// d = drone
// t = teleport
// c = coin
// u = upgrade
// f = fire
// e  = extra life

//Inactive objects
// w = tree
// x = tree2 (snowy)
// l = lampost
// r = stalactite
// s = stalacmite
// m = mine cart
// z = boulders

在我们增强LevelCave类以使用我们的新对象之前,我们想要检测玩家收集它们或与它们碰撞的时刻,并采取适当的行动。我们首先会在Player类中添加一个快速辅助方法。这样做的原因是,当玩家与另一个对象碰撞时,Player类中checkCollisions方法的默认动作是停止角色移动。我们不希望拾取物发生这种情况,因为这会让玩家感到烦恼。因此,我们将在Player类中快速添加一个restorePreviousVelocity方法,在我们不希望发生默认动作时调用它。将此方法添加到Player类中:

public void restorePreviousVelocity() {
  if (!isJumping && !isFalling) {
       if (getFacing() == LEFT) {
           isPressingLeft = true;
           setxVelocity(-MAX_X_VELOCITY);
         } else {
           isPressingRight = true;
                     setxVelocity(MAX_X_VELOCITY);
       }
    }
}

现在,我们可以依次处理每个拾取物的碰撞。在PlatformView类的update方法中处理碰撞的 switch 块内,添加以下情况来处理我们的三个拾取物:

switch (go.getType()) {
 case 'c':
 sm.playSound("coin_pickup");
 go.setActive(false);
 go.setVisible(false);
 ps.gotCredit();

 // Now restore state that was 
 // removed by collision detection
 if (hit != 2) {// Any hit except feet
 lm.player.restorePreviousVelocity();
 }
 break;

case 'u':
 sm.playSound("gun_upgrade");
 go.setActive(false);
 go.setVisible(false);
 lm.player.bfg.upgradeRateOfFire();
 ps.increaseFireRate();
 if (hit != 2) {// Any hit except feet
 lm.player.restorePreviousVelocity();
 }
 break;

case 'e':
 //extralife
 go.setActive(false);
 go.setVisible(false);
 sm.playSound("extra_life");
 ps.addLife();

 if (hit != 2) {
 lm.player.restorePreviousVelocity();
 }
 break;

default:// Probably a regular tile
    if (hit == 1) {// Left or right
        lm.player.setxVelocity(0);
        lm.player.setPressingRight(false);
    }

    if (hit == 2) {// Feet
        lm.player.isFalling = false;
    }
    break;
}

最后,将新对象添加到我们的LevelCave类中。

提示

下面的代码片段,我建议是用于演示我们新对象的简单新布局,但你的布局可以尽可能大或者复杂。我们将在下一章设计并链接一些关卡时做一些更复杂的事情。

将以下代码输入到LevelCave中,或者用你自己的设计进行扩展:

public class LevelCave extends LevelData{
  LevelCave() {
    tiles = new ArrayList<String>();
 this.tiles.add("p.............................................");
 this.tiles.add("..............................................");
 this.tiles.add("..............................................");
 this.tiles.add("..............................................");
 this.tiles.add("....................c.........................");
 this.tiles.add("....................1........u................");
 this.tiles.add(".................c..........u1................");
 this.tiles.add(".................1.........u1.................");
 this.tiles.add("..............c...........u1..................");
 this.tiles.add("..............1..........u1...................");
 this.tiles.add("......................e..1....e.....e.........");
 this.tiles.add("....11111111111111111111111111111111111111....");
}

这就是简单布局的样子:

拾取物

尝试收集拾取物,你会听到愉悦的声音效果。此外,每次我们收集一个拾取物,PlayerState类就会存储一个更新。这将在我们下一章构建一个 HUD 时非常有用。最有趣的是;如果你收集了机枪升级,然后尝试射击,你会发现使用起来更加令人满意。

我们最好让这些子弹发挥作用。不过,在我们这样做之前,让我们给玩家提供一些炮灰,形式是几个敌人。

无人机

无人机是一个简单但邪恶的敌人。它将在视口中检测到玩家并直接向玩家飞去。如果无人机接触到玩家,那么玩家将立即死亡。

让我们构建一个Drone类。创建一个新的 Java 类,将其命名为Drone。我们需要成员变量来记录我们设置最后一个航点的时刻。这将限制无人机获取 Bob 坐标导航更新的频率。这将阻止无人机过于精确地打击目标。它需要一个航点/目标坐标,还需要知道通过MAX_X_VELOCITYMAX_Y_VELOCITY的速度限制。

import android.graphics.PointF;

public class Drone extends GameObject {

    long lastWaypointSetTime;
    PointF currentWaypoint;

    final float MAX_X_VELOCITY = 3;
    final float MAX_Y_VELOCITY = 3;

现在,在Drone构造函数中,初始化常规的GameObject成员,特别是Drone类的成员,如currentWaypoint。不要忘记,如果我们打算射击无人机,它将需要一个碰撞箱,我们在调用setWorldLocation()之后调用setRectHitBox()

Drone(float worldStartX, float worldStartY, char type) {
    final float HEIGHT = 1;
    final float WIDTH = 1;
    setHeight(HEIGHT); // 1 metre tall
    setWidth(WIDTH); // 1 metres wide

    setType(type);

    setBitmapName("drone");
    setMoves(true);
    setActive(true);
    setVisible(true);

    currentWaypoint = new PointF();

    // Where does the drone start
    // X and y locations from constructor parameters
    setWorldLocation(worldStartX, worldStartY, 0);
    setRectHitbox();
    setFacing(RIGHT);
}

这是update方法的实现,它将比较无人机的坐标与其currentWaypoint变量,并据此改变其速度。然后,我们通过调用move()然后是setRectHitbox()来结束update()

public void update(long fps, float gravity) {
  if (currentWaypoint.x > getWorldLocation().x) {
       setxVelocity(MAX_X_VELOCITY);
   } else if (currentWaypoint.x < getWorldLocation().x) {
       setxVelocity(-MAX_X_VELOCITY);
   } else {
       setxVelocity(0);
   }

    if (currentWaypoint.y >= getWorldLocation().y) {
       setyVelocity(MAX_Y_VELOCITY);
     } else if (currentWaypoint.y < getWorldLocation().y) {
       setyVelocity(-MAX_Y_VELOCITY);
     } else {
       setyVelocity(0);
  }

  move(fps);

  // update the drone hitbox
   setRectHitbox();

}

Drone类的最后一个方法中,通过传入 Bob 的坐标作为参数来更新currentWaypoint变量。注意,我们会检查是否已经过了足够的时间来进行更新,以确保我们的无人机不会过于精确。

public void setWaypoint(Vector2Point5D playerLocation) {
  if (System.currentTimeMillis() > lastWaypointSetTime + 2000) {//Has 2 seconds passed
        lastWaypointSetTime = System.currentTimeMillis();
        currentWaypoint.x = playerLocation.x;
        currentWaypoint.y = playerLocation.y;
     }
}
}// End Drone class

drone.png图形文件从Chapter7/drawable文件夹添加到项目的drawable文件夹中。

接下来,我们需要在LevelManager类中添加无人机,就像我们对每个拾取物品所做的那样,在三个常规位置添加。现在,在getBitmap()getBitmapIndex()loadMapData()方法中添加代码。这是按顺序需要添加的三个小部分代码。

getBitmap方法中添加高亮显示的代码:

case 'e':
  index = 5;
  break;

case 'd':
 index = 6;
 break;

default:
  index = 0;
  break;

getBitmapIndex方法中添加高亮显示的代码:

case 'e':
  index = 5;
  break;

case 'd':
 index = 6;
 break;

default:
  index = 0;
  break;

loadMapData方法中添加高亮显示的代码:

case 'e':
   // Add an extra life to the gameObjects
   gameObjects.add(new ExtraLife(j, i, c));
   break;

case 'd':
 // Add a drone to the gameObjects
 gameObjects.add(new Drone(j, i, c));
 break;

一个迫切的问题是:无人机如何知道要去哪里?在每一帧中,如果视口内有无人机,我们可以发送玩家的坐标。在PlatformView类的update方法中执行以下代码块所示的操作。

与往常一样,新代码以高亮形式展示,并嵌入到现有代码的上下文中。如果你记得Drone类中的setWaypoint()代码,它只接受每 2 秒更新一次。这防止了无人机过于精确。

if (lm.isPlaying()) {
   // Run any un-clipped updates
   go.update(fps, lm.gravity);

 if (go.getType() == 'd') {
 // Let any near by drones know where the player is
 Drone d = (Drone) go;
 d.setWaypoint(lm.player.getWorldLocation());
 }
}

现在,这些邪恶的无人机可以策略性地放置在关卡周围,它们会锁定玩家。要使无人机完全运作,我们需要做的最后一件事是检测它们实际上是否与玩家发生了碰撞。这非常简单。只需在PlatformView类的update方法中的碰撞检测switch块中为无人机添加一个案例:

case 'e':
  //extralife
   go.setActive(false);
   go.setVisible(false);
   sm.playSound("extra_life");
   ps.addLife();
   if (hit != 2) {// Any hit except feet
       lm.player.restorePreviousVelocity();
   }
   break;

case 'd':
 PointF location;
 //hit by drone
 sm.playSound("player_burn");
 ps.loseLife();
 location = new PointF(ps.loadLocation().x, 
 ps.loadLocation().y);
 lm.player.setWorldLocationX(location.x);
 lm.player.setWorldLocationY(location.y);
 lm.player.setxVelocity(0);
 break;

default:// Probably a regular tile
  if (hit == 1) {// Left or right
       lm.player.setxVelocity(0);
       lm.player.setPressingRight(false);
  }

   if (hit == 2) {// Feet
       lm.player.isFalling = false;
   }

继续在LevelCave中添加大量无人机,并观察它们向玩家飞去。注意,如果无人机捕捉到玩家,玩家会死亡并重新生成。

无人机

现在,尽管世界上已经有足够多的敌方无人机使它变得危险,但让我们再添加一种类型的敌人。

守卫

守卫敌人将是一个脚本练习。我们将让LevelManager类自动生成一个简单的脚本,为我们的守卫生成一个巡逻路线。

路线将尽可能简单;它只包括两个守卫会不断巡逻的点。预编程两个预定的航点会更快捷、更简单。然而,如果自动生成,我们可以根据需要(在一定的参数范围内)在任何设计的关卡上放置守卫,行为将由系统处理。

我们的守卫将会有动画效果,因此我们将在构造函数中使用一个精灵表单并配置动画细节,就像我们对Player类所做的那样。

创建一个新类,并将其命名为Guard。首先,处理成员变量。我们的Guard类不仅需要两个航点,还需要一个变量来指示当前的航点是哪一个。像其他移动对象一样,它需要速度。以下是开始编写你的类的类声明和成员变量:

import android.content.Context;

public class Guard extends GameObject {

    // Guards just move on x axis between 2 waypoints

    private float waypointX1;// always on left
    private float waypointX2;// always on right
    private int currentWaypoint;
    final float MAX_X_VELOCITY = 3;

我们需要通过构造函数设置我们的守卫。首先,设置我们的动画变量、位图和大小。然后像往常一样,设置守卫在关卡中的位置、它的碰撞箱以及它面向的方向。然而,在构造函数的最后一行,我们将currentWaypoint设置为1;这是新的。我们将在该类的update方法中看到这是如何影响守卫的行为的。

Guard(Context context, float worldStartX, 
  float worldStartY, char type, 
  int pixelsPerMetre) {

        final int ANIMATION_FPS = 8;
        final int ANIMATION_FRAME_COUNT = 5;
        final String BITMAP_NAME = "guard";
        final float HEIGHT = 2f;
        final float WIDTH = 1;

        setHeight(HEIGHT); // 2 metre tall
        setWidth(WIDTH); // 1 metres wide

        setType(type);

        setBitmapName("guard");
        // Now for the player's other attributes
        // Our game engine will use these
        setMoves(true);
        setActive(true);
        setVisible(true);

        // Set this object up to be animated
        setAnimFps(ANIMATION_FPS);
        setAnimFrameCount(ANIMATION_FRAME_COUNT);
        setBitmapName(BITMAP_NAME);
        setAnimated(context, pixelsPerMetre, true);

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);
        setxVelocity(-MAX_X_VELOCITY);
        currentWaypoint = 1;
}

接着,添加一个方法,供我们的LevelManager类使用,以告知Guard类其两个航点是什么:

public void setWaypoints(float x1, float x2){
  waypointX1 = x1;
  waypointX2 = x2;
}

现在,我们将编写Guard类的“大脑”部分,也就是它的update方法。你基本上可以将这个方法分为两个主要部分。首先,if(currentWaypoint == 1),其次,if(currentWaypoint == 2)。在这两个if块内部,只需检查守卫是否已经到达或通过了适当的航点。如果是,则切换航点,反转速度,并让守卫面向另一个方向。

最后,调用move()然后setRectHitbox(),以更新碰撞箱到守卫的新位置。添加update方法的代码,然后我们将看到如何让它工作。

public void update(long fps, float gravity) {
  if(currentWaypoint == 1) {// Heading left
       if (getWorldLocation().x <= waypointX1) {
          // Arrived at waypoint 1
           currentWaypoint = 2;
           setxVelocity(MAX_X_VELOCITY);
           setFacing(RIGHT);
      }
  }

  if(currentWaypoint == 2){
    if (getWorldLocation().x >= waypointX2) {
         // Arrived at waypoint 2
          currentWaypoint = 1;
          setxVelocity(-MAX_X_VELOCITY);
          setFacing(LEFT);
      }
  }

  move(fps);
   // update the guards hitbox
   setRectHitbox();
}
}// End Guard class

记得从下载包的Chapter7/drawables文件夹中添加guard.png到项目的drawable文件夹中。

现在,我们可以在LevelManager类中进行通常的三处添加,以加载可能在我们的关卡设计中找到的任何守卫。

getBitmap()中,添加高亮显示的代码:

case 'd':
  index = 6;
  break;

case 'g':
 index = 7;
 break;

default:
  index = 0;
  break;

getBitmapIndex()中,添加高亮显示的代码:

case 'd':
  index = 6;
  break;

case 'g':
 index = 7;
 break;

default:
  index = 0;
  break;

loadMapData()中,添加高亮显示的代码:

case 'd':
     // Add a drone to the gameObjects
     gameObjects.add(new Drone(j, i, c));
     break;
case 'g':
 // Add a guard to the gameObjects
 gameObjects.add(new Guard(context, j, i, c, pixelsPerMetre));
 break;

我们很快将为LevelManager添加一个全新的功能。那就是一个将创建脚本(设置两个巡逻航点)的方法。为了让这个新方法工作,它需要知道瓦片是否适合行走。我们将为GameObject添加一个新属性、一个获取器和设置器,以便轻松发现这一点。

GameObject类的类声明后直接添加这个新成员:

private boolean traversable = false;

GameObject类添加这两个方法,以获取和设置这个变量:

public void setTraversable(){
  traversable = true;
}

public boolean isTraversable(){
  return traversable;
}

现在,在Grass类的构造函数中,添加对setTraversable()的调用。如果我们希望守卫能够在上面巡逻,我们必须记得为所有未来设计的GameObject派生类做这一点。在Grass中,在构造函数顶部添加这一行:

setTraversable();

接下来,我们将查看为LevelManager类新增加的setWaypoints方法。它需要检查关卡设计,并为关卡中存在的任何Guard对象计算两个航点。

我们将把这个方法分成几个部分,以便我们可以看到每个阶段的操作。

首先,我们需要遍历所有的gameObjects类,寻找Guard对象。

public void setWaypoints() {
  // Loop through all game objects looking for Guards
    for (GameObject guard : this.gameObjects) {
       if (guard.getType() == 'g') {

如果我们到达代码的这一部分,这意味着我们已经找到了一个需要设置两个航点的守卫。首先,我们需要找到守卫“站立”的瓷砖。然后,我们计算每侧最后一个可通行的瓷砖的坐标,但最大范围是每个方向五个瓷砖。这两个点将作为两个航点。以下是添加到setWaypoints方法中的代码。它包含大量注释,以清晰说明情况而不中断流程。

// Set waypoints for this guard
// find the tile beneath the guard
// this relies on the designer putting 
// the guard in sensible location

int startTileIndex = -1;
int startGuardIndex = 0;
float waypointX1 = -1;
float waypointX2 = -1;

for (GameObject tile : this.gameObjects) {
    startTileIndex++;
    if (tile.getWorldLocation().y == 
            guard.getWorldLocation().y + 2) {

        // Tile is two spaces below current guard
        // Now see if has same x coordinate
        if (tile.getWorldLocation().x == 
            guard.getWorldLocation().x) {

            // Found the tile the guard is "standing" on
            // Now go left as far as possible 
            // before non travers-able tile is found
            // Either on guards row or tile row
            // upto a maximum of 5 tiles. 
            //  5 is an arbitrary value you can
            // change it to suit

            for (int i = 0; i < 5; i++) {// left for loop
                if (!gameObjects.get(startTileIndex -
                    i).isTraversable()) {

                    //set the left waypoint
                    waypointX1 = gameObjects.get(startTileIndex - 
                        (i + 1)).getWorldLocation().x;

                     break;// Leave left for loop
                     } else {
                    // Set to max 5 tiles as 
                    // no non traversible tile found
                    waypointX1 = gameObjects.get(startTileIndex -
                        5).getWorldLocation().x;
               }
                }// end get left waypoint

                for (int i = 0; i < 5; i++) {// right for loop
                    if (!gameObjects.get(startTileIndex +
                        i).isTraversable()) {

                        //set the right waypoint
                        waypointX2 = gameObjects.get(startTileIndex +
                            (i - 1)).getWorldLocation().x;

                    break;// Leave right for loop
                    } else {
                    //set to max 5 tiles away
                    waypointX2 = gameObjects.get(startTileIndex +
                       5).getWorldLocation().x;
                }

                }// end get right waypoint

        Guard g = (Guard) guard;
        g.setWaypoints(waypointX1, waypointX2);
    }
}
}
}
}
}// End setWaypoints()

现在,我们可以在LevelManager构造函数的最后调用我们新的setWaypoints方法。我们需要在GameObject类的ArrayList填充完毕后调用此方法,否则其中将没有守卫。像这样突出显示添加对setWaypoints()的调用:

// Load all the GameObjects and Bitmaps
loadMapData(context, pixelsPerMetre, px, py);
// Set waypoints for our guards
setWaypoints();

接下来,将这段代码添加到PlatformView类的update方法中的碰撞检测开关块中,以便我们可以与守卫相撞。

case 'd':
    PointF location;
    //hit by drone
    sm.playSound("player_burn");
    ps.loseLife();
    location = new PointF(ps.loadLocation().x, 
        ps.loadLocation().y);

    lm.player.setWorldLocationX(location.x);
    lm.player.setWorldLocationY(location.y);
    lm.player.setxVelocity(0);
    break;

case 'g':
 // Hit by guard
 sm.playSound("player_burn");
 ps.loseLife();
 location = new PointF(ps.loadLocation().x,
 ps.loadLocation().y);

 lm.player.setWorldLocationX(location.x);
 lm.player.setWorldLocationY(location.y);
 lm.player.setxVelocity(0);
 break;

default:// Probably a regular tile
    if (hit == 1) {// Left or right
        lm.player.setxVelocity(0);
        lm.player.setPressingRight(false);
    }
    if (hit == 2) {// Feet
        lm.player.isFalling = false;
    }

最后,向LevelCave类中添加一些g字母。确保将它们放置在平台上方一个空格的位置,因为它们的高度是 2 米,如下面的伪代码所示:

................g............................
...........................d.................
111111111111111111111111111111111111111111111

守卫

总结

我们实现了枪支、拾取物、无人机和守卫。这意味着我们现在有很多危险,但拥有一把无法造成伤害的机枪。我们将在下一章首先解决这个问题,为我们的子弹实现碰撞检测。然而,我们的目标不仅仅是让子弹击中敌人。

第八章:平台游戏——整合所有功能

最后,我们将让子弹造成一些伤害。当子弹的能量被一团草地吸收时,这种反弹声非常令人满意。我们将添加大量的新平台类型和非动画场景对象,使我们的关卡更有趣。通过实现多个滚动视差背景,我们将提供一种真正的运动感和沉浸感。

我们还将添加一个动画火焰瓦片,让玩家避开,此外,还会添加一个特殊的Teleport类,将各个关卡连接成一个可玩的游戏。然后,我们将使用所有的游戏对象和背景创建四个连接、完全可玩的游戏关卡。

然后,我们将添加一个 HUD 来跟踪拾取物和生命值。最后,我们将讨论一些无法在这四章中容纳的精彩内容。

子弹碰撞检测

检测子弹碰撞相当直接。我们遍历由我们的MachineGun对象持有的所有现有Bullet对象。接下来,我们将每个子弹的点转换成RectHitBox对象,并使用intersects()方法测试我们的视口中的每个对象。

如果我们受到攻击,我们会检查它击中的对象类型。然后,我们会切换到处理我们关心的每种类型的对象。如果是Guard对象,我们将其稍微击退一点;如果是Drone对象,我们将其销毁;如果是其他任何对象,我们只需让子弹消失,并播放一种沉闷的/反弹声。

我们只需在我们处理玩家碰撞的switch块之后,但在我们调用所有未剪辑对象的update()之前,放置我们讨论过的这个逻辑,如下所示:

default:// Probably a regular tile
    if (hit == 1) {// Left or right
        lm.player.setxVelocity(0);
        lm.player.setPressingRight(false);
    }

   if (hit == 2) {// Feet
        lm.player.isFalling = false;
    }
    break;
}
}

//Check bullet collisions
for (int i = 0; i < lm.player.bfg.getNumBullets(); i++) {
 //Make a hitbox out of the the current bullet
 RectHitbox r = new RectHitbox();
 r.setLeft(lm.player.bfg.getBulletX(i));
 r.setTop(lm.player.bfg.getBulletY(i));
 r.setRight(lm.player.bfg.getBulletX(i) + .1f);
 r.setBottom(lm.player.bfg.getBulletY(i) + .1f);

 if (go.getHitbox().intersects(r)) {
 // Collision detected
 // make bullet disappear until it 
 // is respawned as a new bullet
 lm.player.bfg.hideBullet(i);

 //Now respond depending upon the type of object hit
 if (go.getType() != 'g' && go.getType() != 'd') {
 sm.playSound("ricochet");

 } else if (go.getType() == 'g') {
 // Knock the guard back
 go.setWorldLocationX(go.getWorldLocation().x +
 2 * (lm.player.bfg.getDirection(i)));

 sm.playSound("hit_guard");

 } else if (go.getType() == 'd') {
 //destroy the droid
 sm.playSound("explode");
 //permanently clip this drone
 go.setWorldLocation(-100, -100, 0);
 }
 }
}

if (lm.isPlaying()) {
    // Run any un-clipped updates
    go.update(fps, lm.gravity);
        //...

尝试一下,尤其是高射速时,这真的很令人满意。

添加一些火焰瓦片

这些新的基于GameObject的对象将对 Bob 造成即死的效果。它们不会移动,但它们将被动画化。我们将看到,只需设置GameObject已有的属性,我们就可以实现这一点。

将这个功能添加到我们的游戏中非常简单,因为我们已经实现了所需的所有功能。我们已经有了定位和添加新瓦片的方法,检测并响应碰撞的方法,精灵图动画等等。让我们一步步进行,然后我们就可以将这些危险且致命的元素添加到我们的世界中。

我们可以将类的所有功能都放入其构造函数中。我们所要做的就是像配置Grass对象那样配置这个对象,此外,我们还要为其配置所有动画设置,就像我们对PlayerGuard对象所做的那样。fire.png精灵图有三种动画帧,我们希望在一秒钟内播放它们。

添加一些火焰瓦片

创建一个新类,将其命名为Fire,并向其中添加以下代码:

import android.content.Context;

public class Fire extends GameObject{

    Fire(Context context, float worldStartX, 
    float worldStartY, char type, int pixelsPerMetre) {

        final int ANIMATION_FPS = 3;
        final int ANIMATION_FRAME_COUNT = 3;
        final String BITMAP_NAME = "fire";

        final float HEIGHT = 1;
        final float WIDTH = 1;

        setHeight(HEIGHT); // 1 metre tall
        setWidth(WIDTH); // 1 metre wide

        setType(type);
        // Now for the player's other attributes
        // Our game engine will use these
        setMoves(false);
        setActive(true);
        setVisible(true);

        // Choose a Bitmap
        setBitmapName(BITMAP_NAME);
        // Set this object up to be animated
        setAnimFps(ANIMATION_FPS);
        setAnimFrameCount(ANIMATION_FRAME_COUNT);
        setBitmapName(BITMAP_NAME);
        setAnimated(context, pixelsPerMetre, true);

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

 public void update(long fps, float gravity) {
 }
}

现在,当然,我们需要将下载包中Chapter8/drawable目录下的fire.png精灵图添加到项目的drawable文件夹中。

然后,我们按照为所有新的GameObject派生类所做的方式,以通常的三种方法将它们添加到我们的LevelManager类中。

getBitmap方法中,添加突出显示的代码:

case 'g':
    index = 7;
    break;

case 'f':
 index = 8;
 break;

default:
    index = 0;
    break;

getBitmapIndex方法中:

case 'g':
    index = 7;
    break;

case 'f':
 index = 8;
 break;

default:
    index = 0;
    break;

loadMapData()方法中:

case 'g':
     // Add a guard to the gameObjects
     gameObjects.add(new Guard(context, j, i, c, pixelsPerMetre));
     break;

 case 'f':
 // Add a fire tile the gameObjects
 gameObjects.add(new Fire
 (context, j, i, c, pixelsPerMetre));

 break;

最后,我们在碰撞检测的switch块中添加处理触碰这个可怕瓦片的后果。

case 'g':
    //hit by guard
    sm.playSound("player_burn");
    ps.loseLife();
    location = new PointF(ps.loadLocation().x,
        ps.loadLocation().y);
    lm.player.setWorldLocationX(location.x);
    lm.player.setWorldLocationY(location.y);
    lm.player.setxVelocity(0);
    break;

case 'f':
 sm.playSound("player_burn");
 ps.loseLife();
 location = new PointF(ps.loadLocation().x,
 ps.loadLocation().y);
 lm.player.setWorldLocationX(location.x);
 lm.player.setWorldLocationY(location.y);
 lm.player.setxVelocity(0);
 break;

default:// Probably a regular tile
    if (hit == 1) {// Left or right
        lm.player.setxVelocity(0);
        lm.player.setPressingRight(false);
    }

    if (hit == 2) {// Feet
        lm.player.isFalling = false;
    }
    break;

不如在LevelCave中添加一些f瓦片,并实验玩家能够跳跃过哪些。这将帮助我们在本章后面设计一些具有挑战性的关卡。

添加一些火焰瓦片

我们不希望玩家一直走在草地上,所以让我们添加一些多样性。

眼前一亮

本章接下来的三个部分将纯粹关注外观。我们将添加一整套不同的瓦片图像和匹配的类,这样我们可以使用更多的艺术许可来使我们的关卡更有趣。这些瓦片之间的区别将纯粹是视觉上的,但使它们具有比这更多的功能性将相当简单。

例如,我们可以轻松检测与雪瓦片的碰撞,并让玩家在短暂停止后继续移动以模拟滑行,或者;混凝土瓦片可以让玩家移动得更快,因此改变我们设计大跳跃的方式等等。重点是,你不必仅仅复制粘贴这里呈现的类。

我们还将添加一些完全为了美观的道具:矿车、巨石、石钟乳石等。这些对象不会有碰撞检测。它们将允许关卡设计师使关卡在视觉上更有趣。

提示

要使这些美观元素更具功能性很简单。只需添加一个碰撞箱并在碰撞检测switch块中添加一个案例来处理后果。

可能,我们添加的视觉上最重要的改进将是滚动背景。我们将添加一些类,允许关卡设计师向关卡设计中添加多个不同的滚动背景。

提示

不妨将下载包中Chapter8/drawable文件夹的所有图像添加到项目的drawable文件夹中。这样,你将拥有所有图形,包括本节和接下来两节的图形都准备好了。

新的平台瓦片

现在,按照显示的文件名添加所有这些类。我移除了代码中的所有注释,因为它们在功能上都与Grass类相同。按照显示的名称创建以下每个类,并输入代码:

这是Brick类的代码:

public class Brick extends GameObject {

    Brick(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT); 
        setWidth(WIDTH); 
        setType(type);
        setBitmapName("brick");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

这是Coal类的代码:

public class Coal extends GameObject {

    Coal(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT); 
        setWidth(WIDTH);
        setType(type);
        setBitmapName("coal");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

这是Concrete类的代码:

public class Concrete extends GameObject {

    Concrete(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT);
        setWidth(WIDTH);
        setType(type);
        setBitmapName("concrete");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

以下是Scorched类的代码:

public class Scorched extends GameObject {

    Scorched(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT);
        setWidth(WIDTH);
        setType(type);
        setBitmapName("scorched");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

这是Snow类的代码:

public class Snow extends GameObject {

    Snow(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT);
        setWidth(WIDTH);
        setType(type);
        setBitmapName("snow");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

这是Stone类的代码:

public class Stone extends GameObject {

    Stone(float worldStartX, float worldStartY, char type) {
        setTraversable();
        final float HEIGHT = 1;
        final float WIDTH = 1;
        setHeight(HEIGHT);
        setWidth(WIDTH); 
        setType(type);
        setBitmapName("stone");
        setWorldLocation(worldStartX, worldStartY, 0);
        setRectHitbox();
    }

    public void update(long fps, float gravity) {
    }
}

现在,像我们习惯的那样,我们需要将它们全部添加到我们的LevelManager中,在通常的三个地方。

getBitmap()方法中,我们像平常一样将它们添加进去。请注意,尽管这些值是任意的,但我们将为类型 2、3、4 等使用数字。这样在设计关卡时容易记住,我们所有的实际平台都是数字。实际的索引编号对我们来说不重要,只要它们与getBitmapIndex方法中的相同即可。此外,记住我们在LevelData类的注释中有一个类型列表,以便在设计关卡时方便参考。

case 'f':
    index = 8;
    break;

case '2':
 index = 9;
 break;

case '3':
 index = 10;
 break;

case '4':
 index = 11;
 break;

case '5':
 index = 12;
 break;

case '6':
 index = 13;
 break;

case '7':
 index = 14;
 break;

default:
    index = 0;
    break;

getBitmapIndex()中,我们做同样的事情:

case 'f':
    index = 8;
    break;

case '2':
 index = 9;
 break;

case '3':
 index = 10;
 break;

case '4':
 index = 11;
 break;

case '5':
 index = 12;
 break;

case '6':
 index = 13;
 break;

case '7':
 index = 14;
 break;

default:
    index = 0;
    break;

loadMapData()中,我们只需在我们的新GameObjects上调用new(),将它们添加到gameObjects列表中。

case 'f':
    // Add a fire tile the gameObjects
    gameObjects.add(new Fire(context, j, i, c, pixelsPerMetre));
    break;

case '2':
 // Add a tile to the gameObjects
 gameObjects.add(new Snow(j, i, c));
 break;

case '3':
 // Add a tile to the gameObjects
 gameObjects.add(new Brick(j, i, c));
 break;

case '4':
 // Add a tile to the gameObjects
 gameObjects.add(new Coal(j, i, c));
 break;

case '5':
 // Add a tile to the gameObjects
 gameObjects.add(new Concrete(j, i, c));
 break;

case '6':
 // Add a tile to the gameObjects
 gameObjects.add(new Scorched(j, i, c));
 break;

case '7':
 // Add a tile to the gameObjects
 gameObjects.add(new Stone(j, i, c));
 break;

现在,大胆地为LevelCave类添加不同的地形:

新的平台瓦片

现在,我们来添加一些景观对象。

新的景观对象

在这里,我们会添加一些除了看起来漂亮之外什么都不做的对象。我们只需不添加碰撞箱,并将它们随机设置为 z 层-1 或 1,让游戏引擎知道这一点。然后玩家可以出现在它们前面或后面。

我们首先会添加所有类,然后像往常一样更新LevelManager的三个地方。按照以下方式创建每个新类:

这是Boulders类:

public class Boulders extends GameObject {

    Boulders(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 1;
        final float WIDTH = 3;

        setHeight(HEIGHT); // 1 metre tall
        setWidth(WIDTH); // 1 metre wide

        setType(type);

        // Choose a Bitmap
        setBitmapName("boulder");
        setActive(false);//don't check for collisions etc

        // Randomly set the tree either just in front or just 
        //behind the player -1 or 1
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
            setWorldLocation(worldStartX, worldStartY, -1);
        }else{
            setWorldLocation(worldStartX, worldStartY, 1);//
        }
        //No hitbox!!

    }

    public void update(long fps, float gravity) {
    }
}

从现在开始,我删除了所有注释以节省墨水。该类的功能与Boulders中的相同,只是属性有些许不同。

这是Cart类:

public class Cart extends GameObject {

  Cart(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 2;
        final float WIDTH = 3;
        setWidth(WIDTH);
        setHeight(HEIGHT);
        setType(type);
        setBitmapName("cart");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
          setWorldLocation(worldStartX, worldStartY, -1);
        }else{
          setWorldLocation(worldStartX, worldStartY, 1);
        }
     }

  public void update(long fps, float gravity) {
     }
}

这是Lampost类的代码:

public class Lampost extends GameObject {

  Lampost(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 3;
        final float WIDTH = 1;
        setHeight(HEIGHT);
        setWidth(WIDTH); 
        setType(type);
        setBitmapName("lampost");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
          setWorldLocation(worldStartX, worldStartY, -1);
        }else{
          setWorldLocation(worldStartX, worldStartY, 1);
        }
  }

    public void update(long fps, float gravity) {
   }
}

这是Stalagmite类:

import java.util.Random;

public class Stalagmite extends GameObject {

  Stalagmite(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 3;
        final float WIDTH = 2;
        setHeight(HEIGHT);
        setWidth(WIDTH);
        setType(type);
        setBitmapName("stalacmite");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
         setWorldLocation(worldStartX, worldStartY, -1);
        }else{
         setWorldLocation(worldStartX, worldStartY, 1);
        }
    }

    public void update(long fps, float gravity) {
    }
}

这是Stalactite类:

import java.util.Random;

public class Stalactite extends GameObject {

  Stalactite(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 3;
        final float WIDTH = 2;
        setHeight(HEIGHT);
        setWidth(WIDTH);
        setType(type);
        setBitmapName("stalactite");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
          setWorldLocation(worldStartX, worldStartY, -1);
        }else{
          setWorldLocation(worldStartX, worldStartY, 1);
        }
  }

     public void update(long fps, float gravity) {
     }
}

这是Tree类:

import java.util.Random;

public class Tree extends GameObject {

  Tree(float worldStartX, float worldStartY, char type) {

       final float HEIGHT = 4;
       final float WIDTH = 2;
       setWidth(WIDTH);
        setHeight(HEIGHT);
        setType(type);
        setBitmapName("tree1");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
          setWorldLocation(worldStartX, worldStartY, -1);
        }else{
          setWorldLocation(worldStartX, worldStartY, 1);
        }
     }

     public void update(long fps, float gravity) {
     }
}

这是Tree2类:

import java.util.Random;

public class Tree2 extends GameObject {

  Tree2(float worldStartX, float worldStartY, char type) {

        final float HEIGHT = 4;
        final float WIDTH = 2;
        setWidth(WIDTH);
        setHeight(HEIGHT);
        setType(type);
        setBitmapName("tree2");
        setActive(false);
        Random rand = new Random();
        if(rand.nextInt(2)==0) {
          setWorldLocation(worldStartX, worldStartY, -1);
        }else{
          setWorldLocation(worldStartX, worldStartY, 1);
        }
  }

     public void update(long fps, float gravity) {
     }
}

这就是所有新的景观对象类。现在,我们可以在LevelManager类中用七种新类型更新getBitmap方法。

case '7':
    index = 14;
    break;

case 'w':
 index = 15;
 break;

case 'x':
 index = 16;
 break;

case 'l':
 index = 17;
 break;

case 'r':
 index = 18;
 break;

case 's':
 index = 19;
 break;

case 'm':
 index = 20;
 break;

case 'z':
 index = 21;
 break;

default:
    index = 0;
    break;

以同样的方式更新getBitmapIndex方法:

case '7':
    index = 14;
    break;

case 'w':
 index = 15;
 break;

case 'x':
 index = 16;
 break;

case 'l':
 index = 17;
 break;

case 'r':
 index = 18;
 break;

case 's':
 index = 19;
 break;

case 'm':
 index = 20;
 break;

case 'z':
 index = 21;
 break;

default:
    index = 0;
    break;

最后,确保我们的新景观物品被添加到gameObjects数组列表中:

case '7':
    // Add a tile to the gameObjects
    gameObjects.add(new Stone(j, i, c));
    break;

case 'w':
 // Add a tree to the gameObjects
 gameObjects.add(new Tree(j, i, c));
 break;

case 'x':
 // Add a tree2 to the gameObjects
 gameObjects.add(new Tree2(j, i, c));
 break;

case 'l':
 // Add a tree to the gameObjects
 gameObjects.add(new Lampost(j, i, c));
 break;

case 'r':
 // Add a stalactite to the gameObjects
 gameObjects.add(new Stalactite(j, i, c));
 break;

case 's':
 // Add a stalagmite to the gameObjects
 gameObjects.add(new Stalagmite(j, i, c));
 break;

case 'm':
 // Add a cart to the gameObjects
 gameObjects.add(new Cart(j, i, c));
 break;

case 'z':
 // Add a boulders to the gameObjects
 gameObjects.add(new Boulders(j, i, c));
 break;

现在,我们可以设计带有景观的关卡。注意当对象在层零与层一上绘制时外观上的细微差别,以及玩家角色如何穿过它们前面或后面:

新的景观对象

提示

当然,如果你想要碰撞到路灯、被石笋刺穿,或者跳到矿车顶上,只需给它们一个碰撞箱即可。

我们还有一种美化游戏世界的方法。

滚动的视差背景

视差背景是滚动的背景,我们根据它们距离的远近来减慢滚动速度。因此,如果玩家脚边有草地,我们会快速滚动它。然而,如果远处有山脉,我们会慢慢滚动它。这种效果可以给玩家带来运动的感觉。

为了实现这一功能,我们首先将添加一个数据结构来表示背景的参数。我们将这个类称为BackgroundData,然后实现一个Background类,它具有控制滚动所需的功能,然后我们将会看到如何在我们的关卡设计中定位和定义背景。最后,我们将编写一个drawBackground方法,我们将会从常规的draw方法中调用它。

确保你已经将从下载包的Chapter8/drawable文件夹中的所有图像添加到你的项目的drawable文件夹中。

首先,让我们构建一个简单的类来保存定义我们背景的数据结构。正如在下一个代码块中我们可以看到的,我们有很多参数和成员变量。我们需要知道哪个位图将代表背景,在z轴上哪个层面绘制它(前面为 1,后面为-1),在y轴上它在全球的哪个位置开始和结束,背景滚动的速度有多快,以及背景的高度是多少。

isParallax布尔值旨在提供一种让背景静止的选项,但我们不会实现这个功能。当你看到背景类的代码时,你会发现如果你想要,添加这个功能是非常简单的。

创建一个新类,将其命名为BackgroundData,然后用以下代码实现它:

public class BackgroundData {
  String bitmapName;
     boolean isParallax;
     //layer 0 is the map
     int layer;
     float startY;
     float endY;
     float speed;
     int height;
     int width;

     BackgroundData(String bitmap, boolean isParallax, 
     int layer, float startY, float endY, 
     float speed, int height){

      this.bitmapName = bitmap;
      this.isParallax = isParallax;
      this.layer = layer;
      this.startY = startY;
      this.endY = endY;
      this.speed = speed;
      this.height = height;
  }
}

现在,我们在LevelData类中添加了一个我们新类型的ArrayList

ArrayList<String> tiles;
ArrayList<BackgroundData> backgroundDataList;

// This class will evolve along with the project

接下来,让我们创建Background类本身。创建一个新类,并将其命名为Background。首先,我们设置一组变量来保存背景图像以及它的反转副本。我们将通过将图像背靠背交替使用常规图像和反转图像,使背景看起来无限。我们将在代码中进一步了解如何实现这一点。

我们还有用于存储图像的宽度和高度的像素变量。reversedFirst布尔值将决定当前在屏幕左侧(首先)绘制哪个图像副本,并将在玩家移动和图像滚动时改变。xClip变量将保存我们将在屏幕左侧边缘开始绘制的图像的x轴的确切像素。

yendYzspeed成员变量用于保存作为参数传递的相关值:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;

public class Background {

     Bitmap bitmap;
     Bitmap bitmapReversed;

     int width;
     int height;

     boolean reversedFirst;
     int xClip;// controls where we clip the bitmaps each frame
     float y;
     float endY;
     int z;

     float speed;
     boolean isParallax;//Not currently used

在构造函数中,我们从作为参数传递的图形文件名创建一个 Android 资源 ID。然后,通过调用BitmapFactory.decodeResource()创建实际的位图。我们将reversedFirst设置为false,因此将从屏幕左侧开始使用图像的正常(非反转)副本。我们初始化成员变量,并通过调用Bitmap.createScaledBitmap()并传入位图、屏幕宽度和背景在游戏世界中的高度乘以pixelsPerMetre来缩放我们刚刚创建的位图,使位图恰好适合当前设备屏幕的尺寸。

提示

请注意,我们必须为背景设计选择适当的高度,否则它们会出现拉伸。

构造函数中最后要做的就是在调用createScaledBitmap方法时创建一个Matrix对象,并连同位图一起传递,这样我们现在在bitmapReversed Bitmap对象中存储了一个背景图像的反转副本。

  Background(Context context, int yPixelsPerMetre, 
    int screenWidth, BackgroundData data){

      int resID =   context.getResources().getIdentifier
      (data.bitmapName, "drawable", 
      context.getPackageName());

          bitmap = BitmapFactory.decodeResource
          (context.getResources(), resID);

          // Which version of background (reversed or regular) is // currently drawn first (on left)
          reversedFirst = false;

          //Initialize animation variables.
          xClip = 0;  //always start at zero
          y = data.startY;
          endY = data.endY;
          z = data.layer;
          isParallax = data.isParallax;
          speed = data.speed; //Scrolling background speed

          //Scale background to fit the screen.
          bitmap = Bitmap.createScaledBitmap(bitmap, screenWidth,
                data.height * yPixelsPerMetre
                , true); 

          width = bitmap.getWidth();
          height = bitmap.getHeight();

          // Create a mirror image of the background
          Matrix matrix = new Matrix();  
          matrix.setScale(-1, 1); //Horizontal mirror effect.
          bitmapReversed = Bitmap.createBitmap(
          bitmap, 0, 0, width, height, matrix, true);

    }
}

现在,我们在关卡设计中添加两个背景。我们填写已经讨论过的所需参数。请注意,第 1 层的“草地”背景滚动速度比-1 层的“天际线”背景快得多。这将产生所需的视差效果。在LevelCave构造函数的末尾添加以下代码:

backgroundDataList = new ArrayList<BackgroundData>();
// note that speeds less than 2 cause problems
this.backgroundDataList.add(
  new BackgroundData("skyline", true, -1, 3, 18, 10, 15 ));

this.backgroundDataList.add(
  new BackgroundData("grass", true, 1, 20, 24, 24, 4 ));

注意

大多数洞穴确实没有草地和天际线,这只是一个演示,让代码工作起来。我们将在本章稍后重新设计LevelCave,并设计一些更合适的关卡。

现在,我们通过声明一个Arraylist对象作为LevelManager类的成员,用我们的LevelManager类加载它们。

LevelData levelData;
ArrayList<GameObject> gameObjects;
ArrayList<Background> backgrounds;

然后,在LevelManager中添加一个新方法来加载背景数据:

private void loadBackgrounds(Context context, 
  int pixelsPerMetre, int screenWidth) {

  backgrounds = new ArrayList<Background>();
     //load the background data into the Background objects and
     // place them in our GameObject arraylist
     for (BackgroundData bgData : levelData.backgroundDataList) {
            backgrounds.add(new Background(context,       
            pixelsPerMetre, screenWidth, bgData));
     }
}

我们在LevelManager构造函数中调用这个新方法:

// Load all the GameObjects and Bitmaps
loadMapData(context, pixelsPerMetre, px, py);
loadBackgrounds(context, pixelsPerMetre, screenWidth);

并且,不是最后一次,我们将升级我们的Viewport类,让PlatformView方法能够获取它们需要的信息,以绘制视差背景。

public int getPixelsPerMetreY(){
  return  pixelsPerMetreY;
}

public int getyCentre(){
  return screenCentreY;
}

public float getViewportWorldCentreY(){
  return currentViewportWorldCentre.y;
}

然后,我们将在PlatformView类中添加一个实际执行绘图的方法。接下来,我们会在onDraw()中的恰当位置调用这个方法。请注意,我们正在使用刚刚添加到Viewport类中的新方法。

首先,我们定义四个Rect对象,用来保存bitmapreversedBitmap的起始和结束点。

按照所示实现drawBackground方法的第一部分:

private void drawBackground(int start, int stop) {

     Rect fromRect1 = new Rect();
     Rect toRect1 = new Rect();
     Rect fromRect2 = new Rect();
     Rect toRect2 = new Rect();

现在,我们只需遍历所有背景,使用startstop参数来确定哪些背景具有我们当前感兴趣的z层。

     for (Background bg : lm.backgrounds) {
     if (bg.z < start && bg.z > stop) {

接下来,我们将背景的世界坐标发送到Viewport类进行裁剪。如果没有裁剪(并且应该绘制),我们将使用之前添加到我们的Viewport类中的新方法,获取y轴上的起始像素坐标和结束像素坐标。请注意,我们将结果转换为int变量,以便绘制到屏幕上。

          // Is this layer in the viewport?
            // Clip anything off-screen
            if (!vp.clipObjects(-1, bg.y, 1000, bg.height)) {
                float floatstartY = ((vp.getyCentre() -                     
                    ((vp.getViewportWorldCentreY() - bg.y) * 
                    vp.getPixelsPerMetreY())));

                int startY = (int) floatstartY;

                float floatendY = ((vp.getyCentre() -           
                    ((vp.getViewportWorldCentreY() - bg.endY) *                                 
                    vp.getPixelsPerMetreY())));

                int endY = (int) floatendY;

下面的代码块是真正行动发生的地方。我们用两个Bitmap对象的起始和结束坐标初始化四个Rect对象。请注意,计算出的点(或像素)由xClip确定,最初为零。因此,首先我们会看到background(如果它没有被剪辑)拉伸到屏幕的宽度。很快,我们会看到根据 Bob 的速度修改xClip,并展示每个位图的不同区域:

        // Define what portion of bitmaps to capture 
        // and what coordinates to draw them at
        fromRect1 = new Rect(0, 0, bg.width - bg.xClip,     
          bg.height);

        toRect1 = new Rect(bg.xClip, startY, bg.width, endY);
             fromRect2 = new Rect(bg.width - bg.xClip, 0, bg.width, bg.height);

        toRect2 = new Rect(0, startY, bg.xClip, endY);
        }// End if (!vp.clipObjects...

现在,我们确定当前首先绘制的是哪种背景(正常或反向),然后先绘制该背景,接着绘制另一种。

          //draw backgrounds
            if (!bg.reversedFirst) {

                canvas.drawBitmap(bg.bitmap,
                    fromRect1, toRect1, paint);
                canvas.drawBitmap(bg.bitmapReversed, 
                    fromRect2, toRect2, paint);

            } else {
                canvas.drawBitmap(bg.bitmap, 
                    fromRect2, toRect2, paint);

                canvas.drawBitmap(bg.bitmapReversed, 
                    fromRect1, toRect1, paint);
            }

我们可以根据 Bob 的速度和方向滚动,lv.player.getxVelocity(),如果xClip已达到当前第一个背景的末端,if (bg.xClip >= bg.width),只需将xClip设为零,并改变我们首先展示的位图。

          // Calculate the next value for the background's
            // clipping position by modifying xClip
            // and switching which background is drawn first,
            // if necessary.
            bg.xClip -= lm.player.getxVelocity() / (20 / bg.speed);
            if (bg.xClip >= bg.width) {
                bg.xClip = 0;
                bg.reversedFirst = !bg.reversedFirst;
            } 
            else if (bg.xClip <= 0) {
                bg.xClip = bg.width;
                bg.reversedFirst = !bg.reversedFirst;

            }
        }
    }
}

然后,在z层小于零的背景的游戏对象之前,我们添加对drawBackground()的调用。

// Rub out the last frame with arbitrary color
paint.setColor(Color.argb(255, 0, 0, 255));
canvas.drawColor(Color.argb(255, 0, 0, 255));

// Draw parallax backgrounds from -1 to -3
drawBackground(0, -3);

// Draw all the GameObjects
Rect toScreen2d = new Rect();

在绘制子弹之后,但在那些z顺序大于零的背景的调试文本之前。

// Draw parallax backgrounds from layer 1 to 3
drawBackground(4, 0);

// Text for debugging

现在,我们可以真正开始发挥创意设计关卡。

滚动视差背景

很快,我们将制作一些真正可玩的关卡,使用我们在过去四章中实现的所有功能。在我们这样做之前,让我们在Viewport类中找点乐趣。

对于玩家来说,在关卡中扫描并规划路线将非常有用。同样,在设计关卡时,放大并围绕关卡查看某个特定部分的外观,而无需让玩家角色到达该部分以便在屏幕上看到它,也会很有帮助。所以,让我们将暂停屏幕变成一个可移动视口。

带有可移动视口的暂停菜单

这样做既好又快。我们只需向Viewport类添加一堆新方法来改变焦点中心。然后,我们将在InputController中调用它们。

如果你记得我们在第六章实现InputController类时,平台游戏 – Bob, Beeps 和 Bumps,我们将所有控制逻辑封装在一个if(playing)测试中。我们还在else子句中实现了暂停按钮。我们将要做的就是将左、右、跳跃和射击按钮分别用作左、右、上和下来移动视口。

首先,向Viewport类添加以下方法:

public void moveViewportRight(int maxWidth){
  if(currentViewportWorldCentre.x < maxWidth -       
    (metresToShowX/2)+3) {

     currentViewportWorldCentre.x += 1;
  }
}

public void moveViewportLeft(){
  if(currentViewportWorldCentre.x > (metresToShowX/2)-3){
    currentViewportWorldCentre.x -= 1;
     }
}

public void moveViewportUp(){
  if(currentViewportWorldCentre.y > (metresToShowY /2)-3) {
        currentViewportWorldCentre.y -= 1;
   }
}

public void moveViewportDown(int maxHeight){
  if(currentViewportWorldCentre.y < 
    maxHeight - (metresToShowY / 2)+3) {

    currentViewportWorldCentre.y += 1;
  }
}

现在,将以下调用添加到我们在InputController类中刚刚讨论的if条件的else子句中。

//Move the viewport around to explore the map
switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
  case MotionEvent.ACTION_DOWN:
 if (right.contains(x, y)) {
 vp.moveViewportRight(l.mapWidth);
 } else if (left.contains(x, y)) {
 vp.moveViewportLeft();
 } else if (jump.contains(x, y)) {
 vp.moveViewportUp();
 } else if (shoot.contains(x, y)) {
 vp.moveViewportDown(l.mapHeight);
 } else if (pause.contains(x, y)) {
 l.switchPlayingStatus();
 }
      break;
}

在暂停屏幕上,玩家可以四处查看并规划他们在更复杂关卡中的路线。他们可能需要这么做。

关卡和游戏规则

我们已经实现了许多功能,但我们仍然没有一个方法将这些功能整合成一个可玩的游戏。我们需要能够在关卡之间移动,并且在移动时保持玩家状态。

在关卡之间移动

因为我们将要设计四个关卡,我们希望玩家能够在它们之间移动。首先,让我们在LevelManager构造函数的开始部分的switch语句中添加代码,包括我们即将构建的所有四个关卡:

switch (level) {
  case "LevelCave":
     levelData = new LevelCave();
     break;

// We can add extra levels here
case "LevelCity": 
 levelData = new LevelCity(); 
 break; 

case "LevelForest": 
 levelData = new LevelForest(); 
 break;

case "LevelMountain": 
 levelData = new LevelMountain(); 
 break;
}

如我们所知,我们通过从PlatformView构造函数中调用loadLevel()来开始游戏。参数包括关卡名称和玩家生成的坐标。如果你正在设计自己的关卡,那么你需要决定从哪个关卡和坐标开始。如果你将跟随我提供的关卡,请在PlatformView的构造函数中将loadLevel()的调用设置如下:

loadLevel("LevelCave", 1, 16);

if(lm.isPlaying())块中,在update方法中,我们每一帧设置视口以玩家为中心;添加以下代码以检测(并残忍地消灭)玩家如果他掉出地图,以及当他的生命值耗尽时,使游戏重新开始,拥有三条生命,零金钱,没有升级:

if (lm.isPlaying()) {
    // Reset the players location as 
    // the world centre of the viewport
    //if game is playing
    vp.setWorldCentre(lm.gameObjects.get(lm.playerIndex)
        .getWorldLocation().x,
        lm.gameObjects.get(lm.playerIndex)
        .getWorldLocation().y);

 //Has player fallen out of the map?
 if (lm.player.getWorldLocation().x < 0 ||
 lm.player.getWorldLocation().x > lm.mapWidth ||
 lm.player.getWorldLocation().y > lm.mapHeight) {

 sm.playSound("player_burn");
 ps.loseLife();
 PointF location = new PointF(ps.loadLocation().x,
 ps.loadLocation().y);

 lm.player.setWorldLocationX(location.x);
 lm.player.setWorldLocationY(location.y);
 lm.player.setxVelocity(0);
 }

 // Check if game is over
 if (ps.getLives() == 0) {
 ps = new PlayerState();
 loadLevel("LevelCave", 1, 16);
 }
}

现在,我们可以创建一个特殊的GameObject类,当玩家接触这个类时,会将玩家传送到一个预定的关卡和位置。然后我们可以策略性地将这些对象添加到我们的关卡设计中,它们将作为我们关卡之间的链接。创建一个名为Teleport的新类。如果你还没有这样做,请将Chapter8/drawable文件夹中的door.png文件添加到项目的drawable文件夹中。

这就是我们的Teleport对象在游戏中的样子:

在关卡之间移动

让我们创建一个简单的类来保存每个Teleport对象所需的数据。创建一个名为Location的新类,如下所示:

public class Location {
     String level;
     float x;
     float y;

     Location(String level, float x, float y){
        this.level = level;
        this.x = x;
        this.y = y;
     }
}

实际的Teleport类看起来像任何其他的GameObject类,但请注意它还包含一个Location成员变量。我们将看到关卡设计将如何保存Teleport的目的地,LevelManager类将初始化它,然后当玩家与它碰撞时,我们可以加载新的位置,将玩家送往他的目的地。

public class Teleport extends GameObject {

    Location target;

    Teleport(float worldStartX, float worldStartY, 
        char type, Location target) {

        final float HEIGHT = 2;
        final float WIDTH = 2;
        setHeight(HEIGHT); // 2 metres tall
        setWidth(WIDTH); // 1 metre wide
        setType(type);
        setBitmapName("door");

        this.target = new Location(target.level, 
            target.x, target.y);

        // Where does the tile start
        // X and y locations from constructor parameters
        setWorldLocation(worldStartX, worldStartY, 0);

        setRectHitbox();
    }

    public Location getTarget(){
        return target;
    }

    public void update(long fps, float gravity){
    }
}

为了让我们的Teleport类以让关卡设计师决定它将确切执行什么的方式工作,我们需要像这样向我们的LevelData类中添加内容:

ArrayList<String> tiles;
ArrayList<BackgroundData> backgroundDataList;
ArrayList<Location> locations;

// This class will evolve along with the project

然后,我们需要在想要设置传送门/门的关卡设计中的相应位置添加一个t,并在关卡类的构造函数中添加如下代码行。

请注意,你可以在地图中设置任意数量的Teleport对象,只要它们在代码中定义的顺序与设计中出现的顺序相匹配。当我们稍后查看实际的关卡设计时,我们会确切地看到这是如何工作的,但代码将如下所示:

// Declare the values for the teleports in order of appearance
locations = new ArrayList<Location>();
this.locations.add(new Location("LevelCity", 118f, 18f));

与往常一样,我们需要更新LevelManager类以加载和定位我们的传送点。以下是getBitmap()的新代码:

case 'z':
  index = 21;
  break;

case 't':
 index = 22;
 break;

default:
  index = 0;
  break;

getBitmapIndex()的新代码:

case 'z':
  index = 21;
     break;

case 't':
 index = 22;
 break;

default:
  index = 0;
  break;

在加载阶段,我们还需要跟踪我们的Teleport对象,以防有多个。所以,在loadMapData方法中添加一个新的局部变量,如下所示:

//Keep track of where we load our game objects
int currentIndex = -1;
int teleportIndex = -1;
// how wide and high is the map? Viewport needs to know

对于LevelManager类,我们最终需要初始化所有从关卡设计中获取的传送数据,将其存储在对象中,并添加到我们的gameObject ArrayList中。

case 'z':
    // Add a boulders to the gameObjects
    gameObjects.add(new Boulders(j, i, c));
    break;

 case 't':
 // Add a teleport to the gameObjects
 teleportIndex++;
 gameObjects.add(new Teleport(j, i, c,
 levelData.locations.get(teleportIndex)));

 break;

我们已经非常接近能够到处传送了。我们需要检测与传送点的碰撞,然后在玩家所需的位置加载新关卡。这段代码将放在PlatformView类中的碰撞检测开关块里,如下所示:

case 'f':
    sm.playSound("player_burn");
    ps.loseLife();
    location = new PointF(ps.loadLocation().x,
      ps.loadLocation().y); 
    lm.player.setWorldLocationX(location.x);
    lm.player.setWorldLocationY(location.y);
    lm.player.setxVelocity(0);
    break;

case 't':
 Teleport teleport = (Teleport) go;
 Location t = teleport.getTarget();
 loadLevel(t.level, t.x, t.y);
 sm.playSound("teleport");
 break;

default:// Probably a regular tile
    if (hit == 1) {// Left or right
        lm.player.setxVelocity(0);
        lm.player.setPressingRight(false);
    }
    if (hit == 2) {// Feet
        lm.player.isFalling = false;
    }
    break;

当加载新关卡时,PlayerMachineGunBullet对象都将从头开始创建。因此,我们需要在loadLevel方法中添加一行代码,将当前的机枪射速从PlayerState类重新加载到MachineGun类中。添加高亮显示的代码:

ps.saveLocation(location);

// Reload the players current fire rate from the player state
lm.player.bfg.setFireRate(ps.getFireRate());

现在,我们可以真正开始设计关卡了。

关卡设计

你可以从Chapter8/java文件夹中复制并粘贴四个类到你的项目中开始游戏,或者你可以从头开始设计自己的关卡。这些关卡相当大,复杂且难以通关。由于篇幅限制,无法在书籍或电子书中以有意义的方式呈现关卡设计,因此你需要打开LevelCaveLevelCityLevelForestLevelMountain设计文件,以查看四个关卡的详细信息。

然而,以下内容将简要讨论四个设计中的关卡、图片和一些截图,但不会包含实际的代码。

注意

请注意,以下截图展示了本章最后将要介绍的新 HUD。

洞穴

洞穴关卡是整个游戏的开始。它不仅包含一些令人稍微感到沮丧的跳跃,还有大量的火焰,一旦跌落可能致命。

洞穴

由于玩家开始时只有一把微弱的机枪,因此关卡中只有少数无人机。但有两个别扭的守卫需要翻越。

洞穴

城市

城市中拥有巨大的奖励,尤其是在左下角收集硬币和左上角升级机枪。

城市

然而,如果玩家想要收集所有散落的硬币而不选择放弃它们,底层有一个跳跃非常别扭的守卫。必须从左侧几乎垂直上升,这很可能会让玩家感到沮丧。如果玩家选择不去升级机枪,他可能会在与下一层门口外的双守卫战斗中遇到困难。

城市

森林

森林可能是所有关卡中最困难的一个,有一段残酷的长距离跳跃,非常容易跳过或未跳够。

森林

当 Bob 的像素悬挂在平台边缘时,超过一打无人机正等着猛扑向他。

森林

山脉

清新的山间空气意味着 Bob 几乎要成功了。四周没有守卫或无人机的踪影。

山脉

然而,看看那条蜿蜒的跳跃路径,如果 Bob 放错了一个像素的位置,大部分路径都会让他直接掉回底部。

山脉

提示

如果你想要在不完成前面的艰难关卡的情况下尝试每个关卡,当然,你可以直接从你选择的关卡和位置开始。为此,只需将PlatformView构造函数中的loadLevel()调用更改为以下之一:

loadLevel("LevelMountain", 118, 17);
loadLevel("LevelForest", 1, 17);
loadLevel("LevelCity", 118, 18);
loadLevel("LevelCave", 1, 16);

HUD

画龙点睛之笔是添加一个 HUD。PlatformViewdraw方法中的这段代码使用了现有游戏对象中的一些图像。

在最后一次调用drawBackground()之后,并在绘制调试文本之前添加代码:

// Draw the HUD
// This code needs bitmaps: extra life, upgrade and coin
// Therefore there must be at least one of each in the level

int topSpace = vp.getPixelsPerMetreY() / 4;
int iconSize = vp.getPixelsPerMetreX();
int padding = vp.getPixelsPerMetreX() / 5;
int centring = vp.getPixelsPerMetreY() / 6;
paint.setTextSize(vp.getPixelsPerMetreY()/2);
paint.setTextAlign(Paint.Align.CENTER);

paint.setColor(Color.argb(100, 0, 0, 0));
canvas.drawRect(0,0,iconSize * 7.0f, topSpace*2 + iconSize,paint);
paint.setColor(Color.argb(255, 255, 255, 0));

canvas.drawBitmap(lm.getBitmap('e'), 0, topSpace, paint);
canvas.drawText("" + ps.getLives(), (iconSize * 1) + padding, 
  (iconSize) - centring, paint);

canvas.drawBitmap(lm.getBitmap('c'), (iconSize * 2.5f) + padding, 
  topSpace, paint);

canvas.drawText("" + ps.getCredits(), (iconSize * 3.5f) + padding * 2, (iconSize) - centring, paint);

canvas.drawBitmap(lm.getBitmap('u'), (iconSize * 5.0f) + padding, 
  topSpace, paint);

canvas.drawText("" + ps.getFireRate(), (iconSize * 6.0f) + padding * 2, (iconSize) - centring, paint);

我想我们完成了!

总结

我们完成了这个平台游戏,因为篇幅有限。为什么不尝试实施以下一些或全部改进和功能呢?

修改Player类中的代码,使 Bob 逐渐加速和减速,而不是一直以全速运行。只需在玩家按住左右方向的每个帧增加速度,在他们不按的每个帧减少速度。

完成这些后,将前面的代码添加到update方法中的碰撞检测switch块中,以使玩家在雪地上打滑,在混凝土上加速,并为每种瓦片类型提供不同的行走/着陆声效。

在 Bob 身上画一把枪,并调整Bullet对象生成的高度,使其看起来是从他的机枪枪管中射出的。

让一些对象可以被推动。在GameObject中添加一个isPushable成员,并让碰撞检测简单地使对象后退一点。也许,Bob 可以把矿车推入火中,以跳过特别宽的火坑。请注意,推动那些掉到另一个层次的对象将比推动保持在相同y坐标的对象复杂得多。

可破坏的瓦片听起来很有趣。给它们一个力量变量,当被子弹击中时递减,当达到零时从gameObjects中移除。

移动平台是优秀平台游戏的重要组成部分。只需向瓦片对象添加航点,并在update方法中添加移动代码。挑战将是如何分配航点。你可以让它们都向左或向右,或者向上或向下移动固定的空间数量,或者像我们编写Guard对象那样,使用某种setTileWaypoint方法。

通过保存玩家收集到的硬币总数,记住哪些关卡已被解锁,并在菜单屏幕上提供重新玩任何已解锁关卡的选项,使游戏更具持久性。

使用传送点作为路标,让游戏变得更容易。调整视口缩放以适应不同屏幕尺寸。当前的缩放对于一些小手机来说可能有点太低了。

加入计时跑以获得高分、排行榜和成就,并增加更多关卡。

在下一章中,我们将看到一个更小的项目,但仍然很有趣,因为我们将使用 OpenGL ES 进行超快速、流畅的绘制。