Stabilize proxy visibility and smooth movement
Fix proxy orbiting and improve motion/visibility handling. - Capture a per-slot visibleDebugAnchorPosition (reset on reassignment) to keep pre-placed proxies visually stable and avoid orbiting when the local player moves. - Remove the debug +20Z offset so pre-placed actors remain grounded. - Add GetRuntimeProxyVisibleTargetPosition to compute a stable visible target (anchored base + scaled remote delta). - Replace teleport-heavy updates with smooth movement: add InjectProxyMovementVelocity to publish velocity into bhkCharacterController, call Actor::Move() per-frame and inject velocity for animation blending. - Prefer reusing pre-placed pool proxies before attempting dynamic spawn. - Implement dynamic spawn via NEW_REFR_DATA / TESDataHandler::CreateReferenceAtLocation, apply debug visibility to spawned actors, and store handles in the dynamic pool. - Harden debug visibility setup: explicitly enable/disable 3D, alpha, display geometry, set 3D update flags and queue a TaskQueueInterface 3D update; expand debug logging. - Remove unsafe currentProcess target-clearing code and other minor cleanups. - Add dev-log entries describing Phase 6 debugging and fixes.
This commit is contained in:
@@ -13,7 +13,11 @@
|
||||
#include "RE/C/COMMAND_TYPE.h"
|
||||
#include "RE/C/Console.h"
|
||||
#include "RE/H/hkVector4.h"
|
||||
#include "RE/N/NEW_REFR_DATA.h"
|
||||
#include "RE/P/PTYPE.h"
|
||||
#include "RE/R/RESET_3D_FLAGS.h"
|
||||
#include "RE/T/TESDataHandler.h"
|
||||
#include "RE/T/TaskQueueInterface.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
@@ -75,7 +79,7 @@ namespace
|
||||
constexpr bool kDebugForceRuntimeProxyNearLocalPlayer = true;
|
||||
constexpr bool kDebugHoldRuntimeProxyNearLocalPlayer = true;
|
||||
constexpr float kDebugRuntimeProxyHoldSeconds = 15.0F;
|
||||
constexpr auto kDebugRuntimeProxyOffsetZ = 20.0F;
|
||||
constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F;
|
||||
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
||||
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
||||
constexpr std::size_t kMaxRuntimeProxyActors = 4;
|
||||
@@ -240,6 +244,7 @@ namespace
|
||||
std::uint32_t spawnBaseFormId{ 0 };
|
||||
std::unordered_set<std::uint32_t> preexistingCandidateRefs;
|
||||
std::optional<RE::NiPoint3> initialRemoteSpawnPosition;
|
||||
std::optional<RE::NiPoint3> visibleDebugAnchorPosition;
|
||||
std::chrono::steady_clock::time_point candidateSearchStartTime{};
|
||||
std::chrono::steady_clock::time_point settleStartTime{};
|
||||
std::chrono::steady_clock::time_point heldSinceTime{};
|
||||
@@ -488,6 +493,32 @@ namespace
|
||||
return std::sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
|
||||
}
|
||||
|
||||
void InjectProxyMovementVelocity(RE::bhkCharacterController& a_charController, const RE::NiPoint3& a_deltaPos, float a_deltaSeconds)
|
||||
{
|
||||
if (a_deltaSeconds <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto distance = a_deltaPos.Length();
|
||||
if (distance <= 0.001F) {
|
||||
const RE::hkVector4f stoppedVelocity{ 0.0F, 0.0F, 0.0F, 0.0F };
|
||||
a_charController.SetLinearVelocityImpl(stoppedVelocity);
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float kMaxInjectedSpeed = 400.0F;
|
||||
const auto unclampedSpeed = distance / a_deltaSeconds;
|
||||
const auto clampedSpeed = unclampedSpeed > kMaxInjectedSpeed ? kMaxInjectedSpeed : unclampedSpeed;
|
||||
const auto direction = a_deltaPos / distance;
|
||||
const RE::hkVector4f velocity{
|
||||
direction.x * clampedSpeed,
|
||||
direction.y * clampedSpeed,
|
||||
direction.z * clampedSpeed,
|
||||
0.0F
|
||||
};
|
||||
a_charController.SetLinearVelocityImpl(velocity);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -567,18 +598,7 @@ namespace
|
||||
if (distanceDrift > 0.001F) {
|
||||
auto* charController = a_proxy.Move(0.016F, deltaPos, false);
|
||||
if (charController) {
|
||||
// Scale delta to units-per-second so the controller sees walking velocity
|
||||
const float speed = distanceDrift / 0.016F;
|
||||
constexpr float MAX_INJECT_SPEED = 400.0F;
|
||||
const float clampedSpeed = speed > MAX_INJECT_SPEED ? MAX_INJECT_SPEED : speed;
|
||||
const RE::NiPoint3 dir = distanceDrift > 0.0F ? (deltaPos / distanceDrift) : RE::NiPoint3{};
|
||||
const RE::hkVector4f vel{
|
||||
dir.x * clampedSpeed,
|
||||
dir.y * clampedSpeed,
|
||||
dir.z * clampedSpeed,
|
||||
0.0F
|
||||
};
|
||||
charController->SetLinearVelocityImpl(vel);
|
||||
InjectProxyMovementVelocity(*charController, deltaPos, 0.016F);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,30 +732,6 @@ namespace
|
||||
auto packageTypeForLog = -1;
|
||||
|
||||
if (auto* process = a_proxy.currentProcess) {
|
||||
auto clearHandle = [&](auto& a_handle) {
|
||||
if (!a_handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
a_handle.reset();
|
||||
return true;
|
||||
};
|
||||
|
||||
processTargetsCleared |= clearHandle(process->target);
|
||||
processTargetsCleared |= clearHandle(process->followTarget);
|
||||
processTargetsCleared |= clearHandle(process->genericLocation);
|
||||
processTargetsCleared |= clearHandle(process->genericSecondLocation);
|
||||
processTargetsCleared |= clearHandle(process->patrolLocation);
|
||||
processTargetsCleared |= clearHandle(process->currentPackage.target);
|
||||
if (process->acquireObject) {
|
||||
process->acquireObject = nullptr;
|
||||
processTargetsCleared = true;
|
||||
}
|
||||
if (process->savedAcquireObject) {
|
||||
process->savedAcquireObject = nullptr;
|
||||
processTargetsCleared = true;
|
||||
}
|
||||
|
||||
const auto commandType = process->GetCommandType();
|
||||
if (commandType != RE::COMMAND_TYPE::kNone) {
|
||||
process->SetCommandType(RE::COMMAND_TYPE::kNone);
|
||||
@@ -1657,15 +1653,47 @@ namespace
|
||||
}
|
||||
|
||||
const auto debugPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
const auto wasDisabled = a_proxy.IsDisabled();
|
||||
const auto had3D = a_proxy.Get3D() != nullptr || a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.GetCurrent3D() != nullptr;
|
||||
const auto wasVisible = a_proxy.IsVisible();
|
||||
|
||||
if (wasDisabled) {
|
||||
a_proxy.Enable(false);
|
||||
}
|
||||
a_proxy.SetPosition(debugPosition, true);
|
||||
a_proxy.SetHeading(a_player.data.angle.z);
|
||||
a_proxy.SetScale(kDebugRuntimeProxyScale);
|
||||
a_proxy.SetAlpha(1.0F);
|
||||
a_proxy.UpdateAlpha();
|
||||
a_proxy.SetDisplayGeometry(true);
|
||||
a_proxy.Update3DPosition(true);
|
||||
a_proxy.MoveHavok(true);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kModel);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkin);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkeleton);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kInitDefault);
|
||||
if (auto* taskQueue = RE::TaskQueueInterface::GetSingleton()) {
|
||||
const auto updateFlags =
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kModel) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkin) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkeleton) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kInitDefault);
|
||||
taskQueue->QueueUpdate3D(std::addressof(a_proxy), updateFlags);
|
||||
}
|
||||
|
||||
const auto has3D = a_proxy.Get3D() != nullptr || a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.GetCurrent3D() != nullptr;
|
||||
const auto isVisible = a_proxy.IsVisible();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy debug visibility mode active: forcing runtime proxy near local player. debugPosition={}, scale={:.2f}.",
|
||||
"Runtime proxy debug visibility mode active: forcing runtime proxy near local player. debugPosition={}, scale={:.2f}, wasDisabled={}, had3D={}, wasVisible={}, nowDisabled={}, has3D={}, isVisible={}.",
|
||||
FormatPosition(debugPosition),
|
||||
kDebugRuntimeProxyScale));
|
||||
kDebugRuntimeProxyScale,
|
||||
wasDisabled,
|
||||
had3D,
|
||||
wasVisible,
|
||||
a_proxy.IsDisabled(),
|
||||
has3D,
|
||||
isVisible));
|
||||
}
|
||||
|
||||
struct RuntimeProxyVisibilityHoldTiming
|
||||
@@ -2203,32 +2231,19 @@ namespace
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Phase 4: Dynamic spawning implementation
|
||||
// NOTE: Disabled for Phase 6 testing - API verification needed
|
||||
// TODO: Phase 7 - Fix CommonLibF4 API calls for dynamic spawning
|
||||
//
|
||||
// Original code disabled - API calls need verification
|
||||
/*
|
||||
RE::TESNPC* proxyBase = nullptr;
|
||||
|
||||
// First try to find custom proxy base in ESP
|
||||
const auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||
if (!dataHandler) {
|
||||
LogWarningWithLocalPlayerPrefix(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy; data handler unavailable.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Try to load custom proxy base (F4T_RemotePlayerProxy)
|
||||
const auto customFile = dataHandler->LookupLoadedModByName(kTestPluginName);
|
||||
if (customFile && customFile->compileIndex != 0xFF) {
|
||||
const auto customBaseFormId = (customFile->compileIndex << 24) | kProxyBaseActorLocalFormId;
|
||||
proxyBase = RE::TESForm::LookupByID<RE::TESNPC>(customBaseFormId);
|
||||
}
|
||||
|
||||
// Fallback to Codsworth if custom proxy not available
|
||||
auto* proxyBase = ResolveProxyActorBase();
|
||||
if (!proxyBase) {
|
||||
proxyBase = RE::TESForm::LookupByID<RE::TESNPC>(kDebugPlaceAtMeActorBaseFormId);
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Custom proxy base unavailable for remote player {}; trying Codsworth diagnostic base.",
|
||||
a_remotePlayerId));
|
||||
proxyBase = ResolveCodsworthBaseByAbsoluteFormId();
|
||||
}
|
||||
|
||||
if (!proxyBase) {
|
||||
@@ -2237,8 +2252,76 @@ namespace
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
*/
|
||||
return nullptr; // Phase 6: Disabled - API verification needed for Phase 7
|
||||
|
||||
auto* boundObject = proxyBase->As<RE::TESBoundObject>();
|
||||
if (!boundObject) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy for remote player {}; actor base {:08X} is not a TESBoundObject.",
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* parentCell = a_player.GetParentCell();
|
||||
if (!parentCell) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy for remote player {}; player parent cell unavailable.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto spawnPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
RE::NiPoint3 spawnDirection{};
|
||||
spawnDirection.z = a_player.data.angle.z;
|
||||
|
||||
RE::NEW_REFR_DATA spawnData;
|
||||
spawnData.object = boundObject;
|
||||
spawnData.location = spawnPosition;
|
||||
spawnData.direction = spawnDirection;
|
||||
spawnData.interior = parentCell;
|
||||
spawnData.world = nullptr;
|
||||
spawnData.reference = nullptr;
|
||||
spawnData.forcePersist = false;
|
||||
spawnData.initializeScripts = true;
|
||||
spawnData.initiallyDisabled = false;
|
||||
|
||||
const auto spawnedHandle = dataHandler->CreateReferenceAtLocation(spawnData);
|
||||
if (!spawnedHandle) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: CreateReferenceAtLocation failed for remote player {} with base {:08X}.",
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto spawnedRef = spawnedHandle.get();
|
||||
auto* spawnedActor = spawnedRef ? spawnedRef->As<RE::Actor>() : nullptr;
|
||||
if (!spawnedActor) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Created reference for remote player {} did not resolve as actor.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (std::addressof(*spawnedActor) == std::addressof(a_player)) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Created reference {:08X} unexpectedly resolved as the local player.",
|
||||
spawnedActor->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ApplyRuntimeProxyDebugVisibility(*spawnedActor, a_player);
|
||||
|
||||
g_dynamicProxyPool[a_remotePlayerId] = spawnedHandle;
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Spawned dynamic proxy actor {:08X} for remote player {} with base {:08X} at {}.",
|
||||
spawnedActor->GetFormID(),
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID(),
|
||||
FormatPosition(spawnPosition)));
|
||||
|
||||
return spawnedActor;
|
||||
}
|
||||
|
||||
// Phase 4.1: Retrieve or spawn proxy for a remote player
|
||||
@@ -3279,6 +3362,41 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
};
|
||||
}
|
||||
|
||||
RE::NiPoint3 GetRuntimeProxyVisibleTargetPosition(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if constexpr (!kDebugForceRuntimeProxyNearLocalPlayer) {
|
||||
return GetRemotePlayerPosition(a_remotePlayer);
|
||||
}
|
||||
|
||||
if (!a_slot.visibleDebugAnchorPosition) {
|
||||
a_slot.visibleDebugAnchorPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy visible debug anchor captured for remote player {}: anchor={}.",
|
||||
a_remotePlayer.playerId,
|
||||
FormatPosition(*a_slot.visibleDebugAnchorPosition)));
|
||||
}
|
||||
const auto debugBasePosition = *a_slot.visibleDebugAnchorPosition;
|
||||
if (!a_slot.initialRemoteSpawnPosition) {
|
||||
a_slot.initialRemoteSpawnPosition = GetRemotePlayerPosition(a_remotePlayer);
|
||||
a_slot.initialRemoteSpawnHeading = a_remotePlayer.angleZ;
|
||||
return debugBasePosition;
|
||||
}
|
||||
|
||||
auto remoteDelta = GetRemotePlayerPosition(a_remotePlayer) - *a_slot.initialRemoteSpawnPosition;
|
||||
const auto remoteDeltaLength = remoteDelta.Length();
|
||||
if (remoteDeltaLength > 0.001F) {
|
||||
constexpr auto kVisibleRemoteDeltaScale = 0.35F;
|
||||
constexpr auto kVisibleRemoteDeltaMax = 250.0F;
|
||||
const auto scaledLength = std::min(remoteDeltaLength * kVisibleRemoteDeltaScale, kVisibleRemoteDeltaMax);
|
||||
remoteDelta = (remoteDelta / remoteDeltaLength) * scaledLength;
|
||||
}
|
||||
|
||||
return debugBasePosition + remoteDelta;
|
||||
}
|
||||
|
||||
void SetSlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
a_slot.initialRemoteSpawnPosition = GetRemotePlayerPosition(a_remotePlayer);
|
||||
@@ -4387,7 +4505,15 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (!a_slot.proxyHandle) {
|
||||
// Phase 4: Try dynamic spawn first (new model)
|
||||
// Phase 6 validation: prefer pre-placed actors because Creation Kit refs
|
||||
// already have loaded 3D, collision, AI process, and animation state.
|
||||
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
||||
a_slot.proxyHandle = pooledProxy->GetHandle();
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
||||
pooledProxy->GetFormID(),
|
||||
a_slot.remotePlayerId));
|
||||
} else
|
||||
if (g_dynamicProxyPoolEnabled) {
|
||||
if (auto* dynamicProxy = GetOrSpawnDynamicProxy(const_cast<RE::PlayerCharacter&>(a_player), a_slot.remotePlayerId)) {
|
||||
a_slot.proxyHandle = dynamicProxy->GetHandle();
|
||||
@@ -4397,15 +4523,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.remotePlayerId));
|
||||
return dynamicProxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to grab a proxy from the pre-placed pool
|
||||
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
||||
a_slot.proxyHandle = pooledProxy->GetHandle();
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
||||
pooledProxy->GetFormID(),
|
||||
a_slot.remotePlayerId));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -4670,10 +4787,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const RE::PlayerCharacter& a_player,
|
||||
bool a_sameCellPassed)
|
||||
{
|
||||
RE::NiPoint3 targetPosition{};
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
||||
|
||||
ApplyRuntimeProxyTransform(a_proxy, targetPosition, a_remotePlayer.angleZ, a_remotePlayer, true, &a_slot);
|
||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||
@@ -4685,9 +4799,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.remoteStateMovedLogged = false;
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at visible X={:.2f}, Y={:.2f}, Z={:.2f} from remote X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.playerId,
|
||||
targetPosition.x,
|
||||
targetPosition.y,
|
||||
targetPosition.z,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z));
|
||||
@@ -4831,10 +4948,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_proxy.GetFormID()));
|
||||
}
|
||||
|
||||
RE::NiPoint3 targetPosition;
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
||||
|
||||
// Store latest target for smooth frame movement
|
||||
a_slot.targetPosition = targetPosition;
|
||||
@@ -4953,6 +5067,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
}
|
||||
slotValue.previousRemotePlayerId = previousRemotePlayerId;
|
||||
slotValue.remotePlayerId = a_remotePlayer.playerId;
|
||||
slotValue.visibleDebugAnchorPosition.reset();
|
||||
slotValue.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
|
||||
slotValue.holdReason.reset();
|
||||
slotValue.heldSinceTime = {};
|
||||
@@ -5194,17 +5309,23 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
continue;
|
||||
}
|
||||
|
||||
proxy->SetHeading(slot.targetHeading);
|
||||
|
||||
// Use Move() to smoothly approach target via character controller
|
||||
// This lets the AI/character controller drive animations naturally
|
||||
constexpr float MAX_FRAME_DELTA = 100.0F; // Max movement per frame
|
||||
constexpr float kFrameDeltaSeconds = 0.016F;
|
||||
constexpr float MAX_FRAME_DELTA = 12.0F; // Max movement per frame
|
||||
RE::NiPoint3 frameDelta = deltaToTarget;
|
||||
if (distanceToTarget > MAX_FRAME_DELTA) {
|
||||
frameDelta = (deltaToTarget / distanceToTarget) * MAX_FRAME_DELTA;
|
||||
}
|
||||
|
||||
// Call Move() with deferred=true to integrate movement naturally
|
||||
// Call Move() through the character controller and publish a velocity
|
||||
// sample so the graph can blend into locomotion.
|
||||
// This should update velocity in the character controller for animations
|
||||
proxy->Move(0.016F, frameDelta, true);
|
||||
if (auto* charController = proxy->Move(kFrameDeltaSeconds, frameDelta, false)) {
|
||||
InjectProxyMovementVelocity(*charController, frameDelta, kFrameDeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5325,6 +5446,8 @@ namespace F4T::ProxyActorController
|
||||
*player,
|
||||
fallbackCandidateSameCellPassed);
|
||||
}
|
||||
|
||||
ApplySmoothFrameMovement();
|
||||
}
|
||||
|
||||
void UpdateSafetyTest()
|
||||
|
||||
Reference in New Issue
Block a user