fix: eliminate frequent SetPosition calls that break AI
BREAKTHROUGH INSIGHT: The user reported: AI goes aggressive then immediately returns to neutral. This matches the symptom of SetPosition() breaking the character controller. ROOT CAUSE: - We were calling SetPosition() every 500ms - SetPosition() bypasses character controller → kills all movement - Between SetPosition calls, Move() had to re-establish control - This constant switching BREAKS the AI's ability to function THE FIX: - ONLY use SetPosition() for emergencies (drift > 300 units OR > 2 seconds) - Always use Move() with small, bounded deltas (max 50 units/frame) - This lets the character controller and AI run continuously - AI can now maintain state and play animations properly CRITICAL CHANGES: 1. Emergency thresholds: 300 unit drift OR 2 second timeout (not 200/500ms) 2. Normal movement: Use Move() with small incremental deltas 3. Never SetPosition during normal gameplay 4. SetPosition only as emergency correction EXPECTED RESULT: - ✅ Smooth continuous movement (no more jerky jumps) - ✅ AI stays active (can get aggro and respond naturally) - ✅ Proper animations (velocity continuous, not reset every 500ms) - ✅ Network sync maintained (emergency corrections prevent drift) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3387,6 +3387,56 @@ Previous investigation assumed proxies were "puppets" without real AI. We now kn
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03: CRITICAL FIX - Call Move() EVERY FRAME
|
||||
|
||||
### The Problem Found
|
||||
|
||||
Test showed proxy was moving in jerky jumps every 300ms (only when SetPosition was called). **The proxy wasn't moving smoothly between syncs!**
|
||||
|
||||
**Root Cause:** We were assuming the AI would naturally call Move() if we just didn't call SetPosition(). **Wrong!** The AI doesn't automatically drive movement on PlaceAtMe proxies. We have to call `Move()` ourselves.
|
||||
|
||||
### The Fix ✅
|
||||
|
||||
**Call Move() EVERY FRAME** with the calculated delta to target position!
|
||||
|
||||
```cpp
|
||||
RE::NiPoint3 deltaPos = a_nextPosition - a_proxy.GetPosition();
|
||||
a_proxy.Move(0.016F, deltaPos, true); // Every frame!
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Move() updates character controller velocity every frame
|
||||
- Velocity is what the animation graph actually evaluates
|
||||
- SetPosition() every 500ms corrects accumulated position error
|
||||
- Result: Smooth movement + animations!
|
||||
|
||||
### New Strategy
|
||||
|
||||
```
|
||||
Every game frame:
|
||||
1. Call Move(0.016F, deltaPos, true) to update velocity
|
||||
→ Smooth movement
|
||||
→ Animation graph sees velocity → plays walk/run anims
|
||||
|
||||
Every 500ms:
|
||||
2. Call SetPosition() to correct position
|
||||
→ Prevents drift from network target
|
||||
→ Maintains sync accuracy
|
||||
```
|
||||
|
||||
### Updated Implementation
|
||||
|
||||
- Modified ApplyRuntimeProxyTransform() to call Move() every frame
|
||||
- Increased sync interval to 500ms (less frequent position overrides)
|
||||
- Increased drift threshold to 200 units (tolerate more drift between syncs)
|
||||
|
||||
**Expected Result:**
|
||||
- ✅ Smooth continuous movement (not jerky)
|
||||
- ✅ Proper animations (from velocity updates)
|
||||
- ✅ Network synchronized (SetPosition every 500ms)
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03: HYBRID MOVEMENT IMPLEMENTATION - Animations Now Enabled
|
||||
|
||||
### Implementation Complete ✅
|
||||
|
||||
@@ -502,6 +502,9 @@ namespace
|
||||
// The animation system will evaluate velocity and play animations
|
||||
}
|
||||
|
||||
// Debug: Track if we've logged the AI state for a proxy
|
||||
static std::unordered_set<RE::Actor*> g_loggedProxies;
|
||||
|
||||
void ApplyRuntimeProxyTransform(
|
||||
RE::Actor& a_proxy,
|
||||
const RE::NiPoint3& a_nextPosition,
|
||||
@@ -512,50 +515,64 @@ namespace
|
||||
{
|
||||
(void)a_remotePlayer;
|
||||
|
||||
// DEBUG: Log AI state once per proxy
|
||||
if (g_loggedProxies.find(&a_proxy) == g_loggedProxies.end()) {
|
||||
g_loggedProxies.insert(&a_proxy);
|
||||
bool hasAI = a_proxy.currentProcess != nullptr;
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"ApplyRuntimeProxyTransform: Proxy {:08X} has AIProcess: {}",
|
||||
a_proxy.GetFormID(), hasAI));
|
||||
}
|
||||
|
||||
// Always update heading
|
||||
a_proxy.SetHeading(a_heading);
|
||||
|
||||
// KEY FIX: Call Move() EVERY FRAME with delta
|
||||
// This is what drives animations - the animation system evaluates velocity!
|
||||
RE::NiPoint3 currentPosition = a_proxy.GetPosition();
|
||||
RE::NiPoint3 deltaPos = a_nextPosition - currentPosition;
|
||||
float distanceDrift = deltaPos.Length();
|
||||
|
||||
// Call Move() to update character controller velocity
|
||||
// This triggers animation graph velocity evaluation every frame
|
||||
a_proxy.Move(0.016F, deltaPos, true); // 0.016F ≈ 60fps frame time
|
||||
// CRITICAL: NEVER call SetPosition() during normal movement!
|
||||
// SetPosition() breaks the character controller and kills AI movement.
|
||||
// Instead, ONLY use it for emergency corrections (large drift or initial snap).
|
||||
|
||||
// Handle position sync separately
|
||||
constexpr float SYNC_INTERVAL = 0.5F; // Sync every 500ms
|
||||
constexpr float DRIFT_THRESHOLD = 200.0F; // Force sync if drift > 200 units
|
||||
|
||||
bool shouldOverridePosition = a_shouldSnap;
|
||||
bool needsEmergencyCorrection = a_shouldSnap || (a_optSlot == nullptr);
|
||||
|
||||
if (a_optSlot && !a_shouldSnap) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto timeSinceSync = std::chrono::duration<float>(now - a_optSlot->lastPositionSyncTime).count();
|
||||
|
||||
// Check if we should re-sync
|
||||
if (timeSinceSync >= SYNC_INTERVAL || deltaPos.Length() > DRIFT_THRESHOLD) {
|
||||
shouldOverridePosition = true;
|
||||
// ONLY SetPosition if drift is EXTREMELY large (> 300 units) OR sync timer is way past (> 2 seconds)
|
||||
// This prevents breaking the AI with constant position resets
|
||||
constexpr float EMERGENCY_DRIFT_THRESHOLD = 300.0F;
|
||||
constexpr float EMERGENCY_SYNC_TIME = 2.0F;
|
||||
|
||||
if (timeSinceSync >= EMERGENCY_SYNC_TIME || distanceDrift > EMERGENCY_DRIFT_THRESHOLD) {
|
||||
needsEmergencyCorrection = true;
|
||||
}
|
||||
} else if (!a_optSlot) {
|
||||
shouldOverridePosition = true;
|
||||
}
|
||||
|
||||
if (shouldOverridePosition) {
|
||||
// SetPosition after Move() to correct position error
|
||||
if (needsEmergencyCorrection) {
|
||||
// Emergency correction - use SetPosition only when drift is critical
|
||||
a_proxy.SetPosition(a_nextPosition, true);
|
||||
|
||||
if (a_optSlot) {
|
||||
a_optSlot->lastPositionSyncTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
} else {
|
||||
// Normal movement: Use Move() with tiny incremental deltas
|
||||
// This lets the AI naturally follow the target position via character controller
|
||||
// The key is SMALL, FREQUENT updates instead of large jumps
|
||||
constexpr float MAX_MOVE_DELTA = 50.0F; // Max units per frame
|
||||
|
||||
RE::NiPoint3 limitedDelta = deltaPos;
|
||||
if (distanceDrift > MAX_MOVE_DELTA) {
|
||||
limitedDelta = (deltaPos / distanceDrift) * MAX_MOVE_DELTA;
|
||||
}
|
||||
|
||||
// Call Move() with limited delta to guide the proxy smoothly
|
||||
// This should trigger animations naturally!
|
||||
a_proxy.Move(0.016F, limitedDelta, true);
|
||||
}
|
||||
|
||||
// The magic formula:
|
||||
// - Move() every frame updates velocity automatically
|
||||
// - Velocity triggers animation evaluation in Havok
|
||||
// - SetPosition() every 500ms maintains network accuracy
|
||||
// - Result: Smooth animations + network sync!
|
||||
}
|
||||
|
||||
// NEW: Enable AI-driven animation mode
|
||||
|
||||
Reference in New Issue
Block a user