补充输入重放
输入重放是指当收到一个服务器状态包时以那个包为初始状态,然后重新计算所有未确认的输入来得到当前帧的状态,然后再对当前角色的位置进行插值移动。服务器状态包只能给出前几帧的权威数据,而当前帧的状态则需要通过输入回放来得到
This commit is contained in:
@@ -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`(或保留作他用)
|
||||
+36
@@ -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
|
||||
+46
@@ -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 开启网络同步:抖动消除或显著减少
|
||||
Reference in New Issue
Block a user