mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
3rd person camera (#3)
* Add feasibility plan for reusing multiplayer animation system for third-person camera Evaluates reusing the multiplayer extension's RemotePlayer animation system (BuildROIMap, AssignROIIndices, ApplyAnimationTransformation) for the local player to enable a third-person camera mode. Conclusion: feasible with only 3 single-line extension hooks added to core game code. https://claude.ai/code/session_01NC3zdQZ4nqEcYjyvStqcdD * WIP: Third-person camera with animation reuse and movement fix * Fix third-person camera bugs: vehicles, remote facing, emote distortion (#2) - Fix spawn pose and building re-entry by applying idle frame 0 and reinitializing on world enable - Handle vehicle transitions: ride animations for small vehicles, first-person fallback for large vehicles and helicopter - Keep vehicle dashboards visible for exit controls - Disable third-person camera for large vehicles, fix ROI cleanup - Move HandleActorExit hook to end of Exit() for immediate reinit - Fix remote player facing 180 degrees wrong by negating direction in BroadcastLocalState when third-person camera is active - Fix Hat Tip emote distortion from compounding transform scale by saving clean parent transform at emote start and restoring after each frame's animation application * DRY cleanup for third-person camera branch - Extract shared DetectVehicleType() to protocol.h/cpp (was duplicated in ThirdPersonCamera and NetworkManager) - Remove no-op HandlePostApplyTransform hook chain (called every frame for every LegoPathActor but did nothing) - Add ThirdPersonCamera::ClearAnimCaches() helper (pattern repeated 5x) - Add AnimUtils::EnsureROIMapVisibility() inline helper (loop repeated 5x across ThirdPersonCamera and RemotePlayer) - Remove redundant static_cast in multiplayer.cpp (UserActor() already returns LegoPathActor*) - Delete THIRD_PERSON_CAMERA_ANIMATION_REUSE_PLAN.md development artifact
This commit is contained in:
parent
0997610bad
commit
37b328a595
@ -532,9 +532,12 @@ if (ISLE_EXTENSIONS)
|
||||
extensions/src/siloader.cpp
|
||||
extensions/src/textureloader.cpp
|
||||
extensions/src/multiplayer.cpp
|
||||
extensions/src/multiplayer/animutils.cpp
|
||||
extensions/src/multiplayer/charactercloner.cpp
|
||||
extensions/src/multiplayer/networkmanager.cpp
|
||||
extensions/src/multiplayer/protocol.cpp
|
||||
extensions/src/multiplayer/remoteplayer.cpp
|
||||
extensions/src/multiplayer/thirdpersoncamera.cpp
|
||||
extensions/src/multiplayer/worldstatesync.cpp
|
||||
)
|
||||
if(EMSCRIPTEN)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "islepathactor.h"
|
||||
|
||||
#include "3dmanager/lego3dmanager.h"
|
||||
#include "extensions/multiplayer.h"
|
||||
#include "isle_actions.h"
|
||||
#include "jukebox_actions.h"
|
||||
#include "legoanimationmanager.h"
|
||||
@ -16,6 +17,8 @@
|
||||
#include "scripts.h"
|
||||
#include "viewmanager/viewmanager.h"
|
||||
|
||||
using namespace Extensions;
|
||||
|
||||
DECOMP_SIZE_ASSERT(IslePathActor, 0x160)
|
||||
DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38)
|
||||
|
||||
@ -95,6 +98,8 @@ void IslePathActor::Enter()
|
||||
TurnAround();
|
||||
TransformPointOfView();
|
||||
}
|
||||
|
||||
Extension<MultiplayerExt>::Call(HandleActorEnter, this);
|
||||
}
|
||||
|
||||
// FUNCTION: LEGO1 0x1001a3f0
|
||||
@ -154,6 +159,8 @@ void IslePathActor::Exit()
|
||||
TurnAround();
|
||||
TransformPointOfView();
|
||||
ResetViewVelocity();
|
||||
|
||||
Extension<MultiplayerExt>::Call(HandleActorExit, this);
|
||||
}
|
||||
|
||||
// GLOBAL: LEGO1 0x10102b28
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "legopathactor.h"
|
||||
|
||||
#include "define.h"
|
||||
#include "extensions/multiplayer.h"
|
||||
#include "geom/legoorientededge.h"
|
||||
#include "legocachesoundmanager.h"
|
||||
#include "legocameracontroller.h"
|
||||
@ -20,6 +21,8 @@
|
||||
#include <mxdebug.h>
|
||||
#include <vec.h>
|
||||
|
||||
using namespace Extensions;
|
||||
|
||||
DECOMP_SIZE_ASSERT(LegoPathActor, 0x154)
|
||||
DECOMP_SIZE_ASSERT(LegoPathEdgeContainer, 0x3c)
|
||||
|
||||
@ -262,6 +265,11 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform)
|
||||
|
||||
m_worldSpeed = nav->GetLinearVel();
|
||||
|
||||
MxBool invertDir = Extension<MultiplayerExt>::Call(ShouldInvertMovement, this).value_or(FALSE);
|
||||
if (invertDir) {
|
||||
dir *= -1.0f;
|
||||
}
|
||||
|
||||
if (nav->CalculateNewPosDir(pos, dir, newPos, newDir, m_boundary->GetUp())) {
|
||||
Mx3DPointFloat newPosCopy;
|
||||
newPosCopy = newPos;
|
||||
@ -321,6 +329,10 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform)
|
||||
}
|
||||
}
|
||||
|
||||
if (invertDir) {
|
||||
newDir *= -1.0f;
|
||||
}
|
||||
|
||||
p_transform.SetIdentity();
|
||||
|
||||
Vector3 right(p_transform[0]);
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace Extensions
|
||||
{
|
||||
@ -17,14 +18,26 @@ LEGO1_EXPORT void Enable(const char* p_key, std::map<std::string, std::string> p
|
||||
template <typename T>
|
||||
struct Extension {
|
||||
template <typename Function, typename... Args>
|
||||
static auto Call(Function&& function, Args&&... args) -> std::optional<std::invoke_result_t<Function, Args...>>
|
||||
static auto Call(Function&& function, Args&&... args)
|
||||
{
|
||||
using result_t = std::invoke_result_t<Function, Args...>;
|
||||
if constexpr (std::is_void_v<result_t>) {
|
||||
#ifdef EXTENSIONS
|
||||
if (T::enabled) {
|
||||
return std::invoke(std::forward<Function>(function), std::forward<Args>(args)...);
|
||||
}
|
||||
if (T::enabled) {
|
||||
std::invoke(std::forward<Function>(function), std::forward<Args>(args)...);
|
||||
}
|
||||
#endif
|
||||
return std::nullopt;
|
||||
}
|
||||
else {
|
||||
#ifdef EXTENSIONS
|
||||
if (T::enabled) {
|
||||
return std::optional<result_t>(
|
||||
std::invoke(std::forward<Function>(function), std::forward<Args>(args)...)
|
||||
);
|
||||
}
|
||||
#endif
|
||||
return std::optional<result_t>(std::nullopt);
|
||||
}
|
||||
}
|
||||
};
|
||||
}; // namespace Extensions
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class IslePathActor;
|
||||
class LegoEntity;
|
||||
class LegoPathActor;
|
||||
class LegoWorld;
|
||||
|
||||
namespace Multiplayer
|
||||
@ -34,6 +36,10 @@ class MultiplayerExt {
|
||||
static std::string relayUrl;
|
||||
static std::string room;
|
||||
|
||||
static void HandleActorEnter(IslePathActor* p_actor);
|
||||
static void HandleActorExit(IslePathActor* p_actor);
|
||||
static MxBool ShouldInvertMovement(LegoPathActor* p_actor);
|
||||
|
||||
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
||||
static MxBool CheckRejected();
|
||||
|
||||
@ -51,10 +57,16 @@ LEGO1_EXPORT bool IsMultiplayerRejected();
|
||||
|
||||
constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
|
||||
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
|
||||
constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter;
|
||||
constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit;
|
||||
constexpr auto ShouldInvertMovement = &MultiplayerExt::ShouldInvertMovement;
|
||||
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
||||
#else
|
||||
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::ShouldInvertMovement) ShouldInvertMovement = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
||||
#endif
|
||||
|
||||
|
||||
82
extensions/include/extensions/multiplayer/animutils.h
Normal file
82
extensions/include/extensions/multiplayer/animutils.h
Normal file
@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "mxtypes.h"
|
||||
#include "roi/legoroi.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class LegoAnim;
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
namespace AnimUtils
|
||||
{
|
||||
|
||||
// Cached ROI map entry for an animation
|
||||
struct AnimCache {
|
||||
LegoAnim* anim;
|
||||
LegoROI** roiMap;
|
||||
MxU32 roiMapSize;
|
||||
|
||||
AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {}
|
||||
~AnimCache()
|
||||
{
|
||||
if (roiMap) {
|
||||
delete[] roiMap;
|
||||
}
|
||||
}
|
||||
|
||||
AnimCache(const AnimCache&) = delete;
|
||||
AnimCache& operator=(const AnimCache&) = delete;
|
||||
AnimCache(AnimCache&& p_other) noexcept
|
||||
: anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize)
|
||||
{
|
||||
p_other.roiMap = nullptr;
|
||||
p_other.roiMapSize = 0;
|
||||
p_other.anim = nullptr;
|
||||
}
|
||||
AnimCache& operator=(AnimCache&& p_other) noexcept
|
||||
{
|
||||
if (this != &p_other) {
|
||||
if (roiMap) {
|
||||
delete[] roiMap;
|
||||
}
|
||||
anim = p_other.anim;
|
||||
roiMap = p_other.roiMap;
|
||||
roiMapSize = p_other.roiMapSize;
|
||||
p_other.roiMap = nullptr;
|
||||
p_other.roiMapSize = 0;
|
||||
p_other.anim = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
void BuildROIMap(
|
||||
LegoAnim* p_anim,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
LegoROI**& p_roiMap,
|
||||
MxU32& p_roiMapSize
|
||||
);
|
||||
|
||||
AnimCache* GetOrBuildAnimCache(
|
||||
std::map<std::string, AnimCache>& p_cacheMap,
|
||||
LegoROI* p_roi,
|
||||
const char* p_animName
|
||||
);
|
||||
|
||||
inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize)
|
||||
{
|
||||
for (MxU32 i = 1; i < p_roiMapSize; i++) {
|
||||
if (p_roiMap[i] != nullptr) {
|
||||
p_roiMap[i]->SetVisibility(TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace AnimUtils
|
||||
|
||||
} // namespace Multiplayer
|
||||
@ -4,6 +4,7 @@
|
||||
#include "extensions/multiplayer/platformcallbacks.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "extensions/multiplayer/remoteplayer.h"
|
||||
#include "extensions/multiplayer/thirdpersoncamera.h"
|
||||
#include "extensions/multiplayer/worldstatesync.h"
|
||||
#include "mxcore.h"
|
||||
#include "mxtypes.h"
|
||||
@ -49,6 +50,8 @@ class NetworkManager : public MxCore {
|
||||
void OnWorldEnabled(LegoWorld* p_world);
|
||||
void OnWorldDisabled(LegoWorld* p_world);
|
||||
|
||||
ThirdPersonCamera& GetThirdPersonCamera() { return m_thirdPersonCamera; }
|
||||
|
||||
// Called from multiplayer extension when a plant/building entity is clicked.
|
||||
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
||||
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
||||
@ -72,7 +75,6 @@ class NetworkManager : public MxCore {
|
||||
void RemoveAllRemotePlayers();
|
||||
|
||||
void NotifyPlayerCountChanged();
|
||||
int8_t DetectLocalVehicleType();
|
||||
|
||||
// Serialize and send a fixed-size message via the transport
|
||||
template <typename T>
|
||||
@ -81,6 +83,7 @@ class NetworkManager : public MxCore {
|
||||
NetworkTransport* m_transport;
|
||||
PlatformCallbacks* m_callbacks;
|
||||
WorldStateSync m_worldSync;
|
||||
ThirdPersonCamera m_thirdPersonCamera;
|
||||
std::map<uint32_t, std::unique_ptr<RemotePlayer>> m_remotePlayers;
|
||||
|
||||
uint32_t m_localPeerId;
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
class LegoPathActor;
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
@ -127,31 +129,25 @@ struct EmoteMsg {
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// Walk animation table: index -> CNs name
|
||||
static const char* const g_walkAnimNames[] = {
|
||||
"CNs001xx", // 0: Normal (default)
|
||||
"CNs002xx", // 1: Joyful
|
||||
"CNs003xx", // 2: Gloomy
|
||||
"CNs005xx", // 3: Leaning
|
||||
"CNs006xx", // 4: Scared
|
||||
"CNs007xx", // 5: Hyper
|
||||
};
|
||||
static const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
||||
// Animation and vehicle tables (defined in protocol.cpp)
|
||||
extern const char* const g_walkAnimNames[];
|
||||
extern const int g_walkAnimCount;
|
||||
|
||||
// Idle animation table: index -> CNs name
|
||||
static const char* const g_idleAnimNames[] = {
|
||||
"CNs008xx", // 0: Sway (default)
|
||||
"CNs009xx", // 1: Groove
|
||||
"CNs010xx", // 2: Excited
|
||||
};
|
||||
static const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
||||
extern const char* const g_idleAnimNames[];
|
||||
extern const int g_idleAnimCount;
|
||||
|
||||
// Emote table: index -> CNs name
|
||||
static const char* const g_emoteAnimNames[] = {
|
||||
"CNs011xx", // 0: Wave
|
||||
"CNs012xx", // 1: Hat Tip
|
||||
};
|
||||
static const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]);
|
||||
extern const char* const g_emoteAnimNames[];
|
||||
extern const int g_emoteAnimCount;
|
||||
|
||||
extern const char* const g_vehicleROINames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideAnimNames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideVehicleROINames[VEHICLE_COUNT];
|
||||
|
||||
// Returns true if the vehicle type has no ride animation (model swap instead)
|
||||
bool IsLargeVehicle(int8_t p_vehicleType);
|
||||
|
||||
// Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor);
|
||||
|
||||
// Validate actorId is a playable character (1-5, not brickster)
|
||||
inline bool IsValidActorId(uint8_t p_actorId)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "extensions/multiplayer/animutils.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
@ -37,53 +38,8 @@ class RemotePlayer {
|
||||
void TriggerEmote(uint8_t p_emoteId);
|
||||
|
||||
private:
|
||||
// Cached ROI map entry for an animation
|
||||
struct AnimCache {
|
||||
LegoAnim* anim;
|
||||
LegoROI** roiMap;
|
||||
MxU32 roiMapSize;
|
||||
using AnimCache = AnimUtils::AnimCache;
|
||||
|
||||
AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {}
|
||||
~AnimCache()
|
||||
{
|
||||
if (roiMap) {
|
||||
delete[] roiMap;
|
||||
}
|
||||
}
|
||||
|
||||
AnimCache(const AnimCache&) = delete;
|
||||
AnimCache& operator=(const AnimCache&) = delete;
|
||||
AnimCache(AnimCache&& p_other) noexcept
|
||||
: anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize)
|
||||
{
|
||||
p_other.roiMap = nullptr;
|
||||
p_other.roiMapSize = 0;
|
||||
p_other.anim = nullptr;
|
||||
}
|
||||
AnimCache& operator=(AnimCache&& p_other) noexcept
|
||||
{
|
||||
if (this != &p_other) {
|
||||
if (roiMap) {
|
||||
delete[] roiMap;
|
||||
}
|
||||
anim = p_other.anim;
|
||||
roiMap = p_other.roiMap;
|
||||
roiMapSize = p_other.roiMapSize;
|
||||
p_other.roiMap = nullptr;
|
||||
p_other.roiMapSize = 0;
|
||||
p_other.anim = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
void BuildROIMap(
|
||||
LegoAnim* p_anim,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
LegoROI**& p_roiMap,
|
||||
MxU32& p_roiMapSize
|
||||
);
|
||||
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
||||
void UpdateTransform(float p_deltaTime);
|
||||
void UpdateAnimation(float p_deltaTime);
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "extensions/multiplayer/animutils.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "mxgeometry/mxmatrix.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class IslePathActor;
|
||||
class LegoPathActor;
|
||||
class LegoROI;
|
||||
class LegoWorld;
|
||||
class LegoAnim;
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
class ThirdPersonCamera {
|
||||
public:
|
||||
ThirdPersonCamera();
|
||||
|
||||
void Enable();
|
||||
void Disable();
|
||||
bool IsEnabled() const { return m_enabled; }
|
||||
bool IsActive() const { return m_active; }
|
||||
|
||||
// Core hooks
|
||||
void OnActorEnter(IslePathActor* p_actor);
|
||||
void OnActorExit(IslePathActor* p_actor);
|
||||
|
||||
// Called every frame from NetworkManager::Tickle()
|
||||
void Tick(float p_deltaTime);
|
||||
|
||||
// Animation selection (forwarded from NetworkManager)
|
||||
void SetWalkAnimId(uint8_t p_id);
|
||||
void SetIdleAnimId(uint8_t p_id);
|
||||
void TriggerEmote(uint8_t p_emoteId);
|
||||
|
||||
void OnWorldEnabled(LegoWorld* p_world);
|
||||
void OnWorldDisabled(LegoWorld* p_world);
|
||||
|
||||
private:
|
||||
using AnimCache = AnimUtils::AnimCache;
|
||||
|
||||
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
||||
void ClearAnimCaches();
|
||||
void SetupCamera(LegoPathActor* p_actor);
|
||||
void BuildRideAnimation(int8_t p_vehicleType);
|
||||
void ClearRideAnimation();
|
||||
void ApplyIdleFrame0();
|
||||
void ReinitForCharacter();
|
||||
|
||||
bool m_enabled;
|
||||
bool m_active;
|
||||
LegoROI* m_playerROI; // Borrowed, not owned
|
||||
|
||||
// Walk/idle state (same pattern as RemotePlayer)
|
||||
uint8_t m_walkAnimId;
|
||||
uint8_t m_idleAnimId;
|
||||
AnimCache* m_walkAnimCache;
|
||||
AnimCache* m_idleAnimCache;
|
||||
float m_animTime;
|
||||
float m_idleTime;
|
||||
float m_idleAnimTime;
|
||||
bool m_wasMoving;
|
||||
|
||||
// Emote state
|
||||
AnimCache* m_emoteAnimCache;
|
||||
float m_emoteTime;
|
||||
float m_emoteDuration;
|
||||
bool m_emoteActive;
|
||||
MxMatrix m_emoteParentTransform;
|
||||
|
||||
// Vehicle ride state
|
||||
int8_t m_currentVehicleType;
|
||||
LegoAnim* m_rideAnim;
|
||||
LegoROI** m_rideRoiMap;
|
||||
MxU32 m_rideRoiMapSize;
|
||||
LegoROI* m_rideVehicleROI;
|
||||
|
||||
std::map<std::string, AnimCache> m_animCacheMap;
|
||||
};
|
||||
|
||||
} // namespace Multiplayer
|
||||
@ -4,9 +4,11 @@
|
||||
#include "extensions/multiplayer/networkmanager.h"
|
||||
#include "extensions/multiplayer/networktransport.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "islepathactor.h"
|
||||
#include "legoactor.h"
|
||||
#include "legoentity.h"
|
||||
#include "legogamestate.h"
|
||||
#include "legopathactor.h"
|
||||
#include "misc.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
@ -31,10 +33,6 @@ void MultiplayerExt::Initialize()
|
||||
relayUrl = options["multiplayer:relay url"];
|
||||
room = options["multiplayer:room"];
|
||||
|
||||
if (relayUrl.empty() || room.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
s_transport = new Multiplayer::WebSocketTransport(relayUrl);
|
||||
s_callbacks = new Multiplayer::EmscriptenCallbacks();
|
||||
@ -42,7 +40,12 @@ void MultiplayerExt::Initialize()
|
||||
s_networkManager = new Multiplayer::NetworkManager();
|
||||
s_networkManager->Initialize(s_transport, s_callbacks);
|
||||
|
||||
s_networkManager->Connect(room.c_str());
|
||||
// Third-person camera enabled by default, toggled via WASM export
|
||||
s_networkManager->GetThirdPersonCamera().Enable();
|
||||
|
||||
if (!relayUrl.empty() && !room.empty()) {
|
||||
s_networkManager->Connect(room.c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -107,6 +110,29 @@ MxBool MultiplayerExt::HandleEntityNotify(LegoEntity* p_entity)
|
||||
return s_networkManager->HandleEntityMutation(p_entity, changeType);
|
||||
}
|
||||
|
||||
void MultiplayerExt::HandleActorEnter(IslePathActor* p_actor)
|
||||
{
|
||||
if (s_networkManager) {
|
||||
s_networkManager->GetThirdPersonCamera().OnActorEnter(p_actor);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiplayerExt::HandleActorExit(IslePathActor* p_actor)
|
||||
{
|
||||
if (s_networkManager) {
|
||||
s_networkManager->GetThirdPersonCamera().OnActorExit(p_actor);
|
||||
}
|
||||
}
|
||||
|
||||
MxBool MultiplayerExt::ShouldInvertMovement(LegoPathActor* p_actor)
|
||||
{
|
||||
if (s_networkManager && UserActor() == p_actor) {
|
||||
return s_networkManager->GetThirdPersonCamera().IsActive();
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
MxBool MultiplayerExt::CheckRejected()
|
||||
{
|
||||
if (s_networkManager && s_networkManager->WasRejected()) {
|
||||
|
||||
130
extensions/src/multiplayer/animutils.cpp
Normal file
130
extensions/src/multiplayer/animutils.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
#include "extensions/multiplayer/animutils.h"
|
||||
|
||||
#include "anim/legoanim.h"
|
||||
#include "legoanimpresenter.h"
|
||||
#include "legoworld.h"
|
||||
#include "misc.h"
|
||||
#include "misc/legotree.h"
|
||||
#include "roi/legoroi.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace Multiplayer;
|
||||
|
||||
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
|
||||
// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
|
||||
static void AssignROIIndices(
|
||||
LegoTreeNode* p_node,
|
||||
LegoROI* p_parentROI,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
MxU32& p_nextIndex,
|
||||
std::vector<LegoROI*>& p_entries
|
||||
)
|
||||
{
|
||||
LegoROI* roi = p_parentROI;
|
||||
LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData();
|
||||
const char* name = data ? data->GetName() : nullptr;
|
||||
|
||||
if (name != nullptr && *name != '-') {
|
||||
LegoROI* matchedROI = nullptr;
|
||||
|
||||
if (*name == '*' || p_parentROI == nullptr) {
|
||||
roi = p_rootROI;
|
||||
matchedROI = p_rootROI;
|
||||
}
|
||||
else {
|
||||
matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
|
||||
if (matchedROI == nullptr && p_extraROI != nullptr) {
|
||||
matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedROI != nullptr) {
|
||||
data->SetROIIndex(p_nextIndex);
|
||||
p_entries.push_back(matchedROI);
|
||||
p_nextIndex++;
|
||||
}
|
||||
else {
|
||||
data->SetROIIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) {
|
||||
AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimUtils::BuildROIMap(
|
||||
LegoAnim* p_anim,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
LegoROI**& p_roiMap,
|
||||
MxU32& p_roiMapSize
|
||||
)
|
||||
{
|
||||
if (!p_anim || !p_rootROI) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoTreeNode* root = p_anim->GetRoot();
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
MxU32 nextIndex = 1;
|
||||
std::vector<LegoROI*> entries;
|
||||
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
|
||||
|
||||
if (entries.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1-indexed; index 0 reserved as NULL
|
||||
p_roiMapSize = entries.size() + 1;
|
||||
p_roiMap = new LegoROI*[p_roiMapSize];
|
||||
p_roiMap[0] = nullptr;
|
||||
for (MxU32 i = 0; i < entries.size(); i++) {
|
||||
p_roiMap[i + 1] = entries[i];
|
||||
}
|
||||
}
|
||||
|
||||
AnimUtils::AnimCache* AnimUtils::GetOrBuildAnimCache(
|
||||
std::map<std::string, AnimCache>& p_cacheMap,
|
||||
LegoROI* p_roi,
|
||||
const char* p_animName
|
||||
)
|
||||
{
|
||||
if (!p_animName || !p_roi) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if already cached
|
||||
auto it = p_cacheMap.find(p_animName);
|
||||
if (it != p_cacheMap.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
// Look up the animation presenter in the current world
|
||||
LegoWorld* world = CurrentWorld();
|
||||
if (!world) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MxCore* presenter = world->Find("LegoAnimPresenter", p_animName);
|
||||
if (!presenter) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LegoAnim* anim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
|
||||
if (!anim) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Build and cache
|
||||
AnimCache& cache = p_cacheMap[p_animName];
|
||||
cache.anim = anim;
|
||||
BuildROIMap(anim, p_roi, nullptr, cache.roiMap, cache.roiMapSize);
|
||||
|
||||
return &cache;
|
||||
}
|
||||
@ -42,6 +42,8 @@ NetworkManager::~NetworkManager()
|
||||
|
||||
MxResult NetworkManager::Tickle()
|
||||
{
|
||||
m_thirdPersonCamera.Tick(0.016f);
|
||||
|
||||
if (!m_transport) {
|
||||
return SUCCESS;
|
||||
}
|
||||
@ -134,6 +136,8 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
||||
m_registered = true;
|
||||
}
|
||||
|
||||
m_thirdPersonCamera.OnWorldEnabled(p_world);
|
||||
|
||||
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
||||
m_inIsleWorld = true;
|
||||
m_worldSync.SetInIsleWorld(true);
|
||||
@ -158,6 +162,8 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
|
||||
return;
|
||||
}
|
||||
|
||||
m_thirdPersonCamera.OnWorldDisabled(p_world);
|
||||
|
||||
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
||||
m_inIsleWorld = false;
|
||||
m_worldSync.SetInIsleWorld(false);
|
||||
@ -213,9 +219,19 @@ void NetworkManager::BroadcastLocalState()
|
||||
msg.header = {MSG_STATE, m_localPeerId, m_sequence++};
|
||||
msg.actorId = actorId;
|
||||
msg.worldId = (int8_t) currentWorld->GetWorldId();
|
||||
msg.vehicleType = DetectLocalVehicleType();
|
||||
msg.vehicleType = DetectVehicleType(userActor);
|
||||
SDL_memcpy(msg.position, pos, sizeof(msg.position));
|
||||
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
|
||||
|
||||
// Third-person camera: ROI direction is opposite to actual movement direction
|
||||
// (ShouldInvertMovement preserves TurnAround convention). Negate so remote
|
||||
// players receive the true movement-facing direction.
|
||||
if (m_thirdPersonCamera.IsActive()) {
|
||||
msg.direction[0] = -msg.direction[0];
|
||||
msg.direction[1] = -msg.direction[1];
|
||||
msg.direction[2] = -msg.direction[2];
|
||||
}
|
||||
|
||||
SDL_memcpy(msg.up, up, sizeof(msg.up));
|
||||
msg.speed = speed;
|
||||
msg.walkAnimId = m_localWalkAnimId;
|
||||
@ -407,6 +423,7 @@ void NetworkManager::SetWalkAnimation(uint8_t p_index)
|
||||
{
|
||||
if (p_index < g_walkAnimCount) {
|
||||
m_localWalkAnimId = p_index;
|
||||
m_thirdPersonCamera.SetWalkAnimId(p_index);
|
||||
}
|
||||
}
|
||||
|
||||
@ -414,6 +431,7 @@ void NetworkManager::SetIdleAnimation(uint8_t p_index)
|
||||
{
|
||||
if (p_index < g_idleAnimCount) {
|
||||
m_localIdleAnimId = p_index;
|
||||
m_thirdPersonCamera.SetIdleAnimId(p_index);
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,6 +441,8 @@ void NetworkManager::SendEmote(uint8_t p_emoteId)
|
||||
return;
|
||||
}
|
||||
|
||||
m_thirdPersonCamera.TriggerEmote(p_emoteId);
|
||||
|
||||
EmoteMsg msg{};
|
||||
msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++};
|
||||
msg.emoteId = p_emoteId;
|
||||
@ -476,31 +496,3 @@ void NetworkManager::NotifyPlayerCountChanged()
|
||||
m_callbacks->OnPlayerCountChanged(count);
|
||||
}
|
||||
|
||||
int8_t NetworkManager::DetectLocalVehicleType()
|
||||
{
|
||||
static const struct {
|
||||
const char* className;
|
||||
int8_t vehicleType;
|
||||
} vehicleMap[] = {
|
||||
{"Helicopter", VEHICLE_HELICOPTER},
|
||||
{"Jetski", VEHICLE_JETSKI},
|
||||
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
||||
{"Bike", VEHICLE_BIKE},
|
||||
{"SkateBoard", VEHICLE_SKATEBOARD},
|
||||
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
||||
{"TowTrack", VEHICLE_TOWTRACK},
|
||||
{"Ambulance", VEHICLE_AMBULANCE},
|
||||
};
|
||||
|
||||
LegoPathActor* actor = UserActor();
|
||||
if (!actor) {
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
for (const auto& entry : vehicleMap) {
|
||||
if (actor->IsA(entry.className)) {
|
||||
return entry.vehicleType;
|
||||
}
|
||||
}
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
@ -34,6 +34,20 @@ extern "C"
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void mp_toggle_third_person()
|
||||
{
|
||||
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||
if (mgr) {
|
||||
Multiplayer::ThirdPersonCamera& cam = mgr->GetThirdPersonCamera();
|
||||
if (cam.IsEnabled()) {
|
||||
cam.Disable();
|
||||
}
|
||||
else {
|
||||
cam.Enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif
|
||||
|
||||
79
extensions/src/multiplayer/protocol.cpp
Normal file
79
extensions/src/multiplayer/protocol.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
|
||||
#include "legopathactor.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
const char* const g_walkAnimNames[] = {
|
||||
"CNs001xx", // 0: Normal (default)
|
||||
"CNs002xx", // 1: Joyful
|
||||
"CNs003xx", // 2: Gloomy
|
||||
"CNs005xx", // 3: Leaning
|
||||
"CNs006xx", // 4: Scared
|
||||
"CNs007xx", // 5: Hyper
|
||||
};
|
||||
const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
||||
|
||||
const char* const g_idleAnimNames[] = {
|
||||
"CNs008xx", // 0: Sway (default)
|
||||
"CNs009xx", // 1: Groove
|
||||
"CNs010xx", // 2: Excited
|
||||
};
|
||||
const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
||||
|
||||
const char* const g_emoteAnimNames[] = {
|
||||
"CNs011xx", // 0: Wave
|
||||
"CNs012xx", // 1: Hat Tip
|
||||
};
|
||||
const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]);
|
||||
|
||||
// Vehicle model names (LOD names). The helicopter is a compound ROI ("copter")
|
||||
// with no standalone LOD; use its body part instead.
|
||||
const char* const g_vehicleROINames[VEHICLE_COUNT] =
|
||||
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
||||
|
||||
// Ride animation names for small vehicles (NULL = large vehicle, no ride anim)
|
||||
const char* const g_rideAnimNames[VEHICLE_COUNT] =
|
||||
{NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
||||
|
||||
// Vehicle variant ROI names used in ride animations
|
||||
const char* const g_rideVehicleROINames[VEHICLE_COUNT] =
|
||||
{NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
||||
|
||||
bool IsLargeVehicle(int8_t p_vehicleType)
|
||||
{
|
||||
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
||||
}
|
||||
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor)
|
||||
{
|
||||
static const struct {
|
||||
const char* className;
|
||||
int8_t vehicleType;
|
||||
} vehicleMap[] = {
|
||||
{"Helicopter", VEHICLE_HELICOPTER},
|
||||
{"Jetski", VEHICLE_JETSKI},
|
||||
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
||||
{"Bike", VEHICLE_BIKE},
|
||||
{"SkateBoard", VEHICLE_SKATEBOARD},
|
||||
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
||||
{"TowTrack", VEHICLE_TOWTRACK},
|
||||
{"Ambulance", VEHICLE_AMBULANCE},
|
||||
};
|
||||
|
||||
if (!p_actor) {
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
for (const auto& entry : vehicleMap) {
|
||||
if (p_actor->IsA(entry.className)) {
|
||||
return entry.vehicleType;
|
||||
}
|
||||
}
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
} // namespace Multiplayer
|
||||
@ -18,24 +18,9 @@
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <cmath>
|
||||
#include <vec.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace Multiplayer;
|
||||
|
||||
// LOD names for vehicle models. The helicopter is a compound ROI ("copter")
|
||||
// with no standalone LOD; use its body part instead.
|
||||
static const char* g_vehicleROINames[VEHICLE_COUNT] =
|
||||
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
||||
|
||||
static const char* g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
||||
|
||||
static const char* g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
||||
|
||||
static bool IsLargeVehicle(int8_t p_vehicleType)
|
||||
{
|
||||
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
||||
}
|
||||
|
||||
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
|
||||
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
|
||||
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
|
||||
@ -213,38 +198,7 @@ void RemotePlayer::SetVisible(bool p_visible)
|
||||
|
||||
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
|
||||
{
|
||||
if (!p_animName || !m_roi) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if already cached
|
||||
auto it = m_animCacheMap.find(p_animName);
|
||||
if (it != m_animCacheMap.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
// Look up the animation presenter in the current world
|
||||
LegoWorld* world = CurrentWorld();
|
||||
if (!world) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MxCore* presenter = world->Find("LegoAnimPresenter", p_animName);
|
||||
if (!presenter) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LegoAnim* anim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
|
||||
if (!anim) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Build and cache
|
||||
AnimCache& cache = m_animCacheMap[p_animName];
|
||||
cache.anim = anim;
|
||||
BuildROIMap(anim, m_roi, nullptr, cache.roiMap, cache.roiMapSize);
|
||||
|
||||
return &cache;
|
||||
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, m_roi, p_animName);
|
||||
}
|
||||
|
||||
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
||||
@ -269,83 +223,6 @@ void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
||||
m_emoteActive = true;
|
||||
}
|
||||
|
||||
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
|
||||
// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
|
||||
static void AssignROIIndices(
|
||||
LegoTreeNode* p_node,
|
||||
LegoROI* p_parentROI,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
MxU32& p_nextIndex,
|
||||
std::vector<LegoROI*>& p_entries
|
||||
)
|
||||
{
|
||||
LegoROI* roi = p_parentROI;
|
||||
LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData();
|
||||
const char* name = data ? data->GetName() : nullptr;
|
||||
|
||||
if (name != nullptr && *name != '-') {
|
||||
LegoROI* matchedROI = nullptr;
|
||||
|
||||
if (*name == '*' || p_parentROI == nullptr) {
|
||||
roi = p_rootROI;
|
||||
matchedROI = p_rootROI;
|
||||
}
|
||||
else {
|
||||
matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
|
||||
if (matchedROI == nullptr && p_extraROI != nullptr) {
|
||||
matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedROI != nullptr) {
|
||||
data->SetROIIndex(p_nextIndex);
|
||||
p_entries.push_back(matchedROI);
|
||||
p_nextIndex++;
|
||||
}
|
||||
else {
|
||||
data->SetROIIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) {
|
||||
AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries);
|
||||
}
|
||||
}
|
||||
|
||||
void RemotePlayer::BuildROIMap(
|
||||
LegoAnim* p_anim,
|
||||
LegoROI* p_rootROI,
|
||||
LegoROI* p_extraROI,
|
||||
LegoROI**& p_roiMap,
|
||||
MxU32& p_roiMapSize
|
||||
)
|
||||
{
|
||||
if (!p_anim || !p_rootROI) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoTreeNode* root = p_anim->GetRoot();
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
MxU32 nextIndex = 1;
|
||||
std::vector<LegoROI*> entries;
|
||||
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
|
||||
|
||||
if (entries.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1-indexed; index 0 reserved as NULL
|
||||
p_roiMapSize = entries.size() + 1;
|
||||
p_roiMap = new LegoROI*[p_roiMapSize];
|
||||
p_roiMap[0] = nullptr;
|
||||
for (MxU32 i = 0; i < entries.size(); i++) {
|
||||
p_roiMap[i + 1] = entries[i];
|
||||
}
|
||||
}
|
||||
|
||||
void RemotePlayer::UpdateTransform(float p_deltaTime)
|
||||
{
|
||||
@ -394,18 +271,10 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
|
||||
|
||||
// Ensure visibility of all mapped ROIs
|
||||
if (walkRoiMap) {
|
||||
for (MxU32 i = 1; i < walkRoiMapSize; i++) {
|
||||
if (walkRoiMap[i] != nullptr) {
|
||||
walkRoiMap[i]->SetVisibility(TRUE);
|
||||
}
|
||||
}
|
||||
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
|
||||
}
|
||||
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
|
||||
for (MxU32 i = 1; i < m_idleAnimCache->roiMapSize; i++) {
|
||||
if (m_idleAnimCache->roiMap[i] != nullptr) {
|
||||
m_idleAnimCache->roiMap[i]->SetVisibility(TRUE);
|
||||
}
|
||||
}
|
||||
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
|
||||
}
|
||||
|
||||
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
|
||||
@ -569,7 +438,7 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
|
||||
m_rideVehicleROI->SetName(vehicleVariantName);
|
||||
}
|
||||
|
||||
BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
||||
AnimUtils::BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
560
extensions/src/multiplayer/thirdpersoncamera.cpp
Normal file
560
extensions/src/multiplayer/thirdpersoncamera.cpp
Normal file
@ -0,0 +1,560 @@
|
||||
#include "extensions/multiplayer/thirdpersoncamera.h"
|
||||
|
||||
#include "3dmanager/lego3dmanager.h"
|
||||
#include "anim/legoanim.h"
|
||||
#include "islepathactor.h"
|
||||
#include "legoanimpresenter.h"
|
||||
#include "legocameracontroller.h"
|
||||
#include "legocharactermanager.h"
|
||||
#include "legovideomanager.h"
|
||||
#include "legoworld.h"
|
||||
#include "misc.h"
|
||||
#include "misc/legotree.h"
|
||||
#include "mxgeometry/mxgeometry3d.h"
|
||||
#include "mxgeometry/mxmatrix.h"
|
||||
#include "realtime/realtime.h"
|
||||
#include "roi/legoroi.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Multiplayer;
|
||||
|
||||
ThirdPersonCamera::ThirdPersonCamera()
|
||||
: m_enabled(false), m_active(false), m_playerROI(nullptr), m_walkAnimId(0), m_idleAnimId(0),
|
||||
m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f),
|
||||
m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false),
|
||||
m_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0),
|
||||
m_rideVehicleROI(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::Enable()
|
||||
{
|
||||
m_enabled = true;
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::Disable()
|
||||
{
|
||||
m_enabled = false;
|
||||
m_active = false;
|
||||
m_playerROI = nullptr;
|
||||
ClearRideAnimation();
|
||||
m_animCacheMap.clear();
|
||||
ClearAnimCaches();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
||||
{
|
||||
if (!m_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoPathActor* userActor = UserActor();
|
||||
if (static_cast<LegoPathActor*>(p_actor) != userActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoROI* newROI = userActor->GetROI();
|
||||
if (!newROI) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if we're entering a vehicle
|
||||
int8_t vehicleType = DetectVehicleType(userActor);
|
||||
|
||||
if (vehicleType != VEHICLE_NONE) {
|
||||
// Large vehicles and helicopter: stay first-person with dashboard.
|
||||
// Track the vehicle type so OnActorExit can trigger reinit on exit.
|
||||
if (IsLargeVehicle(vehicleType) || vehicleType == VEHICLE_HELICOPTER) {
|
||||
// Hide the walking character ROI that we made visible earlier.
|
||||
// Enter() doesn't call Exit() on the previous actor, so our
|
||||
// OnActorExit never fires for the walking character.
|
||||
if (m_playerROI) {
|
||||
m_playerROI->SetVisibility(FALSE);
|
||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||
}
|
||||
m_currentVehicleType = vehicleType;
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Small vehicle: need the character ROI for ride animations.
|
||||
if (!m_playerROI) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentVehicleType = vehicleType;
|
||||
m_active = true;
|
||||
|
||||
SetupCamera(userActor);
|
||||
BuildRideAnimation(vehicleType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-vehicle (walking character) entry
|
||||
m_playerROI = newROI;
|
||||
m_currentVehicleType = VEHICLE_NONE;
|
||||
m_active = true;
|
||||
|
||||
// Make the player model visible (Enter() hid it for first-person)
|
||||
m_playerROI->SetVisibility(TRUE);
|
||||
|
||||
// SpawnPlayer() removes the ROI from the 3D manager before calling Enter().
|
||||
// Re-add it so the character is actually rendered in third-person mode.
|
||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
||||
|
||||
// Build animation caches
|
||||
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
||||
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
||||
|
||||
// Reset animation state
|
||||
m_animTime = 0.0f;
|
||||
m_idleTime = 0.0f;
|
||||
m_idleAnimTime = 0.0f;
|
||||
m_wasMoving = false;
|
||||
m_emoteActive = false;
|
||||
|
||||
ApplyIdleFrame0();
|
||||
|
||||
SetupCamera(userActor);
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
|
||||
{
|
||||
if (!m_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The hook fires at the end of Exit(), after UserActor() has been restored
|
||||
// to the walking character. For vehicle exit, p_actor is the vehicle (not
|
||||
// UserActor), so we check m_currentVehicleType instead of comparing actors.
|
||||
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||
// Exiting a vehicle: reinitialize immediately for the walking character.
|
||||
ClearRideAnimation();
|
||||
ClearAnimCaches();
|
||||
m_animCacheMap.clear();
|
||||
ReinitForCharacter();
|
||||
}
|
||||
else if (m_active && static_cast<LegoPathActor*>(p_actor) == UserActor()) {
|
||||
// Exiting on foot (e.g., world transition): full teardown.
|
||||
// Hide the player ROI and remove it from the 3D manager (we added it
|
||||
// in OnActorEnter so the character would render in third-person).
|
||||
if (m_playerROI) {
|
||||
m_playerROI->SetVisibility(FALSE);
|
||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||
}
|
||||
ClearRideAnimation();
|
||||
ClearAnimCaches();
|
||||
m_currentVehicleType = VEHICLE_NONE;
|
||||
m_playerROI = nullptr;
|
||||
m_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::Tick(float p_deltaTime)
|
||||
{
|
||||
if (!m_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_playerROI) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Small vehicle with ride animation (like RemotePlayer)
|
||||
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||
if (m_rideAnim && m_rideRoiMap) {
|
||||
LegoPathActor* actor = UserActor();
|
||||
if (!actor || !actor->GetROI()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force visibility of ride ROI map entries
|
||||
AnimUtils::EnsureROIMapVisibility(m_rideRoiMap, m_rideRoiMapSize);
|
||||
|
||||
// Only advance animation time when actually moving
|
||||
float speed = actor->GetWorldSpeed();
|
||||
if (fabsf(speed) > 0.01f) {
|
||||
m_animTime += p_deltaTime * 2000.0f;
|
||||
}
|
||||
|
||||
// Use vehicle actor's transform as base (character ROI may be at old position)
|
||||
MxMatrix transform(actor->GetROI()->GetLocal2World());
|
||||
|
||||
// Position character ROI at the vehicle so bones render at the right place
|
||||
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform);
|
||||
m_playerROI->SetVisibility(TRUE);
|
||||
|
||||
float duration = (float) m_rideAnim->GetDuration();
|
||||
if (duration > 0.0f) {
|
||||
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
|
||||
|
||||
LegoTreeNode* root = m_rideAnim->GetRoot();
|
||||
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||
LegoROI::ApplyAnimationTransformation(
|
||||
root->GetChild(i),
|
||||
transform,
|
||||
(LegoTime) timeInCycle,
|
||||
m_rideRoiMap
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LegoPathActor* userActor = UserActor();
|
||||
if (!userActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the active walk animation and its ROI map
|
||||
LegoAnim* walkAnim = nullptr;
|
||||
LegoROI** walkRoiMap = nullptr;
|
||||
MxU32 walkRoiMapSize = 0;
|
||||
|
||||
if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) {
|
||||
walkAnim = m_walkAnimCache->anim;
|
||||
walkRoiMap = m_walkAnimCache->roiMap;
|
||||
walkRoiMapSize = m_walkAnimCache->roiMapSize;
|
||||
}
|
||||
|
||||
// Ensure visibility of all mapped ROIs
|
||||
if (walkRoiMap) {
|
||||
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
|
||||
}
|
||||
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
|
||||
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
|
||||
}
|
||||
|
||||
float speed = userActor->GetWorldSpeed();
|
||||
bool isMoving = fabsf(speed) > 0.01f;
|
||||
|
||||
// Movement interrupts emotes
|
||||
if (isMoving && m_emoteActive) {
|
||||
m_emoteActive = false;
|
||||
m_emoteAnimCache = nullptr;
|
||||
}
|
||||
|
||||
if (isMoving) {
|
||||
if (!walkAnim || !walkRoiMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_animTime += p_deltaTime * 2000.0f;
|
||||
float duration = (float) walkAnim->GetDuration();
|
||||
if (duration > 0.0f) {
|
||||
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
|
||||
|
||||
MxMatrix transform(m_playerROI->GetLocal2World());
|
||||
LegoTreeNode* root = walkAnim->GetRoot();
|
||||
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, walkRoiMap);
|
||||
}
|
||||
}
|
||||
m_wasMoving = true;
|
||||
m_idleTime = 0.0f;
|
||||
m_idleAnimTime = 0.0f;
|
||||
}
|
||||
else if (m_emoteActive && m_emoteAnimCache && m_emoteAnimCache->anim && m_emoteAnimCache->roiMap) {
|
||||
m_emoteTime += p_deltaTime * 1000.0f;
|
||||
|
||||
if (m_emoteTime >= m_emoteDuration) {
|
||||
m_emoteActive = false;
|
||||
m_emoteAnimCache = nullptr;
|
||||
m_wasMoving = false;
|
||||
m_idleTime = 0.0f;
|
||||
m_idleAnimTime = 0.0f;
|
||||
}
|
||||
else {
|
||||
// Use the saved clean parent transform to prevent scale
|
||||
// accumulation (see TriggerEmote for details).
|
||||
MxMatrix transform(m_emoteParentTransform);
|
||||
|
||||
LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot();
|
||||
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||
LegoROI::ApplyAnimationTransformation(
|
||||
root->GetChild(i),
|
||||
transform,
|
||||
(LegoTime) m_emoteTime,
|
||||
m_emoteAnimCache->roiMap
|
||||
);
|
||||
}
|
||||
|
||||
// Restore the player ROI's transform — the animation's root
|
||||
// node (ACTOR_01) wrote a scaled value into it.
|
||||
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform);
|
||||
}
|
||||
}
|
||||
else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) {
|
||||
if (m_wasMoving) {
|
||||
m_wasMoving = false;
|
||||
m_idleTime = 0.0f;
|
||||
m_idleAnimTime = 0.0f;
|
||||
}
|
||||
|
||||
m_idleTime += p_deltaTime;
|
||||
|
||||
if (m_idleTime >= 2.5f) {
|
||||
m_idleAnimTime += p_deltaTime * 1000.0f;
|
||||
}
|
||||
|
||||
float duration = (float) m_idleAnimCache->anim->GetDuration();
|
||||
if (duration > 0.0f) {
|
||||
float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration);
|
||||
|
||||
MxMatrix transform(m_playerROI->GetLocal2World());
|
||||
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
|
||||
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||
LegoROI::ApplyAnimationTransformation(
|
||||
root->GetChild(i),
|
||||
transform,
|
||||
(LegoTime) timeInCycle,
|
||||
m_idleAnimCache->roiMap
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::SetWalkAnimId(uint8_t p_id)
|
||||
{
|
||||
if (p_id >= g_walkAnimCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p_id != m_walkAnimId) {
|
||||
m_walkAnimId = p_id;
|
||||
if (m_active) {
|
||||
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::SetIdleAnimId(uint8_t p_id)
|
||||
{
|
||||
if (p_id >= g_idleAnimCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p_id != m_idleAnimId) {
|
||||
m_idleAnimId = p_id;
|
||||
if (m_active) {
|
||||
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId)
|
||||
{
|
||||
if (p_emoteId >= g_emoteAnimCount || !m_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoPathActor* userActor = UserActor();
|
||||
if (!userActor || fabsf(userActor->GetWorldSpeed()) > 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]);
|
||||
if (!cache || !cache->anim) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_emoteAnimCache = cache;
|
||||
m_emoteTime = 0.0f;
|
||||
m_emoteDuration = (float) cache->anim->GetDuration();
|
||||
m_emoteActive = true;
|
||||
|
||||
// Save the clean parent transform before the emote starts.
|
||||
// The emote animation's root node (ACTOR_01) maps to the player ROI,
|
||||
// so ApplyAnimationTransformation writes a scaled transform into
|
||||
// m_playerROI->m_local2world each frame. When the character is
|
||||
// stationary the engine's CalculateTransform does not run, so the ROI
|
||||
// is never reset — causing the scale to compound across frames.
|
||||
// Using the saved clean transform as parent prevents this feedback.
|
||||
m_emoteParentTransform = m_playerROI->GetLocal2World();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world)
|
||||
{
|
||||
if (!m_enabled || !p_world) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear stale caches (animation presenters may have been recreated)
|
||||
m_animCacheMap.clear();
|
||||
ClearAnimCaches();
|
||||
|
||||
ReinitForCharacter();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world)
|
||||
{
|
||||
if (!p_world) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_active = false;
|
||||
m_playerROI = nullptr;
|
||||
ClearRideAnimation();
|
||||
m_animCacheMap.clear();
|
||||
ClearAnimCaches();
|
||||
}
|
||||
|
||||
ThirdPersonCamera::AnimCache* ThirdPersonCamera::GetOrBuildAnimCache(const char* p_animName)
|
||||
{
|
||||
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, m_playerROI, p_animName);
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ClearAnimCaches()
|
||||
{
|
||||
m_walkAnimCache = nullptr;
|
||||
m_idleAnimCache = nullptr;
|
||||
m_emoteAnimCache = nullptr;
|
||||
m_emoteActive = false;
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
|
||||
{
|
||||
LegoWorld* world = CurrentWorld();
|
||||
if (!world || !world->GetCameraController()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// After Enter()'s TurnAround, the ROI direction is negated.
|
||||
// The mesh faces -z (local) = +path_forward (correct visual facing).
|
||||
// +z in ROI-local is the negated direction, i.e. behind the visual model.
|
||||
// Movement inversion is handled by ShouldInvertMovement in CalculateTransform.
|
||||
Mx3DPointFloat at(0.0f, 2.5f, 3.0f);
|
||||
Mx3DPointFloat dir(0.0f, -0.3f, -1.0f);
|
||||
Mx3DPointFloat up(0.0f, 1.0f, 0.0f);
|
||||
|
||||
world->GetCameraController()->SetWorldTransform(at, dir, up);
|
||||
p_actor->TransformPointOfView();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType)
|
||||
{
|
||||
if (p_vehicleType < 0 || p_vehicleType >= VEHICLE_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* rideAnimName = g_rideAnimNames[p_vehicleType];
|
||||
const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType];
|
||||
if (!rideAnimName || !vehicleVariantName) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegoWorld* world = CurrentWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
|
||||
MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName);
|
||||
if (!presenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_rideAnim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
|
||||
if (!m_rideAnim) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create variant ROI from base vehicle name, rename for anim tree matching
|
||||
const char* baseName = g_vehicleROINames[p_vehicleType];
|
||||
m_rideVehicleROI = CharacterManager()->CreateAutoROI("tp_vehicle", baseName, FALSE);
|
||||
if (m_rideVehicleROI) {
|
||||
m_rideVehicleROI->SetName(vehicleVariantName);
|
||||
}
|
||||
|
||||
AnimUtils::BuildROIMap(m_rideAnim, m_playerROI, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
||||
m_animTime = 0.0f;
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ClearRideAnimation()
|
||||
{
|
||||
if (m_rideRoiMap) {
|
||||
delete[] m_rideRoiMap;
|
||||
m_rideRoiMap = nullptr;
|
||||
m_rideRoiMapSize = 0;
|
||||
}
|
||||
if (m_rideVehicleROI) {
|
||||
VideoManager()->Get3DManager()->Remove(*m_rideVehicleROI);
|
||||
CharacterManager()->ReleaseAutoROI(m_rideVehicleROI);
|
||||
m_rideVehicleROI = nullptr;
|
||||
}
|
||||
m_rideAnim = nullptr;
|
||||
m_currentVehicleType = VEHICLE_NONE;
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ApplyIdleFrame0()
|
||||
{
|
||||
if (!m_playerROI || !m_idleAnimCache || !m_idleAnimCache->anim || !m_idleAnimCache->roiMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
MxMatrix transform(m_playerROI->GetLocal2World());
|
||||
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
|
||||
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) 0.0f, m_idleAnimCache->roiMap);
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ReinitForCharacter()
|
||||
{
|
||||
LegoPathActor* userActor = UserActor();
|
||||
if (!userActor) {
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
LegoROI* roi = userActor->GetROI();
|
||||
if (!roi) {
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int8_t vehicleType = DetectVehicleType(userActor);
|
||||
|
||||
// Large vehicles and helicopter: stay first-person
|
||||
if (vehicleType == VEHICLE_HELICOPTER || (vehicleType != VEHICLE_NONE && IsLargeVehicle(vehicleType))) {
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentVehicleType = vehicleType;
|
||||
|
||||
if (vehicleType != VEHICLE_NONE) {
|
||||
if (!m_playerROI) {
|
||||
m_active = false;
|
||||
return;
|
||||
}
|
||||
m_active = true;
|
||||
SetupCamera(userActor);
|
||||
BuildRideAnimation(vehicleType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reinitializing for walking character
|
||||
m_playerROI = roi;
|
||||
m_playerROI->SetVisibility(TRUE);
|
||||
|
||||
// Ensure the ROI is in the 3D manager so it gets rendered
|
||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
||||
|
||||
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
||||
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
||||
|
||||
m_animTime = 0.0f;
|
||||
m_idleTime = 0.0f;
|
||||
m_idleAnimTime = 0.0f;
|
||||
m_wasMoving = false;
|
||||
m_emoteActive = false;
|
||||
m_active = true;
|
||||
|
||||
ApplyIdleFrame0();
|
||||
SetupCamera(userActor);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user