八、最大化性能

在本章中,我们将介绍一些提高我们的和工程应用性能的最佳实践。包括的主题如下:

  • 忽略实体更新
  • 禁用背景窗口渲染
  • 限制同步声音流
  • 创建精灵池
  • 使用精灵组减少渲染时间
  • 使用实体剔除禁用渲染

简介

游戏优化对谷歌游戏的成功起着至关重要的作用。如果游戏在他们的设备上运行不佳,用户很可能会对游戏做出负面评价。不幸的是,由于有这么多不同的设备,并且没有办法在谷歌游戏上有效地大规模限制低端设备,最好尽可能地优化安卓游戏。忽略评级,可以公平地假设,一款在中级设备上表现不佳的游戏,就下载量和活跃用户而言,不会发挥出全部潜力。本章将介绍一些对与和工程相关的性能问题最有帮助的解决方案。这将有助于我们提高中低端设备的性能,而无需牺牲质量。

虽然本章中的方法可以极大地提高我们游戏的性能,但重要的是要记住,干净高效的代码同样适用。游戏开发是一项对性能非常关键的任务,就像所有语言一样,有很多小事情要做或避免。网上有许多资源涵盖了大多数与 Java 一般实践以及安卓特有的提示和技巧相关的好与坏的主题。

忽略实体更新

在优化一款游戏时,游戏开发最重要的一条规则就是,不做不需要做的工作!。在这个食谱中,我们将讨论如何在我们的实体上使用setIgnoreUpdate()方法,以限制更新线程只更新应该更新的内容,而不是不断更新我们所有的实体,无论我们是否使用它们。

怎么做…

以下setIgnoreUpdate(boolean)方法允许我们控制哪些实体将通过引擎的更新线程进行更新:

Entity entity = new Entity();

// Ignore updates for this entity
entity.setIgnoreUpdate(true);

// Allow this entity to continue updating
entity.setIgnoreUpdate(false);

它是如何工作的…

正如我们在前面几章中所讨论的,每个孩子的onUpdate()方法都是通过其父母调用的。引擎首先更新,调用主Scene对象的更新方法。然后,场景继续调用其子场景的所有更新方法。接下来,场景的子对象将以这种方式分别调用它们的子对象的更新方法,以此类推。考虑到这一点,通过在我们的主场景对象上调用 setIgnoreUpdate(),我们也可以有效地忽略对场景中所有实体的更新。

忽略对不在使用中的实体的更新,或者甚至是不应该做出反应的实体,除非某个事件发生,可以节省相当多的 CPU 时间。在有大量实体的场景中尤其如此。这看起来不算什么工作,但是请记住,对于每个具有实体修饰符或更新处理器的实体,这些对象也必须更新。除此之外,由于父/子层次结构,每个实体的子级然后继续更新。

最佳做法是将setIgnoreUpdate(true)设置为屏幕外或不需要持续更新的所有实体。对于可能根本不需要任何更新的精灵,比如场景的背景精灵,我们可以无限期的忽略更新,不会造成任何问题。在实体需要更新,但不是很频繁的情况下,例如子弹从炮塔发射,我们可以在子弹从炮塔行进到目的地时启用对它的更新,当它不再被驱动时禁用它。

另见

  • 第 2 章中的【了解和设计实体】部分与实体一起工作

禁用背景窗口渲染

在大多数游戏中,开发人员通常倾向于全屏模式。这可能看起来不明显,因为我们在视觉上看不到真正的区别,但是安卓操作系统没有意识到哪些应用是全屏运行的。这意味着背景窗口将继续在我们的应用下面绘制,除非AndroidManifest.xml中另有规定。在本主题中,我们将介绍如何禁用背景渲染来提高应用 FPS,主要受益于低端设备。

做好准备...

为了阻止背景窗口渲染,我们必须做的第一件事是为我们的应用创建一个主题。我们将通过向我们项目的res/values/文件夹添加一个新的 xml 文件来实现这一点,该文件夹名为theme.xml

用以下代码覆盖默认 xml 文件中的所有代码并保存文件:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <style name="Theme.NoBackground" parent="android:Theme">
        <item name="android:windowBackground">@null</item>
    </style>
</resources>

怎么做...

一旦我们创建并填写了theme.xml文件,我们就可以通过将主题应用到我们项目的AndroidManifest.xml文件中的应用标签来禁用背景窗口渲染。应用标签的属性可能如下所示:

<application
        android:theme="@style/Theme.NoBackground"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" 
        >

请注意,我们也可以将主题应用于特定的活动,而不是通过将android:theme="@style/Theme.NoBackground"代码添加到单个活动标签来应用于整个应用。这与混合游戏最为相关,混合游戏需要在多个活动中同时使用安卓和安卓视图。

它是如何工作的...

禁用背景窗口渲染是一项简单的任务,可以提供几个百分比的性能提升,主要是在旧设备中。负责后台窗口的主要代码行在theme.xml文件中。通过取消android:windowBackground项目,我们通知设备,我们不想绘制背景窗口,而是希望完全将其从渲染中移除。

限制同时的声音流

在使用 AndEngine 进行游戏时,声音回放通常不是问题。但是,在某些情况下,大量声音可能会在很短的时间内播放,这可能会根据播放的声音数量,在较旧的设备(有时是较新的设备)上造成明显的延迟。默认情况下,AndEngine 最多允许同一Sound对象的五个同时播放的声音流在任何给定时间播放。在本主题中,我们将使用EngineOptions来更改同时声音流的数量,以便更好地适应我们应用的需求。

怎么做...

为了增加或减少每个声音对象的同时流数量,我们必须对活动的onCreateEngineOptions()方法中的EngineOptions进行简单调整:

@Override
public EngineOptions onCreateEngineOptions() {
  mCamera = new Camera(0, 0, 800, 480);

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

  engineOptions.getAudioOptions().setNeedsSound(true);
  engineOptions.getAudioOptions().getSoundOptions().setMaxSimultaneousStreams(2);

  return engineOptions;
}

它是如何工作的…

默认情况下,Engine对象的AudioOptions被设置为允许在我们的应用中创建的每个Sound对象同时有五个声音流。在大多数情况下,这不会对不太依赖声音回放的应用造成任何明显的性能损失。另一方面,倾向于在碰撞时产生声音或对物体施加力的游戏可能容易受到同时播放大量声音流的影响,尤其是在任何给定时间场景中有超过 100 个精灵的游戏中。

限制同步声音流的数量是一项容易完成的任务。只需在我们的EngineOptions上调用getAudioOptions().getSoundOptions().setMaxSimultaneousStreams(n),其中n是每个Sound对象的最大流数,我们就可以在不方便的时候减少游戏中不必要的声音。

另见

  • 第一章和【引擎游戏结构】中的介绍声音和音乐部分

创建精灵池

GenericPool类是安卓游戏设计中非常重要的一部分,考虑到移动平台在硬件资源方面相对有限。在安卓游戏开发中,在漫长的过程中获得流畅游戏体验的关键是创建尽可能少的对象。这并不一定意味着我们应该将自己限制在屏幕上的四五个对象,这意味着我们应该考虑回收已经创建的对象的选项。这就是对象池发挥作用的地方。

开始…

参考代码包中名为SpritePool的类。

怎么做…

GenericPool类利用了一些有用的方法,这使得回收对象以备后用变得非常容易。我们将介绍这里使用的主要方法。

构建SpritePool类:

public SpritePool(ITextureRegion pTextureRegion, VertexBufferObjectManager pVertexBufferObjectManager){
  this.mTextureRegion = pTextureRegion;
  this.mVertexBufferObjectManager = pVertexBufferObjectManager;
}
  1. 分配池项目:

    java @Override protected Sprite onAllocatePoolItem() { return new Sprite(0, 0, this.mTextureRegion, this.mVertexBufferObjectManager); }

  2. 获取池项目:

    ```java public synchronized Sprite obtainPoolItem(final float pX, final float pY) { Sprite sprite = super.obtainPoolItem();

    sprite.setPosition(pX, pY); sprite.setVisible(true); sprite.setIgnoreUpdate(false);
    sprite.setColor(1,1,1);

    return sprite; } ```

  3. 回收池项目:

    ```java @Override protected void onHandleRecycleItem(Sprite pItem) { super.onHandleRecycleItem(pItem);

    pItem.setVisible(false); pItem.setIgnoreUpdate(true); pItem.clearEntityModifiers(); pItem.clearUpdateHandlers(); } ```

它是如何工作的…

GenericPool类的想法很简单。我们可以告诉池分配有限数量的对象并存储它们以供以后使用,而不是在需要时创建新对象,并在用完它们后丢弃它们。我们现在可以从池中调用obtainPoolItem()方法来获得一个存储的分配对象,用于我们的关卡,可能作为敌人。例如,一旦那个敌人被玩家消灭,我们现在可以调用recyclePoolItem(pItem)将那个敌人的物体送回池子里。这使我们能够避免垃圾收集调用,并有可能大大减少新对象所需的内存。

中的四种方法怎么做...区段是使用平均池时所需的全部内容。显然,我们必须先创建池,然后才能使用它。然后,以下三种方法定义了在对象分配、获取对象以供使用的情况下会发生什么,以及一旦对象被回收,或者当我们用完它时被发送回存储在池中,直到我们需要一个新对象时会发生什么。然而,对象池不仅仅可以用于 sprite 回收,所以我们将从构造函数开始,更深入地讨论每种方法的作用,它们是如何实现的,以及它们为什么实现。

在第一步中,我们必须传递池对象的构造函数所需的任何对象。在这种情况下,我们需要获得一个TextureRegionVertexBufferObjectManager来创建精灵对象。这并不是什么新鲜事,但是请记住GenericPool类并不仅限于为精灵创建水池。我们可以为任何类型的对象或数据类型创建池。关键注意事项是使用池的构造函数作为一种方法来获取要传递给池的对象分配的必要参数。

第二步,我们覆盖 onAllocatePoolItem()方法。池将在需要分配新对象时随时调用此方法。两种情况是池中最初没有对象,或者所有回收的对象都已获得并正在使用。在这个方法中,我们需要注意的是返回对象的一个新实例。

第三步涉及obtain方法,用于从池中检索一个对象,以便在我们的游戏中使用。我们可以看到这种情况下的 obtainPoolItem()方法需要我们传入pXpY参数,供精灵的setPosition(pX, pY)方法使用,以便重新定位精灵。我们继续将精灵的visibility设置为true,允许更新精灵,并将颜色设置回初始值白色。在任何情况下,该方法都应该用于将对象的值重置回默认状态,或者定义对象的必要属性。在代码中,我们可能会从池中获得一个新的 sprite,如下面的代码片段所示:

// obtain a sprite and attach it to the scene at position (10, 10)
Sprite sprite = pool.obtainPoolItem(10, 10);
mScene.attachChild(sprite);

在最后一种方法中,我们将从GenericPool类使用 recyclePoolItem(pItem)方法,其中pItem是要回收回池中的对象。这种方法应该照顾到所有与在我们的游戏中禁用对象相关的方面。在子画面方面,为了在子画面存储在池中时提高性能,我们将可见性设置为 false,忽略子画面的更新,清除任何实体修饰符和更新处理器,以便一旦我们获得新的子画面,它们就不会继续运行。

即使不使用泳池,也可以考虑在不再需要的Entity上使用setVisible(false)setIgnoreUpdate(true)并排。不断连接和分离Entity对象可能会为垃圾收集器提供运行的机会,并可能在游戏过程中导致明显的帧率中断。

还有更多…

创建池来处理对象回收对于减少性能问题非常重要,但是当游戏第一次初始化时,池中没有任何对象可供使用。这意味着,根据池需要分配多少对象来满足整个级别中对象的最大数量,玩家可能会注意到在游戏开始的几分钟内突然爆发的帧速率中断。为了避免这样的问题,最好在关卡加载时预先分配池对象,以避免在游戏中创建任何对象。

为了在加载过程中分配大量的池项目,我们可以在任何扩展GenericPool的类上调用batchAllocatePoolItems(pCount),其中pCount是我们希望分配的项目数量。请记住,加载比我们需要的更多的项目是对资源的浪费,但是如果我们没有分配足够的项目,它也会导致帧率的打嗝。例如,为了确定在我们的游戏中应该分配多少敌人对象,我们可以想出一个公式,比如默认敌人数量乘以等级难度。然而,所有的游戏都是不同的,创建对象所需的公式也是不同的。

另见

  • 第二章与实体合作中的用精灵将场景带入生活

使用精灵组减少渲染时间

精灵组是任何 AndEngine 游戏的一个很好的补充,该游戏在任何给定的时间处理场景中数百个可见的精灵。SpriteGroup类允许我们通过将许多 sprite 渲染调用分组到有限数量的 OpenGL 调用中来消除大量开销。如果校车接一个孩子,让他们在学校下车,然后接下一个孩子,重复这个过程,直到所有的孩子都在学校,这个过程需要更长的时间来完成。用 OpenGL 绘制精灵也是如此。

开始…

参考代码包中名为ApplyingSpriteGroups的类。这个食谱需要一个名为marble.png的图像,宽 32 像素,高 32 像素。

怎么做…

当在我们的游戏中创建一个SpriteGroup时,我们可以把它们当作一个Entity层,这个层是专门为Sprite对象设计的。以下步骤解释了如何创建Sprite对象并将其附着到SpriteGroup上。

  1. 创建子画面组可以通过以下代码实现:

    ```java // Create a new sprite group with a maximum sprite capacity of 500 mSpriteGroup = new SpriteGroup(0, 0, mBitmapTextureAtlas, 500, mEngine.getVertexBufferObjectManager());

    // Attach the sprite group to the scene mScene.attachChild(mSpriteGroup); ```

  2. 将精灵附加到精灵组是一个同样简单的任务:

    ```java // Create new sprite Sprite sprite = new Sprite(tempX, tempY, spriteWidth, spriteHeight, mTextureRegion, mEngine.getVertexBufferObjectManager());

    // Attach our sprite to the sprite group mSpriteGroup.attachChild(sprite); ```

它是如何工作的…

在这个食谱中,我们设置了一个场景,大约有 375 个精灵应用到我们的场景中,这些精灵都是通过使用mSpriteGroup对象绘制的。一旦创建了精灵组,我们基本上可以把它当作一个普通的实体层,根据需要附加精灵。

  • Create a BuildableBitmapTextureAtlas for our sprite in the onCreateResources( method of our activity:

    ```java // Create texture atlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 32, 32, TextureOptions.BILINEAR);

    // Create texture region mTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, getAssets(), "marble.png");

    // Build/load texture atlas mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder(0, 0, 0)); mBitmapTextureAtlas.load(); ```

    创建纹理用于SpriteGroup可以像我们处理普通的精灵一样处理。

  • Construct our mSpriteGroup object and apply it to the scene:

    ```java // Create a new sprite group with a maximum sprite capacity of 500 mSpriteGroup = new SpriteGroup(0, 0, mBitmapTextureAtlas, 500, mEngine.getVertexBufferObjectManager());

    // Attach the sprite group to the scene mScene.attachChild(mSpriteGroup); ```

    SpriteGroup需要两个我们还没有处理过的新参数。SpriteGroup是一个Entity子类型,所以我们已经知道前两个参数是定位SpriteGroup的 x 和 y 坐标。对于第三个参数,我们传递了一个BitmapTextureAtlas精灵组只能包含与精灵组共享相同纹理图谱的精灵!第四个参数是SpriteGroup能够抽取的最大容量。如果容量是 400,那么我们最多可以给SpriteGroup申请 400 个精灵。将容量限制在我们希望绘制的最大精灵数量是很重要的。超过限制将导致应用的强制关闭

  • 最后一步是将精灵应用到精灵组。

在这个食谱中,我们设置了一个循环,以便将精灵应用到屏幕上的不同位置。然而,我们真正感兴趣的是下面用来创建Sprite并将其附加到SpriteGroup的代码:

Sprite sprite = new Sprite(tempX, tempY, spriteWidth, spriteHeight, mTextureRegion, mEngine.getVertexBufferObjectManager());

// Attach our sprite to the sprite group
mSpriteGroup.attachChild(sprite);

我们可以像创建其他精灵一样创建自己的精灵。我们可以像往常一样设置位置、比例和纹理区域。准备好棘手的部分吧!我们必须调用mSpriteGroup.attachChild(sprite)才能让mSpriteGroup对象处理子画面对象的绘制。仅此而已!

按照这些步骤,我们可以成功地允许我们的精灵组在屏幕上画出很多很多的精灵,甚至在我们的应用中注意到性能下降之前。与用单独的缓冲区单独绘制精灵相比,差别是巨大的。在许多情况下,用户声称在处理一次包含大量实体的游戏时,可以实现高达 50%的改进。

还有更多…

现在还不是时候去运行和转换你所有的项目来使用精灵组!使用雪碧组合的好处不言而喻,但这并不是说没有负面的副作用。OpenGL 不直接支持SpriteGroup类。这个类或多或少是一个“黑客”,它允许我们通过额外的渲染调用来节省一些时间。由于“副作用”,在更复杂的项目中设置精灵组可能会很麻烦。

在附加和分离许多精灵后,有一些情况会利用 alpha 修改器和修改的可见性,导致精灵组中的一些精灵“闪烁”。在越来越多的精灵被附着和分离或者被设置为不可见/可见多次后,这个结果是最明显的。有一种方法不会对性能造成太大影响,这包括将精灵移出屏幕,而不是将它们从图层中分离出来或将它们设置为不可见。然而,对于只利用一个活动并根据当前级别交换场景的大型游戏来说,将精灵移出屏幕可能只会导致未来的问题。

考虑到这一点,并明智地计划,然后再决定使用雪碧组。在将精灵加入游戏之前,测试精灵组对你计划如何使用精灵也有帮助。雪碧小组不会总是引起问题,但这是要记住的事情。此外,AndEngine 是一个开源项目,正在不断更新和增强。保持最新的修正或改进版本。

另见

  • 第 2 章中的【了解和设计实体】部分与实体一起工作
  • 第二章与实体合作中的用精灵将场景带入生活

禁用实体剔除渲染

剔除实体是一种用于防止不必要的实体被渲染的方法。当一个精灵在一个 AndEngine Camera的可视区域内不可见时,这可以导致性能的提高。

怎么做…

对任何预先存在的EntityEntity子类型进行以下方法调用:

entity.setCullingEnabled(true);

它是如何工作的…

剔除实体禁止渲染某些实体,这取决于它们在场景中相对于摄像机可见的场景部分的位置。当我们在一个场景中有很多精灵偶尔会移出摄像机的视野时,这很有用。启用剔除后,那些在相机视图之外的实体将不会被渲染,以避免我们不必要的调用 OpenGL。

请注意,剔除仅发生在那些完全不在摄像机视野内的实体上。这将考虑实体的整个区域,从左下角到右上角。剔除不适用于实体的某些部分,这些部分可能在摄像机的视图之外。

还有更多…

剔除只会停止渲染那些移出Camera可见范围的实体。正因为如此,在所有游戏对象(物品、敌人等)上启用剔除并不是一个坏主意。)不断移出Camera区域。对于具有由较小纹理组成的大背景的实例,剔除也可以大大提高性能,尤其是考虑到背景图像的大小。

剔除确实可以帮助我们节省一些渲染时间,但这并不一定意味着我们应该在所有实体上启用它。毕竟,默认情况下不启用它是有原因的。在平视显示器实体上启用剔除是一个坏主意。将它包含在暂停菜单或其他大型实体中似乎是一个可行的选择,这些实体可能会在相机视图中转换,但这可能会导致移动相机时出现问题。AndEngine 的工作方式是 HUD 永远不会真正随着相机移动,所以如果我们在 HUD 实体上启用剔除,然后将我们的相机向右移动 800 像素(假设我们的相机宽度为 800 像素),我们的 HUD 实体仍然会做出物理响应,就好像它们在我们屏幕上的正确位置,但它们不会渲染。他们仍然会对触摸事件和其他各种场景做出反应,但我们根本不会在屏幕上看到它们。

此外,剔除需要在场景上绘制实体之前增加一层可见性检查。因此,旧设备有可能在实体剔除启用时实际注意到性能损失,而这些实体没有被剔除。这听起来可能不多,但当我们让玩家在几乎不能以每秒 30 帧的速度运行的设备上运行时,很有可能这些额外的可见性检查,例如,200 个精灵,可能就足以将比例向“不方便的游戏性”倾斜。

另见

  • 第二章中的【了解和设计实体】部分与实体一起工作。