Chapter 8. Performance and Memory Optimization

Optimization is one of the most important tasks of any development cycle. It is inevitable, especially for games. Game optimization enhances performance significantly. Through optimization, more hardware platforms can be targeted.

You have already learned that Android supports a range of hardware platforms. Each platform has a separate configuration. By optimizing the use of hardware resources, a game can be run on more hardware platforms. This technique can be applied to visual quality as well. Not all devices have the same quality display, so optimizing the assets for low resolution saves a lot of storage space as well as heap memory during runtime.

In programming, the developer often writes intermediate code and forgets to optimize it later. This may cause a significant amount of performance loss or even cause the game to crash.

We will discuss the scope of various optimizations in Android game development through the following topics:

  • Fields of optimization in Android games
  • Relationship between performance and memory management
  • Memory management in Android
  • Processing segments in Android
  • Different memory segments
  • Importance of memory optimization
  • Optimizing performance
  • Increasing the frame rate
  • Importance of performance optimization
  • Common optimization mistakes
  • Best optimization practices

Fields of optimization in Android games

We all know the requirement of optimization in any development project. In the case of game development, this fact remains the same. In a game development project, the process starts with limited resources and design. After development, the game is expected to be run on maximum possible devices with maximum quality. To achieve that, memory and performance optimization becomes mandatory. So, let's discuss the following four segments of optimization:

  • Resource optimization
  • Design optimization
  • Memory optimization
  • Performance optimization

Resource optimization

Resource optimization is basically optimizing the art, sound, and data files.

Art optimization

We have already discussed many optimization techniques and tools. Here, we will discuss the necessity of art optimization.

Art is visually the most important part in games. Improving the art with bigger and better display quality increases processing and storage costs.

Large textures occupy a large amount of memory. However, scaling up art to fit a bigger resolution screen affects visual quality. So, a balance must be met. Also, various Android devices support various limitations on texture size. Moreover, it takes more time for a shader to work on a larger texture.

One common mistake that developers make is using alpha information for a completely opaque texture. This data increases the texture size significantly.

Art assets can be optimized on the art style. Many developers use flat-colored texture over gradient. Flat color information can be accommodated within 8-bit pixel data. This again saves disk space and processing time.

In spite of these optimization scopes, the developer might not use all of them to increase flexibility in order to create quality visual art without spending much time on optimization.

Sound optimization

Sound is another vital resource for games. Audio may be compressed to save space and effort. A common practice in the Android game industry is to use a compressed format for long audio files.

It takes time to compress and decompress files during runtime. So, using SFX dynamically can be a problem if it is compressed. It can trigger a significant and visible stutter. Developers like to use an uncompressed format for SFX and a compressed format for long and continuous playing sounds such as background music.

Data file optimization

Sometimes, game developers use separate data files to create a flexible project structure to interact with external tools or for better data interface. Such files are commonly in text, XML, JSON, or binary formats. Developers may create their own data format in a binary model.

Binary data can be processed quickly if the correct algorithm is used. There is not much technicality in data optimization. However, developers always need to keep a check on the amount of data and the total file size.

Design optimization

Design optimization is used to increase the scalability, quality experience, flexibility, and durability of the game. The main method is to restructure or modify the game parameters around the core game concept.

Let's divide this section into two parts from the point of view of functionality:

  • Game design optimization
  • Technical design optimization

Game design optimization

A game can be completely different from the initial idea during the game design optimization phase. Design optimization is done based on certain tasks. The developer needs to find different ways to communicate the basic game idea. Then, they can choose the best one, following some analysis.

Game design should be flexible enough to accommodate runtime changes to improve the overall experience and increase user count. A highly optimized game design can be efficient enough to predict user behavior, game performance on various devices, and even monetization.

The game control system design has to be optimized enough to carry out all the tasks easily. Game controls should be easy to spot and understand. For Android touch devices, the placement of controls is also very important.

Technical design optimization

Technical design optimization is limited to the development cycle. It sets the project structure, program structure, development platform dependency, and so on.

The technical design document also specifies the scope and scale of the game. Such specifications help run the game smoothly on a device, because the hardware platform is already covered within the technical design document.

This is a pre-development process. A few assumptions need to be taken care of in this document. These assumptions should be optimized enough to evolve when a real-time situation occurs.

Technical design can also take care of the following tasks during development. By optimizing these tasks, it is much easier to implement and execute:

  • Program architecture
  • System architecture
  • System characteristics
  • Defined dependencies
  • Impacts
  • Risk analysis
  • Assumptions

All these tasks can be optimized for a better development cycle with less effort, and the game will be more polished and will have a higher performance rate.

Memory optimization

Memory optimization is mandatory for any software development procedure. Memory has its physical limitation based on the hardware configuration, but games and applications cannot be made separately for each device.

In a technical design, the range of memory use for the game across all targeted hardware platforms should be mentioned. Now, it is a very common scenario that games take more memory than predicted, which eventually results in the game crashing. The developer is awarded with a memory overflow exception.

To avoid this scenario, there are two main things to be taken care of:

  • Keep memory peak within the defined range
  • Don't keep data loaded in memory unnecessarily

Android uses paging and mapping to manage memory usage. Unfortunately, it does not offer memory swapping. Android knows where to find the paged data and loads accordingly.

Here are some tricks to optimize memory in Android gaming.

Don't create unnecessary objects during runtime

Often, the developer creates an intermediate data object inside a loop. It leaves memory footprints for the garbage collector to collect. Here is an example:

//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.

Use primitive data types as far as possible

User-defined data types take more memory space than primitive data types. Declaring an integer takes less space than embedding an integer in a class. In Android, if the developer uses the Integer class instead of int, the data size increases four times.

For Android compilers (32 bit), int consumes 4 bytes (32 bit), and Integer consumes 16 bytes (128 bit).

With full respect to modern age Android devices, limited use of this data type may cause no significant harm to memory. However, extensive use of non-primitive data types may cause a significant amount of memory block until the developer or garbage collector frees the memory.

So, the developer should avoid enum and use static final int or byte instead. enum, being a user-defined data type, takes more memory than a primitive data type.

Don't use unmanaged static objects

In older Android versions, it is a common issue that a static object does not get destroyed automatically. Developers used to manage static objects manually. This issue is no longer there in newer versions of Android. However, creating many static objects in games is not a good idea as the life span of static objects is equal to the game life. They directly block memory for a longer period.

Using too many static objects may lead to memory exceptions, eventually crashing the game.

Don't create unnecessary classes or interfaces

Each class or interface has some extra binding space in its instance. The modular programming approach demands maximum possible breakage in the coding structure. This is directly proportional to the number of classes or interfaces. This is considered to be a good programming practice.

However, this has a consequence on memory usage. More classes consume more memory space for the same amount of data.

Use the minimum possible abstraction

Many developers use abstraction in multiple layers for a better programming structure. It is very useful to restrict a certain part of a custom library and provide only selective APIs. When it comes to game development, if the developer works on games only, then use of abstraction is not very necessary.

Abstraction results in more instructions, which directly leads to more processing time and more memory use. So, even if abstraction may be convenient sometimes, the developer should always think twice before using abstraction while developing games.

For example, a game may have a set of various enemies. In such a case, creating a single enemy interface and implementing it for different enemy objects helps create a simple and convenient program hierarchy. However, there may be completely different attributes for different enemies. So, the use of abstraction will depend on the game design. Whatever the case is, if developers use abstraction, then it will always increase the set of instructions to be processed at runtime.

Keep a check on services

Services are useful for the completion of one task in the background, but they are very costly in terms of both process and memory. A developer should never keep a service running unless required. The best way to automatically manage the service life cycle is to use IntentService, which will finish once its work is done. For other services, it is the developer's responsibility to make sure that stopService or stopSelf are being called after the task is done.

This process proves to be very efficient for game development, as it actively supports dynamic communication between the user and developer.

Optimize bitmaps

Bitmaps are the heaviest assets for a game. In game development, most of the heap memory is used by bitmaps. So, optimizing bitmaps can significantly optimize the use of heap memory during runtime.

Usually, the memory required for a bitmap to be loaded in memory is given by this formula:

BitmapSize = BitmapWidth * BitmapHeight * bytePerPixel

For example, if a 480 x 800 size bitmap is being loaded in the ARGB_8888 format (4 bytes), the memory will be as follows:

BitmapSize = 480 x 800 x 4 = 1536000 bytes ~ 1.5mb

The format can be of the following types in Android:

  • ARGB_8888 (4 bytes)
  • RGB_565 (2 bytes)
  • ARGB_4444 (2 bytes) (deprecated in API level 13)
  • ALPHA_8 (1 byte)

Each bitmap will occupy memory according to the preceding formula. So, it is recommended that you load a bitmap in memory as per requirement to avoid unnecessary heap usage.

Release unnecessary memory blocks

As we have discussed earlier for freeing memory, the same can be applied on any object. After the task is finished, the instance should be set to null so that the garbage collector can identify and free the allocated memory.

In a game state machine, the class structure should provide an interface to free the memory of instantiated objects. There may be a scenario where a few of the member objects are done with their tasks and a few are still in use, so it would be a bad idea to wait for the entire class instance to be freed. The developer should selectively free the memory of unused objects without deleting the class instance.

Use external tools such as zipalign and ProGuard

The ProGuard tool is efficient at shrinking, optimizing, and obfuscating the code by removing unused code and renaming classes, fields, and methods with a secured and encoded naming structure. ProGuard can make the code more compact, which directly impacts RAM usage.

In game development, developers often use many multiple third-party libraries, which may be pre-compiled with ProGuard. In those cases, the developer must configure ProGuard to exclude those libraries. It is also a good idea to protect the codebase from getting stolen.

zipalign can be used to realign the released APK. This optimizes the APK further to use less space and have a more compact size. Normally, most of the APK building frameworks provide zipalign automatically. However, the developer might need to use it manually for few cases.

Performance optimization

Performance means how smoothly the game will run on the target platform and maintain a decent FPS throughout the gameplay session. In the case of Android gaming, we already know about the wide range of hardware configurations. Maintaining the same performance across all devices is practically impossible. This is the reason developers choose target hardware and minimum hardware configuration to ensure that the game is performing well enough to be published. However, the expectation also varies from device to device.

In real development constraints, performance optimization is limited to the targeting set of hardware. Thus, memory has its own optimizing space in the development process.

Technically, from the programming point of view, performance optimization can be done by paying more attention to writing and structuring code:

  • Using minimum objects possible per task
  • Using minimum floating points
  • Using fewer abstraction layers
  • Using enhanced loops wherever possible
  • Avoiding getters/setters of variables for internal use
  • Using static final for constants
  • Using minimum possible inner classes

Using minimum objects possible per task

Creating unnecessary objects increases processing overhead as they have to be initialized in a new memory segment. Using the same object for the same task multiple times is much faster. Here is an example:

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());
    }
  }
}

Using minimum floating points

In machine-level language, there is nothing like an integer or float. It is always a bit indicating true or false (0 and 1 in technical language). So, an integer can be directly represented by a set of bits, but floating points requires extra processing overhead.

Until a point of time, there was no use of floating points in programming languages. Later, the conversion came, and floating point was introduced with extra processing requirements.

Using fewer abstraction layers

It is very obvious that abstraction demands extra processing per layer. So, as we increase the abstraction layers, the process becomes slower.

Using enhanced loops wherever possible

In the case of array and list parsing, an enhanced for loop works way faster than the usual conventional for loop as it has no iterating variable system, and each array or list element can be accessed directly.

Here is an example of a non-enhanced loop:

int[] testArray = new int[] {0, 1, 2, 3, 5};
for (int i = 0; i < testArray.length; ++ i)
{
  Log.d("EXAMPLE", "value is " + testArray[i]);
}

Here is an example of an enhanced loop:

int[] testArray = new int[] {0, 1, 2, 3, 5};
for (int value : testArray)
{
  Log.d("EXAMPLE", "value is " + value);
}

Avoid getter/setters of variables for internal use

Getters and setters are used to access or change the state of any internal element of an object from outside the object. In high-level reasoning, it does not follow the basic concept of data encapsulation. However, getters and setters are used widely in Android game development.

In many cases, developers use getters and setters from inside the class object. This unnecessarily increases processing time, resulting in degraded performance. So, developers should use getters and setters as little as possible and make sure they are not being used internally.

Use static final for constants

Constants are not meant to be changed during runtime. In the case of global constants, the data is directly associated with the class object. Hence, we're required to parse the class object in order to access it.

Using static is an excellent idea to get rid of this extra process. Element accessibility increases significantly when using static for constants. However, the developer needs to keep a check on memory usage as well.

Using minimum possible inner classes

Each inner class adds an extra layer to processing. Sometimes, it is good to have inner classes in order to structure the codebase in an efficient and readable way. However, it comes with the cost of processing overhead. So, the developer should use the fewest possible inner classes in order to optimize performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.254.90