优化是任何开发周期中最重要的任务之一。这是不可避免的,尤其是对于游戏来说。游戏优化显著提高了性能。通过优化,可以针对更多的硬件平台。
你已经了解到安卓支持一系列硬件平台。每个平台都有单独的配置。通过优化硬件资源的使用,一款游戏可以在更多的硬件平台上运行。这项技术也可以应用于视觉质量。并非所有设备都具有相同的显示质量,因此针对低分辨率优化素材可以节省大量存储空间以及运行时的堆内存。
在编程中,开发人员经常编写中间代码,后来忘记了优化。这可能会导致大量性能损失,甚至导致游戏崩溃。
我们将通过以下主题讨论安卓游戏开发中各种优化的范围:
- 安卓游戏的优化领域
- 性能和内存管理之间的关系
- 安卓系统中的内存管理
- 安卓系统中的处理段
- 不同的内存段
- 内存优化的重要性
- 优化性能
- 提高帧速率
- 性能优化的重要性
- 常见的优化错误
- 最佳优化实践
我们都知道任何开发项目中优化的要求。在游戏开发的情况下,这个事实保持不变。在游戏开发项目中,这个过程从有限的资源和设计开始。开发后,该游戏有望在尽可能多的设备上以最高质量运行。为了实现这一点,内存和性能优化成为强制性的。因此,让我们讨论以下四个优化环节:
- 资源优化
- 设计最优化
- 内存优化
- 性能优化
资源优化基本上就是优化美术、声音、数据文件。
我们已经讨论了许多优化技术和工具。在这里,我们将讨论美术优化的必要性。
艺术在视觉上是游戏中最重要的部分。以更大和更好的显示质量改进艺术会增加处理和存储成本。
大纹理占用大量内存。然而,放大图片以适应更大分辨率的屏幕会影响视觉质量。所以,必须达到一个平衡。此外,各种安卓设备支持纹理大小的各种限制。此外,着色器处理更大的纹理需要更多的时间。
开发人员犯的一个常见错误是将 alpha 信息用于完全不透明的纹理。该数据显著增加了纹理大小。
艺术素材可以在艺术风格上进行优化。许多开发人员在渐变上使用纯色纹理。8 位像素数据可以容纳平面颜色信息。这再次节省了磁盘空间和处理时间。
尽管有这些优化范围,开发人员可能不会使用所有这些来增加灵活性,以便在不花费太多时间优化的情况下创建高质量的视觉艺术。
声音是游戏的另一个重要资源。音频可以压缩,以节省空间和的努力。安卓游戏行业的一个常见做法是对长音频文件使用压缩格式。
运行时压缩和解压缩文件需要时间。因此,动态使用 SFX 可能是一个问题,如果它是压缩的。它会引发明显的口吃。开发人员喜欢对 SFX 使用未压缩格式,对背景音乐等长时间连续播放的声音使用压缩格式。
有时,游戏开发人员使用单独的数据文件来创建灵活的项目结构以与外部工具交互或获得更好的数据接口。这种文件通常是文本、XML、JSON 或二进制格式。开发人员可以在二进制模型中创建自己的数据格式。
如果使用正确的算法,可以快速处理二进制数据。数据优化没有太多技术性。然而,开发人员总是需要检查数据量和总文件大小。
设计优化用于增加游戏的可扩展性、优质体验、灵活性和耐用性。主要方法是围绕核心游戏理念重构或修改游戏参数。
让我们从功能的角度将这一部分分为两部分:
- 游戏设计优化
- 技术设计优化
一款游戏可以完全不同于游戏设计优化阶段的最初想法。设计优化是基于某些任务完成的。开发者需要找到不同的方式来传达基本的游戏理念。然后,他们可以选择最好的,经过一些分析。
游戏设计应该足够灵活,以适应运行时的变化,从而改善整体体验并增加用户数量。高度优化的游戏设计可以足够高效地预测用户行为、各种设备上的游戏性能,甚至货币化。
游戏控制系统的设计必须优化到足以轻松完成所有任务。游戏控制应该易于发现和理解。对于安卓触控设备来说,控件的摆放也非常重要。
技术设计优化仅限于开发周期。设置项目结构、程序结构、开发平台依赖等。
技术设计文件也明确了游戏的范围和规模。这样的规范有助于在设备上流畅地运行游戏,因为硬件平台已经包含在技术设计文档中。
这是一个开发前的过程。本文档中需要注意几个假设。这些假设应该足够优化,以便在实时情况发生时演变。
技术设计在开发期间还可以照顾以下任务。通过优化这些任务,更容易实现和执行:
- 程序体系结构
- 系统结构
- 系统特性
- 已定义的依赖关系
- 影响
- 风险分析
- 假设
所有这些任务都可以用更少的努力优化出更好的开发周期,游戏也会更加打磨,性能提升率也会更高。
内存优化对于任何软件开发程序都是强制性的。内存根据硬件配置有其物理限制,但游戏和应用不能针对每个设备单独制作。
在技术设计中,应该提到游戏在所有目标硬件平台上的内存使用范围。现在,一个非常常见的情况是,游戏占用的内存比预测的要多,这最终会导致游戏崩溃。开发人员被授予内存溢出例外。
为了避免这种情况,有两件事需要注意:
- 将内存峰值保持在定义的范围内
- 不要不必要地将数据加载到内存中
安卓使用分页和映射来管理内存使用。不幸的是,它不提供内存交换。安卓知道在哪里可以找到分页的数据,并相应地加载。
这里有一些优化安卓游戏内存的技巧。
通常,开发人员在循环内部创建一个中间数据对象。它会留下内存足迹供垃圾收集器收集。这里有一个例子:
//Let's have an integer array list and fill some data
List<int> intListFull = new ArrayList<int>();
//Fill data
for( int i = 0; i < 10; ++ i)
{
intListFull.add(i);
}
// No we can have two different approach to print all
// values as debug log.
// Approach 1: not optimized code
for ( int i = 0; i < intListFull.size() ; ++ i)
{
int temp = intListFull.get(i);
Log.d("EXAMPLE CODE", "value at " + i + " is " + temp);
}
// List size will be calculated in each cycle, temp works
//as auto variable and create one memory footprint in each
//loop. Garbage collector will have to clear the memory.
// Approach 2: optimized code
int dataCount = intListFull.size();
int temp;
for ( int i = 0; i < dataCount ; ++ i)
{
temp = intListFull.get(i);
Log.d("EXAMPLE CODE", "value at " + i + " is " + temp);
}
// only two temporary variable introduced to reduce a foot
//print in each loop cycle.
用户定义的数据类型比原始数据类型占用更多的内存空间。声明一个整数比在类中嵌入一个整数占用的空间少。在安卓系统中,如果开发者使用Integer
类而不是int
,数据量会增加四倍。
对于安卓编译器(32 位),int
消耗 4 字节(32 位),Integer
消耗 16 字节(128 位)。
充分尊重现代安卓设备,这种数据类型的有限使用可能不会对内存造成重大损害。然而,大量使用非原语数据类型可能会导致大量内存块,直到开发人员或垃圾收集器释放内存。
所以,开发者应该避开enum
而使用静态最终int
或者byte
来代替。enum
作为用户定义的数据类型,比原始数据类型占用更多的内存。
在旧的安卓版本中,静态对象不会自动销毁是一个常见的问题。开发人员过去常常手动管理静态对象。这个问题在安卓的新版本中已经不存在了。然而,在游戏中创建许多静态对象并不是一个好主意,因为静态对象的寿命等于游戏寿命。它们直接阻塞内存的时间更长。
使用太多静态对象可能会导致内存异常,最终导致游戏崩溃。
每个类或接口在其实例中都有一些额外的绑定空间。模块化编程方法要求编码结构中最大可能的破坏。这与类或接口的数量成正比。这被认为是一个很好的编程实践。
然而,这会对内存使用产生影响。对于相同数量的数据,更多的类消耗更多的内存空间。
许多开发人员在多层中使用抽象来获得更好的编程结构。限制自定义库的某个部分并只提供选择性的 API 是非常有用的。说到游戏开发,如果开发者只在游戏上工作,那么抽象的使用就不是很必要了。
抽象导致更多的指令,这直接导致更多的处理时间和更多的内存使用。因此,即使抽象有时可能很方便,开发人员在开发游戏时使用抽象之前也应该三思。
例如,一个游戏可能有一组不同的敌人。在这种情况下,创建一个单一的敌人接口并为不同的敌人对象实现它有助于创建一个简单方便的程序层次结构。但是,不同的敌人可能有完全不同的属性。所以,抽象的使用将取决于游戏设计。不管是什么情况,如果开发人员使用抽象,那么它总是会增加运行时要处理的指令集。
服务对于在后台完成一个任务是有用的,但是它们在进程和内存方面都非常昂贵。除非需要,否则开发人员永远不应该让服务保持运行。自动管理服务生命周期的最好方法是使用IntentService
,它的工作一旦完成就会结束。对于其他服务,开发人员有责任确保在任务完成后调用stopService
或stopSelf
。
这个过程被证明对游戏开发非常有效,因为它积极支持用户和开发者之间的动态通信。
位图是游戏中最重的素材。在游戏开发中,大部分堆内存被位图使用。因此,优化位图可以显著优化运行时堆内存的使用。
通常,位图加载到内存中所需的内存由以下公式给出:
bitmap size = bitmap width * bitmap height * byteperpixel
例如,如果以ARGB_8888
格式(4 字节)加载 480×800 大小的位图,内存如下:
点阵图大小= 480 x 800 x 4 = 1536000 位元组~ 1.5mb
在安卓系统中,格式可以是以下类型:
ARGB_8888
(4 字节)RGB_565
(2 字节)ARGB_4444
(2 字节)(在 API 级别 13 中已弃用)ALPHA_8
字节数
根据前面的公式,每个位图将占用内存。因此,建议您根据需要在内存中加载位图,以避免不必要的堆使用。
正如我们前面讨论的释放内存,同样的方法可以应用于任何对象。任务完成后,实例应该设置为 null,以便垃圾收集器可以识别并释放分配的内存。
在游戏状态机中,类结构应该提供一个接口来释放实例化对象的内存。可能会有这样一种情况,其中一些成员对象完成了它们的任务,而一些仍在使用中,因此等待整个类实例被释放是一个坏主意。开发人员应该有选择地释放未使用对象的内存,而不删除类实例。
ProGuard 工具通过移除未使用的代码并使用安全和编码的命名结构重命名类、字段和方法,在收缩、优化和混淆代码方面非常有效。ProGuard 可以使代码更加紧凑,这直接影响 RAM 的使用。
在游戏开发中,开发者经常会用到很多多个第三方库,这些库可能是用 ProGuard 预编译的。在这种情况下,开发人员必须配置 ProGuard 来排除这些库。保护代码库不被盗也是一个好主意。
zipalign 可以用来重新对齐释放的 APK。这进一步优化了 APK,以使用更少的空间和更紧凑的尺寸。通常,大多数 APK 建筑框架都自动提供 zipalign。然而,在少数情况下,开发人员可能需要手动使用它。
性能意味着游戏将在目标平台上运行得多么流畅,并在整个游戏过程中保持良好的 FPS。以安卓游戏为例,我们已经了解了广泛的硬件配置。在所有设备上保持相同的性能实际上是不可能的。这就是开发人员选择目标硬件和最低硬件配置的原因,以确保游戏运行良好,足以发布。然而,期望也因设备而异。
在实际的开发约束中,性能优化仅限于目标硬件集。因此,内存在开发过程中有自己的优化空间。
从技术上讲,从编程的角度来看,性能优化可以通过更加关注代码的编写和结构化来实现:
- 使用每个任务可能的最少对象
- 使用最小浮点
- 使用更少的抽象层
- 尽可能使用增强循环
- 避免内部使用变量的获取器/设置器
- 使用静态最终为常数
- 使用尽可能少的内部类
创建不必要的对象会增加处理开销,因为它们必须在新的内存段中初始化。对同一任务多次使用同一个对象要快得多。这里有一个例子:
public class Example
{
public int a;
public int b;
public int getSum()
{
return (a + b);
}
}
//Lets have a look on un-optimized code
// Here one object of Example class is instantiating per loop //cycle
// Same is freed and re-instantiated
public class ExecuterExample
{
public ExecuterExample()
{
for ( int i = 0; i < 10; ++ i)
{
Example test = new Example();
test.a = i;
test.b = i + 1;
Log.d("EXAMPLE", "Loop Sum: " + test.getSum());
}
}
}
// Optimized Code would look like this
// Here only one instance will be created for entire loop
public class ExecuterExample
{
public ExecuterExample()
{
Example test = new Example();
for ( int i = 0; i < 10; ++ i)
{
test.a = i;
test.b = i + 1;
Log.d("EXAMPLE", "Loop Sum: " + test.getSum());
}
}
}
在机器级语言中,没有什么比整数或浮点数更好的了。它总是一个指示真或假的位(在技术语言中是 0 和 1)。因此,一个整数可以直接用一组位来表示,但是浮点运算需要额外的处理开销。
直到有一段时间,在编程语言中没有使用浮点。后来,转换来了,浮点被引入了额外的处理要求。
很明显,抽象要求每层进行额外的处理。因此,随着我们增加抽象层,这个过程会变得更慢。
在数组和列表解析的情况下,增强的for
循环比通常的常规for
循环工作得更快,因为它没有迭代变量系统,并且可以直接访问每个数组或列表元素。
下面是一个非增强循环的示例:
int[] testArray = new int[] {0, 1, 2, 3, 5};
for (int i = 0; i < testArray.length; ++ i)
{
Log.d("EXAMPLE", "value is " + testArray[i]);
}
下面是一个增强循环的示例:
int[] testArray = new int[] {0, 1, 2, 3, 5};
for (int value : testArray)
{
Log.d("EXAMPLE", "value is " + value);
}
Getters 和 setters 用于从对象外部访问或更改对象的任何内部元素的状态。在高级推理中,它不遵循数据封装的基本概念。然而,在安卓游戏开发中,吸气剂和设置剂被广泛使用。
在许多情况下,开发人员使用类对象内部的 getters 和 setters。这不必要地增加了处理时间,导致性能下降。因此,开发人员应该尽可能少地使用 getters 和 setters,并确保它们没有在内部使用。
常量不是在运行时要改变的。在全局常量的情况下,数据直接与类对象相关联。因此,我们需要解析类对象来访问它。
使用静态是摆脱这个额外过程的一个好主意。当对常量使用静态时,元素的可访问性显著提高。然而,开发人员也需要检查内存使用情况。
每个内部类都增加了一个额外的处理层。有时,为了以一种高效和可读的方式构建代码库,拥有内部类是很好的。然而,这伴随着处理开销的成本。因此,开发人员应该使用尽可能少的内部类来优化性能。
在安卓游戏开发中,性能和内存优化往往会产生冲突。为了保持游戏的视觉质量,更好的艺术素材是强制性的,这最终会增加内存开销和性能滞后。
优化内存需要进行频繁的内存操作,导致性能下降。为了提高性能,对象必须随时可供平滑处理。显然,两者都不能在极端水平上适用。
它们之间的平衡是唯一的出路,以优化整个游戏平稳运行,而不会耗尽内存。
我们来讨论一下 Android 中的内存管理系统。它对游戏开发过程有直接的影响。游戏在安卓系统中被视为应用。通常,开发人员在游戏的运行时和最小化状态下都会面临内存问题。要理解工作原理,需要讨论三个主要主题:
- 共享应用内存
- 内存分配和释放
- 应用内存分配
安卓使用 Linux 内核,Linux 使用“共享”页面在运行的进程或服务内共享相同的内存段。例如,安卓经常在进程内共享“代码”内存。通常,外部库和 JVM 的可执行代码内存可以在进程间安全共享,而不会造成死锁。数据页可以在进程间临时共享,直到进程修改共享内存。
安卓为每个应用或进程分配专用内存。这叫做私有内存。同样的过程也可以使用共享内存。安卓会根据两者的总和自动设置一个上限,以确定进程或应用何时会被杀死,尤其是在后台的情况下。这个帽子叫做比例套装尺寸 ( PSS ):
如果一个应用的 PSS 很高,那么这个进程很有可能被安卓杀死。这种情况可以通过编程来控制内存使用,尤其是当应用依赖一些后台活动或服务来执行一些任务时。开发人员必须确保游戏在任何时间点使用尽可能少的内存,尤其是当应用进入后台时。在后台释放不再需要的内存和对象可能是个好主意,当你进入后台时,断开任何不再需要的共享内存。这个会降低你的应用被安卓系统意外杀死的几率。
安卓内存管理系统为每个应用定义了一个虚拟上限,即逻辑堆大小。必要时可以增加,但前提是有可用的空闲内存。然而,这个逻辑堆大小并不是应用实际分配的内存。计算的 PSS 是实际的物理上限,可能在运行时和共享内存依赖期间有所不同。
应用内存使用的物理内存不能超过 PSS。所以,达到这个极限后,如果应用试图分配更多的内存,那么就会收到系统抛出的OutOfMemoryError
。在危急情况下,Android 可能会杀死其他空进程或后台进程,以便为正在运行的应用提供内存。在以下情况下,将取消分配应用内存:
- 如果应用退出
- 如果进程变得不活动,并且其他进程需要内存
- 如果应用因任何原因崩溃
安卓对每个应用的堆大小设置了硬性限制,以维持多任务环境。确切的堆大小限制因硬件配置而异,具体取决于设备的内存容量。如果应用达到堆容量并试图分配更多内存,它将收到OutOfMemoryError
,应用将被安卓杀死。
开发人员需要检查设备上的可用内存量,然后确定平均目标内存使用量。开发者可以通过调用getMemoryClass()
向操作系统查询这个内存量。这将返回一个整数,指示应用堆可用的 MBs 数量。
就功能而言,游戏基本上是一个应用。安卓平台上可以运行多个应用或游戏。然而,对于游戏来说,在一个时间点只有一个游戏是活动的,但是其余的应用在后台运行。
让我们来看看安卓是如何处理其应用的。
安卓设置正在运行的应用的优先级,,可以根据需要杀死一个正在运行的低优先级应用。
每个应用都使用一些内存和处理带宽。可能会出现多个应用同时运行的情况。如果一个新的应用想要运行,那么安卓会为新应用分配内存和进程带宽。如果没有足够的带宽或进程可用,那么安卓会以低优先级杀死一个或多个正在运行的应用。
安卓通过以下状态设置优先级:
活动进程基本上是一个非常频繁地与平台通信并在前台运行的进程。这个过程是最后一个被安卓杀死的,必要的时候。
主动过程满足以下标准:
- 它在前景中运行
- 它是可见的
- 至少有一个安卓活动正在运行
- 它主动与用户界面交互
- 所有事件处理程序都处于活动状态
这个过程基本上是一个不在前台,不与用户界面交互的活动过程。这是安卓平台的第二高优先级。
这一过程的标准如下:
- 它在后台运行
- 它有明显的活性
- 它不与用户界面交互
- 用户界面事件处理程序不活动
- 过程事件处理程序处于活动状态
活动服务是支持没有可见界面的正在进行的过程的服务。安卓会先扼杀这样的服务,然后是实际的活跃进程。
这项服务遵循以下标准:
- 它没有可见的界面
- 它支持或工作于各自的活动进程
- 它在后台运行
后台进程基本都是最小化或者不活跃的进程。这些过程在屏幕上不可见。进程线程不为这些进程运行,但是应用状态保存在内存中。这些很容易被处理器杀死。这些过程可以在中断后恢复。
这些是不活动/最小化的流程。它们留在记忆中。应用保持暂停状态。
空进程也称为空进程。一个空的过程实际上是空的。它在内存中没有应用数据或状态。该进程具有最高优先级,以便被操作系统杀死。
安卓应用服务是实际应用流程的一部分。这些服务可以在父进程内外运行。
让我们澄清关于服务的两个非常常见的误解:
- 服务不是一个单独的过程
- 服务不是线程
事实是,服务是应用流程的一部分,而不是单独的流程。服务不是线程。它们是后台运行的进程的一部分,即使主应用处于挂起状态,它们也会继续运行。
服务旨在执行单个任务,不回调父应用。这就是为什么它们甚至可以在应用关闭后运行。
服务由父应用启动,如下所示:
Context.startService();
启动后,服务开始在后台执行单个任务。任务完成后,服务可以自行停止。例如,一个简单的文件下载服务将在下载任务成功后停止。许多游戏开发人员在他们的游戏中使用这样的功能来改善用户体验。
这些服务可以与一个或多个进程绑定,以实现交互性。应用可以从绑定的服务发送请求并获取响应,这就创建了一个服务器-客户端体系结构。但是这些绑定的服务只有有限的生命周期,直到最后一个应用组件与服务绑定。
安卓有自己的资源流程结构。它有一些预定义的资源类型:
- 可提取的资源
- 布局资源
- 颜色资源
- 菜单资源
- 补间动画资源
- 其他资源
所有可绘制资源都属于这一类,包括帧动画。安卓提供了所有可绘制资源的res/drawable/
项目路径。所有位图、各种 XML 和预定的帧动画都可以放在这里。
这些可以通过R.drawable
类访问。
所有定义的布局都属于这一类。安卓提供了专用于所有布局文件的res/layout/
项目路径。布局对于定义应用用户界面很有用。
这些可以通过R.layout
类访问。
颜色资源基本上是在改变适用对象的视图时由于改变而产生的颜色列表。安卓将此存储在层次结构中的res/color/
文件夹中。
这些可以通过R.color
类访问。
所有菜单内容都可以在这里定义。安卓提供了专用于所有可绘制资源的res/menu/
项目路径。
这些可以通过R.menu
类访问。
所有 tween 动画资源都属于这一类。安卓提供了专用于所有补间动画资源的res/anim/
项目路径。
这些可以通过R.anim
类访问。
所有其他资源都是res/values/
文件夹中的位置。许多开发人员用样式来定义这个类别下的字符串。
这些可以通过R.values
类访问。
在应用运行期间,根据行为使用三种主要的内存段:
- 栈存储器
- 堆内存
- 寄存器存储器
处理期间的所有自动变量和运行时分配将存储在堆栈内存段中。垃圾收集器在使用后释放内存。因此,没有与堆栈内存相关联的手动内存管理过程。
然而,大量使用自动变量也可能导致内存错误。这就是为什么我们已经讨论过为什么最小化不必要的自动变量声明是必要的。
堆栈内存也用于执行程序指令。每个指令被分解成一个操作,并由解释器放入堆栈。然后,使用递归过程来执行所有的指令堆栈并返回结果。
让我们看看堆栈内存是如何为对象和原语工作的:
public class ExampleClass
{
public ExampleClass()
{
int bitMapCount = 0; // primitive type
Bitmap testBmp = BitmapFactory.decodeFile("bitmap path"); // Object loading
bitMapCount = 1;
}
}
在这个例子中,bitMapCount
是一个int
局部变量,直接存储在堆栈中。用于此变量的内存将在作用域之后释放。
但是testBmp
是位图对象,会在堆中分配,但是引用会存储在栈中。当程序指针超出作用域时,引用将被自动删除,垃圾收集器可以识别为testBmp
分配的堆内存为零引用,并将释放该内存段。
堆内存是存储类和数组的所有实例的段。JVM 在实例化任何对象时分配这个内存。
在应用运行时,垃圾收集器不会在此内存段上自动运行。使用后释放内存是开发人员的责任。在安卓的情况下,垃圾收集器只会在运行的应用中没有内存段的引用时释放内存。
游戏素材是存储在这个内存段中的主要元素。艺术是其中最重要的素材。因此,优化位图会直接影响堆内存的使用。通常,开发人员会为素材分配内存,并且不会破坏引用。这将导致内存块在整个运行期间被占用。
这里有一个例子:
// create a bitmap in a class constructor having global
// scope with public access
public class ExampleClass
{
public Bitmap testBmp;
public ExampleClass()
{
testBmp = BitmapFactory.decodeFile("bitmap path");
}
}
在本例中,位图的内存即使在使用后也会被占用,直到内存中有ExampleClass
实例。解释器没有释放内存段的固定指令,因为testBmp
仍然引用分配给位图的内存。
我们可以通过以下方式进行一些修改来优化:
public class ExampleClass
{
public Bitmap testBmp;
public ExampleClass()
{
testBmp = BitmapFactory.decodeFile("bitmap path");
}
// create a method to free memory allocated for the
// bitmap after use
public void unloadBitmap()
{
testBmp = null;
}
}
在这种情况下,通过调用unloadBitmap()
后使用的位图将从testBmp
中移除加载位图的引用。因此,垃圾收集器会发现这个内存位置是零引用内存,并释放它用于其他分配。
在安卓开发的情况下,开发者一定不用担心注册内存。寄存器与处理器直接相关,处理器将最重要和最常用的数据存储在该内存段中。
寄存器内存是任何应用运行时使用的最快内存段。
无论游戏如何,看起来多么好,或者设计得多么好,如果游戏没有在目标平台上运行,那么就不可能成功。我们已经知道安卓有各种各样的硬件配置。
硬件的主要变化特定于处理器和内存。就处理器而言,这取决于它们的速度和质量。在内存或内存的情况下,它只是卷。
即使在今天,安卓设备中的内存也可以从 512 兆字节到 4 GB 不等。根据设计,内存优化应该始终有最小的内存目标。因此,为了在最小可用内存上运行游戏,内存优化非常重要。
有时,开发人员将峰值使用量限制在内存的目标限度内。然而,它们是在测试设备上执行的,而测试设备大部分时间并不投射实时场景。总是有误差的。所以,如果游戏运行在一定限度的内存上,它总是会有相同的内存,这并不总是正确的。这是内存优化发挥主要作用的地方。它在为游戏在实时场景中运行创建缓冲区范围方面有很大帮助。
可能会出现应用内存不足的情况,即使它不需要所需的内存量。这清楚地表明应用正在遭受内存泄漏。内存泄漏是游戏开发中最常见的问题之一。适当优化内存有助于解决这个问题。
内存优化的另一个方面是增加游戏留在后台的概率。当一个应用进入后台时,如果它需要为其他前台应用释放内存空间,安卓可能会杀死该应用。内存优化确保应用在运行时占用尽可能少的内存。因此,对于使用较少内存的应用,可以将状态数据在缓存中保存更长的时间。
许多游戏在后端使用游戏服务。如果应用不是活动的,那么服务很有可能也会被操作系统杀死。
之前,我们仅从编程的角度讨论了性能优化。让我们讨论一下优化安卓游戏性能的其他范围。
开发人员可以通过以下几点优化从设计到开发的性能:
- 选择基本分辨率
- 定义便携性范围
- 程序结构
- 管理数据库
- 管理网络连接
从安卓上的游戏开发来看,选择基础分辨率大概是最有意义的设计决策。基本分辨率定义图形或视觉元素的比例。开发人员选择的分辨率越大,存储和处理时间就越长。基本分辨率也是位图存储的质量和颜色信息的原因。相对较低的分辨率不需要可见素材中的许多细节,这可以优化位图数据。然而,随着分辨率的提高,需要更多的数据来保留细节。最终,这将对处理产生重大影响。
随着技术的进步,安卓设备的分辨率越来越大,越来越好。因此,开发人员现在选择更大的分辨率来支持更大范围的设备。
这也是设计阶段的优化。在这个阶段,开发者需要决定支持的硬件平台范围。这包括各种配置。我们已经知道,安卓设备系列在内存、处理速度、图形质量等方面包含了大量变化。
如果该系列支持类似设备的便携性范围,则优化变得更加容易。然而,大多数游戏开发案例并非如此。通常,开发人员应该将优化分为三个部分:
- 低性能设备
- 性能一般的设备
- 高性能设备
因此,理想情况下,应该有三层优化来正确定义可移植性范围。
程序结构是性能和内存优化的另一个非常重要的技术设计决策。这包括编程优化的所有参数,我们已经讨论过了。
此外,程序层次结构对性能也很重要。通常,开发人员会创建不必要的中间调用来解析几个层。这里有几个单独的类有助于显著优化性能。适当的游戏状态机设计也有助于优化性能。
以数据驱动为主的游戏有很多。在这种情况下,需要正确管理数据库。
例如,问答游戏必须在某个服务器的数据库中维护一个题库,以避免频繁更新游戏版本。数据库查询需要时间来执行,因为两者之间还有一个网络层。因此,游戏层向数据库发送查询。然后,数据库获取数据,相应地绑定数据,并将其发送回游戏。然后,游戏必须解除接收数据的绑定才能使用它。使用最少的查询调用是最小化性能开销的唯一方法。使用更快的数据库也有助于游戏表现良好。
现代游戏已经将增强为多人游戏和服务器控制的机制,这减少了频繁更新游戏版本的工作。在这两种情况下,网络连接都需要以适当的方式实现。目前主要遵循两种类型的多人游戏架构:
- 回合制多人游戏
- 实时多人游戏
管理基于回合的多人游戏系统比管理实时多人游戏相对容易。还有一种多人模式叫做异步多人模式。
每次网络调用都会导致性能滞后,因为游戏依赖于来自服务器的数据。因此,需要优化客户端-服务器架构,以实现以下目标:
- 滞后时间更短
- 较少的层处理
- 到服务器的 pings 次数减少
性能优化的最终目标是提高帧率。高帧率自动提供流畅的游戏。然而,开发者必须确保从流畅度和效果来看,帧率效果在游戏中是可见的。
对于当前的移动游戏行业,2D 游戏或中型 3D 游戏的平均 FPS 为 60 被视为高性能。另一方面,大型 3D 游戏可能会认为 30-35 的平均 FPS 是良好的性能。
FPS 更高的高性能游戏为进一步的视觉效果打开了一扇门,以改善用户体验。这对货币化有直接影响。
正如我们刚刚讨论的,性能优化直接影响帧率,而帧率又直接影响游戏体验。然而,性能优化还有其他的重要性:
- 由于程序未优化,游戏可能会崩溃或进入无响应状态
- 性能优化对内存也有直接影响
- 性能优化可以扩大支持的硬件平台范围
游戏行业现在是增长最快的行业之一。为了跟上速度,在市场上站稳脚跟,许多公司计划在有限的优化下缩短开发周期。在这种情况下,开发人员经常会有意或无意地犯下以下错误:
- 编程错误
- 设计错误
- 错误的数据结构
- 错误地使用游戏服务
编程是一个手动过程,犯错是人之常情。所以,很明显,没有针对游戏的无 bug 且完全优化的编程。然而,很少有方法可以让程序员最大限度地减少错误,从而拥有优化的游戏代码库。让我们讨论一下程序员在安卓系统中开发游戏时犯的主要错误。
程序员经常创建许多临时变量,却忘了跟踪它们。通常,这些变量会占用不必要的内存并增加处理调用。
排序在游戏开发中被广泛用于许多目的。有几种排序算法。大多数时候,开发人员选择方便的技术,而不是高效的技术。对于大型数组或列表,这可能会导致流程严重滞后。
使用太多静态实例来简化可访问性是另一个不好的做法。使用静态可能有助于加快处理速度,但创建许多静态实例并不是一个好主意,因为它会在其生命周期内阻塞大量内存空间。许多程序员甚至忘记手动释放内存。
创建抽象层并广泛使用它们会使过程变慢。然而,一般来说这是一个很好的编程实践,但是对于游戏编程来说,它只在有限的情况下有所帮助。
方便的循环使用是游戏的另一个糟糕的编程实践。有几种方法可以处理循环。程序员应该首先确定什么最适合算法。
游戏编程更多的是逻辑开发,而不是技术开发。为某些任务建立完美的逻辑可能需要时间。许多游戏程序员不考虑用多种方式完成一项任务。大多数时候,它留下了一个很大的优化范围未被探索。
设计师在定义硬件范围和游戏范围时经常会犯错误。这两者都是创建优化游戏设计的非常重要的因素。
另一个错误是以错误的目标分辨率为目标。目标分辨率对艺术品素材规模有直接影响。瞄准错误的分辨率会导致不必要的缩放,从而导致额外的处理开销。
数据结构是游戏编程不可避免的一部分。Android 支持动态数组初始化。然而,许多开发人员更喜欢列表来存储数据。列表比数组慢得多。只有在绝对必要的时候才应该使用列表。
开发人员有责任为数据驱动的游戏找出完美的数据结构。正确的技术设计应该包括数据结构模型及其使用。
服务有时非常有用。在现代游戏行业中,服务用于下载/上传数据、推送通知、游戏中的深度链接或服务器连接。然而,服务的代价是巨大的处理和内存消耗。运行服务也会导致大量功耗。
因此,只有在别无选择的情况下,使用服务才应该是强制性的。
一些定义的和逻辑的优化技术是可用的。我们将讨论与安卓游戏开发相关的主要范围和领域:
- 游戏设计限制
- 游戏开发优化
- 游戏数据结构模型
- 使用游戏素材
- 处理缓存数据
定义目标硬件平台并承认限制始终是最佳实践。技术设计可以根据它构建开发约束。
可扩展性和可移植性也应该在设计游戏时决定。这应该给开发人员一个暂时的平台限制以及其他约束。我们已经讨论了设计优化。所有这些部分都应该在开发之前进行评估。
在设计游戏和创建布局时,目标屏幕尺寸和分辨率必须是固定的,这将适合多种分辨率。这是因为安卓有许多屏幕尺寸,如前所述。
选择最低安卓版本和目标安卓版本可以让开发人员在构建开发项目时获得优势,因为支持的应用编程接口级别和平台特性已经定义好了。
这是优化最重要的环节之一。以下是一些通过优化成功执行开发过程的技巧:
- 使用尽可能多的安卓提供的文件夹结构来实现项目的可扩展性。
- 根据 Android 提供的 dpi 列表使用资源格式。
- 开发人员应该避免缩放图像。这有效地减少了内存和处理开销。
- 将精灵用于多种目的也是创建动画的好方法。
- 平铺技术在减少内存消耗方面非常有用。
- 覆盖
onDraw()
方法始终是刷新旧渲染管道和使用绝对要求的系统绘制顺序的好方法。 - 尽可能使用基于 XML 的布局;然而,游戏对这个安卓功能的使用范围非常有限。
数据结构从一开始就是游戏程序设计不可避免的部分之一,与游戏的规模无关。每个游戏总是为了各种目的处理数据,例如排序、搜索、存储等等。
有许多数据结构模型可用于各种操作。每种操作都有自己的优缺点。开发人员必须根据需求选择效率最高的。
让我们以数组和链表之间的数据存储比较为例。实际上,链表比数组更灵活和动态。然而,这个特性是以处理速度慢和内存消耗高为代价的。
开发人员可能不总是需要存储动态数据。例如,如果板球队的数据需要存储,那么一个数组就足够了,因为每一边总是有 11 个玩家,并且在游戏过程中不能修改。在这种特殊情况下,它将使过程比使用链表更快、更有效。
在另一种情况下,对于射击游戏,开发者无法预测用户在游戏过程中可能发射的子弹数量。因此,为了处理所有发射的子弹,队列数据结构将是最有效的。
同样,只要符合目的,就可以选择堆栈和树结构。排序和搜索算法可以采用相同的方法。
我们已经为游戏分类了素材。让我们从优化技术和最佳实践的角度来讨论它们。
单独的优化技术可以应用于一组艺术素材。艺术素材是游戏的脸面。所以,视觉效果必须足够吸引人,才能开始游戏。
正如我们已经讨论过的,更好的艺术品素材会消耗内存和性能。但是,这可以最小化到一定程度。艺术素材优化有几种工具。但是,使用不适当的工具会导致数据丢失,最终导致视觉质量差。
艺术永远不应该从视觉质量的角度妥协。通常,艺术家开发的素材由于不适当的优化而不能完美地反映在游戏中。
我们已经讨论了艺术素材应该如何制作。现在,让我们假设一些艺术只使用 8 位数据空间作为原始格式,但同样以 24 位格式导出。然后,开发人员可以使用工具将素材优化为典型的 8 位格式,而不会影响视觉质量。
该规则也适用于完全不透明素材。开发者可以去除透明度信息,以便拥有优化的艺术素材。
音频素材也是独立素材。音频已经成为扩展用户体验的一项非常重要的素材。音频配置会随着频率、位深度和压缩技术的不同而变化。配置中的每个变化都有不同的处理和内存消耗级别。
所以,音频优化也是优化过程中非常重要的一环。安卓游戏开发行业的常规做法是为 SFX 和音乐文件选择两种不同格式的音频。
开发人员通常忽略的一件事是音频信息数据。很少有安卓设备有一定的频率上限,但是当使用更多的频率时,声音通常是好的。因此,确定安卓游戏声音的上限是一个技术设计层面的步骤。所以,每个声音都应该在附近发出。
音响设计师需要将质量保持在极限之内。这样,音频素材可以在开发时得到优化。
除了艺术和音频,游戏中可能还有其他数据素材。数据格式可以是任何形式,如二进制、文本、XML、JSON 或自定义。自定义格式与二进制格式基本相同,但有一些加密。
在游戏开发中,单独使用数据集是一种常见的做法。单独的数据集有助于构建项目,并为不同的输出提供使用相同代码的灵活性。通常,开发者更新数据源以更新完整的游戏体验,而无需创建新的 APK。从长远来看,这减少了开发时间,以便维护游戏和进行简单的更新。
从优化的角度来看,这些数据源应该进行足够的优化,以便快速处理,并且不会消耗太多内存。但是,读写外部文件需要时间。通常,二进制文件处理速度最快,大小最小。然而,在读取二进制数据后,它必须被解析才能在游戏中使用,这最终会增加处理量。
最常用的数据格式是 XML 和 JSON。安卓库支持两者,包括一个通用解析器。开发人员无需额外的处理工作就可以获得现成的数据。然而,数据可以在游戏过程中被操纵,这取决于游戏的要求。
高速缓存是一个内存段,从功能的角度来看,它类似于内存,但比传统的内存运行得更快。处理器可以更快地访问这个段。因此,从逻辑上讲,缓存应该只存储经常使用的数据。
处理缓存数据的最佳方法是检查应用的内存使用情况。通常,操作系统至少应该有 10 %的可用内存。经过测试,一个应用平均可以使用总可用内存的 2%。
但是,开发人员无法从技术上控制缓存。它们只能确保最常用的元素以完美的方式优化,以便执行器自动为它们使用缓存。
优化是任何软件开发中最重要的任务之一,尤其是在游戏中,逻辑编程主导着技术编程。有很多优化工具和技术可用于技术编程,因为它有最常见的算法要实现。然而,在游戏编程的情况下,每个游戏都指示一组不同的算法。在许多情况下,人工智能算法也是单独制作的。因此,程序员很有可能必须找到一种有效的方法来优化新编写的算法。
我们已经讨论了安卓游戏开发中所有可能的优化范围。技术优化是强制性的,因为它有固定的指导方针要遵循。然而,逻辑发展将取决于游戏算法及其要求。所以,对于游戏开发者来说,优化安卓游戏是一项额外的工作。
有时候,开发者过度优化游戏。不建议这样做。过度优化通常会降低游戏质量。所以,在技术设计的时候,应该申报优化案例。
大多数大规模开发过程都有单独定义的优化任务集。一些开发人员选择开发一个动态优化过程。这意味着开发者在不同的阶段、不同的尺度上优化游戏。这两个过程都是有效的,但是第一个过程在逻辑上更合理,因为定义一个单独的任务总是会给出一个关于整体优化的暂定持续时间的想法。这有助于以更好的方式管理整个游戏开发过程。
所有优化过程都通过测试阶段进行验证。所有的设计、工程和艺术作品都在游戏开发的这个环节进行测试。我们将在本书的下一章更深入地研究测试。