Esta sección demuestra cómo a usted le gustaría ir acerca de optimizar los scripts y métodos actuales que su juego utiliza, y también va en detalle acerca de las razones por las cuales las optimizaciones funcionan, y por qué aplicarlas le van a beneficiar en ciertas situaciones.
No hay tal cosa como una lista de cajas para marcar que le van a asegurar que su proyecto corra sin problemas. Para optimizar un proyecto lento, usted tiene que profile (perfilar) para encontrar los infractores específicos que toma una cantidad de tiempo desproporcionado. Intentar optimizar sin profiling (perfilar) o sin entender los resultados que el profiler (perfilador) le da es como intentar optimizar con una benda puesta
Usted puede utilizar el internal profiler para averiguar qué tipo de procesos está poniendo lento su juego, sea física, scripts, o el renderizado, pero usted no puede profundizar a los scipts y métodos específicos para encontrar los ofensores en realidad. Sin embargo, al construir switches a su juego que pueden habilitar, deshabilitar ciertas funcionalidades, usted puede limitar los peores ofensores significativamente. Por ejemplo, si usted quita el AI script del personaje enemigo y el framerate (velocidad de frame) se duplica, usted sabe que ese script, o algo que lo trae al juego, tiene que ser optimizado. El único problema es que usted podría intentar muchas cosas diferentes antes de que usted encuentre el problema.
Para más acerca del profiling (perfilamiento) en dispositivos móviles, ver la sección de profiling.
Intentar desarrollar algo que es rápido del comienzo es arriesgado, por que hay un trade-off entre gastar tiempo por perder el tiempo haciendo cosas que sería igual de rápido si no se optimizarán y hacer cosas que tienen que ser cortadas o reemplazadas más tarde por ser demasiado lento. Esto toma intuición y conocimiento del hardware para hacer unas buenas decisiones en este aspecto, especialmente porque cada juego es diferente y lo que podría ser una optimización crucial para un juego puede ser un fracaso en otro.
nosotros damos object pooling (agrupamiento de objetos) como un ejemplo de la intersección entre un buen gameplay y un buen diseño de código en la introducción a métodos de scripting optimizados. Utilizar el object pooling para objetos efímeros es más rápido que crear y destruirlos, ya que hace la asignación de memoria más simple y quita una sobre carga de asignación de memoria dinámica y Garbage Collection, o GC.
Scripts que usted escriba en Unity utilizan la administración de memoria automática. Casi todos los lenguajes de programación hacen esto. En contraste, los lenguajes de bajo nivel como C y C++ utilizan una asignación de memoria manual, dónde el programado es permitido para leer y escribir de direcciones de memoria directamente, y como consecuencia es responsable de quitar cada objeto que crea. Por ejemplo, si usted crea objetos en su C++, usted tiene que manualmente asignar la memoria que toman cuando usted ha terminado con ellos. En un lenguaje de programación, es suficiente decir objectReference = null;
Tenga en cuenta: Si usted tiene una variable game object como GameObject myGameObject;
o var myGameObject : GameObject;
, por qué no es destruida cuando digo myGameObject = null;
?
Destroy(myGameObject);
quita esa referencia y elimina el objeto.Pero si usted crea un objeto que Unity no tiene idea sobre este, por ejemplo, una instancia de una clase que no hereda de nada (en contraste, la mayoría de clases o componentes “script” heredan de MonoBehaviour) y luego configura referencia de la variable a null, lo que en realidad sucede es que el objeto es perdido en cuanto a su script y Unity les preocupa; no lo pueden acceder y nunca lo verán nuevamente, pero se queda en memoria. Entonces, algún tiempo después, el Garbage Collector corre, y quita cualquier cosa en memoria que no está referenciado en cualquier parte. Es capaz de hacer esto ya que, detrás de cámaras, el número de referencia para cada bloque de memoria se mantiene un registro. Esta es una razón porque los lenguajes de programación son más lentos que C++.
Cada vez que un objeto es creado, la memoria es asignada. A menudo en código, usted está creando objetos sin ni siquiera saberlo.
Debug.Log("boo" + "hoo");
crea un objeto.en vez de
""`` cuando maneje muchos strings.Classes son objetos y se comportan como referencias. Si Foo es una clase y
Foo foo = new Foo();
MyFunction(foo);
luego MyFunction va a recibir una referencia al objeto Foo original que fue asignado en el heap. Cualquier cambio a foo dentro de MyFunction será visible en cualquier parte que foo sea referenciado.
Las classes son datos y se comportan como tal. Si Foo es una struct y
Foo foo = new Foo();
MyFunction(foo);
luego MyFunction va a recibir una copia de foo. foo nunca es asignada al heap y nunca es recolectado por el garbage collector. Si MyFunction modifica su copia de foo, el otro foo no es afectado.
El resultado de esto utilizar Instantiate y Destroy le da mucho para hacer al Garbage Collector, y esto puede causar un “tirón” en el gameplay. Como la página de Administración de Memoria Automática explica, hay otras maneras para sobrepasar los ‘tirones’ de rendimiento que envuelven Instantiate y Destroy, tal como triggering (activar/desactivar) el Garbage Collector manualmente cuando nada está pasando, o triggering (activar/desactivar) muy a menudo para que se acumule una gran cantidad de memoria sin utilizar nunca se acumule.
Otra razón es que, cuando un prefab especifico es instanciado por la primera vez, a veces algunas cosas adicionales tienen que ser cargadas al RAM, o texturas y los meshes necesitan ser subidos al GPU. Esto puede causar un tirón también y con el object pooling (el agrupamiento de objetos) esto sucede cuando el nivel carga en vez de durante el gameplay.
Imagine un titiritero que tiene una caja infinita de títeres, dónde cada vez, el script llama para que un personaje aparezca, este obtiene una nueva copia de su títere fuera de la caja, y cada vez el personaje sale del escenario, él lanza la copia actual. El Object pooling (agrupamiento objetos= es equivalente de obtener todos los títeres fuera de la caja antes de que el show empiece, y dejarlos en una tabla antes de que el show comience, y dejarlos en una tabla detrás del escenario cuando no estén supuestamente visibles.
Un problema aquí es que la creación de un pool (agrupamiento) reduce la cantidad de memoria heap disponible para otros propósitos; por lo que si usted sigue asignando memoria arriba del agrupamiento que usted ha creado, usted podría trigger el garbage collection incluso aún más seguido. No solo eso, cada colección será más lenta, ya que el tiempo que toma para una colección aumenta con el número de objetos vivos. Con estos problemas en mente, debe ser aparente que el rendimiento va a sufrir si usted asigna agrupamientos que son muy grandes o los mantiene activos cuando los objetos que contienen no serán necesitado para algún tiempo. Adicionalmente, muchos tipos de objetos no son muy buenos al object pooling (agrupamiento de objetos). Por ejemplo, el juego puede incluir efectos de hechizos que persisten por un tiempo considerable o enemigos que aparecen en grandes números pero que solamente son muertos gradualmente a medida que le juego progresa. En tales casos, la carga de rendimiento de un object pool (agrupamiento de objetos) pesa más que los beneficios y por lo tanto no deberían ser utilizados.
Aquí hay una comparación simple de lado por lado de un script para un proyectil simple, uno utilizando Instantiation, y uno para utilizar Object Pooling (Agrupamiento de Objetos).
// GunWithInstantiate.js // GunWithObjectPooling.js
#pragma strict #pragma strict
var prefab : ProjectileWithInstantiate; var prefab : ProjectileWithObjectPooling;
var maximumInstanceCount = 10;
var power = 10.0; var power = 10.0;
private var instances : ProjectileWithObjectPooling[];
static var stackPosition = Vector3(-9999, -9999, -9999);
function Start () {
instances = new ProjectileWithObjectPooling[maximumInstanceCount];
for(var i = 0; i < maximumInstanceCount; i++) {
// place the pile of unused objects somewhere far off the map
instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity);
// disable by default, these objects are not active yet.
instances[i].enabled = false;
}
}
function Update () { function Update () {
if(Input.GetButtonDown("Fire1")) { if(Input.GetButtonDown("Fire1")) {
var instance : ProjectileWithInstantiate = var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance();
Instantiate(prefab, transform.position, transform.rotation); if(instance != null) {
instance.velocity = transform.forward * power; instance.Initialize(transform, power);
} }
} }
}
function GetNextAvailiableInstance () : ProjectileWithObjectPooling {
for(var i = 0; i < maximumInstanceCount; i++) {
if(!instances[i].enabled) return instances[i];
}
return null;
}
// ProjectileWithInstantiate.js // ProjectileWithObjectPooling.js
#pragma strict #pragma strict
var gravity = 10.0; var gravity = 10.0;
var drag = 0.01; var drag = 0.01;
var lifetime = 10.0; var lifetime = 10.0;
var velocity : Vector3; var velocity : Vector3;
private var timer = 0.0; private var timer = 0.0;
function Initialize(parent : Transform, speed : float) {
transform.position = parent.position;
transform.rotation = parent.rotation;
velocity = parent.forward * speed;
timer = 0;
enabled = true;
}
function Update () { function Update () {
velocity -= velocity * drag * Time.deltaTime; velocity -= velocity * drag * Time.deltaTime;
velocity -= Vector3.up * gravity * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime;
transform.position += velocity * Time.deltaTime; transform.position += velocity * Time.deltaTime;
timer += Time.deltaTime; timer += Time.deltaTime;
if(timer > lifetime) { if(timer > lifetime) {
transform.position = GunWithObjectPooling.stackPosition;
Destroy(gameObject); enabled = false;
} }
} }
Claro está, para una juego grande y complicado, usted querrá hacer una solución genérica que funcione para todos sus prefabs.
El ejemplo de “Hundreds of rotating, dynamically lit, collectable coins onscreen at once” (cientos de monedas coleccionables girando, prendidas dinámicamente en la pantalla a la vez) el cual fue dado en la sección Scripting Methods va a ser utilizada para demonstrar cómo el código script, los componentes de Unity como un sistema de partícula, y unos shaders personalizados pueden ser utilizados para crear un efecto impresionante sin afectar el hardware móvil débil.
Imagine que este efecto vive en el contexto de un juego de desplazamiento lateral con toneladas de monedas que caen, rebotan, y giran. Las monedas son prendidas dinámicamente por point lights. Nosotros queremos capturar la luz reflejándose en las monedas para hacer nuestro juego aun más impresionante.
Si nosotros tuviéramos un poderoso hardware, nosotros podemos utilizar un acercamiento estándar a este problema. Hacer de cada moneda un objeto, shader (sombrear) el objeto ya sea con un lighting (iluminación) vertex-lit, forward, o deferred, y luego agregar un brillo encima como medida para que un efecto de imagen para obtener unas monedas brillando reflejándose para sangrar luz al área de sus alrededores.
Pero el hardware móvil se ahorcaría en todos esos objetos y un efecto de brillo está totalmente fuera de la pregunta. Entonces qué hacemos?
Si usted quiere mostrar muchos de los objetos que se mueven de una manera similar y nunca podrán ser inspeccionados por el jugado, usted podría renderizar una gran cantidad de ellos en nada de tiempo utilizando un sistema de partículas. Aquí hay unas pocas aplicaciones estereotípicas de esta técnica:
Hay una extensión gratuita del editor llamada Sprite Packer que facilita la creación de un sprite animado con sistema de partículas. Este renderiza frames de su objeto a una textura, que luego puede ser utilizada como una sheet (hoja) de animación de sprite en un sistema de partículas. Para nuestro caso de uso, nosotros lo utilizaremos en nuestra moneda girando.
Incluido en el proyecto Sprite Packer hay un ejemplo que demuestra una solución a este problema exacto.
Utiliza una familia de assets de todos los tipos diferentes para lograr un efecto llamativo en un presupuesto de computación bajo:
Un archivo readme es incluido con el ejemplo que intenta explicar por qué y cómo el sistema funciona, descubriendo el proceso que fue utilizado para determinar qué características fueron necesitadas y cómo fueron implementadas. Este es ese archivo:
El problema fue definido como “Hundreds of rotating, dynamically lit, collectable coins onscreen at once.” (Cientos de monedas coleccionables rotando, prendidas dinámicamente en la pantalla a la vez)
El acercamiento ingenuo es instanciar una cantidad de copias de un prefab coin (moneda), pero en vez de eso, nosotros vamos a utilizar partículas para renderizar monedas. Sin embargo, esto introduce un número de retos que nosotros tenemos que superar.
Los ángulos de vista son un problema ya que las partículas no las tienen.
Nosotros asumimos que la cámara se mantiene parada de lado derecho y las monedas giran alrededor del eje Y.
Nosotros creamos la ilusión del giro de la moneda con una textura animada que hemos empacado utilizando el SpritePacker.
Esto introduce un nuevo problema: La monotonía de girar las monedas todas girando a la misma velocidad en la misma dirección
Nosotros mantenemos un seguimiento de la rotación y ciclo de vida nosotros mismo y “renderizamos” la rotación al ciclo de vida de las partículas en el script para arreglar esto.
Las normales son un problema ya que estas partículas no las tienen, y nosotros necesitamos una iluminación en tiempo real.
Genere un solo vector de normal para la cara de la moneda en cada frame de animación generado por el Sprite Packer.
Realice una iluminación Blinn-Phong para cada partícula en el script, basado en el vector de normal cogido de la lista de arriba.
Aplique el resultado a la partícula como un color.
Maneja la cara de la moneda y el borde de la moneda de manera separada en el shader. Introduce un nuevo problema: Cómo el shader sabe dónde el borde está, y en qué parte del borde está?
No se puede utilizar UV’s, estas ya están utilizadas para la animación.
Utilice un mapa de Textura
Necesita la posición en Y relativo a la moneda.
Necesita binario en “on face” vs “on rim”.
Nosotros no queremos introducir otra textura, más lecturas de textura, más memoria de textura.
Combine la información necesita a un canal y remplace uno de los canales de color de la textura con este.
Ahora nuestra moneda es del color equivocado! Qué hacemos?
Utilice el shader para re-construir el canal que falte como una combinación de los dos canales restantes.
Digamos que nosotros queremos un resplendor de una luz que brille de nuestras monedas. El proceso posterior (post process) es muy costoso para dispositivos móviles.
Cree otro sistema de partícula y dele una versión de la animación de la moneda más suave y con brillo.
Dele color al resplendor solamente cuando el color correspondiente de la luz brilla bastante.
No puede tener un brillo renderizado en cada moneda en cada frame - mata el fill rate (la tasa de relleno).
Re-inicia el brillo cada frame, solamente posiciona aquellos con un brillo (brightness) > 0.
La física es un problema, coleccionar monedas es un problemas - las partículas no colisionan bien.
Se puede utilizar una colisión de partículas integrada?
En vez, simplemente escriba collision en el script.
Finalmente, nosostros tenemos un problema más - este script hace mucho, y se está volviendo más lento!
El rendimiento se escala linealmente con el número de monedas activas.
Limita la cantidad máxima de monedas. Esto funciona bien para lograr nuestra meta: 100 monedas, 2 luces, corren bastante rápido en dispositivos móviles.
Cosas para intentar optimizar aun más:
En vez de calcular la iluminación para cada moneda individualmente, corte el mundo a pedazos y calcule las condiciones de iluminación para cada frame de rotación en cada pedazo.
Utilice una tabla de consulta con la posición de moneda y la rotación de moneda como indices.
Aumente la fidelidad al utilizar una interpolación bi-lineal con la posición.
Actualizaciones escasas en la tabla de consulta, o , una tabla de consulta enteramente estática.
Utilice light probes (sondas de luz) para esto?
En vez de calcular la iluminación en un script, utilice unas partículas normal-meapped (mapeadas con normales)?
Utilice el shader “Display Normals” para bake cada animación de frame de normales.
Limita el número de luces.
Arregla problema de script lento.
El objetivo final de este ejemplo, o la “moral de la historia” es que hay algo que su juego realmente necesita, y causa lag cuando usted intenta lograrlo a través de medios convencionales, eso no significa que no es imposible, simplemente que usted tiene que colocar un poco de trabajo en un sistema propio suyo que corra más rápido.
Hay unas optimizaciones de scripting especificas que son aplicables en situaciones dónde hay cientos o miles de objetos dinámicos involucrados. Aplicar estas técnicas a cada script de su juego es una pésima idea; estas deben guardarse como herramientas y lineas guía de diseño para scripts grandes que manejen toneladas de objetos o datos en tiempo de ejecución.
En la ciencia de computación, el Orden de una operación, denotada por O(n), se refiere a la forma en que el número de veces que la operación tiene que ser evaluada aumenta a medida que el número de objetos que se aplica a (n) aumente.
Por ejemplo, considere un algoritmo de ordenamiento básico. Yo tengo n números y yo quiero ordenarlos del más pequeño al más grande.
void sort(int[] arr) {
int i, j, newValue;
for (i = 1; i < arr.Length; i++) {
// record
newValue = arr[i];
//shift everything that is larger to the right
j = i;
while (j > 0 && arr[j - 1] > newValue) {
arr[j] = arr[j - 1];
j--;
}
// place recorded value to the left of large values
arr[j] = newValue;
}
}
La parte importante es que hay dos loops (bucles) aquí, uno dentro del otro.
for (i = 1; i < arr.Length; i++) {
...
j = i;
while (j > 0 && arr[j - 1] > newValue) {
...
j--;
}
}
Digamos que nosotros le damos al algoritmo el peor caso posible: los números de input están ordenados, pero en orden reverso. En este caso, el loop (bucle) de la parte más interna correrá j veces. En promedio, a medida que i va de 1 a arr.Length–1, j será arr.Lenght/2. En términos de O(n), arr.Length es nuestro n, entonces, en total, el loop (bucle) más interno correrá n*n/2 veces, o n2/2 veces. Pero en términos de O(n), nosostros cortamos todas las constantes como 1/2, ya que nosotros queremos hablar acerca de la manera que el número de operaciones aumenta, no acerca del número real de operaciones. Entonces el algoritmo es O(n2). El orden de una operación importa bastante si el conjunto de datos es más grande, ya que el número de operaciones puede explotar exponencialmente.
Un ejemplo de juego de una operación O(n2) es 100 enemigos, dónde el AI de cada enemigo toma los movimientos de cada otro enemigo en cuenta. Puede ser más rápido dividir el mapa a células, grabar el movimiento de cada enemigo a la célula más cercana, y luego tener a cada enemigo muestrar las células más cercanas. Esto sería una operación O(n).
Digamos usted tiene 100 enemigos en su juego, y estos se mueven hacia el jugador.
// EnemyAI.js
var speed = 5.0;
function Update () {
transform.LookAt(GameObject.FindWithTag("Player").transform);
// this would be even worse:
//transform.LookAt(FindObjectOfType(Player).transform);
transform.position += transform.forward * speed * Time.deltaTime;
}
Esto puede ser lento, si hay los suficientes de ellos corriendo a la vez. Un hecho poco conocido: todos los descriptores de acceso en MonoBehaviour, cosas como transform, renderer, y audio, son equivalentes a sus GetComponent(Transform) contrapartes, y estas son en realidad un poco lentas. GameObject.FindWithTag ha sido optimizado, pero en algunos casos, por ejemplo, los loops (bucles) internos, o en scripts que corren muchas instancias, este script podría sera un poco lento.
Esta es una mejor versión del script.
// EnemyAI.js
var speed = 5.0;
private var myTransform : Transform;
private var playerTransform : Transform;
function Start () {
myTransform = transform;
playerTransform = GameObject.FindWithTag("Player").transform;
}
function Update () {
myTransform.LookAt(playerTransform);
myTransform.position += myTransform.forward * speed * Time.deltaTime;
}
Funciones trascendentales (Mathf.Sin, Mathf.Pow, etc), y Raíz Cuadrada toman acerca de 100x el tiempo de multiplicación. (En el gran esquema de las cosas, no hay tiempo, pero si usted está llamándolas miles de veces por frame, pueden agregarse).
El caso más común de esto es la normalización de vectores. Si usted está normalizando el mismo vector una y otra vez, considere normalizarlo una sola vez más bien y caché el resultado para su uso después.
Si usted está utilizando ambas la longitud de un vector y lo está normalizando, sería más rápido obtener el vector normalizado al multiplicar el vector por el reciproco de la longitud en vez de utilizar la propiedad .normalized.
Si usted está comparando distancias, usted no tiene que comparar las distancias actuales. Usted puede comparar los cuadrados de las distancias en vez de utilizar la propiedad .sqrMagnitued y guardar una raíz cuadrada o dos.
Otro, si usted está dividiendo una y otra vez con una constante C, usted puede multiplicar el reciproco más bien. Calcule el reciproco primero al hacer__1.0/c__.
Si usted tiene que hacer algo que sea costoso, usted podría ser capaz de optimizarlo para hacerlo menos frecuente y caché el resultado. Por ejemplo, considere un script de proyectil que utilice Raycast:
// Bullet.js
var speed = 5.0;
function FixedUpdate () {
var distanceThisFrame = speed * Time.fixedDeltaTime;
var hit : RaycastHit;
// every frame, we cast a ray forward from where we are to where we will be next frame
if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) {
// Do hit
} else {
transform.position += transform.forward * distanceThisFrame;
}
}
De inmediato, nosotros podríamos mejorar el script al remplazar el FixedUpdate con Update y fixedDeltaTime con deltaTime. El FixedUpdate se refiere a la actualización de Física, que sucede más a menudo que la actualización de frame. Pero vayamos aun más lejos al solo hacerle raycasting cada n segundos. Un n más pequeño da una resolución temporal mayoral, y una n más grande nos da un mejor rendimiento. Entre más grandes y lentos sean sus objetivos, más grande puede ser el n antes de que un aliasing temporal ocurra. (Apariencia de latencia, dónde el jugador golpea el objetivo, pero la explosión aparece dónde el objetivo estaba n segundos antes, o el jugador golpea el objetivo, pero el proyectil lo atraviesa).
// BulletOptimized.js
var speed = 5.0;
var interval = 0.4; // this is 'n', in seconds.
private var begin : Vector3;
private var timer = 0.0;
private var hasHit = false;
private var timeTillImpact = 0.0;
private var hit : RaycastHit;
// set up initial interval
function Start () {
begin = transform.position;
timer = interval+1;
}
function Update () {
// don't allow an interval smaller than the frame.
var usedInterval = interval;
if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime;
// every interval, we cast a ray forward from where we were at the start of this interval
// to where we will be at the start of the next interval
if(!hasHit && timer >= usedInterval) {
timer = 0;
var distanceThisInterval = speed * usedInterval;
if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) {
hasHit = true;
if(speed != 0) timeTillImpact = hit.distance / speed;
}
begin += transform.forward * distanceThisInterval;
}
timer += Time.deltaTime;
// after the Raycast hit something, wait until the bullet has traveled
// about as far as the ray traveled to do the actual hit
if(hasHit && timer > timeTillImpact) {
// Do hit
} else {
transform.position += transform.forward * speed * Time.deltaTime;
}
}
Solo llamar una función tiene un poco de sobrecarga en sí. Si usted está llamando cosas como x = Mathf.Abs(x) mil veces por frame, podría ser mejor simplemente hacer x = (x > 0 ? x : -x); más bien.
El motor de física NVIDIA PhysX utilizado por Unity está disponible en móviles, pero los limites de rendimiento del hardware serán alcanzado más fácil en plataformas móviles que en desktops.
Aquí hay algunas recomendaciones para ajustar la física para obtener un mejor rendimiento en los móviles:-