docs: animation breakthrough - proxies ARE real NPCs with AI
Key discovery: The proxy is a real NPC (has voice lines, can aggro) which means it has a full active AIProcess. This changes everything! ROOT CAUSE IDENTIFIED: - We use SetPosition() which bypasses the character controller - This prevents Move() from being called - Without Move(), velocity is never updated - Animation graph never sees velocity, so no animations SOLUTION: - Let the proxy's AIProcess drive Move() calls naturally - This will update velocity and trigger animations automatically - Use SetPosition() only occasionally (~200ms) to correct network drift - Result: Smooth networked movement WITH proper animations Why this wasn't realized before: - Previous investigation assumed proxies were 'puppets' - We now know they're real NPCs with full AI capability - We were just bypassing the animation pipeline with SetPosition() Added: - ApplyRuntimeProxyTransformWithAI() function (hybrid approach template) - Comprehensive breakthrough notes in dev-log Next: Implement actual hybrid movement that lets AI drive animations Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -463,6 +463,44 @@ namespace
|
||||
return std::sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
|
||||
}
|
||||
|
||||
// Hybrid movement strategy for AI-driven animations
|
||||
// Phase 1: Let the proxy's natural AI/character controller drive movement
|
||||
// This allows Move() to be called, which updates velocity and plays animations
|
||||
// Phase 2: Occasionally override position for network sync (every ~200ms)
|
||||
void ApplyRuntimeProxyTransformWithAI(
|
||||
RE::Actor& a_proxy,
|
||||
const RE::NiPoint3& a_nextPosition,
|
||||
float a_heading,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
bool a_shouldSnap,
|
||||
float a_timeSinceLastSync)
|
||||
{
|
||||
(void)a_remotePlayer;
|
||||
|
||||
// Always update heading
|
||||
a_proxy.SetHeading(a_heading);
|
||||
|
||||
// Strategy:
|
||||
// - The proxy's AIProcess naturally calls Move() each frame
|
||||
// - Move() updates velocity, which triggers animations
|
||||
// - We only use SetPosition() if drift exceeds threshold or snap is required
|
||||
|
||||
const float DRIFT_THRESHOLD = 100.0F; // Units
|
||||
const float SYNC_INTERVAL = 0.2F; // Seconds between position resets
|
||||
|
||||
RE::NiPoint3 currentPosition = a_proxy.GetPosition();
|
||||
float distanceDrift = (a_nextPosition - currentPosition).Length();
|
||||
|
||||
// If we've drifted too far or it's time to sync, override position
|
||||
if (a_shouldSnap || a_timeSinceLastSync >= SYNC_INTERVAL || distanceDrift > DRIFT_THRESHOLD) {
|
||||
a_proxy.SetPosition(a_nextPosition, true);
|
||||
// Reset sync timer would happen in caller
|
||||
}
|
||||
|
||||
// Otherwise, let the AI drive movement naturally
|
||||
// The animation system will evaluate velocity and play animations
|
||||
}
|
||||
|
||||
void ApplyRuntimeProxyTransform(
|
||||
RE::Actor& a_proxy,
|
||||
const RE::NiPoint3& a_nextPosition,
|
||||
|
||||
Reference in New Issue
Block a user