跳到正文
zenn · 游戏开发社区· 来源· 空音·· 3 天前AI 评分40

公开了面向 Unity 的自制库 Narimono(鳴物)!!

Unity向けの自作ライブラリ Narimono(鳴物) を公開しました!!

正文 · AI 翻译

引言

你好,我是空音!
我是一名学生,平时使用Unity和Unreal Engine进行游戏开发。

最近我公开了UI库Kumiko,而这次作为第二弹,我在GitHub上公开了面向Unity的音频播放库Narimono(鸣物)的v1.0.0!

Kumiko和Narimono,都是把我平时在游戏制作中反复使用的机制加以改良,并作为库公开出来的产物。

本文先谈谈我公开自制库的举措,然后介绍Narimono能做什么,以及设计时留意的事项!

公开自制库的举措

目前,我正在推进一项工作:把平时就在使用的自制库加以改良,并作为库在 GitHub 上公开。Kumiko 和 Narimono 就是这项工作中公开的第一个和第二个库。

以往,即便做出了方便的机制,也只是按项目复制来使用。开始这项工作的理由主要有以下三点。

  • 为了跨项目使用同一个东西
    如果只是复制来重复使用,某个项目里修复的缺陷或做出的改良就不会反映到其他项目,逐渐变成不同的东西。如果作为包公开,就可以通过 Git URL 把同一个东西引入任何项目,并能用版本进行管理。
  • 为了作为重新审视设计的契机
    只有自己使用的代码,只要自己看得懂就能跑起来。而以便于他人使用为前提,就需要把依赖特定项目的部分切分出来,重新审视名称、注释和文档。Narimono 也为了能通用地使用,从零开始重新审视了设计。
  • 为了获得他人的意见
    通过公开,有可能得到他人告知的自己一个人察觉不到的用法和问题点。

今后我也计划继续把平时使用的自制库依次改良并公开!

Narimono是什么?

Narimono是一个用于处理音效和BGM播放的Unity音频播放库。它原本是把我在自己游戏制作中反复使用的音频播放机制加以改良后的产物。

在Unity中,只要把AudioSource挂到GameObject上,就能轻松地播放声音。但是,随着游戏开发的不断深入,就会出现下面这样的烦恼。

  • 像子弹命中音这类会大量播放的声音,会让同时播放数转眼间就超出上限
  • 当达到上限时停止旧的声音,声音就会“啪”地一下中断
  • 远处播放的3D声音明明听不见,却仍然一直占用着播放槽位
  • 每次切换 BGM,都得每次写一遍淡入淡出的处理

Narimono 就是这样一个把这些常见处理一并承包下来的库。

Narimono 的主要特点

  • 通过资产进行声音管理
    将要播放的声音定义为 ScriptableObject(Cue)。无需管理字符串或 enum 的 ID。
  • 三段式同时播放数的控制
    可以按每个 Cue、每个类别、整体这三个层级设置上限,还能选择达到上限时的行为。
  • 3D 声音按距离进行限制
    听不见的距离的声音不播放,播放过程中远离的声音则淡出并释放。
  • 淡入淡出
    支持对每个 Cue 进行淡入,以及指定秒数的淡出停止。BGM 的交叉淡化也只需通过分类设置即可实现。
  • 计量功能
    可以记录同时播放数的峰值,以及未能播放的次数及其原因。记录仅在 Editor 和 Development Build 中进行,在发布构建中会连同处理一起被移除。
  • 不依赖外部库
    仅靠 Unity 标准功能即可运行。AudioClip 和 AudioRandomContainer 两者都可以播放。

为什么叫「Narimono」这个名字?

Narimono 这个名字来源于歌舞伎的鸣物(なりもの)。

鸣物是歌舞伎下座音乐中由太鼓、笛子等乐器演奏的部分。它在舞台下手的黑御帘(くろみす)中演奏,不仅负责烘托场景的音乐,还承担用大太鼓表现风声、雨声等效果音。

在观众看不到的地方,负责音乐与音效的幕后人员。由于这与本库在游戏背后分别播放音效和 BGM 的作用相重合,因此起了这个名字。

继 Kumiko(组子)之后,这次的名字同样取自日本的传统文化!

关于 Narimono 的设计

用资源来指定声音

在 Narimono 中,要播放的声音通过名为Cue的 ScriptableObject 来定义。

资源 役割
AudioCue 2D 音频播放设置
SpatialAudioCue 3D 音频播放设置(增加距离设置)
AudioCategory 统一管理同时播放数量和混音器组的单位

播放时,只需直接传入 Cue 即可。

using Narimono;
using UnityEngine;

public sealed class PlayerSound : MonoBehaviour
{
    [SerializeField] private AudioSystem _audioSystem;
    [SerializeField] private AudioCue _jumpSound;
    [SerializeField] private SpatialAudioCue _footstepSound;


    public void PlayJump()
    {
        _audioSystem.Play(_jumpSound);
    }


    public void PlayFootstep()
    {
        _audioSystem.Play(_footstepSound, transform.position);
    }
}

最初采用的是用 enum 指定音效的设计,但每次添加音效都需要同时更新 enum 和数据库,还必须检查是否漏注册或重复注册。

改为直接引用资源的形式后,就不会再出现指定不存在的音效这类错误,引用失效也能在 Inspector 中确认了。

此外,由于 2D 和 3D 的 Cue 类型是分开的,若在没有位置信息的情况下播放 3D 的 Cue,就会产生编译错误。我们尽量通过类型来防止那些只有运行后才会发现的错误。

三阶段的同时播放数控制

同时播放数通过以下三个阶段来限制。

段階 设置位置 例
每个 Cue AudioCue的Max Instances(开启Limit Instances) 着弹音最多同时 4 个
每个类别 AudioCategory的Max Playback Count 所有音效合计最多 12 个
全体 AudioSystem的Max Voice Count 整个游戏最多 32 个

达到上限时的行为,可分别在 Cue 和类别中通过 AudioOverflowPolicy 选择。

値 挙動
DiscardNew 丢弃新的声音
StopOldest 停止最旧的声音,播放新的声音
FadeOutOldest 让最旧的声音淡出,播放新的声音

FadeOutOldest是为了避免单纯停止旧声音时会出现「啪」的一下中断的问题而准备的。

不过,正在淡出的声音也会持续占用播放槽位直到播放结束。因此,没有空闲槽位时,不是中途切掉正在淡出的声音,而是丢弃新的声音。因为如果切掉正在淡出的声音,最终还是会「啪」的一下中断。

针对大量播放音效的对策

对于像射击游戏中的着弹音那样大量播放的声音,可通过组合以下设置来应对。

  • Min Interval:设置再次播放同一 Cue 之前的最短间隔,对同一帧中的重复进行抽稀
  • Max Instances:将同一 Cue 的同时播放数限制得较小
  • 类别分离:划分到专用类别中,避免波及 других 重要音效
  • 3D 距离限制:根本不播放远处的着弹音

播放的声音通过句柄操作

Play()会返回指向所播放声音的句柄(AudioPlayback)。停止、暂停、恢复都通过这个句柄进行。

AudioPlayback playback = _audioSystem.Play(_loopCue);

_audioSystem.Pause(playback);
_audioSystem.Resume(playback);

// 1.5秒かけてフェードアウトしてから停止する
_audioSystem.Stop(playback, fadeOutDuration: 1.5f);

由于 Narimono 复用 AudioSource,旧的句柄可能会把同一个 AudioSource 之后播放的另一个声音停掉。

因此,每次播放时都会递增编号,使指向已结束的声音或已被其他声音复用的槽位的句柄会被安全地忽略。关于这一机制,计划在下一篇文章中详细介绍!

淡入淡出与 BGM 切换

在 Cue 中设置 Fade In Duration 后,播放开始时会淡入。

如果预先将BGM用的类别按如下方式设置好,只需Play()新的BGM就能实现交叉淡入淡出。

項目 値
最大播放次数 1
溢出策略 Fade Out Oldest
淡出时长 1〜2秒等
_audioSystem.Play(_titleBgm);

// titleBgmがフェードアウトし、stageBgmがフェードインする
_audioSystem.Play(_stageBgm);

每次切换 BGM 时都不需要编写淡入淡出处理,只需进行设置即可,这一点我很喜欢!

通过计测功能调整上限值

同时播放数的上限,在还不习惯的时候很难判断该设置成多少。

因此,Narimono 加入了用于记录实际播放状况的计测功能。将AudioStatsOverlay放到场景中并分配AudioSystem后,就可以在播放过程中于画面上查看计测结果。

  • 同时播放数的峰值
  • 因上限或 Min Interval 等原因而未能播放的次数
  • 淡出中的声音峰值

由于记录处理添加了Conditional属性,在发布构建中,整个调用会在编译时被移除,因此每次播放时都不会执行记录处理。

通过开发学到的东西

在这次开发中,我们把最初的设计整个重新做了一遍。

最初是用 enum 和数据库来管理声音,但考虑到要通用地重复使用,登记的麻烦和重复检查的维护开始让人觉得负担很重。改为直接引用资源的形式后,必须确认的事项大幅减少,让我再次感受到重新审视设计的重要性。

另外,在淡出的实现中,我体会到解决了一个问题,同样的问题又会以另一种形式出现。即使让旧声音淡出,如果在没有空闲槽位时把正在淡出的声音停掉,声音最终还是会被打断。我感到,不仅要看眼前的现象,还要思考真正想要避免的是什么,这一点很重要。

关于同时播放数的上限,仅仅做到可以设置是不够的,还必须考虑到让用户能够决定合适的值。计量功能正是基于这一想法诞生的功能。

关于今后

今后,我们正在考虑以下改进。

  • 按声音重要度进行打断(重要声音停止重要度较低的声音后播放的机制)
  • 淡入淡出曲线的指定
  • AudioSystem初始化之前被调用时的保护
  • 添加示例场景

此外,我也计划在自己的游戏制作中使用 Narimono,同时持续进行改进。就像 Kumiko 一样,我也希望写一些讲解内部设计的文章!

结语

感谢您读到最后!

继 Kumiko 之后,这已经是我第二次公开自己制作的库了。站在使用者的角度来考虑设计虽然很难,但也正因为如此,我感觉学到了很多。

如果有感想或改进建议,欢迎通过 Zenn 的评论或 GitHub 告诉我!缺陷报告和功能建议请在 GitHub 的 Issues 中提交。

上次发布的 UI 库 Kumiko,也请顺便看看!


Input与Update 将昨天的知识,化为今天的技术

来源:zenn · 游戏开发社区 · zenn.dev