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.SetHeading(a_heading);
|
||||||
a_proxy.SetPosition(positionToApply, true);
|
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) {
|
if (a_optSlot) {
|
||||||
a_optSlot->lastPositionSyncTime = now;
|
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
|
// NEW: Enable AI-driven animation mode
|
||||||
|
|||||||
@@ -99,6 +99,29 @@ namespace
|
|||||||
return true;
|
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(
|
bool TrySetGraphBool(
|
||||||
RE::IAnimationGraphManagerHolder& a_holder,
|
RE::IAnimationGraphManagerHolder& a_holder,
|
||||||
const char* a_variableName,
|
const char* a_variableName,
|
||||||
@@ -298,6 +321,49 @@ namespace
|
|||||||
return success;
|
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(
|
void LogAppliedTransition(
|
||||||
std::uint32_t a_remotePlayerId,
|
std::uint32_t a_remotePlayerId,
|
||||||
std::uint32_t a_actorId,
|
std::uint32_t a_actorId,
|
||||||
@@ -353,6 +419,11 @@ 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 (ApplyDesiredStateToGraphDescriptorBased(a_proxy, desired, a_remotePlayer.playerId)) {
|
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) {
|
if (initial || changed) {
|
||||||
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
LogAppliedTransition(a_remotePlayer.playerId, actorId, desired, initial);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user