¿Cómo exprimen los juegos nuestro PC o consola?
Destripando la optimización.
Uno de los temas del mes ha sido Call of Duty: Ghosts. En consolas por la polémica con las resoluciones, y en PC por los elevados requisitos del port. En las discusiones sobre las dos noticias, un término que se ha utilizado mucho es el de "optimización". Pero, ¿sabemos lo que significa? O es el "fue un mago" de la programación de videojuegos, ese recurso que utilizamos cuando algo no acaba de ir como esperamos y no sabemos a quién echar la culpa.
Comenzaremos definiendo qué es optimización. De forma genérica, optimización es el proceso de modificar un programa para que utilice menos recursos o se ejecute más rápido. En nuestro caso, nuestro programa es un videojuego que está formado por cientos (o miles) de ficheros de código fuente, modelos, animaciones y texturas. Nuestro objetivo es que, primero, el juego no use más memoria de la que tenemos disponible. En consola directamente no podías usar más memoria de la que tiene. En PC (y en consolas modernas) funcionará, pero si necesitas usar mucho la memoria virtual (usar el HDD como si fuera RAM) tendrás una penalización al rendimiento. En segundo lugar, el objetivo de optimizar es la acepción más usual: hacer que el juego vaya más rápido; ya sea cargando el nivel o mostrando más fotogramas por segundo.
En cualquiera de estos dos casos, el primer paso es determinar donde está el problema. Cambiar código hasta que el juego vaya fino no es la forma más efectiva de conseguir resultados. En ocasiones, un programador muy bueno o muy experimentado puede intuir de dónde viene la ineficiencia, pero hasta en estos casos no hace ningún daño confirmar la hipótesis antes de ponerse a tocar el código ya que cambiandolo se pueden introducir errores.
Para encontrar el problema se utiliza una herramienta, el "perfilador" (profiler), que nos permite saber en qué secciones del código el procesador gasta más tiempo. Esta herramienta ejecuta el juego y hace mediciones de tiempo por todo el código. Como mucho del trabajo en los juegos actuales lo realiza la tarjeta gráfica, si nuestro problema es de framerate tenemos que perfilar también los shaders (el código que ejecuta la GPU) con otra herramienta específica para ello.
Si hay un problema, estas mediciones nos permitirán saber en que el punto tenemos el cuello de botella. Habitualmente ese trozo de código no será perfecto (de hecho, posiblemente haga algo mal si nos está dando un problema de rendimiento) porque, si no, nos tocará pintar menos polígonos, lo que nos obligará a rehacer los modelos, o desactivar efectos.
Para poder seguir con la explicación, asumamos que hemos encontrado un punto especialmente problemático del programa que está perjudicando el rendimiento del resto del juego y es susceptible de mejorarse. ¿De qué maneras podemos arreglarlo?
La primera forma de optimizar un código es a través del diseño. Escogiendo los algoritmos adecuados o haciendo que el código haga "menos cosas" podemos mejorar mucho el rendimiento. Por ejemplo, si organizamos todos los enemigos en una estructura según su posición en el nivel, podemos descartar rápidamente los enemigos que tenemos muy lejos porque seguro que no interactuamos con ellos (incluso podríamos apagar su IA).
En gráficos hay toda una familia de optimizaciones basadas en la "oclusión" (culling) que se basa en estudiar cómo pintar el menor número de polígonos posible que se vean en pantalla. Estas optimizaciones van desde optimizaciones genéricas, como usar BSP para organizar la escenas hasta trucos concretos para nuestro juego (que son más fáciles de implementar); por ejemplo, al entrar en una habitación la puerta se cierra tras nosotros y el juego descarga todo el nivel anterior. Esto explica los ascensores de Mass Effect y los pasillos de Ikea para cambiar de zona en World of Warcraft. Además, en ambos casos aprovechan para cargar el escenario.
Incluso si hemos escogido la estructura de datos o el algoritmo adecuado, el juego puede seguir yendo lento. Cosas como el acceso a disco puede penalizar bastante el rendimiento (especialmente si son medios ópticos como una unidad de DVD). En estos casos, pueden ayudar bastante pequeñas optimizaciones en el código como guardar el resultado de un cálculo pesado o hacer estos cálculos sólo cuando son necesarios. En el ejemplo de las IA, si tenemos muchos enemigos ir actualizando la animación sólo de los que se ven seguramente nos ahorra bastantes cálculos.
No siempre tienen que optimizar los programadores. Como he dicho, a veces no se puede (o no es fácil) mejorar el rendimiento cambiando el código, así que toca pintar menos cosas en pantalla. Otras veces, realmente hay un problema en los datos. Los artistas también tienen bugs. Por ejemplo, puede ocurrir que un artista se haya pasado de polígonos en un trozo concreto del escenario o que un diseñador haya provocado demasiados efectos de partículas en una escena concreta. En estos casos toca rehacer esas partes para reducir detalle y que el juego vaya a la velocidad que esperamos.
En este punto, es interesante introducir el concepto de "budget" y de "benchmark". Los artistas no hacen los modelos "a ojo de buen cubero", sino que tienen un presupuesto ("budget") tanto de polígonos como de tamaño de texturas para cada personaje y para el escenario, teniendo en cuenta la plataforma objetivo y lo que nuestro motor es capaz de pintar
¿Puede estar un juego más optimizado? Seguramente. ¿Puede permitirse la empresa retrasar el juego, con el coste que ello conlleva, hasta que esté perfecto? Esto casi ninguna.
¿Como sabemos cuánto rinde nuestro motor? Esto se sabe haciendo un "benchmark". Hacemos una escena de prueba, intentando que tenga todo lo que necesita nuestro juego y exprimiendo el motor para ver cuanto rinde en las plataformas que queremos soportar. Normalmente iremos aumentando y ajustando la cantidad de objetos hasta que veamos que podemos pintar a 60 (aunque últimamente el objetivo parece ser 30) y luego distribuiremos los polígonos según la importancia. Por ejemplo, es bastante común que el protagonista tenga más detalle porque lo ves siempre. En cambio, si hay un enemigo menos en pantalla no suele notarse tanto.
En la mayoría de los casos, tenemos que llegar a un compromiso o bien entre calidad y velocidad o bien entre memoria y velocidad. Podemos acelerar muchos procesos precalculando el resultado. Es bastante común, por ejemplo, que las luces estáticas del escenario se pre-calculen, en argot 3D se cocinen ("bake"), lo que nos da más frames a costa de menor fidelidad. Por supuesto, es mucho más bonito si podemos calcular la luz "por pixel" y, si os fijais, los juegos que calculan iluminación por pixel intentan hacer escenas donde justifiquen esta opción, por ejemplo con muchas fuentes de iluminación dinámicas: lámparas que se mueven mucho, hogueras, que el jugador lleve una antorcha, etc. Si estás pagando el coste, al menos que se note.
¿Hasta donde llega la optimización? También es un compromiso. Los motores gráficos pueden optimizarse, los modelos pueden refinarse y, si no, siempre están apareciendo nuevos artículos sobre gráficos que nos ofrecen más fidelidad de imagen. La única variable que casi nunca podemos tocar es el tiempo. Este es inmutable. El juego tiene que salir en navidad. Como mucho se puede forzar la máquina y hacer horas extras, pero los que hayáis hecho overclock sabéis que forzar la máquina a veces tiene consecuencias imprevistas. Así que, en cuanto el juego es jugable y cumple los estándares del mercado, todo el trabajo extra es "para nota". ¿Puede estar un juego más optimizado? Seguramente. ¿Puede permitirse la empresa retrasar el juego, con el coste que ello conlleva, hasta que esté perfecto? Esto casi ninguna.