Add interpolation system and integrate into proxy

Introduce a waypoint-based interpolation system and integrate it into the proxy actor controller. Added F4T::InterpolationSystem (header + implementation) with TimePoint, InterpolationComponent, Lerp helpers and Update() logic. Integrates per-slot InterpolationComponent into F4TProxyActorController, switches to kinematic SetPosition-based smoothing (per-frame Lerp with snap thresholds) and removes character-controller velocity injection to prevent drift; also clears waypoints on hard snaps and uses steady_clock for timing. Added a descriptor-based bulk animation write (ApplyDesiredStateToGraphDescriptorBased) in ProxyAnimationSync for efficient graph variable updates with failure logging. Also included testing and debugging docs (phase2-interpolation-testing.md and phase2-interpolation-debugging.md) and flipped some debug flags to false.
This commit is contained in:
2026-06-03 22:46:57 +12:00
parent b128729ebf
commit a11c7d4475
6 changed files with 918 additions and 35 deletions
+242
View File
@@ -0,0 +1,242 @@
# Phase 2 Interpolation Debugging Guide
## Problem: "Still Jerky, Not Smooth Movement"
This guide will help diagnose why interpolation isn't producing smooth movement.
---
## Quick Diagnostics
### 1. **Check If Interpolation is Even Being Called**
**Log signature to find:**
```
[Local Player ID: X] Phase 2 interpolation: remote player Y, actor=ZZZZZZZZ, smooth blending active.
```
**If this log is NOT appearing:**
- Interpolation is not being triggered
- Check: Is `distanceDrift > 0.01F`? (Remote player must be moving)
- Check: Is `a_shouldSnap == false`? (Not a teleport/snap movement)
- Check: Does `a_optSlot` exist? (Must be a runtime proxy, not fallback)
### 2. **Verify Update Rate is Sufficient**
**What's needed for smooth interpolation:**
- Updates must arrive **frequently enough** to queue waypoints
- Too infrequent updates = large gaps between waypoints = large lerps = still jerky
**Check fake client update rate:**
```bash
# In fake_client.py, look for the movement loop
# It should be sending updates at least 10-30 Hz (10-30 per second)
```
---
## Root Causes of Jerkiness
### **Root Cause 1: Not Enough Waypoints**
**Symptom:** Movement is still teleport-like, not smooth
**Reason:** Interpolation needs AT LEAST 2 waypoints
**Diagram:**
```
Bad (jerky):
Frame 1: ╔══ Position A
Frame 2: ║ ╔══ Position B (teleport)
Frame 3: ║ ║ ╔══ Position C (teleport)
Good (smooth):
Frame 1: ╔══════════════════╗ WayPoint 1 (A) queued
Frame 2: ║ Interp between ║ WayPoint 2 (B) queued, lerp A→B at 0.5
║ A and B at 25% ║
Frame 3: ║ Interp between ║ WayPoint 3 (C) queued, lerp B→C at 0.5
║ B and C at 50% ║
```
**Diagnosis:**
- Add extra logging to `ApplyRuntimeProxyTransform()`:
```cpp
LogInfoWithLocalPlayerPrefix(std::format(
"Waypoint queue size: {}, distanceDrift: {:.2f}, interpolating: {}",
a_optSlot->interpolationComponent.TimePoints.size(),
distanceDrift,
interpResult.IsInterpolating));
```
### **Root Cause 2: Update Arrival Rate Too Slow**
**Symptom:** Movement jumps happen, then freeze, then jump again
**Reason:** Remote player updates arriving slower than game frame rate
**Example:**
```
Game FPS: 60 (16ms per frame)
Update Rate: 10 Hz (100ms per update)
Result:
Frame 1-6: (100ms of frames) Interpolate slowly
Frame 7: NEW UPDATE arrives, big jump
Frame 8-13: Interpolate again
```
**Fix:** Increase fake client update frequency
```python
# In fake_client.py, change sleep duration:
time.sleep(0.033) # 30 Hz instead of slower
```
### **Root Cause 3: Alpha Calculation Wrong**
**Symptom:** Movement speed changes unexpectedly, or stalls
**Reason:** Frame counter might not be incrementing correctly
**Check the alpha calculation:**
```cpp
// In InterpolationSystem::Update()
float alpha = static_cast<float>(tickElapsed) / static_cast<float>(tickDelta);
// If tickDelta is 0 or 1, alpha calculation breaks
// tickDelta should be the FRAME DIFFERENCE between waypoints (e.g., 30-60)
```
### **Root Cause 4: Waypoint Queue Pruning Too Aggressive**
**Symptom:** Interpolation logs appear but movement still jerky
**Reason:** Queue size limited to 3, old waypoints popped too early
**Current code:**
```cpp
while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
a_optSlot->interpolationComponent.TimePoints.pop_front();
}
```
**Try increasing to 5-10 waypoints** for more buffer.
---
## Testing Strategy
### **Step 1: Add Detailed Logging**
Edit `F4TProxyActorController.cpp` in `ApplyRuntimeProxyTransform()`:
```cpp
if (a_optSlot && !a_shouldSnap && distanceDrift > 0.01F) {
static uint64_t frameCounter = 0;
frameCounter++;
// ADD THIS DEBUG LOG:
if (frameCounter % 10 == 0) { // Log every 10th frame to avoid spam
LogInfoWithLocalPlayerPrefix(std::format(
"Phase2-DEBUG: frame={}, distanceDrift={:.2f}, queueSize={}, interpolating={}",
frameCounter,
distanceDrift,
a_optSlot->interpolationComponent.TimePoints.size(),
/* will know after Update */
false));
}
// ... rest of code
}
```
### **Step 2: Monitor Log Output**
After recompiling, run fake client and watch log:
```bash
# Terminal: Watch logs in real-time
Get-Content "c:\Users\User\Documents\My Games\Fallout4\F4SE\Fallout4Together.log" -Wait -Tail 50 | Select-String "Phase2-DEBUG"
```
**Expected pattern (good interpolation):**
```
Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=2, interpolating=true
Phase2-DEBUG: frame=20, distanceDrift=298.45, queueSize=2, interpolating=true
Phase2-DEBUG: frame=30, distanceDrift=251.23, queueSize=2, interpolating=true ← Smooth decrease
```
**Bad pattern (not working):**
```
Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=1, interpolating=false
Phase2-DEBUG: frame=20, distanceDrift=1000.00, queueSize=1, interpolating=false ← Jumps!
Phase2-DEBUG: frame=30, distanceDrift=500.00, queueSize=1, interpolating=false
```
### **Step 3: Check Fake Client Update Rate**
Add logging to `fake_client.py`:
```python
last_update_time = time.time()
while True:
# ... send update ...
now = time.time()
update_interval = now - last_update_time
if update_interval > 0.05: # Log if >50ms between updates
print(f"WARNING: Long update interval: {update_interval*1000:.1f}ms")
last_update_time = now
time.sleep(0.033) # ~30 Hz
```
---
## Comprehensive Fix Checklist
- [ ] **Verify waypoints are queuing**: Add frame counter debug log
- [ ] **Confirm queue size > 1**: Should see "queueSize=2" in logs during movement
- [ ] **Check update frequency**: Fake client sending at 20-30 Hz minimum
- [ ] **Verify distanceDrift threshold**: Is `distanceDrift > 0.01F`?
- [ ] **Test with moving fake client**: Stationary player won't trigger interpolation
- [ ] **Increase waypoint buffer**: Try `queueSize > 5` instead of 3
- [ ] **Monitor alpha calculation**: Should go 0.0 → 1.0 as player moves between waypoints
---
## Quick Fix: Increase Update Frequency
**Easiest fix:** Make fake client send updates more frequently
Edit `fake_client.py`:
```python
# Find the main loop:
while True:
# ... code ...
time.sleep(0.033) # Change from 0.1 or higher to 0.033 (30 Hz)
```
Or increase queue size in `ApplyRuntimeProxyTransform()`:
```cpp
// Change from:
while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
// To:
while (a_optSlot->interpolationComponent.TimePoints.size() > 10) {
```
---
## If Still Jerky After All This
The issue might be **architectural**: Maybe F4 doesn't support sub-frame smooth movement, and we need to adjust strategy.
**Next options:**
1. **Reduce frame times via velocity injection**: Make proxies move via physics instead of SetPosition
2. **Use different lerp approach**: Accumulate delta over frames instead of tick-based
3. **Accept some jerkiness**: Design animations to hide it (blending animations during transitions)
---
## Need More Help?
Post the following in your logs:
1. Output of "Phase2-DEBUG" log lines (10 consecutive)
2. Fake client update interval timing
3. FPS you're getting in-game
4. Distance between waypoints in the logs
This will help diagnose the exact cause.
+365
View File
@@ -0,0 +1,365 @@
# Phase 2: Interpolation System Testing Guide
## Overview
Phase 2 tests the waypoint-based smooth movement interpolation system. This replaces direct position application with time-based lerp between waypoints, resulting in smoother remote player movement.
**Prerequisites:**
- F4T plugin built with interpolation system (just completed ✓)
- Fallout 4 running with F4SE
- Network relay server running (`python server.py`)
- Fake client ready (`python server/fake_client.py`)
---
## Architecture Review
Before testing, understand what we're testing:
### Current System (Pre-Phase 2)
```
Remote Player Update
↓
GetRuntimeProxyVisibleTargetPosition()
↓
targetPosition = direct network position
↓
SetPosition(targetPosition) ← IMMEDIATE, no lerp
↓
Result: Jittery/choppy movement on low update rates
```
### New System (Phase 2)
```
Remote Player Update
↓
GetRuntimeProxyVisibleTargetPosition()
↓
AddWaypoint(targetPosition, angleZ, currentTick) → InterpolationComponent
↓
InterpolationSystem::Update(currentTick)
↓
Lerp between waypoints if tick is between them
↓
SetPosition(interpolatedPosition) ← SMOOTH, tick-based
↓
Result: Smooth movement even on low update rates
```
---
## Testing Phase 2.1: Interpolation System Unit Test
### Goal
Verify interpolation math works correctly before testing in-game.
### Test: Manual Interpolation Check
Create a simple test script (`test_interpolation.py`):
```python
# Pseudo-code showing what Phase 2 does
# Position at tick 100: (1000, 2000, 500)
# Position at tick 200: (1100, 2050, 550)
# At tick 150 (halfway):
# alpha = (150 - 100) / (200 - 100) = 0.5
# interpolated_x = 1000 + (1100 - 1000) * 0.5 = 1050
# interpolated_y = 2000 + (2050 - 2000) * 0.5 = 2025
# interpolated_z = 500 + (550 - 500) * 0.5 = 525
# Result: (1050, 2025, 525) ← smooth midpoint
```
**Expected behavior:** Movement is linear blend between waypoints.
---
## Testing Phase 2.2: In-Game Setup
### Setup Step 1: Start Relay Server
```bash
cd f:\Repos\Fallout-4-Together\server
python server.py
```
Expected output:
```
Server listening on 127.0.0.1:7777
Waiting for connections...
```
### Setup Step 2: Start Fake Client
In a new terminal:
```bash
cd f:\Repos\Fallout-4-Together\server
python fake_client.py
```
Expected output:
```
Connecting to server...
Connected successfully
Fake player registered with ID: <player_id>
```
### Setup Step 3: Launch Fallout 4
1. Start Fallout 4 with F4SE
2. Load into a game with open space (avoid dungeons)
3. Check console for F4T startup messages
Expected log output:
```
[Local Player ID: X] F4T Plugin initialized
[Local Player ID: X] Proxy controller ready
```
---
## Testing Phase 2.3: Basic Interpolation Test
### Test: Remote Player Walks in Circle
This verifies waypoint buffering and interpolation.
**Setup:**
1. Have fake client move in a small circle around you
2. Watch the proxy actor move
**Expected Behavior (PRE-Phase 2):**
- Proxy position updates in discrete jumps
- Movement appears jittery/choppy
- Proxy teleports between waypoints
**Expected Behavior (POST-Phase 2):**
- Proxy moves smoothly
- Movement is continuous, not jerky
- Smooth arc instead of sharp jumps
**How to Verify:**
1. Run fake client for 30 seconds with slow movement
2. Observe proxy actor
3. Compare smoothness to pre-Phase 2 behavior
**Log Indicators:**
```
[Local Player ID: 0] First smoothed runtime proxy movement for remote player X: actor=FORMID, alpha=0.15.
```
This log should appear once per remote player.
---
## Testing Phase 2.4: Movement Speed Variations
### Test: Fast vs Slow Movement
**Objective:** Verify interpolation works across movement speeds.
**Test Case 1: Slow Movement**
- Fake client walks slowly (1-2 units/sec)
- Expected: Smooth continuous movement
- Should NOT have position snapping
**Test Case 2: Fast Movement**
- Fake client sprints (100+ units/sec)
- Expected: Fast smooth movement
- No lag or skipping
**Test Case 3: Teleport**
- Fake client teleports 500 units away
- Expected: Proxy snaps (shouldSnap=true for teleport movement type)
- Movement type check happens first, no interpolation
---
## Testing Phase 2.5: Multiple Remote Players
### Test: Interpolation with 2-4 Remote Players
**Setup:**
1. Start 2-4 fake clients
2. Each sends unique movement patterns
3. Observe all proxies
**Expected Behavior:**
- Each proxy interpolates independently
- No interaction/interference between proxies
- Each has its own InterpolationComponent
- Smooth movement for all
**Performance Check:**
1. Monitor FPS with 4 remote players
2. Should be similar to Phase 1 (baseline)
3. Interpolation adds minimal CPU load (just lerp math)
**Command to simulate:**
```bash
# Terminal 1: Start main relay
python server.py
# Terminal 2-5: Start 4 fake clients
python fake_client.py
python fake_client.py
python fake_client.py
python fake_client.py
```
---
## Testing Phase 2.6: Waypoint Queue Management
### Test: Verify Waypoint Cleanup
**Objective:** Ensure old waypoints are cleaned up, not accumulated.
**How to Check:**
1. Run fake client for 2 minutes straight
2. Monitor memory usage
3. Should be stable (not growing)
**What's Happening:**
```
Tick 100: Add waypoint
Tick 101-150: Lerp between waypoint 0 and 1
Tick 151: Pop waypoint 0 (cleanup!)
Tick 151: Add new waypoint
Tick 152-200: Lerp between waypoint 0 and 1 (still 2 max)
```
The InterpolationSystem::Update() automatically pops old waypoints when they're consumed.
---
## Testing Phase 2.7: Rotation Interpolation
### Test: Verify Smooth Rotation
**Objective:** Test angle lerp with wrap-around handling.
**Test Case 1: Normal Rotation**
- Remote player rotates from 0° to 90°
- Expected: Smooth rotation from 0° → 45° → 90°
**Test Case 2: Wrap-Around**
- Remote player rotates from 350° to 10° (crossing 0°)
- Expected: Shortest path rotation (350° → 0° → 10°), not 350° → 180° → 10°
- Our `LerpRotation()` handles this
**Visual Check:**
- Watch proxy heading as it turns
- Should be smooth, not jerky
- Should take shortest angular path
---
## Testing Phase 2.8: Integration with Animation Sync
### Test: Interpolation + Descriptor-Based Animation
**Setup:**
1. Ensure Phase 1 (descriptor-based animation) is working
2. Run Phase 2 interpolation in parallel
3. Fake client walks around you
**Expected Behavior:**
- Remote player position interpolates smoothly
- Remote player animation (Speed, isSprinting) syncs correctly
- No conflicts between systems
**Verification:**
```
[Local Player ID: 0] Runtime proxy animation sync initial for remote player X:
moving=true, sprinting=false, graphSpeed=55.0, direction=0.123
```
This should still appear alongside interpolation.
---
## Testing Checklist
- [ ] **Unit Test**: Lerp math is correct (waypoint positions smooth)
- [ ] **Single Player**: Fake client walks, proxy moves smoothly
- [ ] **Slow Movement**: Proxy glides smoothly at 1-2 units/sec
- [ ] **Fast Movement**: Proxy glides smoothly at 100+ units/sec
- [ ] **Multiple Players**: 2-4 proxies move smoothly independently
- [ ] **Memory**: Stable after 2+ minutes (no memory growth)
- [ ] **Rotation**: Proxy heading rotates smoothly
- [ ] **Wrap-Around**: Rotation handles 0°↔360° correctly
- [ ] **Integration**: Works with Phase 1 animation sync
- [ ] **Snapping**: Teleports still snap (movement type check first)
---
## Debugging: Common Issues & Fixes
### Issue: Proxy Still Jerky After Phase 2
**Cause:** Interpolation not being called
**Fix:**
1. Check that `AddWaypoint()` is being called
2. Verify `Update()` is called before `SetPosition()`
3. Check that `shouldSnap` isn't overriding interpolation
### Issue: Proxy Moves Too Slowly
**Cause:** Alpha blend factor too small
**Fix:**
1. Increase waypoint update rate (more frequent updates = smoother)
2. Check tick calculation (verify currentTick is advancing)
### Issue: Memory Grows Over Time
**Cause:** Waypoints not being popped
**Fix:**
1. Verify `Update()` is cleaning up old waypoints
2. Check that TimePoints.pop_front() is being called
### Issue: Rotation Wraps Incorrectly
**Cause:** LerpRotation() not handling 0°/360° boundary
**Fix:**
- This is handled in our implementation
- If still wrong, check angle units (radians vs degrees)
---
## Next Steps After Testing
1. **If interpolation works:** Move to Phase 3 or other features
2. **If issues found:** Document in dev-log.md and fix
3. **Performance baseline:** Record FPS with 1, 2, 4 remote players
---
## Quick Start Commands
```bash
# Terminal 1: Start relay server
cd f:\Repos\Fallout-4-Together\server
python server.py
# Terminal 2: Start fake client (single remote player)
cd f:\Repos\Fallout-4-Together\server
python fake_client.py
# In-game: Launch F4T and observe proxy movement
```
---
## Expected Logs for Phase 2
When interpolation is active, look for:
```
[Local Player ID: 0] First smoothed runtime proxy movement for remote player 1: actor=0x14003ED8, alpha=0.15.
[Local Player ID: 0] Moved runtime proxy actor 0x14003ED8 from remote player 1 state for the first time: X=1234.56, Y=2345.67, Z=-100.00, AngleZ=1.570, flags1=00000000, flags2=00000000.
```
These confirm the proxy is spawned and moving smoothly.