Unity 库开发与游戏开发的不同:Kumiko、Narimono 开发者的体会
ライブラリ開発はゲーム開発と勝手が違うと気づいた話
前言
大家好,我是空音!
我将日常游戏开发中反复使用的机制进行改良,作为面向 Unity 的库在 GitHub 上公开发布。截至目前,已发布了 UI 库Kumiko和音频播放库Narimono。
前几天,我做了统一这两个库以及正在开发中的库的代码风格的工作。起初只是想统一一下写法,但随着工作推进,我强烈地感受到库开发与游戏开发的门道是不一样的。
在这篇文章中,我将把在实现库过程中注意到的事项,作为感想来总结一下!
已发布的库
首先,简单介绍一下我们公开的两个库。
Kumiko(组子)
这是一个面向 Unity(uGUI)的 UI 交互库,支持游戏手柄、鼠标与键盘、触摸操作。
- 根据输入类型,自动切换悬停、选中、按下等显示状态。
- 可以为确认输入附加条件,如双击、冷却时间等。
- 通过方向输入,可将选择移动至上、下、左、右方向的元素。
Narimono(鳴物)
一款面向 Unity 的音频播放库,支持同时播放数量控制、3D 音效和淡入淡出。
- 使用 ScriptableObject 定义要播放的声音,然后直接传入即可播放。
- 可按 Cue 级别、类别级别和全局级别三个层级设置同时播放数量上限。
- 当达到上限时,可以淡出旧的声音,而非突然停止。
统一了代码风格
这次工作的起因是,各库的写法略有不同。因此,我们将规则整理到.editorconfig和.sln.DotSettings(Rider 的设置)中,并应用到所有库上。
花括号、空行、命名规则等格式,只要写在配置文件中,就可以通过 IDE 的格式化功能统一。而另一方面,注释的写法、参数的传递方式、日志的措辞等,是无法通过配置强制执行的。因此,我们也逐一确定了这些规则。
在此过程中,我注意到了许多在游戏开发时未曾意识到的事情。
发现一:注释是给使用者读的
在游戏开发中写的注释,基本上是自己和团队成员与源代码一起阅读的内容。
而库的情况则不同,多数使用者不会打开源代码。IDE 工具提示中显示的文档注释,就是面向使用者的说明书。
因此,在文档注释中书写类型名或成员名时,我们决定使用<see cref>。
/// <summary>
/// 音を停止する。すでに終了している音や無効なハンドルを渡した場合は何もしない。
/// </summary>
/// <param name="playback">停止する音のハンドル。</param>
/// <param name="fadeOutDuration">フェードアウトにかける秒数。0以下の場合は即座に停止する。</param>
/// <remarks>
/// フェードアウトを指定すると、フェードアウトの開始と同時にハンドルが無効になる。
/// そのため、フェードアウト中の音に<see cref="Stop" />・<see cref="Pause" />・<see cref="Resume" />を呼んでも何もしない。
/// 一時停止中の音は、秒数を指定しても即座に停止する。
/// </remarks>
void Stop(AudioPlayback playback, float fadeOutDuration = 0f);
用<see cref>书写的名会在工具提示上变成链接,可以直接跳转到定义。此外,当成员名称变更时,Rider 会自动改写引用,因此注释中也不会残留旧名称。
此外,我们还确定了以下事项。
- 书写内容:只书写该成员自身的角色、前提和行为,不书写其在其他类中的用法
-
书写范围:对 public、internal、protected 成员必须书写,对 private 成员仅在需要时用
//书写 - 文体:句末加上"。",统一用字规范(如"其他的"、"已经"等)
发现2:在游戏开发中,这种程度的注释并不适用
写到此处,我也意识到了相反的一面。在游戏开发过程中,撰写这种程度的注释并不合适。
在游戏开发中,由于规格变更和调整,实现会频繁变化。每次都要修改详细的文档注释工作量很大,而遗漏修改的注释则会作为错误说明残留下来。
我把库与游戏开发之间的差异整理成了一张表格。
| 库 | 游戏开发 | |
|---|---|---|
| 阅读代码的人 | 不打开源码的用户 | 自己或团队成员 |
| 主要说明的位置 | 文档注释·README | 源代码本身 |
| 实现层面的易变性 | 公开的部分不易修改 | 因规格变更而频繁变动 |
| 注释的侧重点 | 用法·前提·行为 | 为什么那样做 |
在游戏开发中,我感觉到注释更适合以“为什么这样实现”为中心来写,而使用方法则通过名称和类型来传达。即使是同样的注释,根据谁会读、会读多长时间,最合适的写法也会改变,这一点我深有体会。
发现3:评论之外也要考虑用户
需要考虑用户的,并不只是评论。
公开级别
在开发中的存档库中,用于读写 JSON 的转换器被设为public。然而,该转换器只在库内部使用,也没有将其传给存档库来使用的方法,并不是设想供使用者使用的。
在库中设为public的内容,会成为用户可以依赖的内容。一旦公开,之后若更改名称或行为,就会破坏用户的代码。因此,仅在内部使用的内容设为internal,并将公开的内容精简到必要的最低限度。
在游戏开发中,即使只是「先随便public」也很少会出问题,但在库中,公开级别本身就是对用户的承诺。
日志
阅读库日志的人,是那些不了解库内部实现的用户。因此,我注意做到了以下几点:
- 能看出原因:写明是哪个资产的哪项设置出了问题
- 知道该怎么做:把修复方法也写清楚
- 传递问题对象:点击日志时,让有问题的资产被高亮显示
- 不要大量输出:对反复出现的警告,只在第一次出现时输出
Debug.LogError($"{cue.GetType().Name}: {cue.name} の {nameof(cue.AudioResource)} が設定されていません。", cue);
此外,由于英文和日文日志混杂在一起,已统一为日文。
引数名
将true、false、null作为参数传递时,已决定必须使用命名参数。
SetValue(_value, notify: true);
EventSystem.current.SetSelectedGameObject(selected: null);
这里注意到的是,当使用命名参数调用时,参数名也会成为公开 API 的一部分。如果更改公开方法的参数名,使用命名参数调用的使用者一侧就会出现编译错误。参数名也必须作为使用者代码中可见的名称慎重决定。
版本
在库中,需要根据变更内容提升版本。
- 补丁版本(1.0.0 → 1.0.1):公开API未发生变化的缺陷修复
- 次要版本(1.0.0 → 1.1.0):保持兼容性前提下的功能新增
- 主要版本(1.0.0 → 2.0.0):影响使用者代码的变更,如删除API或更改命名空间等
此外,在v1.0.0之前的开发版本中,即使是对使用者代码产生影响的变更,通常也会升级次要版本。
通过Git URL引入时,很多使用者通过标签指定版本,如果不发布新标签,这些使用者就无法收到修复。此外,影响使用者代码的变更需要在发布说明中醒目地传达。
心得4:文档与实现不一致的可怕之处
还有一件令人印象深刻的经历。
在存档库中,如果手动编辑的文件中Vector2Int的分量包含小数,我们就将其四舍五入后读入。注释和README中也写了"四舍五入"。
然而,在重新审视README时,我注意到实现和说明存在不一致。
// 引数を指定しないMath.Roundは、.5を偶数側に丸める(銀行型丸め)。
Math.Round(2.5); // 2
Math.Round(3.5); // 4
// 0から遠い側に丸めると、四捨五入になる。
Math.Round(2.5, MidpointRounding.AwayFromZero); // 3
不指定参数时,Math.Round在遇到.5时会向偶数一侧取整,所以并不是四舍五入。顺便一提,Unity的Mathf.Round和Mathf.RoundToInt也是同样的取整方式。
如果是游戏开发,也许注意到问题的人看一下实现就了事。但库的使用者会相信文档来使用。库的文档,对用户而言就是规格本身,我切实地意识到了这一点。
在库开发中开始意识到的事项
通过这次工作,我在开发库时开始更加注意以下几点。
- 文档注释就是说明书:要让用户仅凭工具提示就能理解如何使用
- 公开的内容就是承诺:从决定作为
public的部分、参数名称到日志措辞,都要意识到用户能看到的内容 - 让说明与实现保持一致:在注释或 README 中写下的内容,在实现中必须严格遵守
- 将变更告知用户:通过版本号和发布说明告知用户发生了什么变化
另一方面,在游戏开发中实现容易变动,因此我认为与其写同等程度的注释,不如以「为什么这样做」为中心来写。不过,日志的写法和命名参数的规则等,我觉得在游戏开发中同样直接有用。
结语
感谢您读到最后!
在公开库之前,我曾认为“只要把平时使用的机制原样公开就可以了”。但实际上,一旦以并非自己的其他人为使用前提,从注释到日志、公开级别、版本等,需要考虑的事情一下子增多了。这有着与游戏开发不同的难度,让我受益匪浅。
目前,我们正在开发第三个库——存档库Tansu(箪笥)。等它也能以 v1.0.0 版本发布时,我想再向大家介绍!
如果有感想或改进建议,欢迎通过 Zenn 的评论或 GitHub 告诉我!
InputとUpdate 将昨日的知识,化为今日的技术
来源:zenn · 游戏开发社区 · zenn.dev