
#游戏# #音乐游戏# #Phigros# #判定#
强烈建议用电脑观看,可以在左侧目录直接跳转
白字干货,黄字易懂,绿字结论
建议新手直接跳转到专栏末尾看实际图解
有一定了解可以只看黄字+绿字
喜欢看干货的大佬们可以直接看全文![[脱单doge]](https://i0.hdslb.com/bfs/emote/bf7e00ecab02171f8461ee8cf439c73db9797748.png)
有用的话点个赞吧()
持续施工中(最后更新于2026/8/24 1:00)
更新了late Bad与帧内延迟,并给出了真实图解(翻到最下面) 更新了特殊情况·垂直判定 更新了特殊情况·蹭键
Phigros判定范围分为“时间范围”和“空间范围”
其中,“时间范围”是一个阈值,其具体使用逻辑由后续“判定流程”决定;但是,可以先默认“时间范围为”代表时间范围可取
,单位s,开区间
很多人错以为宽判蓝键时间范围是80/160/180ms,实际如下:
Perfect时间范围:(80ms)
Good时间范围:
(180ms)
Bad时间范围:
(220ms)
perfectTimeRange = 0.08
goodTimeRange = 0.18
badTimeRange = 0.22 很多人错以为严判蓝键时间范围是40/75/140ms,实际如下:
游戏会计算最近最多10帧的平均帧时间,再取其一半作为补偿
Perfect时间范围:
Good时间范围:
Bad时间范围:
如果帧率fps稳定,可以近似为
在稳定60帧时:
Perfect时间范围:(48.3ms)
Good时间范围:
(98.3ms)
Bad时间范围:
(148.3ms)
在稳定120帧时:
Perfect时间范围:(44.2ms)
Good时间范围:
(94.2ms)(可见严判FC比宽判AP简单)
Bad时间范围:
(144.2ms)
# t 是这一帧的时间
# T 用来保存最近最多10帧的时间
# count 初始为0
T[count % 10] = t
count += 1
s = 0
n = min(count, 10)
for i in range(n):
s += T[i]
t_pad = s / n * 0.5
perfectTimeRange = 0.04 + t_pad
goodTimeRange = 0.09 + t_pad
badTimeRange = 0.14 + t_pad 长条头部与蓝键的时间范围一致
长条不会评为Bad*,详情见后文“评分逻辑·长条”
*长条仍可以在early Bad时间内被提前标记,继续按住会被评为Good
黄键的宽判与严判时间范围一致
Drag时间范围*:(100ms)
*黄键的时间范围可以取到100ms(闭区间)
红键的时间范围是蓝键Perfect的1.75倍,即
Flick时间范围:(140ms)
在稳定60帧时:
Flick时间范围:(84.6ms)
在稳定120帧时:
Flick时间范围:(77.3ms)
Phigros的判定流程分三步:触发、匹配、评分
触发:触发某一手势,如点击或划动屏幕
匹配:根据手势的位置,选中并标记某一note
评分:根据时间差给出Perfect/Good/Bad/Miss
每个note的判定范围由其判定流程决定

手指点击屏幕(刚按下)即为有效点击手势
手指按在屏幕上即为有效按住手势
finger.isNewFlick
不同于简单的“点击触发”与“按住触发”,“划动触发”有一套更复杂的逻辑
“划动触发”决定了怎样的划动手势算作有效,每一个有效的划动手势最多可以判定一个红键
其基于手指两帧内的位移,如下:
用和
表示上一帧和这一帧的位移向量
(注:位移向量的单位是world unit,1 world unit = 屏幕高度 / 10)
用
表示这一帧的时间
用
表示屏幕dpi
用
表示速度阈值
游戏会计算在
方向上的投影(若
),也就是

并通过它除以时间,计算相对速度
如果相对速度很大,游戏会认为这是同一次划动,不算作新的划动手势
如果相对速度足够小(),或者stopped为true:
计算这一帧的速度
如果:
触发新的划动手势(标记isNewFlick为true),标记stopped为false
否则:
不触发新的划动手势(标记isNewFlick为false),标记stopped为true
(每个手指都有一个独立的isNewFlick和stopped值,初始值分别为false和true)
所以说,触发新的划动手势大致有两种方法:
两帧内方向变化足够大,使得夹角很大,
足够小,而
足够快
手指先被游戏标记为stopped,再加速使足够快
很精妙的是,反复←→划动几乎一定能算作多个划动手势:划得很快可以触发条件1,划得较慢可以触发条件2
画圈圈则不一定,若圆太大(足够平滑),两帧位移向量之间角度太小,使得相对速度仍然较大(
),游戏会认为这还是同一次划动。这会导致第一个划动手势被触发后,stopped一直为false,后续不会再触发新的划动手势,也就不会再判定新的红键了
想自己试试的可以访问 https://www.desmos.com/calculator/jjeaucerat?lang=zh-CN
简单起见,令上一帧指向正下方,且满足
蓝色区域:
绿色区域:
且

这一帧的位移向量如果落在绿色区域: 可以触发一个新的划动手势
这一帧的位移向量如果落在蓝色区域,但在绿色区域之外:
不会立刻触发新的划动手势,但是会标记stopped为true
下一帧只要向任何方向划得足够快()就能触发一个新的划动手势
低dpi/高帧率下,一次不改变方向的划动可以判到多个红键

当dpi很低/帧率很高时(*),0.1 world unit可能大于绿色区域的边缘
如果上一帧,触发了一个划动手势,但是
,则这一帧会跳过投影计算,直接令
因为
一定成立,
自动成立,变为下图

这一帧的速度只要继续满足,位移向量可以依旧指向正下方,即使落在图中约 (0, -15) 处的绿色区域,也能触发一个新的划动手势
理论上,在这种低dpi/高帧率的特殊情况下,后续每一帧如果保持这个速度(位移向量不变),即使不改变方向,也能一帧触发一个新的划动手势,即可判到一连串红键

*推导:
# d0, d1 是上一帧和这一帧的位移向量,单位是 world unit
# t 是这一帧的时间
# dpi 是屏幕 dpi
u = 0.06 / 380 * dpi
for finger in fingers:
v_rel = 0
if |d0| > 0.1:
v_rel = d0 · d1 / |d0|
v_rel = v_rel / 60 / t
if v_rel < u or finger.stopped:
v = |d1| / 60 / t
if v >= u * 5:
finger.isNewFlick = True
finger.stopped = False
else:
finger.isNewFlick = False
finger.stopped = True CheckNote()
时间范围、
的值来自前文“时间范围”
对于每个点击手势,游戏会在已经按时间排序的note列表中截取一段区间
用表示一个note与现在的时间差
区间的末尾end:最晚的的note
区间的开头start:在end及其之前,最早的的note
(若不存在,则start=end)(“late Bad”的主要原因)
然后,游戏按时间顺序遍历区间内的所有note,并在时间和位置上都接近的note中匹配至多一个note,标记isJudged
用best表示目前找到的最佳note
用表示它与现在的绝对时间差
对于遍历中每一个note:
如果note已经被标记isJudged,则跳过
计算note与现在的时间差
计算手指相对于note所在判定线的坐标(单位:world unit)
计算note与手指在x方向的绝对距离差
如果,则跳过
如果,则跳过
对于点得不准的note,降低Bad范围,令
如果,则跳过
如果目前还没有best,或best是黄键或红键,则用当前note替换best
如果目前的best和note都是蓝键或长条,且二者时间差不超过0.01s:
计算note与手指的加权曼哈顿距离(y轴方向有的权重)
对于best进行相同的计算,并与进行比较
如果当前note的更小,则用当前note替换best
遍历结束后,如果best存在且是蓝键/长条/黄键,则标记它isJudged为true
(游戏在谱面开始前,会先把所有判定线上的note收集起来,并按真实时间顺序排序;“点击匹配”和“红键匹配”都会用到这个列表)
(best和初始值分别为空和10000)
“点击匹配”的空间范围是宽度为±1.9world unit(开区间)的带状区域,也就是“垂直判定”(1 world unit = 屏幕高度 / 10)

时间范围上最早到,最晚到
*(开区间)
所以理论上只存在early Bad,不存在late Bad*
*“late Bad”原因详情见后文“判定范围·特殊情况·late Bad”
对于点得不准的note,Bad范围会缩减,如下图 所以点击垂直判定的边缘可以降低Bad的可能性

按顺序遍历note时:
跳过已被标记isJudged的note
如果当前note还在early侧,并且它离当前时间比best还远至少0.01s,则不会替换best(“黄键保护”的原因)
如果没有达成以上条件,需要根据以下替换规则决定当前note是否替换best
黄键/红键best 可以替换为 蓝键/长条/黄键/红键note
蓝键/长条best 不能替换为 黄键/红键note
蓝键/长条best 在特定情况下可以替换为 蓝键/长条note
也就是说,时间差小于0.01s的两个键匹配优先级:蓝键/长条 > 黄键/红键

特定情况:当前note与best时间差不超过0.01s,且当前note与手指的加权曼哈顿距离()更小
也就是说,时间差大于0.01s的两个蓝键/长条,时间更早的优先级更高
时间差小于0.01s的两个蓝键/长条,距离更短的优先级更高
时间差正好等于0.01s的两个蓝键/长条,在过线之前时间更早的优先级更高(”保护“),过线之后距离更短的优先级更高

蓝色区域表示下面蓝键的加权距离更小,白色区域表示右面蓝键的加权距离更小;因此点击相应区域时,会优先匹配相应的蓝键
当然,双指同时点击交叉部分,两个肯定都能被匹配
想自己试试的可以访问 https://www.desmos.com/calculator/igniatrsg2?lang=zh-CN(并开启反色)
附加一个类似Voronoi Diagram的动图方便理解:

遍历结束后,最终选中的best(非红键)会被标记isJudged为true
对于点击,“垂直判定”区域的宽正比于屏幕高度(注意是屏幕高度,不是屏幕宽度),而note大小却正比于屏幕宽度
所以对于不同设备,“垂直判定”的宽度体感不同:
手机:note大、“垂直判定”窄
平板:note小、“垂直判定”宽


这也就是为什么白复生AT和RadianceAT的四长条段,手机更容易断


当黄键提前蓝键至少0.01s,就有可能触发“黄键保护”,导致点击会被算在黄键,而非蓝键上;如果时间差不到0.01s,会直接点到蓝键
“黄键保护”触发条件:
蓝键在early侧
黄键比蓝键更靠近当前时间至少0.01s
“黄键保护”后,这个黄键就会被标记isJudged;第二次点击时,会因为存在isJudged标记被跳过,导致“黄键保护”只能触发一次

如果黄键提前蓝键50ms,需要等到黄键过了判定线30ms才能直接点到蓝键;因为30ms-20ms正好是0.01s的边界情况,过了边界情况就没有“保护”了
“红键保护”与“黄键保护”的触发条件一致
因为红键不会被“点击匹配”标记isJudged,“红键保护”可以触发若干次,且点击不能真正判定到红键,导致红键+蓝键的键型会很难打
在宽判下,因为Bad区间范围在垂直判定的最边缘(±1.9world unit)刚好能缩减为0(即),所以可以在靠近边缘位置快速连点,而不会Bad
在课题模式下,Bad区间范围则不会缩减为0,所以同样的连点不能避免Bad

对于两个几乎同时的蓝键/长条,空间范围由加权曼哈顿距离决定;所以表面上更靠近一个note的点击,也可能会匹配另一个note

附加一个类似Voronoi Diagram的动图方便理解:

“蹭键”指本来想点一个note却点到了另一个note的情况
即使是在同一帧内的两个点击,体感上几乎没区别,也存在先后处理顺序
--这个顺序是很多蹭键的关键原因,后面要用到--
操作:在红圈同时点击
预期结果:判定三个蓝键
实际结果:有概率Miss下方的蓝键

右上方的点击不影响蹭键,可以忽略,不参与分类讨论
若处理顺序为:右侧点击 -> 左侧点击(不会蹭键):
右侧的点击匹配到右侧的蓝键
左侧的点击匹配到下方的蓝键
若处理顺序为:左侧点击 -> 右侧点击(会蹭键):
因为左侧的点击到右侧的蓝键的加权曼哈顿距离更短,匹配到右侧的蓝键
因为右侧的蓝键已经被匹配,右侧的点击匹配到空

操作:在红圈同时点击并划动
预期结果:判定蓝键与红键
实际结果:有概率蹭到左上方的蓝键(Bad)

若处理顺序为:左侧点击 -> 右侧点击(不会蹭键):
左侧的点击匹配到蓝键
右侧的点击匹配到空,划动时再匹配到红键
若处理顺序为:右侧点击 -> 左侧点击(会蹭键):
对于同时的蓝键和红键,“点击匹配”会优先匹配蓝键
右侧的点击在蓝键的“垂直判定”内,匹配到蓝键
左侧的点击因为下方蓝键已经被匹配,所以匹配到它左上方的蓝键

需要注意的是,如果右侧的红键换成蓝键,相同的操作则不会蹭键,因为加权曼哈顿距离一定会让两个点击与两个蓝键一一对应
操作:在红圈同时点击
预期结果:判定两个蓝键
实际结果:有概率右侧的蓝键不被判定,并蹭到第一个红圈上方的蓝键(Bad)

若处理顺序为:左侧点击 -> 右侧点击(不会蹭键):
左侧的点击匹配到左侧的蓝键
右侧的点击匹配到右侧的蓝键
若处理顺序为:右侧点击 -> 左侧点击(会蹭键):
时间差大于0.01s的两个蓝键,时间更早的优先级更高
右侧的点击在左侧蓝键的“垂直判定”内,匹配到左侧蓝键
左侧的点击因为左侧蓝键已经被匹配,所以匹配到上方的蓝键

notes = []
for judgeLine in judgeLines:
for note in judgeLine.notes:
note.realTime = note.time * 1.875 / judgeLine.bpm
notes.append(note)
chartNoteSortByTime = sorted(notes, key=note.realTime)
def CheckNote(finger):
best = None
bestAbsDt = 10000
end = -1
while end + 1 < len(chartNoteSortByTime):
note = chartNoteSortByTime[end + 1]
if note.realTime >= nowTime + badTimeRange:
break
end += 1
start = end
while start > 0:
note = chartNoteSortByTime[start - 1]
if note.realTime <= nowTime - goodTimeRange:
break
start -= 1
for i in range(start, end + 1):
note = chartNoteSortByTime[i]
if note.isJudged:
continue
dt = note.realTime - nowTime
x = relativeX(finger, note.line)
y = relativeY(finger, note.line)
dx = abs(note.positionX - x)
if dx >= 1.9:
continue
if dt >= bestAbsDt + 0.01:
continue
bad_limit = badTimeRange
if dx > 0.9:
bad_limit = badTimeRange - (dx - 0.9) * perfectTimeRange * 0.5
if dt > bad_limit:
continue
if best is not None:
if best.type not in [DRAG, FLICK]:
if note.type not in [TAP, HOLD]:
continue
if abs(best.realTime - note.realTime) > 0.01:
continue
bestX = relativeX(finger, best.line)
bestY = relativeY(finger, best.line)
noteMetric = abs(note.positionX - x) + abs(y / 2.2)
bestMetric = abs(best.positionX - bestX) + abs(bestY / 2.2)
if noteMetric >= bestMetric:
continue
best = note
bestAbsDt = abs(dt)
if best is None:
return
if best.type == FLICK:
return best
if best.type in [TAP, HOLD, DRAG]:
best.isJudged = True
return best CheckFlick()
时间范围的值来自前文“时间范围”
对于每个有效的划动手势(即带有isNewFlick标记的手指,由“划动触发”决定),游戏会在已经按时间排序的note列表中截取一段区间
用表示一个note与现在的时间差
区间的末尾end:最晚的的note
区间的开头start:在end及其之前,最早的的note
(若不存在,则start=end)(“late Bad”的主要原因)
然后,游戏按时间顺序遍历区间内的所有Flick,并在时间和位置上都接近的Flick中匹配至多一个Flick,标记isJudgedForFlick
用best表示目前找到的最佳Flick
用表示它与现在的绝对时间差
对于遍历中每一个Flick:
如果Flick已经被标记isJudgedForFlick,则跳过
计算Flick与现在的时间差
如果,则跳过
计算手指相对于Flick所在判定线的坐标(单位:world unit)
计算Flick与手指在x方向的绝对距离差
如果,则跳过
如果目前还没有best,则用当前Flick替换best
如果目前已经有best,且当前Flick与best的时间差不超过0.01s:
计算Flick与手指的加权曼哈顿距离(y轴方向有的权重)
对于best进行相同的计算,并与进行比较
如果当前Flick的更小,则用当前Flick替换best
遍历结束后,如果best存在,则标记它isJudgedForFlick为true,并令当前手指的isNewFlick为false
(游戏在谱面开始前,会先把所有判定线上的note收集起来,并按真实时间顺序排序;“点击匹配”和“红键匹配”都会用到这个列表)
(best和初始值分别为空和10000)
“红键匹配”与“点击匹配”很类似,但是只匹配Flick
“红键匹配”使用更宽的“垂直判定”,宽度为±2.1world unit(开区间)

时间范围上最早到,最晚到
*(开区间)
*红键也存在类似“late Bad”的情况,原因详情见后文“判定范围·特殊情况·late Bad”
没有Bad范围缩减

对于多个可匹配的红键,“红键匹配”与“点击匹配”类似(详情见“点击匹配”)
如果“红键匹配”选中了红键,会清除手指上来自“划动触发”的isNewFlick标记,所以一个划动手势只能匹配一个红键
划动手势需要手指有一帧位移终点在“垂直判定”范围内,才会匹配到红键

即“红键匹配”只看每一帧的终点,不看路径;所以,一帧内从“垂直判定”范围内划出去,落点在外面,是判不到红键的(这种情况帧数很低会更明显)
正在施工中……(手指的速度还不够就划出“垂直判定”范围,划不到红键)
notes = []
for judgeLine in judgeLines:
for note in judgeLine.notes:
note.realTime = note.time * 1.875 / judgeLine.bpm
notes.append(note)
chartNoteSortByTime = sorted(notes, key=note.realTime)
def CheckFlick(finger):
best = None
bestAbsDt = 10000
end = -1
while end + 1 < len(chartNoteSortByTime):
note = chartNoteSortByTime[end + 1]
if note.realTime >= nowTime + 1.75 * perfectTimeRange:
break
end += 1
start = end
while start > 0:
note = chartNoteSortByTime[start - 1]
if note.realTime <= nowTime - 1.75 * perfectTimeRange:
break
start -= 1
for i in range(start, end + 1):
note = chartNoteSortByTime[i]
if note.type != FLICK:
continue
if note.isJudgedForFlick:
continue
dt = note.realTime - nowTime
if dt >= bestAbsDt + 0.01:
continue
x = relativeX(finger, note.line)
y = relativeY(finger, note.line)
dx = abs(note.positionX - x)
if dx >= 2.1:
continue
if best is not None:
if abs(best.realTime - note.realTime) > 0.01:
continue
bestX = relativeX(finger, best.line)
bestY = relativeY(finger, best.line)
noteMetric = abs(note.positionX - x) + abs(y / 2.2)
bestMetric = abs(best.positionX - bestX) + abs(bestY / 2.2)
if noteMetric >= bestMetric:
continue
best = note
bestAbsDt = abs(dt)
if best is None:
return
best.isJudgedForFlick = True
finger.isNewFlick = False 游戏在谱面开始前,会给每个note创建一个相应类型的Control,用于控制note的移动和评分:
蓝键 Tap -> ClickControl 长条 Hold -> HoldControl 黄键 Drag -> DragControl 红键 Flick -> FlickControl
每一帧,游戏会遍历每个Control并调用相应的Judge()
Judge()会根据以下状态:
isJudged标记(来自“点击匹配”)
isJudgedForFlick(来自“红键匹配”)
并结合note与现在的时间差所在时间范围,决定是否给出评分
时间范围、
、
、
、
的值来自前文“时间范围”
后文中出现的、
、
、
都代表评分结束,只有评分结束才会增加或清零Combo,并增加相应的分数(按住的长条要快到长条尾部,已经匹配的黄/红键要快到判定线,评分才会结束)
(Control处理完会返回true,并被从遍历列表中删除)
ClickControl.Judge()
蓝键有Perfect/Good/Bad/Miss 时间范围受严判影响
若没有isJudged标记:
若有isJudged标记:
class ClickControl:
def __init__(self, note):
self.note = note
self.isJudged = False
def Judge(self):
dt = self.note.realTime - nowTime
self.isJudged = self.isJudged or self.note.isJudged
if not self.isJudged:
if dt < -goodTimeRange:
Miss(self.note)
return True
return False
if abs(dt) < perfectTimeRange:
Perfect(self.note)
elif abs(dt) < goodTimeRange:
Good(self.note)
else:
Bad(self.note)
return True HoldControl.Judge()
“长条头部被击打”指长条头部标记为Perfect/Good(与isJudged标记不同);“长条结束”指长条获得了Perfect/Good/Miss评分
长条有Perfect/Good/Miss,没有Bad 头部时间范围受严判影响,尾部不受其影响
长条初始有2层“抬手保护”:
若长条头部未被击打且未Miss:
若没有isJudged标记:
若有isJudged标记:
若长条头部已被击打且未结束:
若“垂直判定”区域内存在手指:
重置“抬手保护”为2层,
若“垂直判定”区域内不存在手指:
若当前时间进入长条尾部前0.22s内(220ms):
若长条头部已被击打且未结束:
若当前时间超过长条尾部后0.25s(250ms):
若长条头部未被击打且未结束:
由于长条没有Bad,在early Bad时间范围点击并按住长条,会等到Good时间范围再判定头部为Good,并显示Good打击特效
短暂将手指离开长条再按住不会Miss
2层“抬手保护”实际上最多允许手指离开长条3帧
在稳定60帧时:手指可以离开长条0.05s(50ms)
在稳定120帧时:手指可以离开长条0.025s(25ms)
被击打的长条会在时间进入长条尾部前0.22s(220ms)内结算(不受严判影响),所以长条可以提前一点松手,足够短的长条甚至可以只点击一下
如果长条有了isJudged标记,然而处理时已经(“late Bad”),这时长条头部未被击打也未Miss,只有在时间超过长条尾部后0.25s(250ms)才会判为Miss
这个状态下长条不会变暗(未Miss)也不会显示打击特效(头部未被击打)
“点击匹配”通常只会匹配的note;上述情况的出现源于特殊情况,详情见后文“判定范围·特殊情况·late Bad”
class HoldControl:
def __init__(self, note):
self.note = note
self.isJudged = False
self.missed = False
self.judged = False # 头部是否被击打
self.judgeOver = False # 长条判定是否结束
self.isPerfect = False # 头部击打是否为 Perfect
self._safeFrame = 2 # “抬手保护”
def Judge(self):
dt = self.note.realTime - nowTime
tailTime = self.note.realTime + self.note.holdTime
# 长条头部
if not self.judged:
if not self.missed:
self.isJudged = self.isJudged or self.note.isJudged
if not self.isJudged:
if dt < -goodTimeRange:
Miss(self.note)
self.missed = True
else:
if abs(dt) < perfectTimeRange:
self.judged = True
self.isPerfect = True
elif abs(dt) < goodTimeRange:
self.judged = True
self.isPerfect = False
# 长条中部
if self.judged and not self.missed and not self.judgeOver:
self.missed = True
for finger in fingers:
x = relativeX(finger, self.note.line)
if abs(x - self.note.positionX) < 1.9:
self.missed = False
self._safeFrame = 2
break
if self.missed:
if self._safeFrame < 0:
Miss(self.note)
self.judgeOver = True
else:
self._safeFrame -= 1
self.missed = False
# 长条尾部前
if nowTime > tailTime - 0.22:
if self.judged and not self.missed and not self.judgeOver:
if self.isPerfect:
Perfect(self.note)
else:
Good(self.note)
self.judgeOver = True
# 长条尾部后
if nowTime > tailTime + 0.25:
if not self.judged and not self.missed and not self.judgeOver:
Miss(self.note)
return True
return False DragControl.Judge()
这部分包括了“判定流程”中“黄键匹配”与“黄键评分”两个步骤
这部分的isJudgedForDrag是为了方便理解起的名字,对应源码内的DragControl.isJudged,与“点击匹配”的isJudged标记不同
对于每个Drag,游戏会检查附近是否存在按住的手指;若存在,标记isJudgedForDrag(即“黄键匹配”)
黄键只有Perfect/Miss,没有Good/Bad 时间范围不受严判影响
若没有isJudgedForDrag标记且:
计算手指相对于Drag所在判定线的x坐标(单位:world unit)
计算Drag与手指在x方向的绝对距离差
如果,则标记isJudgedForDrag为true
若没有isJudgedForDrag标记:
若有isJudgedForDrag标记:
“黄键匹配”使用更宽的“垂直判定”,宽度为±2.1world unit(开区间)

时间范围上最早到,最晚到
(比较特殊,为闭区间)

一根按住的手指可以通过“黄键匹配”标记任意多个黄键
“黄键评分”只看“黄键匹配”的isJudgedForDrag标记,不看“点击匹配”的isJudged标记
即使很早就标记到了黄键,也要在靠近/过了判定线时才会评分
黄键在宽判下比蓝键Miss得更早
class DragControl:
def __init__(self, note):
self.note = note
self.isJudged = False
def Judge(self):
dt = self.note.realTime - nowTime
# 注意这里并没有使用来自“点击匹配”的 self.note.isJudged 更新 self.isJudged
if abs(dt) <= 0.1 and not self.isJudged:
for finger in fingers:
x = relativeX(finger, self.note.line)
if abs(x - self.note.positionX) < 2.1:
self.isJudged = True
if not self.isJudged:
if dt < -0.1:
Miss(self.note)
self.note.isJudged = True
return True
if self.isJudged:
if dt < 0.005:
Perfect(self.note)
self.note.isJudged = True
return True
return False FlickControl.Judge()
红键只有Perfect/Miss,没有Good/Bad 时间范围与蓝键时间范围成比例,受严判影响
若没有isJudgedForFlick标记:
若有isJudgedForFlick标记:
“红键评分”只看“红键匹配”的isJudgedForFlick标记,不看“点击匹配”的isJudged标记
即使很早就标记到了红键,也要在靠近/过了判定线时才会评分
红键在宽判和严判下,都比蓝键Miss得更早
class FlickControl:
def __init__(self, note):
self.note = note
self.isJudged = False
def Judge(self):
dt = self.note.realTime - nowTime
self.isJudged = self.isJudged or self.note.isJudgedForFlick
if not self.isJudged:
if dt < -1.75 * perfectTimeRange:
Miss(self.note)
self.note.isJudged = True
return True
if self.isJudged:
if dt < 0.005:
Perfect(self.note)
self.note.isJudged = True
self.note.isJudgedForFlick = True
return True
return False 结合“判定流程”中的每一个步骤,可以得出理论上的总判定范围

以下分别为宽判与严判的判定范围,横轴为x距离,纵轴为时间差Δt
黄色区域:Perfect 蓝色区域:Good 红色区域:Bad
虚线表示开区间,实线表示闭区间



以下现象均称为late Bad:
蓝键可以在late Good()范围过后一帧内被匹配,并评为Bad
长条可以在late Good()范围过后一帧内被匹配,并“延迟Miss”(见“评分逻辑·长条·特殊情况·延迟Miss”)
红键可以在late Perfect()范围过后一帧内被匹配,并评为Perfect
late Bad的原因结合了匹配和评分两部分代码的bug
在“点击匹配”和“红键匹配”中,会先在已经按时间排序的note列表中截取一段区间,并在这个区间内匹配至多一个note
就拿“点击匹配”为例:(建议大佬们去“点击匹配·伪代码”,看一眼就懂了)
区间的末尾end:最晚的
的note
区间的开头start:在end及其之前,最早的
的note (若不存在,则start=end)
代码上的逻辑确实可以保证区间的末尾在之前,但是不能保证在
之后
如果内不存在note,且
之前存在note,区间的末尾就会选中一个
之前的note;因为这个note之前不存在满足
的note,所以区间的开头也会选中同一个note,即区间内有且仅有这一个note(甚至已经Miss掉的也能选中,所以只要前面有note,这个区间就不为空)
所以late Bad一般只存在在比较稀疏的谱面,因为内必须没有note
这个过晚的note被算进区间内后,只要没有isJudged标记,且点击位置在“垂直判定”内,它就会被成功匹配,标记isJudged,并交给评分逻辑进行评分
“红键匹配”有同样的bug,原理几乎一致,就不多赘述了
被匹配的蓝键、长条、和红键会被各自的评分逻辑评分
理论上,评分逻辑会让蓝键和长条过了late Good()就Miss、红键过了late Perfect(
)就Miss;但是一帧内的执行顺序是先匹配、再评分,导致刚过一帧的note被匹配时还没来得及评为Miss;再到评分逻辑处理时,因为有isJudged标记不走Miss分支,于是蓝键评为Bad、长条进入“延迟Miss”、红键评为Perfect
对于蓝键:因为,被评为Bad
对于长条:因为,长条头部视为未被击打,一直按住可以暂时不Miss(不变暗、不断连);但是到了长条尾部后0.25s,就会被评为Miss
对于红键:因为,被评为Perfect
因为游戏在每一帧的Update中统一处理触发、匹配、与评分,所以一次点击被处理的时间取决于它发生在一帧内的哪个位置:
如果刚好在Update前:几乎立即处理
如果刚好在Update后:需要等到下一帧才能处理
因此被处理的时间可能比点击时间晚0~一帧时间,可以近似均匀分布
在稳定60帧时:
帧内延迟:(16.7ms),所以帧数低会因为延迟很难受
在稳定120帧时:
帧内延迟:(8.3ms)
这个延迟会让点击结果整体偏晚:可能让early Good变成Perfect,也可能让late Perfect变成Good
结合“late Bad”和“帧内延迟”,可以得出更贴近实际的判定范围
横轴为x距离,纵轴为时间差Δt
黄色区域:Perfect 蓝色区域:Good 红色区域:Bad
在蓝键的late方向增加了一帧的Bad
在长条的late方向增加了一帧的Miss(虽然完全看不出来)
在红键的late方向增加了一帧的Perfect
通过颜色的渐变表示Perfect/Good/Bad/Miss的概率



