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:
2026-06-03 14:59:08 +12:00
co-authored by Cursor
parent e156d048ec
commit 2449525175
2 changed files with 90 additions and 23 deletions
+40 -23
View File
@@ -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