Jump to content
iRobb

Optimización de scripting en Unity

Recommended Posts

Estoy leyendo el libro "Unity Game Optimization" que está en Packt y la tercera edición es de noviembre de 2019. He hecho un resumen de los apartados dedicados a scripting que me parecen muy buenos. Algunos los conoceréis, otros no y otros es posible que no supiérais porqué utilizar una manera u otra. En todos los casos el concepto de optimización ve sus frutos cuando es un desarrollo de cierto tamaño. Desde mi punto de vista, si empiezas con buenos hábitos, luego no tendrás los mismos problemas de rendimiento que el resto de desarrolladores. La optimización se debe realizar desde el día 1. Paso a relatar.

Optimización 1
Eliminar las callbacks vacías de Start() y Update() al crear un script si no son necesarias. El tenerlas afectará a la inicialización de la scene y al instanciar prefabs. La de OnGUI() es especialmente problemática ya que puede llamarse más de una vez por frame. En la de Update() se realiza un Native-Managed Bridge, o sea, hay que enlazar el modo managed de C# con el del código nativo dependiente de plataforma. En general no dejar nunca una callback vacía. 

Optimización 2
Cómo obtener los componentes de un GameObject. Hay tres maneras de hacerlo, aunque a la práctica se utilizan dos de ellas. Los datos hablan por si mismos para 30.000 objetos.

// 6413 ms
test = (TestComponent)GetComponent("TestComponent");
// 89 ms
test = GetComponent<TestComponent>();
// 95 ms
test = (TestComponent)GetComponent(typeof(TestComponent));

El mejor es del uso del template o tipo genérico. Como en C++ ;)

Optimización 3
Cachear los GetComponent, preferiblemente en el Awake. Esto es realmente crítico si se realiza en el Update. Al final obtendremos un mejor rendimiento a cambio de un coste de memoria mínimo (entre 32 ó 64 bytes por item dependiendo de la plataforma).

Optimización 4
El uso de funciones dentro de Update. Si las acciones no requieren se llamadas en cada frame es mucho mejor convertirlo a un InvokeRepeating que puede llamarse en el Start y cancelarse en el OnDestroy:

private void Start() {  
	InvokeRepeating("ProcessAI", 0f, _aiProcessDelay);
}

Optimización 5
La comparación de objetos null. La llamada directa de "gameobject == null" genera una conversión Native-Managed Bridge con la consiguiente sobrecarga. Es mucho mejor utilizar el ReferenceEquals que no utiliza la conversión. Por eso se incluyó en Unity:

if (!System.Object.ReferenceEquals(gameObject, null)) {
  // No es null
}

Optimización 6
La comprobación con Tag directa genera memoria adicional y hará actuar al GC posteriormente. Utilizar el CompareTag no utiliza memoria ya que evita el Native-Managed Bridge completamente y tarda la mitad de tiempo:

// Asignación de memoria y GC. Tarda el doble de tiempo
if (gameObject.tag == "Player") {
  // realizar acción
}
// Evita el Native-Managed Bridge totalmente. No asigna memoria adicional
if (gameObject.CompareTag ("Player")) {
  // realizar acción
}

Optimización 7
Dictionary vs List. Las List son mejores para iteraciones. Los Dictionary son mejores para búsquedas aleatorias. El Dictionary es peor para las iteraciones debido a que debe realizar una comparación hash para cada uno de los elementos. De todos modos tener los dos tipos en algunas situaciones no es mala idea.

Optimización 8
La Transform. Cuando instanciamos un nuevo GameObject con GameObject.Instantiate(), uno de sus argumentos es el componente de la Transform del parent que queremos asignar, que por defecto es null y colocará el GameObject en el root. Todas las Transforms que está a nivel root necesitar crear un buffer de memoria para poder almacenar los children que tienen más aquellos que vendrán después. Esto no ocurre con las Transforms que son child. Pero si una Transform en root le cambiamos el parent una vez creada y la reasignamos, se procederá a descartar el buffer de memoria que iniciamos en el Instantiate. Para evitar esto es mejor proveer del parent en la función.

Otro apartado interesante es que, hay una propiedad en la Transform llamada hierarchyCapacity. Si somos capaces de estimar el número de Transform child de este objeto root, podremos reducir el número de asignaciones de memoria.

Optimización 9
World Position, World Rotation, World Scale. Cuánto más profundo esté en la jerarquía un objeto mayores cálculos son necesarios para determinar el resultado final de su estado. Esto significa que es mucho más eficiente utilizar los elementos de Local que de World.

Optimización 10
Cambios en la Transform. Si es posible, agrupar todos los cambios en una Transform y no realizarlos hasta el final de todos los cálculos y en FixedUpdate. Esto evitará movimientos extraños o teleportaciones de los objetos debido a que Unity lanza eventos internos cada vez que una Transform es modificada.

private bool _positionChanged;
private Vector3 _newPosition;

public void SetPosition(Vector3 position) {
  	_newPosition = position;  
	_positionChanged = true;
}

private void FixedUpdate() {  
	if (_positionChanged) {    
		transform.position = _newPosition;    
		_positionChanged = false;  
	}
}

Optimización 11
SendMessage y Find.
El método SendMessage() y la familia de métodos GameObject.Find() so especialmente costosos y deben evitarse en su totalidad. El método SendMessage() es como 2,000 veces más lento que una simple llamada a una función, y el coste de Find() se escala pobremente según la complejidad de la scene aumente ya que tiene que iterar por todos los GameObject de la misma.

Optimización 12
No solamente Occlusion Culling/Frustum Culling. Para los objetos que solamente tienen renderizado es factible utilizarlas únicamente. Otros objetos que utilicen cálculos internos de manera constante continuarán consumiendo a pesar del Culling.

Una buena solución a este problema es utilizar las callbacks OnBecameVisible() y OnBecameInvisible(). Como los nombres dicen, estas callbacks son invocadas cuando un objeto renderizado se ha vuelto visible a una cámara o invisible respecto a todas las cámaras de la scene. Además, cuando hay múltiples cámara en la scene (por ejemplo en un juego multiplayer) las callbacks son solamente invocadas cuando es visible para una cámara o invisible para todas ellas de igual modo. Esto significa que las callback serán llamadas en el momento preciso que se necesitan. Si nadie puede ver el objeto se llamará a OnBecameInvisible(), si como mínimo un player puede verlo se llamará a OnBecameVisible(). 

Optimización 13
Distance vs sqrMagnitude. El cálculo de raíces cuadradas para las CPU implica bastante proceso comparado con otras instrucciones. Cuando tengamos que utilizar un cálculo de distancia, y si no requerimos de una precisión extrema, es mejor utilizar sqrMagnitude y valor a comparar por su potencia de 2.

// Utilizar la raíz cuadrada en Distance()
float distance = (transform.position  other.transform.position).Distance();
if (distance < targetDistance) {
  // dentro de distancia
}

// No realiza el proceso anterior
float distanceSqrd = (transform.position  other.transform.position).sqrMagnitude;
if (distanceSqrd < (targetDistance * targetDistance)) {  
  // dentro de distancia
}

Optimización 14
Datos de Prefab a un Scriptable Object.
Si tenemos muchos diferentes tipos de Prefabs que contienen datos que pueden compartirse entre ellos, como fuerza, velocidad, puntos, etc. entonces todos estos datos serán serializados en cada Prefab que luego se instancie. Una mejor solución es serializar estos datos en un ScriptableObject. Esto reduce la cantidad de datos a serializar en el Prefab, el tiempo de deserialización y el tamaño del Prefab en sí mismo reduciendo el tiempo de acciones que serán repetitivas.

Optimización 15
Update().
Una mejor solución al problema del Update() es no utilizar nunca, o mejor dicho, una sola vez. Cuando Unity llama al callback Update(), o cualquiera relacionado, cruza la ya comentada frontera del Native-Managed Bridge que es una tarea costosa en términos relativos. En otras palabras, el coste de procesamiento de 1.000 Update() independiente será mucho más costoso que la ejecución de un Update() con 1.000 funciones. Para poder minimizar este problema es mejor agrupar todos los Update() en un solo Update() global que luego llame a las diferentes funciones que requieran la acción.
 

  • Like 6

Share this post


Link to post
Share on other sites

Mil gracias por el aporte, super interesante. Es bueno aplicarse esto aunque sean proyectos pequeños, asi se nos queda la costumbre.  

 

Share this post


Link to post
Share on other sites

Muy buen aporte.

¿Que es Native-Managed Bridge? Se trata de llamadas que deben ser ejecutadas fuera de la "zona segura" del CLR de .NET, por esta razón es bastante costoso aparte de otras muchas desventajas.

--------------------------------------------

Se puede evitar la llamada del GC si los TAG se almacenan como CONSTANTES. Esto tiene algo de cierto según la prioridad con la que se llama ya que el hash de los STRING es relativo a su contenido.

--------------------------------------------

Para el Update Global se puede utilizar un evento estático.

public class GameBehaviour : MonoBehaviour
{  
  private void Awake()
  {
    GameManager.UpdateEvent += OnUpdate;
  }
  
  private void OnDestroy()
  {
    GameManager.UpdateEvent -= OnUpdate;
  }
  
  protected virtual void OnUpdate()
  {
    
  }
}

Por lo general a "GameManager" lo llamo "RuntimeUtilities" y lo instancio mediante una función estática con InitializeOnLoad para asegurarse de que siempre esté presente antes de la llamada Awake en cualquier Escenario.

Saludos.

Share this post


Link to post
Share on other sites

Gracias por el aporte.

Yo hace tiempo que tenía un enlace Unity en que ya comenta alguna de las optimizaciones que indicas.

Lo pongo por si a alguien le resulta interesante: https://learn.unity.com/tutorial/fixing-performance-problems#5c7f8528edbc2a002053b594

Y otro de vídeos con diferentes técnicas para optimización: https://learn.unity.com/search?k=["tag%3A5814ded032b30600246b5d0e"]

Share this post


Link to post
Share on other sites
On 3/6/2020 at 3:31 PM, iRobb said:

Optimización 3
Cachear los GetComponent, preferiblemente en el Awake. Esto es realmente crítico si se realiza en el Update. Al final obtendremos un mejor rendimiento a cambio de un coste de memoria mínimo (entre 32 ó 64 bytes por item dependiendo de la plataforma).

¿A qué se refiere con cachear?

Share this post


Link to post
Share on other sites

@TheShopKeeper Cachear un valor es cuando asignas la referencia de un objeto a una variable con mayor persistencia, por ejemplo obteniendo la referencia desde una función asignas una variable de la misma clase. 

@nomoregames ¿Comprobar que cosa?

Share this post


Link to post
Share on other sites

catchear es asignar

catch = cojer

si un script (porejemplo "enemy") va a necesitar acceder porejemplo a la posicion de otro objeto del juego (porejemplo la del player) es mejor asignar el player al inicio en el "void start" o "void awake" , buscandolo ahi...

ya que hacer un "gameobject.find" en "void update" cuesta muchos recursos...

pero si tienen una variable definida (private GameObject player) y en void start o void awake la buscas (player = gameobject.find("player")) pues puedes usarla en update si que le cueste tiempo buscarla todo el rato, cada frame...

 

@iRobb gracias por los consejos... conocia casi todos pero alguno como el del sqrMagnitude no lo conocia... me lo apunto😊

Edited by Igor

Share this post


Link to post
Share on other sites
2 hours ago, francoe1 said:

@TheShopKeeper Cachear un valor es cuando asignas la referencia de un objeto a una variable con mayor persistencia, por ejemplo obteniendo la referencia desde una función asignas una variable de la misma clase. 

@nomoregames ¿Comprobar que cosa?

 

35 minutes ago, Igor said:

catchear es asignar

catch = cojer

si un script (porejemplo "enemy") va a necesitar acceder porejemplo a la posicion de otro objeto del juego (porejemplo la del player) es mejor asignar el player al inicio en el "void start" o "void awake" , buscandolo ahi...

ya que hacer un "gameobject.find" en "void update" cuesta muchos recursos...

pero si tienen una variable definida (private GameObject player) y en void start o void awake la buscas (player = gameobject.find("player")) pues puedes usarla en update si que le cueste tiempo buscarla todo el rato, cada frame...

 

@iRobb gracias por los consejos... conocia casi todos pero alguno como el del sqrMagnitude no lo conocia... me lo apunto😊

Muchas gracias por la explicación, no asimilaba el termino cachear correctamente.

Share this post


Link to post
Share on other sites
On 3/6/2020 at 9:31 PM, iRobb said:

Optimización 2
Cómo obtener los componentes de un GameObject. Hay tres maneras de hacerlo, aunque a la práctica se utilizan dos de ellas. Los datos hablan por si mismos para 30.000 objetos.


// 6413 ms
test = (TestComponent)GetComponent("TestComponent");
// 89 ms
test = GetComponent<TestComponent>();
// 95 ms
test = (TestComponent)GetComponent(typeof(TestComponent));

El mejor es del uso del template o tipo genérico. Como en C++ 😉

Si hacéis cosas en el Editor, sabed que a partir de Unity 2019.2 también está disponible TryGetComponent() que es aún mejor opción: https://docs.unity3d.com/ScriptReference/Component.TryGetComponent.html

4 hours ago, Igor said:

catchear es asignar

catch = cojer

No, "cachear" viene de caché (https://es.wikipedia.org/wiki/Caché_(informática)). El resto de la explicación es correcta.

Share this post


Link to post
Share on other sites

×
×
  • Create New...