Gran ciudad para dispositivos móviles en Unity. Experiencia en desarrollo y optimización.



Hola habr En esta publicación quiero compartir la experiencia de desarrollar un juego móvil masivo, con una gran ciudad y tráfico. Los ejemplos y técnicas descritos en la publicación no pretenden llamarse referencia e ideal. No soy un especialista certificado y no insto a repetir mi experiencia. El objetivo del juego era obtener una experiencia interesante, obtener un juego optimizado con un mundo abierto. Durante el desarrollo, intenté simplificar el código tanto como sea posible. Desafortunadamente, no usé ECS, pero pequé con singleton.

El juego


Un juego sobre el tema de la mafia. En el juego, intenté recrear América 30-40. Esencialmente, un juego es una estrategia económica en primera persona. El jugador captura el negocio e intenta mantenerlo a flote.
Implementado: tráfico de vehículos (semáforos, prevención de colisiones), tráfico de personas, bares, casinos, clubes, apartamentos de jugadores, compra de trajes, cambio de trajes, compra / pintura / reabastecimiento de combustible, policías, seguridad / gángsters, economía, venta / compra de recursos.

Arquitectura


imagen

Lamento no haber usado ECS, pero traté de andar en bicicleta. Al final, todo resultó ser engorroso y demasiado dependiente. La aplicación tiene un punto de entrada: el objeto de juego de la aplicación (go), en el que se cuelga la clase de aplicación del mismo nombre. Es responsable de precargar la base de datos, completar los grupos y la configuración inicial. Además, varias otras clases de componentes de administrador singleton recaen sobre los hombros de la aplicación (go).

  • Audiomanager
  • UIManager
  • Administrador de entrada

Intenté fanáticamente crear una arquitectura en la que pueda administrar varios componentes desde el administrador. Por ejemplo, AudioManager administra todos los sonidos, UIManager contiene todos los elementos y métodos de UI para la administración. Toda la entrada se procesa a través del InputManager usando eventos y delegados.

AudioManager simplificado. Le permite agregar tantos componentes de audio al objeto del juego y, si es necesario, reproducir sonido:

public class AudioManager : MonoBehaviour { public static AudioManager instance = null; //  public AudioClip metalHitAC; //   private AudioSource metalHitAS; //    public bool isMetalHit = false; private void Awake() { if (instance == null) instance = this; else if (instance == this) Destroy(gameObject); } void Start() { metalHitAS = AddAudio(metalHitAC, false, false, 0.3f, 1); } void LateUpdate() { if (isMetalHit) { metalHitAS.Play(); isMetalHit = false; } } AudioSource AddAudio(AudioClip clip, bool loop, bool playAwake, float vol, float pitch) { var newAudio = gameObject.AddComponent<AudioSource>(); newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = 10; return newAudio; } public AudioSource AddAudioToGameObject(AudioClip clip, bool loop, bool playAwake, float vol, float pitch, float minDistance, float maxDistance, GameObject go) { var newAudio = go.AddComponent<AudioSource>(); newAudio.spatialBlend = 1; newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = minDistance; newAudio.maxDistance = maxDistance; return newAudio; } } 

Al inicio, el método AddAudio agrega un componente, y luego desde cualquier lugar podemos reproducir el sonido que necesitamos:

 AudioManager.instance.isMetalHit = true; 

En este ejemplo, sería más prudente volver a poner el onehot que se está reproduciendo en el método.

Cómo se ve un InputManager simplificado:

 public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked; //public delegate void ClickActionFloatArg(float arg); //public static event ClickActionFloatArg OnRSliderValueChange, OnGSliderValueChange, OnBSliderValueChange; public void AimKeyDown() { OnAimKeyClicked(); } } 

Puse el método AimKeyDown en el botón y firmo el script de control de armas en OnAimKeyClicked:

 InputManager.instance.OnAimKeyClicked += GunShot; 

Todo mi sistema de entrada se implementa de manera similar. No noté ningún problema con la velocidad. Esto nos permitió recopilar todos los controladores de clics en un solo lugar: el InputManager.

Optimización


Pasemos a lo más interesante. Para los principiantes, el tema de la optimización en Unity es doloroso y está lleno de muchas dificultades. Compartiré lo que estaba tratando.

1. Caché de componentes (comience con conceptos básicos simples)

A menudo, en Toster puede encontrar preguntas con ejemplos de cuándo, dónde se usa GetComponent en la Actualización. No puede hacer esto, GetComponent está buscando un componente en el objeto. Esta operación es lenta y, al actualizarla, corre el riesgo de perder preciosos FPS. Aquí hay una buena explicación del almacenamiento en caché de componentes .

2. Usando SendMessage

Usar SendMessage () es más lento que GetComponent (). SendMessage revisa cada script para encontrar el método con el nombre deseado mediante la comparación de cadenas. GetComponent encuentra el script a través de la comparación de tipos y llama al método directamente.

3. Comparación de etiquetas de objeto

Utilice el método CompareTag en lugar de obj.tag == "string". En Unity, la extracción de cadenas de los objetos del juego crea una cadena duplicada, que agrega trabajo al recolector de basura. Es mejor evitar obtener el nombre del objeto del juego. No puede llamar a CompareTag en Update ni leer operaciones pesadas.

4. Materiales

Cuantos menos materiales, mejor. Reduzca la cantidad de materiales como sea posible. Para lograr esto, ayuda a texturizar el satén. Por ejemplo, casi toda la ciudad en mi juego está compuesta por 2-3 atlas. Cabe señalar que no todos los dispositivos móviles pueden trabajar con atlas grandes. Por lo tanto, si desea admitir dispositivos de entre 11 y 13 años, vale la pena considerarlo. Decidí rechazar la compatibilidad con Android por debajo de 5.1, ya que estos son en su mayoría dispositivos antiguos. Además, el juego se ejecuta en OpenGL 3.x debido a la representación lineal.

5. Física

Es fácil reducir el FPS a 10. Resultó que incluso los objetos estáticos interactúan y participan en los cálculos. Pensé erróneamente que los objetos físicos estáticos (objetos que tienen un componente RigidBody) son completamente pasivos bajo demanda. Me desvió el viejo tutorial que decía que donde sea que haya un colisionador debería haber RigidBody. Ahora todos mis objetos estáticos son Static + BoxCollider. Cuando necesito física, por ejemplo, farolas que se pueden derribar, creo que cortar el componente RigidBody si es necesario.

Las capas son la línea de vida para la optimización. Deshabilita la interacción innecesaria usando capas. Al reestructurar, use máscaras de capa. ¿Por qué necesitamos errores de cálculo adicionales? Recuerde que si su objeto tiene una cuadrícula de colisionador complejo y dispara con un rayo, es mejor crear un colisionador principal simple para "atrapar" los rayos. Cuanto más complejo es el colisionador, más errores de cálculo.

6. Eliminación de oclusión + Lod

Con una escena grande, el sacrificio de oclusión es indispensable. Para deshabilitar objetos (árboles, postes, etc.) a una gran distancia, uso Lod.

imagen

imagen

7. Grupo de objetos

Todas las implementaciones listas para usar del grupo de objetos que encontré usan instanciación. También eliminan y crean objetos. Tengo miedo de instanciar en todas sus manifestaciones. Operación lenta, que congela el juego, con un objeto más o menos grande. Decidí seguir un camino simple y rápido: todo mi grupo existe en forma de objetos de juego físicos que solo apago y enciendo si es necesario. Golpea RAM, pero es mejor. RAM para dispositivos modernos de 1GB, el juego consume 300-500 MB.

Grupo simple para gestionar bots de combate:

  public List<Enemy> enemyPool = new List<Enemy>(); private void Start() { //    Enemy Transform enemyGameObjectContainer = Application.instance.objectPool.Find("Enemy"); //  enemyPool  for (int i = 0; i < enemyGameObjectContainer.childCount; i++) { enemyPool.Add(new Enemy() { Id = i, ParentRoomId = 0, GameObj = enemyGameObjectContainer.GetChild(i).gameObject }); } } public void SpawnEnemyForRoom(int roomId, int amount, Transform spawnPosition, bool combatMode) { //Stopwatch sw = new Stopwatch(); //sw.Start(); foreach (Enemy enemy in enemyPool) { if (amount > 0) { if (enemy.ParentRoomId == 0 && enemy.GameObj.activeSelf == false) { // id   enemy.ParentRoomId = roomId; enemy.GameObj.transform.position = spawnPosition.position; enemy.GameObj.transform.rotation = spawnPosition.rotation; enemy.AICombat = enemy.GameObj.GetComponent<AICombat>(); enemy.AICombat.parentRoomId = roomId; // id  enemy.AICombat.id = enemy.Id; //   enemy.GameObj.SetActive(true); //      if (combatMode) enemy.AICombat.ActivateCombatMode(); amount--; } } if (amount == 0) break; } } 

Base de datos


Uso sqlite como base de datos, de manera conveniente y rápida. Los datos se presentan en forma de tabla, puede realizar consultas complejas. En la clase para trabajar con la base de datos, 800 líneas cuando. No puedo imaginar cómo se vería en XML / JSON.

Problemas y planes para el futuro.


Para pasar de la ciudad a las "habitaciones" elegí la implementación de "telepuertos". El jugador se acerca a la puerta, se carga la sala de escena y se teletransporta al jugador. Esto le ahorra tener que mantener habitaciones en la ciudad. Si implementa habitaciones en la ciudad, que es +15 habitaciones con relleno, el consumo de memoria aumentará a un mínimo de 1 GB. Esta implementación no me gusta, no es realista e impone muchas restricciones. Unity recientemente mostró una demostración de su Megaciudad , es impresionante. Quiero transferir gradualmente el juego a esc y usar la tecnología de Megacity para cargar edificios y locales. Esta es una experiencia fascinante e interesante, creo que resultará ser una ciudad verdaderamente vibrante. ¿Por qué no utilicé la escena de carga asíncrona ? Es simple, no funciona, no hay una escena de carga asíncrona lista para usar en la versión 2018.3. Inicialmente, esperaba una escena de carga asíncrona cuando planificaba una ciudad, pero resulta que, en escenas grandes, congela el juego como una escena de carga normal. Esto se confirmó en el foro de Unity, puede moverse, pero se necesitan muletas.

Algunas estadisticas:

Texturas: 304 / 374.3 MB
Mallas: 295 / 304.0 MB
Materiales: 101 / 148.0 KB (probable discrepancia aquí)
Videos de animación: 24 / 2.8 MB
AudioClips: 22 / 30.3 MB
Activos: 21761
GameObjects en escena: 29450
Total de objetos en la escena: 111645
Recuento total de objetos: 133406
Asignaciones de GC por trama: 70 / 2.0 KB
Un total de 4800 líneas de código C #.

Alguien me dijo que ese juego se puede hacer en una semana. Tal vez no soy productivo, tal vez esta persona es talentosa, pero para mí entendí una cosa: es difícil construir tales juegos solo. Quería crear algo interesante en el contexto de "dedos" casuales, me parece que me acerqué a mi sueño.

Puede ejecutar una prueba beta abierta y sentirla aquí: play.google.com/store/apps/details?id=com.ag.mafiaProject01 (si el ensamblaje de repente no funciona, debe adorarlo un poco, las actualizaciones llegan todas las noches). Espero que esto no se considere un enlace publicitario, ya que esta versión beta y las descargas no me darán una calificación y dividendos. Además, no creo que habr sea el público objetivo de mi juego.

Capturas de pantalla



Source: https://habr.com/ru/post/439220/


All Articles