Inject controller velocity & send anim events
Add runtime velocity injection to the proxy character controller so the engine has velocity available when evaluating animation graphs (accessed via Actor::currentProcess->middleHigh->charController). Introduce animation graph event constants, TrySendGraphEvent helper, and SendLocomotionTransitionEvents to fire move/sprint/sneak start/stop events on state transitions (with diagnostic logging). Call SendLocomotionTransitionEvents before applying the new graph state so FSM transitions are driven correctly. Respect compile-time flags (kEnableProxyLocomotionSync, kEnableProxySneakSync).
This commit is contained in:
@@ -620,18 +620,19 @@ namespace
|
||||
a_proxy.SetHeading(a_heading);
|
||||
a_proxy.SetPosition(positionToApply, true);
|
||||
|
||||
// Phase 6.1: Inject character controller velocity
|
||||
// The animation system requires velocity on the character controller to evaluate
|
||||
// animation graphs and blend between locomotion states. Without velocity, the
|
||||
// "Speed" graph variable alone doesn't trigger animations.
|
||||
// Access controller via: Actor::currentProcess -> AIProcess::middleHigh -> MiddleHighProcessData::charController
|
||||
if (a_proxy.currentProcess && a_proxy.currentProcess->middleHigh && a_proxy.currentProcess->middleHigh->charController) {
|
||||
const RE::NiPoint3 latestDeltaPos = a_nextPosition - currentPosition;
|
||||
InjectProxyMovementVelocity(*a_proxy.currentProcess->middleHigh->charController, latestDeltaPos, deltaSeconds);
|
||||
}
|
||||
|
||||
if (a_optSlot) {
|
||||
a_optSlot->lastPositionSyncTime = now;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -99,6 +99,29 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
// Creation Engine humanoid behavior animation events. These drive the locomotion
|
||||
// state machine transitions. Writing the "Speed" graph variable alone only blends
|
||||
// WITHIN a movement state; the graph must be transitioned INTO that state via an
|
||||
// animation event (the same FSM-driving mechanism TiltedEvolution uses via ForceAction).
|
||||
// NotifyAnimationGraph returns true when the active graph recognizes/consumes the event,
|
||||
// which doubles as a diagnostic for which event names this FO4 graph accepts.
|
||||
namespace GraphEvent
|
||||
{
|
||||
inline constexpr const char* kMoveStart = "moveStart";
|
||||
inline constexpr const char* kMoveStop = "moveStop";
|
||||
inline constexpr const char* kSprintStart = "SprintStart";
|
||||
inline constexpr const char* kSprintStop = "SprintStop";
|
||||
inline constexpr const char* kSneakStart = "SneakStart";
|
||||
inline constexpr const char* kSneakStop = "SneakStop";
|
||||
}
|
||||
|
||||
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
||||
{
|
||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||
const RE::BSFixedString eventName{ a_eventName };
|
||||
return graphHolder->NotifyAnimationGraphImpl(eventName);
|
||||
}
|
||||
|
||||
bool TrySetGraphBool(
|
||||
RE::IAnimationGraphManagerHolder& a_holder,
|
||||
const char* a_variableName,
|
||||
@@ -298,6 +321,49 @@ namespace
|
||||
return success;
|
||||
}
|
||||
|
||||
// Fire locomotion animation events on state transitions to actually drive the
|
||||
// proxy's animation graph FSM (idle <-> move, sprint, sneak). Edge-triggered, mirroring
|
||||
// how the game itself emits these events when the local player starts/stops moving.
|
||||
void SendLocomotionTransitionEvents(
|
||||
RE::Actor& a_proxy,
|
||||
const F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_previous,
|
||||
const DesiredProxyAnimationState& a_desired,
|
||||
std::uint32_t a_remotePlayerId)
|
||||
{
|
||||
if constexpr (!kEnableProxyLocomotionSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = a_proxy.GetFormID();
|
||||
const bool wasInitialized = a_previous.hasAppliedGraphState;
|
||||
|
||||
auto fireEvent = [&](const char* a_eventName) {
|
||||
const bool accepted = TrySendGraphEvent(a_proxy, a_eventName);
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy locomotion event '{}' for remote player {}: actor={:08X}, graphAccepted={}.",
|
||||
a_eventName,
|
||||
a_remotePlayerId,
|
||||
actorId,
|
||||
accepted));
|
||||
};
|
||||
|
||||
// Movement start/stop is the critical transition that takes the graph out of idle.
|
||||
if (!wasInitialized || a_previous.lastIsMoving != a_desired.isMoving) {
|
||||
fireEvent(a_desired.isMoving ? GraphEvent::kMoveStart : GraphEvent::kMoveStop);
|
||||
}
|
||||
|
||||
// Sprint sub-state transition (only meaningful while moving).
|
||||
if (wasInitialized && a_previous.lastIsSprinting != a_desired.isSprinting) {
|
||||
fireEvent(a_desired.isSprinting ? GraphEvent::kSprintStart : GraphEvent::kSprintStop);
|
||||
}
|
||||
|
||||
if constexpr (kEnableProxySneakSync) {
|
||||
if (!wasInitialized || a_previous.lastIsSneaking != a_desired.isSneaking) {
|
||||
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogAppliedTransition(
|
||||
std::uint32_t a_remotePlayerId,
|
||||
std::uint32_t a_actorId,
|
||||
@@ -353,6 +419,11 @@ namespace F4T::ProxyAnimationSync
|
||||
const auto initial = !a_state.hasAppliedGraphState;
|
||||
const auto changed = HasDesiredStateChanged(a_state, desired);
|
||||
if (ApplyDesiredStateToGraphDescriptorBased(a_proxy, desired, a_remotePlayer.playerId)) {
|
||||
// Drive the animation graph FSM with locomotion events. Must run before we
|
||||
// overwrite a_state below, since transition detection compares against the
|
||||
// previously applied state.
|
||||
SendLocomotionTransitionEvents(a_proxy, a_state, desired, a_remotePlayer.playerId);
|
||||
|
||||
if (initial || changed) {
|
||||
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user