Introduccion
Entonces, quieres o intentaste crear un juego de ritmo, pero los elementos del juego y la música rápidamente no están sincronizados, y ahora no sabes qué hacer. Este artículo te ayudará con esto. Jugué juegos de ritmo desde la escuela secundaria y, a menudo, pasaba el rato en DDR en la sala de juegos. Hoy siempre estoy buscando nuevos juegos de este género, y proyectos como
Crypt of the Necrodancer o
Bit.Trip.Runner muestran que se puede hacer mucho más en este género. Trabajé un poco en prototipos de juegos de ritmo en Unity, y como resultado pasé un mes creando un juego de ritmo corto / rompecabezas
Atomic Beats . En este artículo, hablaré sobre las técnicas de creación de código más útiles que aprendí al crear estos juegos. No pude encontrar información sobre ellos en ningún otro lado, o se presentó con menos detalle.
Primero, debo expresar mi
profunda gratitud a Yu Chao por la publicación de
Music Syncing in Rhythm Games [
traducción al Habré ]. Yu revisó los conceptos básicos de la sincronización de los tiempos de audio con el motor del juego en Unity y subió el código fuente de su juego Boots-Cut, que me ayudó mucho a crear mi proyecto. Puede estudiar su publicación si desea obtener una breve introducción a la sincronización de música de Unity, pero abordaré este tema con más detalle y de manera más extensa. Mi código utiliza activamente la información del artículo y el código Boots-Cut.
En el corazón de cualquier juego de ritmo están los tiempos. Las personas son extremadamente sensibles a cualquier distorsión en los tiempos de ritmo, por lo que es muy importante que todas las acciones, movimientos y entradas en el juego de ritmo estén directamente sincronizados con la música. Desafortunadamente, los métodos tradicionales de seguimiento de tiempo de Unity como
Time.timeSinceLevelLoad y
Time.time perderán rápidamente la sincronización con el sonido que se está reproduciendo. Por lo tanto, accederemos al sistema de audio directamente usando
AudioSettings.dspTime , que usa la cantidad real de muestras de audio procesadas por el sistema de audio. Gracias a esto, siempre mantiene la sincronización con la música que se está reproduciendo (quizás este no sea el caso con archivos de audio muy largos, cuando entran en juego los efectos de muestreo, pero en el caso de canciones de una duración normal, el sistema debería funcionar perfectamente). Esta función será el núcleo de nuestro seguimiento del tiempo de composición, y en base a ella crearemos la clase principal.
Conductor de clase
La clase Conductor es la clase principal de gestión de composición sobre la base de la cual se construirá el resto del juego de ritmo. Con él, realizaremos un seguimiento de la posición de la composición y administraremos todas las demás acciones sincronizadas. Para rastrear la composición necesitamos algunas variables
Al comenzar la escena, necesitamos realizar cálculos para determinar las variables, y también registrar como referencia el tiempo de inicio de la composición.
void Start() {
Si crea un GameObject vacío con dicho script adjunto, y luego agrega la
Fuente de audio con la composición y ejecuta el programa, verá que el script registrará la hora de inicio de la composición, pero no sucederá nada más. También necesitaremos ingresar manualmente los BPM de la música que agreguemos a la fuente de audio.
Gracias a todos estos valores, podemos rastrear la posición en la composición en tiempo real al actualizar el juego. Determinaremos el tiempo de la composición, primero en segundos, luego en fracciones. Las fracciones son una forma mucho más conveniente de rastrear una composición, ya que nos permiten agregar acciones y temporizaciones en el tiempo en paralelo con la composición, por ejemplo, en las fracciones 1, 3 y 5.5, sin la necesidad de calcular segundos entre fracciones. Agregue los siguientes cálculos a la función Update () de la clase Conductor:
void Update() {
Entonces obtenemos la diferencia entre la hora actual de acuerdo con el sistema de audio y la hora de inicio de la composición, que da el número total de segundos que se reproduce la composición. Lo guardaremos en la variable songPosition.
Tenga en cuenta que la partitura en la música generalmente comienza con una unidad con fracciones 1-2-3-4 y así sucesivamente, y songPositionInBeats comienza en 0 y aumenta a partir de este valor, por lo que la tercera parte de la composición corresponderá a songPositionInBeats, que es 2.0, no 3.0.
En este punto, si desea crear un juego tradicional de estilo Dance Dance Revolution, debe crear notas de acuerdo con la fracción en la que necesita presionarlas, interpolar su posición con respecto a la línea de clic y luego grabar songPositionInBeats cuando se presiona la tecla, y Compare el valor con la proporción deseada de notas. Yu Chao analiza un ejemplo de tal esquema en su
artículo . Para no repetirme, consideraré otras técnicas potencialmente útiles que se pueden construir sobre la clase de Conductor. Los usé al crear
Atomic Beats .
Nos adaptamos a la cuota inicial
Si crea su propia música para un juego de ritmo, es fácil hacer que el primer ritmo coincida exactamente con el comienzo de la música, que, si se especifica correctamente, unirá de manera confiable la canción de la clase ConductorPositionInBeats a la composición.
Sin embargo, si usa música ya preparada, existe una alta probabilidad de que haya una pequeña pausa antes del comienzo de la composición. Si esto no se tiene en cuenta, la canción de la clase ConductorPositionInBeats pensará que el primer ritmo comenzó cuando comenzó a sonar la canción, y no el ritmo ahora. Todo lo que estará más vinculado a los valores de las acciones no está sincronizado con la música.
Para solucionar esto, puede agregar una variable que tenga en cuenta este desplazamiento. Agregue lo siguiente a la clase Conductor:
En Update (), la variable songPosition:
songPosition = (float)(AudioSettings.dspTime - dspSongTime);
reemplazado por:
songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset);
Ahora songPosition calculará correctamente la posición en la canción, teniendo en cuenta el verdadero primer tiempo. Sin embargo, deberá ingresar manualmente el desplazamiento al primer tiempo, por lo que para cada archivo será único. Además, durante este cambio habrá una pequeña ventana en la que songPosition resultará negativa. Es posible que esto no afecte el juego, pero algunos códigos, dependiendo de los valores de songPosition o songPositionInBeats, pueden no ser capaces de procesar números negativos en este momento.

Repeticiones
Si trabaja con una composición que se reproduce de principio a fin, la clase de Director que se muestra arriba será suficiente para seguir la posición. Pero si tiene una pista corta que está en bucle y desea trabajar con este bucle, debe incorporar el soporte de repetidor en Conductor.
Si tiene un fragmento en bucle perfecto (por ejemplo, si el tempo de la composición es de 120 lpm y el fragmento en bucle tiene una longitud de 4 latidos, debería ser exactamente 8,0 segundos a 2,0 segundos por acción) cargado en la clase de fuente de audio del conductor, luego marque la casilla de bucle. Conductor funcionará de la misma manera que antes, y transferirá el tiempo total a songPosition después del
primer inicio del clip. Para determinar la posición del bucle, necesitamos decirle al conductor cuántas acciones hay en un bucle y cuántos bucles ya se han reproducido. Agregue las siguientes variables a la clase Conductor:
Ahora con cada actualización de SongPositionInBeats, también podemos actualizar la posición Update () del bucle.
Esto nos da un marcador que le dice a loopPositionInBeats cuántos recursos compartimos por el bucle, lo que es útil para muchos otros elementos sincronizados. Recuerde ingresar el número de acciones del bucle en GameObject Conductor.
También debemos considerar cuidadosamente el cálculo de las acciones. La música siempre comienza en 1, por lo que la medición de 4 partes toma la forma 1-2-3-4-, y en nuestra clase loopPositionInBeats comienza en 0.0 y pasa por encima de 4.0. Por lo tanto, el centro exacto del bucle, que al calcular las proporciones musicales será 3, en loopPositionInBeats tendrá un valor de 2.0. Puede modificar loopPositionInBeats para tener esto en cuenta, pero esto afectará a todos los demás cálculos, así que tenga cuidado al insertar notas.
También para las herramientas restantes será útil agregar dos aspectos más a la clase Conductor. En primer lugar, una versión analógica de LoopPositionInBeats llamada LoopPositionInAnalog, que mide la posición en el bucle en el rango de 0 a 1.0. El segundo es una instancia de la clase Conductor para llamadas convenientes desde otras clases. Agregue las siguientes variables a la clase Conductor:
En la función Despertar (), agregue:
void Awake() { instance = this; }
y agregue a la función Update ():
loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;
Turn Sync
Sería muy útil sincronizar el movimiento o la rotación con los lóbulos para que los elementos estén en los lugares correctos. En mi juego Atomic Beats, usé esto para rotar dinámicamente las notas alrededor de un eje central. Inicialmente, se colocaron alrededor de la circunferencia de acuerdo con su parte dentro del bucle, y luego se rotó toda el área de juego para que las notas coincidieran con la línea de depresión en su parte.
Para lograr esto, cree un nuevo script llamado SyncedRotation y adjúntelo al GameObject que desea rotar. Agregue a la función Update () del script SyncedRotation:
void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); }
Este código interpolará la rotación del GameObject al que está vinculado este juego en el rango de 0 a 360 grados, girándolo para que complete una revolución completa al final de cada ciclo. Esto es útil como ejemplo, pero para la animación en bucle o cuadro por cuadro, sería más útil sincronizar las animaciones en bucle para que encajen perfectamente con el tempo.
Sincronización de animación
Unity
Animator es extremadamente poderoso, pero no siempre es preciso. Para una alineación confiable de animaciones y música, tuve que competir con la clase Animator y su tendencia a desincronizarse gradualmente con el ritmo. Además, era difícil ajustar las mismas animaciones a diferentes tempos, de modo que al cambiar entre composiciones, no tenía que redefinir los fotogramas clave de la animación al tempo actual. En cambio, podemos ir directamente al bucle de animación y establecer la posición en este bucle de acuerdo con el lugar en el que nos encontramos en el bucle de la clase Conductor.
Primero, cree una nueva clase llamada SyncedAnimation y agréguele las siguientes variables:
Adjúntelo a un GameObject nuevo o existente que desee animar. En este ejemplo, simplemente moveremos el objeto de un lado a otro de la pantalla, pero el mismo principio se puede aplicar a cualquier animación, ya sea antes de establecer la propiedad, o de la animación cuadro por cuadro. Agregue un elemento Animator a GameObject y cree un nuevo
Controlador Animator llamado SyncedAnimController, así como un
Clip de Animación llamado BackAndForth. Cargamos el controlador en la clase Animator adjunta al GameObject y agregamos Animación al árbol de animación como animación predeterminada.
Por ejemplo, configuré la animación para que primero mueva el objeto a la derecha en 6 unidades, luego a la izquierda en -6 y luego nuevamente a 0.
Ahora, para sincronizar la animación, agregue el siguiente código a la función Start () de la clase SyncedAnimation, que inicializa la información sobre Animator:
void Start() {
Luego agregue el siguiente código a Update () para configurar la animación:
void Update() {
Entonces colocamos la animación en el cuadro exacto de la animación en relación con un bucle completo. Por ejemplo, si usa la animación anterior, cuando se encuentra en el medio del bucle, la posición de GameObject solo cruzará 0. Esto se puede aplicar a cualquier animación que cree que desee sincronizar con el tempo del Conductor.
También vale la pena señalar que para crear un bucle continuo de animaciones, debe configurar las tangentes de
fotogramas clave individuales
de la animación en la curva de animación. La configuración Lineal creará una línea recta que va de un cuadro clave al siguiente, y Constant mantendrá la animación en un valor hasta el siguiente cuadro clave, lo que dará un movimiento brusco y brusco.
Aunque este método es útil, afecta a todas las transiciones de la animación, ya que hace que
animationState permanezca en el estado en que estaba cuando se ejecutó el script por primera vez. Este método es útil para los objetos que solo necesitan usar infinitamente una animación sincronizada, pero para crear objetos más complejos con diferentes animaciones sincronizadas, debe agregar código que procese estas transiciones y establezca la variable currentState de acuerdo con el estado de animación deseado.
Conclusión
Estos son solo algunos de los aspectos que me han sido útiles para crear Atomic Beats. Algunos de ellos fueron recolectados de otras fuentes o creados por necesidad, pero la mayoría de ellos no los pude encontrar en la forma final, ¡así que espero que esto sea útil! Quizás parte de mi sistema ya no sea útil en proyectos grandes debido a limitaciones de la CPU o del sistema de audio, pero será una buena base para jugar un atasco o un proyecto de pasatiempo.
Crear un juego de ritmo o elementos de juego sincronizados con música puede ser difícil. Para mantener todo a un ritmo constante, es posible que necesite un código complicado; un resultado que le permita jugar a un ritmo constante puede ser muy atractivo para el jugador. Se puede hacer mucho más en este género que los juegos en el estilo tradicional Dance Dance Revolution, y espero que este artículo te ayude a realizar tales proyectos. También recomiendo, si es posible, evaluar mi juego
Atomic Beats . Lo hice en un mes en la primavera de este año, tiene 8 pistas cortas y es gratis.