Project Hospital es un juego sobre la gestión de un edificio hospitalario con todos los aspectos estándar del género: escenas dinámicas creadas por el jugador, muchos personajes y objetos activos, implementados por el sistema UI. Para que el juego funcionara en diferentes equipos, tuvimos que hacer muchos esfuerzos, y este fue un gran ejemplo de la infame "muerte por mil cortes", muchos pasos pequeños que resuelven un montón de problemas muy específicos y mucho tiempo dedicado a la creación de perfiles.
Nivel de rendimiento: lo que queríamos lograr
En una etapa temprana de desarrollo, decidimos los parámetros principales: el tamaño máximo de las escenas, el nivel de rendimiento y los requisitos del sistema.
Nos propusimos proporcionar soporte para al menos un centenar de personajes activos y totalmente animados en una pantalla, trescientos personajes activos en total, mapas en mosaico que miden aproximadamente 100x100 y hasta cuatro pisos en el edificio.
Estábamos firmemente convencidos de que el juego debería funcionar en 1080p con una velocidad de cuadros decente incluso en tarjetas gráficas integradas, y en sí mismo este objetivo no era tan difícil de lograr: el principal factor limitante es la CPU, especialmente con un aumento en el volumen del hospital. Las tarjetas gráficas integradas modernas comienzan a experimentar problemas solo a resoluciones de aproximadamente 2560 x 1440.
Para simplificar el soporte de mods, la mayoría de los datos se abrieron, es decir, tuvimos que sacrificar el rendimiento logrado al empaquetar los archivos, pero esto no tuvo un impacto particularmente fuerte, excepto por un tiempo de descarga un poco más largo.
Gráficos
Project Hospital es un juego isométrico 2D "clásico", por lo que puede comprender que todo se dibuja de atrás hacia adelante: en Unity, esto se hace estableciendo los valores apropiados a lo largo del eje Z (o distancia a la cámara) para objetos gráficos individuales. Si es posible, los objetos que no interactúan entre sí se organizan en capas, por ejemplo, los pisos son independientes de los objetos y los personajes.
Toda la geometría en una escena renderizada isométricamente se crea dinámicamente en C #, por lo que uno de los dos aspectos más importantes para el rendimiento de los gráficos es la frecuencia de la reconstrucción de la geometría. El segundo aspecto es el número de llamadas de extracción.
Dibujar llamadas
El número de objetos individuales dibujados en un cuadro, independientemente de su simplicidad, es la principal limitación, especialmente en equipos pobres (además, el motor Unity en sí mismo agrega un consumo excesivo de recursos). La solución obvia es agrupar (lote) tantos objetos gráficos como sea posible en una sola llamada de sorteo. Para que pueda obtener resultados bastante interesantes, por ejemplo, agrupe objetos que estén a la misma distancia de la cámara para que el resto de los gráficos se muestren correctamente detrás o delante de ellos.
Aquí hay algunos números: en un mosaico de 96 x 96, teóricamente puede colocar 9216 objetos, lo que requeriría 9216 llamadas de sorteo. Después del procesamiento por lotes, este número cae a 192.
Sin embargo, en la vida real, todo es un poco más complicado, porque solo puede agrupar objetos con la misma textura, por lo que los resultados son un poco menos óptimos, pero el sistema aún funciona bastante bien.
La mayoría de los lotes se realizan manualmente para tener control sobre los resultados. Además, en casos extremos, también utilizamos el procesamiento por lotes dinámico de Unity, pero esta es una espada de doble filo: en realidad ayuda a reducir la cantidad de llamadas de extracción, pero conduce a un desperdicio innecesario de recursos en cada marco, y en algunos casos puede ser impredecible. Por ejemplo, dos sprites superpuestos a la misma distancia de la cámara en diferentes marcos pueden renderizarse en un orden diferente, lo que provoca un parpadeo que no aparece cuando se procesa manualmente.
De varios pisos
Los jugadores pueden construir edificios con varios pisos, y esto aumenta la complejidad, pero, sorprendentemente, ayuda al rendimiento. Solo los personajes en el piso activo y en la calle deben renderizarse y animarse, y todo lo que se encuentra en los otros pisos del hospital puede ocultarse.
Sombreadores
Project Hospital utiliza sombreadores autoescritos relativamente simples con pequeños trucos, como el intercambio de colores. Suponga que un sombreador de caracteres puede reemplazar hasta cinco colores (dependiendo de las condiciones en el código del sombreador) y, por lo tanto, es bastante costoso, pero esto no parece causar problemas, porque los caracteres rara vez ocupan mucho espacio en la pantalla. El sombreador justificó el esfuerzo puesto en él, porque la capacidad de usar un número infinito de colores de ropa puede aumentar enormemente la variabilidad de los personajes y el entorno.
Además, aprendimos lo suficientemente rápido como para evitar especificar parámetros de sombreado y, en su lugar, usamos colores de vértice siempre que sea posible.
Calidad de la textura
Un hecho interesante: en Project Hospital no utilizamos ninguna compresión de textura: los gráficos se realizan en un estilo vectorial, y en algunas texturas la compresión se ve muy mal.
Para ahorrar memoria de la CPU en sistemas con menos de 1 GB, reducimos automáticamente el tamaño de las texturas del juego a la mitad de la resolución (excepto las texturas de la interfaz de usuario); esto se puede entender al ver el parámetro "calidad de textura: baja" en las opciones. Las texturas de la interfaz de usuario conservan su resolución original.
Optimizar el rendimiento de la CPU: subprocesamiento múltiple
Aunque la lógica de secuencias de comandos de Unity es esencialmente de un solo subproceso, siempre tenemos la capacidad de ejecutar varios subprocesos directamente en C #. Quizás este enfoque no sea adecuado para la lógica del juego, pero a menudo hay tareas de tiempo crítico que se pueden realizar en hilos separados organizando un sistema de tareas. En nuestro caso, los hilos se usaron para dos funciones:
1. La tarea de encontrar una ruta, especialmente en mapas grandes con una disposición confusa, puede tomar hasta cientos de milisegundos, por lo que este fue el principal candidato para la transferencia de la transmisión principal. Las tareas paralelas tienen en cuenta la cantidad de subprocesos de hardware de una máquina.
2. Las tarjetas de iluminación también se pueden actualizar en un flujo separado, pero solo un piso a la vez; este no es un sistema crítico, y las lámparas automáticas en las habitaciones se apagan a una velocidad tal que una actualización rara es suficiente.
Animaciones
Casi al comienzo del desarrollo, decidimos usar un sistema de animación esquelética bidimensional. Después de estudiar varios programas de animación modernos, finalmente decidimos modificar un sistema simple que creé hace varios años (esencialmente como un proyecto de pasatiempo), adaptándolo a las necesidades de Project Hospital: se asemeja a una columna vertebral simplificada con soporte directo para crear variaciones de personajes. Al igual que Spine, utiliza el tiempo de ejecución de C #, que obviamente es más costoso que el código nativo, por lo que durante el proceso de desarrollo realizamos un par de ciclos de optimización. Afortunadamente, nuestros equipos son bastante simples, solo unos 20 huesos por personaje.
Dato curioso: la mejora más útil para optimizar el acceso a la transformación de huesos individuales resultó ser la transición de la búsqueda de mapas a la simple indexación de matrices.
Además del hecho de que los personajes no están animados fuera de la cámara, hay otro truco: los personajes ocultos detrás de las ventanas de la interfaz de usuario principal tampoco necesitan ser animados. Desafortunadamente, en la versión final del juego, cambiamos a una interfaz de usuario translúcida, por lo que no pudimos usarla.
Almacenamiento en caché
Si es posible, tratamos de realizar los cálculos más costosos solo con cambios que afecten sus valores. El mejor ejemplo de esto son las habitaciones y los ascensores: cuando un jugador coloca un ascensor o construye muros, ejecutamos un algoritmo de relleno que marca las baldosas desde las cuales hay ascensores y habitaciones disponibles. Esto acelera la búsqueda posterior de rutas y puede usarse para mostrar al jugador qué habitaciones aún no están disponibles.
Actualizaciones dispersas y diferidas
En algunos casos, es lógico realizar ciertas actualizaciones solo parcialmente. Aquí hay algunos ejemplos:
Algunas actualizaciones se pueden realizar en cada cuadro solo para una parte de los caracteres, por ejemplo, los guiones de comportamiento de la mitad de los pacientes se actualizan solo en cuadros impares, y para la segunda mitad, en cuadros pares (aunque las animaciones y los movimientos se realizan sin problemas).
En ciertas condiciones, especialmente cuando los caracteres están en modo de espera, pero activan partes costosas del código (por ejemplo, empleados que verifican lo que debe llenarse y buscan equipos desocupados), las operaciones se realizan solo a ciertos intervalos, por ejemplo, una vez por segundo.
Uno de los desafíos más caros y al mismo tiempo comunes es verificar qué pruebas están disponibles para cada paciente. Al mismo tiempo, deben evaluarse muchos factores, por ejemplo, cuál del personal del departamento está ocupado actualmente y qué equipo está reservado. Además, esta información no es común a todos los pacientes porque se ve afectada, por ejemplo, por el médico que se les asignó y su capacidad para hablar. Es necesario verificar docenas de tipos de análisis disponibles, por lo tanto, en un marco, la actualización se realiza solo para algunos y continúa en el siguiente.
Conclusión
La optimización de un administrador de juegos con muchas partes interactivas ha demostrado ser un proceso largo. Regularmente tuve que trabajar con el generador de perfiles de Unity y solucionar los problemas más obvios, esto se ha convertido en una parte integral del proceso de desarrollo.
Por supuesto, siempre hay margen de mejora, pero estamos bastante satisfechos con los resultados. El juego hace frente a nuestras tareas, y los jugadores crean constantemente modificaciones para ello, superando significativamente el límite original en el número de personajes.
También vale la pena mencionar que, incluso en comparación con algunos juegos AAA en los que trabajé, en Project Hospital conocí la lógica de juego más compleja en mi práctica, por lo tanto, muchos de los problemas eran específicos de este proyecto. Sin embargo, todavía recomiendo dejar suficiente tiempo en cualquier proyecto para la optimización de acuerdo con la complejidad del juego.