判定系统详解
目录
- 判定系统概览
- 四种音符类型的判定逻辑
- OBB 碰撞检测算法
- 判定等级与时间窗口
- 延迟补偿机制
- 链式判定补救(LOOK/HOLD Chain Rescue)
- 判定优先级
- JudgeCallback 回调系统
- NoteObject 坐标变换
判定系统概览
四种音符类型的判定逻辑
NoteType 参数
| 类型 | type | zNear | zFar | 判定方式 |
|---|---|---|---|---|
| TAP | 0 | 0 | 25 | 左臂挥动 + 时间差 + OBB |
| LOOK | 1 | 0 | 25 | 视线方向 + OBB(无需点击) |
| HOLD | 2 | 1 | 25 | 玩家身体碰撞箱 |
| DODGE | 3 | -2 | 25 | 玩家身体碰撞箱(碰到=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 / LOOK | TOLERATE_TAP_LOOK | 0.15f |
| HOLD | TOLERATE_HOLD | 0.15f |
| DODGE | TOLERATE_DODGE | 0.1f |
判定等级与时间窗口
| 判定结果 | 条件 | 分数权重 |
|---|---|---|
PERFECT | |noteTDiff| <= 110ms | ACC 100% |
FAST_GREAT | -220ms <= noteTDiff < -110ms | ACC 50% |
LATE_GREAT | 110ms < noteTDiff <= 220ms | ACC 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 (旋转后)