Skip to content

Latest commit

 

History

History
2464 lines (1906 loc) · 120 KB

File metadata and controls

2464 lines (1906 loc) · 120 KB

二十、更多模式,滚动背景,建造玩家之船

这是本书最长的章节之一,在我们的设备/仿真器上看到结果之前,我们有相当多的工作和理论要完成。然而,你将学到的东西,然后实施,会给你能力,大幅增加游戏的复杂性,你可以建立。一旦你理解了什么是实体-组件系统,以及如何使用工厂模式构建游戏对象,你将能够将几乎任何你能想象的游戏对象添加到你的游戏中。如果你买这本书不仅仅是为了学习 Java,而是因为你想设计和开发你自己的电子游戏,那么这一章就属于你了。

在本章中,我们将涵盖以下主题:

  • 更仔细地观察游戏对象及其多样性导致的问题
  • 引入实体-组件模式
  • 介绍简单工厂模式
  • 每个物体都是一个理论
  • 指定所有游戏对象
  • 编码接口以匹配所有需要的组件
  • 准备一些空的组件类
  • 编写通用Transform
  • 每个对象都是一个GameObject——实现
  • 对播放器的组件进行编码
  • 对激光器组件进行编码
  • 编码背景的组成部分
  • 建一个GameObjectFactory
  • 编码Level
  • 把这一章的所有内容放在一起

这是一个相当大的清单,所以我们最好开始。

遇见游戏对象

因为我们将在本章开始游戏对象,让我们添加所有的图形文件到项目。图形文件可以从 GitHub repo 上的Chapter 20/drawable文件夹中获取。直接复制粘贴到 AndroidStudio 项目浏览器窗口的app/res/drawable文件夹中。

提醒所有这些对象的行为

这是一个重要的话题,当我们接下来更详细地讨论设计模式时,它将为我们做好准备。快速查看以下图形,所有图形都代表游戏对象,以便我们充分了解我们将使用的内容:

Figure 20.1 – Representation of the game objects

图 20.1–游戏对象的表示

现在,我们可以了解实体-组件模式。

实体-组件模式

我们现在将花 5 分钟时间沉浸在显然无法解决的混乱的痛苦中。然后,我们将看到实体-组件模式是如何拯救的。

为什么很多不同的对象类型很难管理

这个项目设计提出了多个需要讨论的问题,然后我们才能开始敲击键盘。首先是游戏对象的多样性。让我们考虑如何处理所有不同的对象。

在前面的项目中,我们为每个对象编写了一个类。我们有像BatBallSnakeApple这样的课程。然后,在update方法中,我们会更新它们,而在draw方法中,我们会绘制它们。在最近的项目 Snake 中,我们朝着正确的方向迈出了一步,并在更新和绘制阶段让每个对象处理自己。

我们可以开始使用相同的结构来完成这个项目。这是可行的,但是在项目接近尾声时,一些主要的编码噩梦会变得很明显。

第一个编码噩梦

在前面的项目中,一旦我们对对象进行了编码,我们所需要做的就是在游戏开始时实例化它们,也许是这样的:

Snake mSnake = new Snake(Whatever parameters);
Apple mApple = new Apple(Whatever parameters);

然后,我们会更新它们,可能是这样的:

mSnake.update(mFPS);
// Apple doesn't need updating

像这样画它们:

mSnake.draw(mPaint, mCanvas);
mApple.draw(mPaint, mCanvas);

看起来我们这次需要做的只是编码、实例化、更新和绘制一堆不同的对象,可能是这样的:

Diver mDiver = new Diver(Whatever parameters);
Chaser mChaser = new Chaser(Whatever parameters);
Patroller mPatroller = new Patroller(Whatever parameters);
PlayerShip mPlayerShip = new PlayerShip(Whatever parameters);

然后,我们需要更新它们:

mDiver.update(mFPS);
mChaser.update(mFPS);
mPatroller.update(mFPS);
mPlayerShip.update(mFPS);

之后,我们需要画出它们:

mDiver.draw(mPaint, mCanvas);
mChaser.draw(mPaint, mCanvas);
mPatroller.draw(mPaint, mCanvas);
mPlayerShip.draw(mPaint, mCanvas);

但是当我们需要,比如说,三个追逐者的时候,我们该怎么办?下面是对象初始化部分:

Diver mDiver = new Diver(Whatever parameters);
Chaser mChaser1 = new Chaser(Whatever parameters);
Chaser mChaser2 = new Chaser(Whatever parameters);
Chaser mChaser3 = new Chaser(Whatever parameters);
Patroller mPatroller = new Patroller(Whatever parameters);
PlayerShip mPlayerShip = new PlayerShip(Whatever parameters);

它会起作用,但是接下来updatedraw方法也必须增长。现在,考虑碰撞检测。我们需要分别获得每个外星人的碰撞器和每个激光器,然后在玩家面前测试它们。然后,有所有玩家的激光对付所有外星人。它已经很笨重了。如果我们有 10 个甚至 20 个游戏对象呢?游戏引擎会失控,变成编程噩梦。

这种方法的另一个问题是我们不能利用继承。例如,所有的外星人、激光和玩家都以几乎相同的方式绘制自己。我们最终将得到大约六个具有相同代码的draw方法。如果我们改变调用draw的方式或者处理位图的方式,我们将需要更新所有六个类。

肯定有更好的办法。

使用通用游戏对象来获得更好的代码结构

如果每一个物体、玩家、所有外星人类型和所有激光都是一个通用类型,那么我们可以将它们打包成ArrayList实例或类似的东西,并循环通过它们的每一个update方法,然后是它们的每一个draw方法。

我们已经知道了一种方法——继承。乍一看,这似乎是一个完美的解决方案。我们可以创建一个抽象的GameObject类,然后用PlayerLaserDiverChaserPatroller类扩展它

在六个类中相同的draw方法可以保留在父类中,这样我们就不会有浪费、难以维护、重复代码的问题。太好了。

这种方法的问题在于游戏对象在某些方面有多多样。例如,所有的外星人移动方式都不同。追逐者追逐玩家,而巡逻者只是从左到右,从右到左的飞来飞去。潜水员不断地从顶部呼吸和潜水。

我们如何将这种多样性融入到需要控制这种运动的update方法中?也许我们可以这样做:

void update(long fps){
     switch(alienType){
          case "chaser":
               // All the chaser's logic goes here
               Break;
          case "patroller":
               // All the patroller's logic here
               Break;
          case "diver":
               // All the diver's logic here
               Break;
     }
}

光是update方法就比整个GameEngine类都大,我们甚至还没有考虑如何处理玩家和激光。

正如大家可能从 第八章面向对象编程中回忆到的,我们扩展一个类的时候,也可以覆盖具体的方法。这意味着我们可以为每一种外星人类型准备一个不同版本的update。不幸的是,这种方法也有一个问题。

GameEngine类引擎必须“知道”它正在更新的是哪种类型的对象,或者至少能够查询它正在更新的GameObject类,以便调用正确的update方法,可能是这样的:

if(currentObject.getType == "diver"){
// Get the diver element from the GameObject
diver temporaryDiver = (Diver)currentObject;
// Now we can call update- at last
temporaryDiver.update(fps);
// Now handle every other type of GameObject sub-class
}

即使是解决方案中看起来有效的部分,在仔细观察后也会分崩离析。我之前提到过draw方法中的代码对于六个对象是相同的,所以draw方法可以是父类的一部分,并且被所有子类使用,而不是我们必须编码六个单独的绘制方法。

那么,当可能只有一个对象需要以不同的方式绘制时,比如滚动背景,会发生什么呢?答案是解决方案不起作用。

现在我们已经看到了当对象彼此不同,但却大声呼喊来自同一个父类时出现的问题,是时候看看我们将在这个项目和下一个项目中使用的解决方案了。

我们需要的是一种新的思维方式来构建我们所有的游戏对象。

成分重于遗传

继承之上的组合是指在其他对象中组合对象的思想。如果我们可以编写一个类(而不是一个方法)来处理对象是如何绘制的呢?然后,对于所有以相同方式绘制自己的类,我们可以在GameObject内实例化其中一个特殊的绘制类。然后,当GameObject做了不同的事情时,我们可以简单地用不同的绘图、更新或任何相关的类来组合它。我们所有对象中的所有相似之处都可以从使用相同的代码中受益,所有的不同之处不仅可以从封装中受益,还可以从基类中抽象出来。

注意这一节的标题是构图战胜继承,不是构图代替继承。组合并不能取代继承,你在 第八章面向对象编程中所学的一切,依然适用,但是在适当的时候,组合而不是继承。

GameObject类是实体,它将由做诸如更新其位置并将其绘制到屏幕上的事情的类组成,这些类是它的组件,因此是实体-组件模式。

当我们使用组合而不是继承来创建一组类来表示我们这里的行为/算法时,这也被称为策略模式。你可以愉快地使用你在这里学到的一切,并将其称为战略模式。实体组件是一个不太为人所知但更具体的实现,这就是为什么我们称之为。区别在于学术性,但如果你想进一步探索事物,请随时求助于谷歌。

然而,我们自己面临的问题是,知道任何给定的对象将由什么组成以及知道如何将它们组合在一起本身就有点技术性。

我的意思是,这几乎就像我们需要一个工厂来完成我们所有的物体组装。

简单工厂模式

实体-组件模式,以及优先于继承的组合使用,起初听起来很棒,但也带来了一些问题,尤其是它加剧了我们已经讨论过的所有问题。这意味着我们新的GameObject类需要“知道”游戏中所有不同的组件和每一种类型的对象。它将如何向自身添加所有正确的组件?

的确,如果我们要拥有这个通用的GameObject类,它可以是我们想要的任何东西,无论是DiverPatrollerPlayerShipPinkElephant还是其他什么东西,那么我们将不得不编码一些逻辑,这些逻辑“知道”如何构建这些超级灵活的GameObject实例,并用正确的组件组成它们。但是,将所有这些代码添加到类本身会使它异常笨拙,并首先否定使用实体组件模式的全部理由。

我们需要一个构造函数来完成类似于这个假设的GameObject代码所做的事情:

class GameObject{
     MovementComponent mMoveComp;
     GraphicsComponent mGraphComp;
     InputComponent mInpComp;
     Transform mTransform;
     // More members here
// The constructor
GameObject(String type){
          if(type == "diver"){
               mMoveComp = new DiverMovementComponent ();
               mGraphComp = new StdGraphicsComponent();
          }
          else if(type =="chaser"){
               mMoveComp = new ChaserMovementComponent();
               mGraphComp = new StdGraphicsComponent();
          }
          // etc.
          …
}
}

别忘了我们需要对每种类型的外星人、背景和每个组件的玩家做同样的事情。GameObject类不仅需要知道哪些组件与哪些GameObject一起使用,还需要知道哪些GameObject实例不需要某些组件,例如用于控制玩家的输入相关组件GameObject

比如在 第 19 章用观察者模式收听、多点触控、构建粒子系统中,我们编码了一个UIController类,注册了GameEngine类作为观察者。每次GameEngineonTouchEvent方法中接收到触摸数据时,我们通过将Rect对象的ArrayList传递到其handleInput方法,使其知道抬头显示器按钮。

GameObject类将需要理解所有这些逻辑。在实体-组件模式中使用组合而不是继承所获得的任何好处或效率都将完全丧失。

此外,如果游戏设计者突然宣布了一种新的外星人——可能是一个传送到玩家附近的隐形人外星人,拍了一张照片,然后又传送走了呢?编码一个新的GraphicsComponent,也许是一个CloakingGraphicsComponent,当它可见和不可见的时候“知道”,还有一个新的MovementComponent,也许是一个CloakerMovementComponent,它传送而不是常规移动,这没问题,但是我们真的要给GameObject构造器添加一大堆新的if语句吗?是的,是这种情况下不幸的答案。

事实上,情况甚至比这更糟。如果游戏设计师在某天早上宣布潜水员现在可以穿斗篷了,会发生什么?潜水员现在不仅仅需要一种不同类型的GraphicsComponent。回到GameObject类,我们需要编辑所有那些if语句。然后,游戏设计师意识到,虽然隐形潜水员很酷,但原来总是可见的潜水员更具威胁性。我们需要潜水员和隐形潜水员,所以需要更多的改变。

如果你想要一个接受输入的新的GameObject,事情会变得更糟,因为实例化GameObject的类必须确保传入一个GameEngineBroadcaster引用,并且GameObject必须知道如何处理它。

另外,请注意每个GameObject都有一个InputComponent,但不是每个GameObject都需要一个。这是对内存的浪费,意味着要么在不需要的时候初始化InputComponent,浪费来自GameEngineBroadcaster的呼叫,要么几乎每个GameObject都有一个空InputComponent,随时等待游戏崩溃。

在前面的假设代码中,最后要注意的是Transform类型的对象。所有GameObject实例都将由一个Transform对象组成,该对象保存诸如大小、位置等细节。随着本章的继续,我们将提供更多关于Transform课程的详细信息。

终于,一些好消息

其实可以想象的场景更多,最终都是越来越大的GameObject类。工厂模式——或者更准确地说,在这个项目中,简单工厂模式——是这些问题的解决方案,也是实体组件模式的完美合作伙伴。

简单的工厂模式只是开始学习工厂模式的一种更简单的方式。完成这本书后,考虑在网上搜索工厂模式。

游戏设计者将为游戏中的每种类型的对象提供一个规范,程序员将提供一个工厂类,根据游戏设计者的规范构建GameObject实例。当游戏设计者为实体提出古怪的想法时,我们所需要做的就是要求一个新的规范。有时,这将涉及在使用现有组件的工厂中增加一条新的生产线,尽管有时,这将意味着编码新的组件或者更新现有组件。关键是游戏设计师有多有创造力并不重要;GameObjectGameEngineRendererPhysicsEngine保持不变。也许我们有这样的东西:

GameObject currentObject = new GameObject;
switch (objectType) {
     case "diver":
          currentObject.setMovement (new DiverMovement());
          currentObject.setDrawing (new StdDrawing());
          break;
     case "chaser":
          currentObject.setMovement (new ChaserMovement());
          currentObject.setDrawing (new StdDrawing());
          break;
}

这里,检查当前对象类型,并向其添加适当的组件(类)。追逐者和潜水员都有一个StdDrawing组件,但是两者都有不同的移动(更新)组件。setMovementsetDrawing方法是GameObject类的一部分,我们将在本章的后面部分看到它们的真实等价物。这段代码与我们将要使用的代码并不完全相同,但它离我们并不太远。

这是真的该代码非常类似于我们刚刚讨论并揭示为完全不充分的代码。然而,最大的区别是这段代码只能存在于工厂类的一个实例中,而不能存在于GameObject的每个实例中。此外,这个类甚至不需要在我们游戏的阶段之后持续存在,当GameObject实例被建立,准备行动。

我们还将通过编写一个Level类来进一步研究,该类将决定这些规范的类型和数量。这进一步分离了游戏设计、特定级别设计和游戏引擎/工厂编码的角色和职责。

至今总结

看看这些要点,它们描述了我们到目前为止讨论的所有内容。

  • 我们将有MovementComponentGraphicsComponentSpawnComponentInputComponent等组件类。这些将是没有特定功能的接口。
  • 会有具体实现这些接口的类,比如DiverMovementPlayerMovementStandardGraphicsBackgroundGraphicsPlayerInput等等。
  • 我们将为每个游戏对象设置规范类,指定游戏中每个对象将拥有的组件。这些规格还将有额外的细节,如尺寸、速度、名称和所需外观所需的图形文件。
  • 将会有一个工厂类知道如何读取规范类并组装通用但内部不同的GameObject实例。
  • 将会有一个等级类知道每种类型的GameObject需要哪种和多少种,并将从工厂类“订购”它们。
  • 最终结果将是我们将有一个整洁的GameObject实例ArrayList,非常容易更新、绘制并传递给需要它们的类。

现在,让我们看看我们的对象规范。

物体规格

现在,我们知道我们所有的游戏对象都将由精选的组件构建而成。有时,这些组件对于特定的游戏对象来说会是唯一的,但大多数情况下,这些组件会用于多个不同的游戏对象中。我们需要一种方法来指定一个游戏对象,以便工厂类知道使用什么组件来构造每个对象。

首先,我们需要一个父规范类,其他规范可以从中派生出来。这允许我们以多种形式使用它们,而不必为每种类型的对象在同一个工厂中构建不同的工厂或不同的方法。

对 ObjectSpec 父类进行编码

这个类将是所有规范类的基类/父类。它将拥有所有必需的获取器,这样工厂类就可以获得它需要的所有数据。然后,正如我们将很快看到的,所有表示真实游戏对象的类将只需初始化适当的成员变量并调用父类的构造函数。因为我们永远不想实例化这个父类的一个实例,只需要extend它,我们将将其声明为abstract

创建一个名为ObjectSpec的新类,并按如下方式编码:

import android.graphics.PointF;
abstract class ObjectSpec {
    private String mTag;
    private String mBitmapName;
    private float mSpeed;
    private PointF mSizeScale;
    private String[] mComponents;
    ObjectSpec(String tag, String bitmapName, 
          float speed, PointF relativeScale, 
          String[] components) {

        mTag = tag;
        mBitmapName = bitmapName;
        mSpeed = speed;
        mSizeScale = relativeScale;
        mComponents = components;
    }
    String getTag() {
        return mTag;
    }
    String getBitmapName() {
        return mBitmapName;
    }
    float getSpeed() {
        return mSpeed;
    }
    PointF getScale() {
        return mSizeScale;
    }
    String[] getComponents() {
        return mComponents;
    }
}

记下所有成员变量的。每个规格都有一个标签/标识符(mTag),工厂会将该标签/标识符传递给已完成的GameObject实例,以便PhysicsEngine可以做出碰撞决策。每个都有一个位图(mBitmapName)的名称,它对应于我们添加到drawable文件夹中的一个图形文件。此外,每个规格将有一个速度和尺寸(mSpeedmSizeScale)。

我们不使用简单的大小变量来代替听起来有点复杂的mSizeScale变量的原因与使用屏幕坐标而不是世界坐标的问题有关。因此,我们可以缩放所有游戏对象,使其在不同设备上看起来大致相同。我们将使用相对于屏幕上像素数量的尺寸,因此mSizeScale。在下一个项目中,当我们学习如何实现一个在游戏世界中移动的虚拟相机时,我们的尺寸将更加自然。你可以把尺寸想象成米或者游戏单位。

可能最需要注意的成员变量是字符串的mComponents数组列表。这将包含构建这个游戏对象所需的所有组件的列表。

如果你回头看构造函数,你会看到它有一个匹配每个成员的参数。然后,在构造函数内部,从参数中初始化每个成员。正如我们在编写真正的规范时所看到的,我们所需要做的就是用相关的值调用这个超类构造函数,新的规范将被完全初始化。

看看这个类的所有其他方法;它们所做的只是提供对成员变量值的访问。

现在,我们可以编写真正的规范来扩展这个类。

对所有特定对象规范进行编码

虽然我们将只实现本章中的播放器、激光器和后台组件类,但是我们现在将实现所有的规范类。在下一章中,他们将为我们编写与外星人相关的组件代码做好准备。

该规范确切地定义了哪些组件被组合成一个对象,以及其他属性,如标签、位图、速度和大小/比例。

让我们一个一个来看。您将需要为每一个创建一个新的类,但是我不需要继续提示您这样做了。

AlienChaseSpec

这指定了追赶玩家的外星人,一旦他们在一条线上或几乎在一条线上,就向他们发射激光。添加并检查以下代码,以便我们可以谈论它:

import android.graphics.PointF;
class AlienChaseSpec extends ObjectSpec {
     // This is all the unique specifications 
     // for an alien that chases the player
     private static final String tag = "Alien";
     private static final String bitmapName = 
     "alien_ship1";
     private static final float speed = 4f;
     private static final PointF relativeScale = 
     new PointF(15f, 15f);
     private static final String[] components = new String 
      [] {
          "StdGraphicsComponent",
          "AlienChaseMovementComponent", 
          "AlienHorizontalSpawnComponent"};
     AlienChaseSpec(){
          super(tag, bitmapName, 
                speed, relativeScale, 
                components);
     }
}

标签变量被初始化为Alien。所有的外星人,不管他们是什么类型,都会有一个Alien的标签。正是它们不同的组成部分决定了它们不同的行为。Alien的通用标签将足以让Physics等级确定与玩家或玩家激光的碰撞。bitmapName变量被初始化为alien_Ship1。请随意检查drawable文件夹,并确认这是代表追逐者的图形。

它将具有4的速度,我们将在本章稍后开始对一些组件进行编码时,看到如何准确地将其转化为像素速度。

尺寸(relativeScale)的PointF实例被初始化为15f, 15f。这意味着游戏对象及其位图将被缩放到屏幕宽度的十五分之一。我们将在本章后面对组件类进行编码时查看这方面的代码。

组件数组已由三个组件初始化:

  • StdGraphicsComponent类处理draw方法,游戏每一帧都可以调用。StdGraphicsComponent类将实现GraphicsComponent接口。请记住,正是通过这个界面,我们可以确定StdGraphicsComponent将处理draw方法。
  • AlienChaseMovementComponent类将掌握追逐者外星人如何追逐的逻辑。它将实现MovementComponent接口,因此将保证处理move方法,每次我们在GameObject上调用update时都会调用该方法。
  • AlienHorizontalSpawnComponent类将保存在屏幕外水平生成一个对象所需的逻辑。

显然,我们将不得不对所有这些表示组件的类以及它们实现的接口进行编码。

最后,我们用super…调用超类构造函数,所有的值都被传递到的ObjectSpec类构造函数中,在这里它们被初始化,以便准备在工厂中使用。

AlienDiverSpec

添加以下职业来指定潜水员外星人,它将不断扑向玩家试图摧毁他们:

import android.graphics.PointF;
class AlienDiverSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for an alien that dives
    private static final String tag = "Alien";
    private static final String bitmapName = "alien_ship3";
    private static final float speed = 4f;
    private static final PointF relativeScale = 
        new PointF(60f, 30f);

    private static final String[] components = new String 
    [] {
          "StdGraphicsComponent",
          "AlienDiverMovementComponent", 
          "AlienVerticalSpawnComponent"};
    AlienDiverSpec(){
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

这个类是,格式和我们详细讨论过的上一个类一样。不同之处在于它有不同的位图和大小/比例。或许你会注意到最有意义的是,图形组件保持不变,但移动和产卵组件不同。

AlienDiverMovementComponent类将包含潜水逻辑,而AlienVerticalSpawnComponent将负责在屏幕顶部生成潜水员外星人。

AlienLaserSpec

接下来,添加外星激光的规格。这个将是一些外星人发射的炮弹:

import android.graphics.PointF;
class AlienLaserSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for an alien laser
    private static final String tag = "Alien Laser";
    private static final String bitmapName = "alien_laser";
    private static final float speed = .75f;
    private static final PointF relativeScale = 
          new PointF(14f, 160f);

    private static final String[] components = new String 
    [] {
          "StdGraphicsComponent",
          "LaserMovementComponent", 
          "LaserSpawnComponent"};
    AlienLaserSpec(){
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

AlienLaserSpec类也使用StdGraphicsComponent,但是有自己的LaserMovementComponentLaserSpawnComponent

为了让获得可以在合适的时间调用的合适的方法,三个组件将分别实现GraphicsComponentMovementComponentSpawnComponent接口。

这些基于激光的组件也将被PlayerLaserSpec类使用,但是PlayerLaserSpec类将具有不同的图形、不同的标签,并且速度也会稍微快一些。

AlienPatrolSpec

我确信你能猜到这个类是巡逻者外星人的规范:

import android.graphics.PointF;
class AlienPatrolSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for a patrolling alien
    private static final String tag = "Alien";
    private static final String bitmapName = "alien_ship2";
    private static final float speed = 5f;
    private static final PointF relativeScale = 
          new PointF(15f, 15f);

    private static final String[] components = new String 
    [] {
          "StdGraphicsComponent",
          "AlienPatrolMovementComponent", 
          "AlienHorizontalSpawnComponent"};
    AlienPatrolSpec(){
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

注意AlienPatrolSpec类使用了一个独特的移动组件(AlienPatrolMovementComponent)但是使用了StdGraphicsComponent类并且使用了与追逐者外星人相同的AlienHorizontalSpawnComponent类。

BackgroundSpec

现在,是时候做一些有点不同的事情了。增加BackgroundSpec类:

import android.graphics.PointF;
class BackgroundSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for the background
    private static final String tag = "Background";
    private static final String bitmapName = "background";
    private static final float speed = 2f;
    private static final PointF relativeScale = 
          new PointF(1f, 1f);

    private static final String[] components = new String 
    [] {
          "BackgroundGraphicsComponent",
          "BackgroundMovementComponent", 
          "BackgroundSpawnComponent"};
    BackgroundSpec() {
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

这个类有三个全新的背景相关组件:

  • BackgroundGraphicsComponent将负责并排绘制背景图像的两个副本。
  • BackgroundMovementComponent将注意移动屏幕上两个图像之间的连接,以给出滚动的错觉。这到底是如何工作的,我们将在本章后面对组件进行编码时讨论。
  • BackgroundSpawnComponent以背景必须是的独特方式生成游戏对象。

只剩下两个规范了,然后我们可以对接口和组件类进行编码。

playerlasersspec

这个类是给玩家的激光用的:

import android.graphics.PointF;
class PlayerLaserSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for a player laser
    private static final String tag = "Player Laser";
    private static final String bitmapName = 
    "player_laser";
    private static final float speed = .65f;
    private static final PointF relativeScale = 
          new PointF(8f, 160f);

    private static final String[] components = new String 
    [] {
          "StdGraphicsComponent",
          "LaserMovementComponent", 
          "LaserSpawnComponent"};
    PlayerLaserSpec(){
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

这个类与AlienLaserSpec类相同,只是它有一个绿色图形,一个不同的标签和一个更快的速度。

PlayerSpec

这个类规范有一个额外的组成部分。增加这个PlayerSpec类,我们可以讨论一下:

import android.graphics.PointF;
class PlayerSpec extends ObjectSpec {
    // This is all the unique specifications 
    // for a player
    private static final String tag = "Player";
    private static final String bitmapName = "player_ship";
    private static final float speed = 1f;
    private static final PointF relativeScale = 
          new PointF(15f, 15f);

    private static final String[] components = new String 
    [] {
          "PlayerInputComponent",
          "StdGraphicsComponent",
          "PlayerMovementComponent", 
          "PlayerSpawnComponent"};
    PlayerSpec() {
        super(tag, bitmapName, 
              speed, relativeScale, 
              components);
    }
}

PlayerSpec比其他规格更先进。它有一个共同的地方图形组件,但玩家特定的运动和产卵组件。

但是,请注意,有一种全新类型的组件。PlayerInputComponent将处理屏幕触摸,还将作为观察员注册到GameEngineBroadcaster ( GameEngine)班级。

现在,我们可以对组件接口进行编码。

对组件接口进行编码

每个组件将实现一个接口,这样尽管它们的行为不同,但由于的多态性,它们可以以相同的方式使用。

图形组件

添加到GraphicsComponent界面:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
interface GraphicsComponent {
    void initialize(Context c, 
                    ObjectSpec s, 
                    PointF screensize);

    void draw(Canvas canvas, 
              Paint paint, 
              Transform t);
}

所有的图形组件都需要初始化(位图加载和缩放),并且每帧都要绘制。该界面的initializedraw方法确保这种情况能够发生。这是由特定的图形相关组件类处理的。

请注意每种方法的参数。initialize方法将获得一个Context、一个特定的(派生的)ObjectSpec和屏幕的大小。所有这些东西都将用于设置对象,以便可以绘制。

正如我们所料,draw方法会收到一个Canvas和一个Paint。它还需要一个Transform。正如我们在本章前面提到的,每个游戏对象都有一个Transform实例,它保存着关于它在哪里、有多大以及它朝哪个方向行进的数据。在我们开始编码Transform类之前,我们编码的每个接口都会有一个错误。

输入元件

接下来,对InputComponent界面进行编码:

interface InputComponent {
    void setTransform(Transform t);
}

InputComponent界面只有一个方法——即setTransform方法。考虑到玩家拥有的各种按钮选项, InputComponent 相关类,PlayerInputComponent将在处理屏幕触摸的方式上相当深入。但是InputComponent界面唯一需要方便的是组件可以更新其相关的TransformsetTransform方法传入一个引用,然后组件可以操作标题、位置等等。

移动组件

这是所有运动相关组件类将实现的接口;例如,PlayerMovementComponentLaserMovementComponent,以及所有三个与外星人相关的运动组件。添加MovementComponent界面:

interface MovementComponent {
    boolean move(long fps, 
                 Transform t, 
                 Transform playerTransform);
}

只需要单一的方法;也就是move。看看这些参数,它们非常重要。首先是帧率,这样所有的物体都可以根据帧的时长自行移动。这里没有什么新东西,但是move方法也收到了两个Transform参考。一个是GameObject本身的Transform参照物,一个是玩家的Transform参照物。需要GameObject类本身的Transform引用,这样它就可以根据特定组件的任何逻辑来移动自己。

它之所以还需要玩家的GameObject Transform是因为外星人的大部分移动逻辑都取决于他们相对于玩家的位置。如果你不知道玩家在哪里,你就不能追他们或向他们开枪。

产卵成分

这是组件接口的最后一个。添加SpawnComponent界面:

interface SpawnComponent {
    void spawn(Transform playerTransform, 
               Transform t);
}

只需要一种方法;也就是spawnspawn法还接收具体的GameObject Transform和玩家的Transform。有了这个数据,游戏对象就可以用自己特定的逻辑,沿着跟玩家的位置,来决定在哪里产卵。

如果我们不实现它们,所有这些接口都是无用的。我们现在就开始吧。

对玩家和背景的空组件类进行编码

为每个与玩家相关的组件编写一个空类将允许我们快速编写代码来运行游戏。然后,我们可以在进行过程中充实每个组件的真实/完整代码,而不需要多次进入同一个类(主要是GameObject)。

在本章中,我们将讨论玩家(和他们的激光)和背景。对空轮廓进行编码也将允许我们对一个包含所有这些组件的无错误GameObject类进行编码。通过这样做,在我们对每个组件内部的细节进行编码之前,我们可以看到组件是如何通过GameObject类与游戏引擎进行交互的。

每个组件都将实现我们在上一节中编码的一个接口。我们将为每个类添加足够的代码来履行它对接口的约定义务,因此不会导致任何错误。我们还将在组件类之外进行非常小的更改,以使开发顺利进行,但是当我们到达适当的部分时,我将介绍细节。

我们将在本章稍后对GameObject类进行编码后,将丢失的代码放入。如果你想偷偷看看各种组件方法的细节,你可以在本章后面的完成玩家和背景的组件部分。

标准图形组件

让我们从所有组件类中使用最多的开始;那就是,StdGraphicsComponent。添加新类和该类的起始代码,如下所示:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
class StdGraphicsComponent implements GraphicsComponent {  
private Bitmap mBitmap;
   private Bitmap mBitmapReversed;
    @Override
    public void initialize(Context context, 
                           ObjectSpec spec, 
                           PointF objectSize){

    }
    @Override
    public void draw(Canvas canvas, 
                     Paint paint, 
                     Transform t) {

    }
}

有几条import语句目前没有使用,但在本章结束时将全部使用。该类实现了GraphicsComponent接口,因此它必须为initializedraw方法提供一个实现。这些实现现在是空的,一旦我们编码了GameObjectTransform类,我们将返回到它们。

播放器运动组件

现在,对PlayerMovementComponent类进行编码,该类实现MovementComponent接口:

import android.graphics.PointF;
class PlayerMovementComponent implements MovementComponent {

@Override
     public boolean move(long fps, Transform t,
                         Transform playerTransform){
        return true;
    }
}

务必添加返回true的所需move方法。

我们正在编码的所有组件类最初都将处于类似的半途而废的状态,直到我们在本章稍后重新访问它们。

玩家重生组件

接下来,将编码为近空PlayerSpawnComponent,实现SpawnComponent:

class PlayerSpawnComponent implements SpawnComponent {

    @Override
    public void spawn(Transform playerTransform, Transform 
    t) {

    }
}

添加空spawn方法以避免任何错误。

PlayerInputComponent 和 PlayerLaserSpawner 接口

现在,我们可以将的轮廓编码到PlayerInputComponent和的PlayerLaserSpawn界面。我们将把它们放在一起讨论,因为它们相互关联。

PlayerInputComponent开始,我们将也给这个类增加几个成员变量;然后,我们将讨论它们。创建一个名为PlayerInputComponent的新类并进行编码,如下所示:

import android.graphics.Rect;
import android.view.MotionEvent;
import java.util.ArrayList;
class PlayerInputComponent implements InputComponent,
InputObserver {
    private Transform mTransform;
    private PlayerLaserSpawner mPLS;
    PlayerInputComponent(GameEngine ger) {

    }
    @Override
    public void setTransform(Transform transform) {

    }
     // Required method of InputObserver 
     // interface called from the onTouchEvent method
    @Override
    public void handleInput(MotionEvent event, 
                            GameState gameState, 
                            ArrayList<Rect> buttons) {

    }
}

请注意,该类实现了两个界面–InputComponent和我们的老朋友来自 第 18 章**设计模式介绍等等!InputObserver。该代码实现了两个界面所需的方法,setTransformhandleInput(因此GameEngine可以用玩家的屏幕交互来调用它)。

*还有一个实例成员和另一个成员将作为错误出现,直到我们很快对其进行编码。有一个名为mPLS的成员属于PlayerLaserSpawner类型。

将的思绪拉回到 第十八章**设计模式介绍等等!,当我们对GameStarter界面进行编码时。我们对GameStarter接口进行了编码,这样我们就可以将对它的引用传递到GameState中。然后我们实现了接口,包括GameEngine中的startNewGame方法,从而允许GameState调用GameEngine类中的startNewGame方法。

*我们现在将做一些类似的事情来允许PlayerInputComponent类调用GameEngine类中的一个方法并产生一个激光。

PlayerLaserSpawner界面会有一种方法;也就是spawnPlayerLaser。通过在PlayerInputComponent中有一个PlayerLaserSpawner的实例,我们将能够调用它的方法,并且让GameEngine类在我们需要的时候产生激光。

新建一个类,对PlayerLaserSpawner界面进行编码,如下图:

public interface PlayerLaserSpawner {
        boolean spawnPlayerLaser(Transform transform);
}

切换到GameEngine类并使其实现PlayerLaserSpawner,如下图所示:

class GameEngine extends SurfaceView 
                    implements Runnable, 
                    GameStarter, 
                    GameEngineBroadcaster, 
                    PlayerLaserSpawner {
…

现在,在GameEngine中添加所需的方法spawnPlayerLaser。它现在将是空的:

@Override
public boolean spawnPlayerLaser(Transform transform) {
     return false;
}

现在,我们可以继续下一个组件。

激光运动元件

LaserMovementComponent类进行编码,实现MovementComponent接口的:

import android.graphics.PointF;
class LaserMovementComponent implements MovementComponent {
    @Override
    public boolean move(long fps, 
                        Transform t, 
                        Transform playerTransform) {

        return true;
    }
}

类实现所需的move方法。

激光生成组件

LaserSpawnComponent类进行编码,实现SpawnComponent接口的:

import android.graphics.PointF;
class LaserSpawnComponent implements SpawnComponent {
    @Override
    public void spawn(Transform playerTransform, 
                      Transform t) {

    }
}

该代码包括空但必需的spawn方法。

背景图形元件

BackgroundGraphicsComponent类进行编码,该类实现GraphicsComponent接口:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
class BackgroundGraphicsComponent implements GraphicsComponent {
    private Bitmap mBitmap;
    private Bitmap mBitmapReversed;
    @Override
    public void initialize(Context c,
                           ObjectSpec s,
                           PointF objectSize) {
    }
    @Override
    public void draw(Canvas canvas, 
                     Paint paint, 
                     Transform t) {
    }
}

这里已经实现了GraphicsComponentinitializedraw这两个必需的方法,这里还有两个成员变量,当我们在本章后面对类进行完整编码时,它们就可以使用了。

两个变量名和Matrix类的导入暗示了我们将如何创建滚动背景效果。

BackgroundMovementComponent

BackgroundMovementComponent类进行编码,该类实现MovementComponent接口:

class BackgroundMovementComponent implements MovementComponent {
    @Override
    public boolean move(long fps, 
                        Transform t, 
                        Transform playerTransform) {
        return true;
    }
}

这个代码包含了需要的move方法。

背景生成组件

BackgroundSpawnComponent类进行编码,该类实现SpawnComponent接口:

class BackgroundSpawnComponent implements SpawnComponent {
    @Override
    public void spawn(Transform playerLTransform, Transform 
    t) {

    }
}

请注意,它包含所需的spawn方法。

我们现在已经有了本章结束时要完成的所有组件类的大纲。接下来,我们将对Transform类进行编码。

每个游戏对象都有一个变换

正如我们在本章前面所学的,在实体-组件模式部分,每个GameObject都将有一个Transform类作为成员。Transform类将保存所有数据,并执行所有GameObject实例共有的所有操作,此外还有更多操作。在一个更完整的游戏引擎中,Transform类将会是典型的小类,但是我们将会作弊并且使它变得相当大,以便合并一些典型的不属于Transform的数据和方法。

我们这样做是为了防止代码结构变得更加复杂。在计划这本书的时候,这个项目的结构似乎已经变得更加复杂了。因此,这个类将是我们的组件类不处理的公共事物的一种总括。您会注意到并非所有GameObject实例都需要所有的功能/数据。这不是一个最佳实践,但它将作为构建这个游戏的垫脚石,我们将改进Transform类,使其在下一个也是最后一个项目中更加精致。

通常,一个Transform类只包含大小、位置、方向和标题等数据,以及相关的方法。我们的还将包含一些东西,比如对撞机和一种帮助我们从船的正确部分发射激光的方法。

创建一个名为Transform的新类,并添加以下import语句、成员变量和构造函数:

import android.graphics.PointF;
import android.graphics.RectF;
class Transform {
    // These two members are for scrolling background
    private int mXClip;
    private boolean mReversedFirst = false;
    private RectF mCollider;
    private PointF mLocation;
    private boolean mFacingRight = true;
    private boolean mHeadingUp = false;
    private boolean mHeadingDown = false;
    private boolean mHeadingLeft = false;
    private boolean mHeadingRight = false;
    private float mSpeed;
    private float mObjectHeight;
    private float mObjectWidth;
    private static PointF mScreenSize;
    Transform(float speed, float objectWidth, 
              float objectHeight, 
              PointF startingLocation, 
              PointF screenSize){
        mCollider = new RectF();
        mSpeed = speed;
        mObjectHeight = objectHeight;
        mObjectWidth = objectWidth;
        mLocation = startingLocation;
        mScreenSize = screenSize;
    }
}

让我们重新列出所有这些成员,并快速解释他们将做什么。请注意,所有成员都是private,因此我们需要大量的获取者和设置者来处理我们想要操作的对象:

  • int mXClip:这是一个代表屏幕上两个代表背景的位图相遇的水平位置的值。如果这听起来有点奇怪,那么不要担心——所有这些都将在本章后面的编码滚动背景一节中解释。

  • boolean mReversedFirst:这决定了代表背景的两个位图中哪个先画出来。其中一个位图将是另一个位图的反转版本。通过并排绘制它们,并改变它们在屏幕上相遇的位置,我们可以实现滚动效果。有关详细信息,请参见编码滚动背景部分。

  • RectF mCollider:和我们在其他项目中做的一样,一个RectF实例将代表物体所占据的区域,可以用来进行碰撞检测。

  • PointF mLocation:这是游戏对象左上角的像素位置。它用于移动对象,以及确定在哪里绘制它。

  • boolean mFacingRight: Is the object currently facing to the right. Several decisions in the logic of the component classes depend on which way the game object is facing or heading. Remember that all the movement-related component classes receive a reference to their Transform instance (and the player's) as a parameter in the move method. The following are some more movement/heading-related Booleans:

    boolean mHeadingUp:物体是否向上?

    boolean mHeadingDown:物体是不是在往下走?

    boolean mHeadingLeft:物体航向向左吗?

    boolean mHeadingRight:物体航向对吗?

    float mSpeed:物体行进的速度有多快(朝哪个方向)?

    float mObjectHeight:物体有多高?

    float mObjectWidth:物体有多宽?

  • static PointF mScreenSize:这个变量其实和Transform本身没有什么关系;然而,Transform往往指的是屏幕尺寸,所以保留一份数值是有意义的。请注意mScreenSizestatic,所以它是类的变量,而不是实例,这意味着mScreenSize只有一个副本在Transform的所有实例中共享。

构造器接收大量数据,其中大部分来自规范类,并且它还接收起始位置的PointF实例和屏幕大小的PointF实例(以像素为单位)。接下来,代码初始化我们刚刚讨论过的一些成员变量。

让我们添加一些Transform类的方法。将以下代码添加到Transform类中:

// Here are some helper methods that the background will use
boolean getReversedFirst(){
return mReversedFirst;
}
void flipReversedFirst(){
mReversedFirst = !mReversedFirst;
}
int getXClip(){
return mXClip;
}
void setXClip(int newXClip){
mXClip = newXClip;
}

我们刚才添加的四个方法被GameObject类用来表示滚动背景。它们允许组件类(通过其Transform引用)获取值并更改/设置mXClipmReversedFirst的值。

添加以下简单方法。一定要看一看它们的名称、返回值和它们操作的变量。它将使组件类的编码更容易理解:

PointF getmScreenSize(){
     return mScreenSize;
}
void headUp(){
     mHeadingUp = true;
     mHeadingDown = false;
}
void headDown(){
     mHeadingDown = true;
     mHeadingUp = false;
}
void headRight(){
     mHeadingRight = true;
     mHeadingLeft = false;
     mFacingRight = true;
}
void headLeft(){
     mHeadingLeft = true;
     mHeadingRight = false;
     mFacingRight = false;
}
boolean headingUp(){
     return mHeadingUp;
}
boolean headingDown(){
     return mHeadingDown;
}
boolean headingRight(){
     return mHeadingRight;
}
boolean headingLeft(){
     return mHeadingLeft;
}

我们刚才添加的短方法如下:

  • getmScreenSize:获取一个PointF对象中屏幕的宽度和高度。
  • headUp:操纵方向相关变量,显示物体向上的方向。
  • headDown:操纵与方向相关的变量,显示物体向下的方向。
  • headRight:操纵方向相关变量,向右显示物体航向。
  • headLeft:操纵方向相关变量,向左显示物体航向。
  • headingUp:检查物体当前是否向上。
  • headingDown:检查对象当前是否向下。
  • headingRight:检查对象当前是否向右方向。
  • headingLeft:检查对象当前是否向左方向。

接下来,添加updateCollider方法;然后,我们将讨论它:

void updateCollider(){
      // Pull the borders in a bit (10%)
     mCollider.top = mLocation.y + (mObjectHeight / 10);
     mCollider.left = mLocation.x + (mObjectWidth /10);
     mCollider.bottom = (mCollider.top + mObjectHeight) 
               - mObjectHeight/10;

     mCollider.right = (mCollider.left + mObjectWidth) 
               -  mObjectWidth/10;
}

updateCollider方法使用游戏对象的位置、宽度和高度一个接一个地重新初始化RectF实例的四个值。移动的物体在游戏的每一帧都会调用这个方法。

接下来是要添加到Transform类的另一长串短方法。大多数都是不言自明的,但我们将简要解释每一个,以确保在继续之前理解它们的目的。也许比方法本身更有趣的是我们如何使用它们,当我们在本章后面对组件类进行编码时,这一点将变得显而易见:

float getObjectHeight(){
     return mObjectHeight;
}
void stopVertical(){
     mHeadingDown = false;
     mHeadingUp = false;
}
float getSpeed(){
     return mSpeed;
}
void setLocation(float horizontal, float vertical){
     mLocation = new PointF(horizontal, vertical);
     updateCollider();
}
PointF getLocation() {
     return mLocation;
}
PointF getSize(){
     return new PointF((int)mObjectWidth, 
                       (int)mObjectHeight);
}
void flip(){
     mFacingRight = !mFacingRight;
}
boolean getFacingRight(){
     return mFacingRight;
}
RectF getCollider(){
     return mCollider;
}

以下是您刚刚添加的方法的解释:

  • getObjectHeight:返回对象的高度。
  • stopVertical:操纵成员变量停止上下移动。
  • getSpeed:返回游戏对象的速度。
  • setLocation:取一个PointF作为参数。这包含将游戏对象移动到的位置,并更新mLocation成员变量。
  • getLocation:返回一个包含游戏对象左上角像素位置的PointF
  • getSize:返回一个包含游戏对象宽度和高度的PointF
  • flip:改变/翻转游戏对象的水平朝向。
  • getFacingRight:游戏对象是面向右边的吗?
  • getCollider:返回RectF,作为碰撞器。这被PhysicsEngine类用来检测碰撞。

Transform类的最后一个方法是getFiringLocation方法。在此添加此方法;然后,我将解释它的功能:

PointF getFiringLocation(float laserLength){
     PointF mFiringLocation = new PointF();
     if(mFacingRight) {
          mFiringLocation.x = mLocation.x 
                    + (mObjectWidth / 8f);
     }else
     {
          mFiringLocation.x = mLocation.x 
                    + (mObjectWidth / 8f) - (laserLength);
     }
     // Move the height down a bit of ship height from 
     origin
     mFiringLocation.y = mLocation.y + (mObjectHeight / 
     1.28f);
     return mFiringLocation;
}

这种方法使用激光的长度、船只面对的方向、船只的大小和一些其他值来确定激光的产生点。这样做是必要的,因为如果你只使用mLocation变量,激光会在飞船的左上角产卵,看起来有点傻。这种方法的计算使激光看起来像来自前方(尖点)。

只是重申一下,Transform的一些功能和成员浪费在了我们的一些游戏对象上。比如一个激光不需要翻转也不需要飞起,除了背景之外的游戏物体都不需要水平剪裁,只有部分舰船需要getFiringPosition法。从技术上来说,这是不好的做法,但是我们将细化Transform类,并在下一个项目中使用继承使其更加高效。

最后,我们准备好为备受关注的GameObject类编码。

每个对象都是一个游戏对象

这个类将成为我们各个组成部分的活呼吸(或飞射或潜水)组合。

创建GameObject类,并添加如下所示的import语句和构造函数:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
class GameObject {
    private Transform mTransform;
    private boolean isActive = false;
    private String mTag;
    private GraphicsComponent graphicsComponent;
    private MovementComponent movementComponent;
    private SpawnComponent spawnComponent;
}

在这里,我们可以看到我们有一个名为mTransformTransform类的实例。此外,我们还有一个名为isActiveboolean成员变量。这将作为对象当前是否正在使用的指示器。mTag变量的值将与我们在编码所有特定对象规范部分中编码的规范类的标记相同。

我们声明的最后三个成员是最有趣的,因为它们是我们组件类的实例。请注意,类型被声明为GraphicsComponentMovementComponentSpawnComponent接口类型。因此,不管游戏对象需要什么组件(适合玩家、外星人、背景或其他什么),这三个实例都是合适的。

将这些吸气剂和沉降剂加入GameObject类;然后,我们将讨论它们:

void setSpawner(SpawnComponent s) {
     spawnComponent = s;
}
void setGraphics(GraphicsComponent g, Context c, 
ObjectSpec spec, PointF objectSize) {

     graphicsComponent = g;
     g.initialize(c, spec, objectSize);
}
void setMovement(MovementComponent m) {
     movementComponent = m;
}
void setInput(InputComponent s) {
     s.setTransform(mTransform);
}
void setmTag(String tag) {
     mTag = tag;
}
void setTransform(Transform t) {
     mTransform = t;
}

请注意我们刚刚添加的所有方法都初始化了一个或多个成员变量。setSpawner方法使用作为参数传递的SpawnComponent引用初始化SpawnComponent实例。该实例可以是实现SpawnComponent接口的任何类。

setGraphics方法使用传入的引用(以及一些其他值)初始化GraphicsComponent实例。与SpawnComponent一样,GraphicsComponent可以是实现GraphicsComponent接口的任何类型。

setMovement方法使用在MovementComponent中传递的初始化MovementComponent实例。同样,和前面两个方法一样,如果传入的引用实现了MovementComponent接口,那么代码就会完成它的工作,无论它是AlienDiverMovementComponentAlienChaseMovement…AlienPatrolMovement…PlayerMovement…LaserMovement…还是我们未来梦想的任何其他类型的…Movement…类(也许是一个PinkElephantStampedingMovementComponent)都没有关系。只要正确实现MovementComponent界面,它在我们的游戏中就能正常工作。

setInput方法有点不同,因为它使用传递给它的InputComponent组件,并调用其setTransform方法来传入mTransformInputComponent现在有了一个引用合适的Transform实例。请记住,只有玩家有InputComponent,但如果我们扩展游戏,这可能会改变,这种安排将适应它。GameObject类不需要依赖于对InputComponent的引用;它只是在Transform实例中通过,现在可以忘记它的存在。InputComponent也必须注册成为ObserverGameEngine类(我们很快就会看到);然后,系统就会工作。

setmTag方法初始化mTag变量,setTransform方法接收一个Transform引用来初始化mTransform

此时可能困扰你的问题是,这些…ComponentTransform和标签到底是从哪里来的?什么叫这些方法?答案是工厂。我们将很快编写代码的工厂类将知道什么游戏对象将具有什么组件。它将创建一个新的游戏对象,调用其set…方法,然后将完美组装的GameObject返回到另一个类,该类将保持一个ArrayList充满所有可爱组装和多样的GameObject引用。

这是艰难的一章,但我们正接近享受我们的劳动成果,所以让我们继续前进。

添加这三个关键方法,使用我们的三个关键组件:

void draw(Canvas canvas, Paint paint) {
     graphicsComponent.draw(canvas, paint, mTransform);
}
void update(long fps, Transform playerTransform) {
     if (!(movementComponent.move(fps, 
               mTransform, playerTransform))) {
          // Component returned false`
          isActive = false;
     }
}
boolean spawn(Transform playerTransform) {
     // Only spawnComponent if not already active
     if (!isActive) {
          spawnComponent.spawn(playerTransform, 
          mTransform);
          isActive = true;
          return true;
     }
     return false;
}

draw方法使用GraphicsComponent实例调用其draw方法,update方法使用MovementComponent实例调用其move方法,spawn方法使用SpawnComponent实例调用其spawn方法。游戏对象是什么并不重要(外星人、激光、玩家、背景等等),因为特定的组件会知道如何相应地处理自己。

draw方法会发送常用的CanvasPaint引用,以及对Transform的引用。

move方法获得发送的所需当前对象的Transform,以及玩家的Transform参考。它还检查move方法的返回值,以查看对象是否需要停用。

在组件上调用spawn并将对象设置为激活之前,spawn方法检查对象是否还未激活。

还有四种更简单的 getter 和 setter 方法。将它们添加到GameObject 类;然后,我们可以继续前进:

boolean checkActive() {
     return isActive;
}
String getTag() {
     return mTag;
}
void setInactive() {
     isActive = false;
}
Transform getTransform() {
     return mTransform;
}

我们刚刚添加的四个方法告诉我们对象是否是活动的,它的标签,以及它的Transform实例。

我们现在可以开始添加代码,让我们的各种组件类做一些事情,因为它们还没有做任何事情。

完成玩家和背景的组件

所有的游戏对象都依赖于玩家或者对玩家做出反应。例如,外星人会相对于玩家的位置产卵、追逐和射击。甚至背景也会根据玩家正在做的事情来提示向哪个方向滚动。因此,正如我们前面提到的,让玩家先工作是有意义的。

然而,请记住使用实体-组件模式将意味着我们为玩家编码的一些组件也将在我们实现一些其他游戏对象时使用。

重要说明

如果我们没有在Transform类之前以及随后的GameObject之前对空的组件类进行编码,那么所有这些对Transform类的调用以及这些组件工作的上下文可能会更难理解。

当我们对所有玩家和背景组件进行编码时,我将明确什么是新代码,以及我们在编码玩家和背景的空组件类部分编码了什么。

玩家的组件

请记住尽管这一部分有标题,但其中一些组件也包含与玩家无关的游戏对象。我们只是先编写这些代码,因为让玩家马上工作是有意义的。

完成标准图形组件

我们目前有两个空方法;即initializedraw。让我们将代码添加到他们的身体中,从initialize开始。新代码(方法主体中的所有内容)被突出显示。

将新代码添加到initialize方法中:

@Override
public void initialize(
Context context, 
ObjectSpec spec, 
PointF objectSize){
     // Make a resource id out of the string of the file 
     name
     int resID = context.getResources()
               .getIdentifier(spec.getBitmapName(),
               "drawable", 
               context.getPackageName());
     // Load the bitmap using the id
     mBitmap = BitmapFactory.decodeResource(
               context.getResources(), resID);
     // Resize the bitmap
     mBitmap = Bitmap
               .createScaledBitmap(mBitmap,
                                  (int)objectSize.x,
                                  (int)objectSize.y,
                                  false);
     // Create a mirror image of the bitmap if needed
     Matrix matrix = new Matrix();
     matrix.setScale(-1, 1);
     mBitmapReversed = Bitmap.createBitmap(mBitmap,
                            0, 0,
                            mBitmap.getWidth(),
                            mBitmap.getHeight(),
                            matrix, true);
}

所有的代码我们已经在initialize方法中见过几次了。提醒一下,这就是正在发生的事情:getResources.getIdentifier方法使用位图的名称来识别来自drawable文件夹的图形文件。

该标识符随后被decodeResource方法用于将图形加载到Bitmap对象中。

接下来,使用createScaledBitmap方法将Bitmap对象缩放到游戏对象的正确大小。

最后,用另一个Bitmap创建一个反转版本的Bitmap。现在,我们可以用面向左或右的StdGraphicsComponent实例显示任何GameObject

现在,将以下突出显示的代码添加到StdGraphicsComponent类的draw方法中:

@Override
public void draw(Canvas canvas, 
                 Paint paint, 
                 Transform t) {

     if(t.getFacingRight())
          canvas.drawBitmap(mBitmap,
                            t.getLocation().x,
                            t.getLocation().y,
                            paint);
     else
          canvas.drawBitmap(mBitmapReversed,
                            t.getLocation().x,
                            t.getLocation().y,
                            paint);
}

draw方法中的代码使用Transform类的getFacingRight方法来决定是画Bitmap使其面向右还是面向左。

正在完成播放移动组件

这个类是解决方案的一部分,将使玩家的宇宙飞船复活。move方法中的代码使用Transform实例来确定船只的航向,并相应地移动船只。在几页的时间里,我们还将对PlayerInputComponent类进行编码,该类将根据玩家的屏幕交互操作Transform实例。这是响应这些交互的类。

PlayerMovementComponent类的move方法中添加以下新的高亮代码:

@Override
public boolean move(long fps, Transform t,
                         Transform playerTransform){
     // How high is the screen?
     float screenHeight = t.getmScreenSize().y;
     // Where is the player?
     PointF location = t.getLocation();
     // How fast is it going
     float speed = t.getSpeed();
     // How tall is the ship
     float height = t.getObjectHeight();
     // Move the ship up or down if needed
     if(t.headingDown()){
          location.y += speed / fps;
     }
     else if(t.headingUp()){
          location.y -= speed / fps;
     }
     // Make sure the ship can't go off the screen
     if(location.y > screenHeight - height){
          location.y = screenHeight - height;
     }
     else if(location.y < 0){
          location.y = 0;
     }
     // Update the collider
     t.updateCollider();
     return true;
}

move方法中发生的第一件事是使用 getter 方法从Transform实例初始化一些局部变量。由于变量被使用了不止一次,代码将会比重复调用Transform类的 getters 更加整洁和快速。此时,我们有一个变量来表示屏幕的高度、对象的位置、对象的速度和对象的高度。这些分别由screenHeightlocationspeedheight局部变量表示。

接下来,在move方法中,我们使用一个if语句和一个else if语句来确定船是向上还是向下。又来了:

// Move the ship up or down if needed
if(t.headingDown()){
     location.y += speed / fps;
}
else if(t.headingUp()){
     location.y -= speed / fps;
}

如果是,则通过speed / fps上下移动船舶。下一对ifelse if检查船只是离开屏幕顶部还是底部:

// Make sure the ship can't go off the screen
if(location.y > screenHeight - height){
     location.y = screenHeight - height;
}
else if(location.y < 0){
     location.y = 0;
}

如果它已经离开屏幕,location.y被改变以反映屏幕上船只应该被允许到达的最低(screenHeight-height)或最高(0)点。

注意我说最高和最低的时候,这个稍微有点暧昧。屏幕上的最高点由最低的数字(像素位置零)表示,而屏幕上的最低点是较高的数字。

最后一行代码调用updateCollider方法,这样碰撞器就可以根据船只的新位置进行更新。

正在完成 PlayerSpawnComponent

这是一个非常简单的组件。每当调用GameObject实例的spawn方法时,该代码都会执行。添加以下突出显示的代码:

@Override
public void spawn(Transform playerTransform, Transform t) {
     // Spawn in the centre of the screen
     t.setLocation(
               t.getmScreenSize().x/2, 
               t.getmScreenSize().y/2);
}

我们需要做的就是用setLocation方法把飞船放在屏幕中间。中间是用高度和宽度除以二计算出来的。

正在完成播放输入组件

这门课相当长,但一次学一点不会太复杂。首先,在PlayerInputComponent构造函数和setTransform方法中添加以下代码:

PlayerInputComponent(GameEngine ger) {
     ger.addObserver(this);
     mPLS = ger;
}
@Override
public void setTransform(Transform transform) {
     mTransform = transform;
}

构造函数接收对GameEngine类的引用,使用addObserver方法注册为观察者。现在,这个类将在玩家每次触摸屏幕时接收触摸细节。

setTransform方法中,mTransform是引用GameObjectTransform部分。请记住,GameObject类被工厂类传递了一个InputController引用(GameObjectFactory,我们将很快对其进行编码),并使用该引用来调用该方法。

既然mTransform是对实际Transform的引用,而实际Transform是玩家飞船的GameObject类的一部分,handleInput方法可以用它来操纵它。请记住,我们在handleInput方法中操作Transform实例,并且PlayerMovementComponent对这些操作做出响应。再提醒一下,GameEngine类中的onTouchEvent方法调用handleInput方法,因为PlayerInputComponent类注册为观察者。

handleInput方法中添加以下高亮显示的代码;然后,我们将讨论它。请务必查看传入的参数,因为它们是我们讨论该方法如何工作的关键:

// Required method of InputObserver 
// and is called from the onTouchEvent method
@Override
public void handleInput(MotionEvent event, 
                        GameState gameState, 
                        ArrayList<Rect> buttons) { 
     int i = event.getActionIndex();
     int x = (int) event.getX(i);
     int y = (int) event.getY(i);
     switch (event.getAction() & MotionEvent.ACTION_MASK) {
          case MotionEvent.ACTION_UP:
               if (buttons.get(HUD.UP).contains(x,y)
               || buttons.get(HUD.DOWN).contains(x,y)) {
                    // Player has released either up or 
                    down
                    mTransform.stopVertical();
               }
               break;
          case MotionEvent.ACTION_DOWN:
               if (buttons.get(HUD.UP).contains(x,y)) {
                    // Player has pressed up
                    mTransform.headUp();
               } else if (buttons.get(
               HUD.DOWN).contains(x,y)) {
                    // Player has pressed down
                    mTransform.headDown();
               } else if (buttons.get(
               HUD.FLIP).contains(x,y)) {
                    // Player has released the flip button
                    mTransform.flip();
               } else if (buttons.get(
               HUD.SHOOT).contains(x,y)) {
                    mPLS.spawnPlayerLaser(mTransform);
               }
               break;
          case MotionEvent.ACTION_POINTER_UP:
               if (buttons.get(HUD.UP).contains(x, y)
                    || 
                    buttons.get(HUD.DOWN).contains(x, y)) {
                    // Player has released either up or 
                    down
                    mTransform.stopVertical();
               }
               break;
          case MotionEvent.ACTION_POINTER_DOWN:
               if (buttons.get(HUD.UP).contains(x, y)) {
                    // Player has pressed up
                    mTransform.headUp();
               } else if (buttons.get(
               HUD.DOWN).contains(x, y)) {
                    // Player has pressed down
                    mTransform.headDown();
               } else if (buttons.get(
               HUD.FLIP).contains(x, y)) {
                    // Player has released the flip button
                    mTransform.flip();
               } else if (buttons.get(
               HUD.SHOOT).contains(x, y)) {
                    mPLS.spawnPlayerLaser(mTransform);
               }
               break;
     }
}

让我们将handleInput方法的内部分解成易于管理的块。首先,我们使用getActionIndexgetXgetY方法来确定触发该方法被调用的触摸的坐标。这些值现在存储在xy变量中:

int i = event.getActionIndex();
int x = (int) event.getX(i);
int y = (int) event.getY(i);

现在,我们进入switch块,它决定动作类型。我们处理四个case语句。这是新的。之前我们只处理了两个案件:ACTION_UPACTION_DOWN。不同的是,多个手指可以同时互动。让我们看看我们如何处理这个问题,以及四个case声明是什么:

switch (event.getAction() & MotionEvent.ACTION_MASK) {
}

第一种说法并不新鲜。ACTION_UP被处理,我们只对被释放的上下按钮感兴趣。如果松开上升或下降按钮,则调用stopVertical方法,下一次调用PlayerMovementComponent的移动方法时,船不会上升或下降:

case MotionEvent.ACTION_UP:
     if (buttons.get(HUD.UP).contains(x,y)
     || buttons.get(HUD.DOWN).contains(x,y)) {
          // Player has released either up or down
          mTransform.stopVertical();
     }
     break;

接下来我们来处理ACTION_DOWN,这个案例稍微宽泛一些。我们需要控制所有的船只。每个if - else块处理在特定按钮内计算xy时发生的情况。仔细看看下面的代码:

case MotionEvent.ACTION_DOWN:
     if (buttons.get(HUD.UP).contains(x,y)) {
          // Player has pressed up
          mTransform.headUp();
     } else if (buttons.get(HUD.DOWN).contains(x,y)) {
          // Player has pressed down
          mTransform.headDown();
     } else if (buttons.get(HUD.FLIP).contains(x,y)) {
          // Player has released the flip button
          mTransform.flip();
     } else if (buttons.get(HUD.SHOOT).contains(x,y)) {
          mPLS.spawnPlayerLaser(mTransform);
     }
     break;

按下 up 时,调用headUp方法。按下时,调用headDown方法。按下翻转时调用flip方法,按下拍摄时使用mPLS调用GameEngine类上的spawnPlayerLaser方法。

如果你看一下接下来的两个case语句,它们紧挨着出现,看起来会很熟悉。事实上,除了每个case的第一行代码之外,case语句的代码与前面两个case语句相同。

你会注意到我们现在响应的不是ACTION_UPACTION_DOWN,而是ACTION_POINTER_UPACTION_POINTER_DOWN。对此的解释很简单。如果第一根手指接触屏幕导致动作被触发,则它被MotionEvent物体作为ACTION_UPACTION_DOWN握住。如果是第二个、第三个、第四个等等,那么就保持为ACTION_POINTER_UP或者ACTION_POINTER_DOWN。这在我们早期的任何一款游戏中都无关紧要,我们已经能够避免额外的代码:

case MotionEvent.ACTION_POINTER_UP:
     if (buttons.get(HUD.UP).contains(x, y)
          || 
          buttons.get(HUD.DOWN).contains(x, y)) {
          // Player has released either up or down
          mTransform.stopVertical();
     }
     break;
case MotionEvent.ACTION_POINTER_DOWN:
     if (buttons.get(HUD.UP).contains(x, y)) {
          // Player has pressed up
          mTransform.headUp();
     } else if (buttons.get(HUD.DOWN).contains(x, y)) {
          // Player has pressed down
          mTransform.headDown();
     } else if (buttons.get(HUD.FLIP).contains(x, y)) {
          // Player has released the flip button
          mTransform.flip();
     } else if (buttons.get(HUD.SHOOT).contains(x, y)) {
          mPLS.spawnPlayerLaser(mTransform);
     }
     break;

在我们的游戏中,不管是不是POINTER都没有关系,只要我们对所有的按压和释放做出反应。玩家可以交叉双臂玩游戏——这对滚动射击游戏没有任何影响。

然而,如果您检测到更复杂的手势,如捏、缩放或一些自定义触摸,那么顺序——甚至是触摸屏幕时的时间和移动——可能很重要。MotionEvent类可以处理所有这些情况,但在本书中我们不需要这样做。

让我们把注意力转向激光。

正在完成激光移动组件

我们已经对PlayerLaserSpawner接口进行了编码,通过GameEngine类实现了它,对PlayerInputComponent进行了编码,使它接收到一个PlayerLaserSpawner实例,然后在玩家按下屏幕拍摄按钮时调用spawnPlayerLaser方法(在GameEngine上)。此外,我们还在Transform类(getFiringLocation)中编写了一个辅助方法,根据玩家船只的位置和方向,计算出一个美观的位置来产生激光。

为了实现所有这些,我们需要对激光器本身的组件类别进行编码。将以下高亮显示的代码添加到LaserMovementComponent类的move方法中:

@Override
public boolean move(long fps, 
                    Transform t, 
                    Transform playerTransform) {
     // Laser can only travel two screen widths
     float range = t.getmScreenSize().x * 2;
     // Where is the laser
     PointF location = t.getLocation();
     // How fast is it going
     float speed = t.getSpeed();
     if(t.headingRight()){
          location.x += speed / fps;
     }
     else if(t.headingLeft()){
          location.x -= speed / fps;
     }
     // Has the laser gone out of range
     if(location.x < - range || location.x > range){
          // disable the laser
          return false;
     }
     t.updateCollider();

     return true;
}

move方法中的新代码初始化三个局部变量:rangelocationspeed。它们使用激光器的Transform参考进行初始化。他们的名字不言自明,也许除了rangerange变量由屏幕宽度(t.getmScreensize.x)乘以 2 初始化。我们将使用该值来监控何时关闭激光器。

接下来,在move方法中,我们可以看到一些与PlayerMovementComponent类非常相似的代码。有一个if和一个else - if模块检测激光向哪个方向前进(t.headingRightt.headingLeft)。在ifelse - if模块内,使用speed / fps沿适当方向水平移动激光器。

下一个if程序块使用这里显示的公式检查是否到了关闭激光器的时间:

if(location.x < - range || location.x > range){

if语句检测屏幕宽度是否在左侧或右侧超过了两倍。如果有,则move方法返回假。回想一下我们在GameObject类的update方法中调用move方法的时候——当move方法返回false时,GameObjectmIsActive成员被设置为false。在这个GameObject上将不再调用move方法。

move方法的最后一行代码使用updateCollider方法将激光对撞机更新到新位置。

正在完成激光生成组件

激光的最后一位代码是LaserSpawnComponentspawn方法。如果你是想知道激光将如何绘制自己,请参考PlayerLaserSpec课;你会看到它使用了StdGraphicsComponent,我们已经对其进行了编码。

spawn方法中添加以下新的高亮代码:

@Override
public void spawn(Transform playerTransform, 
                         Transform t) {
     PointF startPosition = 
               playerTransform.getFiringLocation(
               t.getSize().x);
     t.setLocation((int)startPosition.x,
     (int)startPosition.y);
     if(playerTransform.getFacingRight()){
        t.headRight();
     }
      else{
          t.headLeft();
     }
}

新代码做的第一件事是通过在玩家的Transform引用上调用getFiringLocation方法来初始化一个名为startPositionPointF。另外,请注意,激光的大小被传递给getFiringLocation方法,这是该方法进行计算所需的。

接下来,在激光器的Transform参考上调用setLocation方法,并且startPosition现在持有的水平和垂直值被用作参数。

最后,在if - else语句中使用玩家的航向来决定激光航向的设置方式。如果玩家面朝右,激光也将朝右(反之亦然)是有道理的。

在这一点上,激光已经准备好被牵引和移动。

对滚动背景进行编码

游戏的第一帧显示的是背景图,如下图。这没有受到任何操纵:

Figure 20.2 – Background image of the game

图 20.2–游戏的背景图像

下一帧的显示方式是将图像移出屏幕向左。那么,我们在屏幕右侧最后一个像素宽的垂直列上显示什么呢?我们将制作同一图像的反转副本,并将其显示在原始(未反转)图像的右侧。下图显示了两个图像之间的间隙,以清楚地表明存在连接以及两个图像的位置,但实际上,我们将图像放在彼此旁边,这样连接就不可见了:

Figure 20.3 – Joining two images

图 20.3–连接两幅图像

随着原始图像和反转图像稳定地向左滚动,最终将显示每个图像的一半,以此类推:

Figure 20.4 – Screen after joining the images

图 20.4–连接图像后的屏幕

最终,我们将到达原始图像的末尾,反转图像右侧的最后一个像素将最终显示在屏幕上。

此时,当反转的图像在屏幕上完整显示时,就像原始图像在开始时一样,我们将原始图像移到右侧。这两个背景将连续滚动,当右手图像(原始或反转)成为玩家看到的整个视图时,左手图像(原始或反转)将移动到右手侧,准备滚动到视图中。

请注意,当向相反方向滚动时,整个过程是相反的!

在下一个项目中,我们还将在滚动背景前绘制平台和其他场景,从而创建一个整洁的视差效果。

现在我们知道了滚动背景需要实现什么,我们可以开始编码三个与背景相关的组件类。

正在完成 BackgroundGraphicsComponent 组件

让我们从BackgroundGraphicsComponent课开始。我们必须编码的第一个方法是initialize方法。将以下突出显示的代码添加到initialize方法中:

@Override
public void initialize(Context c, 
                       ObjectSpec s, 
                       PointF objectSize) {
     // Make a resource id out of the string of the file 
     name
     int resID = c.getResources()
          .getIdentifier(s.getBitmapName(),
          "drawable", c.getPackageName());
     // Load the bitmap using the id
     mBitmap = BitmapFactory
          .decodeResource(c.getResources(), resID);
     // Resize the bitmap
     mBitmap = Bitmap
          .createScaledBitmap(mBitmap,
                             (int)objectSize.x,
                             (int)objectSize.y,
                             false);
     // Create a mirror image of the bitmap
     Matrix matrix = new Matrix();
     matrix.setScale(-1, 1);
     mBitmapReversed = Bitmap
               .createBitmap(mBitmap,
               0, 0,
               mBitmap.getWidth(),
               mBitmap.getHeight(),
               matrix, true);
}

前面的代码是我们还没有看到的。使用位图的名称选择资源,使用decodeResource方法加载资源,使用createScaledBitmap方法缩放资源,然后结合使用Matrix类和createBitmap方法创建图像的反转版本。我们现在有两个Bitmap对象(mBitmapmBitmapReversed)准备进行绘图。

现在,我们可以对draw方法进行编码,在游戏的每一帧都会调用这个方法来绘制背景。在draw方法中添加以下新的高亮代码;然后,我们可以讨论它:

@Override
public void draw(Canvas canvas, 
                 Paint paint, 
                 Transform t) {
   int xClip = t.getXClip();
   int width = mBitmap.getWidth();
   int height = mBitmap.getHeight();
   int startY = 0;
   int endY = (int)t.getmScreenSize().y +20;
     // For the regular bitmap
     Rect fromRect1 = new Rect(0, 0, width - xClip, 
     height);
     Rect toRect1 = new Rect(xClip, startY, width, endY);
     // For the reversed background
     Rect fromRect2 = new Rect(width - xClip, 0, width, 
     height);
     Rect toRect2 = new Rect(0, startY, xClip, endY);
     //draw the two background bitmaps
     if (!t.getReversedFirst()) {
          canvas.drawBitmap(mBitmap, 
                            fromRect1, toRect1, paint);

          canvas.drawBitmap(mBitmapReversed, 
                            fromRect2, toRect2, paint);
     } else {
          canvas.drawBitmap(mBitmap, fromRect2, 
                            toRect2, paint);

          canvas.drawBitmap(mBitmapReversed, 
                            fromRect1, toRect1, paint);
     }
}

我们在新的draw方法代码中做的第一件事情是声明一些局部变量,帮助我们在正确的地方绘制两幅图像。

通过使用Transform引用调用getXclip来初始化xClip变量。xClip的值是决定图像中连接位置的关键。Transform持有这个值,它在BackgroundMovementComponent中被操纵,我们接下来将对其进行编码。

width变量从Bitmap缩放到的宽度开始初始化。

height变量从Bitmap缩放到的高度开始初始化。

startY变量是我们想要开始绘制图像的垂直点。这很简单——在屏幕顶部——零。它被相应地初始化。

endY变量位于屏幕底部,初始化为屏幕高度,加上 20 个像素,确保没有小故障。

接下来,我们初始化四个Rect对象。当我们将位图绘制到屏幕上时,我们将需要为每个Bitmap创建两个Rect对象:一个从确定要绘制的位图部分*,另一个确定要绘制的屏幕区域。因此,我们将Rect对象命名为fromRect1toRect1fromRect2toRect2。再看看这四行代码:*

// For the regular bitmap
Rect fromRect1 = new Rect(0, 0, width - xClip, height);
Rect toRect1 = new Rect(xClip, startY, width, endY);
// For the reversed background
Rect fromRect2 = new Rect(width - xClip, 0, width, height);
Rect toRect2 = new Rect(0, startY, xClip, endY);

首先,请注意,出于解释的目的,我们可以忽略四个Rect对象的所有垂直值。垂直值是第二个和第四个参数,它们总是startYendY

可以看到fromRect1总是从零开始水平延伸到全宽,少了不管xClip的值是多少。

跳到fromRect2,我们可以看到它总是从全幅开始,少了xClip,延伸到全幅。

试着在脑海中想象一下xClip值增加时,第一个图像会水平缩小,第二个图像会变大。随着xClip值的降低,情况正好相反。

现在,把你的注意力转向toRect1。我们可以看到图像是从xClip画到屏幕上的,不管width是什么。现在,看看toRect2,看到它是从宽度,少了xClip,到无论width是什么。这些值的作用是根据图像当前的宽度将图像精确地并排放置,并确保这些宽度正好覆盖屏幕的整个宽度。

作者考虑了解释如何计算这些Rect值的不同方法,并建议为了绝对清楚地说明这是如何工作的,读者应该使用铅笔和纸来计算和绘制xClip不同值的矩形。一旦你完成了背景相关组件的编码,看看xClip是如何操作的,这个练习将会非常有用。

代码的最后一部分使用Transform参考来确定应该在左侧(首先)绘制哪个图像,然后使用drawBitmap和四个先前计算的Rect对象绘制两个图像。

这是的重载版本drawBitmap,它采用要绘制的Bitmap属性、要绘制的图像的一部分(fromRect1fromRect2)以及屏幕目的地坐标(toRect1toRect2)。

正在完成 BackgroundMovementComponent 组件

接下来,我们将移动背景。这主要是通过增加和减少Transform类中的mXClip来实现的,但也可以通过切换图像的绘制顺序来实现。将以下突出显示的代码添加到BackgroundMovementComponent类的move方法中:

@Override
public boolean move(long fps, 
                    Transform t, 
                    Transform playerTransform) {
     int currentXClip = t.getXClip();
     if(playerTransform.getFacingRight()) {
          currentXClip -= t.getSpeed() / fps;
          t.setXClip(currentXClip);
     }
     else {
          currentXClip += t.getSpeed() / fps;
          t.setXClip(currentXClip);
     }
     if (currentXClip >= t.getSize().x) {
          t.setXClip(0);
          t.flipReversedFirst();
     } 
     else if (currentXClip <= 0) {
          t.setXClip((int)t.getSize().x);
          t.flipReversedFirst();
     }
     return true;
}

代码做的第一件事是从Transform引用中获取裁剪/连接位置的当前值,并将其存储在currentXClip局部变量中。

第一个if - else区块测试玩家是面向左还是面向右。如果玩家正面对着右边,currentXClip会被背景速度降低,除以当前帧率。如果玩家面向左侧,currentXClip将被背景速度增加,除以当前帧率。在这两种情况下,setXClip方法用于更新Transform中的mXClip

代码中下一个是if - else - if区块。这测试currentXClip是大于背景宽度还是小于零。如果currentXClip大于背景的宽度,则setXClip用于将其设置回零,图像的顺序用flipReversedFirst翻转。如果currentXClip小于或等于零,则setXClip用于将其设置为背景宽度,图像的顺序用flipReversedFirst翻转。

方法总是返回true,因为我们从来不想去激活背景。

现在,我们只需要生成背景。然后,我们可以开始在工厂工作,该工厂将把所有这些组件组合并构造成GameObject实例。

正在完成 BackgroundSpawnComponent 类

BackgroundSpawnComponentspawn方法中添加以下高亮显示的代码:

@Override
public void spawn(Transform playerLTransform, Transform t) {
     // Place the background in the top left corner
     t.setLocation(0f,0f);
}

一行代码将背景的位置设置在屏幕的左上角。

游戏对象/组件真实性检查

但是等一下!我们还没有实例化一个游戏对象。为此,我们还需要两个班;这将完成我们的游戏对象生产线。第一个是工厂本身,而下一个是Level类,你可以编辑这个类来决定游戏是什么样子的(敌人有多少和哪种类型)。你甚至可以扩展它来制作一个有多个不同关卡的游戏。

一旦这两个职业完成,将很容易在游戏中添加许多不同的外星人。设计并添加你自己的…Spec类,编写它们的组件类,并将其添加到游戏中,这也是微不足道的。

构建游戏对象工厂类

使用基于对象规范的类来组装具有正确组件的GameObject实例是GameObjectFactory类的工作。

创建一个名为GameObjectFactory的新类,并添加以下成员和构造函数:

import android.content.Context;
import android.graphics.PointF;
class GameObjectFactory {
    private Context mContext;
    private PointF mScreenSize;
    private GameEngine mGameEngineReference;
    GameObjectFactory(Context c, PointF screenSize, 
                      GameEngine gameEngine) {

        this.mContext = c;
        this.mScreenSize = screenSize;
        mGameEngineReference = gameEngine;
    }
}

这里,我们有一个Context对象,一个PointF对象保存屏幕分辨率,一个GameEngine对象保存对GameEngine类的引用。所有这些成员变量都在构造函数中初始化。正是Level类将创建并使用对该类的引用。正是GameEngine类将创建Level类的实例,并为Level类提供必要的引用来调用这个GameObjectFactory构造函数。

接下来,添加create方法。这将完成创建GameObject实例的所有艰苦工作。我们将很快向该方法添加更多代码:

GameObject create(ObjectSpec spec) {
        GameObject object = new GameObject();
        int numComponents = spec.getComponents().length;
        final float HIDDEN = -2000f;
        object.setmTag(spec.getTag());
        // Configure the speed relative to the screen size
        float speed = mScreenSize.x / spec.getSpeed();
        // Configure the object size relative to screen 
        size
        PointF objectSize = 
                new PointF(mScreenSize.x / 
                spec.getScale().x,
                mScreenSize.y / spec.getScale().y);
        // Set the location to somewhere off-screen
        PointF location = new PointF(HIDDEN, HIDDEN);
        object.setTransform(new Transform(speed, 
                            objectSize.x, 
                            objectSize.y, location,
                            mScreenSize));

          // More code here next...
}

首先,查看在create方法上的签名,注意它接收到一个ObjectSpec引用。请记住ObjectSpec是抽象的,不能实例化,所以这意味着这必须是对扩展ObjectSpec的类的引用——玩家、背景、外星人或激光。最后,我们可以使用以下代码创建一个新实例:

GameObject object = new GameObject();

接下来,我们将计算出规范中有多少组件包含在这一行代码中:

int numComponents = spec.getComponents().length;

我们可以创建一个很快就会有用的名为HIDDENint,并用下面的代码将其初始化为-2000:

final float HIDDEN = -2000f;

现在,我们可以使用setTag方法将规范中的标签存储在GameObject实例中,如下所示:

object.setmTag(spec.getTag());

如果在这个方法中我的解释看起来有点费力,那么不要担心——这是故意的。这个类,特别是create方法,是这个庞大章节中所有工作的汇集之处。正是在这里,TransformObjectSpec子类、GameObject和众多组件类最终相互作用,创造出有意义的“东西”,在游戏中真正有所作为,我想确保你不会错过任何一个技巧。实体-组件模式与简单工厂模式的融合是构建您自己的深度游戏的关键,而不会被包含数千行杂乱无章、难以管理的代码的类所困扰。

下面几行代码根据屏幕宽度和我们当前构建的规范中的速度来声明和初始化speed变量:

// Configure the speed relative to the screen size
float speed = mScreenSize.x / spec.getSpeed();

下面几行代码根据屏幕的宽度和我们当前构建的规范的大小,声明并初始化一个名为objectSizePointF:

// Configure the object size relative to screen size
PointF objectSize = 
new PointF(mScreenSize.x / spec.getScale().x,
           mScreenSize.y / spec.getScale().y);

现在,我们将创建另一个名为locationPointF,并使用HIDDEN变量将其初始化为-2000-2000:

// Set the location to somewhere off-screen
PointF location = new PointF(HIDDEN, HIDDEN);

create方法中的最后一段代码(到目前为止)通过调用我们新的GameObject上的setTransform变量来放置我们刚刚初始化使用的所有变量。以下是我所指的代码行:

object.setTransform(new Transform(speed, objectSize.x, 
objectSize.y, location, mScreenSize));

// More code here next...

GameObject类现在有一个完全初始化的Transform。现在,该是组件的时候了。

create方法的右花括号内添加以下代码。请注意代码顶部突出显示的注释,它指示了新代码相对于我们在上一步中添加的代码的位置:

// More code here next...
// Loop through and add/initialize all the components
for (int i = 0; i < numComponents; i++) {
     switch (spec.getComponents()[i]) {
          case "PlayerInputComponent":
               object.setInput(new PlayerInputComponent
                              (mGameEngineReference));
               break;
          case "StdGraphicsComponent":
               object.setGraphics(new 
               StdGraphicsComponent(), 
                         mContext, spec, objectSize);
               break;
          case "PlayerMovementComponent":
               object.setMovement(new 
               PlayerMovementComponent());
               break;
          case "LaserMovementComponent":
               object.setMovement(new 
               LaserMovementComponent());
               break;
          case "PlayerSpawnComponent":
               object.setSpawner(new 
               PlayerSpawnComponent());
               break;
          case "LaserSpawnComponent":
               object.setSpawner(new 
               LaserSpawnComponent());
               break;
          case "BackgroundGraphicsComponent":
               object.setGraphics(new 
               BackgroundGraphicsComponent(),
                    mContext, spec, objectSize);
               break;
          case "BackgroundMovementComponent":
               object.setMovement(new 
               BackgroundMovementComponent());
               break;
          case "BackgroundSpawnComponent":
               object.setSpawner(new 
               BackgroundSpawnComponent());
               break;
          default:
               // Error unidentified component
               break;
     }
}
// Return the completed GameObject to the Level class
return object;

让我们更仔细地看看那些forswitch条件。他们又来了:

for (int i = 0; i < mNumComponents; i++) {
     switch (spec.getComponents()[i]) {
…

这段代码将遍历我们正在构建的当前规范中的组件数组中的每个组件。请记住,不是所有的规格都有相同数量的组件,但是由于numComponents等于数组的长度,for循环会处理这个问题。

switch条件根据组件的名称进行切换。如果我们编写一个case来匹配我们想要使用的每种类型的组件,那么我们将处理它们。我们添加的case语句只处理我们已经编码的组件。一旦我们编写了更多的组件类,我们将在下一章中添加更多的case语句。让我们看看每一个case的陈述。

以下是前三个:

case "PlayerInputComponent":
object.setInput(new PlayerInputComponent
               (mGameEngineReference));
     break;
case "StdGraphicsComponent":
object.setGraphics(new StdGraphicsComponent(), 
                   mContext, spec, objectSize);
break;
case "PlayerMovementComponent":
object.setMovement(new PlayerMovementComponent());
     break;

检测到PlayerInputComponent时,调用GameObjectsetInput方法。在该方法中,传递了一个新的PlayerInputComponent引用,在PlayerInputComponent构造函数中,传递了GameEngine引用。这有两个作用。首先PlayerInputComponent使用GameEngine引用来调用addObserver,其次GameObject实例可以使用PlayerInputComponent引用来调用setTransform方法,并传递PlayerInputComponent需要的Transform引用。

下一个case通过调用setGraphics方法创建一个新的StdGraphicsComponentGameObject实例通过存储引用并调用initialize方法来准备要绘制的对象。

接下来,通过调用setMovement并传入新的PlayerMovementComponent引用来创建PlayerMovementComponent

LaserMovementComponent出现时,下一个case语句执行。case调用setMovement的方式与调用PlayerMovementComponent的方式完全相同,只是LaserMovementComponent被创建并传递给GameObject而不是PlayerMovementComponent:

case "LaserMovementComponent":
object.setMovement(new LaserMovementComponent());
     break;

接下来的两个case语句处理激光和玩家产卵组件。所有这些case语句需要做的就是调用setSpawner并传入适当的种子相关类。下面这两个cases再次供参考:

case "PlayerSpawnComponent":
          object.setSpawner(new PlayerSpawnComponent());
          break;
case "LaserSpawnComponent":
     object.setSpawner(new LaserSpawnComponent());
     break;

接下来的三个case语句处理所有与背景相关的组件。它们的创建方式与其他图形、移动和衍生相关组件完全相同,只是创建了用于创建背景的适当的新组件类,然后将其传递给GameObject实例:

case "BackgroundGraphicsComponent":
object.setGraphics(new BackgroundGraphicsComponent(),
          mContext, spec, objectSize);
     break;
case "BackgroundMovementComponent":
object.setMovement(new BackgroundMovementComponent());
     break;
case "BackgroundSpawnComponent":
object.setSpawner(new BackgroundSpawnComponent());
     break;

最后一个case可能会添加一些错误处理,以向控制台输出一条消息:

default:
     // Error unidentified component
     break;

最后一行代码返回对工厂刚刚构建的GameObject实例的引用:

// Return the completed GameObject to the Level class
return object;

现在,我们将看到我们在哪里调用create方法,以及我们在哪里存储create方法返回的所有GameObject引用。

对级别类进行编码

Level类是你设计关卡的地方。如果你想要更多特定类型的敌人或者更少的激光来降低射速,那么这就是你应该做的。在你计划发布的一个游戏中,你可能会extend Level用不同的敌人、数量和背景设计多个实例。对于这个项目,我们将只坚持一个刚性级别,但在下一个项目中,我们将进一步推进级别设计理念。

创建一个名为Level的类,并添加以下所有成员和import语句:

import android.content.Context;
import android.graphics.PointF;
import java.util.ArrayList;
class Level {
    // Keep track of specific types
    public static final int BACKGROUND_INDEX = 0;
    public static final int PLAYER_INDEX = 1;
    public static final int FIRST_PLAYER_LASER = 2;
    public static final int LAST_PLAYER_LASER = 4;
    public static int mNextPlayerLaser;
    public static final int FIRST_ALIEN = 5;
    public static final int SECOND_ALIEN = 6;
    public static final int THIRD_ALIEN = 7;
    public static final int FOURTH_ALIEN = 8;
    public static final int FIFTH_ALIEN = 9;
    public static final int SIXTH_ALIEN = 10;
    public static final int LAST_ALIEN = 10;
    public static final int FIRST_ALIEN_LASER = 11;
    public static final int LAST_ALIEN_LASER = 15;
    public static int mNextAlienLaser;
    // This will hold all the instances of GameObject
    private ArrayList<GameObject> objects;
}

我们刚刚编码的大部分变量是publicstaticfinal。它们将被用来追踪我们产生了多少外星人和激光,以及追踪我们的GameObject ArrayList中某些物体的存储位置。因为它们是publicstaticfinal,所以它们可以很容易地被引用,但不能从任何类中修改。

有两个变量不是final。这是mNextPlayerLasermNextAlienLaser。我们将使用这些来循环我们产生的激光,以确定下一个拍摄哪一个。

前面代码中的最后一个声明是一个名为objectsGameObject实例的ArrayList。这是我们将隐藏GameObjectFactory类为我们组装的所有实例的地方。

接下来,添加构造函数方法:

public Level(Context context, 
             PointF mScreenSize, 
             GameEngine ge){

     objects = new ArrayList<>();
     GameObjectFactory factory = new GameObjectFactory(
               context, mScreenSize, ge);

     buildGameObjects(factory);
}

ArrayList在构造函数内部初始化。接下来,创建GameObjectFactory类的新实例。最后一行代码调用buildGameObjects方法,并传入这个名为factory的新GameObjectFactory实例。

现在,对buildGameObjects方法进行编码:

ArrayList<GameObject> buildGameObjects(
          GameObjectFactory factory){

     objects.clear();
     objects.add(BACKGROUND_INDEX, factory
          .create(new BackgroundSpec()));

     objects.add(PLAYER_INDEX, factory
          .create(new PlayerSpec()));
     // Spawn the player's lasers
     for (int i = FIRST_PLAYER_LASER; 
          i != LAST_PLAYER_LASER + 1; i++) {

          objects.add(i, factory
               .create(new PlayerLaserSpec()));
     }
     mNextPlayerLaser = FIRST_PLAYER_LASER;
     // Create some aliens
     // Create some alien lasers
     return objects;
}
ArrayList<GameObject> getGameObjects(){
     return objects;
}

首先,清除ArrayList,以防这不是第一次调用该方法。我们不希望更新和绘制两个或更多级别的对象。

我们再来看看下面一行代码;如果我们能理解它,我们就能理解所有GameObject实例是如何创建的:

objects.add(BACKGROUND_INDEX, factory
          .create(new BackgroundSpec()));

代码以objects.add开头,用于给ArrayList添加新的引用。第一个参数是BACKGROUND_INDEX并指出ArrayList中我们希望背景走向的位置。

传递给add方法的第二个参数需要是对GameObject的引用。就是这样,但是有点复杂。这是代码:

factory.create(new BackgroundSpec())

这将调用GameObjectFactory实例上的create方法,您可能还记得该方法返回所需类型的GameObject。所以,在这个阶段,有一个适当构建的背景以GameObject的形式等待更新和绘制。

接下来,我们以同样的方式添加一个玩家,然后我们从FIRST_PLAYER_LASERLAST_PLAYER_LASER循环一个for循环,然后添加一束激光让玩家拍摄。

mNextPlayerLaser变量被初始化为FIRST_PLAYER_LASER的值,准备好让GameEngine类在需要时生成它。

最后一种方法是getGameObjects法。它返回对objects数组列表的引用。这就是我们如何与其他需要的班级分享objects

把所有东西放在一起

我们只需要解决一些小问题,这样我们就可以运行游戏了。

更新游戏引擎

GameEngine添加一个Level类的实例:

...
HUD mHUD;
Renderer mRenderer;
ParticleSystem mParticleSystem;
PhysicsEngine mPhysicsEngine;
Level mLevel;

GameEngine构造函数中初始化Level的实例:

public GameEngine(Context context, Point size) {
     super(context);
     mUIController = new UIController(this, size);
     mGameState = new GameState(this, context);
     mSoundEngine = new SoundEngine(context);
     mHUD = new HUD(size);
     mRenderer = new Renderer(this);
     mPhysicsEngine = new PhysicsEngine();
     mParticleSystem = new ParticleSystem();
     mParticleSystem.init(1000);
     mLevel = new Level(context,
               new PointF(size.x, size.y), this);
}

现在,我们可以为我们在 第 18 章**设计模式介绍中为签名编码的deSpawnRespawn方法添加一些代码,还有更多!。记住这个方法是GameState类在新游戏需要开始的时候调用的。添加以下突出显示的代码:

public void deSpawnReSpawn() {
// Eventually this will despawn
// and then respawn all the game objects
     ArrayList<GameObject> objects = 
     mLevel.getGameObjects();
     for(GameObject o : objects){
          o.setInactive();
     }
     objects.get(Level.PLAYER_INDEX)
          .spawn(objects.get(Level.PLAYER_INDEX)
          .getTransform());

     objects.get(Level.BACKGROUND_INDEX)
          .spawn(objects.get(Level.PLAYER_INDEX)
          .getTransform());

}

前面的代码创建了一个名为objects的新ArrayList,并通过在Level实例上调用getGameObjects方法来初始化它。一个for循环遍历每个GameObject实例,并将它们设置为非活动状态。

接下来我们在播放器上调用spawn方法,然后在后台调用spawn方法。我们使用来自Level类的public static final变量来确保我们使用的是正确的指数。

现在,我们可以对我们添加的用于产生玩家激光的方法的主体进行编码:

public boolean spawnPlayerLaser(Transform transform) {
ArrayList<GameObject> objects = 
     mLevel.getGameObjects();

     if (objects.get(Level.mNextPlayerLaser)
          .spawn(transform)) {

          Level.mNextPlayerLaser++;
          mSoundEngine.playShoot();
if (Level.mNextPlayerLaser == 
               Level.LAST_PLAYER_LASER + 1) {

               // Just used the last laser
Level.mNextPlayerLaser = 
               Level.FIRST_PLAYER_LASER;
          }
     }
     return true;
}

spawnPlayerLaser方法通过调用getGameObjects创建并初始化对我们的GameObject实例的ArrayList的引用。

代码然后试图从Level.mNextPlayerLaser索引处的物体中产生激光。如果成功,Level.mNextPlayerLaser递增,准备下一个镜头,并使用SoundEngine类播放声音效果。

最后,还有一个嵌套的if语句,用于检查是否使用了LAST_PLAYER_LASER,如果使用了,则将mNextPlayerLaser设置回FIRST_PLAYER_LASER

现在,更新run方法,从Level类获取所有游戏对象。然后,把它们传到PhysicsEngine班,再传到Renderer班。此处突出显示了新的和更改的线条:

@Override
public void run() {
     while (mGameState.getThreadRunning()) {
          long frameStartTime = System.currentTimeMillis();
ArrayList<GameObject> objects = 
          mLevel.getGameObjects();

          if (!mGameState.getPaused()) {
               // Update all the game objects here
               // in a new way
               // This call to update will evolve 
               // with the project
if(mPhysicsEngine.update(mFPS,objects, 
mGameState, 
                    mSoundEngine, mParticleSystem)){

                    // Player hit
                    deSpawnReSpawn();
               }
          }
          // Draw all the game objects here
          // in a new way
mRenderer.draw(objects, mGameState, mHUD, 
                         mParticleSystem);
          // Measure the frames per second in the usual way
          long timeThisFrame = System.currentTimeMillis()
                    - frameStartTime;
          if (timeThisFrame >= 1) {
               final int MILLIS_IN_SECOND = 1000;
               mFPS = MILLIS_IN_SECOND / timeThisFrame;
          }
     }
}

这个新的代码将包含错误,直到我们下一步更新PhysicsEngineRenderer类。

更新物理引擎

将这些新的和修改过的高亮代码行添加到update方法中:

// This signature and much more will change later in the project
boolean update(long fps, ArrayList<GameObject> objects, 
GameState gs, SoundEngine se, 
               ParticleSystem ps){
     // Update all the GameObjects
     for (GameObject object : objects) {
          if (object.checkActive()) {
              object.update(fps, objects.get(
              Level.PLAYER_INDEX)
              .getTransform());
          }
     }

     if(ps.mIsRunning){
          ps.update(fps);
     }
     return false;
}

第一个变化是我们让方法签名接受一个GameObject实例的ArrayList。下一个变化是一个增强的for循环,遍历objects ArrayList中的每个项目,检查它们是否处于活动状态,并检查它们是否调用了它们的update方法。

此时,每一帧的所有对象都在更新。我们只需要能看到他们。

更新渲染器

用以下新的和修改过的行更新GameEngine中的draw方法:

void draw(ArrayList<GameObject> objects, GameState gs, 
HUD hud, ParticleSystem ps) {
     if (mSurfaceHolder.getSurface().isValid()) {
          mCanvas = mSurfaceHolder.lockCanvas();
          mCanvas.drawColor(Color.argb(255, 0, 0, 0));
          if (gs.getDrawing()) {
               // Draw all the game objects here
               for (GameObject object : objects) {
                    if(object.checkActive()) {
                         object.draw(mCanvas, mPaint);
                    }
               }
          }
          if(gs.getGameOver()) {
               // Draw a background graphic here
               objects.get(Level.BACKGROUND_INDEX)
                    .draw(mCanvas, mPaint);
          }
          // Draw a particle system explosion here
          if(ps.mIsRunning){
               ps.draw(mCanvas, mPaint);
          }
          // Now we draw the HUD on top of everything else
          hud.draw(mCanvas, mPaint, gs);
          mSurfaceHolder.unlockCanvasAndPost(mCanvas);
     }
}

首先,我们更新了方法签名,以接收包含所有对象的ArrayList实例。接下来,有一个增强的for循环,依次获取每个对象,检查是否活动,以及它是否调用其draw方法。最后一个小改动是在if(gs.getGameOver)块增加一行代码,在游戏暂停时绘制背景(仅限)。

现在,让我们运行游戏。

运行游戏

最后,你可以运行游戏,在后台看到城市飞速而过:

Figure 20.5 – Running the game

图 20.5–运行游戏

测试暂停按钮,看看你能不能暂停游戏,测试翻转按钮,让飞船反方向飞行,一定要快速点击开火按钮,测试你的速射激光。

如果你还没有在真正的设备上运行我们在这本书中创建的任何游戏,那么现在是时候尝试一下了,因为模拟器不会特别流畅地滚动。

总结

这不是一个容易的章节。如果你还不明白所有不同的类和接口是如何相互连接的,那也没关系。如果需要的话,可以回去重读介绍实体-组件模式介绍简单工厂模式部分,以及学习我们到目前为止所学的代码。然而,当我们编写更多的组件并完成游戏时,继续这个项目也会有同样的帮助。

如果你不完全清楚部件和工厂,不要花太长时间挠头——继续进步;使用这些概念会比思考它们更快地清晰。

当我计划这本书的时候,我想在蛇的游戏后停下来。其实第一版通过搭建安卓游戏学习 Java在蛇游戏之后确实停了。从第二版开始,我认为这是不公平的,因为你已经有了足够的知识来开始实现一个更大的游戏,但是你的知识有足够的差距,以至于类和方法有数百或数千行代码。

请记住,我们正在学习处理的结构中的这种复杂性是一种非常值得的权衡,它与杂乱无章、不可读、有缺陷的类和方法进行了权衡。一旦你掌握了某些模式(包括最终项目中的更多模式)以及它们是如何工作的,你将无法阻止你能够计划和实施的游戏。

在下一章中,我们将通过对剩余的组件类进行编码,并通过LevelGameObjectFactory类将新对象添加到游戏中来完成这个游戏。我们还将增强PhysicsEngine类来处理碰撞检测。***