AndEngine 安卓游戏开发秘籍(一)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

AndEngine 是一个卓越的、功能齐全的、免费的、开源的 Android 平台 2D 框架。它是少数几个持续被独立开发者和专业开发者用来创建时尚有趣游戏的 Android 平台 2D 框架之一,甚至一些市场上最成功的游戏也使用了它。然而,要取得成功,仅仅使用特定的框架是不够的。

AndEngine for Android Game Development Cookbook 提供了许多关于 AndEngine 最重要方面的信息性演练,这些方面属于一般游戏编程级别。这本书涵盖了从 AndEngine 游戏的生命周期到在场景中放置精灵并移动它们,一直到创建可破坏物体和光线投射技术等内容。更重要的是,这本书完全基于 AndEngine 最新的、最高效的 Anchor-Center 分支。

这本书涵盖的内容。

第一章,AndEngine 游戏结构,涵盖了使用 AndEngine 进行游戏开发的重要方面,关于大多数游戏需要生存的核心组件。从音频、纹理、AndEngine 生命周期、保存/加载游戏数据等,这一章都有所涉及。

第二章,使用实体,开始让我们熟悉 AndEngine 的 Entity 类及其子类型,如精灵、文本、基元等。Entity 类是 AndEngine 的核心组件,它允许代码中的对象在屏幕上显示。更具体地说,这一章包括 Entity 类中最重要方法的列表,以使我们能够完全控制实体的行为、反应或它们的外观。

第三章,设计你的菜单,介绍了一些移动游戏中菜单设计较常见的方面。本章涵盖的主题包括创建按钮,为菜单添加主题音乐,创建视差背景和菜单屏幕导航。本章中的主题很容易被用在游戏的其他区域。

第四章,使用摄像头,讨论了 AndEngine 中包含的各种关于游戏摄像头和引擎如何查看游戏场景的选项。我们从不同的摄像头对象开始,以便让我们正确理解每种摄像头的优点,从而做出有见地的决定。然后,我们继续涵盖摄像头的移动和缩放,创建超大背景,创建抬头显示,甚至介绍分屏游戏引擎以应对更复杂的游戏设计。

第五章,场景和图层管理,展示了如何创建一个健壮的场景管理框架,该框架包含特定场景的加载屏幕和动画图层。本章中的管理场景使用资源管理器,并且非常易于定制。

第六章,物理学的应用,探索了使用 Box2D 物理扩展创建 AndEngine 物理模拟的各种技术。本章的内容涵盖了 Box2D 物理世界的基本设置:体类型、类别过滤、具有多个固定装置的物体、基于多边形的物体、力、关节、布娃娃、绳索、碰撞事件、可破坏物体和光线投射。

第七章,使用更新处理器,展示了每次引擎更新时调用的更新处理器的使用方法。本章的内容展示了如何注册基于实体的更新处理器、条件更新和创建游戏计时器。

第八章,最大化性能,介绍了一些在提高任何 Android 游戏性能时最有效的高级实践。本章涵盖了涉及音频、图形/渲染和一般内存管理的优化技术,以帮助在必要时减轻性能问题。

第九章,AndEngine 扩展概述,在这一章中我们讨论了一些更受欢迎的 AndEngine 扩展,根据游戏的不同,这些扩展可能对项目有益。这些扩展并非适合所有人,但对于感兴趣的人来说,本章包含了我们如何着手创建动态壁纸、通过网络服务器和客户端实现多人组件、创建高分辨率 SVG 纹理以及色彩映射纹理的见解。

第十章,更深入了解 AndEngine,提供了几个有用的食谱,这些食谱扩展了前几章介绍的概念。本章的内容包括批量纹理加载、纹理网格、自动阴影、移动平台和绳索桥梁。

附录 A,MagneTank 的源代码,概述了 MagneTank 游戏,通过逐类描述来展示如何设置用 AndEngine 制作完整的游戏。该游戏包括贯穿各章节的许多食谱,并且附带的代码中提供了源代码。

附录 B,附加食谱,书中未包含,但可以通过以下链接免费下载:downloads.packtpub.com/sites/default/files/downloads/8987OS_AppB_Final.pdf

你需要为这本书准备什么

AndEngine for Android Game Development Cookbook》对大多数 AndEngine 开发者都有用。从最初的几章开始,读者将开始学习 AndEngine 的基础知识,即使是中级开发者也能在这些章节中找到有用的提示。随着读者章节的深入,将涉及更难的话题,因此初学者不要跳过。此外,那些尚未过渡到 AndEngine 最新开发分支的中级开发者,在整个书中都能找到关于 GLES1/GLES2 分支与本书讨论的 Anchor-Center 分支之间的差异的有用信息。

建议具备 Java 编程语言的基本理解。

为了执行本书中的各种主题,所需的软件包括用于构建和编译代码的 Eclipse IDE,用于图像绘制/编辑的 GIMP,以及用于 SVG 绘制/编辑的 Inkscape。如果您对它们更熟悉,请随意使用这些产品的替代品。此外,本书假设读者在开始使用食谱之前已经获得了所需的库,包括 AndEngine 及其各种扩展。

本书适合的读者

AndEngine for Android Game Development Cookbook》面向那些对使用最新版本的 AndEngine 感兴趣的开发者,该版本采用了全新的 GLES 2.0 Anchor-Center 分支。这本书将帮助那些试图进入移动游戏市场,打算发布有趣且刺激的游戏,同时减少进入 AndEngine 开发时不可避免的学习曲线的开发者。

约定

在这本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码字如下所示:“以最基础的Entity方法为例,我们将一个Entity对象附加到一个Scene对象上。”

代码块设置如下:

  float baseBufferData[] = {
      /* First Triangle */
      0, BASE_HEIGHT, UNUSED, /* first point */
      BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */
      BASE_WIDTH, 0, UNUSED, 	/* third point */

      /* Second Triangle */
      BASE_WIDTH, 0, UNUSED, /* first point */
      0, 0, UNUSED, /* second point */
      0, BASE_HEIGHT, UNUSED, /* third point */
  };

注意

警告或重要注意事项会像这样出现在一个框里。

提示

提示和技巧会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或可能不喜欢的内容。读者的反馈对我们来说很重要,可以帮助我们开发出您真正能从中获得最大收益的标题。

如需向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经拥有了 Packt 的一本书,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从您的账户www.PacktPub.com下载您购买的所有 Packt 书籍的示例代码文件。如果您在别处购买了这本书,可以访问www.PacktPub.com/support注册,我们会直接将文件通过电子邮件发送给您。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误信息,请通过访问www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误信息得到验证,您的提交将被接受,勘误信息将被上传到我们的网站,或添加到该标题下的现有勘误列表中。任何现有的勘误信息可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

互联网上版权资料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供我们该位置地址或网站名称,以便我们可以寻求补救措施。

如果您有疑似盗版资料的链接,请联系<copyright@packtpub.com>

我们感谢您帮助保护我们的作者,以及我们为您带来有价值内容的能力。

问题

如果您在书的任何方面遇到问题,可以联系<questions@packtpub.com>,我们将尽力解决。

第一章:AndEngine 游戏结构

在本章中,我们将了解构建AndEngine游戏中所需的主要组成部分。主题包括:

  • 了解生命周期

  • 选择我们的引擎类型

  • 选择分辨率策略

  • 创建对象工厂

  • 创建游戏管理器

  • 引入声音和音乐

  • 使用不同类型的纹理

  • 应用纹理选项

  • 使用 AndEngine 字体资源

  • 创建资源管理器

  • 保存和加载游戏数据

引言

AndEngine 最吸引人的方面是创建游戏的极大便捷性。在首次接触 AndEngine 后,在几周内设计和编码一个游戏并非遥不可及,但这并不意味着它将是一个完美的游戏。如果我们不理解引擎的工作原理,编码过程可能会很繁琐。为了创建精确、有序且可扩展的项目,了解 AndEngine 的主要构建块和游戏结构是一个好主意。

在本章中,我们将介绍 AndEngine 和一般游戏编程中最必要的几个组成部分。我们将查看一些类,这些类将帮助我们快速高效地创建各种游戏的基础。此外,我们还将介绍资源和对象类型之间的区别,这些区别在塑造游戏的整体外观和感觉方面起着最重要的作用。如果需要,建议将本章作为参考资料保存。

了解生命周期

在初始化游戏时,了解操作的顺序是很重要的。游戏的基本需求包括创建引擎、加载游戏资源、以及设置初始屏幕和设置。这就是创建 AndEngine 游戏基础所需的一切。但是,如果我们计划在游戏中实现更多多样性,那么了解 AndEngine 中包含的完整生命周期是明智的。

准备就绪

请参考代码包中名为PacktRecipesActivity的类。

如何操作…

AndEngine 生命周期包括我们直接负责定义的几个方法。这些方法包括创建EngineOptions对象,创建Scene对象,以及用子实体填充场景。这些方法的调用顺序如下:

  1. 定义onCreateEngineOptions()方法:

    @Override
    public EngineOptions onCreateEngineOptions() {
    
      // Define our mCamera object
      mCamera = new Camera(0, 0, WIDTH, HEIGHT);
    
      // Declare & Define our engine options to be applied to our Engine object
      EngineOptions engineOptions = new EngineOptions(true,
          ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(),
          mCamera);
    
      // It is necessary in a lot of applications to define the following
      // wake lock options in order to disable the device's display
      // from turning off during gameplay due to inactivity
      engineOptions.setWakeLockOptions(WakeLockOptions.SCREEN_ON);
    
      // Return the engineOptions object, passing it to the engine
      return engineOptions;
    }
    
  2. 定义onCreateResources()方法:

    @Override
    public void onCreateResources(
        OnCreateResourcesCallback pOnCreateResourcesCallback) {
    
      /* We should notify the pOnCreateResourcesCallback that we've finished
        * loading all of the necessary resources in our game AFTER they are loaded.
        * onCreateResourcesFinished() should be the last method called.  */
      pOnCreateResourcesCallback.onCreateResourcesFinished();
    }
    
  3. 定义onCreateScene()方法:

    @Override
    public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) {
      // Create the Scene object
      mScene = new Scene();
    
      // Notify the callback that we're finished creating the scene, returning
      // mScene to the mEngine object (handled automatically)
      pOnCreateSceneCallback.onCreateSceneFinished(mScene);
    }
    
  4. 定义onPopulateScene()方法:

    @Override
    public void onPopulateScene(Scene pScene,
      OnPopulateSceneCallback pOnPopulateSceneCallback) {
    
      // onPopulateSceneFinished(), similar to the resource and scene callback
      // methods, should be called once we are finished populating the scene.
      pOnPopulateSceneCallback.onPopulateSceneFinished();
    }
    

工作原理…

在此食谱类中找到的代码是任何 AndEngine 游戏的基础。我们设置了一个主活动类,作为进入我们应用程序的入口点。活动包含 AndEngine 活动生命周期中我们负责的四个主要方法,从创建EngineOptions选项开始,创建资源,创建场景,以及填充场景。

在第一步中,我们覆盖了引擎的onCreateEngineOptions()方法。在这个方法内部,我们主要关注实例化Camera对象和EngineOptions对象。这两个对象的构造函数允许我们定义应用程序的显示属性。此外,通过调用engineOptions.setWakeLockOptions(WakeLockOptions.SCREEN_ON)方法,我们阻止了在应用程序不活动期间屏幕自动关闭。

在第二步中,我们继续覆盖onCreateResources()方法,该方法为我们提供了一个特定方法,用于创建和设置游戏所需的所有资源。这些资源可能包括纹理、声音和音乐以及字体。在这一步和接下来的两步中,我们需要调用相应的方法回调,以继续应用程序的生命周期。对于onCreateResources()方法,我们必须在方法的最后包含调用pOnCreateResourcesCallback.onCreateResourcesFinished()

第三步涉及实例化和设置Scene对象。设置场景可以像本食谱中显示的那么简单,或者对于更复杂的项目,它可能包括设置触摸事件监听器、更新处理器等。完成场景设置后,我们必须调用pOnCreateSceneCallback.onCreateSceneFinished(mScene)方法,将我们新创建的mScene对象传递给引擎,以便在设备上显示。

最后需要处理的步骤包括定义onPopulateScene()方法。此方法专门用于将子实体附加到场景。与之前的两个步骤一样,我们必须调用pOnPopulateSceneCallback.onPopulateSceneFinished()以继续剩余的 AndEngine 生命周期调用。

在以下列表中,我们将按照从活动启动到终止时调用的顺序介绍生命周期方法。

启动期间的生命周期调用如下:

  • onCreate:此方法是 Android SDK 的原生应用程序入口点。在 AndEngine 开发中,此方法只需调用我们BaseGameActivity类中的onCreateEngineOptions()方法,然后将返回的选项应用到游戏引擎中。

  • onResume:这是 Android SDK 的另一个原生方法。在这里,我们从EngineOptions对象获取唤醒锁设置,然后为引擎的RenderSurfaceView对象调用onResume()方法。

  • onSurfaceCreated:此方法将在我们活动的初始启动过程中调用onCreateGame(),或者如果活动之前已经部署,则将布尔变量注册为true以重新加载资源。

  • onReloadResources:如果我们的应用程序从最小化状态恢复到焦点状态,此方法将重新加载游戏资源。在应用程序首次执行时不会调用此方法。

  • onCreateGame:这是为了处理 AndEngine 生命周期中接下来三个回调的执行顺序。

  • onCreateResources:这个方法允许我们声明和定义在启动活动时应用所需的最初资源。这些资源包括但不限于纹理、声音和音乐以及字体。

  • onCreateScene:在这里,我们处理活动场景对象的初始化。在这个方法中可以附加实体到场景,但为了保持组织性,通常最好在onPopulateScene()中附加实体。

  • onPopulateScene:在生命周期中的onPopulateScene()方法里,我们几乎完成了场景的设置,尽管还有一些生命周期调用会由引擎自动处理。这个方法应该用来定义应用首次启动时场景的视觉结果。注意,此时场景已经被创建并应用到引擎中。如果此时没有加载屏幕或启动画面,并且有许多实体需要附加到场景中,那么在某些情况下可能会看到实体被附加到场景上。

  • onGameCreated:这表明onCreateGame()序列已经完成,如有必要,重新加载资源,否则什么都不做。是否重新加载资源取决于在五个生命周期调用之前的onSurfaceCreated方法中简要提到的布尔变量。

  • onSurfaceChanged:每次应用的方向从横屏模式变为竖屏模式,或者从竖屏模式变为横屏模式时,都会调用这个方法。

  • onResumeGame:这是在活动启动周期中最后一个调用的方法。如果我们的活动在没有问题的情况下到达这一点,将调用引擎的start()方法,使游戏的更新线程活跃起来。

在最小化/终止过程中的生命周期调用如下:

  • onPause:活动最小化或结束时首先调用的方法。这是原生安卓的暂停方法,它调用RenderSurfaceView对象的暂停方法,并恢复游戏引擎应用的唤醒锁设置。

  • onPauseGame:接下来,AndEngine 的onPause()实现,它只是简单地在引擎上调用stop()方法,导致引擎的所有更新处理器以及更新线程停止。

  • onDestroy:在onDestroy()方法中,AndEngine 会清除由引擎管理类持有的ArrayList对象中包含的所有图形资源。这些管理类包括VertexBufferObjectManager类、FontManager类、ShaderProgramManager类,以及最后的TextureManager类。

  • onDestroyResources:这个方法名称可能有些误导,因为我们已经在onDestroy()中卸载了大部分资源。这个方法真正的作用是,通过调用相应管理器的releaseAll()方法,释放所有存储在其中的声音和音乐对象。

  • onGameDestroyed:最后,我们到达在整个 AndEngine 生命周期中需要调用的最后一个方法。在这个方法中没有太多动作发生。AndEngine 只是将用于 Engine 的mGameCreated布尔变量设置为false,表示活动不再运行。

在以下图片中,我们可以看到当创建游戏、最小化或销毁游戏时,生命周期的实际表现:

工作原理…

注意

由于 AndEngine 生命周期的异步性质,在单个启动实例期间可能会多次执行某些方法。这些事件的发生在设备之间是不同的。

还有更多…

在本食谱的前一部分中,我们已经介绍了主要的BaseGameActivity类。以下类可以作为BaseGameActivity类的替代品,每个类都有自己的一些细微差别。

LayoutGameActivity

LayoutGameActivity类是一个有用的活动类,它允许我们将 AndEngine 场景图视图集成到普通的 Android 应用程序中。另一方面,使用这个类,我们还可以将原生的 Android SDK 视图,如按钮、滑动条、下拉列表、附加布局或其他任何视图包含到我们的游戏中。然而,使用这种活动最流行的原因是便于在游戏中实现广告,作为一种获取收益的手段。

LayoutGameActivity类设置需要几个额外的步骤。

  1. 在项目的默认布局 XML 文件中添加以下行。这个文件通常称为main.xml。以下代码段将 AndEngine RenderSurfaceView类添加到我们的布局文件中。这是将在设备上显示我们游戏的视图:

    <org.andengine.opengl.view.RenderSurfaceView
    android:id="@+id/gameSurfaceView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"/>
    
  2. 这种活动类型的第二个也是最后一个额外步骤是在第一步中引用布局 XML 文件和RenderSurfaceView,在LayoutGameActivity重写方法中。以下代码假设布局文件在res/layout/文件夹中称为main.xml;在这种情况下,可以在完成第一步后将其复制/粘贴到我们的LayoutGameActivity类中:

    @Override
    protected int getLayoutID() {
      return R.layout.main;
    }
    
    @Override
    protected int getRenderSurfaceViewID() {
      return R.id.gameSurfaceView;
    }
    

SimpleBaseGameActivitySimpleLayoutGameActivity

如建议的那样,SimpleBaseGameActivitySimpleLayoutGameActivity类使重写生命周期方法变得更容易处理。它们不要求我们重写onPopulateScene()方法,而且,在我们定义完重写的方法后,我们也不需要调用方法回调。使用这些活动类型,我们可以简单地添加未实现的生命周期方法,AndEngine 会为我们处理回调。

SimpleAsyncGameActivity

我们将要讨论的最后一个游戏活动类是SimpleAsyncGameActivity类。这个类包括三个可选的生命周期方法:onCreateResourcesAsync()onCreateSceneAsync()onPopulateSceneAsync(),以及通常的onCreateEngineOptions()方法。这个活动与其他活动的主要区别在于,它为每个"Async"方法提供了加载进度条。以下代码片段展示了当纹理加载时我们如何增加加载进度条:

@Override
public void onCreateResourcesAsync(IProgressListener pProgressListener)
    throws Exception {

  // Load texture number one
  pProgressListener.onProgressChanged(10);

  // Load texture number two
  pProgressListener.onProgressChanged(20);

  // Load texture number three
  pProgressListener.onProgressChanged(30);

  // We can continue to set progress to whichever value we'd like
  // for each additional step through onCreateResourcesAsync...
}

提示

下载示例代码

你可以从你在www.PacktPub.com的账户中下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.PacktPub.com/support注册,我们会将文件直接通过电子邮件发送给你。

选择我们的引擎类型

在我们开始编程游戏之前,最好先确定游戏所需的性能需求。AndEngine 包含几种不同类型的引擎供我们选择使用,每种都有其自身的优势。当然,这些优势取决于我们计划创建的游戏类型。

准备工作

执行本章中的了解生命周期食谱,以在我们的 IDE 中设置一个基本的 AndEngine 项目,然后继续到*如何操作…*部分。

如何操作…

为了正确地为我们的游戏定义一个特定的Engine对象,我们必须重写onCreateEngine()方法,这是 AndEngine 启动过程的一部分。在任意基础的 AndEngine 活动中添加以下代码,以手动处理引擎的创建:

/* The onCreateEngine method allows us to return a 'customized' Engine object
* to the Activity which for the most part affects the way frame updates are 
* handled. Depending on the Engine object used, the overall feel of the 
* gameplay can alter drastically. 
*/
@Override
public Engine onCreateEngine(EngineOptions pEngineOptions) {
  return super.onCreateEngine(pEngineOptions);
  /* The returned super method above simply calls:
      return new Engine(pEngineOptions);
  */
}

工作原理…

以下是 AndEngine 中可用的各种Engine对象的概览,以及一个简短的代码片段,展示如何设置每个Engine对象:

  • Engine:首先,我们有一个普通的Engine对象。对于大多数游戏开发来说,Engine对象并不理想,因为它在每秒帧数上没有任何限制。在两个不同的设备上,你很可能会注意到游戏速度的差异。一个思考方式是,如果两个不同的设备同时开始观看同一个视频,较快的设备可能会先完成视频观看,而不是同时完成。因此,在运行较慢的设备上可能会出现明显的问题,尤其是在物理是游戏重要部分的情况下。将这种类型的引擎集成到我们的游戏中不需要额外的步骤。

  • FixedStepEngine:我们可用的第二种引擎是FixedStepEngine。这是游戏开发中理想的引擎,因为它强制游戏循环以恒定速度更新,而与设备无关。这是通过根据经过的时间更新游戏,而不是根据设备执行代码的能力来实现的。FixedStepEngine要求我们按顺序传递EngineOptions对象和一个int值。这个int值定义了每秒引擎将强制运行的步数。以下代码创建了一个以恒定60步每秒运行的引擎:

    @Override
    public Engine onCreateEngine(EngineOptions pEngineOptions) {
      // Create a fixed step engine updating at 60 steps per second
        return new FixedStepEngine(pEngineOptions, 60);
      }
    
  • LimitedFPSEngineLimitedFPSEngine引擎允许我们设置引擎运行的每秒帧数限制。这将导致引擎进行一些内部计算,如果首选 FPS 与引擎当前实现的 FPS 之间的差值大于预设值,引擎将会等待一小段时间后再进行下一次更新。LimitedFPSEngine在构造函数中需要两个参数,包括EngineOptions对象和一个指定最大每秒帧数的int值。以下代码创建了一个最大以 60 帧每秒运行的引擎:

    @Override
    public Engine onCreateEngine(EngineOptions pEngineOptions) {
      // Create a limited FPS engine, which will run at a maximum of 60 FPS
      return new LimitedFPSEngine(pEngineOptions, 60);
    }
    
  • SingleSceneSplitScreenEngineDoubleSceneSplitScreenEngineSingleSceneSplitScreenEngine引擎和DoubleSceneSplitScreenEngine引擎允许我们创建带有两个独立摄像头的游戏,可以是单个场景,通常用于单人游戏,也可以是两个场景,用于单个设备上的多人游戏。这些只是示例,然而,这两个引擎可以具有广泛的应用,包括迷你地图、多重视角、菜单系统等等。更多关于设置这些类型Engine对象的详细信息,请参见第四章,创建分屏游戏

选择分辨率策略

选择分辨率策略可能是一个敏感的话题,特别是考虑到我们正在处理的平台目前主要运行在从 3 英寸显示屏到 10.1 英寸的设备上。通常,开发者和用户都希望游戏能够占据设备显示的完整宽度和高度,但在某些情况下,我们可能需要仔细选择分辨率策略,以便按照我们开发者的意愿正确显示场景。在本节中,我们将讨论 AndEngine 中包含的各种分辨率策略,这将帮助我们决定哪种策略可能最适合我们应用程序的需求。

如何操作…

我们选择遵循的分辨率策略必须作为参数包含在EngineOptions构造函数中,该函数是在 AndEngine 生命周期中的onCreateEngineOptions()方法里创建的。以下代码使用FillResolutionPolicy类创建我们的EngineOptions对象,这一部分将在本章后面进行解释:

EngineOptions engineOptions = new EngineOptions(true,
    ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(),
    mCamera); 

我们只需向构造函数传递另一个分辨率策略类变体,就可以选择不同的分辨率策略。

它的工作原理…

以下是 AndEngine 的BaseResolutionPolicy子类型的概述。这些策略用于指定 AndEngine 如何根据各种因素处理应用程序的显示宽度和高度:

  • FillResolutionPolicy:如果我们只是希望应用程序占据显示器的全部宽度和高度,FillResolutionPolicy类是典型的分辨率策略。虽然此策略允许应用程序以真正的全屏模式运行,但它可能会导致场景为了占据显示器的全部可用尺寸而在某些部分产生明显的拉伸。我们只需在EngineOptions构造函数中的分辨率策略参数中包含new FillResolutionPolicy(),即可选择此分辨率策略。

  • FixedResolutionPolicyFixedResolutionPolicy类允许我们为应用程序应用固定的显示尺寸,无论设备显示尺寸或Camera对象尺寸如何。此策略可以通过new FixedResolutionPolicy(pWidth, pHeight)传递给EngineOptions,其中pWidth定义了应用程序视图将覆盖的最终宽度,而pHeight定义了应用程序视图将覆盖的最终高度。例如,如果我们向此策略类型的构造函数传递 800 的宽度和 480 的高度,在一个分辨率为 1280 x 752 的平板电脑上,由于分辨率策略与实际显示尺寸之间没有补偿,我们将得到一个空白黑色区域。

  • RatioResolutionPolicy:如果需要在不扭曲精灵的情况下获得最大显示尺寸,RatioResolutionPolicy类是最佳的分辨率策略选择。另一方面,由于 Android 设备范围广泛,涵盖了许多显示尺寸,某些设备可能会在显示的顶部和底部,或左右两侧看到“黑边”。此分辨率策略的构造函数可以传递一个float值,用于定义显示尺寸的首选比率值,或者传递宽度和高度参数,从中通过宽度除以高度来提取比率值。例如,new RatioResolutionPolicy(1.6f)来定义一个比率,或者new RatioResolutionPolicy(mCameraWidth, mCameraHeight),假设mCameraWidthmCameraHeight是定义的Camera对象尺寸。

  • RelativeResolutionPolicy:这是最终的分辨率策略。该策略允许我们根据缩放因子对整个应用程序视图进行放大或缩小,1f是默认值。我们可以使用构造函数对视图应用一般缩放——new RelativeResolutionPolicy(1.5f)——这将使宽度和高度都增加1.5倍;或者我们可以指定单独的宽度和高度缩放比例,例如,new RelativeResolutionPolicy(1.5f, 0.5f)。需要注意的是,在使用此策略时,我们必须小心缩放因子,因为过大的缩放会导致应用程序在无警告的情况下关闭。尽量保持缩放因子小于1.8f;否则,请确保在各种设备上进行大量测试。

创建对象工厂

对象工厂是在编程的各个领域中都有使用的有用设计模式。特别是在游戏开发中,工厂可能被用来生成敌人对象、生成子弹对象、粒子效果、物品对象等等。实际上,AndEngine 在创建声音、音乐、纹理和字体等时也使用了工厂模式。在这个示例中,我们将了解如何创建一个对象工厂,并讨论如何在我们自己的项目中使用它们来简化对象创建。

准备工作

请参考代码包中名为ObjectFactory的类。

如何操作…

在这个示例中,我们使用ObjectFactory类作为我们轻松创建和返回BaseObject类子类型的方式。然而,在实际项目中,工厂通常不会包含内部类。

  1. 在我们创建对象工厂之前,我们应该创建我们的基类以及至少几个扩展基类的子类型:

    public static class BaseObject {
    
      /* The mX and mY variables have no real purpose in this recipe, however in
       * a real factory class, member variables might be used to define position,
       * color, scale, and more, of a sprite or other entity.   */
      private int mX;
      private int mY;
    
      // BaseObject constructor, all subtypes should define an mX and mY value on creation
      BaseObject(final int pX, final int pY){
        this.mX = pX;
        this.mY = pY;
      }
    }
    
  2. 一旦我们拥有一个带有任意数量的子类型的基类,我们现在可以开始考虑实现工厂设计模式。ObjectFactory类包含处理创建并返回类型为LargeObjectSmallObject对象的方法:

    public class ObjectFactory {
    
      // Return a new LargeObject with the defined 'x' and 'y' member variables.
      public static LargeObject createLargeObject(final int pX, final int pY){
        return new LargeObject(pX, pY);
      }
      // Return a new SmallObject with the defined 'x' and 'y' member variables.
      public static SmallObject createSmallObject(final int pX, final int pY){
        return new SmallObject(pX, pY);
      }
    }
    

它的工作原理是…

在这个示例的第一步中,我们创建了一个BaseObject类。这个类包括两个成员变量mXmY,如果我们处理的是 AndEngine 实体,可以想象它们将定义设备显示上的位置。一旦我们设置好了基类,就可以开始创建基类的子类型。这个示例中的BaseObject类有两个内部类扩展它,一个名为LargeObject,另一个名为SmallObject。对象工厂的工作是确定我们需要创建的基类的哪个子类型,以及定义对象的属性,或者在这个实例中是mXmY成员变量。

在第二步中,我们将查看ObjectFactory代码。这个类应该包含与工厂处理的具体对象类型相关的任何对象创建的变化。在这种情况下,两个独立的对象仅需要一个定义了mXmY变量的变量。在现实世界中,我们可能会发现创建一个SpriteFactory类很有帮助。这个类可能包含几种不同的方法,用于通过SpriteFactory.createSprite()SpriteFactory.createButtonSprite()SpriteFactory.createTiledSprite()创建普通精灵、按钮精灵或平铺精灵。此外,这些方法可能还需要定义位置、缩放、纹理区域、颜色等参数。这个类最重要的方面是它的方法返回一个对象的新子类型,因为这是工厂类背后的整个目的。

创建游戏管理器

游戏管理器是大多数游戏的重要组成部分。游戏管理器是一个类,应该包含与游戏玩法相关的数据;包括但不限于跟踪分数、信用/货币、玩家健康和其他一般游戏信息。在本主题中,我们将研究一个游戏管理器类,以了解它们如何融入我们的游戏结构。

准备就绪

请参考代码包中名为GameManager的类。

如何操作…

我们将要介绍的游戏管理器将遵循单例设计模式。这意味着在整个应用程序生命周期中,我们只创建类的单个实例,并且可以在整个项目中访问其方法。按照以下步骤操作:

  1. 创建游戏管理器单例:

    private static GameManager INSTANCE;
    
    // The constructor does not do anything for this singleton
    GameManager(){
    }
    
    public static GameManager getInstance(){
      if(INSTANCE == null){
        INSTANCE = new GameManager();
      }
      return INSTANCE;
    }
    
  2. 创建成员变量以及相应的获取器和设置器,以跟踪游戏数据:

    // get the current score
    public int getCurrentScore(){
      return this.mCurrentScore;
    }
    
    // get the bird count
    public int getBirdCount(){
      return this.mBirdCount;
    }
    
    // increase the current score, most likely when an enemy is destroyed
    public void incrementScore(int pIncrementBy){
      mCurrentScore += pIncrementBy;
    }
    
    // Any time a bird is launched, we decrement our bird count
    public void decrementBirdCount(){
      mBirdCount -= 1;
    }
    
  3. 创建一个重置方法,将所有数据恢复到它们的初始值:

    // Resetting the game simply means we must revert back to initial values.
    public void resetGame(){
      this.mCurrentScore = GameManager.INITIAL_SCORE;
      this.mBirdCount = GameManager.INITIAL_BIRD_COUNT;
      this.mEnemyCount = GameManager.INITIAL_ENEMY_COUNT;
    }
    

它是如何工作的…

根据创建的游戏类型,游戏管理器肯定有不同的任务。这个示例的GameManager类旨在模仿某个情感鸟品牌的类。我们可以看到,这个特定GameManager类中的任务有限,但随着游戏玩法的复杂化,游戏管理器通常会增长,因为它需要跟踪更多信息。

在这个配方的第一步中,我们将GameManager类设置为单例模式。单例是一种设计模式,旨在确保整个应用程序生命周期中只存在一个静态的此类实例。由于其静态特性,我们可以全局调用游戏管理器的方法,这意味着我们可以在项目中任何类中访问其方法,而无需创建新的GameManager类。为了获取GameManager类的实例,我们可以在项目的任何类中调用GameManager.getInstance()。这样做将会在GameManager类尚未被引用的情况下,为其分配一个新的GameManager类给INSTANCE。然后返回INSTANCE对象,这样我们就可以调用GameManager类中的数据修改方法,例如GameManager.getInstance().getCurrentScore()

在第二步中,我们创建了用于修改和获取存储在GameManager类中的数据的 getter 和 setter 方法。这个配方中的GameManager类包含三个int值,用于跟踪重要的游戏数据:mCurrentScore(当前得分)、mBirdCount(鸟类计数)和mEnemyCount(敌人计数)。这些变量各自都有对应的 getter 和 setter,使我们能够轻松地修改游戏数据。在游戏过程中,如果有一个敌人被摧毁,我们可以调用GameManager.getInstance().decrementEnemyCount()以及GameManager.getInstance().incrementScore(pValue),其中pValue可能由被摧毁的敌人对象提供。

设置这个游戏管理器的最后一步是提供一个重置游戏数据的方法。由于我们使用的是单例模式,无论我们是从小游戏转到主菜单、商店还是其他任何场景,GameManager类的数据都不会自动恢复到默认值。这意味着每次重置关卡时,我们也必须重置游戏管理器的数据。在GameManager类中,我们设置了一个名为resetGame()的方法,其作用是简单地将数据恢复到原始值。

当开始一个新关卡时,我们可以调用GameManager.getInstance().resetGame()以快速将所有数据恢复到初始值。然而,这是一个通用的GameManager类,具体哪些数据应该在关卡重置或加载时重置完全由开发者决定。如果GameManager类存储了信用/货币数据,例如在商店中使用时,最好不要将这个特定变量重置回默认值。

引入声音和音乐。

声音和音乐在游戏玩法中对用户起着重要作用。如果使用得当,它们可以给游戏带来额外的优势,让玩家在玩游戏时能够完全沉浸其中。另一方面,如果使用不当,它们也可能引起烦恼和不满。在这个配方中,我们将深入探讨 AndEngine 中的SoundMusic对象,涵盖从加载它们到修改它们的速率等内容。

准备工作

完成本章提供的了解生命周期配方,以便我们在 IDE 中设置一个基本的 AndEngine 项目。此外,我们应在项目的assets/文件夹中创建一个新的子文件夹。将此文件夹命名为sfx,并添加一个名为sound.mp3的声音文件,以及另一个名为music.mp3的文件。完成这些操作后,继续阅读*如何操作…*部分。

如何操作…

执行以下步骤,设置游戏以使用SoundMusic对象。请注意,Sound对象用于声音效果,例如爆炸、碰撞或其他短音频播放事件。而Music对象用于长时间音频播放事件,如循环菜单音乐或游戏音乐。

  1. 第一步是确保我们的Engine对象认识到我们计划在游戏中使用SoundMusic对象。在创建EngineOptions对象之后,在我们的活动生命周期的onCreateEngineOptions()方法中添加以下几行:

    engineOptions.getAudioOptions().setNeedsMusic(true);
    engineOptions.getAudioOptions().setNeedsSound(true);
    
  2. 在第二步中,我们将为声音和音乐工厂设置资源路径,然后加载SoundMusic对象。SoundMusic对象是资源,所以你可能已经猜到,以下代码可以放入我们活动生命周期的onCreateResources()方法中:

    /* Set the base path for our SoundFactory and MusicFactory to
      * define where they will look for audio files.
     */
    SoundFactory.setAssetBasePath("sfx/");
    MusicFactory.setAssetBasePath("sfx/");
    
    // Load our "sound.mp3" file into a Sound object
    try {
      Sound mSound = SoundFactory.createSoundFromAsset(getSoundManager(), this, "sound.mp3");
    } catch (IOException e) {
      e.printStackTrace();
    }
    
    // Load our "music.mp3" file into a music object
    try {
      Music mMusic = MusicFactory.createMusicFromAsset(getMusicManager(), this, "music.mp3");
    } catch (IOException e) {
      e.printStackTrace();
    }
    
  3. 一旦Sound对象被加载到SoundManager类中,我们就可以根据需要通过调用play()来播放它们,无论是碰撞时、按钮点击还是其他情况:

    // Play the mSound object
    mSound.play();
    
  4. Music对象应该与Sound对象以不同的方式处理。在大多数情况下,如果我们的Music对象应该在游戏中持续循环,我们应在活动生命周期内处理所有的play()pause()方法:

    /* Music objects which loop continuously should be played in
    * onResumeGame() of the activity life cycle
    */
    @Override
    public synchronized void onResumeGame() {
      if(mMusic != null && !mMusic.isPlaying()){
        mMusic.play();
      }
    
      super.onResumeGame();
    }
    
    /* Music objects which loop continuously should be paused in
    * onPauseGame() of the activity life cycle
    */
    @Override
    public synchronized void onPauseGame() {
      if(mMusic != null && mMusic.isPlaying()){
        mMusic.pause();
      }
    
      super.onPauseGame();
    }
    

工作原理…

在这个配方的第一步,我们需要让引擎知道我们是否将利用 AndEngine 播放SoundMusic对象的能力。如果忽略这一步,将导致应用程序出现错误,因此在我们将音频实现到游戏中之前,请确保在onCreateEngineOptions()方法中返回EngineOptions之前完成这一步。

在第二步中,我们访问应用程序生命周期的onCreateResources()方法。首先,我们设置了SoundFactoryMusicFactory的基路径。如准备就绪部分所述,我们应在项目的assets/sfx文件夹中为我们的音频文件保留一个文件夹,其中包含所有音频文件。通过在两个用于音频的工厂类上调用setAssetBasePath("sfx/"),我们现在指向了查找音频文件的正确文件夹。完成此操作后,我们可以通过使用SoundFactory类加载Sound对象,以及通过使用MusicFactory类加载Music对象。SoundMusic对象要求我们传递以下参数:根据我们正在加载的音频对象类型选择mEngine.getSoundManager()mEngine.getMusicManager()Context类即BaseGameActivity,或者是这个活动,以及音频文件名称的字符串格式。

在第三步中,我们现在可以对希望播放的音频对象调用play()方法。但是,这个方法应该在onCreateResources()回调通知所有资源都已加载之后才能调用。为了安全起见,我们只需在 AndEngine 生命周期的onCreateResources()部分之后,不再播放任何SoundMusic对象。

在最后一步中,我们设置Music对象,以便在活动启动时以及从生命周期中调用onResumeGame()时调用其play()方法。在另一端,在onPauseGame()期间,调用Music对象的pause()方法。在大多数情况下,最好以这种方式设置我们的Music对象,特别是由于应用程序中断的最终不可避免性,例如电话或意外弹出点击。这种方法将允许我们的Music对象在应用程序失去焦点时自动暂停,并在我们从最小化返回后重新开始执行。

注意事项

在这个配方和其他与资源加载相关的配方中,文件名已经被硬编码到代码片段中。这样做是为了增加简单性,但建议使用我们项目的strings.xml Android 资源文件,以保持字符串的组织和易于管理。

还有更多…

AndEngine 使用 Android 原生的声音类为我们的游戏提供音频娱乐。除了play()pause()方法之外,这些类还包含一些额外的方法,允许我们在运行时对音频对象有更多的控制。

音乐对象

以下列表包括为Music对象提供的方法:

  • seekToseekTo(pMilliseconds)方法允许我们定义特定Music对象的音频播放应从哪里开始。pMilliseconds等于音频轨道的位置(毫秒),我们希望在调用Music对象的play()时从此位置开始播放。为了获取Music对象的持续时间(毫秒),我们可以调用mMusic.getMediaPlayer().getDuration()

  • setLoopingsetLooping(pBoolean)方法简单定义了Music对象在到达持续时间末端后是否应从开始处重新播放。如果setLooping(true),则Music对象会持续重复,直到应用程序关闭或调用setLooping(false)为止。

  • setOnCompletionListener:此方法允许我们在Music对象中应用一个监听器,这给了我们待音频完成时执行函数的机会。这是通过向我们的Music对象添加OnCompletionListener来完成的,如下所示:

    mMusic.setOnCompletionListener(new OnCompletionListener(){
      /* In the event that a Music object reaches the end of its duration,
      * the following method will be called
      */
      @Override
      public void onCompletion(MediaPlayer mp) {
      // Do something pending Music completion
      }
    });
    
  • setVolume:使用setVolume(pLeftVolume, pRightVolume)方法,我们可以独立调整左和右立体声通道。音量控制的最低和最高范围等于0.0f(无音量)和1.0f(全音量)。

Sound 对象

以下列表包括为Sound对象提供的方法:

  • setLooping:具体详情请参阅上文Music对象的setLooping方法的描述。此外,Sound对象允许我们使用mSound.setLoopCount(pLoopCount)设置音频轨道循环的次数,其中pLoopCount是一个定义循环次数的int值。

  • setRatesetRate(pRate)方法允许我们定义Sound对象的播放速率或速度,其中pRate等于浮点值表示的速率。默认速率为1.0f,降低速率会降低音频音调,提高速率会增加音频音调。请注意,Android API 文档指出,速率接受的范围在0.5f2.0f之间。超出此范围可能会在播放时产生错误。

  • setVolume:具体详情请参阅上文Music对象的setVolume方法的描述。

注意

对于那些不擅长音频创作的我们来说,有许多免费资源可供使用。网上有许多免费的音频数据库,我们可以在公共项目中使用,例如www.soundjay.com。请注意,大多数免费使用的数据库要求对使用的文件进行署名。

处理不同类型的纹理

了解如何管理纹理应该是每位游戏开发者的主要优先任务之一。当然,仅了解纹理的基础知识也是可以制作游戏的,但长远来看,这很可能会导致性能问题、纹理溢出和其他不希望出现的结果。在本教程中,我们将探讨如何将纹理构建到游戏中,以提供效率,同时减少纹理填充问题出现的可能性。

准备工作

执行本章中给出的了解生命周期教程,以便我们在 IDE 中设置了一个基本的 AndEngine 项目。此外,此教程需要三个 PNG 格式的图像。第一个矩形命名为rectangle_one.png,宽 30 像素,高 40 像素。第二个矩形命名为rectangle_two.png,宽 40 像素,高 30 像素。最后一个矩形命名为rectangle_three.png,宽 70 像素,高 50 像素。将这些矩形图像添加到项目的assets/gfx/文件夹后,继续进行*如何操作…*部分。

如何操作…

在 AndEngine 中构建纹理时涉及两个主要组成部分。在以下步骤中,我们将创建一个所谓的纹理图集,它将存储在准备工作部分提到的三个矩形 PNG 图像中的三个纹理区域。

  1. 此步骤是可选的。我们将BitmapTextureAtlasTextureRegionFactory类指向我们的图像所在的文件夹。默认情况下,工厂指向assets/文件夹。通过在工厂的默认基本路径后附加gfx/,现在它将在assets/gfx/中查找我们的图像:

    BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
    
  2. 接下来,我们将创建BitmapTextureAtlas。纹理图集可以看作是包含许多不同纹理的地图。在这种情况下,我们的“地图”或BitmapTextureAtlas的大小将为 120 x 120 像素:

    // Create the texture atlas at a size of 120x120 pixels
    BitmapTextureAtlas mBitmapTextureAtlas = new BitmapTextureAtlas(mEngine.getTextureManager(), 120, 120);
    
  3. 当我们有了BitmapTextureAtlas可以使用时,现在可以创建我们的ITextureRegion对象,并将它们放置在BitmapTextureAtlas纹理中的特定位置。我们将使用BitmapTextureAtlasTextureRegionFactory类,它帮助我们绑定 PNG 图像到特定的ITextureRegion对象,并在我们上一步创建的BitmapTextureAtlas纹理图集中定义一个位置来放置ITextureRegion对象:

    /* Create rectangle one at position (10, 10) on the mBitmapTextureAtlas */
    ITextureRegion mRectangleOneTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_one.png", 10, 10);
    
    /* Create rectangle two at position (50, 10) on the mBitmapTextureAtlas */
    ITextureRegion mRectangleTwoTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_two.png", 50, 10);
    
    /* Create rectangle three at position (10, 60) on the mBitmapTextureAtlas */
    ITextureRegion mRectangleThreeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_three.png", 10, 60);
    
  4. 最后一步是将我们的ITextureRegion对象加载到内存中。我们可以通过调用包含所述ITextureRegion对象的BitmapTextureAtlas图集来实现这一点:

    mBitmapTextureAtlas.load();
    

工作原理…

在 AndEngine 开发中,为了给我们的项目创建纹理,我们将使用两个主要组件。第一个组件被称为BitmapTextureAtlas,可以将其视为一个具有最大宽度和高度的平面,可以在其宽度和高度范围内存储子纹理。这些子纹理被称为纹理区域,或者具体到 AndEngine 中是ITextureRegion对象。ITextureRegion对象的目的仅是作为对内存中特定纹理的引用,该纹理位于BitmapTextureAtlas图集中的 x 和 y 位置。看待这两个组件的一种方式是想象一块空白的画布,这代表纹理图集,以及一把贴纸,这些将代表纹理区域。画布会有一个最大尺寸,在这个区域内我们可以将贴纸放在任何我们想要的地方。有了这个想法,我们在画布上放置了一把贴纸。现在,我们所有的贴纸都整齐地摆放在这个画布上,我们可以随时取用并放置到任何我们想要的地方。还有一些更细节的内容,但这会在稍后介绍。

了解了BitmapTextureAtlasITextureRegion对象的基础知识之后,创建我们纹理的步骤现在应该更有意义了。如第一步所述,设置BitmapTextureAtlasTextureRegionFactory类的基路径是完全可选的。我们包含这一步只是因为它让我们无需在创建ITextureRegion对象时重复说明我们的图像位于哪个文件夹。例如,如果我们不设置基路径,我们就必须以gfx/rectangle_one.pnggfx/rectangle_two.png等方式引用我们的图像。

在第二步中,我们创建BitmapTextureAtlas对象。这一步相当直接,因为我们只需指定引擎的TextureManager对象来处理纹理加载,以及纹理图集的宽度和高度,按此顺序。由于在这些步骤中我们只处理三个小图像,120x120 像素就非常合适。

关于纹理图集,有一点非常重要,那就是永远不要创建过多的纹理图集;比如,不要为了存放一个 32x32 像素的单个图像而创建一个 256x256 的图集。另一个重要点是,避免创建超过 1024x1024 像素的纹理图集。安卓设备在最大纹理尺寸上各不相同,尽管有些设备可能能够存储高达 2048x2048 像素的纹理,但大量设备的最大限制是 1024x1024。超过最大纹理尺寸将会导致在启动时强制关闭,或者在特定设备上无法正确显示纹理。如果没有其他选择,确实需要大图像,请参考第四章中的背景拼接部分,使用摄像头

在这个食谱的第三步中,我们正在创建我们的ITextureRegion对象。换句话说,我们正在将指定的图像应用到mBitmapTextureAtlas对象上,并定义该图像在图集中的确切位置。使用BitmapTextureAtlasTextureRegionFactory类,我们可以调用createFromAsset(pBitmapTextureAtlas, pContext, pAssetPath, pTextureX, pTextureY)方法,这使得创建纹理区域变得轻而易举。从左到右列出参数的顺序,pBitmapTextureAtlas参数指定了希望存储ITextureRegion对象的纹理图集。pContext参数允许类从gfx/文件夹中打开图像。pAssetPath参数定义了我们正在寻找的特定文件的名称,例如rectangle_one.png。最后的两个参数,pTextureXpTextureY,定义了放置ITextureRegion对象的纹理图集上的位置。以下图像表示在第三步中定义的三个ITextureRegion对象的样子。请注意,代码和图像之间的位置是一致的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在前一个图像中,请注意,每个矩形与纹理边缘之间至少有 10 个像素的间隔。ITextureRegion对象并不是像这样间隔开来以使事物更易于理解,尽管这样做有帮助。实际上,它们是间隔开来的,以便添加所谓的纹理图集源间隔。这种间隔的作用是防止在将纹理应用到精灵时发生纹理重叠。这种重叠被称为纹理溢出。尽管按照本食谱创建的纹理并不能完全消除纹理溢出的可能性,但在将某些纹理选项应用于纹理图集时,它确实降低了这个问题发生的可能性。

想了解更多关于纹理选项的信息,请参阅本章中提供的应用纹理选项食谱。此外,本主题中的*还有更多…*部分描述了创建纹理图集的另一种方法,这种方法完全解决了纹理溢出的问题!强烈推荐。

还有更多内容…

当涉及到将纹理添加到我们的游戏时,我们可以采取多种不同的方法。它们都有自己的优点,有些甚至涉及到负面因素。

BuildableBitmapTextureAtlas

BuildableBitmapTextureAtlas对象是一种将ITextureRegion对象实现到我们的纹理图集中的便捷方式,无需手动定义位置。BuildableBitmapTextureAtlas纹理图集的目的是通过将它们放置到最方便的坐标上来自动放置其ITextureRegion对象。这种创建纹理的方法是最简单且最高效的,因为当构建包含许多纹理图集的大型游戏时,这种方法可能会节省时间,有时甚至可以避免错误。除了BuildableBitmapTextureAtlas的自动化之外,它还允许开发者定义纹理图集源的透明边距,从而消除纹理溢出的任何情况。这是 AndEngine 的 GLES 1.0 分支中最突出的视觉问题之一,因为当时没有内置方法为纹理图集提供边距。

使用BuildableBitmapTextureAtlas图集与BitmapTextureAtlas路径略有不同。以下是使用BuildableBitmapTextureAtlas图集的此食谱代码:

/* Create a buildable bitmap texture atlas - same parameters required
* as with the original bitmap texture atlas */
BuildableBitmapTextureAtlas mBuildableBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 120, 120);

/* Create the three ITextureRegion objects. Notice that when using 
 * the BuildableBitmapTextureAtlas, we do not need to include the final
 * two pTextureX and pTextureY parameters. These are handled automatically! */
ITextureRegion mRectangleOneTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_one.png");
ITextureRegion mRectangleTwoTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_two.png");
ITextureRegion mRectangleThreeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_three.png");

// Buildable bitmap texture atlases require a try/catch statement
try {
  /* Build the mBuildableBitmapTextureAtlas, supplying a BlackPawnTextureAtlasBuilder
    * as its only parameter. Within the BlackPawnTextureAtlasBuilder's parameters, we
    * provide 1 pixel in texture atlas source space and 1 pixel for texture atlas source
    * padding. This will alleviate the chance of texture bleeding.
    */
  mBuildableBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1));
} catch (TextureAtlasBuilderException e) {
  e.printStackTrace();
}

// Once the atlas has been built, we can now load
mBuildableBitmapTextureAtlas.load();

如此代码所示,BuildableBitmapTextureAtlasBitmapTextureAtlas图集之间存在一些细微差别。首先要注意的是,在创建我们的ITextureRegion对象时,我们不再需要指定纹理区域应在纹理图集上的放置位置。使用BuildableBitmapTextureAtlas替代方案时的第二个小变化是,在调用load()方法之前,我们必须在mBuildableBitmapTextureAtlas上调用build(pTextureAtlasBuilder)方法。在build(pTextureAtlasBuilder)方法中,我们必须提供一个BlackPawnTextureAtlasBuilder类,定义三个参数。按顺序,这些参数是pTextureAtlasBorderSpacingpTextureAtlasSourceSpacingpTextureAtlasSourcePadding。在上述代码片段中,我们几乎可以消除所有情况下的纹理溢出可能性。然而,在极端情况下,如果仍有纹理溢出,只需增加第三个参数,这将有助于解决任何问题。

纹理区域块

纹理区域块本质上与普通纹理区域是相同的对象。两者的区别在于,纹理区域块允许我们传递一个图像文件并从中创建一个精灵表。这是通过指定我们精灵表中的列数和行数来完成的。从此,AndEngine 将自动将纹理区域块均匀分布成段。这将允许我们在TiledTextureRegion对象中导航每个段。这就是纹理区域块如何表现为创建具有动画的精灵的样子。

纹理区域块

注意

实际的精灵表不应该在每列和每行周围有轮廓。在上一张图片中它们是为了显示如何将精灵表划分为等分段。

假设前面的图像宽度为 165 像素,高度为 50 像素。由于我们有 11 个单独的列和单行,我们可以像这样创建TiledTextureRegion对象:

TiledTextureRegion mTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(mBitmapTextureAtlas, context,"sprite_sheet.png",11,1);

这段代码的作用是告诉 AndEngine 将sprite_sheet.png图像划分为11个独立的部分,每个部分宽度为 15 像素(因为 165 像素除以 11 个部分等于 15)。现在我们可以使用这个分块纹理区域对象实例化一个带有动画的精灵。

压缩纹理

除了更常见的图像类型(.bmp.jpeg.png),AndEngine 还内置了对 PVR 和 ETC1 压缩纹理的支持。使用压缩纹理的主要好处是它对减少加载时间和可能在游戏过程中提高帧率的影响。就此而言,使用压缩纹理也有缺点。例如,ETC1 不支持在其纹理中使用 alpha 通道。压缩纹理也可能导致纹理质量明显下降。这些类型纹理的使用应与压缩纹理所表示的对象的重要性相关。你很可能不希望将整个游戏的纹理格式基于压缩纹理,但对于大量微妙的图像,使用压缩纹理可以为你的游戏带来明显的性能提升。

另请参阅

  • 本章中的创建资源管理器

  • 本章中的应用纹理选项

应用纹理选项

我们已经讨论了 AndEngine 提供的不同类型的纹理;现在让我们看看我们可以为纹理提供哪些选项。这个主题的内容往往会对我们游戏的质量和性能产生显著影响。

准备就绪

执行本章中提供的处理不同类型的纹理的步骤,以便我们使用BitmapTextureAtlasBuildableBitmapTextureAtlas加载,设置了一个基本的 AndEngine 项目。

如何操作…

为了修改纹理图集的选项和/或格式,我们需要根据是否要定义选项、格式或两者都定义,向BitmapTextureAtlas构造函数中添加一个或两个参数。以下是修改纹理格式和纹理选项的代码:

BitmapTextureAtlas mBitmapTextureAtlas = new BitmapTextureAtlas(mEngine.getTextureManager(), 1024, 1024, BitmapTextureFormat.RGB_565, TextureOptions.BILINEAR);

从这里开始,放置在此特定纹理图集中的所有纹理区域都将应用定义的纹理格式和选项。

工作原理…

AndEngine 允许我们为纹理图集应用纹理选项和格式。应用于纹理图集的各种选项和格式的组合将影响精灵对我们游戏的整体质量和性能影响。当然,如果提到的精灵使用了与修改后的BitmapTextureAtlas图集相关的ITextureRegion对象,情况也是如此。

AndEngine 中可用的基本纹理选项如下:

  • 最近邻:此纹理选项默认应用于纹理图集。这是我们能够应用在纹理图集中的最快性能的纹理选项,但也是质量最差的。这个选项意味着纹理将通过获取与像素最近的纹理元素颜色来应用构成显示的像素的混合。类似于像素代表数字图像的最小元素,**纹理元素(texel)**代表纹理的最小元素。

  • 双线性:AndEngine 中的第二个主要的纹理过滤选项称为双线性纹理过滤。这种方法在性能上会有所下降,但缩放后精灵的质量将提高。双线性过滤获取每个像素的四个最近的纹理元素,以提供更平滑的屏幕图像混合。

请查看以下图表,以比较双线性过滤和最近邻过滤:

工作原理…

这两张图像以最高的位图格式渲染。在这种情况下,最近邻与双线性过滤之间的区别非常明显。在图像的左侧,双线性星星几乎看不到锯齿边缘,颜色非常平滑。在右侧,我们得到了一个使用最近邻过滤渲染的星星。由于锯齿边缘更加明显,质量水平受到影响,如果仔细观察,颜色也不够平滑。

以下是几个额外的纹理选项:

重复:重复纹理选项允许精灵“重复”纹理,假设精灵的大小超出了ITextureRegion对象的宽度和高度。在大多数游戏中,地形通常是通过创建重复纹理并拉伸精灵的大小来生成的,而不是创建许多独立的精灵来覆盖地面。

让我们看看如何创建一个重复纹理:

    /* Create our repeating texture. Repeating textures require width/height which are a power of two */
    BuildableBitmapTextureAtlas texture = new BuildableBitmapTextureAtlas(engine.getTextureManager(), 32, 32, TextureOptions.REPEATING_BILINEAR);

    // Create our texture region - nothing new here
    mSquareTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(texture, context, "square.png");

    try {
      // Repeating textures should not have padding
      texture.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0));
      texture.load();

    } catch (TextureAtlasBuilderException e) {
      Debug.e(e);
    }

之前的代码基于一个 32 x 32 像素的方形图像。创建重复纹理时需要注意的两点是:

  • 使用重复纹理选项格式的纹理图集需要尺寸为 2 的幂(2, 4, 8, 16 等)

  • 如果你使用的是可构建的纹理图集,在build()方法中不要应用填充或间距,因为这在纹理的重复中会被考虑在内,破坏了重复纹理的第一个规则。

接下来,我们需要创建一个使用这种重复纹理的精灵:

/* Increase the texture region's size, allowing repeating textures to stretch up to 800x480 */
ResourceManager.getInstance().mSquareTextureRegion.setTextureSize(800, 480);
// Create a sprite which stretches across the full screen
Sprite sprite = new Sprite(0, 0, 800, 480, ResourceManager.getInstance().mSquareTextureRegion, mEngine.getVertexBufferObjectManager());

我们在这里所做的的是将纹理区域的尺寸增加到 800 x 480 像素。这并不会改变应用了重复选项的纹理图像的大小,而是允许图像最多重复至 800 x 480 像素。这意味着,如果我们创建了一个精灵并提供了重复纹理,我们可以将精灵的尺寸缩放到 800 x 480 像素,同时仍然显示重复效果。然而,如果精灵超出了纹理区域的宽度或高度尺寸,超出区域将不应用纹理。

这是来自设备截图的结果:

工作原理…

预乘透明度:最后,我们有一个选项可以将预乘透明度纹理选项添加到我们的纹理中。这个选项的作用是将每个 RGB 值乘以指定的透明通道,然后在最后应用透明通道。这个选项的主要目的是让我们能够修改颜色的不透明度而不会损失颜色。请记住,直接修改带有预乘透明度值的精灵的透明度值可能会产生不想要的效果。当这个选项应用于透明度为0的精灵时,精灵可能不会完全透明。

当将纹理选项应用到我们的纹理图集时,我们可以选择最近邻或双线性纹理过滤选项。除了这些纹理过滤选项,我们还可以选择重复选项、预乘透明度选项,或者两者都选。

还有更多…

除了纹理选项,AndEngine 还允许我们设置每个纹理图集的纹理格式。纹理格式,类似于纹理选项,通常根据其用途来决定。纹理的格式可以极大地影响图像的性能和质量,甚至比纹理选项更明显。纹理格式允许我们选择纹理图集中 RGB 值的可用颜色范围。根据所使用的纹理格式,我们还可能允许或不允许精灵具有任何透明度值,这会影响纹理的透明度。

纹理格式的命名约定并不复杂。所有格式的名称类似于RGBA_8888,下划线左侧指的是纹理可用的颜色或透明通道。下划线右侧指的是每个颜色通道可用的位数。

纹理格式

以下是可以使用的纹理格式:

  • RGBA_8888:允许纹理使用红色、绿色、蓝色和透明通道,每个通道分配 8 位。由于我们有 4 个通道,每个通道分配 8 位(4 x 8),我们得到一个 32 位的纹理格式。这是这四种格式中最慢的纹理格式。

  • RGBA_4444:允许纹理使用红色、绿色、蓝色和透明通道,每个通道分配 4 位。按照与前一个格式相同的规则,我们得到一个 16 位的纹理格式。与RGBA_8888相比,你会注意到这个格式的改进,因为我们保存的信息量只有 32 位格式的一半。质量将明显受损;请看以下图片:纹理格式

    在这张图片中,我们比较了两种纹理格式的差异。两颗星星都使用默认的纹理选项(最近邻)进行渲染,这与图像的 RGBA 格式无关。我们更感兴趣的是两颗星星的颜色质量。左侧的星星以全 32 位颜色能力进行渲染,右侧的则是 16 位。两颗星星之间的差异相当明显。

  • RGB_565:这是另一种 16 位的纹理格式,不过它不包括透明通道;使用这种纹理格式的纹理将不支持透明度。由于缺乏透明度,这种格式的需求有限,但它仍然很有价值。这种纹理格式的一个使用场景是显示全屏图像,如背景。背景不需要透明度,因此在引入背景时,记住这种格式是明智的。这样节省的性能相当明显。

    提示

    RGB_565格式的颜色质量与之前展示的RGBA_4444星形图像大致相同。

  • A_8:最后,我们来看最后一种纹理格式,它是 8 位的透明通道(不支持颜色)。这也是一种使用范围有限的格式;A_8 格式通常用作具有颜色的精灵的透明遮罩(叠加)。这种格式的一个使用例子是,通过简单地叠加这种纹理的精灵,然后随着时间的推移改变透明度,使屏幕渐变出现或消失。

在创建纹理图集时,考虑哪些类型的精灵将使用哪种类型的纹理区域,并据此将它们打包到纹理图集中是一个好主意。对于较重要的精灵,我们很可能会选择使用RGBA_8888纹理格式,因为这些精灵将是我们游戏的主要焦点。这些对象可能包括前景精灵、主角精灵或屏幕上任何视觉上更突出的物体。背景覆盖了设备整个表面区域,所以我们很可能不需要透明度。对于这些精灵,我们将使用RGB_565以移除透明通道,这将有助于提高性能。最后,我们有那些可能颜色不多、可能较小或只是不需要太多视觉吸引力的物体。对于这类精灵,我们可以使用RGBA_4444纹理格式,以减少这些纹理所需的内存一半。

参见

  • 本章了解生命周期

  • 本章介绍不同类型的纹理的处理方法。

  • 在第二章中,使用实体,介绍了如何通过精灵使场景生动起来

使用 AndEngine 字体资源

AndEngine 字体设置简单,可以包含在我们的Text对象中使用,显示在屏幕上。我们可以选择预设字体,也可以通过assets文件夹添加自己的字体。

准备就绪

执行本章提供的了解生命周期的步骤,这样我们就可以在 IDE 中设置基本的 AndEngine 项目,然后继续阅读*如何操作…*部分。

如何操作…

下面的代码片段展示了创建预设、自定义资源、预设描边和自定义资源描边字体对象的四种不同选项。字体创建应该在BaseGameActivity类的onCreateResources()方法中进行。

  • 预设字体的create()方法如下:

    Font mFont = FontFactory.create(mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL),  32f, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT)
    
    mFont.load();
    
  • 自定义字体的createFromAsset()方法如下:

    Font mFont = FontFactory.createFromAsset(mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, this.getAssets(), "Arial.ttf", 32f, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT); 
    
    mFont.load();
    
  • 描边字体的createStroke()createStrokeFromAsset()方法如下:

    BitmapTextureAtlas mFontTexture = new BitmapTextureAtlas(mEngine.getTextureManager(), 256, 256, TextureOptions.BILINEAR);
    
    Font mFont = FontFactory.createStroke(mEngine.getFontManager(), mFontTexture, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT, 3, org.andengine.util.adt.color.Color.BLACK_ABGR_PACKED_INT);
    
    mFont.load();
    

工作原理…

如我们所见,根据我们希望字体呈现的效果,我们可以采取不同的方法来创建我们的Font对象。然而,所有字体都需要我们定义纹理宽度和纹理高度,无论是直接作为FontFactorycreate方法的参数,还是通过使用BitmapTextureAtlas对象间接定义。在之前的代码片段中,我们使用宽度为256像素、高度为256像素的纹理大小创建了所有三个Font对象。不幸的是,目前还没有简单的方法在运行时自动确定所需的纹理大小,以支持不同的语言、文本大小、描边值或字体样式。

目前,最常见的方法是将纹理宽度和高度设置为大约256像素,然后向上或向下进行小调整,直到纹理大小刚好合适,不会在Text对象中产生伪影。字体大小在确定Font对象所需的最终纹理大小中起着最重要的作用,因此非常大的字体,例如 32 及以上,可能需要更大的纹理大小。

注意

所有Font对象在能够正确显示Text对象中的字符之前,都需要调用load()方法。

让我们看看*如何操作…*部分中介绍的各种方法是如何工作的:

  • create()方法:create()方法不允许太多自定义。从第五个参数开始,这个方法的参数包括提供字体样式、字体大小、抗锯齿选项和颜色。我们使用的是 Android 原生字体类,它只支持几种不同的字体和样式。

  • createFromAsset()方法:我们可以使用这个方法将自定义字体引入到我们的项目中,通过我们的assets文件夹。假设我们有一个叫做Arial.ttf的真类型字体位于项目的assets文件夹中。我们可以看到,一般的创建过程是相同的。在这个方法中,我们必须传递活动的AssetManager类,这可以通过我们活动的getAssets()方法获得。接下来的参数是我们想要导入的真类型字体。

  • createStroke()createStrokeFromAsset()方法:最后,我们有了描边字体。描边字体使我们能够为Text对象中的字符添加轮廓。在这些情况下,当我们希望我们的文本“突出”时,这些字体很有用。为了创建描边字体,我们需要提供一个纹理图集作为第二个参数,而不是传递引擎的纹理管理器。从这个点开始,我们可以通过字体类型或通过我们的assets文件夹来创建描边字体。此外,我们还提供了定义两个新颜色值的选项,这两个值作为最后两个参数添加。有了这些新参数,我们能够调整轮廓的厚度以及颜色。

还有更多…

Font类目前的设置,最好预加载我们期望通过Text对象显示的字符。不幸的是,AndEngine 目前在还有新字母要绘制时仍然调用垃圾回收器,因此为了避免Text对象首次“熟悉”字母时的卡顿,我们可以调用以下方法:

mFont.prepareLetters("abcdefghijklmnopqrstuvwxyz".toCharArray())

此方法调用将准备从 a 到 z 的小写字母。这个方法应该在游戏加载屏幕期间的某个时刻被调用,以避免任何可察觉的垃圾回收。在离开Font对象的话题之前,还有一个重要的类我们应该讨论。AndEngine 包含一个名为FontUtils的类,它允许我们通过measureText(pFont, pText)方法获取关于Text对象在屏幕上的宽度的信息。在处理动态变化的字符串时,这很重要,因为它为我们提供了重新定位Text对象的选项,假设字符串的宽度或高度(以像素为单位)已经改变。

另请参阅

  • 了解本章中的生命周期

  • 在本章中处理不同类型的纹理

  • 在第二章《使用实体》中,将文本应用到图层。

创建资源管理器

在本主题中,我们最终将从更大的角度查看我们的资源。有了资源管理器,我们将能够轻松地通过单一、方便的位置,调用如loadTextures()loadSounds()loadFonts()等方法,来加载游戏需要的不同类型的资源。

准备就绪

请参考代码包中名为ResourceManager的类。

如何操作…

ResourceManager类是以单例设计模式为理念设计的。这允许我们通过简单的调用ResourceManager.getInstance()来全局访问我们游戏的所有资源。ResourceManager类的主要目的是存储资源对象,加载资源,以及卸载资源。以下步骤展示了我们如何使用ResourceManager来处理我们游戏场景之一的纹理。

  1. 声明将在我们游戏的不同场景中使用的所有资源:

    /* The variables listed should be kept public, allowing us easy access
    to them when creating new Sprites, Text objects and to play sound files */
    public ITextureRegion mGameBackgroundTextureRegion;
    public ITextureRegion mMenuBackgroundTextureRegion;
    public Sound  mSound;
    
    public Font  mFont;
    
  2. 提供处理在ResourceManager类中声明的音频、图形和字体资源加载的load方法:

    public synchronized void loadGameTextures(Engine pEngine, Context pContext){
    // Set our game assets folder in "assets/gfx/game/"
        BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/game/");
    
    BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(pEngine.getTextureManager(), 800, 480);
    
    mGameBackgroundTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, pContext, "game_background.png");
    
    try {
      mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1));
      mBitmapTextureAtlas.load();
    } catch (TextureAtlasBuilderException e) {
      Debug.e(e);
    }
    }
    
  3. 第三步涉及提供一个与我们的ResourceManager类的load方法相对应的所有资源的卸载方法:

    public synchronized void unloadGameTextures(){
      // call unload to remove the corresponding texture atlas from memory
      BuildableBitmapTextureAtlas mBitmapTextureAtlas = (BuildableBitmapTextureAtlas) mGameBackgroundTextureRegion.getTexture();
      mBitmapTextureAtlas.unload();
    
      // ... Continue to unload all textures related to the 'Game' scene
    
      // Once all textures have been unloaded, attempt to invoke the Garbage Collector
      System.gc();
    }
    

它是如何工作的…

通过在项目中实现一个ResourceManager类,我们可以轻松地完全独立地加载各种场景资源。因此,我们必须确保我们的public类方法是同步的,以确保我们在一个线程安全的环境中运行。这对于单例的使用尤为重要,因为我们只有一个类实例,有多个线程访问它的可能性。除此之外,现在我们只需要一行代码即可加载场景资源,这极大地帮助我们的主活动类保持更有条理。以下是使用资源管理器时,我们的onCreateResources()方法应该看起来像什么样子:

@Override
public void onCreateResources(
    OnCreateResourcesCallback pOnCreateResourcesCallback) {

  // Load the game texture resources
  ResourceManager.getInstance().loadGameTextures(mEngine, this);

  // Load the font resources
  ResourceManager.getInstance().loadFonts(mEngine);

  // Load the sound resources
  ResourceManager.getInstance().loadSounds(mEngine, this);

  pOnCreateResourcesCallback.onCreateResourcesFinished();
}

在第一步中,我们声明了所有的资源,包括Font对象,ITextureRegion对象,以及Sound/Music对象。在这个特定的示例中,我们只处理有限数量的资源,但在一个功能齐全的游戏中,这个类可能包括 50、75,甚至超过 100 个资源。为了从我们的ResourceManager类中获取资源,我们只需在任何类中包含以下代码行:

ResourceManager.getInstance().mGameBackgroundTextureRegion

在第二步中,我们创建了loadGameTextures(pEngine, pContext)方法,用于加载Game场景的纹理。对于游戏中的每个附加场景,我们应该有一个单独的load方法。这使得可以轻松地动态加载资源。

在最后一步中,我们创建unload方法,处理与每个load方法相对应的资源卸载。然而,如果有任何数量的资源在我们的游戏多个场景中使用,可能需要创建一个没有伴随unload方法的load方法。

还有更多…

在较大的项目中,有时我们可能会发现自己频繁地将主要对象传递给类。资源管理器的另一个用途是存储一些更重要的游戏对象,如EngineCamera。这样我们就不必不断地将这些对象作为参数传递,而可以调用相应的get方法以获取游戏的CameraEngine或我们将在类中引用的任何其他特定对象。

另请参阅

  • 在本章中引入声音和音乐

  • 在本章中处理不同类型的纹理

  • 在本章中使用 AndEngine 字体资源

保存和加载游戏数据

在游戏结构章节的最后一个主题中,我们将设置一个可以在项目中使用的类来管理和设置数据。我们必须保存的更明显的游戏数据应该包括角色状态、高分和其他可能在我们的游戏中包含的各种数据。我们还应该跟踪游戏可能具有的某些选项,例如用户是否静音、血腥效果等。在这个示例中,我们将使用一个名为SharedPreferences的类,它将允许我们轻松地将数据保存到设备上,以便在稍后的时间检索。

注意

SharedPreferences类是快速存储和检索原始数据类型的一种很好的方式。然而,随着数据量的增加,我们用来存储数据的方法的需求也会增加。如果我们的游戏确实需要存储大量数据,可以考虑使用 SQLite 数据库来存储数据。

准备工作

请参考代码包中名为UserData的类。

如何操作…

在这个示例中,我们设置了一个名为UserData的类,该类将存储一个布尔变量以决定是否静音,以及一个int变量,该变量将定义用户已解锁的最高级别。根据游戏的需求,可能需要在类中包含更多或更少的数据类型,无论是最高分、货币还是其他与游戏相关的数据。以下步骤描述了如何设置一个类,在用户的设备上包含和存储用户数据:

  1. 第一步涉及声明我们的常量String变量,这些变量将保存对我们偏好文件的引用,以及保存对偏好文件内部数据引用的“键”名称,以及相应的“值”变量。此外,我们还声明了SharedPreferences对象以及一个编辑器:

    // Include a 'filename' for our shared preferences
    private static final String PREFS_NAME = "GAME_USERDATA";
    
    /* These keys will tell the shared preferences editor which
      data we're trying to access */
    
    private static final String UNLOCKED_LEVEL_KEY = "unlockedLevels";
    private static final String SOUND_KEY = "soundKey";
    
    /* Create our shared preferences object & editor which will
     be used to save and load data */
    private SharedPreferences mSettings;
    private SharedPreferences.Editor mEditor;
    
    // keep track of our max unlocked level
    private int mUnlockedLevels;
    
    // keep track of whether or not sound is enabled
    private boolean mSoundEnabled;
    
  2. 为我们的SharedPreferences文件创建一个初始化方法。这个方法将在我们的游戏首次启动时被调用,如果不存在,则为我们的游戏创建一个新文件,如果存在,则从偏好文件加载现有值:

    public synchronized void init(Context pContext) {
      if (mSettings == null) {
        /* Retrieve our shared preference file, or if it's not yet
          * created (first application execution) then create it now
          */
        mSettings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        /* Define the editor, used to store data to our preference file
         */
        mEditor = mSettings.edit();
    
        /* Retrieve our current unlocked levels. if the UNLOCKED_LEVEL_KEY
          * does not currently exist in our shared preferences, we'll create
          * the data to unlock level 1 by default
          */
        mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1);
    
        /* Same idea as above, except we'll set the sound boolean to true
          * if the setting does not currently exist
          */
        mSoundEnabled = mSettings.getBoolean(SOUND_KEY, true);
      }
    }
    
  3. 接下来,我们将为那些打算存储在SharedPreferences文件中的每个值提供获取方法,以便我们可以在整个游戏中访问数据:

    /* retrieve the max unlocked level value */
    public synchronized int getMaxUnlockedLevel() {
      return mUnlockedLevels;
    }
    
  4. 最后,我们必须为那些打算存储在SharedPreferences文件中的每个值提供设置方法。设置方法将负责将数据保存到设备上:

    public synchronized void unlockNextLevel() {
      // Increase the max level by 1
      mUnlockedLevels++;
    
      /* Edit our shared preferences unlockedLevels key, setting its
       * value our new mUnlockedLevels value
        */
      mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels);
    
      /* commit() must be called by the editor in order to save
        * changes made to the shared preference data
       */
      mEditor.commit();
    }
    

工作原理…

这个类展示了我们如何通过使用SharedPreferences类轻松地存储和检索游戏的数据和选项。UserData类的结构相当直接,可以以相同的方式使用,以便适应我们可能想要在游戏中包含的各种其他选项。

在第一步中,我们只是开始声明所有必要的常量和成员变量,这些变量我们将用于处理游戏中的不同类型的数据。对于常量,我们有一个名为PREFS_NAMEString变量,它定义了游戏的偏好文件的名称,还有另外两个String变量,它们将分别作为对偏好文件中单个原始数据类型的引用。对于每个键常量,我们应该声明一个相应的变量,当数据第一次加载时,偏好文件数据将存储到这个变量中。

在第二步中,我们提供了从游戏的偏好文件中加载数据的方法。这个方法只需要在游戏启动过程中调用一次,以将SharedPreferences文件中的数据加载到UserData类的成员变量中。首先调用context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE),我们检查是否有针对我们的应用程序在PREFS_NAME字符串下的SharedPreference文件,如果没有,那么我们将创建一个新的文件——MODE_PRIVATE,意味着该文件对其他应用程序不可见。

一旦完成,我们可以从偏好文件中调用获取器方法,如mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1)。这将偏好文件中UNLOCKED_LEVEL_KEY键的数据传递给mUnlockedLevels。如果游戏的偏好文件当前没有为定义的键保存任何值,那么默认值1将被传递给mUnlockedLevels。这将针对UserData类处理的每种数据类型继续进行。在这种情况下,只是关卡和声音。

在第三步中,我们设置了对UserData类处理的每种数据类型相对应的获取器方法。这些方法可以在游戏中的任何地方使用;例如,在关卡加载时,我们可以调用UserData.getInstance().isSoundMuted(),以确定是否应该对Music对象调用play()

在第四步中,我们创建了将数据保存到设备的方法。这些方法非常直接,无论我们处理哪种数据,它们都应该相当相似。我们可以从参数中获取一个值,如setSoundMuted(pEnableSound),或者简单地递增,如unlockNextLevel()中所示。

当我们最终想要将数据保存到设备上时,我们使用mEditor对象,使用适合我们要存储的原始数据类型的方法,指定存储数据的键以及值。例如,对于关卡解锁,我们使用方法mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels),因为我们正在存储一个int变量。对于boolean变量,我们调用putBoolean(pKey, pValue),对于String变量,我们调用putString(pKey, pValue),依此类推。

还有更多…

不幸的是,在客户端设备上存储数据时,无法保证用户不会访问数据以进行操纵。在 Android 平台上,大多数用户无法访问保存我们游戏数据的SharedPreferences文件,但是拥有 root 权限的用户则能够查看该文件并根据需要做出修改。为了解释的方便,我们使用了明显的键名,比如soundKeyunlockedLevels。使用某种形式的混淆可以帮助让文件对于偶然在 root 设备上发现游戏数据的普通用户来说更像是一堆乱码。

如果我们想要进一步保护游戏数据,那么更为安全的做法是对偏好设置文件进行加密。Java 的javax.crypto.*包是一个不错的起点,但请记住,加密和解密确实需要时间,这可能会增加游戏加载时间。

第二章:使用实体

在本章中,我们将开始探讨如何在屏幕上显示对象以及我们可以处理这些对象的多种方式。主题包括:

  • 理解 AndEngine 实体

  • 将原始图形应用到图层

  • 使用精灵为场景注入生命

  • 将文本应用到图层

  • 使用相对旋转

  • 重写onManagedUpdate方法

  • 使用修饰符和实体修饰符

  • 使用粒子系统

引言

在本章中,我们将开始使用 AndEngine 中包含的所有精彩的实体。实体为我们提供了一个基础,游戏世界中显示的每个对象都将依赖它,无论是分数文本、背景图像、玩家的角色、按钮以及所有其他内容。可以这样想,通过 AndEngine 的坐标系统,我们游戏中任何可以放置的对象在最基本的层面上都是一个实体。在本章中,我们将开始使用Entity对象及其许多子类型,以便在我们的游戏中充分利用它们。

理解 AndEngine 实体

AndEngine 游戏引擎遵循实体-组件模型。实体-组件设计在当今许多游戏引擎中非常普遍,这有充分的理由。它易于使用,模块化,并且在所有游戏对象都可以追溯到单一的、最基本的Entity对象的程度上非常有用。实体-组件模型可以被认为是游戏引擎对象系统最基本级别的“实体”部分。Entity类只处理我们游戏对象依赖的最基本数据,比如位置、旋转、颜色、与场景的附加和分离等。而“组件”部分指的是Entity类的模块化子类型,比如SceneSpriteTextParticleSystemRectangleMesh以及所有可以放入我们游戏中的其他对象。组件旨在处理更具体的任务,而实体则作为所有组件依赖的基础。

如何操作…

为了从最基础的Entity方法开始,我们将一个Entity对象附加到Scene对象上:

创建并将一个Entity对象附加到Scene对象只需要以下两行代码:

Entity layer = new Entity();
mScene.attachChild(layer);

工作原理…

这里给出的两行代码允许我们创建一个基本的Entity对象并将其附加到我们的Scene对象上。正如本食谱中*如何操作…*一节所定义的,一个Entity对象通常被用作图层。接下来几段将会讨论图层的用途。

实体在游戏开发中非常重要。在 AndEngine 中,事实是,我们场景上显示的所有对象都源自实体(包括Scene对象本身!)。在大多数情况下,我们可以假设实体要么是场景上视觉显示的对象,如SpriteTextRectangle对象,要么是一个层,如Scene对象。由于Entity类的广泛性,我们将分别讨论实体的两种用途,好像它们是不同的对象。

实体的第一个,也可能是最重要的方面是分层能力。在游戏设计中,层是一个非常简单的概念;然而,由于游戏在游戏过程中倾向于支持大量的实体,在初次了解它们时,事情可能会很快变得混乱。我们必须将层视为一个具有一个父级和无限数量的子级的对象,除非另有定义。顾名思义,层的目的在于以有组织的方式将我们的各种实体对象应用到场景上,幸运的是,这也使我们能够对层执行一个会影响其所有子级一致的动作,例如,重新定位和施加某些实体修饰符。我们可以假设,如果我们有一个背景、一个中景和一个前景,那么我们的游戏将会有三个独立的层。这三个层将根据它们附加到场景的顺序以特定的顺序出现,就像将纸张堆叠在一起一样。如果我们从上往下看这个堆叠的纸张,最后添加到堆栈中的纸张将出现在其余纸张的前面。对于附加到Scene对象的Entity对象,同样的规则适用;这在前面的图片中显示:

它是如何工作的…

前面的图片描绘了一个由三个Entity对象层组成的基本游戏场景。这三个层都有特定的目的,即按照深度存储所有相关实体。首先应用到场景的是背景层,包括一个包含蓝天和太阳的精灵。接着应用到场景的是中景层。在这个层上,我们会找到与玩家相关的对象,包括玩家行走的景观、可收集的物品、敌人等等。最后,我们有了前景层,用于在设备的显示屏上显示最前面的实体。在所展示的图中,前景层用于显示用户界面,包括一个按钮和两个Text对象。

让我们再次看看一个带有子实体附加层的场景可能是什么样子:

它是如何工作的…

这张图显示了场景如何在屏幕上显示实体的深度/层次。在图的底部,我们有设备的显示。我们可以看到背景层首先附属于场景,然后是玩家层。这意味着附属于背景的实体将位于玩家层子实体的后面。记住这一点,这个规则同样适用于子实体。首先附着在层上的子实体在深度上将会位于任何随后附着物体的后面。

最后,关于一般 AndEngine 实体的一个最后一个关键主题是实体组合。在继续之前,我们应该了解的一个事实是*子实体继承父实体的值!*这是许多新的 AndEngine 开发者在设置游戏中的多层时遇到问题的地方。从倾斜、缩放、位置、旋转、可见性等所有属性,当父实体的属性发生变化时,子实体都会考虑在内。查看下面的图,它展示了 AndEngine 中实体的位置组合:

工作原理…

首先,我们应该了解在 AndEngine 的锚点中心分支中,坐标系统是从实体的左下角开始的。增加 x 值会将实体位置向右移动,增加 y 值会将实体位置向上移动。减少 x/y 值则会产生相反的效果。有了这个概念,我们可以看到附属于场景的较大矩形在场景上的位置被设定为坐标**(6, 6)。由于较小矩形附属于较大矩形,而不是相对于场景的坐标系统,它实际上是使用大矩形的坐标系统。这意味着小矩形的锚点中心位置将直接位于大矩形坐标系统的(0, 0)位置上。正如我们在前一张图片中看到的,大矩形坐标系统上的(0, 0)**位置是其左下角。

注意

旧的 AndEngine 分支与 AndEngine 最新的锚点中心分支之间的主要区别在于,定位实体不再意味着我们将实体的左上角设置在坐标系统上的一个位置。相反,实体的中心点将被放置在定义的位置上,这也在前面的图中有所展示。

还有更多…

AndEngine 中的 Entity 对象包含许多不同的方法,这些方法影响实体的许多方面。这些方法在塑造 Entity 对象的整体特性方面发挥着至关重要的作用,无论实体的子类型如何。为了完全控制实体的外观、反应、存储信息等,了解如何操作实体是一个好主意。使用以下列表来熟悉 Entity 对象的一些最重要的方法及其相应的获取方法。本章及后续章节将详细介绍此列表中未提及的方法。

  • setVisible(pBoolean)isVisible(): 这个方法可以用来设置实体是否在场景中可见。将这些方法设置为 true 将允许实体渲染,设置为 false 将禁用渲染。

  • setChildrenVisible(pBoolean)isChildrenVisible(): 类似于 setVisible(pBoolean) 方法,不同之处在于它定义了调用实体的子实体的可见性,而不是自身。

  • setCullingEnabled(pBoolean)isCullingEnabled(): 实体剔除可能是一种非常有前景的性能优化技术。更多详情请参见第八章中的通过实体剔除禁用渲染最大化性能

  • collidesWith(pOtherEntity): 这个方法用于检测调用此方法的实体与作为此方法参数提供的Entity对象发生碰撞或重叠时。如果实体正在碰撞,此方法将返回 true

  • setIgnoreUpdate(pBoolean)isIgnoreUpdate(): 忽略实体更新可以提供明显的性能提升。更多详情请参见第八章中的忽略实体更新最大化性能

  • setChildrenIgnoreUpdate(pBoolean)isChildrenIgnoreUpdate(): 类似于 setIgnoreUpdate(pBoolean) 方法,不同之处在于它只影响调用实体的子实体,而不是自身。

  • getRootEntity(): 这个方法将遍历实体的父实体,直到找到根父实体。找到根父实体后,此方法将返回根 Entity 对象;在大多数情况下,根是我们的游戏 Scene 对象。

  • setTag(pInt)getTag(): 这个方法可以用来在实体中存储整数值。通常用于为实体设置标识值。

  • setParent(pEntity)hasParent(): 将父实体设置为调用此方法的实体。hasParent() 方法根据调用实体是否有父实体返回 truefalse 值。

  • setZIndex(pInt)getZIndex(): 设置调用实体的 Z 索引。值较大的实体将出现在值较小的实体前面。默认情况下,所有实体的 Z 索引都是 0,这意味着它们将按照附加的顺序出现。更多详情请参见下面的 sortChildren() 方法。

  • sortChildren(): 在对实体或实体组的 Z 索引进行修改后,必须在它们的父对象上调用此方法,修改后的效果才能在屏幕上显示。

  • setPosition(pX, pY)setPosition(pEntity): 此方法用于将实体的位置设置为特定的 x/y 值,或者可以用来设置到另一个实体的位置。此外,我们可以使用 setX(pX)setY(pY) 方法仅对单个轴的位置进行更改。

  • getX()getY(): 这些方法用于获取实体的本地坐标位置;即相对于其父对象的位置。

  • setWidth(pWidth)setHeight(pHeight)setSize(pWidth, pHeight): 这些方法用于设置调用实体的宽度和高度。此外,我们还可以使用 getWidth()getHeight() 方法,它们将返回各自值作为浮点数据类型。

  • setAnchorCenter(pAnchorCenterX, pAnchorCenterY): 此方法用于设置实体的锚点中心。锚点中心是 Entity 对象内部的一个位置,实体将围绕它旋转、倾斜和缩放。此外,修改锚点中心值将重新定位实体的“定位”锚点,从默认的中心点移动。例如,如果我们把锚点中心位置移动到实体的左上角,调用 setPosition(0,0) 将会把实体的左上角放置在位置 (0,0)

  • setColor(pRed, pGreen, pBlue)getColor(): 此方法用于设置实体的颜色,颜色值从 0.0f(无颜色)到 1.0f(全颜色)不等。

  • setUserData(pObject)getUserData(): 这两个方法在开发 AndEngine 游戏时非常有用。它们允许我们在实体中存储我们选择的任何对象,并在任何时刻修改或检索它。用户数据存储的一个可能用途是确定玩家角色持有什么类型的武器。充分利用这些方法吧!

将原始图形应用于图层

AndEngine 的原始类型包括 LineRectangleMeshGradient 对象。在本主题中,我们将重点关注 Mesh 类。Mesh 对象对于创建游戏中更为复杂的形状非常有用,其应用场景无限广阔。在本教程中,我们将使用 Mesh 对象来构建如下所示的房屋:

将原始图形应用于图层

准备工作…

请参考代码包中名为 ApplyingPrimitives 的类。

如何操作…

为了创建一个Mesh对象,我们需要比典型的RectangleLine对象做更多的工作。使用Mesh对象有很多好处。它们可以帮助我们加强 OpenGL 坐标系统的技能,我们可以创建形状奇特的原始物体,并且能够改变单个顶点的位置,这对于某些类型的动画来说非常有用。

  1. 创建Mesh对象的第一步是创建我们的缓冲数据,这些数据用于指定构成网格形状的点:

      float baseBufferData[] = {
          /* First Triangle */
          0, BASE_HEIGHT, UNUSED, /* first point */
          BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */
          BASE_WIDTH, 0, UNUSED, 	/* third point */
    
          /* Second Triangle */
          BASE_WIDTH, 0, UNUSED, /* first point */
          0, 0, UNUSED, /* second point */
          0, BASE_HEIGHT, UNUSED, /* third point */
      };
    
  2. 一旦缓冲数据配置完成,我们就可以继续创建Mesh对象。

    Mesh baseMesh = new Mesh((WIDTH * 0.5f) - (BASE_WIDTH * 0.5f), 0, baseBufferData, baseBufferData.length / POINTS_PER_TRIANGLE, DrawMode.TRIANGLES, mEngine.getVertexBufferObjectManager());
    

它是如何工作的…

让我们进一步分解这个过程,以了解我们是如何使用原始Mesh对象制作房屋的。

在第一步中,我们创建baseMesh对象的缓冲数据。这个缓冲数据用于存储 3D 空间中的点。缓冲数据中每三个值,由换行符分隔,构成 3D 世界中的一个顶点。但是,应该明白,由于我们使用的是 2D 游戏引擎,第三个值,即Z索引,对我们来说是没有用的。因此,我们将每个顶点的第三个值定义为该食谱类中声明的UNUSED常量,等于0。每个三角形的点表示为(x, y, z),以避免混淆顺序。请参阅以下图表,了解第一步中定义的点如何绘制到网格上的矩形:

它的工作原理…

前一个图表展示了在*如何操作…*部分第一步中看到的baseMesh对象的缓冲数据,或者说是绘制点。黑色线条代表第一组点:

      0, BASE_HEIGHT, UNUSED, /* first point */
      BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */
      BASE_WIDTH, 0, UNUSED,  /* third point */

baseMesh对象缓冲数据中的第二组点由灰色线条表示:

      BASE_WIDTH, 0, UNUSED, /* first point */
      0, 0, UNUSED, /* second point */
      0, BASE_HEIGHT, UNUSED, /* third point */

由于BASE_HEIGHT等于200BASE_WIDTH等于400,我们可以读取到第一个三角形的第一个点(0, BASE_HEIGHT)位于矩形形状的左上角。顺时针移动,第一个三角形的第二个点位于(BASE_WIDTH, BASE_HEIGHT)的位置,这将是矩形形状的右上角。显然,一个三角形由三个点组成,所以这让我们还有一个顶点要绘制。我们第一个三角形的最后一个顶点位于(BASE_WIDTH, 0)的位置。作为一个个人挑战,使用前一个图中的场景图,找出灰色三角形的绘制点与缓冲数据相比如何!

在第二步中,我们将baseMesh对象的缓冲区数据用来构建Mesh对象。Mesh对象是Entity类的一个子类型,因此一旦我们创建了Mesh对象,就可以对其进行重新定位、缩放、旋转以及进行其他必要的调整。按照构造函数中出现的顺序,参数如下:x 轴位置、y 轴位置、缓冲区数据、顶点数量、绘制模式和顶点缓冲对象管理器。前两个参数和最后一个参数对所有实体都是典型的,但缓冲区数据、顶点数量和绘制模式对我们来说是新的。缓冲区数据是数组,它指定了已绘制的顶点,这在第一步中已经介绍过。顶点数量只是缓冲区数据中包含的顶点数。我们缓冲数据中的每一个 x、y、z 坐标组成一个单独的顶点,这就是为什么我们用baseBufferData.length值除以三来得到这个参数。最后,DrawMode定义了Mesh对象将如何解释缓冲区数据,这可以极大地改变网格的最终形状。不同的DrawMode类型和用途可以在本主题的*还有更多…*部分中找到。

在继续之前,您可能会注意到“门”,或者更确切地说,代表门的蓝色线条并不是以与屋顶和基础Mesh对象相同的方式创建的。相反,我们使用线条而不是三角形来绘制门的外框。请查看以下代码,它来自doorBufferData数组,定义了线条连接的点:

      0, DOOR_HEIGHT, UNUSED, /* first point */
      DOOR_WIDTH, DOOR_HEIGHT, UNUSED, /* second point */
      DOOR_WIDTH, 0, UNUSED, /* third point */
      0, 0, UNUSED, /* fourth point */
      0, DOOR_HEIGHT, UNUSED /* fifth point */

再次,如果我们绘制一个场景图,并像之前代表baseMesh对象点的图那样标出这些点,我们实际上可以连接这些点,线条将形成一个矩形形状。一开始可能会让人感到困惑,尤其是在试图在脑海中创建形状时。从定义的顶点开始绘制自定义形状的诀窍是,在您喜欢的文档或图像编辑软件中保存一个空白场景图。创建一个类似于baseMesh对象缓冲数据表示图的场景图,并使用它来标出点,然后简单地将点复制到代码中!

注意事项

需要特别记住的是,在之前场景图中的(0,0)位置代表了Mesh对象的中心。由于我们是向上和向右构建网格顶点,网格的锚定中心位置将不代表手动绘制的形状的中心!在构建Mesh对象时,这一点非常重要。

还有更多…

对于初学者来说,创建网格可能是一个相当令人畏惧的主题,但有很多原因让我们习惯它们。AndEngine 开发者们的一个主要原因是它可以帮助我们理解 OpenGL 在较低层次上如何将形状绘制到显示上,这反过来又使我们更容易掌握更高层次的游戏开发功能。以下图片包含了 AndEngine 为我们提供的各种DrawMode类型,以便以不同方式创建Mesh对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前图展示了我们的缓冲数据中的顶点将如何根据所选的DrawMode类型由Mesh对象绘制到场景中。此图中的每个**p#**代表我们缓冲数据数组中的顶点(x,y 和 z 值)。以下是每个DrawMode类型的图像表示的解释:

  • DrawMode.POINTS:这种选择允许我们在网格的缓冲数据中为每个顶点绘制单独的点。这些点不会由任何线条连接;它们仅仅在网格上为每个点显示一个点。

  • DrawMode.LINES:这种选择允许我们在网格上绘制单独的线条。每两个顶点将由线条连接。

  • DrawMode.LINE_STRIP:这种选择允许我们在网格上绘制点,第一个点之后的每个点都连接到前一个点。例如,p1将连接到p0p2将连接到p1,依此类推。

  • DrawMode.LINE_LOOP:这种选择与DrawMode.LINE_STRIP类型类似,但是,第一个点与最后一个点也会由线条连接。这允许我们通过线条创建闭合的形状。

  • DrawMode.TRIANGLES:这种选择允许我们在网格上为缓冲数据中定义的每组三个顶点绘制单独的三角形。这种绘制模式要求我们将顶点保持在三的倍数。

  • DrawMode.TRIANGLE_FAN:这种选择允许我们绘制锥形或金字塔形状的网格。正如在之前的图中可以看到的,我们首先指定一个点,定义锥形的顶部点,然后继续指定形状的底部点。这种绘制模式需要定义三个或更多的顶点在缓冲数据中。

  • DrawMode.TRIANGLE_STRIP:这种选择使我们能够轻松创建自定义的多边形网格。在初始化三角形的第三个顶点之后,缓冲数据中定义的每个顶点都会生成一个新的三角形,创建一个新的“带”。请参阅图表示例。这种绘制模式需要定义三个或更多的顶点在缓冲数据中。

另请参阅

  • 本章节中提供的了解 AndEngine 实体

使用精灵为场景带来生机

我们在这里讨论的可能是创建任何 2D 游戏最必要的一个方面。精灵(Sprites)允许我们在场景中显示 2D 图像,这些图像可以用来展示按钮、角色/化身、环境主题、背景以及游戏中可能需要通过图像文件来表示的任何其他实体。在本教程中,我们将介绍 AndEngine 的Sprite实体的各个方面,这将为我们提供在以后更复杂的情况下继续使用Sprite对象所需的信息。

准备工作…

在深入了解精灵如何创建的内部工作机制之前,我们需要了解如何创建和管理 AndEngine 的BitmapTextureAtlas/BuildableBitmapTextureAtlas对象以及ITextureRegion对象。更多信息,请参考第一章,AndEngine 游戏结构中的教程,使用不同类型的纹理应用纹理选项

阅读完这些教程后,创建一个新的空 AndEngine 项目,使用BaseGameActivity类,提供一个尺寸最大为 1024 x 1024 像素的 PNG 格式图像,将其命名为sprite.png并放在项目的assets/gfx/文件夹中,然后继续本教程的*如何操作…*部分。

如何操作…

我们只需几个快速步骤就可以创建并将精灵应用到我们的Scene对象中。我们首先必须设置精灵将使用的必要纹理资源,创建Sprite对象,然后必须将Sprite对象附加到我们的Scene对象。以下步骤将提供更多详细信息:

  1. 我们将从在BaseGameActivity类的onCreateResources()方法中创建纹理资源开始。确保mBitmapTextureAtlasmSpriteTextureRegion对象是全局变量,这样它们就可以在活动的各种生命周期方法中被访问:

      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
    
        /* Create the bitmap texture atlas for the sprite's texture region */
        BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 256, 256, TextureOptions.BILINEAR);
    
        /* Create the sprite's texture region via the BitmapTextureAtlasTextureRegionFactory */
        mSpriteTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "sprite.png");
    
        /* Build the bitmap texture atlas */
        try {
          mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1));
        } catch (TextureAtlasBuilderException e) {
          e.printStackTrace();
        }
        /* Load the bitmap texture atlas into the device's gpu memory */
        mBitmapTextureAtlas.load();
    
  2. 接下来,我们将创建Sprite对象。我们可以在活动的onCreateScene()onPopulateScene()方法中创建并附加Sprite对象到Scene对象。在它的构造函数中需要提供的参数包括,按此顺序,精灵的初始 x 坐标、初始 y 坐标、ITextureRegion对象,最后是mEngine对象的顶点缓冲区管理器:

        final float positionX = WIDTH * 0.5f;
        final float positionY = HEIGHT * 0.5f;
    
        /* Add our marble sprite to the bottom left side of the Scene initially */
        Sprite mSprite = new Sprite(positionX, positionY, mSpriteTextureRegion, mEngine.getVertexBufferObjectManager());
    The last step is to attach our Sprite to the Scene, as is necessary in order to display any type of Entity on the device's display:
        /* Attach the marble to the Scene */
        mScene.attachChild(mSpriteTextureRegion);
    

它的工作原理…

如前一部分的步骤所示,实际上设置mBitmapTextureAtlasmSpriteTextureRegion对象比专门创建和设置mSprite对象需要更多的工作。因此,建议在开始之前先完成*入门…*部分提到的两个教程。

在第一步中,我们将创建适合我们sprite.png图像需求的mBitmapTextureAtlasmSpriteTextureRegion对象。在这一步中,请随意使用任何纹理选项或纹理格式。很好地了解它们是非常有想法的。

一旦我们创建了ITextureRegion对象并且它已经准备好使用,我们可以进入第二步,创建Sprite对象。创建一个精灵是一个直接的任务。前两个参数将用于定义精灵的初始位置,相对于其中心点。对于第三个参数,我们将传递在第一步中创建的ITextureRegion对象,以便为场景中的精灵提供图像外观。最后,我们传递mEngine.getVertexBufferObjectManager()方法,这是大多数实体子类型所必需的。

一旦我们的Sprite对象被创建,我们必须在它能在设备上显示之前将它附加到Scene对象,或者我们可以将它附加到已经连接到Scene对象的另一个Entity对象上。关于实体组合、放置以及其他各种必须了解的Entity对象方面,请参阅本章中提供的了解 AndEngine 实体食谱。

还有更多内容…

没有某种形式的精灵动画,游戏是不完整的。毕竟,玩家只能在游戏中返回这么多次,之后就会对那些角色在屏幕上滑动而不动脚、攻击敌人时不挥舞武器,或者手榴弹只是消失而不是产生漂亮的爆炸效果的游戏感到厌倦。在这个时代,人们想要玩看起来和感觉都很棒的游戏,而像黄油般平滑的动画精灵没有什么能比得上“好极了!”,不是吗?

在第一章,AndEngine 游戏结构中的使用不同类型的纹理食谱中,我们介绍了如何创建一个TiledTextureRegion对象,该对象允许我们将可用的精灵表作为纹理导入到游戏中。现在让我们找出如何使用TiledTextureRegion对象与AnimatedSprite对象为游戏的精灵添加动画。在这个演示中,代码将处理一个尺寸为 300 x 50 像素的图像。精灵表可以是如图所示的那样简单,以了解如何创建动画:

还有更多...

前图中的精灵表可用于创建一个有 12 列 1 行的TiledTextureRegion对象。为这个精灵表创建BuildableBitmapTextureAtlasTiledTextureRegion对象可以使用以下代码。但是,在导入这段代码之前,请确保在测试项目中全局声明纹理区域—TiledTextureRegion mTiledTextureRegion

    /* Create the texture atlas at the same dimensions as the image (300x50)*/
    BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 300, 50, TextureOptions.BILINEAR);

    /* Create the TiledTextureRegion object, passing in the usual parameters,
     * as well as the number of rows and columns in our sprite sheet for the 
     * final two parameters */
    mTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(mBitmapTextureAtlas, this, "gfx/sprite_sheet.png", 12, 1);

    /* Build and load the mBitmapTextureAtlas object */
    try {
      mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0));
    } catch (TextureAtlasBuilderException e) {
      e.printStackTrace();
    }
    mBitmapTextureAtlas.load();

既然我们的项目中已经有了可以操作的mTiledTextureRegion精灵表,我们可以创建并动画化AnimatedSprite对象。如果你使用的是如图所示带有黑色圆圈的精灵表,别忘了将Scene对象的颜色改为非黑色,这样我们才能看到AnimatedSprite对象:

    /* Create a new animated sprite in the center of the scene */
    AnimatedSprite animatedSprite = new AnimatedSprite(WIDTH * 0.5f, HEIGHT * 0.5f, mTiledTextureRegion, mEngine.getVertexBufferObjectManager());

    /* Length to play each frame before moving to the next */
    long frameDuration[] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200};

    /* We can define the indices of the animation to play between */
    int firstTileIndex = 0;
    int lastTileIndex = mTiledTextureRegion.getTileCount();

    /* Allow the animation to continuously loop? */
    boolean loopAnimation = true;

    * Animate the sprite with the data as set defined above */
    animatedSprite.animate(frameDuration, firstTileIndex, lastTileIndex, loopAnimation, new IAnimationListener(){

      @Override
      public void onAnimationStarted(AnimatedSprite pAnimatedSprite,
          int pInitialLoopCount) {
        /* Fired when the animation first begins to run*/
      }

      @Override
      public void onAnimationFrameChanged(AnimatedSprite pAnimatedSprite,
          int pOldFrameIndex, int pNewFrameIndex) {
         /* Fired every time a new frame is selected to display*/
      }

      @Override
      public void onAnimationLoopFinished(AnimatedSprite pAnimatedSprite,
          int pRemainingLoopCount, int pInitialLoopCount) {
        /* Fired when an animation loop ends (from first to last frame) */
      }

      @Override
      public void onAnimationFinished(AnimatedSprite pAnimatedSprite) {
        /* Fired when an animation sequence ends */
      }
      );

    mScene.attachChild(animatedSprite);

创建AnimatedSprite对象可以按照本食谱中创建常规Sprite对象的步骤进行。一旦创建完成,我们就可以设置其动画数据,包括单个帧的持续时间、要动画化的第一块和最后一块图块索引,以及是否要连续循环动画。注意,frameDuration数组必须等于帧数!不遵循此规则将导致抛出IllegalArgumentException异常。数据设置完成后,我们可以在AnimatedSprite对象上调用animate()方法,提供所有数据,并在需要时添加IAnimationListener监听器。正如监听器中的注释所示,通过 AndEngine 的AnimatedSprite类,我们对动画的控制能力得到了大幅提升。

使用 OpenGL 的抖动功能

在移动平台上开发视觉上吸引人的游戏时,我们很可能会希望图像中包含一些渐变,特别是在处理 2D 图形时。渐变非常适合创建光照效果、阴影以及许多其他无法应用于完整 2D 世界的对象。问题在于,我们是为移动设备开发,因此不幸的是,我们无法使用无限量的资源。因此,AndEngine 默认将表面视图的颜色格式下采样为RGB_565。无论我们在纹理中定义的纹理格式如何,它们在设备上显示之前总是会被下采样。我们可以更改应用于 AndEngine 表面视图的颜色格式,但在开发包含许多精灵的大型游戏时,性能损失可能不值得。

这里,我们有两张具有渐变纹理的简单精灵的独立屏幕截图;这两种纹理都使用了RGBA_8888纹理格式和BILINEAR纹理过滤(最高质量)。

使用 OpenGL 的抖动功能

右侧的图像未经任何修改就应用到了Scene对象上,而左侧的图像启用了 OpenGL 的抖动功能。这两张其他方面相同的图像之间的差异立即显而易见。抖动是我们对抗表面视图应用的降采样的一种很好的方法,而无需依赖最大颜色质量格式。简而言之,通过在图像颜色中加入低级别的随机噪声,结果得到了更平滑的完成效果,如左侧的图像所示。

在 AndEngine 中,为我们的实体应用抖动很简单,但与所有事物一样,明智的做法是选择哪些纹理应用抖动。该算法确实增加了一点额外的开销,如果使用过于频繁,可能会导致比简单地将我们的表面视图恢复为RGBA_8888更大的性能损失。在以下代码中,我们在preDraw()方法中启用抖动,在postDraw()方法中禁用它:

@Override
protected void preDraw(GLState pGLState, Camera pCamera) {
  // Enable dithering
  pGLState.enableDither();
  super.preDraw(pGLState, pCamera);
}

@Override
protected void postDraw(GLState pGLState, Camera pCamera) {
  // Disable dithering
  pGLState.disableDither();
  super.postDraw(pGLState, pCamera);
}

晕染可以应用于 AndEngine 的Shape类的任何子类型(SpritesText、基元等)。

注意

有关 OpenGL ES 2.0 以及如何使用所有不同函数的更多信息,请访问www.khronos.org/opengles/sdk/docs/man/

另请参阅

  • 在第一章中处理不同类型的纹理处理实体

  • 在第一章中应用纹理选项处理实体

  • 在本章中了解 AndEngine 实体

将文本应用到图层

文本是游戏开发的重要组成部分,因为它可以用来动态显示积分系统、教程、描述等。AndEngine 还允许我们通过指定自定义的Font对象来创建更适合个别游戏类型的文本样式。在本教程中,我们将创建一个Text对象,它会随当前系统时间更新自身,并在字符串长度增长或缩短时调整其位置。这将为我们需要显示分数、时间和其他非特定动态字符串情况下的Text对象使用做好准备。

准备就绪…

Text对象应用到我们的Scene对象需要了解 AndEngine 的字体资源。请执行第一章中的教程,使用 AndEngine 字体资源,然后继续本教程的*如何操作…*部分。参考与此食谱活动代码捆绑中的名为ApplyingText的类。

如何操作…

当我们将Text对象应用到我们的Scene对象上时,需要创建一个Font对象来定义文本的样式,并创建Text对象本身。以下步骤将说明我们必须采取的具体操作,以便在我们的场景上正确显示Text对象:

  1. 创建任何Text对象的第一步是为自己准备一个Font对象。Font对象将作为定义Text对象样式的资源。此外,我们还需要准备我们计划让Text对象显示的字母:

        mFont = FontFactory.create(mEngine.getFontManager(),
            mEngine.getTextureManager(), 256, 256,
            Typeface.create(Typeface.DEFAULT, Typeface.NORMAL), 32f, true,
            Color.WHITE);
        mFont.load();
    
        /*
         * Prepare the mFont object for the most common characters used. This
         * will eliminate the need for the garbage collector to run when using a
         * letter/number that's never been used before
         */
          mFont.prepareLetters("Time: 1234567890".toCharArray());
    Once we've got our Font object created and ready for use, we can create the Text:
        /* Create the time Text object which will update itself as time passes */
        Text mTimeText = new Text(0, timeTextHeight, mFont, TIME_STRING_PREFIX
            + TIME_FORMAT, MAX_CHARACTER_COUNT, mEngine.getVertexBufferObjectManager()) {
    
          // Overridden methods as seen in step 3...
        };
    
  2. 如果我们处理的是可能永远不会改变的最终字符串,那么只需要涵盖前两个步骤。然而,在本教程中,我们将需要覆盖Text实体的onManagedUpdate()方法,以便随时间对其字符串进行调整。在本例中,每经过一秒钟,我们就会更新字符串的时间值:

        int lastSecond = 0;
    
        @Override
        protected void onManagedUpdate(float pSecondsElapsed) {
    
          Calendar c = Calendar.getInstance();
    
          /*
          * We will only obtain the second for now in order to verify
           * that it's time to update the Text's string
          */
          final int second = c.get(Calendar.SECOND);
    
          /*
           * If the last update's second value is not equal to the
          * current...
           */
          if (lastSecond != second) {
    
          /* Obtain the new hour and minute time values */
            final int hour = c.get(Calendar.HOUR);
            final int minute = c.get(Calendar.MINUTE);
    
            /* also, update the latest second value */
            lastSecond = second;
    
             /* Build a new string with the current time */
            final String timeTextSuffix = hour + ":" + minute + ":"
               + second;
    
            /* Set the Text object's string to that of the new time */
            this.setText(TIME_STRING_PREFIX + timeTextSuffix);
    
            /*
              * Since the width of the Text will change with every change
             * in second, we should realign the Text position to the
              * edge of the screen minus half the Text's width
            */
            this.setX(WIDTH - this.getWidth() * 0.5f);
          }
    
          super.onManagedUpdate(pSecondsElapsed);
        }
    Finally, we can make color adjustments to the Text and then attach it to the Scene or another Entity:
        /* Change the color of the Text to blue */
        mTimeText.setColor(0, 0, 1);
    
        /* Attach the Text object to the Scene */
        mScene.attachChild(mTimeText);
    

它是如何工作的…

在这一点上,我们应该已经了解了如何创建Font对象,因为我们在第一章中已经讨论过。如果还不知道如何创建Font对象,请访问第一章中的教程,使用 AndEngine 字体资源处理实体

在第一步中,我们只是创建了一个基本的Font对象,它将为我们的Text对象创建一个相当通用的样式。创建Font对象后,我们只准备Text对象在其生命周期内将显示的必要字符,使用mFont.prepareLetters()方法。这样做可以避免在Font对象内调用垃圾收集器。这个配方中使用的值显然是从09,因为我们处理的是时间,以及组成字符串Time:的单个字符。

完成第一步后,我们可以进入第二步,创建Text对象。Text对象需要我们指定其在屏幕上的初始位置(x 和 y 坐标),使用的Font对象样式,要显示的初始字符串,其最大字符数,以及所有Entity对象所需的顶点缓冲对象管理器。然而,由于我们处理的这个Text对象有一个动态更新的String值,这将需要调整 x 轴,包括 x 坐标以及初始字符串在内的参数并不重要,因为它们将在更新Text对象时频繁调整。最重要的参数是最大字符数。如果Text对象的最大字符数超过了此参数内指定的值,将导致应用程序接收到ArrayIndexOutOfBoundsException异常,很可能会需要终止。因此,我们在以下代码片段中累加最大字符串的长度:

  private static final String TIME_STRING_PREFIX = "Time: ";
  private static final String TIME_FORMAT = "00:00:00";

  /* Obtain the maximum number of characters that our Text 
   * object will need to display*/
  private static final int MAX_CHARACTER_COUNT = TIME_STRING_PREFIX.length() + TIME_FORMAT.length();

在第三步中,我们覆盖了Text对象的onManagedUpdate()方法,以便在每秒过去后对Text对象的字符串应用更改。首先,我们只需获取设备的当前秒值,用它来与上一次调用Text对象的onManagedUpdate()方法中的秒值进行比较。这样,我们可以避免在每次更新时都使用系统时间更新Text对象。如果Text对象字符串上次更新的秒值与新的秒值不同,那么我们继续通过Calendar.getInstance().get(HOUR)方法和MINUTE变体获取当前的分钟和小时值。现在我们已经获得了所有的值,我们构建了一个包含更新时间的新字符串,并在Text对象上调用setText(pString)来更改它将在设备上显示的字符串。

然而,由于每个单独的字符宽度可能具有不同的值,我们也需要调整位置,以保持整个Text对象在屏幕上。默认情况下,锚点位置被设置为Entity对象的中心,因此通过调用this.setX(WIDTH - this.getWidth() * 0.5f)(其中this指的是Text对象),我们将实体最中心的点定位在屏幕最大宽度右侧,然后减去实体宽度的一半。这将允许文本即使在其字符改变了Text对象的宽度后,也能沿着屏幕边缘正确显示。

还有更多…

有时我们的游戏可能需要对Text对象的字符串进行一些格式化处理。在我们需要调整Text对象的水平对齐方式、如果字符串超出一定宽度则对文本应用自动换行,或者在文本前添加一个空格的情况下,我们可以使用一些非常易于使用的方法。以下方法可以直接在Text对象上调用;例如,mText.setLeading(3)

  • setAutoWrap(pAutoWrap): 这个方法允许我们定义Text实体是否执行自动换行,以及如何执行。我们可以为参数选择的选项包括AutoWrap.NONEAutoWrap.LETTERSAutoWrap.WORDSAutoWrap.CJK。使用LETTERS时,行中断不会在空白前等待,而WORDS会等待。CJK变体是允许对中、日、韩字符进行自动换行的选项。这个方法应该与setAutoWrapWidth(pWidth)一起使用,其中pWidth定义了Text对象字符串中任意单行的最大宽度,在需要时导致换行。

  • setHorizontalAlign(pHorizontalAlign): 这个方法允许我们定义Text对象字符串应遵循的对齐类型。参数包括HorizontalAlign.LEFTHorizontalAlign.CENTERHorizontalAlign.RIGHT。其结果类似于我们在文本编辑器内设置对齐时看到的效果。

  • setLeading(pLeading): 这个方法允许我们在Text对象字符串的开始处设置一个前置空间。所需的参数是一个浮点值,它定义了字符串的前导宽度。

另请参阅

  • 在第一章中使用 AndEngine 字体资源处理实体

  • 在本章中覆盖 onManagedUpdate 方法

使用相对旋转

在 2D 空间中相对于其他实体的位置旋转实体是一个很棒的功能。相对旋转的使用是无限的,并且似乎总是移动游戏开发新手中的“热门话题”。这种技术被广泛应用的一个较为突出的例子是在塔防游戏中,它允许塔的炮塔朝向敌人(非玩家角色)行走的方向。在这个示例中,我们将介绍一种旋转我们的Entity对象的方法,以便它们指向给定的 x/y 位置。以下图像展示了我们如何在场景上创建一个箭头,它会自动指向圆形图像的位置,无论它移动到哪里:

使用相对旋转

准备工作…

这个示例我们需要包含两个图像;一个名为marble.png,尺寸为 32x32 像素,另一个名为arrow.png,宽 31 像素,高 59 像素。弹珠可以是任何图像,我们只需随意在场景中拖动这个图像。箭头图像应该呈箭头形状,图像上的箭头直接朝上。请参考引言中的屏幕截图以了解需要包含的图像示例。将这些资源包含在空的BaseGameActivity测试项目中,然后请参考代码包中的名为RelativeRotation的类。

如何操作…

按照以下步骤操作:

  1. BaseGameActivity类中实现IOnSceneTouchListener监听器:

    public class RelativeRotation extends BaseGameActivity implements IOnSceneTouchListener{
    
  2. onCreateScene()方法中设置Scene对象的onSceneTouchListener

    mScene.setOnSceneTouchListener(this);
    
  3. 使用弹珠和小箭头的图像填充Scene对象。小箭头图像位于场景中心,而弹珠的位置会更新为任意触摸事件位置的坐标:

        /* Add our marble sprite to the bottom left side of the Scene initially */
        mMarbleSprite = new Sprite(mMarbleTextureRegion.getWidth(), mMarbleTextureRegion.getHeight(), mMarbleTextureRegion, mEngine.getVertexBufferObjectManager());
    
        /* Attach the marble to the Scene */
        mScene.attachChild(mMarbleSprite);
    
        /* Create the arrow sprite and center it in the Scene */
        mArrowSprite = new Sprite(WIDTH * 0.5f, HEIGHT * 0.5f, mArrowTextureRegion, mEngine.getVertexBufferObjectManager());
    
        /* Attach the arrow to the Scene */
        mScene.attachChild(mArrowSprite);
    
  4. 第四步介绍了onSceneTouchEvent()方法,它通过设备显示上的触摸事件处理弹珠图像的移动:

      @Override
      public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) {
        // If a user moves their finger on the device
        if(pSceneTouchEvent.isActionMove()){
    
          /* Set the marble's position to that of the touch even coordinates */
         mMarbleSprite.setPosition(pSceneTouchEvent.getX(), pSceneTouchEvent.getY());
    
          /* Calculate the difference between the two sprites x and y coordinates */
          final float dX = mMarbleSprite.getX() - mArrowSprite.getX();
          final float dY = mMarbleSprite.getY() - mArrowSprite.getY();
    
          /* Calculate the angle of rotation in radians*/
          final float angle = (float) Math.atan2(-dY, dX);
          /* Convert the angle from radians to degrees, adding the default image rotation */
          final float rotation = MathUtils.radToDeg(angle) + DEFAULT_IMAGE_ROTATION;
    
          /* Set the arrow's new rotation */
          mArrowSprite.setRotation(rotation);
    
          return true;
        }
    
        return false;
      }
    

工作原理…

在这个类中,我们创建了一个由箭头图像表示的精灵,并将其放置在屏幕正中心,自动指向由弹珠表示的另一个精灵。通过在BaseGameActivity类中实现IOnSceneTouchListener监听器,弹珠可以拖动。然后,我们将触摸监听器注册到mScene对象。在实体根据另一个实体的位置进行旋转的情况下,我们不得不在某个持续更新的方法中包含旋转功能,否则我们的箭头不会持续反应。我们可以通过更新线程来实现这一点,但在这个实例中,我们将在重写的onSceneTouchEvent()方法中包含该功能,因为直到我们触摸场景,“目标”实际上不会移动。

在第一步中,我们通过实现IOnSceneTouchListener接口,允许我们的活动重写onSceneTouchEvent()方法。一旦我们实现了触摸监听器,就可以进行第二步,让Scene对象接收触摸事件,并根据活动重写的onSceneTouchEvent()方法中的代码做出响应。这是通过setOnSceneTouchListener(pSceneTouchListener)方法完成的。

在第四步中,if(pSceneTouchEvent.isActionMove())条件语句判断是否有一个手指在场景上移动,更新大理石的位置,并在条件语句返回true时计算箭头精灵的新旋转。

我们首先通过以下代码段中看到的setPosition(pX, pY)方法,将大理石的位置更新到触摸的位置:

mMarbleSprite.setPosition(pSceneTouchEvent.getX(), pSceneTouchEvent.getY());

接下来,我们从目标的 x/y 坐标中减去指针的 x/y 坐标。这为我们提供了每个精灵坐标之间的差值,这将用于计算两个位置之间的角度。在这种情况下,指针是mArrowSprite对象,目标是mMarbleSprite对象:

/* Calculate the difference between the two sprites x and y coordinates */
final float dX = mMarbleSprite.getX() - mArrowSprite.getX();
final float dY = mMarbleSprite.getY() - mArrowSprite.getY();

/* Calculate the angle of rotation in radians*/
final float angle = (float) Math.atan2(-dY, dX);

最后,由于 AndEngine 的setRotation(pRotation)方法使用度数,而atan2(pY, pX)方法返回弧度,我们必须进行简单的转换。我们将使用 AndEngine 的MathUtils类,其中包括一个radToDeg(pRadian)方法,将我们的角度值从弧度转换为度数。一旦我们获得了正确的度数角度,我们将设置mArrowSprite对象的旋转:

/* Convert the angle from radians to degrees, adding the default image rotation */
final float rotation = MathUtils.radToDeg(angle) + DEFAULT_IMAGE_ROTATION;

/* Set the arrow's new rotation */
mArrowSprite.setRotation(rotation);

需要注意的最后一点是,DEFAULT_IMAGE_ROTATION值是一个表示90度的int值。这个值仅用于偏移mArrowSprite精灵的旋转,否则我们将需要在我们的图像编辑软件中适当旋转图像。如果自定义图像中的指针没有指向图像的最顶部,这个值可能需要调整,以便将指针与目标对齐。

重写onManagedUpdate方法

重写Entity对象的onManagedUpdate()方法在所有类型的情况下都非常有用。这样做,我们可以让我们的实体在每次通过更新线程更新实体时执行代码,每秒发生多次,除非实体被设置为忽略更新。可能性非常多,包括动画化我们的实体,检查碰撞,产生定时事件等等。使用我们的Entity对象的onManagedUpdate()方法还可以节省我们为单一实体创建和注册新的定时处理器以处理基于时间的事件。

准备就绪…

这个示例需要具备对 AndEngine 中Entity对象的基本了解。请阅读本章提供的了解 AndEngine 实体的整个示例,然后创建一个新的空 AndEngine 项目,包含一个BaseGameActivity类,并参考代码包中名为OverridingUpdates的类。

如何操作…

在这个示例中,我们将创建两个Rectangle对象。一个矩形将保持场景中心位置,持续旋转。第二个矩形将在场景中从左到右、从下到上连续移动,当到达右侧时重置回左侧,当到达场景顶部时重置回底部。此外,移动的矩形在与中心矩形碰撞时将变为绿色。所有这些移动和条件判断都将通过每个对象重写的onManagedUpdate(pSecondsElapsed)方法来应用和执行。

  1. 重写第一个Rectangle对象的onManagedUpdate()方法,以实现连续旋转:

        /* Value which defines the rotation speed of this Entity */
        final int rotationIncrementalFactor = 25;
    
        /* Override the onManagedUpdate() method of this Entity */
        @Override
        protected void onManagedUpdate(float pSecondsElapsed) {
    
          /* Calculate a rotation offset based on time passed */
          final float rotationOffset = pSecondsElapsed * rotationIncrementalFactor;
    
          /* Apply the rotation offset to this Entity */
          this.setRotation(this.getRotation() + rotationOffset);
    
          /* Proceed with the rest of this Entity's update process */
          super.onManagedUpdate(pSecondsElapsed);
        }
    
  2. 重写第二个Rectangle对象的onManagedUpdate()方法,以实现连续的位置更新、条件检查和碰撞检测:

        /* Value to increment this rectangle's position by on each update */
        final int incrementXValue = 5;
    
        /* Obtain half the Entity's width and height values */
        final float halfWidth = this.getWidth() * 0.5f;
        final float halfHeight = this.getHeight() * 0.5f;
    
        /* Override the onManagedUpdate() method of this Entity */
        @Override
        protected void onManagedUpdate(float pSecondsElapsed) {
    
          /* Obtain the current x/y values */
          final float currentX = this.getX();
          final float currentY = this.getY();
    
          /* obtain the max width and next height, used for condition checking */
          final float maxWidth = currentX + halfWidth;
          final float nextHeight = currentY + halfHeight;
    
          // On every update...
          /* Increment the x position if this Entity is within the camera WIDTH */
           if(maxWidth <= WIDTH){
            /* Increase this Entity's x value by 5 pixels */
            this.setX(currentX + incrementXValue);
          } else {
            /* Reset the Entity back to the bottom left of the Scene if it exceeds the mCamera's
            * HEIGHT value */
            if(nextHeight >= HEIGHT){
            this.setPosition(halfWidth, halfHeight);
            } else {
              /* if this Entity reaches the WIDTH value of our camera, move it
               * back to the left side of the Scene and slightly increment its y position */
              this.setPosition(halfWidth, nextHeight);
            }
          }
    
           /* If the two rectangle's are colliding, set this rectangle's color to GREEN */
          if(this.collidesWith(mRectangleOne) && this.getColor() != org.andengine.util.adt.color.Color.GREEN){
          this.setColor(org.andengine.util.adt.color.Color.GREEN);
    
          /* If the rectangle's are no longer colliding, set this rectangle's color to RED */
          } else if(this.getColor() != org.andengine.util.adt.color.Color.RED){
          this.setColor(org.andengine.util.adt.color.Color.RED);
          }
    
          /* Proceed with the rest of this Entity's update process */
          super.onManagedUpdate(pSecondsElapsed);
       }
    

工作原理…

在我们创建的第一个Rectangle对象中,我们重写其onManagedUpdate(pSecondsElapsed)方法,以持续更新旋转到新值。对于第二个Rectangle对象,我们使其从屏幕最左侧连续移动到最右侧。当第二个矩形到达屏幕最右侧时,它会被重新定位到左侧,并将场景中的Rectangle对象提高半个Rectangle对象的高度。此外,当两个矩形重叠时,移动的矩形将改变颜色为绿色,直到它们不再接触。

第一步的代码允许我们在每次实体更新时创建一个事件。在这个特定的重写方法中,我们基于自上次更新以来经过的秒数计算Rectangle对象的旋转偏移量。因为实体每秒更新多次,具体取决于设备能够达到的每秒帧数,我们将pSecondsElapsed乘以25以稍微增加旋转速度。否则,我们每次更新时将使实体沿0.01度旋转,那样物体以该速率完成一次完整旋转将需要相当长的时间。我们可以在处理更新时利用pSecondsElapsed更新,以便基于自上次更新以来经过的时间对事件进行修改。

第二步比第一步要复杂一些。在第二步中,我们覆盖了第二个矩形的onManagedUpdate()方法,以便在每次实体更新时执行位置检查、碰撞检查以及更新矩形的定位。首先,我们声明了一些变量,这些变量将包含如实体当前位置、实体的半宽和半高值以便从锚点中心正确偏移,以及用于检查位置的下一个更新位置等值。这样做可以减少实体更新过程中所需计算的数量。如果在更新线程中应用了优化不佳的代码,很快就会导致帧率降低。尽可能多地使用方法调用和计算是很重要的;例如,在onManagedUpdate()方法中多次获取currentX值,比多次调用this.getX()更为理想。

继续第二步中的位置检查和更新,我们首先确定矩形的锚点中心加上其半宽(由maxWidth变量表示)是否小于或等于表示显示最右侧坐标的WIDTH值。如果为真,我们会将矩形的 x 坐标增加incrementXValue,即 5 个像素。另一方面,如果nextHeight值大于或等于摄像机的HEIGHT值,我们会将矩形对象重置回场景的左下角;或者如果矩形还没有到达显示顶部,只需将矩形的宽度增加其半宽并返回到左侧。

最后,我们在第二个Rectangle对象的onManagedUpdate()方法中拥有了碰撞检查方法。通过调用this.collidesWith(mRectangleOne),我们可以确定this对象是否与指定对象(在本例中是mRectangleOne)发生重叠。然后我们会进行一个额外的检查,以确定如果检测到碰撞,颜色是否已经等于我们打算将Rectangle对象改变成的颜色;如果条件返回true,则将Rectangle对象设置为绿色。然而,如果每个更新都由多个Entity对象执行,collidesWith()可能是一个相当昂贵的碰撞检查方法!在这个示例中,我们纯粹是将此碰撞检查方法作为示例。一个可以考虑的选项是在执行碰撞检测之前,对两个对象进行轻量级的距离检测。

还有更多…

如前所述,所有子对象都会从其父对象接收到更新调用。在这种情况下,子实体也继承了父级修改后的pSecondsElapsed值。我们甚至可以通过重写其onManagedUpdate()方法并减少pSecondsElapsed值,来减慢整个Scene对象及其所有子对象的运行速度,如下所示:

super.onManagedUpdate(pSecondsElapsed * 0.5f);

将等于pSecondsElapsed值一半的返回值传递给super方法,将导致所有附加到该Scene对象的实体在各个方面都减慢一半。这是在考虑游戏暂停或创建慢动作效果选项时需要记住的一点小技巧。

使用修改器和实体修改器

AndEngine 为我们提供了所谓的修改器实体修改器。通过使用这些修改器,我们可以非常轻松地为实体应用整洁的效果。这些修改器在定义的时间范围内对Entity对象应用特定的变化,如移动、缩放、旋转等。此外,我们还可以为实体修改器包含监听器和缓动函数,以完全控制它们的工作方式,这使得它们成为在我们的Scene对象中应用某些类型动画的最强大方法之一。

注意

在继续之前,我们应该提到 AndEngine 中的修改器和实体修改器是两个不同的对象。修改器是直接应用于实体,随时间修改实体的属性,如缩放、移动和旋转。而实体修改器则用作任何数量的修改器的容器,处理一组修改器的执行顺序。这将在本食谱的后续内容中进一步讨论。

准备就绪…

此食谱需要了解 AndEngine 中Entity对象的基础知识。请阅读本章提供的了解 AndEngine 实体的整个食谱,然后创建一个新的空 AndEngine 项目,包含一个BaseGameActivity类,然后参考此食谱中的*如何操作…*部分。

如何操作…

在此食谱中,我们将介绍 AndEngine 的实体修改器,包括修改器监听器和缓动函数,以应用平滑的过渡效果。如果这听起来令人困惑,不必害怕!AndEngine 的修改器实际上非常易于使用,只需几个基本步骤就可以为我们的Entity对象应用不同类型的动画。以下步骤涵盖了设置具有移动修改器的Entity对象,这将引导我们进一步讨论实体修改器。将这些步骤中的代码导入到活动的onPopulateScene()方法中:

  1. 创建并附加任何类型的实体到Scene对象。我们将为这个实体应用实体修改器:

    /* Define the rectangle's width/height values */
    final int rectangleDimensions = 80;
    
    /* Define the initial rectangle position in the bottom 
     * left corner of the Scene */
    final int initialPosition = (int) (rectangleDimensions * 0.5f);
    
    /* Create the Entity which we will apply modifiers to */
    Rectangle rectangle = new Rectangle(initialPosition, initialPosition, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager());
    
    /* Set the rectangle's color to white so we can see it on the Scene */
    rectangle.setColor(org.andengine.util.adt.color.Color.WHITE);
    
    /* Attach the rectangle to the Scene */
    mScene.attachChild(rectangle);
    
  2. 一旦我们在Scene对象上放置了一个实体,我们就可以开始创建我们的修改器了。在这一步中,我们将创建一个MoveModifier对象,它允许我们随时间对实体的位置进行更改。但首先,我们将定义其值:

    /* Define the movement modifier values */
    final float duration = 3;
    final float fromX = initialPosition;
    final float toX = WIDTH - rectangleDimension * 0.5f;
    final float fromY = initialPosition;
    final float toY = HEIGHT - rectangleDimension * 0.5f;
    
    /* Create the MoveModifier with the defined values */
    MoveModifier moveModifier = new MoveModifier(duration, fromX, fromY, toX, toY);
    
  3. 现在我们已经创建并设置好了moveModifier对象,我们可以通过以下调用将此修改器注册到我们希望的任何实体上,这将开始移动效果:

    /* Register the moveModifier to our rectangle entity */
    rectangle.registerEntityModifier(moveModifier);
    

它的工作原理是……

实体修改器的话题相当广泛,因此我们将从步骤开始深入。从那里,我们将使用这些步骤作为基础,以便进一步深入到关于实体修改器使用更复杂的讨论和示例。

在第一步中,我们只是创建了一个Entity对象,在这个案例中是一个Rectangle,我们将用它作为应用修改器的测试对象。只需将此步骤中的代码添加到onPopulateScene()方法中;在接下来的修改器和实体修改器“实验”中,这段代码将保持不变。

在第二步中,我们将开始使用最基本的修改器之一,当然是MoveModifier。这个修改器允许我们定义移动的起始位置、结束位置以及从起点到终点移动所需的秒数。正如我们所看到的,这非常简单,修改器最值得注意的是,在大多数情况下,这就是设置大多数修改器所需的全部内容。所有修改器真正需要的是一个“from”值、一个“to”值以及定义“from-to”发生秒数的时长。记住这一点,在大多数情况下,使用修改器将会非常轻松!

接下来,在第三步中,我们只需通过registerEntityModifier(pModifier)方法将我们新创建的moveModifier对象应用到rectangle对象上。这将使moveModifier效果应用到矩形上,首先将其定位到“from”坐标,然后在 3 秒的时间内移动到“to”坐标。

我们知道,要向Entity对象注册修改器或实体修改器,可以调用entity.registerEntityModifier(pEntityModifier),但我们也应该知道,一旦完成修改器,我们应该将其从Entity对象中移除。我们可以通过调用entity.unregisterEntityModifier(pEntityModifier)来实现,或者如果我们想移除附加到Entity对象的所有实体修改器,可以调用entity.clearEntityModifiers()。另一方面,如果一个修改器或实体修改器运行了完整的时长,而我们还没有准备好从实体中移除它,我们必须调用modifier.reset()以重新播放效果。或者,如果我们想在重新播放效果之前对修改器进行微调,可以调用modifier.reset(duration, fromValue, toValue)。其中reset方法中的参数将相对于我们要重置的修改器类型。

moveModifier对象有效,但它非常无聊!毕竟,我们只是在将一个矩形从场景的左下角移动到右上角。幸运的是,这只是修改器应用表面的刮擦。以下小节包含了 AndEngine 能够应用到我们的Entity对象的所有修改器的参考,必要时还提供了示例。

AndEngine 的修改器

以下是我们可以应用到实体上的所有 AndEngine 修改器的集合。更高级的修改器将提供一个快速示例代码片段。在介绍它们时,请随意在您的测试项目中尝试:

  • AlphaModifier:使用这个修改器,可以随时间调整实体的透明度值。构造函数的参数包括持续时间、起始透明度和结束透明度,依次排列。

  • ColorModifier:使用这个修改器,可以随时间调整实体的颜色值。构造函数的参数包括持续时间、起始红色、结束红色、起始绿色、结束绿色、起始蓝色和结束蓝色,依次排列。

  • DelayModifier:这个修改器旨在分配给实体修改器对象,以便在一个修改器被执行和另一个修改器被执行之间提供延迟。参数包括持续时间。

  • FadeInModifier:基于AlphaModifier类,FadeInModifier修改器在定义的持续时间内在构造函数中提供,将实体的透明度值从0.0f更改为1.0f

  • FadeOutModifier:与FadeOutModifier类似,只不过透明度值被交换了。

  • JumpModifier:这个修改器可以用来向实体应用“跳跃”动作。参数包括持续时间、起始 X、结束 X、起始 Y、结束 Y 和跳跃高度。这些值将定义在定义的持续时间内在视觉上实体跳跃的距离和高度。

  • MoveByModifier:这个修改器允许我们偏移实体的位置。参数包括持续时间、X 偏移和 Y 偏移,依次排列。例如,指定一个偏移量为-15将使实体在场景上向左移动 15 个单位。

  • MoveXModifierMoveYModifier:这些修改器与MoveModifier类似,允许我们向实体提供移动。然而,这些方法只根据方法名称确定在单个轴上应用移动。参数包括持续时间、起始坐标和结束坐标,依次排列。

  • RotationAtModifier:这个修改器允许我们在偏移旋转中心的同时向实体应用旋转。参数包括持续时间、起始旋转、结束旋转、旋转中心 X 和旋转中心 Y。

  • RotationByModifier:这个修改器允许我们偏移实体的当前旋转值。参数包括持续时间和旋转偏移值。例如,提供一个旋转偏移值为90将使实体顺时针旋转九十度。

  • RotationModifier:这个修改器允许我们从一个特定值旋转实体到另一个特定值。参数包括持续时间、起始旋转和目标旋转。

  • ScaleAtModifier:这个修改器允许我们在缩放时偏移缩放中心来缩放实体。参数包括持续时间、起始缩放、目标缩放、缩放中心 x 和缩放中心 y。

  • ScaleModifier:这个修改器允许我们从一个特定值缩放实体到另一个特定值。参数包括持续时间、起始缩放和目标缩放,按此顺序。

  • SkewModifier:这个修改器允许我们随时间改变实体的 x 和 y 值。参数包括持续时间、起始斜切 x、目标斜切 x、起始斜切 y 和目标斜切 y,顺序是特定的。

  • PathModifier:这个修改器相对于MoveModifier,不过我们可以添加任意多的“到”坐标。这使得我们可以在Scene对象上为实体创建一个路径,通过为PathModifier修改器指定 x/y 坐标对来跟随。在以下步骤中,我们将了解如何为我们的实体创建一个PathModifier修改器:

    1. 定义路径的航点。x 和 y 坐标的航点数组应该具有相同数量的点,因为它们将按顺序配对以形成PathModifier的单个 x/y 坐标。我们必须在每个数组中至少设置两个点,因为我们需要至少一个起始点和结束点:

          /* Create a list which specifies X coordinates to follow */
          final float pointsListX[] = {
              initialPosition, /* First x position */
              WIDTH - initialPosition, /* Second x position */
              WIDTH - initialPosition, /* Third x position */
              initialPosition, /* Fourth x position */
              initialPosition /* Fifth x position */
          };
      
          /* Create a list which specifies Y coordinates to follow */
          final float pointsListY[] = {
              initialPosition, /* First y position */
              HEIGHT - initialPosition, /* Second y position */
              initialPosition, /* Third y position */
              HEIGHT - initialPosition, /* Fourth y position */
              initialPosition /* Fifth y position */
          };
      
    2. 创建一个Path对象,我们将使用它将分开数组中的各个点配对成航点。我们通过遍历数组并在path对象上调用to(pX, pY)方法来实现这一点。请注意,每次我们调用这个方法,我们都在path对象中添加一个额外的航点:

          /* Obtain the number of control points we have */
          final int controlPointCount = pointsListX.length;
      
          /* Create our Path object which we will pair our x/y coordinates into */
          org.andengine.entity.modifier.PathModifier.Path path = new Path(controlPointCount);
      
          /* Iterate through our point lists */
          for(int i = 0; i < controlPointCount; i++){
            /* Obtain the coordinates of the control point at the index */
            final float positionX = pointsListX[i];
            final float positionY = pointsListY[i];
      
            /* Setup a new way-point by pairing together an x and y coordinate */
            path.to(positionX, positionY);
          }
      
    3. 最后,一旦我们定义了航点,就可以创建PathModifier对象,提供持续时间以及我们的path对象作为参数:

          /* Movement duration */
          final float duration = 3;
          /* Create the PathModifier */
          PathModifier pathModifier = new PathModifier(duration, path);
      
          /* Register the pathModifier object to the rectangle */
          rectangle.registerEntityModifier(pathModifier);
      
  • CardinalSplineMoveModifier:这是我们最后要讨论的修改器。这个修改器与PathModifier修改器相对相似,不过我们可以对Entity对象的移动施加张力。这允许在接近拐角或改变方向时实现更流畅和平滑的移动,实际上看起来相当不错。在以下步骤中,我们将了解如何为我们的实体创建一个CardinalSplineMoveModifier修改器:

    1. 第一步与PathModifier修改器类似,是创建我们的点数组。在这个例子中,我们可以从PathModifier示例的第一步复制代码。然而,这个修改器与PathModifier对象的一个区别在于,我们需要至少 4 个单独的 x 和 y 点。

    2. 第二步是确定控制点的数量,定义张力,并创建一个CardinalSplineMoveModifierConfig对象。这是CardinalSplineMoveModifier修改器的PathModifier修改器中Path对象的等价物。张力可以在-11之间,不能多也不能少。张力为-1将使Entity对象的移动非常松散,在转角和方向变化时非常松散;而张力为1将非常像PathModifier修改器,在移动上非常严格:

          /* Obtain the number of control points we have */
          final int controlPointCount = pointsListX.length;
      
          /* Define the movement tension. Must be between -1 and 1 */
          final float tension = 0f;
      
          /* Create the cardinal spline movement modifier configuration */
          CardinalSplineMoveModifierConfig config = new CardinalSplineMoveModifierConfig(controlPointCount, tension);
      
    3. 在第三步中,与PathModifier修改器非常相似,我们必须将 x/y 坐标配对在我们的点数组中,不过在这个情况下,我们是将它们存储在config对象中:

          /* Iterate through our control point indices */
          for(int index = 0; index < controlPointCount; index++){
      
            /* Obtain the coordinates of the control point at the index */
            final float positionX = pointsListX[index];
            final float positionY = pointsListY[index];
      
            /* Set position coordinates at the current index in the config object */
            config.setControlPoint(index, positionX, positionY);
          }
      
    4. 接下来,我们只需简单地定义移动的持续时间,创建CardinalSplineMoveModifier修改器,提供持续时间和config对象作为参数,并最终将修改器注册到Entity对象上:

          /* Movement duration */
          final float duration = 3;
      
          /* Create the cardinal spline move modifier object */
          CardinalSplineMoveModifier cardinalSplineMoveModifier = new CardinalSplineMoveModifier(duration, config);
      
          /* Register the cardinalSplineMoveModifier object to the rectangle object */
          rectangle.registerEntityModifier(cardinalSplineMoveModifier);
      

现在我们已经对可以应用到实体上的各个修改器有了深入的理解,我们将介绍 AndEngine 中的三个主要实体修改器以及它们的用途。

AndEngine 的实体修改器

AndEngine 包含三种实体修改器对象,用于通过将两个或更多修改器组合成一个单一事件或序列,为我们的Entity对象构建复杂的动画。这三种不同的实体修改器包括LoopEntityModifierParallelEntityModifierSequenceEntityModifier对象。接下来,我们将描述这些实体修改器的具体细节和示例,展示如何将它们组合成单一动画事件。

  • LoopEntityModifier:这个实体修改器允许我们无限次数或指定次数(如果提供了第二个int参数)循环指定的修改器。这是最简单的实体修改器。一旦我们设置好了LoopEntityModifier,就可以直接将其应用于Entity对象:

        /* Define the move modifiers properties */
        final float duration = 3;
        final float fromX = 0;
        final float toX = 100;
    
        /* Create the move modifier */
        MoveXModifier moveXModifier = new MoveXModifier(duration, fromX, toX);
    
        /* Create a loop entity modifier, which will loop the move modifier
         *  indefinitely, or until unregistered from the rectangle.
         *  If we want to provide a loop count, we can add a second int parameter 
         *  to this constructor */
        LoopEntityModifier loopEntityModifier = new LoopEntityModifier(moveXModifier);
    
        /* register the loopEntityModifier to the rectangle */
        rectangle.registerEntityModifier(loopEntityModifier);
    
    
  • ParallelEntityModifier:这个实体修改器允许我们将无限数量的修改器组合成一个单一动画。这个实体修改器提供的参数中的修改器将同时运行在Entity对象上。这使得我们可以在旋转时缩放修改器,例如,在以下示例中可以看到。欢迎在示例中添加更多修改器进行练习:

        /* Scale modifier properties */
        final float scaleDuration = 2;
        final float fromScale = 1;
        final float toScale = 2;
        /* Create a scale modifier */
        ScaleModifier scaleModifier = new ScaleModifier(scaleDuration, fromScale, toScale);
    
        /* Rotation modifier properties */
        final float rotateDuration = 3;
        final float fromRotation = 0;
        final float toRotation = 360 * 4;
        /* Create a rotation modifier */
        RotationModifier rotationModifier = new RotationModifier(rotateDuration, fromRotation, toRotation);
    
        /* Create a parallel entity modifier */
        ParallelEntityModifier parallelEntityModifier = new ParallelEntityModifier(scaleModifier, rotationModifier);
    
        /* Register the parallelEntityModifier to the rectangle */
        rectangle.registerEntityModifier(parallelEntityModifier);
    
    
  • SequenceEntityModifier:这个实体修改器允许我们将修改器串联起来,在单个Entity对象上按顺序执行。这个修改器是在之前提到的修改器列表中使用DelayModifier对象的理想实体修改器。以下示例显示了一个从屏幕左下角移动到屏幕中心的Entity对象,暂停2秒,然后缩小到比例因子为0

        /* Move modifier properties */
        final float moveDuration = 2;
        final float fromX = initialPosition;
        final float toX = WIDTH * 0.5f;
        final float fromY = initialPosition;
        final float toY = HEIGHT * 0.5f;
        /* Create a move modifier */
        MoveModifier moveModifier = new MoveModifier(moveDuration, fromX, fromY, toX, toY);
    
        /* Create a delay modifier */
        DelayModifier delayModifier = new DelayModifier(2);
    
        /* Scale modifier properties */
        final float scaleDuration = 2;
        final float fromScale = 1;
        final float toScale = 0;
        /* Create a scale modifier */
        ScaleModifier scaleModifier = new ScaleModifier(scaleDuration, fromScale, toScale);
    
        /* Create a sequence entity modifier */
        SequenceEntityModifier sequenceEntityModifier = new SequenceEntityModifier(moveModifier, delayModifier, scaleModifier);
    
        /* Register the sequenceEntityModifier to the rectangle */
       rectangle.registerEntityModifier(sequenceEntityModifier);
    

更重要的是要知道我们可以将SequenceEntityModifier修改器添加到ParallelEntityModifier修改器中,将ParallelEntityModifier修改器添加到LoopEntityModifier修改器中,或者是我们能想到的任何其他组合!这使得修改器和实体修改器的可能性变得极其广泛,并允许我们以相当大的便利性为实体创建极其复杂的动画。

还有更多内容…

在继续下一个主题之前,我们应该看看为实体修改器包含的额外特性。还有两个参数我们可以传递给实体修改器,我们之前还没有讨论过;那就是修改器监听器和缓动函数。这两个类可以帮助我们使修改器比我们在*如何工作…*部分看到的更加定制化。

IEntityModifierListener监听器可以用来在修改器开始和结束时触发事件。在以下代码段中,我们只是简单地向 logcat 打印日志,以通知我们修改器何时开始和结束。

IEntityModifierListener entityModifierListener = new IEntityModifierListener(){

  // When the modifier starts, this method is called
  @Override
  public void onModifierStarted(IModifier<IEntity> pModifier,
      IEntity pItem) {
    Log.i("MODIFIER", "Modifier started!");
  }

  // When the modifier finishes, this method is called
  @Override
  public void onModifierFinished(final IModifier<IEntity> pModifier,
      final IEntity pItem) {
    Log.i("MODIFIER", "Modifier started!");
  }
};

modifier.addModifierListener();

之前的代码展示了一个带有基本日志输出的修改器监听器的框架。在更接近游戏开发的场景中,一旦修改器完成,我们可以调用pItem.setVisible(false)。例如,这可以用于处理场景中细微的落叶或雨滴,这些落叶或雨滴离开了摄像头的视野。然而,我们决定用监听器来做什么完全取决于我们自己的判断。

最后,我们将快速讨论 AndEngine 中的缓动函数。缓动函数是给实体修改器添加额外“酷炫”层次的好方法。习惯了修改器之后,缓动函数可能会真正吸引你,因为它们给修改器带来了所需的额外动力,以产生完美效果。解释缓动函数的最好方法是想象一个游戏,菜单按钮从屏幕顶部落下并“弹跳”到位。这里的弹跳就是我们的缓动函数产生效果的情况。

    /* Move modifier properties */
    final float duration = 3;
    final float fromX = initialPosition;
    final float toX = WIDTH - initialPosition;
    final float fromY = initialPosition;
    final float toY = HEIGHT - initialPosition;

    /* Create a move modifier with an ease function */
    MoveModifier moveModifier = new MoveModifier(duration, fromX, fromY, toX, toY, org.andengine.util.modifier.ease.EaseElasticIn.getInstance());

    rectangle.registerEntityModifier(moveModifier);

正如我们在这里看到的,给修改器应用缓动函数只需在修改器的构造函数中添加一个额外的参数即可。通常最困难的部分是选择使用哪一个,因为缓动函数列表相当长。花些时间查看org.andengine.util.modifier.ease包提供的各种缓动函数。只需将前一段代码中的EaseElasticIn替换为你想要测试的缓动函数,然后重新构建项目以查看效果!

提示

缓动函数参考

从 Google Play 将AndEngine – Examples应用程序下载到你的设备上。打开应用程序并找到Using EaseFunctions的例子。尽管与最新的 AndEngine 分支相比,示例应用程序相当过时,但缓动函数示例仍然是一个绝对有效的工具,用于确定哪些缓动函数最适合我们游戏的需求!

另请参阅

  • 本章节中了解 AndEngine 实体

使用粒子系统

粒子系统可以为我们的游戏提供非常吸引人的效果,涵盖游戏中的许多不同事件,如爆炸、火花、血腥、雨等。在本章中,我们将介绍 AndEngine 的ParticleSystem类,这将用于创建定制化的粒子效果,满足我们的各种需求。

准备工作…

本食谱需要了解 AndEngine 中Sprite对象的基础知识。请阅读第一章中的整个食谱,使用不同类型的纹理以及本章中给出的了解 AndEngine 实体。接下来,创建一个带有BaseGameActivity类的新的空 AndEngine 项目,并从代码包中的WorkingWithParticles类导入代码。

如何操作…

为了开始在 AndEngine 中创建粒子效果,我们需要至少三个对象。这些对象包括代表生成的单个粒子的ITextureRegion对象,一个ParticleSystem对象和一个ParticleEmitter对象。一旦我们有了这些,我们就可以开始向我们的粒子系统添加所谓的粒子初始化器和粒子修改器,以创建我们自己的个性化效果。以下步骤将指导如何设置一个基本的粒子系统,以便在此基础上进行构建。

  1. 第一步涉及决定我们希望粒子系统生成的图像。这可以是任何图像、任何颜色和任何大小。随意创建一个图像,并设置BuildableBitmapTextureAtlasITextureRegion来将图像加载到测试项目的资源中。为了保持事情简单,请将图像的尺寸控制在 33x33 像素以下以适应本食谱。

  2. 创建ParticleEmitter对象。现在我们将使用PointParticleEmitter对象子类型:

        /* Define the center point of the particle system spawn location */
        final int particleSpawnCenterX = (int) (WIDTH * 0.5f);
        final int particleSpawnCenterY = (int) (HEIGHT * 0.5f);
    
        /* Create the particle emitter */
        PointParticleEmitter particleEmitter = new PointParticleEmitter(particleSpawnCenterX, particleSpawnCenterY);
    
  3. 创建ParticleSystem对象。我们将使用BatchedSpriteParticleSystem对象实现,因为它是 AndEngine 中包含的最新和最好的ParticleSystem对象子类型。它允许我们创建大量粒子,同时大大降低典型SpriteParticleSystem对象的开销:

        /* Define the particle system properties */
        final float minSpawnRate = 25;
        final float maxSpawnRate = 50;
        final int maxParticleCount = 150;
    
        /* Create the particle system */
        BatchedSpriteParticleSystem particleSystem = new BatchedSpriteParticleSystem(
            particleEmitter, minSpawnRate, maxSpawnRate, maxParticleCount,
            mTextureRegion,
            mEngine.getVertexBufferObjectManager());
    
  4. 在创建粒子系统的最后一步中,我们将添加任意组合的粒子发射器和粒子修改器,然后将粒子系统附加到Scene对象上:

        /* Add an acceleration initializer to the particle system */
        particleSystem.addParticleInitializer(new AccelerationParticleInitializer<UncoloredSprite>(25f, -25f, 50f, 100f));
    
        /* Add an expire initializer to the particle system */
        particleSystem.addParticleInitializer(new ExpireParticleInitializer<UncoloredSprite>(4));
    
        /* Add a particle modifier to the particle system */
        particleSystem.addParticleModifier(new ScaleParticleModifier<UncoloredSprite>(0f, 3f, 0.2f, 1f));
    
        /* Attach the particle system to the Scene */
        mScene.attachChild(particleSystem);
    

它是如何工作的…

对于许多新的 AndEngine 开发者来说,处理粒子似乎是一个相当困难的课题,但实际上恰恰相反。在 AndEngine 中创建粒子效果非常简单,但如往常一样,我们应该学会走再尝试飞!在本食谱的步骤中,我们设置了一个相当基础的粒子系统。随着话题的深入,我们将讨论并插入粒子系统的其他模块化组件,以拓宽我们对构成复杂粒子系统效果各个部分的知识。

在第一步中,我们需要建立一个ITextureRegion对象来为我们的粒子系统提供资源。ITextureRegion对象将视觉上代表每个生成的独立粒子。纹理区域可以是任何大小,但通常它们会在 2 x 2 到 32 x 32 像素之间。请记住,粒子系统旨在生成大量的对象,因此ITextureRegion对象越小,就粒子系统而言性能会越好。

在第二步中,我们创建了一个粒子发射器并将其置于Scene对象的中心。粒子发射器是粒子系统中的一个组件,它控制着粒子的初始生成位置。在本食谱中,我们使用的是PointParticleEmitter对象类型,它会简单地在场景上以particleSpawnCenterXparticleSpawnCenterY变量定义的相同坐标生成所有粒子。AndEngine 还包括其他四种粒子发射器类型,我们稍后会进行讨论。

当我们创建并适当地设置好粒子发射器后,我们可以进入第三步并创建BatchedSpriteParticleSystem对象。我们需要按顺序向BatchedSpriteParticleSystem对象传递的参数包括:粒子发射器、粒子的最小生成速率、最大生成速率、可以同时显示的最大粒子数量、粒子应视觉代表的ITextureRegion对象,以及mEngine对象的顶点缓冲区对象管理器。

最后,在第四步中,我们添加了一个AccelerationParticleInitializer对象,它将为粒子提供加速运动,使它们不仅仅停留在它们产生的地方。我们还添加了一个ExpireParticleInitializer对象,用于在定义的时间后销毁粒子。如果没有某种初始化器或修改器移除粒子,BatchedParticleSystem对象最终会达到其最大粒子限制,并停止产生粒子。最后,我们向粒子系统添加了一个ScaleParticleModifier对象,它将随时间改变每个粒子的缩放比例。这些粒子初始化器和粒子修改器将稍作深入解释,现在只需知道这是我们应用它们到粒子系统的步骤。添加完我们选择的初始化器和修改器后,我们将particleSystem对象附加到Scene对象上。

完成这四个步骤后,粒子系统将开始产生粒子。然而,我们可能并不总是希望粒子从特定的粒子系统中产生。要禁用粒子产生,可以调用particleSystem.setParticlesSpawnEnabled(false),或者设置为true以重新启用粒子产生。除了这个方法,BatchedSpriteParticleSystem对象还包含Entity对象的所有普通功能和方法。

有关粒子系统的各个组成部分的更多信息,请参见以下子主题。这些主题包括粒子发射器、粒子初始化器和粒子修改器。

粒子发射器的选择

AndEngine 包含五种可立即使用的粒子发射器,它们可以改变场景上粒子的初始放置,这不应与定义粒子发射器位置混淆。有关每个粒子发射器的工作原理,请查看粒子发射器列表。请随时在步骤二的配方中用以下列表中的粒子发射器替换粒子发射器。

  • PointParticleEmitter:这是最基础的粒子发射器;这种粒子发射器使所有产生的粒子在场景上同一定义的位置产生。粒子产生的位置不会有任何变化。然而,可以通过调用pointParticleEmitter.setCenter(pX, pY)方法来改变粒子发射器的位置,其中pXpY定义了产生粒子的新坐标。

  • CircleOutlineParticleEmitter:这种粒子发射器子类型将使粒子在圆形轮廓的位置产生。这个发射器构造函数中需要包含的参数包括 x 坐标、y 坐标和一个定义圆形轮廓整体大小的半径。请看以下示例:

        /* Define the center point of the particle system spawn location */
        final int particleSpawnCenterX = (int) (WIDTH * 0.5f);
        final int particleSpawnCenterY = (int) (HEIGHT * 0.5f);
    
        /* Define the radius of the circle for the particle emitter */
        final float particleEmitterRadius = 50;
    
        /* Create the particle emitter */
        CircleOutlineParticleEmitter particleEmitter = new CircleOutlineParticleEmitter(particleSpawnCenterX, particleSpawnCenterY, particleEmitterRadius);
    
  • CircleParticleEmitter:这种粒子发射器子类型允许粒子在CircleOutlineParticleEmitter对象仅限于边缘轮廓的圆形区域内任何位置生成。CircleParticleEmitter对象在其构造函数中需要与CircleOutlineParticleEmitter对象相同的参数。要测试这种粒子发射器子类型,只需将CircleOutlineParticleEmitter示例中的对象重构为使用CircleParticleEmitter对象即可。

  • RectangleOutlineParticleEmitter:这种粒子发射器子类型将导致粒子从由构造函数参数定义大小的矩形的四个角生成。与CircleOutlineParticleEmitter对象不同,这种粒子发射器不允许粒子围绕矩形的整个边缘生成。请参阅以下示例:

        /* Define the center point of the particle system spawn location */
        final int particleSpawnCenterX = (int) (WIDTH * 0.5f);
        final int particleSpawnCenterY = (int) (HEIGHT * 0.5f);
    
        /* Define the width and height of the rectangle particle emitter */
        final float particleEmitterWidth = 50;
        final float particleEmitterHeight = 100;
    
        /* Create the particle emitter */
        RectangleOutlineParticleEmitter particleEmitter = new RectangleOutlineParticleEmitter(particleSpawnCenterX, particleSpawnCenterY, particleEmitterWidth, particleEmitterHeight);
    
  • RectangleParticleEmitter:这种粒子发射器子类型允许粒子在由构造函数参数定义的矩形形状的边界区域内任何位置生成。要测试这种粒子发射器子类型,只需将RectangleOutlineParticleEmitter示例中的对象重构为使用RectangleParticleEmitter对象即可。

粒子初始化器选择

粒子初始化器对粒子系统至关重要。它们为我们提供了对最初生成的每个单独粒子执行操作的可能性。这些粒子初始化器最棒的一点是,它们允许我们提供最小/最大值,这使我们有机会随机化生成粒子的属性。以下列出了 AndEngine 提供的所有粒子初始化器及其使用示例。请随意用此列表中的粒子初始化器替换配方中的那些。

注意

以下粒子初始化器可以通过简单的调用particleSystem.addParticleInitializer(pInitializer)添加,此外,还可以通过particleSystem.removeParticleInitializer(pInitializer)移除。

  • ExpireParticleInitializer:我们将从列表中最必要的粒子初始化器开始。ExpireParticleInitializer对象提供了一种移除存活时间过长的粒子的方法。如果我们不包括某种形式的粒子过期机制,那么随着所有粒子系统在任意给定时间都有可以激活的粒子数量的限制,我们的粒子很快就会没有粒子可以生成。以下示例创建了一个ExpireParticleModifier对象,该对象使单个粒子在24秒之间过期:

        /* Define min/max particle expiration time */
        final float minExpireTime = 2;
        final float maxExpireTime = 4;
        ExpireParticleInitializer<UncoloredSprite> expireParticleInitializer = new ExpireParticleInitializer<UncoloredSprite>(minExpireTime, maxExpireTime);
    
  • AccelerationParticleInitializer:这个初始化器允许我们以加速度的形式应用移动,使得生成的粒子在达到定义的速度之前会加速。x 轴或 y 轴上的正值将使粒子向上向右移动,而负值将使粒子向下向左移动。在以下示例中,将为粒子赋予最小/最大值,这将导致粒子的移动方向是随机的:

        /* Define the acceleration values */
        final float minAccelerationX = -25;
        final float maxAccelerationX = 25;
        final float minAccelerationY = 25;
        final float maxAccelerationY = 50;
    
        AccelerationParticleInitializer<UncoloredSprite> accelerationParticleInitializer = new AccelerationParticleInitializer<UncoloredSprite>(minAccelerationX, maxAccelerationX, minAccelerationY, maxAccelerationY);
    
  • AlphaInitializerAlphaInitializer对象非常基础。它仅允许我们使用未确定的 alpha 值初始化粒子。以下示例将导致每个单独的粒子以0.5f1f之间的 alpha 值生成:

        /* Define the alpha values */
        final float minAlpha = 0.5f;
        final float maxAlpha = 1;
    
        AlphaParticleInitializer<UncoloredSprite> alphaParticleInitializer = new AlphaParticleInitializer<UncoloredSprite>(minAlpha, maxAlpha);
    
  • BlendFunctionParticleInitializer:这个粒子初始化器允许我们生成应用了特定 OpenGL 混合函数的粒子。关于混合函数及其结果的更多信息,可以在网上找到许多资源。以下是使用BlendFunctionParticleInitializer对象的示例:

        BlendFunctionParticleInitializer<UncoloredSprite> blendFunctionParticleInitializer = new BlendFunctionParticleInitializer<UncoloredSprite>(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
    
  • ColorParticleInitializerColorParticleInitializer对象允许我们为精灵提供最小/最大值之间的颜色。这使得我们可以随机化每个生成粒子的颜色。以下示例将生成具有完全不同随机颜色的粒子:

        /* Define min/max values for particle colors */
        final float minRed = 0f;
        final float maxRed = 1f;
        final float minGreen = 0f;
        final float maxGreen = 1f;
        final float minBlue = 0f;
        final float maxBlue = 1f;
    
        ColorParticleInitializer<UncoloredSprite> colorParticleInitializer = new ColorParticleInitializer<UncoloredSprite>(minRed, maxRed, minGreen, maxGreen, minBlue, maxBlue);
    
  • GravityParticleInitializer:这个粒子初始化器允许我们生成像遵循地球重力规则一样的粒子。GravityParticleInitializer对象在其构造函数中不需要参数:

        GravityParticleInitializer<UncoloredSprite> gravityParticleInitializer = new GravityParticleInitializer<UncoloredSprite>();
    
  • RotationParticleInitializerRotationParticleInitializer对象允许我们定义粒子生成时的旋转最小/最大值。以下示例将导致每个单独的粒子以0359度之间的任意角度生成:

        /* Define min/max values for the particle's rotation */
        final float minRotation = 0;
        final float maxRotation = 359;
    
        RotationParticleInitializer<UncoloredSprite> rotationParticleInitializer = new RotationParticleInitializer<UncoloredSprite>(minRotation, maxRotation);
    
  • ScaleParticleInitializerScaleParticleInitializer对象允许我们定义粒子生成时的缩放最小/最大值。以下示例将允许粒子以0.5f1.5f之间的任意比例因子生成:

        /* Define min/max values for the particle's scale */
        final float minScale = 0.5f;
        final float maxScale = 1.5f;
        ScaleParticleInitializer<UncoloredSprite> scaleParticleInitializer = new ScaleParticleInitializer<UncoloredSprite>(minScale, maxScale);
    
  • VelocityParticleInitializer:这个最后的粒子初始化器,与AccelerationParticleInitializer对象类似,允许我们在生成粒子时为它们提供移动。然而,这个初始化器使粒子以恒定速度移动,并且除非手动配置,否则不会随时间增加或减少速度:

        /* Define min/max velocity values of the particles */
        final float minVelocityX = -25;
        final float maxVelocityX = 25;
        final float minVelocityY = 25;
        final float maxVelocityY = 50;
    
        VelocityParticleInitializer<UncoloredSprite> velocityParticleInitializer = new VelocityParticleInitializer<UncoloredSprite>(minVelocityX, maxVelocityX, minVelocityY, maxVelocityY);
    

有关 AndEngine 的粒子修改器列表,请参阅以下部分。

粒子修改器选择

AndEngine 的粒子修改器在开发复杂的粒子系统时非常有用。它们允许我们根据粒子存活的时间为单个粒子提供变化。与实体修改器类似,粒子修改器是“从时间到时间,从值到值”的格式。再次强调,请随意将列表中的任何粒子修改器添加到您当前测试项目中。

注意

以下粒子修改器可以通过简单的调用particleSystem.addParticleModifier(pModifier)添加,并且可以通过particleSystem.removeParticleModifier(pModifier)移除。

  • AlphaParticleModifier:这个修改器允许粒子在其生命周期内,在两个时间点之间改变 alpha 值。以下示例中,修改器将在1秒内从 alpha 值1过渡到0。修改器将在粒子生成后1秒生效:

        /* Define the alpha modifier's properties */
        final float fromTime = 1;
        final float toTime = 2;
        final float fromAlpha = 1;
        final float toAlpha = 0;
        AlphaParticleModifier<UncoloredSprite> alphaParticleModifier = new AlphaParticleModifier<UncoloredSprite>(fromTime, toTime, fromAlpha, toAlpha);
    
  • ColorParticleModifier:这个修改器允许粒子在其生命周期内,在两个时间点之间改变颜色。以下修改器将导致粒子在两秒内从绿色变为红色,从时间0开始。这意味着过渡将在粒子生成后立即开始:

        /* Define the color modifier's properties */
        final float fromTime = 0;
        final float toTime = 2;
        final float fromRed = 0;
        final float toRed = 1;
        final float fromGreen = 1;
        final float toGreen = 0;
        final float fromBlue 0;
        final float toBlue = 0;
    
        ColorParticleModifier<UncoloredSprite> colorParticleModifier = new ColorParticleModifier<UncoloredSprite>(fromTime, toTime, fromRed, toRed, fromGreen, toGreen, fromBlue, toBlue);
    
  • OffCameraExpireParticleModifier:将此修改器添加到粒子系统中,离开Camera对象视野的粒子将被销毁。我们可以将此作为ExpireParticleInitializer对象的替代,但任何粒子系统至少应该激活这两者之一。需要提供给这个修改器的唯一参数是我们的Camera对象:

        OffCameraExpireParticleModifier<UncoloredSprite> offCameraExpireParticleModifier = new OffCameraExpireParticleModifier<UncoloredSprite>(mCamera);
    
  • RotationParticleModifier:这个修改器允许我们在粒子的生命周期内,在两个时间点之间改变粒子的旋转角度。以下示例将导致粒子在其生命周期的14秒之间旋转180度:

        /* Define the rotation modifier's properties */
        final float fromTime = 1;
        final float toTime = 4;
        final float fromRotation = 0;
        final float toRotation = 180;
    
        RotationParticleModifier<UncoloredSprite> rotationParticleModifier = new RotationParticleModifier<UncoloredSprite>(fromTime, toTime, fromRotation, toRotation);
    
  • ScaleParticleModifierScaleParticleModifier对象允许我们在粒子的生命周期内,在两个时间点之间改变粒子的缩放比例。以下示例将导致粒子在其生命周期的13秒之间,从缩放比例0.5f增长到1.5f

        /* Define the scale modifier's properties */
        final float fromTime = 1;
        final float toTime = 3;
        final float fromScale = 0.5f;
        final float toScale = 1.5f;
    
        ScaleParticleModifier<UncoloredSprite> scaleParticleModifier = new ScaleParticleModifier<UncoloredSprite>(fromTime, toTime, fromScale, toScale);
    
  • IParticleModifier:最后,我们有了粒子修改器接口,它允许我们在粒子初始化时或通过更新线程对每个粒子进行更新时,对单个粒子进行自定义修改。以下示例展示了如何通过在粒子到达Scene对象坐标系下20以下值时,禁用 y 轴上的移动来模拟粒子着陆。我们可以使用这个接口,根据需要虚拟地对粒子进行任何更改:

        IParticleModifier<UncoloredSprite> customParticleModifier = new IParticleModifier<UncoloredSprite>(){
    
          /* Fired only once when a particle is first spawned */
          @Override
          public void onInitializeParticle(Particle<UncoloredSprite> pParticle) {
              * Make customized modifications to a particle on initialization */
          }
    
          /* Fired on every update to a particle in the particle system */
          @Override
          public void onUpdateParticle(Particle<UncoloredSprite> pParticle) {
              * Make customized modifications to a particle on every update to the particle */
                    Entity entity = pParticle.getEntity();
              * Obtain the particle's position and movement properties */
            final float currentY = entity.getY();
            final float currentVelocityY = pParticle.getPhysicsHandler().getVelocityY();
            final float currentAccelerationY = pParticle.getPhysicsHandler().getAccelerationY();
    
            /* If the particle is close to the bottom of the Scene and is moving... */
            if(entity.getY() < 20 && currentVelocityY != 0 || currentAccelerationY != 0){
    
              /* Restrict movement on the Y axis. Simulates landing on the ground */
              pParticle.getPhysicsHandler().setVelocityY(0);
              pParticle.getPhysicsHandler().setAccelerationY(0);
            }
            }
    
        };
    

既然我们已经介绍了所有的粒子发射器、粒子初始化器和粒子修改器,尝试通过组合你想要的初始化器和修改器,创建更复杂的粒子系统吧!

另请参阅

  • 在第一章,AndEngine 游戏结构使用不同类型的纹理

  • 本章节中的了解 AndEngine 实体

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值