mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-03 11:04:04 +00:00
Fix 3rd/1st person camera switch direction bugs (#4)
- Fix broadcast direction: use IsActive() instead of IsROITurnedAround() so the negate in BroadcastLocalState only fires when movement inversion is active, not based on a default-true flag - Fix vehicle ROI direction for 3rd-person camera: undo Enter()'s TurnAround on small vehicles so the backward-z convention is preserved. Vehicles are placed with ROI z opposite to visual forward, and Enter()'s TurnAround breaks this for 3rd-person rendering. Applied in both OnActorEnter (entering while 3rd-person enabled) and ReinitForCharacter (enabling 3rd-person while already on a vehicle) - Fix vehicle direction on exit: apply extra TurnAround in OnActorExit when 3rd-person is active, since Exit()'s TurnAround assumes Enter()'s TurnAround is still in effect - Add WrappedUpdateWorldData() after manual direction flips in Disable() and ReinitForCharacter() to keep bounding volumes consistent and prevent stale world data from causing momentary camera/direction glitches - Remove unused IsROITurnedAround() method - Fix data race between WASM exports and game thread
This commit is contained in:
parent
37b328a595
commit
c8c3b7276e
@ -9,6 +9,7 @@
|
|||||||
#include "mxcore.h"
|
#include "mxcore.h"
|
||||||
#include "mxtypes.h"
|
#include "mxtypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -47,6 +48,13 @@ class NetworkManager : public MxCore {
|
|||||||
void SetIdleAnimation(uint8_t p_index);
|
void SetIdleAnimation(uint8_t p_index);
|
||||||
void SendEmote(uint8_t p_emoteId);
|
void SendEmote(uint8_t p_emoteId);
|
||||||
|
|
||||||
|
// Thread-safe request methods for cross-thread callers (e.g. WASM exports
|
||||||
|
// running on the browser main thread). Deferred to the game thread in Tickle().
|
||||||
|
void RequestToggleThirdPerson() { m_pendingToggleThirdPerson.store(true, std::memory_order_relaxed); }
|
||||||
|
void RequestSetWalkAnimation(uint8_t p_index) { m_pendingWalkAnim.store(p_index, std::memory_order_relaxed); }
|
||||||
|
void RequestSetIdleAnimation(uint8_t p_index) { m_pendingIdleAnim.store(p_index, std::memory_order_relaxed); }
|
||||||
|
void RequestSendEmote(uint8_t p_emoteId) { m_pendingEmote.store(p_emoteId, std::memory_order_relaxed); }
|
||||||
|
|
||||||
void OnWorldEnabled(LegoWorld* p_world);
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
void OnWorldDisabled(LegoWorld* p_world);
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
|
|
||||||
@ -71,6 +79,7 @@ class NetworkManager : public MxCore {
|
|||||||
void HandleHostAssign(const HostAssignMsg& p_msg);
|
void HandleHostAssign(const HostAssignMsg& p_msg);
|
||||||
void HandleEmote(const EmoteMsg& p_msg);
|
void HandleEmote(const EmoteMsg& p_msg);
|
||||||
|
|
||||||
|
void ProcessPendingRequests();
|
||||||
void RemoveRemotePlayer(uint32_t p_peerId);
|
void RemoveRemotePlayer(uint32_t p_peerId);
|
||||||
void RemoveAllRemotePlayers();
|
void RemoveAllRemotePlayers();
|
||||||
|
|
||||||
@ -96,6 +105,11 @@ class NetworkManager : public MxCore {
|
|||||||
bool m_inIsleWorld;
|
bool m_inIsleWorld;
|
||||||
bool m_registered;
|
bool m_registered;
|
||||||
|
|
||||||
|
std::atomic<bool> m_pendingToggleThirdPerson;
|
||||||
|
std::atomic<int> m_pendingWalkAnim;
|
||||||
|
std::atomic<int> m_pendingIdleAnim;
|
||||||
|
std::atomic<int> m_pendingEmote;
|
||||||
|
|
||||||
static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz
|
static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz
|
||||||
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout
|
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout
|
||||||
static const int EXIT_ROOM_FULL = 10;
|
static const int EXIT_ROOM_FULL = 10;
|
||||||
|
|||||||
@ -55,6 +55,7 @@ class ThirdPersonCamera {
|
|||||||
|
|
||||||
bool m_enabled;
|
bool m_enabled;
|
||||||
bool m_active;
|
bool m_active;
|
||||||
|
bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies
|
||||||
LegoROI* m_playerROI; // Borrowed, not owned
|
LegoROI* m_playerROI; // Borrowed, not owned
|
||||||
|
|
||||||
// Walk/idle state (same pattern as RemotePlayer)
|
// Walk/idle state (same pattern as RemotePlayer)
|
||||||
|
|||||||
@ -31,7 +31,8 @@ void NetworkManager::SendMessage(const T& p_msg)
|
|||||||
NetworkManager::NetworkManager()
|
NetworkManager::NetworkManager()
|
||||||
: m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0),
|
: m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0),
|
||||||
m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), m_inIsleWorld(false),
|
m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), m_inIsleWorld(false),
|
||||||
m_registered(false)
|
m_registered(false), m_pendingToggleThirdPerson(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1),
|
||||||
|
m_pendingEmote(-1)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ NetworkManager::~NetworkManager()
|
|||||||
|
|
||||||
MxResult NetworkManager::Tickle()
|
MxResult NetworkManager::Tickle()
|
||||||
{
|
{
|
||||||
|
ProcessPendingRequests();
|
||||||
m_thirdPersonCamera.Tick(0.016f);
|
m_thirdPersonCamera.Tick(0.016f);
|
||||||
|
|
||||||
if (!m_transport) {
|
if (!m_transport) {
|
||||||
@ -50,9 +52,8 @@ MxResult NetworkManager::Tickle()
|
|||||||
|
|
||||||
uint32_t now = SDL_GetTicks();
|
uint32_t now = SDL_GetTicks();
|
||||||
|
|
||||||
// Broadcast BEFORE receiving: the Send proxy call gives the main thread a
|
// Broadcast before receiving so the Send proxy lets the main thread
|
||||||
// chance to process incoming WebSocket onmessage events before we drain
|
// process WebSocket events before we drain the queue.
|
||||||
// the queue with Receive.
|
|
||||||
if (m_transport->IsConnected() && (now - m_lastBroadcastTime) >= BROADCAST_INTERVAL_MS) {
|
if (m_transport->IsConnected() && (now - m_lastBroadcastTime) >= BROADCAST_INTERVAL_MS) {
|
||||||
BroadcastLocalState();
|
BroadcastLocalState();
|
||||||
m_lastBroadcastTime = now;
|
m_lastBroadcastTime = now;
|
||||||
@ -61,8 +62,7 @@ MxResult NetworkManager::Tickle()
|
|||||||
ProcessIncomingPackets();
|
ProcessIncomingPackets();
|
||||||
UpdateRemotePlayers(0.016f);
|
UpdateRemotePlayers(0.016f);
|
||||||
|
|
||||||
// Re-read time because ProcessIncomingPackets updates player timestamps
|
// Re-read time; ProcessIncomingPackets may have advanced SDL_GetTicks.
|
||||||
// via SDL_GetTicks(), which may be newer than the 'now' captured above.
|
|
||||||
uint32_t timeoutNow = SDL_GetTicks();
|
uint32_t timeoutNow = SDL_GetTicks();
|
||||||
std::vector<uint32_t> timedOut;
|
std::vector<uint32_t> timedOut;
|
||||||
for (auto& [peerId, player] : m_remotePlayers) {
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
@ -180,6 +180,33 @@ MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeT
|
|||||||
return m_worldSync.HandleEntityMutation(p_entity, p_changeType);
|
return m_worldSync.HandleEntityMutation(p_entity, p_changeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkManager::ProcessPendingRequests()
|
||||||
|
{
|
||||||
|
if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) {
|
||||||
|
if (m_thirdPersonCamera.IsEnabled()) {
|
||||||
|
m_thirdPersonCamera.Disable();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_thirdPersonCamera.Enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int walkAnim = m_pendingWalkAnim.exchange(-1, std::memory_order_relaxed);
|
||||||
|
if (walkAnim >= 0) {
|
||||||
|
SetWalkAnimation(static_cast<uint8_t>(walkAnim));
|
||||||
|
}
|
||||||
|
|
||||||
|
int idleAnim = m_pendingIdleAnim.exchange(-1, std::memory_order_relaxed);
|
||||||
|
if (idleAnim >= 0) {
|
||||||
|
SetIdleAnimation(static_cast<uint8_t>(idleAnim));
|
||||||
|
}
|
||||||
|
|
||||||
|
int emote = m_pendingEmote.exchange(-1, std::memory_order_relaxed);
|
||||||
|
if (emote >= 0) {
|
||||||
|
SendEmote(static_cast<uint8_t>(emote));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkManager::BroadcastLocalState()
|
void NetworkManager::BroadcastLocalState()
|
||||||
{
|
{
|
||||||
if (!m_transport) {
|
if (!m_transport) {
|
||||||
@ -223,9 +250,10 @@ void NetworkManager::BroadcastLocalState()
|
|||||||
SDL_memcpy(msg.position, pos, sizeof(msg.position));
|
SDL_memcpy(msg.position, pos, sizeof(msg.position));
|
||||||
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
|
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
|
||||||
|
|
||||||
// Third-person camera: ROI direction is opposite to actual movement direction
|
// When 3rd-person camera is active, ShouldInvertMovement causes movement
|
||||||
// (ShouldInvertMovement preserves TurnAround convention). Negate so remote
|
// inversion, and CalculateTransform re-inverts to keep ROI z backward.
|
||||||
// players receive the true movement-facing direction.
|
// Negate to send the visual-forward direction that remote players expect.
|
||||||
|
// RemotePlayer::UpdateTransform negates again to restore backward-z.
|
||||||
if (m_thirdPersonCamera.IsActive()) {
|
if (m_thirdPersonCamera.IsActive()) {
|
||||||
msg.direction[0] = -msg.direction[0];
|
msg.direction[0] = -msg.direction[0];
|
||||||
msg.direction[1] = -msg.direction[1];
|
msg.direction[1] = -msg.direction[1];
|
||||||
|
|||||||
@ -14,7 +14,7 @@ extern "C"
|
|||||||
{
|
{
|
||||||
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
if (mgr) {
|
if (mgr) {
|
||||||
mgr->SetWalkAnimation(static_cast<uint8_t>(index));
|
mgr->RequestSetWalkAnimation(static_cast<uint8_t>(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ extern "C"
|
|||||||
{
|
{
|
||||||
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
if (mgr) {
|
if (mgr) {
|
||||||
mgr->SetIdleAnimation(static_cast<uint8_t>(index));
|
mgr->RequestSetIdleAnimation(static_cast<uint8_t>(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ extern "C"
|
|||||||
{
|
{
|
||||||
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
if (mgr) {
|
if (mgr) {
|
||||||
mgr->SendEmote(static_cast<uint8_t>(index));
|
mgr->RequestSendEmote(static_cast<uint8_t>(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,13 +38,7 @@ extern "C"
|
|||||||
{
|
{
|
||||||
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
if (mgr) {
|
if (mgr) {
|
||||||
Multiplayer::ThirdPersonCamera& cam = mgr->GetThirdPersonCamera();
|
mgr->RequestToggleThirdPerson();
|
||||||
if (cam.IsEnabled()) {
|
|
||||||
cam.Disable();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cam.Enable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -146,8 +146,7 @@ size_t WebSocketTransport::Receive(std::function<void(const uint8_t*, size_t)> p
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain all queued messages in a single proxy call to avoid starving the main thread event loop.
|
// Drain queued messages in one proxy call: [4-byte LE length][payload...] each.
|
||||||
// Each message is concatenated as [4-byte LE length][payload...].
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
int totalBytes = MAIN_THREAD_EM_ASM_INT({
|
int totalBytes = MAIN_THREAD_EM_ASM_INT({
|
||||||
var socketId = $0;
|
var socketId = $0;
|
||||||
|
|||||||
@ -96,7 +96,7 @@ void RemotePlayer::Despawn()
|
|||||||
m_roi = nullptr;
|
m_roi = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all cached animation ROI maps (anim pointers are world-owned, not ours)
|
// Clear cached animation ROI maps (anim pointers are world-owned).
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
m_walkAnimCache = nullptr;
|
m_walkAnimCache = nullptr;
|
||||||
m_idleAnimCache = nullptr;
|
m_idleAnimCache = nullptr;
|
||||||
@ -230,7 +230,10 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
|
|||||||
LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f);
|
LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f);
|
||||||
LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f);
|
LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f);
|
||||||
|
|
||||||
// Character clones need negated direction
|
// Negate the received direction to restore the backward-z ROI convention.
|
||||||
|
// BroadcastLocalState sends visual-forward; negating here gives ROI z =
|
||||||
|
// backward, so mesh faces -z = forward (matching the sender's visual).
|
||||||
|
// See also: BroadcastLocalState in networkmanager.cpp.
|
||||||
Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]);
|
Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]);
|
||||||
Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]);
|
Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]);
|
||||||
Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]);
|
Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]);
|
||||||
|
|||||||
@ -19,8 +19,21 @@
|
|||||||
|
|
||||||
using namespace Multiplayer;
|
using namespace Multiplayer;
|
||||||
|
|
||||||
|
// Flip the ROI's z-axis direction in place (same operation as IslePathActor::TurnAround).
|
||||||
|
static void FlipROIDirection(LegoROI* p_roi)
|
||||||
|
{
|
||||||
|
MxMatrix transform(p_roi->GetLocal2World());
|
||||||
|
Vector3 right(transform[0]);
|
||||||
|
Vector3 up(transform[1]);
|
||||||
|
Vector3 direction(transform[2]);
|
||||||
|
direction *= -1.0f;
|
||||||
|
right.EqualsCross(up, direction);
|
||||||
|
p_roi->SetLocal2World(transform);
|
||||||
|
p_roi->WrappedUpdateWorldData();
|
||||||
|
}
|
||||||
|
|
||||||
ThirdPersonCamera::ThirdPersonCamera()
|
ThirdPersonCamera::ThirdPersonCamera()
|
||||||
: m_enabled(false), m_active(false), m_playerROI(nullptr), m_walkAnimId(0), m_idleAnimId(0),
|
: m_enabled(false), m_active(false), m_roiUnflipped(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_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_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_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0),
|
||||||
@ -31,13 +44,46 @@ ThirdPersonCamera::ThirdPersonCamera()
|
|||||||
void ThirdPersonCamera::Enable()
|
void ThirdPersonCamera::Enable()
|
||||||
{
|
{
|
||||||
m_enabled = true;
|
m_enabled = true;
|
||||||
|
ReinitForCharacter();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThirdPersonCamera::Disable()
|
void ThirdPersonCamera::Disable()
|
||||||
{
|
{
|
||||||
m_enabled = false;
|
m_enabled = false;
|
||||||
|
|
||||||
|
if (m_active && m_playerROI) {
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
LegoWorld* world = CurrentWorld();
|
||||||
|
|
||||||
|
// Undo TurnAround so the ROI z-axis points in the visual forward
|
||||||
|
// direction. This keeps the 1st-person camera facing the same way
|
||||||
|
// as the 3rd-person camera, and ensures the network direction stays
|
||||||
|
// consistent (no 180-degree flip for others).
|
||||||
|
// For walking characters the target is m_playerROI; for vehicles it
|
||||||
|
// is the vehicle actor's ROI (UserActor() returns the vehicle).
|
||||||
|
LegoROI* turnAroundROI =
|
||||||
|
(m_currentVehicleType == VEHICLE_NONE) ? m_playerROI : (userActor ? userActor->GetROI() : nullptr);
|
||||||
|
|
||||||
|
if (turnAroundROI) {
|
||||||
|
FlipROIDirection(turnAroundROI);
|
||||||
|
m_roiUnflipped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_playerROI->SetVisibility(FALSE);
|
||||||
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
|
|
||||||
|
// Restore vanilla 1st-person camera (eye-height offset, same as ResetWorldTransform).
|
||||||
|
if (userActor && world && world->GetCameraController()) {
|
||||||
|
world->GetCameraController()->SetWorldTransform(
|
||||||
|
Mx3DPointFloat(0.0F, 1.25F, 0.0F),
|
||||||
|
Mx3DPointFloat(0.0F, 0.0F, 1.0F),
|
||||||
|
Mx3DPointFloat(0.0F, 1.0F, 0.0F)
|
||||||
|
);
|
||||||
|
userActor->TransformPointOfView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_active = false;
|
m_active = false;
|
||||||
m_playerROI = nullptr;
|
|
||||||
ClearRideAnimation();
|
ClearRideAnimation();
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
ClearAnimCaches();
|
ClearAnimCaches();
|
||||||
@ -45,12 +91,19 @@ void ThirdPersonCamera::Disable()
|
|||||||
|
|
||||||
void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
||||||
{
|
{
|
||||||
if (!m_enabled) {
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (static_cast<LegoPathActor*>(p_actor) != userActor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LegoPathActor* userActor = UserActor();
|
// Always track vehicle type so OnActorExit can handle exits
|
||||||
if (static_cast<LegoPathActor*>(p_actor) != userActor) {
|
// even if Enable() was called after entering the vehicle.
|
||||||
|
m_currentVehicleType = DetectVehicleType(userActor);
|
||||||
|
|
||||||
|
// Enter() calls TurnAround(), so any previous undo is superseded.
|
||||||
|
m_roiUnflipped = false;
|
||||||
|
|
||||||
|
if (!m_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,21 +112,14 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect if we're entering a vehicle
|
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||||
int8_t vehicleType = DetectVehicleType(userActor);
|
// Large vehicles and helicopter: stay first-person.
|
||||||
|
if (IsLargeVehicle(m_currentVehicleType) || m_currentVehicleType == VEHICLE_HELICOPTER) {
|
||||||
if (vehicleType != VEHICLE_NONE) {
|
// Hide walking character ROI (Enter doesn't call Exit on it).
|
||||||
// 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) {
|
if (m_playerROI) {
|
||||||
m_playerROI->SetVisibility(FALSE);
|
m_playerROI->SetVisibility(FALSE);
|
||||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
}
|
}
|
||||||
m_currentVehicleType = vehicleType;
|
|
||||||
m_active = false;
|
m_active = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -83,24 +129,26 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentVehicleType = vehicleType;
|
// Undo Enter()'s TurnAround. Vehicles are placed with ROI z opposite
|
||||||
m_active = true;
|
// to their visual forward (mesh faces -z = forward). Enter()'s
|
||||||
|
// TurnAround flips ROI z to match the visual forward, which breaks
|
||||||
|
// the backward-z convention the 3rd-person camera relies on.
|
||||||
|
p_actor->TurnAround();
|
||||||
|
|
||||||
|
m_active = true;
|
||||||
SetupCamera(userActor);
|
SetupCamera(userActor);
|
||||||
BuildRideAnimation(vehicleType);
|
BuildRideAnimation(m_currentVehicleType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-vehicle (walking character) entry
|
// Non-vehicle (walking character) entry — Enter() already called TurnAround.
|
||||||
m_playerROI = newROI;
|
m_playerROI = newROI;
|
||||||
m_currentVehicleType = VEHICLE_NONE;
|
m_roiUnflipped = false;
|
||||||
m_active = true;
|
m_active = true;
|
||||||
|
|
||||||
// Make the player model visible (Enter() hid it for first-person)
|
|
||||||
m_playerROI->SetVisibility(TRUE);
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
// SpawnPlayer() removes the ROI from the 3D manager before calling Enter().
|
// Re-add ROI so it renders in third-person (SpawnPlayer removes it).
|
||||||
// Re-add it so the character is actually rendered in third-person mode.
|
|
||||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
||||||
|
|
||||||
@ -126,20 +174,27 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hook fires at the end of Exit(), after UserActor() has been restored
|
// For vehicle exit, p_actor is the vehicle, not UserActor —
|
||||||
// to the walking character. For vehicle exit, p_actor is the vehicle (not
|
// check m_currentVehicleType instead.
|
||||||
// UserActor), so we check m_currentVehicleType instead of comparing actors.
|
|
||||||
if (m_currentVehicleType != VEHICLE_NONE) {
|
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||||
// Exiting a vehicle: reinitialize immediately for the walking character.
|
// When 3rd-person camera is active, movement inversion causes the
|
||||||
|
// vehicle to physically drive opposite to vanilla. CalculateTransform
|
||||||
|
// re-inverts to keep the ROI z backward. Exit()'s TurnAround restores
|
||||||
|
// the vanilla convention, but that's wrong for the visual driving
|
||||||
|
// direction. Flip once more so the parked vehicle faces the way it
|
||||||
|
// was visually driven.
|
||||||
|
if (m_active) {
|
||||||
|
p_actor->TurnAround();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exiting a vehicle: reinitialize for the walking character.
|
||||||
ClearRideAnimation();
|
ClearRideAnimation();
|
||||||
ClearAnimCaches();
|
ClearAnimCaches();
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
ReinitForCharacter();
|
ReinitForCharacter();
|
||||||
}
|
}
|
||||||
else if (m_active && static_cast<LegoPathActor*>(p_actor) == UserActor()) {
|
else if (m_active && static_cast<LegoPathActor*>(p_actor) == UserActor()) {
|
||||||
// Exiting on foot (e.g., world transition): full teardown.
|
// Exiting on foot: 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) {
|
if (m_playerROI) {
|
||||||
m_playerROI->SetVisibility(FALSE);
|
m_playerROI->SetVisibility(FALSE);
|
||||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
@ -179,10 +234,10 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
|
|||||||
m_animTime += p_deltaTime * 2000.0f;
|
m_animTime += p_deltaTime * 2000.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use vehicle actor's transform as base (character ROI may be at old position)
|
// Use vehicle actor's transform as base.
|
||||||
MxMatrix transform(actor->GetROI()->GetLocal2World());
|
MxMatrix transform(actor->GetROI()->GetLocal2World());
|
||||||
|
|
||||||
// Position character ROI at the vehicle so bones render at the right place
|
// Position character ROI at the vehicle for bone rendering.
|
||||||
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform);
|
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform);
|
||||||
m_playerROI->SetVisibility(TRUE);
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
@ -268,8 +323,7 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
|
|||||||
m_idleAnimTime = 0.0f;
|
m_idleAnimTime = 0.0f;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Use the saved clean parent transform to prevent scale
|
// Use saved clean transform to prevent scale accumulation.
|
||||||
// accumulation (see TriggerEmote for details).
|
|
||||||
MxMatrix transform(m_emoteParentTransform);
|
MxMatrix transform(m_emoteParentTransform);
|
||||||
|
|
||||||
LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot();
|
LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot();
|
||||||
@ -282,8 +336,7 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the player ROI's transform — the animation's root
|
// Restore player ROI transform (animation root overwrote it).
|
||||||
// node (ACTOR_01) wrote a scaled value into it.
|
|
||||||
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform);
|
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,13 +420,8 @@ void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId)
|
|||||||
m_emoteDuration = (float) cache->anim->GetDuration();
|
m_emoteDuration = (float) cache->anim->GetDuration();
|
||||||
m_emoteActive = true;
|
m_emoteActive = true;
|
||||||
|
|
||||||
// Save the clean parent transform before the emote starts.
|
// Save clean transform to prevent scale accumulation during emote
|
||||||
// The emote animation's root node (ACTOR_01) maps to the player ROI,
|
// (the animation root writes scaled values into the ROI each frame).
|
||||||
// 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();
|
m_emoteParentTransform = m_playerROI->GetLocal2World();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +431,7 @@ void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear stale caches (animation presenters may have been recreated)
|
// Animation presenters may have been recreated.
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
ClearAnimCaches();
|
ClearAnimCaches();
|
||||||
|
|
||||||
@ -397,6 +445,7 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_active = false;
|
m_active = false;
|
||||||
|
m_roiUnflipped = false;
|
||||||
m_playerROI = nullptr;
|
m_playerROI = nullptr;
|
||||||
ClearRideAnimation();
|
ClearRideAnimation();
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
@ -423,10 +472,8 @@ void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After Enter()'s TurnAround, the ROI direction is negated.
|
// Camera behind the character; +z in ROI-local is behind the model
|
||||||
// The mesh faces -z (local) = +path_forward (correct visual facing).
|
// after TurnAround. Movement inversion in CalculateTransform corrects controls.
|
||||||
// +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 at(0.0f, 2.5f, 3.0f);
|
||||||
Mx3DPointFloat dir(0.0f, -0.3f, -1.0f);
|
Mx3DPointFloat dir(0.0f, -0.3f, -1.0f);
|
||||||
Mx3DPointFloat up(0.0f, 1.0f, 0.0f);
|
Mx3DPointFloat up(0.0f, 1.0f, 0.0f);
|
||||||
@ -462,7 +509,7 @@ void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create variant ROI from base vehicle name, rename for anim tree matching
|
// Create variant ROI, rename to match animation tree.
|
||||||
const char* baseName = g_vehicleROINames[p_vehicleType];
|
const char* baseName = g_vehicleROINames[p_vehicleType];
|
||||||
m_rideVehicleROI = CharacterManager()->CreateAutoROI("tp_vehicle", baseName, FALSE);
|
m_rideVehicleROI = CharacterManager()->CreateAutoROI("tp_vehicle", baseName, FALSE);
|
||||||
if (m_rideVehicleROI) {
|
if (m_rideVehicleROI) {
|
||||||
@ -531,6 +578,22 @@ void ThirdPersonCamera::ReinitForCharacter()
|
|||||||
m_active = false;
|
m_active = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Undo TurnAround on the vehicle ROI so the backward-z convention
|
||||||
|
// is restored. This handles both entering from 1st-person (Enter's
|
||||||
|
// TurnAround still in effect) and the Disable→Enable cycle (Disable
|
||||||
|
// re-applied TurnAround). In both cases ROI z currently matches
|
||||||
|
// the visual forward and needs to be flipped back.
|
||||||
|
{
|
||||||
|
LegoROI* vehicleROI = userActor->GetROI();
|
||||||
|
if (vehicleROI) {
|
||||||
|
FlipROIDirection(vehicleROI);
|
||||||
|
}
|
||||||
|
m_roiUnflipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
|
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
||||||
m_active = true;
|
m_active = true;
|
||||||
SetupCamera(userActor);
|
SetupCamera(userActor);
|
||||||
BuildRideAnimation(vehicleType);
|
BuildRideAnimation(vehicleType);
|
||||||
@ -539,9 +602,17 @@ void ThirdPersonCamera::ReinitForCharacter()
|
|||||||
|
|
||||||
// Reinitializing for walking character
|
// Reinitializing for walking character
|
||||||
m_playerROI = roi;
|
m_playerROI = roi;
|
||||||
|
|
||||||
|
// Re-apply TurnAround if we undid it in Disable().
|
||||||
|
// Only set the local matrix here; the subsequent Add() will propagate world data.
|
||||||
|
if (m_roiUnflipped) {
|
||||||
|
FlipROIDirection(m_playerROI);
|
||||||
|
m_roiUnflipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
m_playerROI->SetVisibility(TRUE);
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
// Ensure the ROI is in the 3D manager so it gets rendered
|
// Ensure the ROI is in the 3D manager.
|
||||||
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
VideoManager()->Get3DManager()->Add(*m_playerROI);
|
||||||
|
|
||||||
|
|||||||
@ -68,15 +68,13 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
|
|||||||
|
|
||||||
const uint8_t* snapshotData = p_data + sizeof(WorldSnapshotMsg);
|
const uint8_t* snapshotData = p_data + sizeof(WorldSnapshotMsg);
|
||||||
|
|
||||||
// Apply the snapshot using LegoMemory with the existing Read() methods
|
// Apply the snapshot via LegoMemory.
|
||||||
LegoMemory memory((void*) snapshotData, header.dataLength);
|
LegoMemory memory((void*) snapshotData, header.dataLength);
|
||||||
|
|
||||||
PlantManager()->Read(&memory);
|
PlantManager()->Read(&memory);
|
||||||
BuildingManager()->Read(&memory);
|
BuildingManager()->Read(&memory);
|
||||||
|
|
||||||
// If we're in the Isle world, update entity visuals after applying the snapshot.
|
// Read() updates data arrays but not entity positions; reload to refresh.
|
||||||
// Read() calls AdjustHeight() which updates data arrays, but doesn't update
|
|
||||||
// entity positions. We need to reload world info to refresh visuals.
|
|
||||||
if (m_inIsleWorld) {
|
if (m_inIsleWorld) {
|
||||||
LegoWorld* world = CurrentWorld();
|
LegoWorld* world = CurrentWorld();
|
||||||
if (world && world->GetWorldId() == LegoOmni::e_act1) {
|
if (world && world->GetWorldId() == LegoOmni::e_act1) {
|
||||||
@ -87,7 +85,7 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply any world events that were queued between snapshot request and response
|
// Replay events queued while snapshot was in flight.
|
||||||
for (const auto& evt : m_pendingWorldEvents) {
|
for (const auto& evt : m_pendingWorldEvents) {
|
||||||
ApplyWorldEvent(evt.entityType, evt.changeType, evt.entityIndex);
|
ApplyWorldEvent(evt.entityType, evt.changeType, evt.entityIndex);
|
||||||
}
|
}
|
||||||
@ -183,10 +181,7 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize plant + building state into a buffer using existing Write() methods
|
// Serialize plant + building state (~1133 bytes max, use 4096 for safety).
|
||||||
// Max sizes: 81 plants * (1+4+4+1+1+1) = 81*12 = 972 bytes
|
|
||||||
// 16 buildings * (4+4+1+1) = 16*10 = 160 bytes + 1 byte nextVariant
|
|
||||||
// Total ~1133 bytes. Use 4096 for safety.
|
|
||||||
uint8_t stateBuffer[4096];
|
uint8_t stateBuffer[4096];
|
||||||
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
|
LegoMemory memory(stateBuffer, sizeof(stateBuffer));
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user