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:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <deque>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "RE/N/NiPoint3.h"
|
||||||
|
|
||||||
|
namespace F4T::InterpolationSystem
|
||||||
|
{
|
||||||
|
// TimePoint: Represents a position/rotation at a specific game tick
|
||||||
|
struct TimePoint
|
||||||
|
{
|
||||||
|
RE::NiPoint3 Position{};
|
||||||
|
float Rotation{}; // Z-axis rotation (angleZ)
|
||||||
|
std::chrono::steady_clock::time_point Timestamp{};
|
||||||
|
uint64_t Tick{ 0 };
|
||||||
|
|
||||||
|
TimePoint() = default;
|
||||||
|
TimePoint(const RE::NiPoint3& a_pos, float a_rot, uint64_t a_tick)
|
||||||
|
: Position(a_pos), Rotation(a_rot), Tick(a_tick), Timestamp(std::chrono::steady_clock::now())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// InterpolationComponent: Manages smooth movement between waypoints
|
||||||
|
struct InterpolationComponent
|
||||||
|
{
|
||||||
|
// Deque maintains 2 waypoints for lerp: current and next
|
||||||
|
std::deque<TimePoint> TimePoints;
|
||||||
|
|
||||||
|
// Last interpolated position (for delta checks)
|
||||||
|
RE::NiPoint3 LastInterpolatedPosition{};
|
||||||
|
|
||||||
|
// Add a new waypoint to the queue
|
||||||
|
void AddWaypoint(const TimePoint& a_point) noexcept
|
||||||
|
{
|
||||||
|
TimePoints.push_back(a_point);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current number of waypoints
|
||||||
|
std::size_t GetWaypointCount() const noexcept
|
||||||
|
{
|
||||||
|
return TimePoints.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all waypoints
|
||||||
|
void Clear() noexcept
|
||||||
|
{
|
||||||
|
TimePoints.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have enough waypoints for interpolation (need at least 2)
|
||||||
|
bool HasEnoughWaypoints() const noexcept
|
||||||
|
{
|
||||||
|
return TimePoints.size() >= 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update interpolation for an actor
|
||||||
|
// Returns the interpolated position if interpolation occurred, nullopt otherwise
|
||||||
|
struct InterpolationResult
|
||||||
|
{
|
||||||
|
RE::NiPoint3 Position{};
|
||||||
|
float Rotation{};
|
||||||
|
bool IsInterpolating{ false };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the interpolation for a remote player
|
||||||
|
// a_currentTime: Current game time (in milliseconds or ticks)
|
||||||
|
// Returns interpolated position/rotation if movement is happening
|
||||||
|
InterpolationResult Update(
|
||||||
|
InterpolationComponent& a_component,
|
||||||
|
uint64_t a_currentTick) noexcept;
|
||||||
|
|
||||||
|
// Helper: Linear interpolation between two 3D points
|
||||||
|
RE::NiPoint3 Lerp(const RE::NiPoint3& a_from, const RE::NiPoint3& a_to, float a_alpha) noexcept;
|
||||||
|
|
||||||
|
// Helper: Circular interpolation for rotation (handles wrap-around)
|
||||||
|
float LerpRotation(float a_from, float a_to, float a_alpha) noexcept;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
|
||||||
|
#include "F4TInterpolationSystem.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace F4T::InterpolationSystem
|
||||||
|
{
|
||||||
|
// Helper: Linear interpolation for 3D points
|
||||||
|
RE::NiPoint3 Lerp(const RE::NiPoint3& a_from, const RE::NiPoint3& a_to, float a_alpha) noexcept
|
||||||
|
{
|
||||||
|
a_alpha = std::clamp(a_alpha, 0.0F, 1.0F);
|
||||||
|
|
||||||
|
return RE::NiPoint3{
|
||||||
|
a_from.x + (a_to.x - a_from.x) * a_alpha,
|
||||||
|
a_from.y + (a_to.y - a_from.y) * a_alpha,
|
||||||
|
a_from.z + (a_to.z - a_from.z) * a_alpha
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Circular interpolation for angles (handles 0-2π wrap-around)
|
||||||
|
float LerpRotation(float a_from, float a_to, float a_alpha) noexcept
|
||||||
|
{
|
||||||
|
a_alpha = std::clamp(a_alpha, 0.0F, 1.0F);
|
||||||
|
|
||||||
|
// Normalize both angles to [0, 2π]
|
||||||
|
const auto normalize = [](float a_angle) {
|
||||||
|
while (a_angle < 0.0F) a_angle += 2.0F * 3.14159265F;
|
||||||
|
while (a_angle >= 2.0F * 3.14159265F) a_angle -= 2.0F * 3.14159265F;
|
||||||
|
return a_angle;
|
||||||
|
};
|
||||||
|
|
||||||
|
float from = normalize(a_from);
|
||||||
|
float to = normalize(a_to);
|
||||||
|
|
||||||
|
// Find the shortest path
|
||||||
|
float delta = to - from;
|
||||||
|
if (delta > 3.14159265F) {
|
||||||
|
delta -= 2.0F * 3.14159265F;
|
||||||
|
} else if (delta < -3.14159265F) {
|
||||||
|
delta += 2.0F * 3.14159265F;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalize(from + delta * a_alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
InterpolationResult Update(
|
||||||
|
InterpolationComponent& a_component,
|
||||||
|
uint64_t a_currentTick) noexcept
|
||||||
|
{
|
||||||
|
InterpolationResult result{};
|
||||||
|
|
||||||
|
// Need at least 2 waypoints to interpolate
|
||||||
|
if (a_component.TimePoints.size() < 2) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& first = a_component.TimePoints[0];
|
||||||
|
auto& second = a_component.TimePoints[1];
|
||||||
|
|
||||||
|
// If current tick is before the first waypoint, return the first position
|
||||||
|
if (a_currentTick <= first.Tick) {
|
||||||
|
result.Position = first.Position;
|
||||||
|
result.Rotation = first.Rotation;
|
||||||
|
result.IsInterpolating = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current tick is past the second waypoint, remove the first and try again
|
||||||
|
if (a_currentTick >= second.Tick) {
|
||||||
|
a_component.TimePoints.pop_front();
|
||||||
|
|
||||||
|
// Recursively check if we can interpolate with the next pair
|
||||||
|
if (a_component.TimePoints.size() >= 2) {
|
||||||
|
return Update(a_component, a_currentTick);
|
||||||
|
} else {
|
||||||
|
// Only one waypoint left; use it
|
||||||
|
result.Position = a_component.TimePoints[0].Position;
|
||||||
|
result.Rotation = a_component.TimePoints[0].Rotation;
|
||||||
|
result.IsInterpolating = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate between first and second waypoint
|
||||||
|
const uint64_t tickDelta = second.Tick > first.Tick ? second.Tick - first.Tick : 1;
|
||||||
|
const uint64_t tickElapsed = a_currentTick - first.Tick;
|
||||||
|
float alpha = static_cast<float>(tickElapsed) / static_cast<float>(tickDelta);
|
||||||
|
alpha = std::clamp(alpha, 0.0F, 1.0F);
|
||||||
|
|
||||||
|
result.Position = Lerp(first.Position, second.Position, alpha);
|
||||||
|
result.Rotation = LerpRotation(first.Rotation, second.Rotation, alpha);
|
||||||
|
result.IsInterpolating = true;
|
||||||
|
|
||||||
|
a_component.LastInterpolatedPosition = result.Position;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "F4TProxyAnimationSync.h"
|
#include "F4TProxyAnimationSync.h"
|
||||||
#include "F4TRemotePlayerState.h"
|
#include "F4TRemotePlayerState.h"
|
||||||
#include "F4RemoteActionComponent.h"
|
#include "F4RemoteActionComponent.h"
|
||||||
|
#include "F4TInterpolationSystem.h"
|
||||||
|
|
||||||
#include "RE/A/ACTOR_STANCE.h"
|
#include "RE/A/ACTOR_STANCE.h"
|
||||||
#include "RE/A/ActorValue.h"
|
#include "RE/A/ActorValue.h"
|
||||||
@@ -76,8 +77,8 @@ namespace
|
|||||||
constexpr auto kRuntimeProxyDebugForwardDistance = 250.0F;
|
constexpr auto kRuntimeProxyDebugForwardDistance = 250.0F;
|
||||||
constexpr auto kRuntimeProxyDebugFallbackOffsetX = 200.0F;
|
constexpr auto kRuntimeProxyDebugFallbackOffsetX = 200.0F;
|
||||||
constexpr auto kDebugPlaceAtMeSpawnDetectionRadius = 700.0F;
|
constexpr auto kDebugPlaceAtMeSpawnDetectionRadius = 700.0F;
|
||||||
constexpr bool kDebugForceRuntimeProxyNearLocalPlayer = true;
|
constexpr bool kDebugForceRuntimeProxyNearLocalPlayer = false;
|
||||||
constexpr bool kDebugHoldRuntimeProxyNearLocalPlayer = true;
|
constexpr bool kDebugHoldRuntimeProxyNearLocalPlayer = false;
|
||||||
constexpr float kDebugRuntimeProxyHoldSeconds = 15.0F;
|
constexpr float kDebugRuntimeProxyHoldSeconds = 15.0F;
|
||||||
constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F;
|
constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F;
|
||||||
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
||||||
@@ -276,6 +277,11 @@ namespace
|
|||||||
|
|
||||||
// Phase 5.1: Action queue for discrete action replay
|
// Phase 5.1: Action queue for discrete action replay
|
||||||
F4T::RemoteActionComponent::RemoteActionQueue actionQueue;
|
F4T::RemoteActionComponent::RemoteActionQueue actionQueue;
|
||||||
|
|
||||||
|
// Phase 2: Interpolation system for smooth movement between waypoints
|
||||||
|
F4T::InterpolationSystem::InterpolationComponent interpolationComponent{};
|
||||||
|
uint64_t lastInterpolationTick{ 0 };
|
||||||
|
bool interpolationEnabledLogged{ false };
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||||
@@ -579,28 +585,53 @@ namespace
|
|||||||
a_proxy.GetFormID(), hasAI));
|
a_proxy.GetFormID(), hasAI));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always update heading
|
|
||||||
a_proxy.SetHeading(a_heading);
|
|
||||||
|
|
||||||
RE::NiPoint3 currentPosition = a_proxy.GetPosition();
|
RE::NiPoint3 currentPosition = a_proxy.GetPosition();
|
||||||
RE::NiPoint3 deltaPos = a_nextPosition - currentPosition;
|
RE::NiPoint3 deltaPos = a_nextPosition - currentPosition;
|
||||||
float distanceDrift = deltaPos.Length();
|
float distanceDrift = deltaPos.Length();
|
||||||
|
|
||||||
// SetPosition moves the proxy to the correct network position.
|
// Stable smoothing strategy:
|
||||||
a_proxy.SetPosition(a_nextPosition, true);
|
// - snap for explicit snap movements or huge drift
|
||||||
if (a_optSlot) {
|
// - otherwise, smoothly lerp from current -> latest network target
|
||||||
a_optSlot->lastPositionSyncTime = std::chrono::steady_clock::now();
|
constexpr float kLargeDriftSnapDistance = 1800.0F;
|
||||||
|
constexpr float kInterpolationRatePerSecond = 8.0F;
|
||||||
|
constexpr float kMinInterpolationAlpha = 0.08F;
|
||||||
|
constexpr float kMaxInterpolationAlpha = 0.35F;
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
float deltaSeconds = 1.0F / 60.0F;
|
||||||
|
if (a_optSlot && a_optSlot->lastPositionSyncTime != std::chrono::steady_clock::time_point{}) {
|
||||||
|
deltaSeconds = std::chrono::duration<float>(now - a_optSlot->lastPositionSyncTime).count();
|
||||||
|
}
|
||||||
|
deltaSeconds = std::clamp(deltaSeconds, 1.0F / 120.0F, 0.1F);
|
||||||
|
|
||||||
|
RE::NiPoint3 positionToApply = a_nextPosition;
|
||||||
|
if (!a_shouldSnap && distanceDrift < kLargeDriftSnapDistance) {
|
||||||
|
const float alpha = std::clamp(
|
||||||
|
deltaSeconds * kInterpolationRatePerSecond,
|
||||||
|
kMinInterpolationAlpha,
|
||||||
|
kMaxInterpolationAlpha);
|
||||||
|
positionToApply = F4T::InterpolationSystem::Lerp(currentPosition, a_nextPosition, alpha);
|
||||||
|
} else if (a_optSlot) {
|
||||||
|
// Clear queued waypoints when hard-snapping to avoid stale interpolation state.
|
||||||
|
a_optSlot->interpolationComponent.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject velocity into the character controller so the movement controller
|
// Always apply authoritative heading directly.
|
||||||
// computes a non-zero Speed, which the animation graph reads to blend walk/run.
|
a_proxy.SetHeading(a_heading);
|
||||||
// Without this, SetPosition produces zero velocity and the NPC stands still.
|
a_proxy.SetPosition(positionToApply, true);
|
||||||
if (distanceDrift > 0.001F) {
|
|
||||||
auto* charController = a_proxy.Move(0.016F, deltaPos, false);
|
if (a_optSlot) {
|
||||||
if (charController) {
|
a_optSlot->lastPositionSyncTime = now;
|
||||||
InjectProxyMovementVelocity(*charController, deltaPos, 0.016F);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Movement is kinematic (SetPosition-based). We deliberately do NOT
|
||||||
|
// inject character-controller velocity here. Persistent havok velocity plus
|
||||||
|
// the proxy's active AIProcess caused the actor to drift/wander around the
|
||||||
|
// network target (visible as teleporting), especially while the remote was
|
||||||
|
// idle. Locomotion animation is driven separately by writing the "Speed"
|
||||||
|
// graph variable in ProxyAnimationSync (Phase 1), so velocity injection is
|
||||||
|
// unnecessary for animation blending. Per-frame SetPosition in
|
||||||
|
// ApplySmoothFrameMovement pins the actor to the network target.
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Enable AI-driven animation mode
|
// NEW: Enable AI-driven animation mode
|
||||||
@@ -5304,28 +5335,33 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition;
|
RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition;
|
||||||
float distanceToTarget = deltaToTarget.Length();
|
float distanceToTarget = deltaToTarget.Length();
|
||||||
|
|
||||||
// Skip if too close to target
|
proxy->SetHeading(slot.targetHeading);
|
||||||
|
|
||||||
|
// Already at target: pin exactly and stop. This prevents the actor from
|
||||||
|
// drifting/wandering around a fixed target while the remote is idle.
|
||||||
if (distanceToTarget < 0.1F) {
|
if (distanceToTarget < 0.1F) {
|
||||||
|
proxy->SetPosition(slot.targetPosition, true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy->SetHeading(slot.targetHeading);
|
// Kinematic smoothing toward the network target.
|
||||||
|
// We intentionally avoid proxy->Move() + velocity injection: physics-driven
|
||||||
// Use Move() to smoothly approach target via character controller
|
// movement combined with the proxy's active AIProcess caused unstable
|
||||||
// This lets the AI/character controller drive animations naturally
|
// wandering/overshoot. SetPosition pins the actor deterministically while
|
||||||
constexpr float kFrameDeltaSeconds = 0.016F;
|
// the locomotion animation is driven by the "Speed" graph variable (Phase 1).
|
||||||
constexpr float MAX_FRAME_DELTA = 12.0F; // Max movement per frame
|
constexpr float kLargeDriftSnapDistance = 1500.0F;
|
||||||
RE::NiPoint3 frameDelta = deltaToTarget;
|
RE::NiPoint3 nextPosition;
|
||||||
if (distanceToTarget > MAX_FRAME_DELTA) {
|
if (distanceToTarget > kLargeDriftSnapDistance) {
|
||||||
frameDelta = (deltaToTarget / distanceToTarget) * MAX_FRAME_DELTA;
|
// Recover instantly from very large gaps (e.g. cell change / teleport).
|
||||||
|
nextPosition = slot.targetPosition;
|
||||||
|
} else {
|
||||||
|
// Smoothly converge a fraction of the remaining distance each frame.
|
||||||
|
constexpr float kPerFrameApproachAlpha = 0.25F;
|
||||||
|
nextPosition = F4T::InterpolationSystem::Lerp(
|
||||||
|
currentPosition, slot.targetPosition, kPerFrameApproachAlpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call Move() through the character controller and publish a velocity
|
proxy->SetPosition(nextPosition, true);
|
||||||
// sample so the graph can blend into locomotion.
|
|
||||||
// This should update velocity in the character controller for animations
|
|
||||||
if (auto* charController = proxy->Move(kFrameDeltaSeconds, frameDelta, false)) {
|
|
||||||
InjectProxyMovementVelocity(*charController, frameDelta, kFrameDeltaSeconds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,65 @@ namespace
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ApplyDesiredStateToGraphDescriptorBased(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const DesiredProxyAnimationState& a_desired,
|
||||||
|
std::uint32_t a_remotePlayerId)
|
||||||
|
{
|
||||||
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
|
const auto actorId = a_proxy.GetFormID();
|
||||||
|
|
||||||
|
auto& descriptor = F4T::AnimationDescriptor::F4AnimationDescriptor::GetHumanoidDescriptor();
|
||||||
|
F4T::AnimationDescriptor::AnimationVariableSnapshot snapshot;
|
||||||
|
|
||||||
|
// Resize vectors to match descriptor counts
|
||||||
|
snapshot.floats.resize(descriptor.GetFloatVariableCount(), 0.0F);
|
||||||
|
snapshot.bools.resize(descriptor.GetBoolVariableCount(), false);
|
||||||
|
snapshot.ints.resize(descriptor.GetIntVariableCount(), 0);
|
||||||
|
|
||||||
|
// Populate float variables
|
||||||
|
const auto speedIdx = descriptor.GetFloatVariableIndex("Speed");
|
||||||
|
if (speedIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.floats[speedIdx] = a_desired.graphSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto directionIdx = descriptor.GetFloatVariableIndex("direction");
|
||||||
|
if (directionIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.floats[directionIdx] = a_desired.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate bool variables
|
||||||
|
const auto isSprintingIdx = descriptor.GetBoolVariableIndex("isSprinting");
|
||||||
|
if (isSprintingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isSprintingIdx] = a_desired.isSprinting;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto isSneakingIdx = descriptor.GetBoolVariableIndex("isSneaking");
|
||||||
|
if (isSneakingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isSneakingIdx] = a_desired.isSneaking;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto isMovingIdx = descriptor.GetBoolVariableIndex("isMoving");
|
||||||
|
if (isMovingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isMovingIdx] = a_desired.isMoving;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single bulk write operation (major performance win)
|
||||||
|
const auto success = descriptor.LoadAnimationVariablesToCache(*graphHolder, snapshot);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||||
|
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
||||||
|
LogWarningWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy animation sync (descriptor-based) failed for remote player {}: actor={:08X}.",
|
||||||
|
a_remotePlayerId,
|
||||||
|
actorId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
void LogAppliedTransition(
|
void LogAppliedTransition(
|
||||||
std::uint32_t a_remotePlayerId,
|
std::uint32_t a_remotePlayerId,
|
||||||
std::uint32_t a_actorId,
|
std::uint32_t a_actorId,
|
||||||
@@ -293,7 +352,7 @@ namespace F4T::ProxyAnimationSync
|
|||||||
|
|
||||||
const auto initial = !a_state.hasAppliedGraphState;
|
const auto initial = !a_state.hasAppliedGraphState;
|
||||||
const auto changed = HasDesiredStateChanged(a_state, desired);
|
const auto changed = HasDesiredStateChanged(a_state, desired);
|
||||||
if (ApplyDesiredStateToGraph(a_proxy, desired, a_remotePlayer.playerId)) {
|
if (ApplyDesiredStateToGraphDescriptorBased(a_proxy, desired, a_remotePlayer.playerId)) {
|
||||||
if (initial || changed) {
|
if (initial || changed) {
|
||||||
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user