Optimization Approaches for Project Assets

Tutorial

·

advanced

·

+10XP

·

60 mins

·

(349)

Unity Technologies

Optimization Approaches for Project Assets

It’s common for a project to grow beyond a designer’s initial vision, or for that initial vision to be more resource-intensive than originally thought. Much of a project’s optimization will be unique to its design, but even the simplest project can benefit from optimization. In this tutorial, you will learn about different approaches to take for optimizing projects.

Languages available:

1. Optimization Approaches for Project Assets

Verified: Unity 2019.4 LTS


It’s common for a project to grow beyond a designer’s initial vision, or for that initial vision to be more resource-intensive than originally thought. Much of a project’s optimization will be unique to its design, but even the simplest project can benefit from optimization.


The less work Unity has to do to load, access, and unload your assets, the more optimally your project will run. Some tips for better memory handling include:


  • All 2D (Sprites and UI) graphic elements should be packed into Sprite Atlases. A Sprite Atlas is a large texture that contains multiple Sprites and lets the graphics processing unit (GPU) process them in batches rather than individually. In the case of games with large amounts of graphics, Sprite Atlases can be separated by character, world, or purpose. To learn more about the Sprite Atlas, see Introduction to Sprite Atlas.

  • All textures that are not packed in a Sprite Atlas should have dimensions that are powers of 2 in order to take advantage of memory handling optimizations on the GPU. They don’t necessarily need to be square, but being square and a power of 2 further aids optimization.

  • Longer audio such as background music should have its Load Type in Import Settings set to Streaming rather than Decompress on Load or Compressed in Memory. The tradeoff is a potential slight delay in starting, but the benefit is that only a fraction of RAM is necessary to stream vs. loading and decompressing the entire song in memory.

If you have limited assets, creating some controlled randomized parameters can be a simple way to add variety. Some examples include:


A single sound effect can sound like several different takes by randomly varying the pitch by a small amount, for example 5 percent in either direction. The code snippet below is one possible implementation:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

A single or small set of Sprites or particles can look like more by simply varying the color of the renderer. To set a GameObject’s Sprite and color randomly, attach the following script:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Non-player characters (NPCs), weapons, or anything that can be assembled piece-by-piece, can be built of randomly selected different parts. Individual parts are quicker to make than full assets and can be assembled in combinations the designer may never have thought of. In the following implementation, the Random Assembly component would be attached to a player or NPC. Manually assign your SpriteRenderer components by dragging them into their respective slots in the Inspector. Finally, drag as many Sprites as you’d like for each option. During Start, the character is assembled from random pieces. This could be extended to a character creation utility, where pieces are specifically chosen by the user, and/or combined with the above script to add color as well as configuration.


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Finally, there are designer-friendly ways to optimize the management of resources in code.


It’s not uncommon to discover potentially show-stopping lag as a project approaches final quality, making it necessary to restructure its design. One common solution is the adoption of object pooling. In short, object pooling recycles objects when possible rather than creating new ones. To learn more about object pooling, see this Unity tutorial. Pooling can dramatically improve the user experience, but converting a project late in production means potentially rebuilding hundreds of prefabs or ScriptableObjects. In a game project, this could be spells that fire projectiles. For example, say we have 200 spells that all need this conversion. Rather than redesign the spell in a way that forces the designer to rebuild all 200, we can change the way the spell uses the already supplied data.


The following code is excerpted for illustration, and is not intended to represent complete scripts.


Before conversion, the projectile code for a simple spell might look like this:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

The spell holds a projectile as a data member and instantiates a new projectile each time it’s activated. This seems innocent enough, but the demands it creates from instantiating and destroying new objects can quickly overwhelm a system’s resources. We could convert the spell using the following code, but because myProjectile and myProjectilePool are different data types and variable names, and we are no longer using the previously designated SpellProjectile, we are essentially throwing it away. This means that the myProjectilePool data member would need to be specified for all 200 spells.


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop


Instead, let’s make the SpellProjectilePool a private data member that gets added to our spell’s GameObject automatically. We’ll then use our previously specified projectile Prefab to be handled by the projectile pool.


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop


This allows a seamless conversion to using object pooling without needing to redo previous prefab setup or wasting previous work.


Any time a number of MonoBehaviours duplicate functionality, consider moving them to a single script. For example, it’s common in games to have many types of pickup (an object your character touches to “pick up” that modifies their abilities or statistics in some way). Rather than a separate script for health, ammunition, gear, etc., make a universal pickup MonoBehaviour and use either an enum or ScriptableObjects for the abilities. Here’s an example implementation using enum:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

This solution is fine if you have a fixed or limited number of pickup types. The more cases (pickup types) you add, the more chances there are to introduce bugs as the pickup code becomes messier. The benefit of using a ScriptableObject to describe a pickup is that you only need to write the pickup code once, and all unique functionality is handled in the PickupAbility ScriptableObjects. In that scenario, the pickup code would look like this:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

When the pickup is activated, its ActivateOn method is called. This is a universal method that all inheritors of the base class PickupAbility must implement. Here’s the code for the PickupAbilty:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop


The gold pickup:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Because the only requirement for the “pickup” to activate is a player touching it, this could also be used for in-game events. For example, the line in ActivateOn could be changed to:


[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Consolidating duplicate functionality optimizes the addition and debugging of new features.


As your experience with Unity grows, you’ll find yourself incorporating optimization into your workflow from the beginning, rather than as an afterthought.


Complete this tutorial