引言
因此,您想要或试图创建一个节奏游戏,但是游戏元素和音乐很快就不同步了,现在您不知道该怎么做。 本文将为您提供帮助。 我从高中就开始玩节奏游戏,经常在当地的游戏大厅里和DDR呆在一起。 今天,我一直在寻找这种类型的新游戏,并且诸如
《死灵舞者的地穴》或
Bit.Trip.Runner之类的项目表明,在这种类型中可以做更多的事情。 我在Unity中做了一些节奏游戏的原型工作,结果我花了一个月的时间制作了一个简短的节奏游戏/
Atomic Beats拼图。 在本文中,我将讨论在创建这些游戏时学到的最有用的代码构建技术。 我在其他任何地方都找不到有关它们的信息,或者显示的信息较少。
首先,我必须对Yu Chao的
节奏游戏中的“
音乐同步”一词深表谢意。 Yu回顾了在Unity中与游戏引擎同步音频定时的基础知识,并上传了他的Boots-Cut游戏的源代码,这对我创建项目有很大帮助。 如果您想了解Unity音乐同步的简要介绍,可以研究他的文章,但是我将更详细,更广泛地介绍该主题。 我的代码积极使用文章和Boots-Cut代码中的信息。
时间是任何节奏游戏的核心。 人们对节奏定时中的任何失真都非常敏感,因此将节奏游戏中的所有动作,动作和输入与音乐直接同步非常重要。 不幸的是,传统的Unity时间跟踪方法(如
Time.timeSinceLevelLoad和
Time.time)将很快与正在播放的声音失去同步。 因此,我们将使用
AudioSettings.dspTime直接访问音频系统,该设置使用音频系统处理的真实音频样本数。 因此,它始终与正在播放的音乐保持同步(也许很长的音频文件不会出现这种情况,当播放采样效果时,但是对于正常长度的歌曲,系统应该可以正常工作)。 此功能将是组成时间跟踪的核心,并在此基础上创建主类。
类导体
指挥班是主要的作曲管理班,其余的节奏游戏将以此为基础。 使用它,我们将跟踪合成的位置并管理所有其他同步动作。 要跟踪组成,我们需要一些变量
开始场景时,我们需要执行计算以确定变量,并记录构图的开始时间以供参考。
void Start() {
如果您创建一个空的GameObject并附加了这样的脚本,然后添加带有合成的
音频源并运行程序,您将看到该脚本将记录合成的开始时间,但不会发生任何其他情况。 我们还需要手动输入添加到“音频源”中的音乐的BPM。
由于所有这些值,我们可以在更新游戏时实时跟踪构图中的位置。 我们将确定合成的时间,首先是几秒钟,然后是几分之一。 分数是追踪构图的一种更为方便的方法,因为分数使我们能够与构图并行地(例如,分数1、3和5.5)实时添加动作和计时,而无需计算分数之间的秒数。 将以下计算添加到Conductor类的Update()函数中:
void Update() {
因此,我们得到了根据音频系统的当前时间与乐曲的开始时间之间的差,从而得出了乐曲播放的总秒数。 我们将其保存在songPosition变量中。
请注意,音乐中的乐谱通常以分数为1-2-3-4的单位开始,依此类推,而songPositionInBeats从0开始并从该值开始增加,因此乐曲的第三部分将对应于songPositionInBeats,即2.0,而不是3.0。
此时,如果要创建传统的Dance Dance Revolution风格的游戏,则需要根据需要按的分数创建音符,相对于单击线插入其位置,然后在按下键时记录songPositionInBeats,并且将值与所需比例的音符进行比较。 于超在
文章中讨论了这种方案的一个例子。 为了不重复我自己,我将考虑可以在Conductor类之上构建的其他潜在有用技术。 我在创建
Atomic Beats时使用了它们。
我们适应最初的份额
如果您为节奏游戏创建自己的音乐,则很容易使第一个节拍与音乐的开头完全匹配,如果正确指定,则可以将Conductor类songPositionInBeats可靠地绑定到乐曲。
但是,如果您使用现成的音乐,则很有可能在作曲开始之前稍有停顿。 如果不考虑这一点,那么Conductor类songPositionInBeats将认为第一首节拍是在歌曲开始播放时开始的,而不是现在的节拍。 一切将进一步与份额的价值联系在一起的东西不会与音乐同步。
要解决此问题,您可以添加一个考虑了此偏移量的变量。 将以下内容添加到Conductor类中:
在Update()中,songPosition变量:
songPosition = (float)(AudioSettings.dspTime - dspSongTime);
替换为:
songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset);
现在,songPosition将考虑真正的第一拍,正确计算出歌曲中的位置。 但是,您将必须手动输入第一个节拍的偏移量,因此对于每个文件而言,它都是唯一的。 此外,在此转换期间,将会有一小段窗口,其中songPosition变为负数。 这可能不会影响游戏,但是某些代码(取决于songPosition或songPositionInBeats的值)目前可能无法处理负数。

重复次数
如果您使用从头到尾播放的构图,那么上面显示的Conductor类将足以跟踪位置。 但是,如果您有一段循环的短轨道,并且想要使用此循环,则需要在Conductor中建立Repeater支持。
如果您有一个完美循环的片段(例如,如果歌曲速度为120bpm,并且循环的片段的长度为4节拍,那么它应该恰好是8.0秒,即每股2.0秒)加载到Conductor类的Audio Source类中,然后选中loop框。 指挥将以与以前相同的方式工作,并将总时间转移到剪辑的
第一个开始之后的songPosition中。 为了确定循环的位置,我们需要以某种方式告诉Conductor一个循环中有多少个份额以及已经播放了多少个循环。 将以下变量添加到Conductor类中:
现在,随着SongPositionInBeats的每次更新,我们还可以更新循环的Update()位置。
这给了我们一个标记,该标记告诉loopPositionInBeats我们循环了多少份额,这对于许多其他同步项目很有用。 请记住在GameObject Conductor中输入循环的份额数。
我们还应该仔细考虑份额的计算。 音乐总是从1开始,因此四部分的测量形式为1-2-3-4-,在我们的类中loopPositionInBeats从0.0开始并在4.0上循环。 因此,循环的确切中间位置(在计算音乐比例时将为3)在loopPositionInBeats中的值为2.0。 您可以修改loopPositionInBeats以考虑到这一点,但这会影响所有其他计算,因此在插入注释时要小心。
另外,对于其余工具,将另外两个方面添加到Conductor类中将很有用。 首先,一个名为LoopPositionInAnalog的LoopPositionInBeats模拟版本,可测量循环在0到1.0范围内的位置。 第二个是Conductor类的实例,用于从其他类方便地调用。 将以下变量添加到Conductor类中:
在Awake()函数中,添加:
void Awake() { instance = this; }
并添加到Update()函数:
loopPositionInAnalog = loopPositionInBeats / beatsPerLoop;
同步
使运动或旋转与瓣同步,以使元素位于正确的位置非常有用。 在我的Atomic Beats游戏中,我使用它来绕中心轴动态旋转音符。 最初,根据它们在循环中的份额将它们围绕圆周放置,然后旋转整个演奏区域,以使音符与份额中的下沉线匹配。
为此,请创建一个名为SyncedRotation的新脚本,并将其附加到要旋转的GameObject上。 添加到SyncedRotation脚本的Update()函数:
void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); }
此代码将在0到360度的区间内插值与该游戏绑定的GameObject的旋转,并对其进行旋转,以使其在每个循环结束时完成一整圈。 这作为示例很有用,但是对于循环或逐帧动画,同步循环动画以使其与速度完美契合会更有用。
动画同步
Unity
Animator非常强大,但并不总是准确的。 为了可靠地对齐动画和音乐,我不得不与Animator类竞争,并且其趋势逐渐与节奏不同步。 另外,很难将相同的动画调整为不同的速度,因此在合成之间切换时,您不必将动画的关键帧重新定义为当前速度。 相反,我们可以直接进入动画循环,并根据我们在Conductor类循环中的位置来设置此循环中的位置。
首先,创建一个名为SyncedAnimation的新类,并向其中添加以下变量:
将其附加到要设置动画的新的或现有的GameObject。 在此示例中,我们将简单地在屏幕上来回移动对象,但是相同的原理可以应用于任何动画,无论是在设置属性之前还是在逐帧动画之前。 将一个Animator元素添加到GameObject并创建一个名为SyncedAnimController的新
Animator控制器以及一个名为BackAndForth的
动画剪辑 。 我们将控制器加载到GameObject附带的Animator类中,并将Animation作为默认动画添加到动画树中。
例如,我设置了动画,以使其首先将对象向右移动6个单位,然后向左移动-6个单位,然后再返回0。
现在,要同步动画,请将以下代码添加到SyncedAnimation类的Start()函数中,该函数初始化有关Animator的信息:
void Start() {
然后将以下代码添加到Update()中以设置动画:
void Update() {
因此,我们将动画放置在相对于一个完整循环的确切帧中。 例如,如果您使用上面的动画,则当您处于循环中间时,GameObject位置将刚刚越过0。这可以应用于您创建的任何要与Conductor速度同步的动画。
还值得注意的是,要创建无缝的动画循环,您需要在动画曲线上配置动画的各个
关键帧的切线。 “线性”设置将创建从一个关键帧到下一个关键帧的直线,并且“常量”将使动画保持在一个值中,直到下一个关键帧为止,这将使动作变得急促而尖锐。
尽管此方法很有用,但它会影响动画的所有过渡,因为它会导致
animationState保持最初运行脚本时的状态。 此方法对于只需要无限地使用一个同步动画的对象很有用,但是要创建具有不同同步动画的更复杂的对象,您需要添加代码以处理这些过渡并根据所需的动画状态设置currentState变量。
结论
这些只是对我创建原子节拍有用的方面。 其中一些是从其他来源收集或根据需要创建的,但是我大多数都无法以最终形式找到,所以我希望这能派上用场! 由于CPU或音频系统的限制,也许我的系统的一部分在大型项目中将不再有用,但它将成为玩游戏或爱好项目的良好基础。
创建节奏游戏或与音乐同步的游戏元素可能很困难。 为了使所有内容保持一致的速度,您可能需要一个棘手的代码;使您以恒定的速度进行游戏的结果可能对玩家非常有吸引力。 与传统的Dance Revolution风格的游戏相比,在这种类型中可以完成的工作更多,并且我希望本文可以帮助您实现这样的项目。 如果可能的话,我也建议评估我的
Atomic Beats游戏。 我是在今年春季的一个月内制作的,它有8首短曲,而且是免费的!