安卓游戏编程示例(三)
原文:
zh.annas-archive.org/md5/B228CC957519C7ABCD7559EDEA0B426A译者:飞龙
第七章:平台游戏 - 枪支、生命、金钱和敌人
在本章中,我们将做很多事情。首先,我们将构建一个可变射速的机枪,让它射击子弹。然后,我们将引入拾取物或收藏品。这些给玩家在尝试逃到下一个关卡时提供了搜寻的目标。
然后,就在 Bob 开始认为他的生活是充满草丛和收藏品的幸福生活时,我们将为他构建两个对手,让他智取或消灭。一个追踪无人机和一个巡逻的守卫。我们可以轻松地将所有这些事物添加到我们的关卡设计中。
准备,瞄准,开火。
现在,我们可以给我们的英雄一把枪,稍后,我们可以给他敌人射击。我们将创建一个MachineGun类来完成所有工作,以及一个Bullet类来表示它发射的炮弹。Player类将控制MachineGun类,而MachineGun类将控制和跟踪它发射的所有Bullet对象。
创建一个新的 Java 类,将其命名为Bullet。子弹并不复杂。我们的子弹需要有一个x和y的位置,一个水平速度和一个方向,以帮助计算速度。
这意味着以下简单的类、构造函数以及一堆的 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 个就足够了,正如我们将看到的。成员numBullets和nextBullet帮助类跟踪其 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,如果你想知道更多,请访问:
我们有一个构造函数,它只是初始化子弹,lastShotTime和nextBullet:
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类记住一个x和y位置,以便在玩家失去生命时进行重生。输入这些成员变量和简单的构造函数:
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 类:Coin,ExtraLife和MachineGunUpgrade。注意,收集物比平台稍小一些,这可能正如我们所预期的。依次输入它们的代码。
以下是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类,使其能够处理我们的关卡设计中这三个新对象,并将它们添加到GameObjects的ArrayList中。为此,我们需要在三个地方更新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.png,coin.png和life.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_VELOCITY和MAX_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对象那样配置这个对象,此外,我们还要为其配置所有动画设置,就像我们对Player和Guard对象所做的那样。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轴的确切像素。
y、endY、z和speed成员变量用于保存作为参数传递的相关值:
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对象,用来保存bitmap和reversedBitmap的起始和结束点。
按照所示实现drawBackground方法的第一部分:
private void drawBackground(int start, int stop) {
Rect fromRect1 = new Rect();
Rect toRect1 = new Rect();
Rect fromRect2 = new Rect();
Rect toRect2 = new Rect();
现在,我们只需遍历所有背景,使用start和stop参数来确定哪些背景具有我们当前感兴趣的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;
当加载新关卡时,Player、MachineGun和Bullet对象都将从头开始创建。因此,我们需要在loadLevel方法中添加一行代码,将当前的机枪射速从PlayerState类重新加载到MachineGun类中。添加高亮显示的代码:
ps.saveLocation(location);
// Reload the players current fire rate from the player state
lm.player.bfg.setFireRate(ps.getFireRate());
现在,我们可以真正开始设计关卡了。
关卡设计
你可以从Chapter8/java文件夹中复制并粘贴四个类到你的项目中开始游戏,或者你可以从头开始设计自己的关卡。这些关卡相当大,复杂且难以通关。由于篇幅限制,无法在书籍或电子书中以有意义的方式呈现关卡设计,因此你需要打开LevelCave、LevelCity、LevelForest和LevelMountain设计文件,以查看四个关卡的详细信息。
然而,以下内容将简要讨论四个设计中的关卡、图片和一些截图,但不会包含实际的代码。
注意
请注意,以下截图展示了本章最后将要介绍的新 HUD。
洞穴
洞穴关卡是整个游戏的开始。它不仅包含一些令人稍微感到沮丧的跳跃,还有大量的火焰,一旦跌落可能致命。
由于玩家开始时只有一把微弱的机枪,因此关卡中只有少数无人机。但有两个别扭的守卫需要翻越。
城市
城市中拥有巨大的奖励,尤其是在左下角收集硬币和左上角升级机枪。
然而,如果玩家想要收集所有散落的硬币而不选择放弃它们,底层有一个跳跃非常别扭的守卫。必须从左侧几乎垂直上升,这很可能会让玩家感到沮丧。如果玩家选择不去升级机枪,他可能会在与下一层门口外的双守卫战斗中遇到困难。
森林
森林可能是所有关卡中最困难的一个,有一段残酷的长距离跳跃,非常容易跳过或未跳够。
当 Bob 的像素悬挂在平台边缘时,超过一打无人机正等着猛扑向他。
山脉
清新的山间空气意味着 Bob 几乎要成功了。四周没有守卫或无人机的踪影。
然而,看看那条蜿蜒的跳跃路径,如果 Bob 放错了一个像素的位置,大部分路径都会让他直接掉回底部。
提示
如果你想要在不完成前面的艰难关卡的情况下尝试每个关卡,当然,你可以直接从你选择的关卡和位置开始。为此,只需将PlatformView构造函数中的loadLevel()调用更改为以下之一:
loadLevel("LevelMountain", 118, 17);
loadLevel("LevelForest", 1, 17);
loadLevel("LevelCity", 118, 18);
loadLevel("LevelCave", 1, 16);
HUD
画龙点睛之笔是添加一个 HUD。PlatformView的draw方法中的这段代码使用了现有游戏对象中的一些图像。
在最后一次调用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 进行超快速、流畅的绘制。