- Unity 2017 Game Optimization(Second Edition)
- Chris Dickinson
- 872字
- 2021-07-02 23:21:08
Avoid Find() and SendMessage() at runtime
The SendMessage() method and family of GameObject.Find() methods are notoriously expensive and should be avoided at all costs. The SendMessage() method is about 2,000 times slower than a simple function call, and the cost of the Find() method scales very poorly with Scene complexity since it must iterate through every GameObject in the Scene. It is sometimes forgivable to call Find() during initialization of a Scene, such as during an Awake() or Start() callback. Even in this case, it should only be used to acquire objects that we know for certain already exist in the Scene and for scenes that have only a handful of GameObjects in them. Regardless, using either of these methods for interobject communication at runtime is likely to generate a very noticeable overhead and potentially dropped frames.
Relying on Find() and SendMessage() is typically symptomatic of poor design, inexperience in programming with C# and Unity, or just plain laziness during prototyping. Their usage has become something of an epidemic among beginner-level and intermediate-level projects, so much so that Unity Technologies feels the need to keep reminding users to avoid using them in a real game over and over again in their documentation and at their conferences. They only exist as a less programmer-y way to introduce new users to interobject communication, and for some special cases where they can be used in a responsible way (which are few and far between). In other words, they’re so ridiculously expensive that they break the rule of not preoptimizing our code, and it’s worth going out of our way to avoid using them if our project is going beyond the prototyping stage (which is a distinct possibility since you’re reading this book).
To be fair, Unity targets a wide demographic of users, from hobbyists, students, professionals, to those with delusions of grandeur, and also in team sizes from inpidual developers to hundreds of people on the same team. This results in an incredibly wide range of software development ability. When you're starting out with Unity, it can be difficult to figure out on your own what you should be doing differently, especially given how the Unity engine does not adhere to the design paradigms of many other game engines we might be familiar with. It has some foreign and quirky concepts surrounding scenes and Prefabs, does not have a built-in God Class entry point, nor any obvious raw data storage systems to work with.
A God Class is a fancy name for the first object we might create in our application and whose job would be to create everything else we need based on the current context (what level to load, which subsystems to activate, and so on). These can be particularly useful if we want a single centralized location that controls the order of events as they happen during the entire lifecycle of our application.
This is an important topic not just for performance, but also for any real-time event-driven system design (including, but not limited to games), so it is worth exploring the subject in some detail, evaluating some alternative methods for interobject communication.
Let's start by examining a worst case example, which uses both Find() and SendMessage() to communicate between objects, and then look into ways to improve upon it.
The following is a class definition for a simple EnemyManagerComponent that tracks a list of GameObjects representing enemies in our game, and provides a KillAll() method to destroy them all when needed:
using UnityEngine;
using System.Collections.Generic;
class EnemyManagerComponent : MonoBehaviour {
List<GameObject> _enemies = new List<GameObject>();
public void AddEnemy(GameObject enemy) {
if (!_enemies.Contains(enemy)) {
_enemies.Add(enemy);
}
}
public void KillAll() {
for (int i = 0; i < _enemies.Count; ++i) {
GameObject.Destroy(_enemies[i]);
}
_enemies.Clear();
}
}
We would then place a GameObject in our Scene containing this Component, and name it EnemyManager.
The following example method attempts to instantiate a number of enemies from a given Prefab, and then notifies the EnemyManager object of their existence:
public void CreateEnemies(int numEnemies) {
for(int i = 0; i < numEnemies; ++i) {
GameObject enemy = (GameObject)GameObject.Instantiate(_enemyPrefab,
5.0f * Random.insideUnitSphere,
Quaternion.identity);
string[] names = { "Tom", "Dick", "Harry" };
enemy.name = names[Random.Range(0, names.Length)];
GameObject enemyManagerObj = GameObject.Find("EnemyManager");
enemyManagerObj.SendMessage("AddEnemy",
enemy,
SendMessageOptions.DontRequireReceiver);
}
}
Initializing data and putting method calls inside any kind of loop, which always output to the same result, is a big red flag for poor performance, and when we're dealing with expensive methods, such as Find(), we should always look for ways to call them as few times as possible. Ergo, one improvement we can make is to move the Find() call outside of the for-loop and cache the result in a local variable so that we don’t need to keep reacquiring the EnemyManager object over and over again.
Moving the initialization of the names variable outside of the for-loop is not necessarily critical, since the Compiler is often smart enough to realize it doesn't need to keep reinitializing data that isn't being changed elsewhere. However, it does often make code easier to read.
Another big improvement we can implement, is to optimize our usage of the SendMessage() method by replacing it with a GetComponent() call. This replaces a very costly method with an equivalent and much cheaper alternative.
This gives us the following result:
public void CreateEnemies(int numEnemies) {
GameObject enemyManagerObj = GameObject.Find("EnemyManager");
EnemyManagerComponent enemyMgr = enemyManagerObj.GetComponent<EnemyManagerComponent>();
string[] names = { "Tom", "Dick", "Harry" };
for(int i = 0; i < numEnemies; ++i) {
GameObject enemy = (GameObject)GameObject.Instantiate(_enemyPrefab,
5.0f * Random.insideUnitSphere,
Quaternion.identity);
enemy.name = names[Random.Range(0, names.Length)];
enemyMgr.AddEnemy(enemy);
}
}
If this method is called during the initialization of the Scene, and we're not overly concerned with loading time, then we can probably consider ourselves finished with our optimization work.
However, we will often need new objects that are instantiated at runtime to find an existing object to communicate with. In this example, we want new enemy objects to register with our EnemyManagerComponent so that it can do whatever it needs to do to track and control the enemy objects in our Scene. We would also like the EnemyManager to handle all enemy-related behavior, so that objects calling its functions don't need to perform work on its behalf. This will improve the Coupling (how well our codebase separates related behavioru) and Encapsulation (how well our classes prevent outside changes to the data they manage) of our application. The ultimate aim is to find a reliable and fast way for new objects to find existing objects in the Scene without unnecessary usage of the Find() method, so that we can minimize complexity and performance costs.
There are multiple approaches we can take to solving this problem, each with their own benefits and pitfalls:
- Assign references to preexisting objects
- Static Classes
- Singleton Components
- A global Messaging System
- FuelPHP Application Development Blueprints
- C語言程序設計(第2 版)
- Hands-On Machine Learning with scikit:learn and Scientific Python Toolkits
- TensorFlow Lite移動端深度學習
- INSTANT FreeMarker Starter
- Instant RubyMotion App Development
- Modern JavaScript Applications
- 組態軟件技術與應用
- C/C++數據結構與算法速學速用大辭典
- Java EE企業級應用開發教程(Spring+Spring MVC+MyBatis)
- Java程序設計與項目案例教程
- OpenCV Android開發實戰
- 數字媒體技術概論
- Web開發新體驗
- Python繪圖指南:分形與數據可視化(全彩)