1. Introdução
Então, você quer ou tentou criar um jogo de ritmo, mas os elementos do jogo e a música rapidamente ficam fora de sincronia, e agora você não sabe o que fazer. Este artigo irá ajudá-lo com isso. Joguei jogos de ritmo no ensino médio e costumava frequentar a DDR no salão de jogos locais. Hoje estou sempre procurando novos jogos desse gênero, e projetos como
Crypt of the Necrodancer ou
Bit.Trip.Runner mostram que muito mais pode ser feito nesse gênero. Eu trabalhei um pouco em protótipos de jogos de ritmo no Unity e, como resultado, passei um mês criando um jogo de ritmo curto / quebra-cabeça
Atomic Beats . Neste artigo, falarei sobre as técnicas de construção de código mais úteis que aprendi na criação desses jogos. Não consegui encontrar informações sobre eles em nenhum outro lugar, ou foram apresentadas com menos detalhes.
Primeiro, devo expressar minha
profunda gratidão a Yu Chao pelo post de
Music Syncing in Rhythm Games [
tradução para Habré ]. Yu revisou o básico da sincronização de tempos de áudio com o mecanismo de jogo no Unity e fez o upload do código-fonte do jogo Boots-Cut, o que me ajudou muito na criação do meu projeto. Você pode estudar o post dele se quiser aprender uma breve introdução à sincronização de músicas do Unity, mas abordarei esse tópico com mais detalhes e de maneira mais abrangente. Meu código usa ativamente as informações do artigo e o código Boots-Cut.
No coração de qualquer jogo de ritmo estão os horários. As pessoas são extremamente sensíveis a qualquer distorção nos tempos do ritmo, por isso é muito importante que todas as ações, movimentos e entradas no jogo de ritmo sejam diretamente sincronizados com a música. Infelizmente, os métodos tradicionais de rastreamento de tempo do Unity, como
Time.timeSinceLevelLoad e
Time.time, perdem rapidamente a sincronização com o som que está sendo reproduzido. Portanto, acessaremos o sistema de áudio diretamente usando
AudioSettings.dspTime , que usa o número real de amostras de áudio processadas pelo sistema de áudio. Devido a isso, ele sempre mantém a sincronização com a música que está sendo reproduzida (talvez não seja o caso de arquivos de áudio muito longos, quando efeitos de amostragem entram em cena, mas no caso de composições de tamanho normal, o sistema deve funcionar perfeitamente). Essa função será o núcleo do nosso rastreamento de tempo de composição e, com base nela, criaremos a classe principal.
Condutor de classe
A classe Conductor é a principal classe de gerenciamento de composição, com base na qual o restante do jogo de ritmo será construído. Com ele, rastrearemos a posição da composição e gerenciaremos todas as outras ações sincronizadas. Para rastrear a composição, precisamos de algumas variáveis
Ao iniciar a cena, precisamos realizar cálculos para determinar as variáveis e também registrar para referência o horário de início da composição.
void Start() {
Se você criar um GameObject vazio com esse script anexado e, em seguida, adicionar a
fonte de áudio à composição e executar o programa, verá que o script registrará a hora de início da composição, mas nada mais acontecerá. Também precisaremos inserir manualmente o BPM da música que adicionamos à fonte de áudio.
Graças a todos esses valores, podemos acompanhar a posição na composição em tempo real ao atualizar o jogo. Determinaremos o tempo da composição, primeiro em segundos e depois em frações. As frações são uma maneira muito mais conveniente de rastrear uma composição, porque permitem adicionar ações e tempos no tempo paralelamente à composição, por exemplo, nas frações 1, 3 e 5,5, sem a necessidade de calcular segundos entre as frações. Adicione os seguintes cálculos à função Update () da classe Conductor:
void Update() {
Portanto, obtemos a diferença entre a hora atual de acordo com o sistema de áudio e a hora de início da composição, que fornece o número total de segundos em que a composição é reproduzida. Vamos salvá-lo na variável songPosition.
Observe que a partitura na música geralmente começa com uma unidade com as frações 1-2-3-4 e assim por diante, e songPositionInBeats começa em 0 e aumenta a partir desse valor; portanto, a terceira parte da composição corresponderá à songPositionInBeats, que é 2,0, e não 3,0.
Nesse ponto, se você deseja criar um jogo tradicional no estilo Dance Dance Revolution, crie notas de acordo com a fração em que precisa pressioná-las, interpole sua posição em relação à linha de clique e grave a músicaPositionInBeats quando a tecla for pressionada, e Compare o valor com a proporção desejada de notas. Yu Chao discute um exemplo desse esquema em seu
artigo . Para não me repetir, considerarei outras técnicas potencialmente úteis que podem ser construídas no topo da classe Conductor. Eu os usei ao criar
Atomic Beats .
Nós nos adaptamos ao compartilhamento inicial
Se você criar sua própria música para um jogo de ritmo, é fácil fazer com que a primeira batida corresponda exatamente ao início da música, que, se especificada corretamente, vinculará com segurança a música song da classe ConductorPositionInBeats à composição.
No entanto, se você usar músicas prontas, há uma alta probabilidade de que haja uma pequena pausa antes do início da composição. Se isso não for levado em consideração, a músicaPositionInBeats da classe Conductor pensará que a primeira batida começou quando a música começou a tocar, e não a batida agora. Tudo o que estará mais vinculado aos valores das ações não será sincronizado com a música.
Para corrigir isso, você pode adicionar uma variável que leva em consideração esse deslocamento. Adicione o seguinte à classe Conductor:
Em Update (), a variável songPosition:
songPosition = (float)(AudioSettings.dspTime - dspSongTime);
substituído por:
songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset);
Agora, o songPosition calculará corretamente a posição da música, levando em consideração a verdadeira primeira batida. No entanto, você precisará inserir manualmente o deslocamento na primeira batida; portanto, para cada arquivo, ele será único. Além disso, durante esse turno, haverá uma pequena janela na qual songPosition se tornará negativo. Isso pode não afetar o jogo, mas algum código, dependendo dos valores de songPosition ou songPositionInBeats, pode não ser capaz de processar números negativos no momento.

Repetições
Se você trabalha com uma composição que é reproduzida do começo ao fim, a classe Conductor mostrada acima será suficiente para rastrear a posição. Mas se você possui uma faixa curta em loop e deseja trabalhar com esse loop, precisa criar suporte ao Repeater no Conductor.
Se você tiver um fragmento perfeitamente em loop (por exemplo, se o andamento da música for 120bpm e o fragmento em loop tiver uma duração de 4 tempos, deverá ser exatamente 8,0 segundos a 2,0 segundos por compartilhamento) carregado na classe Audio Source da classe Conductor, marque a caixa de loop. O Conductor funcionará da mesma maneira que antes e transferirá o tempo total para songPosition após o
primeiro início do clipe. Para determinar a posição do loop, precisamos dizer de alguma forma ao Conductor quantas ações há em um loop e quantos loops já foram executados. Adicione as seguintes variáveis à classe Conductor:
Agora, a cada atualização no SongPositionInBeats, também podemos atualizar a posição Update () do loop.
Isso nos fornece um marcador que diz ao loopPositionInBeats quantos compartilhamentos passamos pelo loop, o que é útil para muitos outros itens sincronizados. Lembre-se de inserir o número de compartilhamentos do loop no GameObject Conductor.
Também devemos considerar cuidadosamente o cálculo das ações. A música sempre começa em 1; portanto, a medição em 4 partes assume o formato 1-2-3-4- e, em nossa classe, o loopPositionInBeats começa em 0.0 e passa sobre 4.0. Portanto, o meio exato do loop, que ao calcular as proporções musicais será 3, no loopPositionInBeats terá um valor de 2,0. Você pode modificar o loopPositionInBeats para levar isso em consideração, mas isso afetará todos os outros cálculos; portanto, tenha cuidado ao inserir notas.
Também para as ferramentas restantes, será útil adicionar mais dois aspectos à classe Conductor. Primeiro, uma versão analógica do LoopPositionInBeats chamada LoopPositionInAnalog, que mede a posição no loop no intervalo de 0 a 1,0. A segunda é uma instância da classe Conductor para chamadas convenientes de outras classes. Adicione as seguintes variáveis à classe Conductor:
Na função Desperta (), adicione:
void Awake() { instance = this; }
e adicione à função Update ():
loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;
Turn Sync
Seria muito útil sincronizar movimento ou rotação com os lobos para que os elementos estejam nos lugares certos. Nas minhas batidas atômicas, usei isso para girar notas dinamicamente em torno de um eixo central. Inicialmente, eles foram colocados em torno da circunferência de acordo com sua parte dentro do loop e, em seguida, toda a área de jogo foi girada para que as notas correspondessem à linha de depressão em sua parte.
Para conseguir isso, crie um novo script chamado SyncedRotation e anexe-o ao GameObject que você deseja girar. Adicione à função Update () do script SyncedRotation:
void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); }
Esse código interpolará a rotação do GameObject ao qual esse jogo está vinculado no intervalo de 0 a 360 graus, girando-o para que ele complete uma rotação completa no final de cada loop. Isso é útil como exemplo, mas para animação em loop ou quadro a quadro, seria mais útil sincronizar animações de loop para que elas se ajustassem perfeitamente ao andamento.
Sincronização de animação
O Unity
Animator é extremamente poderoso, mas nem sempre preciso. Para um alinhamento confiável de animações e músicas, tive que competir com a classe Animator e sua tendência a desincronizar gradualmente com o ritmo. Além disso, era difícil ajustar as mesmas animações para ritmos diferentes, para que, ao alternar entre composições, não fosse necessário redefinir os quadros-chave da animação para o andamento atual. Em vez disso, podemos ir diretamente para o loop de animação e definir a posição nesse loop de acordo com o local onde estamos no loop da classe Conductor.
Primeiro, crie uma nova classe chamada SyncedAnimation e adicione as seguintes variáveis:
Anexe-o a um GameObject novo ou existente que você deseja animar. Neste exemplo, simplesmente moveremos o objeto para frente e para trás na tela, mas o mesmo princípio pode ser aplicado a qualquer animação, antes de definir a propriedade ou animação quadro a quadro. Adicione um elemento Animator ao GameObject e crie um novo
Controlador Animator chamado SyncedAnimController, além de um
Clipe de animação chamado BackAndForth. Carregamos o controlador na classe Animator anexada ao GameObject e adicionamos Animação à árvore de animação como a animação padrão.
Por exemplo, configurei a animação para que primeiro mova o objeto para a direita em 6 unidades, depois para a esquerda em -6 e depois para 0.
Agora, para sincronizar a animação, adicione o seguinte código à função Start () da classe SyncedAnimation, que inicializa informações sobre o Animator:
void Start() {
Em seguida, adicione o seguinte código a Update () para definir a animação:
void Update() {
Então, posicionamos a animação no quadro exato da animação em relação a um loop completo. Por exemplo, se você usar a animação acima, quando estiver no meio do loop, a posição GameObject cruzará apenas 0. Isso pode ser aplicado a qualquer animação criada que você deseja sincronizar com o andamento do Conductor.
Também é importante notar que, para criar um loop contínuo de animações, você precisa configurar as tangentes dos
quadros-chave individuais
da animação na curva de animação. A configuração Linear criará uma linha reta que vai de um quadro-chave para o próximo, e Constant manterá a animação em um valor até o próximo quadro-chave, o que dará um movimento brusco e nítido.
Embora esse método seja útil, ele afeta todas as transições da animação, pois faz com que o
animationState permaneça no estado em que estava quando o script foi executado inicialmente. Esse método é útil para objetos que precisam usar apenas uma animação sincronizada infinitamente, mas para criar objetos mais complexos com diferentes animações sincronizadas, é necessário adicionar código que processa essas transições e define a variável currentState de acordo com o estado de animação desejado.
Conclusão
Estes são apenas alguns dos aspectos que me ajudaram na criação de Atomic Beats. Alguns deles foram coletados de outras fontes ou criados por necessidade, mas a maioria deles não consegui encontrar na forma final, então espero que isso seja útil! Talvez parte do meu sistema não seja mais útil em grandes projetos devido a limitações da CPU ou do sistema de áudio, mas será uma boa base para jogar um game jam ou um projeto de hobby.
Criar um jogo de ritmo, ou elementos de jogo sincronizados com a música, pode ser difícil. Para manter tudo em um ritmo consistente, você pode precisar de um código complicado; um resultado que permita tocar em um ritmo constante pode ser muito atraente para o jogador. Muito mais pode ser feito nesse gênero do que jogos no estilo tradicional Dance Dance Revolution, e espero que este artigo o ajude a implementar esses projetos. Também recomendo, se possível, avaliar meu jogo
Atomic Beats . Eu fiz em um mês na primavera deste ano, tem 8 faixas curtas e é grátis!