实测数据
这里记录了一些笔者实测的数据,以及一些笔者的猜测。
JSON 读取规则
读取到重复的属性时,后者会被忽略;这与 JavaScript 的
JSON.parse
方法不同。- 参考实测:
{"bpm":120,"bpm":60}
等价于{"bpm":120}
。
- 参考实测:
目前为止,浮点数的精度为
float
而非double
。在
v2.5.0
之前,记录精度与 JavaScript 一致,如2.2
记为2.200000047683716
;在
v2.5.0
及后续版本,长度被尽可能压缩,如29.217391967773439
记为29.217392
。
数字类型支持科学计数法表示;不支持的格式会读取失败,表现为谱面无法打开。
- 参考正则表达式:
/^-?(0|[1-9]\d*)(\.\d+)?([Ee][+-]?\d+)?$/
。
- 参考正则表达式:
所有的属性值都是非必需的;即存在默认值,但不保证其符合规范。
默认值对应类型的空值,如
int
为0
,float
为0.0
,Array
为[]
,Object
为{}
。int
若为其他类型的数值,将被强制取整(非四舍五入),如"3.9Music"
转换为3
。
游戏画面
右上角分数显示规律(基于v2.3.1
)
实时分数大于等于
1000000
时,始终显示为1000000
。否则存在以下实测结果(部分结果使用技术手段得到):
实时分数 右上角显示 7812.5
0007813
-33333.3
0-033332
∞
0-2147483648
只有对实时分数先加 0.5 再取整,才能满足上述实测结果。
以下是笔者对此的 JavaScript 实现:
// 模拟右上角分数显示逻辑
function getScoreText(score) {
score = isFinite((score += 0.5)) ? score | 0 : 1 << 31;
if (score >= 1e6) return '1000000';
return '0' + (score / 1e5).toFixed(5).replace('.', '');
}
音符渲染细节(基于v2.3.1
)
音符
currentFloorPosition
小于-0.001
且未打击时不会被渲染。- 由于浮点误差的存在,该判定极其复杂,难以用自然语言描述。
以下是笔者对此的 JavaScript 实现:
// 获取note最大可见位置,当判定线实时位置超过此值时,note不会被渲染
function getMaxVisiblePos(x) {
const n = Math.fround(x);
if (!isFinite(n)) throw new TypeError('Argument must be a finite number');
const magic = 11718.75;
const prime = n >= magic ? 2 ** Math.floor(1 + Math.log2(n / magic)) : 1;
const a = n / prime + 0.001;
const r = Math.fround(a);
if (r <= a) return r * prime;
const a_ = new Float32Array([a]);
new Uint32Array(a_.buffer)[0] += a_[0] <= 0 ? 1 : -1;
return a_[0] * prime;
}
长度为
0
的 Hold 音符不会被渲染。这包含两种情况:
speed
为0
或holdTime
(取整后)为0
。holdTime
的取整逻辑在负数的表现实测比较奇怪:-0.999
-1
的结果为0
,-1.001
-2
的结果为-1
。
以下是笔者对此的 JavaScript 实现:
// 模拟 holdTime 的取整逻辑
function floor(t) {
t = Math.floor(t);
return t < 0 ? -~t : t;
}
在
v2.0.0
及后续版本中,音符实时垂直距离时变得不可见。 即
speed * currentFloorPosition > 3.3333336
→音符不可见。实测数据:
3.3333336
正常显示,3.3333337
消失。分析:这里的
3.3333336
为浮点数,乘以0.6
刚好等于2
;如果是
3.3333337
则计算结果为2.000000238418579
,略大于2
。
Hold 音符的打击特效间隔似乎与对应判定线
bpm
成反比,且与实际打击时间无关。如
bpm
为30
时打击特效间隔为 1 秒,bpm
为60
时打击特效间隔为 0.5 秒(需要验证)。打击特效触发时刻不随实际打击时间变化,提前/延迟打击不会提前/延迟打击特效。
判定线渲染细节(基于v2.3.1
)
以下像素数据均为 1920x1080 的屏幕实测。
长度:
宽度:
- 实测:
- 实测:
不透明度叠加公式:
颜色值:
#ffffff
、#a2eeff
(FC)、#feffa9
(AP)。
谱面读取速度事件时不会使用
floorPosition
原始值,而是实时计算。第一个事件的
startTime
不为0
时,其floorPosition
计算值为startTime / bpm * 1.875
。相当于在其之前插入一个起止时刻分别为
0
和startTime
,value
为1
的事件。无论其后的事件时间如何,其
floorPosition
均正常计算。
接下来考虑其它事件各种不规范的情况:
事件与上一个事件相离时,该事件在相离的这段时间进行解析延拓;
事件与上一个事件相交时,该事件在相交的这段时间被忽略。相当于:
该事件的
startTime
变为上一个事件的endTime
,endTime
不变;同时保持
(end-start)/(endTime-startTime)
和end
不变,重新计算start
;同时保持
(end2-start2)/(endTime-startTime)
和end2
不变,重新计算start2
。
事件的
startTime
大于或等于endTime
时,该事件被忽略。以下行为会导致谱面停顿或播放异常:
判定线数目超过
100
;任何一条判定线的 bpm 为负数或零;
任何一个事件列表为空数组;
谱面运行时刻超过任何一个事件列表所有事件的结束时刻;
异常的具体表现:音符不再垂直移动,问题所在及其后的判定线动画中断。
谱面性能分析(基于v2.3.1
)
音符或事件数目没有上限,但过多会导致谱面打开延迟,对于音符尤为明显;
实测分析音符时间复杂度为
,事件时间复杂度为 。 音符数目 谱面打开延迟 16384
0.25s
32768
1.50s
65536
6.00s
131072
30.25s
262144
159.75s
音频与进度条动画流程(基于v2.3.1
)
音频大小可能影响谱面的播放状态:
要使谱面正常播放,音频大小至少 4B(否则谱面静止)。
要使谱面正常结束,音频大小至少 180B(否则进度条停在中间)。
笔者将谱面播放过程分为以下几个阶段(以下 offset 均指根结构的
offset
值):播放开始过渡动画,即 UI 从上下两侧淡入,表演判定线从中央扩展。
谱面从头开始播放,进度条从 0% 开始移动。
进度条范围为音乐时长,值为谱面播放进度。
对于音乐:offset 为负时延迟 offset 的绝对值秒播放,否则立即播放。
对于谱面:延迟 offset 的绝对值秒,然后从 (音乐进度 - offset) 处开始播放。
音乐进度到达 (音乐时长 - 0.22049) 秒时,谱面和进度条停止移动,音乐停止播放。
播放结束过渡动画,即 UI 向上下两侧淡出,画面向中间收缩然后结算。
由上可知,进度条位置与谱面进度关联,但结算时刻与音乐进度关联,因此存在以下情况:
offset 为负时,进度条可能会提前到达终点甚至超过终点;
offset 为正时,进度条未到达终点时已经结算。
特别地,offset 大于 (音乐时长 - 0.22049) 将直接结算。
offset 相对于音乐时长很小,且官谱从未出现过负数 offset;
因此以上情况不会影响游戏体验,可以放心游玩。
补充
实测数据(基于v1.6.11
)
SpeedEvent.floorPosition
不会被读取,实际上以float
精度实时计算;- 判定线长度与屏幕高度成正比,比值为
5.76
; Note.positionX
单位为0.05625
个屏幕宽度,值为1
时常用于时钟效果;Note.floorPosition
单位为0.6
个屏幕高度;- 事件时间
等于现实时间(秒)乘以 JudgeLine.bpm
除以1.875
;
实测数据(基于v2.3.1
)
- 感谢 luch4736 提供的 adb 教程;
- 暂停按钮的判定区间
(249.6,-1.0)~(336.0,102.6)
测试设备分辨率:2400x1080
。