Personajes de sprites modulares y su animación.

Esta publicación de blog está completamente dedicada a mi sistema de animación de personajes, está llena de consejos útiles y fragmentos de código.

En los últimos dos meses, he creado hasta 9 nuevas acciones de jugadores (cosas divertidas como bloquear con un escudo, esquivar un salto y armas), 17 nuevos elementos portátiles, 3 conjuntos de armadura (placa, seda y cuero) y 6 tipos de peinados. También terminé de crear toda la automatización y las herramientas, por lo que todo ya está en uso en el juego. ¡En el artículo contaré cómo logré esto!


Espero que esta información sea útil y demuestre que no es necesario ser un genio para crear independientemente tales herramientas / automatización.

Breve descripción


Inicialmente, quería comprobar si es posible combinar sprites superpuestos entre sí con animadores sincronizados para crear un personaje modular con peinados reemplazables, equipos y artículos portátiles. ¿Es posible combinar la animación de píxeles dibujados a mano con un personaje verdaderamente personalizable?

Por supuesto, tales funciones se usan activamente en juegos 3D y 2D con sprites pre-renderizados o en juegos 2D con animación esquelética, pero que yo sepa, no hay muchos juegos que combinen animación creada manualmente y personajes modulares (generalmente porque el proceso resulta ser demasiado monótono)


Descubrí este antiguo GIF de mi primer mes con Unity. ¡De hecho, este sprite modular fue uno de mis primeros experimentos en el desarrollo de juegos!

Creé un prototipo usando el sistema de animación Unity, y luego agregué una camisa, un par de pantalones, un peinado y tres elementos para probar el concepto. Esto requirió 26 animaciones separadas.

En ese momento, creé toda mi animación en Photoshop y no me molesté con la automatización del proceso, por lo que fue muy aburrido. Entonces pensé: "Entonces, la idea básica funcionó, luego agregaré nuevas animaciones y equipos". Resultó que "más tarde" es unos años más tarde.

En marzo de este año, dibujé el diseño de una gran cantidad de armadura (vea mi publicación anterior) y noté cómo este proceso puede hacerse más conveniente. Continué posponiendo la implementación, porque incluso con la automatización estaba nerviosa de que nada funcionaría.

Esperaba tener que abandonar la personalización del personaje y crear el único personaje principal, como en la mayoría de los juegos con animación manual. Pero tenía un plan de acción, ¡y era hora de comprobar si podía derrotar a este monstruo!



Spoiler: Todo salió genial. A continuación revelaré mis *** secretos ***

Sistema de sprites modulares


I. Conozca sus límites


Anteriormente, realicé muchas pruebas de arte y control de tiempo para averiguar cuánto tiempo puede llevar ese trabajo y si un nivel similar de calidad será alcanzable para mí.

Escribí todas mis ideas para la animación, las reuní en una hoja de cálculo y las organicé de acuerdo con varios criterios, como la utilidad, la belleza y el uso repetido. Para mi sorpresa, el primero en esta lista fue la animación del artículo (pociones, bombas, cuchillos, hachas, bolas).

Se me ocurrió una puntuación numérica para cada animación y abandoné todo con un bajo rendimiento. Inicialmente, planeé crear 6 conjuntos de armadura, pero rápidamente me di cuenta de que era demasiado y descarté tres tipos.

El aspecto del seguimiento del tiempo resultó ser muy importante, y recomiendo usarlo para responder preguntas como: "¿Cuántos enemigos puedo darme el lujo de crear en el juego?". Después de solo unas pocas pruebas, logré extrapolar una estimación bastante precisa. Con más trabajo en animaciones, continué haciendo un seguimiento del tiempo y revisando mis expectativas.

Compartiré una copia de mi diario de trabajo durante los últimos dos meses. Tenga en cuenta que este tiempo es adicional a mi trabajo habitual, donde paso 30 horas a la semana:

https://docs.google.com/spreadsheets/d/1Nbr7lujZTB4pWMsuedVcgBYS6n5V-rHrk1PxeGxr6Ck/edit?usp=sharing

II Cambiar la paleta para un futuro mejor


Usando sabiamente los colores en el diseño del sprite, puede dibujar un sprite y crear muchas variaciones diferentes cambiando la paleta. Puede cambiar no solo los colores, sino también crear varios elementos de encendido y apagado (por ejemplo, reemplazando los colores con transparencia).

Cada conjunto de armadura tiene 3 variaciones, y al mezclar las partes superior e inferior, puedes obtener muchas combinaciones. Planeo implementar un sistema en el que puedas recolectar un conjunto de armadura para la apariencia del personaje, y otro para sus características (como en Terraria).


En el proceso, me sorprendieron gratamente las combinaciones curiosas descubiertas. Si conecta la parte superior de la placa con un fondo de seda, puede obtener algo al estilo de un mago de guerra.

Es mejor cambiar las paletas usando los colores que codifican el valor en el sprite para que luego pueda tomarlas para encontrar el color real de la paleta. Sé que estoy simplificando un poco, así que aquí hay un video para comenzar:


No explicaré todo en detalle, sino que hablaré sobre formas de implementar esta técnica en Unity, y sus ventajas y desventajas.

1. Buscar textura para cada paleta


Esta es la mejor estrategia para crear variaciones de enemigos, fondos y todo donde muchos sprites tienen la misma paleta / material. No se pueden agrupar diferentes materiales en lotes, incluso si usan el mismo sprite / atlas. Trabajar con texturas es bastante doloroso, pero puede cambiar las paletas en tiempo real reemplazando materiales usando SpriteRenderer.sharedMaterial.SetTexture o MaterialPropertyBlock si necesita diferentes paletas para cada instancia del material. Aquí hay un ejemplo de una función de fragmento de sombreador:

sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = tex2D(_PaletteTex, half2(lookup.r * (_PaletteTex_TexelSize.x / 0.00390625f), 0.5)); color.a *= lookup.a; return color * input.color; } 

2. Matriz de colores.


Me decidí por esta decisión porque necesitaba reemplazar las paletas cada vez que cambiaba la apariencia del personaje (por ejemplo, al ponerme elementos), y crear algunas paletas dinámicamente (para mostrar los colores de piel y cabello elegidos por el jugador). Me pareció que en tiempo de ejecución y en el editor para estos fines sería mucho más fácil trabajar con matrices.

Código:

 sampler2D _MainTex; half4 _Colors[32]; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = _Colors[round(lookup.r * 255)]; color.a *= lookup.a; return color * input.color; } 

Presenté mis paletas como un tipo ScriptableObject y utilicé la herramienta MonoBehaviour para editarlas. Después de haber trabajado durante mucho tiempo en la edición de paletas en el proceso de creación de animaciones en Aseprite, me di cuenta de las herramientas que necesitaba y escribí estos scripts en consecuencia. Si desea escribir su propia herramienta para editar paletas, aquí hay algunas funciones que definitivamente recomiendo implementar:

- Actualización de paletas en diversos materiales al editar colores para mostrar los cambios en tiempo real.

- Asignación de nombres y cambio del orden de los colores en la paleta (use el campo para almacenar el índice de color, no su orden en la matriz).

- Seleccione y edite múltiples colores a la vez. (Consejo: puede copiar y pegar los campos de Color en Unity: simplemente haga clic en un color, copie, haga clic en otro color, pegue, ¡ahora son iguales!)

- Aplicar color superpuesto a toda la paleta

- Grabar paleta en textura

3. Una textura de búsqueda única para todas las paletas.


Si desea cambiar las paletas sobre la marcha, pero al mismo tiempo necesita agrupar para reducir el número de llamadas de extracción, puede utilizar esta técnica. Puede ser útil para plataformas móviles, pero usarlo es bastante inconveniente.

En primer lugar, deberá empacar todas las paletas en una textura grande. Luego, utiliza el color especificado en el componente SpriteRenderer (color de vértice AKA) para determinar la línea que se leerá desde la textura de la paleta en el sombreador. Es decir, la paleta de este sprite se controla a través de SpriteRenderer.color. El color del vértice es la única propiedad de SpriteRenderer que se puede cambiar sin romper la condición (siempre que todos los materiales sean iguales).

En la mayoría de los casos, es mejor usar el canal alfa para controlar el índice, porque probablemente no necesitará un montón de sprites con diferente transparencia.

Código:

 sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half2 paletteUV = half2( lookup.r * _(PaletteTex_TexelSize.x / 0.00390625f), input.color.a * _(PaletteTex_TexelSize.y / 0.00390625f) ) half4 color = tex2D(_PaletteTex, paletteUV); color.a *= lookup.a; color.rgb *= input.color.rgb; return color; } 


Las maravillas de reemplazar paletas y capas de sprites. Tantas combinaciones

III. Automatice todo y use las herramientas adecuadas.


Para implementar esta función, la automatización era absolutamente necesaria, porque como resultado obtuve unas 300 animaciones y miles de sprites.

Mi primer paso fue crear un exportador para Aseprite para administrar mi esquema de capa de sprite loco usando una interfaz de línea de comando conveniente. Este es solo un script en perl que omite todas las capas y etiquetas en mi archivo Aseprite y exporta imágenes en un directorio específico y una estructura de nombres para que pueda leerlas más tarde.

Luego escribí un importador para Unity. Aseprite muestra un conveniente archivo JSON con datos de cuadros, para que pueda crear activos de animación mediante programación. Manejar Aseprite JSON y escribir este tipo de datos resultó ser bastante tedioso, así que los traigo aquí. Puede cargarlos fácilmente en Unity utilizando JsonUtility.FromJson <AespriteData>, solo recuerde ejecutar Aseprite con la opción --format 'json-array'.

Código:

 [System.Serializable] public struct AespriteData { [System.Serializable] public struct Size { public int w; public int h; } [System.Serializable] public struct Position { public int x; public int y; public int w; public int h; } [System.Serializable] public struct Frame { public string filename; public Position frame; public bool rotated; public bool trimmed; public Position spriteSourceSize; public Size sourceSize; public int duration; } [System.Serializable] public struct Metadata { public string app; public string version; public string format; public Size size; public string scale; } public Frame[] frames; public Metadata meta; } 

En el lado de la Unidad, tuve serios problemas en dos lugares: cargar / cortar una hoja de sprite y construir un clip de animación. Un ejemplo claro me ayudaría mucho, así que aquí hay un fragmento de código de mi importador para que no sufras tanto:

Código:

 TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); 

Si aún no lo ha hecho, créame, es muy fácil comenzar a crear sus propias herramientas. El truco más fácil es colocar un GameObject en la escena con el MonoBehaviour adjunto, que tiene el atributo [ExecuteInEditMode]. ¡Agrega un botón y estarás listo para la batalla! Recuerde que sus herramientas personales no tienen que verse bien, pueden ser puramente utilitarias.

Código:

 [ExecuteInEditMode] public class MyCoolTool : MonoBehaviour { public bool button; void Update() { if (button) { button = false; DoThing(); } } } 

Cuando se trabaja con sprites, es bastante fácil automatizar tareas estándar (por ejemplo, crear texturas de paleta o reemplazar colores por lotes en varios archivos sprite). Aquí hay un ejemplo a partir del cual puedes comenzar a aprender cómo cambiar tus sprites.

Código:

 string path = "Assets/Whatever/Sprite.png"; Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path); TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter; if (!textureImporter.isReadable) { textureImporter.isReadable = true; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); } Color[] pixels = texture.GetPixels(0, 0, texture.width, texture.height); for (int i = 0; i < pixels.Length; i++) { // Do something with the pixels, eg replace one color with another. } texture.SetPixels(pixels); texture.Apply(); textureImporter.isReadable = false; // Make sure textures are marked as un-readable when you're done. There's a performance cost to using readable textures in your project that you should avoid unless you plan to change a sprite at runtime. byte[] bytes = ImageConversion.EncodeToPNG(texture); File.WriteAllBytes(Application.dataPath + path.Substring(6), bytes); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); 

Cómo superé las oportunidades de Mecanim: queja


Con el tiempo, el prototipo de sistema de sprites modulares que creé usando Mecanim se convirtió en el mayor problema al actualizar Unity, porque la API estaba cambiando constantemente y estaba mal documentada. En el caso de una máquina de estado simple, sería aconsejable poder consultar el estado de cada clip o cambiar los clips en tiempo de ejecución. Pero no! Por razones de rendimiento, Unity hornea clips en su estado y nos obliga a usar un sistema de redefinición torpe para cambiarlos.

Mecanim en sí no es un sistema tan malo, pero me parece que no logra darse cuenta de su característica principal declarada: la simplicidad. La idea de los desarrolladores era reemplazar lo que parecía complicado y doloroso (scripting) con algo simple (máquina de estado visual). Sin embargo:

- Cualquier máquina de estado finito no trivial se convierte rápidamente en una red salvaje de nodos y conexiones, cuya lógica está dispersa en diferentes capas.

- Los casos de uso simples se ven obstaculizados por los requisitos generales del sistema. Para reproducir una o dos animaciones, debe crear un nuevo controlador y asignar estados / transiciones. Por supuesto, hay un desperdicio excesivo de recursos.

- Es curioso que, como resultado, todavía tenga que escribir código, porque para que la máquina de estado haga algo interesante, necesita un script que llame a Animator.SetBool y métodos similares.

- Para el uso múltiple de la máquina de estado con otros clips, debe duplicarlo y reemplazar los clips manualmente. En el futuro, deberá realizar cambios en varios lugares.

- Si desea cambiar lo que está en un estado en tiempo de ejecución, entonces tiene problemas. La solución es una API incorrecta o un gráfico loco con un nodo para cada animación posible.


La historia de cómo los desarrolladores de Firewatch se metieron en el infierno de las secuencias de comandos visuales . Lo curioso es que cuando el hablante muestra los ejemplos más simples, todavía se ven locos. Los espectadores literalmente gimen a las 12:41 . Agregue enormes costos de mantenimiento y comprenderá por qué no me gusta mucho este sistema.

Muchos de estos problemas ni siquiera son culpa de los desarrolladores de Mecanim, sino simplemente el resultado natural de ideas incompatibles: no se puede crear un sistema común y al mismo tiempo simple, y describir la lógica usando imágenes es más difícil que solo palabras / símbolos (¿alguien recuerda los diagramas de flujo UML?) . Recordé un fragmento del informe de Zack McClendon en Practice NYC 2018 , y si tiene tiempo, ¡le recomiendo que vea el video completo!

Sin embargo, lo descubrí. Las secuencias de comandos visuales siempre son censuradas por nerds agresivos de "escribe tu propio motor" que no entienden las necesidades del artista. Además, no se puede negar que la mayor parte del código parece una jerga técnica incomprensible.

Si ya eres un pequeño programador y haces juegos con sprites, entonces debes pensarlo dos veces. Cuando comencé, estaba seguro de que nunca podría escribir algo relacionado con el motor mejor que los desarrolladores de Unity.

¿Y sabes que? Resultó que el animador de sprites es solo un script que cambia el sprite después de un número específico de segundos. Sea como fuere, todavía tenía que escribir el mío. Desde entonces he agregado eventos de animación y otras funciones a mi proyecto específico, pero la versión básica que escribí en medio día cubre el 90% de mis necesidades. Consta de solo 120 líneas y se puede descargar de forma gratuita desde aquí: https://pastebin.com/m9Lfmd94 . Gracias por leer mi artículo. Hasta pronto!

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


All Articles