Buscar en Unity

Trabaja de una manera óptima con las estructuras de datos en C# y las API de Unity

Last updated: December 2018

What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.

A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.

All of these tips are from the Unite talk, Exprimiendo a Unity: Consejos para aumentar el rendimiento. For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Evolución de las mejores prácticas.

Unity APIs

Problema: iteración recurrente con las API del sistema de partículas:

Cuando invocas a una de las API principales del sistema de partículas, como Start, Stop, IsAlive(), este efectúa iteraciones en forma recurrente, en forma predeterminada, a través de todos los hijos de una jerarquía del sistema de partículas:

  • La API encuentra a todos los hijos del Transform de un sistema de partículas, invoca a GetComponent en cada Transform y, si existe un sistema de partículas, invoca al método interno adecuado correspondiente.
  • Si estos Transforms hijos tienen sus propios hijos, se producirá una recurrencia por todo el camino hasta la parte inferior de la jerarquía del Transform: cada hijo, nieto, y así sucesivamente, se procesará. Este puede ser un problema si tienes una jerarquía de Transforms profunda.

Solución:

Estos API tienen un parámetro withChildren que de manera predeterminada se pone en Verdadero. Configúralo como Falso para eliminar esta conducta recursiva. Cuando withChildren se configura como Falso, solo se modificará el sistema de partículas al que estás invocando directamente.

Problema: Sistemas de partículas múltiples que arrancan y paran al mismo tiempo

Es algo común que los artistas creen efectos visuales que poseen sistemas de partículas dispersos a través de una serie de Transforms hijos; es necesario que todos ellos arranquen y paren al mismo tiempo.

Solución:

Crea un MonoBehaviour que almacene en la memoria caché la lista de sistemas de partículas, llamando a la lista GetComponent al inicializar. Luego, cuando sea necesario cargar el sistema de partículas, invoca, a su vez, a Start, Stop, etc., en cada uno de ellos y asegúrate de pasar el valor Falso como el parámetro de withChildren.

Problema: Asignación de memoria frecuente

Cuando un cierre de C# se cierra sobre una variable local, el tiempo de ejecución de C# debe asignar una referencia en la pila para controlar la variable. En las versiones 5.4 a 2017.1 de Unity, existe un par de API del sistema de partículas que, cuando se invoca, utiliza los cierres a nivel interno. En estas versiones de Unity, todas las llamadas a Stop and Simulate asignan memoria, aunque el sistema de partículas ya se haya detenido.

Solución:

Todas las API del sistema de partículas, al igual que muchas API públicas de Unity, son solo funciones envolventes en torno a las funciones internas, que ejecutan el trabajo real de la API. Algunas de estas funciones internas son solo de C#, pero muchas terminan invocando al núcleo de C++ del motor de Unity. En el caso de las API del sistema de partículas, la nomenclatura de todas las API internas es bastante conveniente; utilizan la palabra "Internal_" seguida del nombre de la función pública, como por ejemplo:

work-optimally-with-Unity-APIs-table-graph

... y así sucesivamente.

En lugar de dejar que las API públicas de Unity llamen a sus ayudantes internas a través de los cierres, puedes escribir un método de extensión o examinar el método interno a través de Reflection y almacenar la referencia en la memoria caché para utilizarla posteriormente. A continuación se presentan las firmas de la función correspondiente:

work-optimally-with-Unity-APIs-functions-signatures

Todos los argumentos tienen los mismos significados que las API públicas, excepto el primero, que es una referencia al sistema de partículas que deseas detener o simular.

Problema: API que devuelven Matrices

Cada vez que accedes a una API de Unity que devuelve una matriz, se asigna una copia nueva de dicha matriz. Esto ocurre tanto cuando utilizas funciones que devuelven matrices como cuando utilizas propiedades que devuelven matrices. Como ejemplos comunes de esto se tiene Mesh.vertices: accede a esta función y obtendrás una copia nueva de los vértices de la malla. Aedmás, está Input.touches, que te entrega una copia de todos los toques que el usuario efectúa durante el cuadro actual.

Solución:

Muchas API de Unity ahora tienen versiones sin asignaciones y debes utilizar estas en lugar de las versiones anteriores. Por ejemplo, en lugar de Input.touches, utiliza Input.GetTouch e Input.touchCount. A partir de la versión 5.3, se introdujeron las versiones sin asignaciones de todas las API de consultas de Physics. Para la aplicaciones 2D, también se cuenta con versiones sin asignaciones de todas las API de consultas de Physics2D. Lee más acerca de las versiones sin asignaciones de las API de Physics de Unity. aquí.

Finalmente, si utilizas GetComponents o GetComponentsInChildren, ahora existen versiones que aceptan una lista genérica, generada a partir de una plantilla. Estos API llenan la lista con los resultados de la llamada a GetComponents. No se trata de una simple falta de asignaciones: Si los resultados de la llamada a GetComponents exceden la capacidad de la lista, esta lista se redimensionará. Pero si estás reutilizando o agrupando la lista, al menos la frecuencia de todas las asignaciones disminuirá durante el curso de la vida útil de tus asignaciones.

Uso óptimo de las estructuras de datos

Problema: Elección de estructuras de datos incorrectas

Evita elegir estructuras de datos solo porque sea conveniente utilizarlas; en cambio, elige estructuras que ofrezcan características de desempeño que se alineen mejor con el algoritmo o sistema de juego que estás escribiendo.

Solución:

La indexación en una matriz o en una lista es extremadamente simple: solo requiere la adición de un poco de memoria física y tiene lugar en un tiempo constante. Por lo tanto, la indexación aleatoria o la interación a través de una matriz o una Lista tiene una carga extremadamente baja. Entonces, si estás iterando cada cuadro a través de una lista de objetos, debes preferir las matrices o las Listas.

Si necesitas una adición o eliminación de tiempo constante, probablemente necesites utilizar un Diccionario o HashSet (esto se explicará con mayor detalle, a continuación).

Si estás relacionando datos con orientación a los valores clave, en la que una porción de datos se relaciona con otra de una manera unidireccional, utiliza un Diccionario.

Algo más acerca de los HashSet y Diccionarios: Ambas estructuras de datos están respaldadas por una tabla hash. Recuerda que una tabla hash tiene una serie de sectores de almacenamiento; cada sector de almacenamiento es, básicamente, una lista que contiene todos los valores que poseen un código hash específico. En C#, este código hash proviene del método GetHashCode. Debido a que, por lo general, el número de valores en un sector de almacenamiento determinado es mucho menor que el tamaño total del HashSet o Diccionario, agregar o eliminar objetos de un sector de almacenamiento está más cerca al tiempo constante que a hacerlo en forma aleatoria con objetos de una Lista o Matriz. La diferencia precisa depende de la capacidad de tu tabla hash y del número de elementos que estás almacenando en ella.

Verificar la presencia (o ausencia) de un valor dado en un HashSet o un Diccionario es muy simple, por el mismo motivo: el proceso de verificar el número (relativamente pequeño) de valores en el sector de almacenamiento que representa el código hash del valor es rápido.

Problema: Demasiada carga como resultado del uso inadecuado de los diccionarios

Si se desea iterar pares de elementos de datos en cada cuadro, con frecuencia se utiliza un diccionario porque es más rápido. El problema con esto es que estás realizando la iteración sobre una tabla hash. Esto significa que la iteración debe efectuarse en cada sector de almacenamiento individual de dicha tabla hash, independientemente de que contenga o no valores. Esto agrega una carga considerable, en especial cuando la iteración se realiza en una tablas hash que contiene pocos valores almacenados en ella.

Solución:

En cambio, crea una estructura de datos o tupla y luego almacena una Lista o una matriz de estas estructuras o tuplas que contenga las relaciones de tus datos. Efectúa la iteración sobre esta Lista o Matriz en lugar de hacerlo sobre un diccionario.

Aproximadamente en la marca de 14:25 minutos en su charla, Ian nos ofrece un consejo rápido acerca de cómo y cuándo utilizar los diccionarios con clave InstanceID para reducir la carga.

Problema: ¿Y si tienes varios problemas?

En el mundo real, se te presentan muchos problemas con requisitos múltiples superpuestos y no existe una sola estructura de datos que satisfaga todas tus necesidades.

Un ejemplo común podría ser un Update Manager. Este es un patrón de programación arquitectónica en la que un objeto (por lo general, un MonoBehavious) distribuye rellamadas de Actualización a diferentes sistemas dentro de tu juego. Cuando los sistemas desean recibir Actualizaciones, se suscriben a ellos a través del objeto Update Manager o Gestor de actualizaciones. Para los sistemas de este tipo, necesitarías una iteración de carga baja, inserción de tiempo constante y comprobaciones duplicadas en tiempo constante.

Solución: Utiliza 2 estructuras de datos

  • Mantén una lista o una matriz para la iteración.
  • Antes de modificar la lista, utiliza un HashSet (u otro tipo de conjunto de indexación) para asegurar que el elemento que estás agregando o eliminando se encuentre presente realmente.
  • Si la eliminación constituye un problema, considera la implementación de una lista enlazada o una lista enlazada de una manera intrusiva (ten presente que esto produce como resultado un costo de memoria mayor y una carga de iteración ligeramente mayor).
Mas recursos

¡Debemos saberlo! ¿Te gustó este contenido?

Sí. Que sigan llegando Me da igual. Podría ser mejor
Lo tengo

Usamos cookies para brindarte la mejor experiencia en nuestro sitio web. Visita nuestra página de política de cookies si deseas más información.