Plugin isométrico para Unity3D


Es una historia sobre cómo escribir un complemento para Unity Asset Store , resolver los problemas isométricos conocidos en los juegos y ganar un poco de dinero con eso, y también para comprender cuán expansible es el editor de Unity. Imágenes, código, gráficos y pensamientos en el interior.


Prologo


Entonces, fue una noche cuando descubrí que no tenía prácticamente nada que hacer. El año entrante no fue realmente prometedor en mi vida profesional (a diferencia del personal, sin embargo, esa es una historia completamente diferente). De todos modos, tuve la idea de escribir algo divertido por el bien de los viejos tiempos, que sería bastante personal, algo por mi cuenta, pero que todavía tenía una pequeña ventaja comercial (me gusta esa sensación cálida cuando su proyecto es interesante para otra persona, excepto para su empleador). Y todo esto va de la mano con el hecho de que he esperado mucho tiempo para ver las posibilidades de la extensión del editor de Unity y ver si hay algo bueno en su plataforma para vender las extensiones propias del motor.


Dediqué un día a estudiar la tienda de activos: modelos, scripts, integraciones con varios servicios. Y primero parecía que todo ya estaba escrito e integrado, con incluso una serie de opciones de diferentes niveles de calidad y detalle, tanto como los precios y el soporte. Así que de inmediato lo he reducido a:


  • solo código (después de todo, soy programador)
  • Solo 2D (ya que me encanta 2D y acaban de ofrecer un soporte inmediato decente para eso en Unity)

Y luego recordé cuántos cactus he comido y cuántos ratones han muerto cuando estábamos haciendo un juego isométrico antes. No creerá cuánto tiempo hemos matado buscando soluciones viables y cuántas copias hemos roto en los intentos de resolver esta isometría y dibujarla. Entonces, luchando por mantener mis manos quietas, busqué por diferentes palabras clave y no tanto y no pude encontrar nada más que una gran pila de arte isométrico, hasta que finalmente decidí hacer un complemento isométrico desde cero.


Establecer los objetivos


Lo primero que necesitaba era describir brevemente qué problemas debía resolver este complemento y qué uso haría el desarrollador de juegos isométricos. Entonces, los problemas de isometría son los siguientes:


  • ordenar objetos por lejanía para dibujarlos correctamente
  • extensión para creación, posicionamiento y desplazamiento de objetos isométricos en el editor

Por lo tanto, con los objetivos principales para la primera versión formulada, me puse un plazo de 2-3 días para la primera versión borrador. Por lo tanto, no se puede diferir, ya ves, ya que el entusiasmo es algo frágil y si no tienes algo listo en los primeros días, hay una gran posibilidad de que lo arruines. Y las vacaciones de Año Nuevo no son tan largas como parece, incluso en Rusia, y quería lanzar la primera versión dentro de unos diez días.


Clasificación


Para abreviar, la isometría es un intento realizado por sprites 2D para parecerse a modelos 3D. Eso, por supuesto, resulta en docenas de problemas. El principal es que los sprites deben clasificarse en el orden en que se dibujarán para evitar problemas de superposición mutua.



En la captura de pantalla puedes ver cómo se dibuja primero el sprite verde (2,1) y luego el azul (1,1)


La captura de pantalla muestra la clasificación incorrecta cuando el sprite azul se dibuja primero

En este caso simple, la ordenación no será un problema, y ​​habrá opciones, por ejemplo:


  • ordenando por posición de Y en la pantalla, que es * (isoX + isoY) 0.5 + isoZ **
  • dibujo de la celda de cuadrícula isométrica más remota de izquierda a derecha, de arriba a abajo [(3,3), (2,3), (3,2), (1,3), (2,2), (3, 1), ...]
  • y un montón de otras formas interesantes y no realmente interesantes

Todos son bastante buenos, rápidos y funcionan, pero solo en el caso de tales objetos unicelulares o columnas extendidas en dirección isoZ :) Después de todo, estaba interesado en una solución más común que funcione para los objetos extendidos en la dirección de una coordenada, o incluso las "cercas" que no tienen absolutamente ningún ancho, pero se extienden en la misma dirección que la altura necesaria.



La captura de pantalla muestra la forma correcta de clasificar objetos extendidos 3x1 y 1x3 con "cercas" que miden 3x0 y 0x3

Y ahí es donde comienzan nuestros problemas y nos ponen en el lugar donde tenemos que decidir el camino a seguir:


  • dividir objetos "multicelulares" en objetos "unicelulares", es decir, cortarlos verticalmente y luego clasificar las franjas emergidas


  • pensar en el nuevo método de clasificación, más complicado e interesante



Elegí la segunda opción, ya que no tenía ningún deseo particular de entrar en el procesamiento complicado de cada objeto, en el corte (incluso automático) y el enfoque especial de la lógica. Para el registro, utilizaron la primera forma en algunos juegos famosos como Fallout 1 y Fallout 2 . De hecho, puedes ver esas tiras si ingresas a los datos de los juegos.


Entonces, la segunda opción no implica ningún criterio de clasificación. Significa que no hay un valor precalculado por el que pueda ordenar los objetos. Si no me cree (y supongo que muchas personas que nunca trabajaron con isometría no lo hacen), tome un pedazo de papel y dibuje objetos pequeños que midan como 2x8 y, por ejemplo, 2x2 . Si de alguna manera logra calcular un valor para calcular su profundidad y clasificación, simplemente agregue un objeto 8x2 e intente ordenarlos en diferentes posiciones entre sí.


Entonces, no existe tal valor, pero aún podemos usar dependencias entre ellos (más o menos, cuál se superpone) para la clasificación topológica . Podemos calcular las dependencias de los objetos mediante el uso de proyecciones de coordenadas isométricas en el eje isométrico.



La captura de pantalla muestra que el cubo azul depende del rojo


La captura de pantalla muestra que el cubo verde depende del azul

Un pseudocódigo para la determinación de dependencia para dos ejes (lo mismo funciona con el eje Z):


bool IsIsoObjectsDepends(IsoObject obj_a, IsoObject obj_b) { var obj_a_max_size = obj_a.position + obj_a.size; return obj_b.position.x < obj_a_max_size.x && obj_b.position.y < obj_a_max_size.y; } 

Con este enfoque construimos dependencias entre todos los objetos, pasándolos recursivamente y marcando la coordenada Z de la pantalla. El método es bastante universal y, lo más importante, funciona. Puede leer una descripción detallada de este algoritmo, por ejemplo, aquí o aquí . También usan este tipo de enfoque en la popular biblioteca isométrica flash ( as3isolib ).


Y todo fue genial, excepto que la complejidad temporal de este enfoque es O (N ^ 2) ya que tenemos que comparar cada objeto con cada uno para crear las dependencias. Dejé la optimización para versiones posteriores, agregando solo una reordenación perezosa para que nada se clasifique hasta que algo se mueva. Entonces, hablaremos sobre la optimización un poco más tarde.


Extensión del editor


De ahora en adelante, tenía los siguientes objetivos:


  • la clasificación de objetos tenía que funcionar en el editor (no solo en un juego)
  • tenía que haber otro tipo de Gizmos-Arrow (flechas para mover objetos)
  • opcionalmente, habría una alineación con los mosaicos cuando el objeto se mueve
  • los tamaños de los mosaicos se aplicarían y establecerían en el inspector mundial isométrico automáticamente
  • Los objetos AABB se dibujan de acuerdo con sus tamaños isométricos.
  • salida de coordenadas isométricas en el inspector de objetos, cambiando cuál cambiaríamos la posición del objeto en el mundo del juego

Y todos estos objetivos se han logrado. Unity realmente permite expandir significativamente su editor. Puede agregar nuevas pestañas, ventanas, botones, nuevos campos en el inspector de objetos. Si lo desea, incluso puede crear un inspector personalizado para un componente del tipo exacto que necesita. También puede generar información adicional en la ventana del editor (en mi caso, en los objetos AABB) y reemplazar los gizmos de movimiento estándar de los objetos. El problema de ordenar dentro del editor se resolvió mediante esta etiqueta mágica ExecuteInEditMode , que permite ejecutar componentes del objeto en modo editor, es decir, hacerlo de la misma manera que en un juego.


Todo esto se hizo, por supuesto, no sin dificultades y trucos de todo tipo, pero no hubo un solo problema en el que haya pasado más de un par de horas (Google, foros y comunidades seguramente me ayudaron a resolver todos los problemas surgido que no fueron mencionados en la documentación).



La captura de pantalla muestra mis artilugios para objetos de movimiento dentro del mundo isométrico

Lanzamiento


Entonces, preparé la primera versión, tomé la captura de pantalla. Incluso dibujé un icono y escribí una descripción. Es hora Entonces, establezco un precio nominal de $ 5, subo el complemento en la tienda y espero a que Unity lo apruebe. No pensé mucho en el precio, ya que realmente no quería ganar mucho dinero todavía. Mi propósito era averiguar si existe una demanda general y si fuera así, me gustaría estimarla. También quería ayudar a los desarrolladores de juegos isométricos que de alguna manera terminaron privados de oportunidades y adiciones.


En 5 días bastante dolorosos (pasé casi el mismo tiempo escribiendo la primera versión, pero sabía lo que estaba haciendo, sin preguntarme ni pensar demasiado, eso me dio la mayor velocidad en comparación con las personas que acababan de comenzar a trabajar con isometría) Recibí una respuesta de Unity que decía que el complemento estaba aprobado y que ya podía verlo en la tienda, así como sus cero ventas (hasta ahora). Se registró en el foro local, incorporó Google Analytics en la página del complemento en la tienda y me preparé para esperar a que crezca la hierba.


No pasó mucho tiempo antes de las primeras ventas, justo cuando surgieron los comentarios en el foro y en la tienda. Para los días restantes del 12 de enero se vendieron copias de mi complemento, lo que consideré como un signo del interés público y decidí continuar.


Optimización


Entonces, no estaba contento con dos cosas:


  • Complejidad temporal de la clasificación - O (N ^ 2)
  • Problemas con la recolección de basura y el rendimiento general

Algoritmo


Teniendo 100 objetos y O (N ^ 2), tuve que hacer 10,000 iteraciones solo para encontrar dependencias, y también tuve que pasarlos todos y marcar la pantalla Z para ordenar. Debería haber alguna solución para eso. Entonces, probé una gran cantidad de opciones, no pude dormir pensando en este problema. De todos modos, no voy a contarte sobre todos los métodos que he probado, pero describiré el que he encontrado el mejor hasta ahora.


Lo primero es lo primero, por supuesto, clasificamos solo los objetos visibles. Lo que significa es que constantemente necesitamos saber qué hay en nuestro tiro. Si hay algún objeto nuevo, debemos agregarlo en el proceso de clasificación, y si uno de los viejos se ha ido, ignórelo. Ahora, Unity no permite determinar el cuadro delimitador del objeto junto con sus hijos en el árbol de la escena. Pasar por alto a los niños (cada vez, por cierto, ya que se pueden agregar y quitar) no funcionaría, demasiado lento. Tampoco podemos usar OnBecameVisible y otros eventos porque funcionan solo para objetos primarios. Pero podemos obtener todos los componentes de Renderer del objeto necesario y sus hijos. Por supuesto, no parece nuestra mejor opción, pero no pude encontrar otra forma, la misma universal y aceptable por rendimiento.


 List<Renderer> _tmpRenderers = new List<Renderer>(); bool IsIsoObjectVisible(IsoObject iso_object) { iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); for ( var i = 0; i < _tmpRenderers.Count; ++i ) { if ( _tmpRenderers[i].isVisible ) { return true; } } return false; } 

Hay un pequeño truco de usar la función GetComponentsInChildren que permite obtener componentes sin asignaciones en el búfer necesario, a diferencia de otro que devuelve una nueva matriz de componentes


En segundo lugar, todavía tenía que hacer algo con respecto a O (N ^ 2) . He probado varias técnicas de división del espacio antes de detenerme en una cuadrícula bidimensional simple en el espacio de visualización donde proyecto mis objetos isométricos. Cada uno de estos sectores contiene una lista de objetos isométricos que lo cruzan. Entonces, la idea es simple: si las proyecciones de los objetos no se cruzan, entonces no tiene sentido construir dependencias entre los objetos. Luego pasamos por alto todos los objetos visibles y construimos dependencias solo en los sectores donde es necesario, reduciendo así la complejidad temporal del algoritmo y aumentando el rendimiento. Calculamos el tamaño de cada sector como un promedio entre los tamaños de todos los objetos. Encontré el resultado más que satisfactorio.


Rendimiento general


Por supuesto, podría escribir un artículo separado sobre esto ... Bien, tratemos de hacer esto corto. Primero, estamos cobrando los componentes (usamos GetComponent para encontrarlos, lo que no es rápido). Recomiendo a todos que estén atentos cuando trabajen con cualquier cosa que tenga que ver con la Actualización . Siempre debe tener en cuenta que sucede para cada cuadro, por lo que debe tener mucho cuidado. Además, recuerde todas las características interesantes, como el operador personalizado == . Hay muchas cosas a tener en cuenta, pero al final puedes conocer cada una de ellas en el generador de perfiles incorporado. Hace que sea mucho más fácil memorizarlos y usarlos :)


También puedes entender realmente el dolor del recolector de basura. ¿Necesita un mayor rendimiento? Luego, olvídate de cualquier cosa que pueda asignar memoria, que en C # (especialmente en el antiguo compilador Mono ) puede hacerse por cualquier cosa, desde foreach (!) Hasta lambdas emergentes, y mucho menos LINQ, que ahora está prohibido para ti, incluso en los casos más simples. Al final, en lugar de C # con su azúcar sintáctico, obtienes una apariencia de C con capacidades ridículas.


Aquí voy a dar algunos enlaces sobre el tema que pueden serle útiles:
Parte1 , Parte2 , Parte3 .


Resultados


Nunca antes había conocido a nadie que usara esta técnica de optimización, así que me alegré especialmente de ver los resultados. Y si en las primeras versiones se necesitaron literalmente 50 objetos en movimiento para que el juego se convirtiera en una presentación de diapositivas, ahora funciona bastante bien incluso cuando hay 800 objetos en un marco: todo gira a la máxima velocidad y se reorganiza solo por 3-6 ms, que es muy bueno para este número de objetos en isometría. Además, después de la inicialización, casi no ha asignado memoria para un marco :)


Oportunidades adicionales


Después de leer comentarios y sugerencias, hubo algunas características que agregué en las versiones anteriores.


Mezcla 2D / 3D


Mezclar 2D y 3D en juegos isométricos es una oportunidad interesante que permite minimizar el dibujo de diferentes opciones de movimiento y rotación (por ejemplo, modelos 3D de personajes animados). No es realmente difícil de hacer, pero requiere integración dentro del sistema de clasificación. Todo lo que necesita es obtener un cuadro delimitador del modelo con todos sus elementos secundarios y luego mover el modelo a lo largo de la pantalla Z por el ancho del cuadro.


 Bounds IsoObject3DBounds(IsoObject iso_object) { var bounds = new Bounds(); iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); if ( _tmpRenderers.Count > 0 ) { bounds = _tmpRenderers[0].bounds; for ( var i = 1; i < _tmpRenderers.Count; ++i ) { bounds.Encapsulate(_tmpRenderers[i].bounds); } } return bounds; } 

ese es un ejemplo de cómo puedes obtener Bounding Box del modelo con todos sus elementos secundarios



y así es como se ve cuando está hecho

Configuraciones isométricas personalizadas


Eso es relativamente simple. Me pidieron que hiciera posible establecer el ángulo isométrico, la relación de aspecto y la altura del mosaico. Después de sufrir algo de dolor relacionado con las matemáticas, obtienes algo como esto:




Física


Y aquí se pone más interesante. Dado que la isometría simula el mundo 3D, se supone que la física también es tridimensional, con altura y todo. Se me ocurrió este truco fascinante. Replico todos los componentes de la física, como Rigidbody , Collider , etc., para el mundo isométrico. De acuerdo con estas descripciones y configuraciones, hago la copia del mundo tridimensional físico invisible usando el motor y el PhysX incorporado. Después de eso, tomo los datos de simulación calculados y obtengo esos bacl en componentes duplicados para el mundo isométrico. Luego hago lo mismo para simular eventos de choque y activación.



El conjunto de herramientas de demostración física GIF

Epílogo y conclusiones


Después de implementar todas las sugerencias del foro, decidí aumentar el precio hasta 40 dólares, por lo que no se vería como otro plugin barato con cinco líneas de código :) Estaré encantado de responder preguntas y Escucha tus consejos. Como es la primera vez que escribo algo sobre Habr, agradezco todo tipo de críticas, ¡gracias! Y ahora, algo que estaba ahorrando para el final, las estadísticas de ventas del mes:


Mes5 $40 $
Enero120 0
Febrero220 0
Marzo170 0
Abril9 90 0
Mayo9 90 0
Junio9 90 0
Julio7 74 4
Agosto0 04 4
Septiembre0 05 5

Enlace de la página de Unity Asset Store: Isometric 2.5D Toolset

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


All Articles