Unity3D中基于声音和音乐的环境生成。 第2部分。从音乐创建2D轨道

注解


大家好 相对最近,我写了一篇文章,在Unity3D中基于声音和音乐生成环境 ,其中给出了一些游戏示例,这些示例使用了基于音乐生成内容的机制,还谈到了此类游戏的基本方面。 文章中几乎没有代码,我保证会有续集。 在这里,就在您的面前。 这次,我们将尝试根据您的音乐,以Hill Climb的风格创建2D竞赛的音轨。 让我们看看我们得到了..



引言


我提醒您,本系列文章是为初学者和刚开始使用声音的开发人员设计的。 如果您在脑海中进行快速的傅立叶变换,那么您可能会感到无聊。


这是我们今天的路线图:


  1. 考虑什么是离散化。
  2. 找出我们可以从Audio Clip Unity获得的数据
  3. 了解如何处理这些数据。
  4. 找出我们可以从这些数据中生成什么。
  5. 了解如何利用所有这些(或者类似游戏的东西)制作游戏

所以走吧!


模拟僧伽罗语的离散化


众所周知,为了在数字系统中使用信号,我们需要对其进行转换。 转换步骤之一是对信号进行采样,其中将模拟信号分为多个部分(临时报告),然后为每个报告分配选定时刻的振幅值。


字母T表示采样周期。 周期越短,信号转换将越准确。 但大多数情况下,他们谈论的是相反的情况:采样率(这是F = 1 / T是合乎逻辑的)。 8,000 Hz足以用于电话信号,例如,DVD音频格式的选项之一需要192,000 Hz的采样频率。 数字录音(在游戏编辑器,音乐编辑器中)的标准是44 100 Hz-这是CD音频的频率。


振幅的数值存储在所谓的样本中,我们将与它们一起工作。 样本的值是float,可以是-1到1。经过简化,看起来像这样。



声波渲染(静态)


基本资料


波形(或音频形式,在普通人中为“鱼”)是声音信号随时间变化的视觉表示。 该波形可以向我们显示声音在哪个点上发生了有源相位,以及在哪里衰减。 通常,每个通道的波形分别显示,例如:



想象一下,我们已经有了一个AudioSource和一个可以在其中工作的脚本。 让我们看看Unity可以给我们带来什么。


//  AudioSource    AudioSource myAudio = gameObject.GetComponent<AudioSource>(); //     .     44100. int freq = myAudio.clip.frequency; 

选择报告数


在继续之前,我们需要谈谈声音的渲染深度。 以每秒44100 Hz的采样频率,我们能够处理44100个报告。 假设我们需要渲染10秒长的轨道。 我们将用像素宽度的线条绘制每个报告。 事实证明,我们的波形长为441,000像素。 您会得到一个很长,很细且几乎无法理解的声波。 但是,您可以在其中看到每个特定的报告! 无论您如何绘制,都将极大地加载系统。



如果您不制作专业音频软件,则不需要这种准确性。 对于一般的音频图片,我们可以将所有采样分成更大的周期,例如,取每100个采样的平均值。 然后我们的wave将具有非常独特的形式:



当然,这并不是完全准确的,因为您可以跳过可能需要的体积峰值,因此您可以尝试的不是平均值,而是该分段的最大值。 这将产生稍微不同的图像,但是您的峰值不会消失。


准备接收音频


让我们将样本的准确性定义为质量,将最终报告的数目定义为sampleCount。


 int quality = 100; int sampleCount = 0; sampleCount = freq / quality; 

下面是计算所有数字的示例。


接下来,我们需要自己获取样本。 可以使用GetData方法从音频剪辑中完成此操作。


 public bool GetData(float[] data, int offsetSamples); 

此方法将一个数组写入示例。 offsetSamples-负责读取数据数组的起点的参数。 如果从头开始读取数组,则应该为零。


要记录样本,我们需要为它们准备一个数组。 例如,像这样:


 float[] samples; float[] waveFormArray; //      samples = new float[myAudio.clip.samples * myAudio.clip.channels]; 

为什么我们将长度乘以通道数? 现在我告诉...


Unity音频频道信息


许多人都知道,在声音上,我们通常使用两个声道:左声道和右声道。 有人知道有2.1系统以及5.1和7.1,其中声源从四面八方环绕。 Wiki上对频道的主题进行了很好的描述。 这在Unity中如何工作?


下载文件时,打开剪辑时,您可以找到以下图像:



此处仅显示了我们有两个渠道,您甚至可以注意到它们彼此不同。 Unity依次记录这些通道的样本。 原来这张图:
[L1R1L2R2L3R3L4R4L5R5L6R6L7R7L8R8...]


这就是为什么我们在数组中需要的空间比仅是样本数大两倍的原因。


如果选择“强制到单声道”剪辑选项,则声道将为一个,所有声音将位于中央。 您的wave的预览将立即更改。




接收音频数据


这是我们得到的:


 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); //  ,    .       waveFormArray = new float[(samples.Length / sampleCount)]; //             for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { //Abs     ""    . .  waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount; } } 

总计,如果音轨走了10秒并且是两个通道,那么我们得到以下信息:


  • 剪辑中的样本数(myAudio.clip.sample)= 44100 * 10 = 441000
  • 两个通道的样本数组很长(samples.Length)= 441000 * 2 = 882000
  • 报告数量(sampleCount)= 44100/100 = 441
  • 最终数组的长度= samples.Length / sampleCount = 2000

结果,我们将使用2000点,这足以吸引我们。 现在,您需要发挥想象力,并考虑如何使用这些数据。


渲染音频信息


使用调试工具创建简单的音轨


众所周知,Unity具有显示各种Debug信息的便捷方式。 基于这些工具的聪明开发人员可以为编辑器进行非常强大的扩展。 我们的案例显示了Debug方法的非典型用法。


为了绘制,我们需要一条线。 我们可以借助将根据数组值创建的向量来完成此操作。 请注意,要制作精美的镜像音频形式,我们需要“粘合”可视化的两个部分。


 for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * .01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * .01f, -waveFormArray[i] * 10, 0); } 

接下来,只需使用Debug.DrawLine绘制矢量即可。 任何颜色都可以选择。 所有这些方法都必须在Update中调用,因此我们将在每一帧更新信息。


 Debug.DrawLine(upLine, downLine, Color.green); 

如果需要,可以添加“滑块”,以显示正在播放的曲目的当前位置。 该信息可以从“ AudioSource.timeSamples”字段获得。


 private float debugLineWidth = 5; // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); 

总计,这是我们的脚本:


 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>(); //  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; } } private void Update() { for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * 0.01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * 0.01f, -waveFormArray[i] * 10, 0); // Debug  Debug.DrawLine(upLine, downLine, Color.green); } // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); } } 

结果如下:



使用PolygonCollider2D创建平滑的音景


在继续本节之前,我要注意以下几点:当然,沿着由音乐生成的轨道行驶很有趣,但是从游戏性的角度来看,这实际上是没有用的。 这就是为什么:


  1. 为了使轨道可以通过,我们需要对数据进行平滑处理。 所有的高峰都消失了,您几乎停止了“感觉音乐”
  2. 通常,音乐曲目是高度压缩的,并且代表一个音砖,不适合2D游戏。
  3. 我们的运输速度尚未解决的问题,应该适合于轨道的速度。 我想在下一篇文章中考虑这个问题。

因此,作为实验,这种类型的生成非常有趣,但是很难基于此生成真实的游戏功能。 无论如何,我们继续。


因此,我们需要使用我们的数据制作PolygonCollider2D。 这很容易做到。 PolygonCollider2D有一个接受Vector2 []的公共点字段。 首先,我们需要将点转移到所需类型的向量上。 让我们做一个函数,将样本数组转换为向量数组:


 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; } 

之后,只需将我们得到的向量数组传递给对撞机:


 path = CreatePath(waveFormArray); poly.points = path; 

我们看一下结果。 这是我们旅程的开始……嗯……看起来不太顺利(暂时不要考虑可视化,以后再发表评论)。



我们的音频形式过于清晰,因此音轨显得很奇怪。 需要使其平滑。 在这里,我们使用移动平均算法。 您可以在文章《移动平均算法(简单移动平均)》中了解有关Habr的更多信息。


在Unity中,该算法的实现方式如下:


 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; } 

我们修改路径创建:


 float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; 

正在检查...



现在我们的轨迹看起来很正常。 我使用的窗口宽度为10。您可以修改此参数以选择所需的平滑度。


这是此部分的完整脚本:


 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; } //  ,    frameSize float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; } 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; } 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; } } 

正如我在本节开头所说的那样,通过这种平滑处理,我​​们不再感觉到音轨,此外,机器的速度与音乐速度(BPM)无关。 我们将在本系列文章的下一部分中分析此问题。 此外,我们将在此讨论特殊主题。 节拍下的效果。 顺便说一句,我从这笔免费资产中拿了一台打字机。


也许很多人在看屏幕截图时都想知道我是如何绘制曲目的? 毕竟,对撞机是不可见的。


我利用Internet的智慧,找到了一种方法,可以将多边形对撞机变成可以分配任何材质的网格,并且线条渲染器将绘制出时尚的轮廓。 此方法在此处详细介绍。 您可以在Unity Community上使用Triangulator。


完成时间


我们从本文中学到的内容是音乐游戏的基本草图。 是的,到目前为止,这种形式有点难看,但是您可以放心地说:“伙计们,我让机器沿着音轨走了!”。 为了使它成为一个真正的游戏,您需要付出很多努力。 以下是我们可以在此处执行的操作的列表:


  1. 将机器的速度绑定到BPM轨道。 播放器只能控制汽车的倾斜度,而不能控制速度。 然后在整个过程中,音乐会变得更加强烈。
  2. 做一个位检测器,并添加特价。 在节拍下将起作用的效果。 此外,您可以向汽车车身添加动画,动画将随着拍子的跳动而反弹。 这完全取决于您的想象力。
  3. 而不是移动平均线,您需要更熟练地处理轨迹并获取数据数组,以使峰不会消失,但是创建轨迹很容易。
  4. 好吧,当然,您需要使游戏玩法有趣。 您可以在每次击中放置硬币位,添加危险区域等。

我们将在本系列文章的其余部分中研究所有这些以及更多内容。 谢谢大家的阅读!

Source: https://habr.com/ru/post/zh-CN432134/


All Articles