补充输入重放

输入重放是指当收到一个服务器状态包时以那个包为初始状态,然后重新计算所有未确认的输入来得到当前帧的状态,然后再对当前角色的位置进行插值移动。服务器状态包只能给出前几帧的权威数据,而当前帧的状态则需要通过输入回放来得到
This commit is contained in:
SepComet
2026-04-08 10:33:42 +08:00
parent da2b93e59c
commit 75289b5690
29 changed files with 850 additions and 573 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-07
@@ -0,0 +1,51 @@
## Context
当前 `MovementComponent` 的 `Reconcile()` 方法在收到服务器 PlayerState 后:
1. 强制 snap 到 server position
2. 重放 pending inputs(可能产生位移)
3. 用 bounded correction 从重放后位置收敛回 server position
问题:server position 每帧都在变化(服务器在广播),bounded correction 的收敛目标一直在变,导致永远追不上。
## Goals / Non-Goals
**Goals:**
- 将模拟层(服务器权威 truth)和表现层(纯视觉插值)解耦
- 表现层用 Lerp 替代 bounded correction,消除追赶震荡
- 模拟层只输出"目标位置",表现层只管插值
**Non-Goals:**
- 不改变网络协议或服务器逻辑
- 不修改远程玩家的插值逻辑
## Decisions
### Decision: 表现层直接设置 rigid.position,不使用 MovePosition
**选择:表现层直接设置 `_rigid.position`**
- Rigidbody interpolation 设为 `None`
- 表现层直接写入 `_rigid.position` 和 `_rigid.rotation`
- 不经过 `Rigidbody.MovePosition`(避免物理引擎介入)
### Decision: 模拟层收到服务器状态时立即计算 target
收到服务器 PlayerState 时:
1. Acknowledge inputs(移除 tick <= AckTick 的输入)
2. 从 authoritative position 重放剩余 pending inputs
3. 计算 `target = authoritativePosition + replayDisplacement`
4. 判断 error:error > SnapThreshold → snap;else → 更新 `_presentationTarget`
**关键**:`_presentationTarget` 在两次收到服务器状态之间保持不变,表现层稳定 Lerp。
### Decision: 插值策略使用 Lerp
固定 alpha = 0.15~0.2。后续可根据 RTT 动态调整或改用 SmoothDamp。
## Risks / Trade-offs
[Risk] 插值延迟导致本地玩家看到的位置比服务器延迟
→ [Mitigation] Lerp 的延迟是固定的(不像 bounded correction 那样持续追赶),且不影响服务器权威性
[Risk] 删除 bounded correction 后无法平滑大误差
→ [Mitigation] Snap 阈值(0.5 unit)处理大误差,直接跳到目标;Lerp 处理小误差自然收敛
@@ -0,0 +1,27 @@
## Why
当前 `MovementComponent` 将模拟层(pending inputs 维护、服务器校正、重放)和表现层(视觉插值、bounded correction)耦合在一起。bounded correction 的收敛目标是实时变化的 server position,导致客户端永远在追赶——每次收到服务器状态,收敛目标就变了。表现为本地移动平滑,加上网络同步后抖动。
## What Changes
- **新增** `LocalPlayerPresentationState` 类型:持有 `_currentPosition/Rotation`(当前显示)和 `_targetPosition/Rotation`(模拟层给的目标)
- **新增** `LocalPlayerSimulationState` 类型:持有 `_lastAuthoritativePosition/Rotation`、`_pendingInputs`、`_presentationTarget`
- **重构** `MovementComponent`:`OnAuthoritativeState` 只更新 `_presentationTarget`,不直接修改 rigid.position
- **重构** 表现层:每帧用 Lerp 将 `_currentPosition` 插值到 `_targetPosition`,再设置 rigid.position
- **移除** `ControlledPlayerCorrection` 相关逻辑:bounded correction 被表现层 Lerp 替代
## Capabilities
### New Capabilities
- `local-player-presentation-state`: 表现层状态(current/target position & rotation)及每帧插值更新
- `local-player-simulation-state`: 模拟层状态(authoritative baseline、pending inputs、presentation target)
### Modified Capabilities
- `local-player-reconciliation`: 模拟层收到服务器状态后计算 PresentationTarget,表现层负责插值收敛,不再使用 bounded correction
## Impact
- 新增 `Assets/Scripts/Network/NetworkApplication/LocalPlayerPresentationState.cs`
- 新增 `Assets/Scripts/Network/NetworkApplication/LocalPlayerSimulationState.cs`
- 修改 `Assets/Scripts/MovementComponent.cs`
- 删除 `Assets/Scripts/ControlledPlayerCorrection.cs`(或保留作他用)
@@ -0,0 +1,36 @@
# local-player-presentation-state Specification
## Purpose
Define how the local player's presentation layer holds current display state and smoothly interpolates toward the simulation layer's target state each frame.
## ADDED Requirements
### Requirement: Presentation layer holds current and target state
The local player's presentation layer SHALL maintain `_currentPosition` and `_currentRotation` (the actively displayed state) separately from `_targetPosition` and `_targetRotation` (the simulation layer's output).
#### Scenario: Presentation state initializes from first simulation target
- **WHEN** the presentation layer is initialized or first receives a simulation target
- **THEN** `_currentPosition` and `_currentRotation` are set equal to the initial target
- **THEN** subsequent updates lerp toward the target
### Requirement: Presentation layer lerps toward target each frame
The presentation layer SHALL each frame interpolate `_currentPosition` and `_currentRotation` toward `_targetPosition` and `_targetRotation` using linear interpolation, then apply the result to the Rigidbody.
#### Scenario: Lerp position and rotation toward target
- **WHEN** the presentation layer updates each frame with interpolation alpha α
- **THEN** `_currentPosition` is updated to `Vector3.Lerp(_currentPosition, _targetPosition, α)`
- **THEN** `_currentRotation` is updated to `Quaternion.Slerp(_currentRotation, _targetRotation, α)`
- **THEN** `_rigid.position` and `_rigid.rotation` are set to `_currentPosition` and `_currentRotation`
### Requirement: Presentation layer snaps when target error exceeds threshold
When the distance between `_currentPosition` and `_targetPosition` exceeds the snap threshold, the presentation layer SHALL immediately snap `_currentPosition` to `_targetPosition` without lerping.
#### Scenario: Snap when error exceeds threshold
- **WHEN** `Vector3.Distance(_currentPosition, _targetPosition) > SnapThreshold`
- **THEN** `_currentPosition` is set equal to `_targetPosition`
- **THEN** `_currentRotation` is set equal to `_targetRotation`
- **THEN** no lerping occurs in this frame
@@ -0,0 +1,46 @@
# local-player-simulation-state Specification
## Purpose
Define how the simulation layer maintains authoritative state, pending inputs, and computes the presentation target when receiving server PlayerState messages.
## ADDED Requirements
### Requirement: Simulation layer maintains authoritative baseline
The simulation layer SHALL maintain `_lastAuthoritativePosition`, `_lastAuthoritativeRotation`, and `_lastAcknowledgedTick` as the authoritative baseline. These are updated when the server acknowledges input through a PlayerState message.
#### Scenario: Authoritative baseline updates on PlayerState
- **WHEN** the client receives a PlayerState with tick T
- **THEN** `_lastAuthoritativePosition` is set to the PlayerState position
- **THEN** `_lastAuthoritativeRotation` is set to the PlayerState rotation
- **THEN** `_lastAcknowledgedTick` is set to T
### Requirement: Simulation layer maintains pending inputs
The simulation layer SHALL maintain a list of pending inputs that have been recorded locally but not yet acknowledged by the server.
#### Scenario: Pending inputs are pruned on acknowledgment
- **WHEN** the client receives a PlayerState with AcknowledgedMoveTick N
- **THEN** all pending inputs with tick <= N are removed from the pending list
- **THEN** remaining pending inputs (tick > N) are preserved for replay
### Requirement: Simulation layer computes presentation target on PlayerState
When receiving a server PlayerState, the simulation layer SHALL compute the presentation target by replaying unacknowledged pending inputs from the authoritative baseline, and update the `_presentationTarget`.
#### Scenario: Presentation target is computed after replay
- **WHEN** the client receives a PlayerState
- **THEN** all acknowledged inputs are pruned (tick <= AcknowledgedMoveTick)
- **THEN** remaining pending inputs are replayed starting from the authoritative position using 50ms fixed-step substeps
- **THEN** `_presentationTarget` is set to (authoritative position + replay displacement, authoritative rotation + replay rotation delta)
### Requirement: Simulation layer updates presentation target only on PlayerState
The simulation layer SHALL only update `_presentationTarget` when a new PlayerState is received. Between PlayerState messages, the presentation target remains constant.
#### Scenario: Presentation target is stable between PlayerState messages
- **WHEN** the client receives a PlayerState and computes `_presentationTarget`
- **AND** no further PlayerState is received in the following frames
- **THEN** `_presentationTarget` remains unchanged
- **THEN** the presentation layer continues lerping toward the same target
@@ -0,0 +1,27 @@
## 1. 准备阶段
- [x] 1.1 阅读 `openspec/specs/local-player-presentation-state/spec.md` 和 `openspec/specs/local-player-simulation-state/spec.md`
- [x] 1.2 阅读现有 `MovementComponent.cs` 和 `ControlledPlayerCorrection.cs`
## 2. 新增表现层状态
- [x] 2.1 添加 `_presentationPosition`、`_presentationRotation`、`_presentationTargetPosition`、`_presentationTargetRotation` 字段到 MovementComponent
- [x] 2.2 实现 `UpdatePresentation()` 方法:Lerp 或 snap 到 target,设置 rigid.position/rotation
## 3. 新增模拟层状态
- [x] 3.1 使用现有的 `_predictionBuffer` 和新增的 authoritative baseline 字段
- [x] 3.2 在 `Reconcile()` 中实现:prune inputs + replay + 计算 target
## 4. 重构 MovementComponent
- [x] 4.1 添加表现层和模拟层状态字段(不使用独立类型,直接在 MovementComponent 中)
- [x] 4.2 `OnAuthoritativeState()` 移除 `ClearPendingInputs()` 和 `_simulationAccumulator = 0f`(移到 Reconcile 后)
- [x] 4.3 `Update()` 中调用 `UpdatePresentation()`
- [x] 4.4 移除 `ControlledPlayerCorrection` 相关逻辑(bounded correction 被 Lerp 替代)
## 5. 验证
- [x] 5.1 编译验证(代码无语法错误)
- [ ] 5.2 关闭网络同步:移动平滑
- [ ] 5.3 开启网络同步:抖动消除或显著减少