Cómo mejorar el rendimiento para funciones costosas en 2D City Builder


9

Ya he buscado respuestas, pero no pude encontrar el mejor enfoque para manejar costosas funciones / cálculos.

En mi juego actual (un edificio de ciudad basado en fichas 2d) el usuario puede colocar edificios, construir carreteras, etc. Todos los edificios necesitan una conexión a un cruce que el usuario debe colocar en el borde del mapa. Si un edificio no está conectado a este cruce, aparecerá un letrero de "No conectado a la carretera" sobre el edificio afectado (de lo contrario, debe eliminarse). La mayoría de los edificios tienen un radio y pueden estar relacionados entre sí (por ejemplo, un departamento de bomberos puede ayudar a todas las casas dentro de un radio de 30 baldosas). Eso es lo que también necesito actualizar / verificar cuando cambia la conexión de la carretera.

Ayer me encontré con un gran problema de rendimiento. Echemos un vistazo al siguiente escenario: un usuario también puede borrar edificios y carreteras. Entonces, si un usuario ahora rompe la conexión justo después del cruce , necesito actualizar muchos edificios al mismo tiempo . Creo que uno de los primeros consejos sería evitar los bucles anidados (que definitivamente es una gran razón en este escenario), pero tengo que verificar ...

  1. si un edificio todavía está conectado al cruce en caso de que se haya eliminado una baldosa de la carretera (lo hago solo para los edificios afectados por esa carretera). (Podría ser un problema menor en este escenario)
  2. la lista de mosaicos de radio y obtener edificios dentro del radio (bucles anidados: ¡gran problema!) .

    // Go through all buildings affected by erasing this road tile.
    foreach(var affectedBuilding in affectedBuildings) {
        // Get buildings within radius.
        foreach(var radiusTile in affectedBuilding.RadiusTiles) {
            // Get all buildings on Map within this radius (which is technially another foreach).
            var buildingsInRadius = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);  
    
            // Do stuff.
        }
    }
    

Todo esto descompone mi FPS de 60 a casi 10 por un segundo.

Entonces, ¿podría hacerlo? Mis ideas serían:

  • No utiliza el hilo principal (Función de actualización) para este, sino para otro hilo. Podría tener problemas de bloqueo cuando empiezo a usar subprocesos múltiples.
  • Usar una cola para manejar muchos cálculos (¿cuál sería el mejor enfoque en este caso?)
  • Mantenga más información en mis objetos (edificios) para evitar más cálculos (por ejemplo, edificios en radio).

Usando el último enfoque, podría eliminar una anidación en forma de foreach en su lugar:

// Go through all buildings affected by erasing this road tile.
foreach(var affectedBuilding in affectedBuildings) {
    // Go through buildings within radius.
    foreach(var buildingInRadius in affectedBuilding.BuildingsInRadius) {
        // Do stuff.
    }
}

Pero no sé si esto es suficiente. Los juegos como Cities Skylines tienen que manejar muchos más edificios si el jugador tiene un mapa grande. ¿Cómo manejan esas cosas? Puede haber una cola de actualización ya que no todos los edificios se actualizan al mismo tiempo.

¡Espero sus ideas y comentarios!

¡Muchas gracias!


2
El uso de un generador de perfiles debería ayudar a identificar qué parte del código tiene el problema. Podría ser la forma en que encuentras los edificios afectados, o tal vez el // haz cosas. Como nota al margen, los grandes juegos como City Skylines abordan estos problemas mediante el uso de estructuras de datos espaciales como los árboles cuádruples, por lo que todas las consultas espaciales son mucho más rápidas que atravesar una matriz con un bucle for. En su caso, por ejemplo, podría tener un gráfico de dependencia de todos los edificios y, al seguir ese gráfico, podría saber de inmediato qué afecta a qué sin iteraciones.
Exaila

Gracias por la información detallada. ¡Me gusta la idea de las dependencias! ¡Voy a echar un vistazo a eso!
Yheeky

Tu consejo fue genial! Acabo de usar el generador de perfiles VS que me mostró que tenía una función de búsqueda de ruta para cada edificio afectado para verificar si la conexión de unión todavía es válida. ¡Por supuesto que es caro como el infierno! Son solo 5 FPS pero mejor que nada. Me desharé de eso y asignaré edificios a las baldosas de la carretera, así que no necesito hacer esta comprobación de ruta una y otra vez. ¡Muchas gracias! No, solo necesito arreglar los edificios en cuestión de radio, que es el más grande.
Yheeky

Me alegra que lo hayas encontrado útil: D
Exaila

Respuestas:


3

Caché de cobertura del edificio

La idea de almacenar en caché la información de qué edificios están dentro del alcance de un edificio efector (que puede almacenar en caché desde el efector o en el afectado) es definitivamente una buena idea. Los edificios (generalmente) no se mueven, por lo que hay pocas razones para rehacer estos costosos cálculos. "A qué afecta este edificio" y "qué afecta a este edificio" es algo que solo necesita verificar cuando se crea o se elimina un edificio.

Este es un intercambio clásico de ciclos de CPU por memoria.

Manejo de información de cobertura por región

Si resulta que está utilizando demasiada memoria para realizar un seguimiento de esta información, vea si puede manejar dicha información por regiones del mapa. Divide tu mapa en regiones cuadradas de n*nlosas. Si una región está completamente cubierta por un departamento de bomberos, todos los edificios en esa región también están cubiertos. Por lo tanto, solo necesita almacenar información de cobertura por región, no por edificio individual. Si una región solo está parcialmente cubierta, debe recurrir al manejo de conexiones de construcción en esa región. Entonces, la función de actualización para sus edificios primero verificaría "¿La región en la que se encuentra este edificio está cubierta por un departamento de bomberos?" y si no "¿Este edificio está cubierto individualmente por un departamento de bomberos?". Esto también acelera las actualizaciones, porque cuando se elimina un departamento de bomberos, ya no necesita actualizar los estados de cobertura de 2000 edificios, solo necesita actualizar 100 edificios y 25 regiones.

Actualización demorada

Otra optimización que puede hacer es no actualizar todo de inmediato y no actualizar todo al mismo tiempo.

Si un edificio todavía está conectado o no a la red de carreteras no es algo que necesite verificar en cada cuadro (por cierto, también puede encontrar algunas formas de optimizar esto específicamente al analizar un poco la teoría de gráficos). Sería completamente suficiente si los edificios solo lo revisan periódicamente cada pocos segundos después de la construcción del edificio (Y si hubo un cambio en la red de carreteras). Lo mismo se aplica a los efectos de rango de construcción. Es perfectamente aceptable si un edificio solo verifica cada pocos cientos de fotogramas "¿Al menos uno de los departamentos de bomberos que me afectan todavía está activo?"

Por lo tanto, puede hacer que su ciclo de actualización solo haga estos costosos cálculos para unos cientos de edificios a la vez para cada actualización. Es posible que desee dar preferencias a los edificios que se encuentran actualmente en la pantalla, para que los jugadores reciban comentarios inmediatos sobre sus acciones.

En cuanto a Multithreading

Los constructores de ciudades tienden a estar en el lado computacionalmente más costoso, especialmente si quieres permitir que los jugadores construyan realmente grandes y si quieres tener una alta complejidad de simulación. Entonces, a la larga, podría no estar equivocado pensar en qué cálculos en su juego se pueden manejar de forma asíncrona.


Esto explica por qué SimCity en el SNES tarda un tiempo en volver a conectarse, supongo que también sucede con sus otros efectos de área amplia.
lozzajp

Gracias por tu comentario útil! También creo que tener más información en la memoria podría acelerar mi juego. También me gusta la idea de dividir el TileMap en regiones, pero no sé si este enfoque es lo suficientemente bueno como para deshacerme de mi problema inicial de larga data. Tengo una pregunta sobre la actualización retrasada. Supongamos que tengo una función que hace que mi FPS caiga de 60 a 45. ¿Cuál es el mejor enfoque para dividir los cálculos para manejar la cantidad perfecta que la CPU puede manejar?
Yheeky

@Yheeky No hay una solución universalmente aplicable para esto, porque depende en gran medida de la situación qué cálculos puede retrasar, cuáles no y cuál es una unidad de cálculo sensata.
Philipp

La forma en que traté de retrasar estos cálculos fue crear una cola con elementos que tienen un indicador "Actualizando actualmente". Solo se manejó este elemento que tenía este indicador establecido en verdadero. Cuando se ha completado el cálculo, el artículo se eliminó de la lista y se manejó el siguiente artículo. Esto debería funcionar, ¿verdad? Pero, ¿qué tipo de método podría usarse si sabe que un cálculo en sí mismo reduciría su FPS?
Yheeky

1
@Yheeky Como dije, no existe una solución universalmente aplicable. Lo que normalmente intentaría (en ese orden): 1. Vea si puede optimizar ese cálculo utilizando algoritmos y / o estructuras de datos más apropiados. 2. Vea si puede dividirlo en subtareas que puede retrasar individualmente. 3. Vea si puede hacerlo en una amenaza separada. 4. Deshágase de la mecánica del juego que necesita ese cálculo y vea si puede reemplazarlo por algo menos costoso desde el punto de vista computacional.
Philipp

3

1. Trabajo duplicado .

Su affectedBuildingsson presumiblemente cerca uno del otro, por lo que los diferentes radios se solapará. Marque los edificios que deben actualizarse y luego actualícelos.

var toBeUpdated = new HashSet<Tiles>();
foreach(var affectedBuilding in affectedBuildings) {
    foreach(var radiusTile in affectedBuilding.RadiusTiles) {
         toBeUpdated.Add(radiusTile);

}
foreach (var tile in toBeUpdated)
{
    var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);
    // Do stuff.
}

2. Estructuras de datos inadecuadas.

var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);

claramente debería ser

var buildingsInRadius = tile.Buildings;

donde Edificios es un IEnumerablecon tiempo de iteración constante (por ejemplo, a List<Building>)


¡Buen punto! Supongo que intenté usar un Distinct () en ese usando MoreLINQ, pero estoy de acuerdo en que esto podría ser más rápido que verificar los duplicados.
Yheeky
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.