Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.
01/04/2024 | General,Tecnologías
Optimización de rendimiento en desarrollo de videojuegos. Estrategias y Herramientas
Introducción
La programación de videojuegos supone un papel fundamental a la hora de definir el rendimiento del videojuego y su eficiencia. A diferencia del desarrollo de aplicaciones tradicionales, en el desarrollo de videojuegos es necesario simplificar y evitar el uso de funciones complejas que pueden empeorar el rendimiento general del programa. Esto es algo que se tiene en común con el desarrollo de software en sistemas embebidos (cuando hay restricciones de potencia del hardware).
En este documento, nos centraremos en buenas prácticas y optimizaciones que pueden ser utilizadas en cualquier motor de juegos o lenguaje de programación orientado a objetos, pero se usarán ejemplos dentro del motor Unity y el lenguaje C#.
Programación
Durante la programación de un videojuego hay que tener especial cuidado con todas las operaciones que se hagan durante la partida. El coste de estas operaciones debe ser consistente para que el videojuego se reproduzca de forma fluida y sin tirones.
En Unity, existen varios eventos que dependen del estado de los objetos:
- Start(): cuando el objeto es creado. Aquí se deberá inicializar todos los recursos y variables necesarias para poder actualizar posteriormente el objeto
- Update(): actualización del objeto en cada frame. En un escenario en el que el videojuego está funcionando a 60 FPS (“frames per second”), este evento se llamará 60 veces cada segundo.
- Destroy(): cuando el objeto es destruido. Aquí será necesario liberar recursos y referencias del objeto.
El evento más importante de cara a optimizar el código de un videojuego es Update(), ya que es el que más veces va a ejecutarse. Para ello, podemos poner en práctica lo siguiente:
- Actualizar el objeto cada X segundos. Esto hará que en objetos que no requieren mucha precisión, eviten cálculos innecesarios.
- Actualizar información, y no crearla. La información debe ser actualizada, la creación de objetos en un Update generará problemas que veremos más adelante, además de ser operaciones generalmente caras para la CPU.
- Evitar operaciones de búsqueda o con alto coste que puedan ser cacheadas e inicializadas en el Start().
Gestión de memoria
Uno de los errores más habituales en el desarrollo de videojuegos es no tener en cuenta la creación y destrucción de objetos. Estas operaciones son seguras, pero afectan a la memoria utilizada por el videojuego. En el caso de Unity, al usar C#, se usa el garbage collector (GC) que se encarga de eliminar los objetos creados y reservados en memoria que ya no son necesarios.
El GC se activa cada cierto tiempo y, al activarse, genera un pequeño tirón en el videojuego. Para evitar este tirón, es necesario evitar al máximo la creación de objetos durante la partida. Para ello, existen varias técnicas:
Object pooling
En lugar de crear y destruir objetos de forma dinámica, lo ideal es crear una pool de objetos. Una pool se refiere a una lista de objetos creados y reservados en memoria al ejecutar el videojuego, y que son usados cuando sea necesario. En el caso de objetos 3D u objetos con una representación visual dentro del juego, como enemigos, balas, etc., estos son desactivados hasta que son usados, por lo que no son renderizados ni ejecutan ninguna lógica.
A la hora de querer usar un objeto (crearlo), simplemente se obtiene un objeto desactivado de la lista de objetos ya creada y se activa. Al finalizar su uso (destruirlo), simplemente se desactiva y se vuelven a almacenar en la lista de objetos.
Cachear valores
Relacionado con el punto anterior, a la hora de crear objetos que requieren un “new”, como una lista de objetos, se estará añadiendo al GC, por lo que acabará provocando problemas de rendimiento.
Un ejemplo de esto podría ser una función que devuelve los enemigos cercanos al jugador. Lo habitual sería crear una lista nueva y devolverla, pero esto generaría el problema comentado anteriormente. Para evitar esto, esa lista puede ser reutilizada por cada llamada, y eliminar su información una vez utilizada, o que el objeto que está pidiendo esta lista, la pase como parámetro por referencia y haya sido creada anteriormente.
Con esto, la carga del GC será muy reducida, por lo que la estabilidad del videojuego mejorará considerablemente.
Profiling y herramientas de debug
Dentro de Unity, encontramos la herramienta “Profiler«. Esta herramienta muestra el uso de recursos del videojuego en todos sus apartados. CPU, GPU, sonido, renderizado… En el caso de la lógica del juego, se encarga de analizar todas las funciones del videojuego, mostrando su tiempo de ejecución y uso de CPU.
Esta herramienta es útil para identificar cuellos de botella y operaciones con alto coste.
Resumen
A diferencia del desarrollo de software convencional, el desarrollo de videojuegos requiere ser muy meticuloso con las operaciones de alto coste y con los ciclos de actualización de objetos durante el tiempo de ejecución. Liberar la carga de estas actualizaciones y hacer un buen uso de la memoria, alocando la que sea necesaria en el inicio del programa, ayudará a conseguir un rendimiento óptimo y fluido.