跳到主要内容

判定系统详解

目录

  1. 判定系统概览
  2. 四种音符类型的判定逻辑
  3. OBB 碰撞检测算法
  4. 判定等级与时间窗口
  5. 延迟补偿机制
  6. 链式判定补救(LOOK/HOLD Chain Rescue)
  7. 判定优先级
  8. JudgeCallback 回调系统
  9. NoteObject 坐标变换

判定系统概览


四种音符类型的判定逻辑

NoteType 参数

类型typezNearzFar判定方式
TAP0025左臂挥动 + 时间差 + OBB
LOOK1025视线方向 + OBB(无需点击)
HOLD2125玩家身体碰撞箱
DODGE3-225玩家身体碰撞箱(碰到=MISS)

TAP 判定(preJudgeTap)

判定时间窗口:

noteTDiff = elapsedMillis - noteHitTimeMillis + playerRTT/2

PERFECT : |noteTDiff| <= 110ms
FAST_GREAT: noteTDiff < -110ms && noteTDiff >= -220ms(提前打)
LATE_GREAT: noteTDiff > 110ms && noteTDiff <= 220ms(延迟打)
MISS : |noteTDiff| > 220ms 或完全未点击

LOOK 判定(preJudgeLook)

LOOK 不需要玩家点击,只需视线指向 Note 所在判定面的 OBB 区域即可触发。


HOLD 判定(preJudgeHold)


DODGE 判定(preJudgeDodge)


OBB 碰撞检测算法

判定面计算(TrackObject.calculateData → JudgeUtils.calculatePlane)

射线-平面求交(JudgeUtils.calculateRayTraceResult)

输入:playerEye(玩家眼睛世界坐标)、plane(ax+by+cz+d=0)
输出:hitPoint(交点的局部坐标 Vector3f)

1. 计算射线方向:direction = playerLookDirection(单位向量)
2. t = -(a*eye.x + b*eye.y + c*eye.z + d) / (a*dir.x + b*dir.y + c*dir.z)
3. worldHit = eye + t * direction
4. 将 worldHit 变换到 Note 的局部坐标系

2D AABB 碰撞(局部坐标)

容差常量:

判定类型常量
TAP / LOOKTOLERATE_TAP_LOOK0.15f
HOLDTOLERATE_HOLD0.15f
DODGETOLERATE_DODGE0.1f

判定等级与时间窗口

判定结果条件分数权重
PERFECT|noteTDiff| <= 110msACC 100%
FAST_GREAT-220ms <= noteTDiff < -110msACC 50%
LATE_GREAT110ms < noteTDiff <= 220msACC 50%
MISS超出范围 / 未点击ACC 0%

ACC 计算公式:

ACC = (totalPerfect * 1.0 + totalGreat * 0.5) / totalNotes * 100%

延迟补偿机制

RTT 数据来源: LagDetectionManager 持续采样最近 10 次 RTT,取平均值作为 playerRTTime

位置补偿(HOLD/DODGE):

补偿距离 = (RTT/2 + deltaTime/2) * trackSpeed

将玩家坐标沿音符运动方向偏移补偿距离后再做碰撞检测

链式判定补救

commitJudgments 中对 LOOK 和 HOLD 类型 Note 有**链式补救(Chain Rescue)**机制:

设计意图: 在视角快速移动的情况下,同一帧内可能有多个 LOOK/HOLD Note 应该被判定成功,链式补救防止因单帧判定面计算顺序导致的误判。


判定优先级

commitJudgments 追踪本 tick 的「最终提交结果」thisTickCommitResult,用于 FeedbackManager 显示:

当同一帧有多个 Note 被判定时,显示优先级最高的结果(即最差的那个)。


JudgeCallback 回调系统

JudgeManager 维护回调列表,判定提交后通知所有注册的回调:


NoteObject 坐标变换

每帧 NoteObject.update() 会先判断当前区间是否满足“定流速 + Track 变换静态”。

  • 满足时:只在区间起点下发一次 ItemDisplay.teleport,让客户端用多 tick 插值补完这段位移;服务端本地逻辑坐标仍按线性插值逐帧更新,保证判定与引导位置连续。
  • 不满足时:继续按原方式逐帧计算并更新显示,覆盖变速段和 Track 动画段。

在需要逐帧计算的位置,依旧执行 Z→Y→X Euler 旋转,将 Note 从本地空间变换到世界坐标:

前帧坐标保存(延迟补偿插值):

prevRealX/Y/Z = realX/Y/Z(上帧)
realX/Y/Z = 本帧计算结果

JudgeUtils 在碰撞检测时可对这两个时间点做线性插值
以精确匹配玩家击打时刻的 Note 实际位置

TrackObject 变换叠加顺序:

Note 世界坐标 = screenCenter
+ Track.xTransformInterpolated * axisX
+ Track.yTransformInterpolated * axisY
+ Track.zTransformInterpolated * axisZ
+ Note.localTransform (旋转后)