Anotação
Olá pessoal. Recentemente, escrevi um artigo Gerando um ambiente baseado em som e música no Unity3D , no qual dei vários exemplos de jogos que usam a mecânica de geração de conteúdo com base em música e também falei sobre os aspectos básicos de tais jogos. Praticamente não havia código no artigo e prometi que haveria uma sequência. E aqui está, na sua frente. Desta vez, tentaremos criar uma faixa para uma corrida 2D, no estilo de Hill Climb, a partir da sua música. Vamos ver o que temos ..

1. Introdução
Lembro que esta série de artigos foi projetada para desenvolvedores iniciantes e para aqueles que recentemente começaram a trabalhar com som. Se você estiver fazendo uma transformação rápida de Fourier em sua mente, provavelmente ficará entediado.
Aqui está o nosso roteiro para hoje:
- Considere o que é discretização.
- Descubra quais dados podemos obter do Audio Clip Unity
- Entenda como podemos trabalhar com esses dados.
- Descubra o que podemos gerar com esses dados.
- Aprenda a criar um jogo com tudo isso (bem, ou algo semelhante a um jogo)
Então vamos lá!
Discretização de cingalês analógico
Como muitas pessoas sabem, para usar um sinal em sistemas digitais, precisamos convertê-lo. Uma das etapas de conversão é a amostragem de sinal, na qual o sinal analógico é dividido em partes (relatórios temporários), após o qual é atribuído a cada relatório o valor de amplitude que estava no momento selecionado.

A letra T indica o período de amostragem. Quanto menor o período, mais precisa será a conversão do sinal. Mas na maioria das vezes eles falam sobre o inverso: Taxa de amostragem (é lógico que seja F = 1 / T). 8.000 Hz são suficientes para um telefone individual e, por exemplo, uma das opções para o formato DVD-Audio requer uma frequência de amostragem de 192.000 Hz. O padrão na gravação digital (em editores de jogos, editores de música) é 44 100 Hz - essa é a frequência do CD Audio.
Os valores numéricos da amplitude são armazenados nas chamadas amostras e é com elas que iremos trabalhar. O valor da amostra é flutuante e pode ser de -1 a 1. Simplificado, fica assim.

Renderização de ondas sonoras (estática)
A forma de onda (ou forma de áudio e em pessoas comuns - "peixe") é uma representação visual do sinal sonoro ao longo do tempo. A forma de onda pode nos mostrar em que ponto do som a fase ativa ocorre e onde a atenuação ocorre. Frequentemente, a forma de onda é apresentada para cada canal separadamente, por exemplo, assim:

Imagine que já temos um AudioSource e um script no qual trabalhamos. Vamos ver o que a Unity pode nos dar.
Selecione o número de relatórios
Antes de prosseguirmos, precisamos falar um pouco sobre a profundidade de renderização do nosso som. Com uma frequência de amostragem de 44100 Hz por segundo, somos capazes de processar 44100 relatórios. Digamos que precisamos renderizar uma faixa com 10 segundos de duração. Desenharemos cada relatório com uma linha em um pixel de largura. Acontece que nossa forma de onda terá 441.000 pixels de comprimento. Você obtém uma onda sonora muito longa, alongada e pouco compreendida. Mas, nele você pode ver cada relatório específico! E você carregará terrivelmente o sistema, não importa como o desenhe.

Se você não cria software de áudio profissional, não precisa dessa precisão. Para uma imagem de áudio geral, podemos dividir todas as amostras em períodos maiores e obter, por exemplo, a média de cada 100 amostras. Então nossa onda terá uma forma muito distinta:

Obviamente, isso não é totalmente preciso, pois você pode pular os picos de volume necessários para tentar não o valor médio, mas o máximo desse segmento. Isso dará uma imagem um pouco diferente, mas seus picos não desaparecerão.
Preparando para receber áudio
Vamos definir a precisão de nossa amostra como qualidade e o número final de relatórios como sampleCount.
int quality = 100; int sampleCount = 0; sampleCount = freq / quality;
Um exemplo de cálculo de todos os números estará abaixo.
Em seguida, precisamos obter as amostras nós mesmos. Isso pode ser feito a partir de um clipe de áudio usando o método GetData .
public bool GetData(float[] data, int offsetSamples);
Este método utiliza uma matriz na qual grava amostras. offsetSamples - parâmetro responsável pelo ponto inicial da leitura da matriz de dados. Se você ler a matriz desde o início, deve haver zero.
Para registrar amostras, precisamos preparar uma matriz para elas. Por exemplo, assim:
float[] samples; float[] waveFormArray;
Por que multiplicamos o comprimento pelo número de canais? Agora vou contar ...
Muitas pessoas sabem que no som geralmente usamos dois canais: esquerdo e direito. Alguém sabe que existem sistemas 2.1, bem como 5.1, 7.1 nos quais as fontes sonoras envolvem todos os lados. O tema dos canais está bem descrito no wiki . Como isso funciona no Unity?
Ao baixar um arquivo, ao abrir um clipe, você pode encontrar a seguinte imagem:

É apenas mostrado aqui que temos dois canais, e você pode até notar que eles são diferentes um do outro. O Unity registra amostras desses canais um após o outro. Acontece esta imagem:
[L1,R1,L2,R2,L3,R3,L4,R4,L5,R5,L6,R6,L7,R7,L8,R8...]
É por isso que precisamos do dobro do espaço na matriz do que apenas para o número de amostras.
Se você selecionar a opção Forçar clipe mono, o canal será um e todo o som estará no centro. A pré-visualização da sua onda mudará imediatamente.


Receber dados de áudio
Aqui está o que temos:
private int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples,0);
Total, se a faixa durar 10 segundos e for de dois canais, obteremos o seguinte:
- O número de amostras no clipe (myAudio.clip.sample) = 44100 * 10 = 441000
- A matriz de amostras para dois canais é longa (samples.Length) = 441000 * 2 = 882000
- Número de relatórios (sampleCount) = 44100/100 = 441
- O comprimento da matriz final = samples.Length / sampleCount = 2000
Como resultado, trabalharemos com 2000 pontos, o que é suficiente para desenhar a onda. Agora você precisa incluir a imaginação e pensar em como podemos usar esses dados.
Crie uma faixa de áudio simples usando as ferramentas de depuração
Como muitas pessoas sabem, o Unity possui meios convenientes para exibir todos os tipos de informações de depuração. Um desenvolvedor inteligente baseado nessas ferramentas pode criar, por exemplo, extensões muito poderosas para o editor. Nosso caso mostra um uso muito atípico dos métodos Debug.
Para desenhar, precisamos de uma linha. Podemos fazer isso com a ajuda de um vetor que será criado a partir dos valores de nossa matriz. Observe que, para criar uma bela forma de espelho de áudio, precisamos "colar" as duas metades da nossa visualização.
for (int i = 0; i < waveFormArray.Length - 1; i++) {
Em seguida, basta usar Debug.DrawLine para desenhar nossos vetores. Qualquer cor pode escolher. Todos esses métodos devem ser chamados em Atualizar, portanto, atualizaremos as informações a cada quadro.
Debug.DrawLine(upLine, downLine, Color.green);
Se desejar, você pode adicionar um "controle deslizante" que mostrará a posição atual da faixa que está sendo reproduzida. Esta informação pode ser obtida no campo "AudioSource.timeSamples".
private float debugLineWidth = 5;
Total, aqui está o nosso script:
using UnityEngine; public class WaveFormDebug : MonoBehaviour { private readonly int quality = 100; private int sampleCount = 0; private int freq; private readonly float debugLineWidth = 5; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>();
E aqui está o resultado:

Crie uma paisagem sonora suave com o PolygonCollider2D
Antes de prosseguir para esta seção, quero observar o seguinte: é claro que dirigir pela trilha gerada pela música é divertido, mas do ponto de vista da jogabilidade, é praticamente inútil. E aqui está o porquê:
- Para que a faixa seja aceitável, precisamos suavizar nossos dados. Todos os picos desaparecem e você praticamente para de "sentir sua música"
- Geralmente, as faixas de música são altamente compactadas e representam um tijolo de som, pouco adequado para um jogo 2D.
- A questão não resolvida da velocidade do nosso transporte, que deve ser adequada à velocidade da pista. Quero considerar esse problema no próximo artigo.
Portanto, como um experimento, esse tipo de geração é bastante engraçado, mas é difícil criar um recurso de jogabilidade real com base nele. De qualquer forma, continuamos.
Então, precisamos criar o PolygonCollider2D usando nossos dados. Isso é fácil de fazer. PolygonCollider2D possui um campo de pontos públicos que aceita Vector2 []. Primeiro, precisamos transferir nossos pontos para os vetores do tipo desejado. Vamos criar uma função para converter a matriz de nossas amostras em uma matriz vetorial:
private Vector2[] CreatePath(float[] src) { Vector2[] result = new Vector2[src.Length]; for (int i = 0; i < size; i++) { result[i] = new Vector2(i * 0.01f, Mathf.Abs(src[i] * lineScale)); } return result; }
Depois disso, basta passar nossa matriz de vetores resultante para o colisor:
path = CreatePath(waveFormArray); poly.points = path;
Nós olhamos para o resultado. Aqui está o começo da nossa faixa ... hmm ... não parece muito aceitável (não pense em visualização ainda, os comentários virão mais tarde).

Temos uma forma de áudio muito nítida, então a faixa sai estranha. Precisa suavizar. Aqui usamos o algoritmo de média móvel. Você pode ler mais sobre isso no Habr, no artigo O algoritmo da média móvel (média móvel simples) .
No Unity, o algoritmo é implementado da seguinte maneira:
private float[] MovingAverage(int frameSize, float[] data) { float sum = 0; float[] avgPoints = new float[data.Length - frameSize + 1]; for (int counter = 0; counter <= data.Length - frameSize; counter++) { int innerLoopCounter = 0; int index = counter; while (innerLoopCounter < frameSize) { sum = sum + data[index]; innerLoopCounter += 1; index += 1; } avgPoints[counter] = sum / frameSize; sum = 0; } return avgPoints; }
Modificamos nossa criação de caminho:
float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path;
Verificando ...

Agora nossa pista parece bastante normal. Usei uma largura de janela de 10. Você pode modificar esse parâmetro para escolher a suavização de que precisa.
Aqui está o script completo para esta seção:
using UnityEngine; public class WaveFormTest : MonoBehaviour { private const int frameSize = 10; public int size = 2048; public PolygonCollider2D poly; private readonly int lineScale = 5; private readonly int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private Vector2[] path; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples, 0); waveFormArray = new float[(samples.Length / sampleCount)]; for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount * 2; }
Como eu disse no início da seção, com essa suavização, paramos de sentir a faixa, além disso, a velocidade da máquina não está atrelada à velocidade da música (BPM). Analisaremos esse problema na próxima parte desta série de artigos. Além disso, abordaremos o tópico de promoções. efeitos sob o ritmo. A propósito, peguei uma máquina de escrever desse ativo gratuito .
Provavelmente muitos de vocês, olhando as capturas de tela, se perguntaram como eu desenhei a trilha? Afinal, os colisores não são visíveis.
Usei a sabedoria da Internet e encontrei uma maneira de transformar um colisor de polígono em uma malha à qual você pode atribuir qualquer material, e o renderizador de linhas fará um esboço elegante. Este método é descrito em detalhes aqui . Triangulador que você pode enfrentar na Unity Community .
Conclusão
O que aprendemos neste artigo é um esboço básico para jogos musicais. Sim, nesta forma, é, até agora, um pouco feio, mas você pode dizer com segurança "Pessoal, fiz a máquina seguir a trilha de áudio!". Para tornar isso um jogo real, você precisa fazer muito esforço. Aqui está uma lista do que podemos fazer aqui:
- Ligue a velocidade da máquina à faixa de BPM. O jogador pode controlar apenas a inclinação do carro, mas não a velocidade. Então a música será sentida muito mais forte durante o curso.
- Faça um pouco de detector e adicione promoções. efeitos que funcionarão sob o ritmo. Além disso, você pode adicionar animação à carroceria do carro, que saltará na batida de uma batida. Tudo depende da sua imaginação.
- Em vez de mover a média, você precisa processar a trilha com mais competência e obter uma matriz de dados para que os picos não desapareçam, mas foi fácil criar um rastreio.
- Bem, e, claro, você precisa tornar a jogabilidade interessante. Você pode colocar um pouco de moeda em cada golpe, adicionar zonas de perigo etc.
Estudaremos tudo isso e muito mais nas demais partes desta série de artigos. Obrigado a todos pela leitura!