34,79 €
Competition within the gaming industry has become significantly fiercer in recent years with the adoption of game development frameworks such as Unity3D. Through its massive feature-set and ease-of-use, Unity helps put some of the best processing and rendering technology in the hands of hobbyists and professionals alike. This has led to an enormous explosion of talent, which has made it critical to ensure our games stand out from the crowd through a high level of quality. A good user experience is essential to create a solid product that our users will enjoy for many years to come.
Nothing turns gamers away from a game faster than a poor user-experience. Input latency, slow rendering, broken physics, stutters, freezes, and crashes are among a gamer's worst nightmares and it's up to us as game developers to ensure this never happens. High performance does not need to be limited to games with the biggest teams and budgets.
Initially, you will explore the major features of the Unity3D Engine from top to bottom, investigating a multitude of ways we can improve application performance starting with the detection and analysis of bottlenecks. You'll then gain an understanding of possible solutions and how to implement them. You will then learn everything you need to know about where performance bottlenecks can be found, why they happen, and how to work around them.
This book gathers a massive wealth of knowledge together in one place, saving many hours of research and can be used as a quick reference to solve specific issues that arise during product development.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Seitenzahl: 446
Veröffentlichungsjahr: 2015
Copyright © 2015 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
First published: November 2015
Production reference: 1281015
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham B3 2PB, UK.
ISBN 978-1-78588-458-0
www.packtpub.com
Author
Chris Dickinson
Reviewers
Clifford Champion
Dr. Sebastian T. Koenig
Acquisition Editor
Indrajit Das
Content Development Editor
Athira Laji
Technical Editor
Prajakta Mhatre
Copy Editor
Charlotte Carneiro
Project Coordinator
Bijal Patel
Proofreader
Safis Editing
Indexer
Rekha Nair
Graphics
Jason Monteiro
Production Coordinator
Aparna Bhagat
Cover Work
Aparna Bhagat
Chris Dickinson grew up in England with a strong passion for science, mathematics, and video games. He received his master's degree in physics with electronics from the University of Leeds in 2005, and immediately traveled to California to work on scientific research in the heart of Silicon Valley. Finding that career path unsuitable, he began working in the software industry.
Over the last decade, he has made a career in software development, becoming a senior software developer. Chris has primarily worked in software automation and internal test tool development, but his passion for video games never fully faded. In 2010, he took the path of discovering the secrets of game development and 3D graphics by completing a second degree—a bachelor's degree in game and simulation programming. He authored a tutorial book on game physics (Learning Game Physics with Bullet Physics and OpenGL by Packt Publishing). He continues to work in software development, creating independent game projects in his spare time with tools such as Unity 3D.
I've managed to grasp an absolutely ridiculous amount of knowledge in just 5 years. None of this would have been possible without the constant motivation from my coworkers, tutors, friends, and family.
Thanks to my comrades in software development for being so understanding of my erratic schedule while I was learning game development at school.
Thanks also to my college tutors for helping accelerate me past the core material and learn so much about game development so quickly.
Thanks to my friends for always being a constant source of harassment and inquisitiveness about what I've been working on.
Thanks to my family for giving me the opportunity to learn, live, and love so much in such a short time.
And of course, thanks to my wonderful wife and best friend, Jamie, for being so caring and supportive of all the late nights and helping me stay creative.
Clifford Champion has a broad background in software engineering, with years of experience spanning 3D games, Internet applications, and artificial intelligence. He holds degrees in mathematics and computer science from UCLA and UCSD, respectively. In the past, Clifford worked for video game technology company Havok (now part of Microsoft), and interactive media and design company PlainJoe Studios. Currently, he leads a software team at zSpace (zspace.com), a VR company specializing in 3D for classrooms and industry.
Clifford can be found on Twitter at @duckmaestro and welcomes discussions on any topic.
Dr. Sebastian T. Koenig received his PhD in human interface technology from the University of Canterbury, New Zealand, developing a framework for individualized virtual reality cognitive rehabilitation. He obtained his diploma in psychology from the University of Regensburg, Germany, in the areas of clinical neuropsychology and virtual reality rehabilitation.
Sebastian is the founder and CEO of Katana Simulations, where he oversees the design, development, and evaluation of cognitive assessment and training simulations. His professional experience spans over 10 years of clinical work in cognitive rehabilitation and over 8 years of virtual reality research, development, and user testing. He has extensive experience as a speaker at international conferences and as a reviewer of scientific publications in the areas of rehabilitation, cognitive psychology, neuropsychology, software engineering, game development, game user research, and virtual reality.
Sebastian has developed numerous software applications for cognitive assessment and training. For his work on the virtual memory task, he was awarded the prestigious Laval Virtual Award in 2011, for the Medicine and Health category. Other applications of his include the virtual reality executive function assessment in collaboration with the Kessler Foundation, New Jersey, USA, and the patent-pending Microsoft Kinect-based motor and cognitive training JewelMine/Mystic Isle at the USC Institute for Creative Technologies, California, USA.
He maintains the website at www.virtualgamelab.com, which features his research and his software development projects. His website also contains a comprehensive list of tutorials for the Unity game engine.
For support files and downloads related to your book, please visit www.PacktPub.com.
Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.PacktPub.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at <[email protected]> for more details.
At www.PacktPub.com, you can also read a collection of free technical articles, sign up for a range of free newsletters and receive exclusive discounts and offers on Packt books and eBooks.
https://www2.packtpub.com/books/subscription/packtlib
Do you need instant solutions to your IT questions? PacktLib is Packt's online digital book library. Here, you can search, access, and read Packt's entire library of books.
If you have an account with Packt at www.PacktPub.com, you can use this to access PacktLib today and view 9 entirely free books. Simply use your login credentials for immediate access.
User experience is a critical component of any game. User experience includes not only our game's story and its gameplay, but also how smoothly the graphics run, how reliably it connects to multiplayer servers, how responsive it is to user input, and even how large the final application file size is due to the prevalence of app stores and cloud downloads. The barrier to entering game development has been lowered considerably thanks to the release of cheap, AAA-industry-level game development tools such as Unity. However, the features and quality of the final product that our players expect us to provide is increasing with every passing day. We should expect that every facet of our game can and will be scrutinized by players and critics alike.
The goals of performance optimization are deeply entwined with user experience. Poorly optimized games can result in low frame rates, freezes, crashes, input lag, long loading times, inconsistent and jittery runtime behavior, physics engine breakdowns, and even excessively high battery power consumption (particularly important in this era of mobile devices). Having just one of these issues can be a game developer's worst nightmare as reviews will tend to focus on the one thing that we did badly, in spite of all the things that we did well.
Performance is all about making the best use of the available resources, which includes the CPU resources such as CPU cycles and main memory space, Graphics Processing Unit (GPU) resources such as memory space (VRAM) and memory bandwidth, and so on. But optimization also means making sure that no single resource causes a bottleneck at an inappropriate time, and that the highest priority tasks get taken care of first. Even small, intermittent hiccups and sluggishness in performance can pull the player out of the experience, breaking immersion and limiting our potential to create the experience we intended.
It is also important to decide when to take a step back and stop making performance enhancements. In a world with infinite time and resources, there would always be another way to make it better, faster, or easier to maintain. There must be a point during development where we decide that the product has reached acceptable levels of quality. If not, we risk dooming ourselves to implementing further changes that result in little or no tangible benefit.
The best way to decide if a performance issue is worth fixing is to answer the question "will the user notice it?". If the answer to this questions is "no", then performance optimization would be a wasted effort. There is an old saying in software development:
Premature optimization is the root of all evil
Premature optimization is the cardinal sin of reworking and refactoring code to enhance performance without any proof that it is necessary. This could mean either making changes without showing that a performance problem even exists (answering the question of whether or not it would be noticeable to the user), or making changes because we only believe a performance issue might stem from a particular area before it has been proven to be true. Making these mistakes has cost software developers, as a collective whole, a depressing number of work hours for nothing.
This book intends to give us the tools, knowledge, and skills we need to both detect and fix performance issues in our application, no matter where they stem from. This could be hardware components such as the CPU, GPU, or RAM, within software subsystems such as Physics, Rendering, or within the Unity Engine itself. In addition, the more resources we save, the more we can do within the Unity Engine with the same hardware system, allowing us to generate more interesting and dynamic gameplay.
This will give our game a better chance of succeeding and standing out from the crowd in a marketplace that is inundated with new, high quality games every single day.
Chapter 1, Detecting Performance Issues, provides an exploration of the Unity Profiler and a series of methods to profile our application, detect performance bottlenecks, and perform root cause analysis.
Chapter 2, Scripting Strategies, deals with the best practices for our Unity C# Script code, minimizing Component overhead, improving inter-object communication, and more.
Chapter 3, The Benefits of Batching, explores Unity's Dynamic and Static Batching systems to ease the burden on the rendering system.
Chapter 4, Kickstart Your Art, helps you understand the underlying technology behind our art assets and learn how to avoid common pitfalls with importing, compression, and encoding.
Chapter 5, Faster Physics, is about investigating the nuances of Unity's physics system for both 3D and 2D games, and how to properly organize our physics objects for improved performance.
Chapter 6, Dynamic Graphics, provides an in-depth exploration of the rendering system, and how to improve applications that suffer rendering bottlenecks in the GPU, or CPU, and specialized techniques for mobile devices.
Chapter 7, Masterful Memory Management, examines the inner workings of the Unity Engine, the Mono Framework, and how memory is managed within these components to protect our application from heap allocations and runtime garbage collection.
Chapter 8, Tactical Tips and Tricks, deals with a multitude of useful techniques used by professionals to improve project workflow and scene management.
The majority of this book will focus on features within the context of Unity version 5.x. Most of the techniques can be applied to Unity 4.x projects, but may require an upgrade to Unity 4 Pro Edition in order to access some of them (such as Occlusion Culling, Static Batching, and even the Profiler itself).
This book is intended for intermediate and advanced Unity developers who have experience with most of Unity's feature set, and those who want to maximize the performance of their game or solve particular bottlenecks. Whether the bottleneck is caused by CPU overload, runtime spiking, slow memory access, fragmentation, garbage collection, poor GPU fillrate, or memory bandwidth, this book will teach you the techniques you need to identify the source of the problem and help explore multiple ways of reducing their impact on your application.
Familiarity with the C# language will be needed for sections involving scripting and memory usage, and a basic understanding of cg will be needed for areas involving Shader optimization.
In this book, you will find a number of text styles that distinguish between different kinds of information. Here are some examples of these styles and an explanation of their meaning.
Code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "The main Unity callback for application updates is the Update() function."
A block of code is set as follows:
When we wish to draw your attention to a particular part of a code block, the relevant lines or items are set in bold:
New terms and important words are shown in bold. Words that you see on the screen, for example, in menus or dialog boxes, appear in the text like this: "The threshold value for the sleeping state can be modified under Edit | Project Settings | Physics | Sleep Threshold."
Warnings or important notes appear in a box like this.
Tips and tricks appear like this.
Feedback from our readers is always welcome. Let us know what you think about this book—what you liked or disliked. Reader feedback is important for us as it helps us develop titles that you will really get the most out of.
To send us general feedback, simply e-mail <[email protected]>, and mention the book's title in the subject of your message.
If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide at www.packtpub.com/authors.
Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase.
You can download the example code files from your account at http://www.packtpub.com for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in the text or the code—we would be grateful if you could report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub.com/submit-errata, selecting your book, clicking on the ErrataSubmissionForm link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded to our website or added to any list of existing errata under the Errata section of that title.
To view the previously submitted errata, go to https://www.packtpub.com/books/content/support and enter the name of the book in the search field. The required information will appear under the Errata section.
Piracy of copyrighted material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works in any form on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy.
Please contact us at <[email protected]> with a link to the suspected pirated material.
We appreciate your help in protecting our authors and our ability to bring you valuable content.
If you have a problem with any aspect of this book, you can contact us at <[email protected]>, and we will do our best to address the problem.
Performance evaluation for most software products is a very scientific process: determine the maximum supported performance metrics (number of concurrent users, maximum allowed memory usage, CPU usage, and so on); perform load testing against the application in scenarios that try to simulate real-world behavior; gather instrumentation data from test cases; analyze the data for performance bottlenecks; complete a root-cause analysis; make changes in the configuration or application code to fix the issue; and repeat.
Just because game development is a very artistic process does not mean it should not be treated in equally objective and scientific ways. Our game should have a target audience in mind, who can tell us the hardware limitations our game might be under. We can perform runtime testing of our application, gather data from multiple components (CPU, GPU, memory, physics, rendering, and so on), and compare them against the desired metrics. We can use this data to identify bottlenecks in our application, perform additional instrumentation to determine the root cause of the issue, and approach the problem from a variety of angles.
To give us the tools and knowledge to complete this process, this chapter will introduce a variety of methods that we will use throughout the book to determine whether we have a performance problem, and where the root cause of the performance issue can be found. These skills will give us the techniques we need to detect, analyze, and prove that performance issues are plaguing our Unity application, and where we should begin to make changes. In doing so, you will prepare yourselves for the remaining chapters where you will learn what can be done about the problems you're facing.
We will begin with an exploration of the Unity Profiler and its myriad of features. We will then explore a handful of scripting techniques to narrow-down our search for the elusive bottleneck and conclude with some tips on making the most of both techniques.
The Unity Profiler is built into the Unity Editor itself, and provides an expedient way of narrowing our search for performance bottlenecks by generating usage and statistics reports on a multitude of Unity3D components during runtime:
With the release ofUnity 5.0, Unity Technologies has made the Profiler available to all developers running the Personal Edition (the new name for the Free Edition).
Users running the Free Edition of Unity 4 must either upgrade to Unity 5, or purchase a license for Unity 4 Pro Edition.
This additional reporting comes with a price, however. Additional instrumentation flags will be enabled within the compiler, generating runtime logging events and a different level of automated code optimization while the Profiler is in use, which causes some additional CPU and memory overhead at runtime. This profiling cost is not completely negligible, and is likely to cause inconsistent behavior when the Profiler is toggled on and off.
In addition, we should always avoid using Editor Mode for any kind of profiling and benchmarking purposes due to the overhead costs of the Editor; its interface, and additional memory consumption of various objects and components. It is always better to test our application in a standalone format, on the target device, in order to get a more accurate and realistic data sample.
Users who are already familiar with connecting the Unity Profiler to their applications should skip to the section titled The Profiler window
We will begin with a brief tutorial on how to connect our game to the Unity Profiler within a variety of contexts:
The only way to access the Profiler is to launch it through the Unity Editor and connect it to a running instance of our application. This is the case whether we're running our game within the Editor, running a standalone application on the local or remote device, or when we wish to profile the Editor itself.
To open the Profiler, navigate to Window | Profiler within the Editor. If the Editor is already running in Play Mode, then we may see reporting data gathering in the Profiler Window:
To profile standalone projects, ensure that the Use Development Mode and Autoconnect Profiler flags are enabled when the application is built.
Selecting whether to profile an Editor-based instance (through the Editor's Play Mode) or a standalone instance (separately built and running in the background) can be achieved through the Active Profiler option in the Profiler window.
The Unity Profiler
Profiling the Editor itself, such as profiling custom Editor Scripts, can be enabled with the Profile Editor option in the Profiler window as shown in the following screenshot. Note that this requires the Active Profiler option to be configured to Editor.
The Profiler can also connect to an instance of the Unity Webplayer that is currently running in a browser. This enables us to profile our web-based application in a more real-world scenario through the target browser, and test multiple browser types for inconsistencies in behavior.
Note that changing the Release Channel option will restart the Webplayer application.
You should now see reporting data collecting in the Profiler window.
The Profiler can also be connected to an active instance of the application running remotely on an iOS device, such as an iPad or iPhone. This can be achieved through a shared WiFi connection. Follow the given steps to connect the Profiler to an Apple device:
Note that remote connection to an Apple device is only possible when the Profiler is running on an Apple Mac device.
You should now see the iOS device's profiling data gathering in the Profiler window.
The Profiler uses ports 54998 to 55511 to broadcast profiling data. Make sure these ports are available for outbound traffic if there is a firewall on the network.
There are two different methods for connecting an Android device to the Unity Profiler: either through a WiFi connection or using the Android Debug Bridge (ADB) tool. ADB is a suite of debugging tools that comes bundled with the Android SDK.
Follow the given steps for profiling over a WiFi connection:
You should now see the Android device's profiling data gathering in the Profiler Window.
For ADB profiling, follow the given steps:
Note that, if the adbdevices command isn't found when it is run from the command prompt, then the Android SDK folder may need to be appended onto the Environment's PATH variable.
You should now see the Android device's profiling data gathering in the Profiler window.
We will now cover the essential features of the Profiler as they can be found within the interface.
Note that this section covers features as they appear in the Unity Profiler within Unity 5. Additional features were added to the Profiler with the release of Unity 5; these may be different, or not exist, in Unity 4's Profiler.
The Profiler window is split into three main areas:
These areas are as shown in the following screenshot:
The top bar contains multiple controls we can use to affect what is being profiled and how deeply in the system data is gathered from. They are:
Note that Deep Profiling requires the project to be recompiled before profiling can begin, so it is best to avoid toggling the option during runtime.
Because this option measures all methods across our codebase in a blind fashion, it should not be enabled during most of our profiling tests. This option is best reserved for when default profiling is not providing enough detail, or in small test Scenes, which are used to profile a small subset of game features.
If Deep Profiling is required for larger projects and Scenes, but the Deep Profile option is too much of a hindrance during runtime, then there are alternatives that can be found in the upcoming section titled Targeted Profiling of code segments.
Profile Editor: This option enables Editor profiling—that is, gathering profiling data for the Unity Editor itself. This is useful in order to profile any custom Editor Scripts we have developed.Note that Active Profiler must be set to the Editor option for this feature to work.
Each Area has multiple colored boxes for various subsections of those components. These colored boxes can be toggled to reveal/hide the corresponding data types within the Timeline View.
Each Area focuses on profiling data for a different component of the Unity engine. When an Area is selected in the Timeline View, essential information for that component will be revealed in the Breakdown View for the currently selected frame.
The Breakdown View shows very different information, depending on which Area is currently selected.
Areas can be removed from the Timeline View by clicking on the 'X' at the top right of an Area. Areas can be restored to the Timeline View through the Add Profiler option in the Controls bar.
This Area shows CPU Usage for multiple Unity subsystems during runtime, such as MonoBehaviour components, cameras, some rendering and physics processes, user interfaces (including the Editor's interface, if we're running through the Editor), audio processing, the Profiler itself, and more.
There are three ways of displaying CPU Usage data in the Breakdown View:
The Hierarchy Mode groups similar data elements and global Unity function calls together for convenience—for instance, rendering delimiters, such as BeginGUI() and EndGUI() calls are combined together in this Mode.
The Raw Hierarchy Mode will separate global Unity function calls into individual lines. This will tend to make the Breakdown View more difficult to read, but may be helpful if we're trying to count how many times a particular global method has been invoked, or determining if one of these calls is costing more CPU/memory than expected. For example, each BeginGUI() and EndGUI() call will be separated into different entries, possibly cluttering the Breakdown View, making it difficult to read.
Perhaps, the most useful mode for the CPU Area is the Timeline Mode option (not to be confused with the main Timeline View). This Mode organizes CPU usage during the current frame by how the call stack expanded and contracted during processing. Blocks at the top of this view were directly called by the Unity Engine (such as the Start(), Awake(), or Update() methods), while blocks underneath them are methods that those methods had called, which can include methods on other Components or objects.
Meanwhile, the width of a given CPU Timeline Block gives us the relative time it took to process that method compared to other blocks around it. In addition, method calls that consume relatively little processing time, relative to the more greedy methods, are shown as gray boxes to keep them out of sight.
The design of the CPU Timeline Mode offers a very clean and organized way of determining which particular method in the call stack is consuming the most time, and how that processing time measures up against other methods being called during the same frame. This allows us to gauge which method is the biggest culprit with minimal effort.
For example, let's assume that we are looking at a performance problem in the following screenshot. We can tell, with a quick glance, that there are three methods that are causing a problem, and they each consume similar amounts of processing time, due to having similar widths.
In this example, the good news is that we have three possible methods through which to find performance improvements, which means lots of opportunities to find code that can be improved. The bad news is that increasing the performance of one method will only improve about one-third of the total processing for that frame. Hence, all three methods will need to be examined and improved in order to minimize the amount of processing time during this frame.
The CPU Area will be most useful during Chapter 2, Scripting Strategies.
The GPU Area is similar to the CPU Area, except that it shows method calls and processing time as it occurs on the GPU. Relevant Unity method calls in this Area will relate to cameras, drawing, opaque and transparent geometry, lighting and shadows, and so on.
The GPU Area will be beneficial during Chapter 6, Dynamic Graphics.
The Rendering Area provides rendering statistics, such as the number of SetPass calls, the total number of Batches used to render the scene, the number of Batches saved from Dynamic and Static Batching, memory consumed for Textures, and so on.
The Rendering Area will be useful in Chapter 3, The Benefits of Batching.
The Memory Area allows us to inspect memory usage of the application in the Breakdown View in two different ways:
The Simple Mode provides only a high-level overview of memory consumption of components such as Unity's low-level Engine, the Mono framework (total heap size that will be garbage-collected), Graphics, Audio (FMOD), and even memory used to store data collected by the Profiler.
The Detailed Mode shows memory consumption of individual game objects and components, for both their native and managed representations. It also has a column explaining the reason for that object consuming memory and when it might be de-allocated.
The Memory Area will be the main focal point of Chapter 7, Masterful Memory Management.
The Audio Area grants an overview of audio statistics and can be used both to measure CPU usage from the audio system, as well as total memory consumed by Audio Sources (for both playing and paused sources) and Audio Clips.
The Audio Area will come in handy as we explore art assets in Chapter 4, Kickstart Your Art.
Audio is often overlooked when it comes to performance enhancements, but audio can become of the biggest sources of bottlenecks if it is not managed properly. It's worth performing occasional checks on the Audio system's memory and CPU consumption during development.
There are two different Physics Areas, one for 3D physics (Nvidia's PhysX) and another for the 2D physics system (Box2D) that was integrated into the Unity Engine in version 4.5. This Area provides various physics statistics such as Rigidbody, Collider, and Contact counts.
We will be making use of this Area in Chapter 5, Faster Physics.
As of the publication of this text, with Unity v5.2.2f1 as the most recent version, the Physics3D Area only provides a handful of items, while the Physics2D Area provides significantly more information.
Good coding practices and project asset management often make finding the root cause of a performance issue relatively simple, at which point the only real problem is figuring out how to improve the code. For instance, if the method only processes a single gigantic for loop, then it will be a pretty safe assumption that the problem is either with the iteration of the loop or how much work is processed each iteration.
Of course, a lot of our code, whether we're working individually or in a group setting, is not always written in the cleanest way possible, and we should expect to have to profile some poor coding work from time to time. Sometimes, hack-y solutions are inevitable, and we don't always have the time to go back and refactor everything to keep up with our best coding practices.
It's easy to overlook the obvious when problem solving and performance optimization is just another form of problem solving. The goal is to use Profilers and data analysis to search our codebase for clues about where a problem originates, and how significant it is. It's often very easy to get distracted by invalid data or jump to conclusions because we're being too impatient or missed a subtle clue. Many of us have run into occasions, during software debugging, where we could have found the root cause of the problem much faster if we had simply challenged and verified our earlier assumptions. Always approaching debugging under the belief that the problem is highly complex and technical is a good way to waste valuable time and effort. Performance analysis is no different.
A checklist of tasks would be helpful to keep us focused on the issue, and not waste time chasing "ghosts". Every project is different and has a different set of concerns and design paradigms, but the following checklist is general enough that it should be able to apply to any Unity project:
Sometimes there are things we expect to see, but don't. These are usually easy to note, because the human brain is very good at pattern recognition. If something doesn't match the expected pattern, then it tends to be very obvious. Meanwhile, there are times where we assume something has been happening, but it didn't. These are generally more difficult to notice, because we're often scanning for the first kind of problem. Verification of the intended order of events is critical, or we risk jumping to conclusions, wasting valuable time.
In the context of Unity, this means it is essential to verify that the script we expect to see the event coming from is actually present in the Scene, and that the method calls happen in the order we intended.
Script presence can be quickly verified by typing the following into the Hierarchy window textbox:
For example, typing t:mytestmonobehaviour (note: it is not case-sensitive) into the Hierarchy textbox will show a shortlist of all GameObjects that currently have a MyTestMonobehaviour script attached as a Component.
Note that this shortlist feature also includes any GameObjects with Components that derive from the given script name.
We should also double-check that the GameObjects they are attached to are still enabled, since we may have disabled them during earlier testing, or someone/something has accidentally deactivated the object.
If we assume that a MonoBehaviour, which is causing performance problems, only appears once in our Scene, then we may ignore the possibility that conflicting method invocations are causing a bottleneck. This is dangerous; what if someone created the object twice or more in the Scene file, or we accidentally instantiated the object more than once from code? What we see in the Profiler can be a consequence of the same expensive method being invoked more than once at the same time. This is something we will want to double-check using the same shortlist method as before.
If we expected only one of the Components to appear in the Scene, but the shortlist revealed more than one, then we may wish to rethink our earlier assumptions about what's causing the bottlenecks. We may wish to write some initialization code that prevents this from ever happening again, and/or write some custom Editor helpers to display warnings to any level designers who might be making this mistake.
Preventing casual mistakes like this is essential for good productivity, since experience tells us that, if we don't explicitly disallow something, then someone, somewhere, at some point, for whatever reason, will do it anyway, and cost us a good deal of analysis work.
Making code changes to the application in order to hunt down performance issues is not recommended, as the changes are easy to forget as time wears on. Adding debug logging statements to our code can be tempting, but remember that it costs us time to introduce these calls, recompile our code, and remove these calls once our analysis is complete. In addition, if we forget to remove them, then they can cost unnecessary runtime overhead in the final build since Unity's Debug logging can be prohibitively expensive in both CPU and memory.
One way to combat this problem is to use a source-control tool to differentiate the contents of any modified files, and/or revert them back to their original state. This is an excellent way to ensure that unnecessary changes don't make it into the final version.
Making use of breakpoints during runtime debugging is the preferred approach, as we can trace the full call stack, variable data, and conditional code paths (for example, if-else blocks), without risking any code changes or wasting time on recompilation.
The Unity Editor has its own little quirks and nuances that can leave us confused by certain issues.
Firstly, if a single frame takes a long time to process, such that our game noticeably freezes, then the Profiler may not be capable of picking up the results and recording them in the Profiler window. This can be especially annoying if we wish to catch data during application/Scene initialization. The upcoming section, Custom CPU Profiling, will offer some alternatives to explore to solve this problem.
One common mistake (that I have admittedly fallen victim to multiple times during the writing of this book) is: if we are trying to initiate a test with a keystroke and we have the Profiler open, we should not forget to click back into the Editor's Game window before triggering the keystroke! If the Profiler is the most recently clicked window, then the Editor will send keystroke events to that, instead of the runtime application, and hence no GameObject will catch the event for that keystroke.
Vertical Sync (otherwise known as VSync) is used to match the application's frame rate to the frame rate of the device it is being displayed on (for example, the monitor). Executing the Profiler with this feature enabled will generate a lot of spikes in the CPU usage area under the heading WaitForTargetFPS, as the application intentionally slows itself down to match the frame rate of the display. This will generate unnecessary clutter, making it harder to spot the real issue(s). We should make sure to disable the VSync colored box under the CPU Area when we're on the lookout for CPU spikes during performance tests. We can disable the VSync feature entirely by navigating to Edit | Project Settings | Quality and then the subpage for the currently selected build platform.
We should also ensure that a drop in performance isn't a direct result of a massive number of exceptions and error messages appearing in the Editor console. Unity's Debug.Log(), and similar methods such as Debug.LogError(), Debug.LogWarning(), and so on, are notoriously expensive in terms of CPU usage and heap memory consumption, which can then cause garbage collection to occur and even more lost CPU cycles.
This overhead is usually unnoticeable to a human being looking at the project in Editor Mode, where most errors come from the compiler or misconfigured objects. But they can be problematic when used during any kind of runtime process; especially during profiling, where we wish to observe how the game runs in the absence of external disruptions. For example, if we are missing an object reference that we were supposed to assign through the Editor and it is being used in an Update() method, then a single MonoBehaviour could be throwing new exceptions every single update. This adds lots of unnecessary noise to our profiling data.
Note that we can disable the Info or Warning checkboxes (shown in the following screenshot) for the project during Play Mode runtime, but it still costs CPU and memory to execute debug statements, even though they are not being rendered. It is often a good practice to keep all of these options enabled, to verify that we're not missing anything important.
This one is simple but absolutely necessary. We should double-check that there are no background processes eating away CPU cycles or consuming vast swathes of memory. Being low on available memory will generally interfere with our testing, as it can cause more cache misses, hard-drive access for virtual memory page-file swapping, and generally slow responsiveness of the application.
If our performance problem isn't solved by the above checklist, then we probably have a real issue on our hands that demands further analysis. The task of figuring out exactly where the problem is located still remains. The Profiler window is effective at showing us a broad overview of performance; it can help us find specific frames to investigate and can quickly inform us which MonoBehaviour and/or method may be causing issues. However, we must still determine exactly where the problem exists. We need to figure out if the problem is reproducible, under what circumstances a performance bottleneck arises, and where exactly within the problematic code block the issue is originating from.
To accomplish these, we will need to perform some profiling of targeted sections of our code, and there are a handful of useful techniques we can employ for this task. For Unity projects, they essentially fit into two categories:
Note that the following section mostly focusses on how to investigate Scripting bottlenecks through C# code. Detecting the source of bottlenecks in other engine components will be discussed in their related chapters.
The Profiler can be controlled in script code through the static Profiler class. There are several useful methods in the Profiler class that we can explore within the Unity documentation, but the most important methods are the delimiter methods that activate and deactivate profiling at runtime: Profiler.BeginSample() and Profiler.EndSample().
Note that the delimiter methods, BeginSample() and EndSample()
