diff --git a/LEGO1/lego/legoomni/src/paths/legopathactor.cpp b/LEGO1/lego/legoomni/src/paths/legopathactor.cpp index a33ca596..a91a51be 100644 --- a/LEGO1/lego/legoomni/src/paths/legopathactor.cpp +++ b/LEGO1/lego/legoomni/src/paths/legopathactor.cpp @@ -1,7 +1,6 @@ #include "legopathactor.h" #include "define.h" -#include "extensions/multiplayer.h" #include "geom/legoorientededge.h" #include "legocachesoundmanager.h" #include "legocameracontroller.h" @@ -21,8 +20,6 @@ #include #include -using namespace Extensions; - DECOMP_SIZE_ASSERT(LegoPathActor, 0x154) DECOMP_SIZE_ASSERT(LegoPathEdgeContainer, 0x3c) @@ -265,11 +262,6 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform) m_worldSpeed = nav->GetLinearVel(); - MxBool invertDir = Extension::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; @@ -329,10 +321,6 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform) } } - if (invertDir) { - newDir *= -1.0f; - } - p_transform.SetIdentity(); Vector3 right(p_transform[0]); diff --git a/extensions/docs/multiplayer/roi-direction-conventions.md b/extensions/docs/multiplayer/roi-direction-conventions.md new file mode 100644 index 00000000..a817506a --- /dev/null +++ b/extensions/docs/multiplayer/roi-direction-conventions.md @@ -0,0 +1,97 @@ +# ROI Direction Conventions & Third-Person Camera + +## Background: The Two Z-Axis Conventions + +The game engine represents an actor's facing direction via the z-axis of its ROI +(Real-time Object Instance) local-to-world transform. Two opposite conventions +exist throughout the codebase: + +| Convention | ROI z-axis points... | Used by | +|----------------|--------------------------|--------------------------------------------------------| +| **forward-z** | Toward visual forward | `PlaceActor` (with `m_cameraFlag=TRUE`), cam anim end | +| **backward-z** | Away from visual forward | After `Enter()`'s `TurnAround()`, vehicle ROIs | + +Toggling between conventions is done by `IslePathActor::TurnAround` (or the +local `FlipMatrixDirection` helper): negate the z-axis and recompute the right +vector. + +## Design Choice: Forward-Z + +The third-person orbit camera uses **forward-z**, matching the convention that +`PlaceActor` naturally produces. This eliminates the need to flip the ROI +direction after every `PlaceActor` call. + +`ComputeOrbitVectors` treats local Z+ as the character's visual forward and +places the camera at local −Z (behind the character), looking toward +Z. + +## Engine Behavior + +The engine's actor lifecycle: + +``` +Enter() + → ResetWorldTransform(TRUE) sets m_cameraFlag = TRUE + → TurnAround() flips to backward-z + → TransformPointOfView() sets 1st-person camera + +PlaceActor() resets to forward-z ← what we use +``` + +`PlaceActor` always produces forward-z (when `m_cameraFlag=TRUE`), which is +exactly what the orbit camera expects. No direction correction is needed after +`PlaceActor` runs. + +## World Transition Timing + +The one remaining complexity is timing during world transitions. The event order +is: + +1. `OnWorldEnabled` fires (from `LegoWorld::Enable`, BEFORE `SpawnPlayer`) +2. `ReinitForCharacter` sets up the display ROI and marks `m_active = true` +3. `Enter()` fires `OnActorEnter` — ROI is at **stale position** from previous session +4. `PlaceActor` sets ROI to correct spawn position +5. First `Tick` — `ApplyOrbitCamera` sets the camera at the correct position + +Between steps 3 and 5, the ROI position is stale. If we set up the orbit camera +in step 3, the stale view would freeze on screen during the ~500ms world load. + +The `m_pendingWorldTransition` flag handles this: set in `OnWorldEnabled`, +it causes `OnActorEnter` and `ReinitForCharacter` to skip camera setup. +Cleared in the first `Tick` after `PlaceActor`, where `ApplyOrbitCamera` +naturally handles the camera. The orbit state (yaw, pitch, distance) is also +reset to defaults in `OnWorldEnabled`. + +## Display Clone Direction + +The native actor ROI is invisible in 3rd-person mode. A display clone renders +the character model instead. Character meshes face −z, so the clone needs +backward-z to look correct. When syncing the clone's transform from the native +ROI (which is in forward-z), `Tick` negates the z-axis and recomputes the right +vector — the same operation as `TurnAround`. + +This also affects the right vector (X-axis): forward-z and backward-z produce +opposite right vectors. The orbit yaw input is negated to compensate, keeping +drag-right = camera-moves-right. + +## Cam Anim Interaction + +While a cam anim locks the player (`GetActorState() == c_disabled`), two +things protect the orbit camera: + +1. **Tick guard**: `ApplyOrbitCamera` is skipped so it doesn't fight the cam + anim's `TransformPointOfView`. Without this, the cam anim end handler would + read our elevated orbit camera position and place the actor in the air. + +2. **`OnCamAnimEnd`**: When the cam anim releases the player (first space bar + interruption or natural end), this callback calls `SetupCamera` to restore + the orbit camera. + +After the first interruption, the actor state resets to `c_initial` and the +orbit camera resumes immediately — even if `m_animRunning` is still true for +background animations playing in the world. + +## Network Direction + +The network protocol sends forward-z direction (visual forward). Remote players +negate the received direction to backward-z for their ROI, since character meshes +face −z. diff --git a/extensions/include/extensions/multiplayer.h b/extensions/include/extensions/multiplayer.h index 1d0fc5d4..a184308c 100644 --- a/extensions/include/extensions/multiplayer.h +++ b/extensions/include/extensions/multiplayer.h @@ -50,7 +50,6 @@ class MultiplayerExt { static void HandleActorEnter(IslePathActor* p_actor); static void HandleActorExit(IslePathActor* p_actor); static void HandleCamAnimEnd(LegoPathActor* p_actor); - static MxBool ShouldInvertMovement(LegoPathActor* p_actor); // Returns TRUE if the name belongs to a multiplayer clone (entity-less ROI). static MxBool IsClonedCharacter(const char* p_name); @@ -91,7 +90,6 @@ constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick; constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter; constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit; constexpr auto HandleCamAnimEnd = &MultiplayerExt::HandleCamAnimEnd; -constexpr auto ShouldInvertMovement = &MultiplayerExt::ShouldInvertMovement; constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter; constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad; constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded; @@ -107,7 +105,6 @@ constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr; constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr; constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr; constexpr decltype(&MultiplayerExt::HandleCamAnimEnd) HandleCamAnimEnd = nullptr; -constexpr decltype(&MultiplayerExt::ShouldInvertMovement) ShouldInvertMovement = nullptr; constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr; constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr; constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr; diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index 6beddd27..8b86cda9 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -78,8 +78,8 @@ class ThirdPersonCamera { bool m_enabled; bool m_active; - bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies - LegoROI* m_playerROI; // Borrowed, not owned + bool m_pendingWorldTransition; // True between OnWorldEnabled and first Tick; defers camera setup + LegoROI* m_playerROI; // Borrowed, not owned // Display actor override uint8_t m_displayActorIndex; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index f6d1aa05..f8508e1d 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -283,15 +283,6 @@ void MultiplayerExt::HandleCamAnimEnd(LegoPathActor* p_actor) } } -MxBool MultiplayerExt::ShouldInvertMovement(LegoPathActor* p_actor) -{ - if (s_networkManager && UserActor() == p_actor) { - return s_networkManager->GetThirdPersonCamera().IsActive(); - } - - return FALSE; -} - MxBool MultiplayerExt::IsClonedCharacter(const char* p_name) { if (!s_networkManager) { diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 2806ea34..225b0132 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -318,17 +318,6 @@ void NetworkManager::BroadcastLocalState() msg.vehicleType = DetectVehicleType(userActor); SDL_memcpy(msg.position, pos, sizeof(msg.position)); SDL_memcpy(msg.direction, dir, sizeof(msg.direction)); - - // When 3rd-person camera is active, ShouldInvertMovement causes movement - // inversion, and CalculateTransform re-inverts to keep ROI z backward. - // Negate to send the visual-forward direction that remote players expect. - // RemotePlayer::UpdateTransform negates again to restore backward-z. - 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; diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index a0a2cb45..ef54d079 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -258,10 +258,8 @@ void RemotePlayer::UpdateTransform(float p_deltaTime) LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f); LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f); - // 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. + // The network sends forward-z (visual forward). Character meshes face -z, + // so negate to get backward-z for the ROI (mesh faces the correct way). Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]); Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]); Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]); diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index 7b63eb56..476552be 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -7,6 +7,7 @@ #include "islepathactor.h" #include "legocameracontroller.h" #include "legocharactermanager.h" +#include "legopathactor.h" #include "legovideomanager.h" #include "legoworld.h" #include "misc.h" @@ -14,27 +15,26 @@ #include "mxgeometry/mxgeometry3d.h" #include "mxgeometry/mxmatrix.h" #include "realtime/realtime.h" +#include "realtime/vector.h" #include "roi/legoroi.h" #include using namespace Multiplayer; -// Flip the ROI's z-axis direction in place (same operation as IslePathActor::TurnAround). -static void FlipROIDirection(LegoROI* p_roi) +// Flip a matrix from forward-z to backward-z (or vice versa) in place. +// Same operation as IslePathActor::TurnAround: negate z, recompute right. +static void FlipMatrixDirection(MxMatrix& p_mat) { - MxMatrix transform(p_roi->GetLocal2World()); - Vector3 right(transform[0]); - Vector3 up(transform[1]); - Vector3 direction(transform[2]); + Vector3 right(p_mat[0]); + Vector3 up(p_mat[1]); + Vector3 direction(p_mat[2]); direction *= -1.0f; right.EqualsCross(up, direction); - p_roi->SetLocal2World(transform); - p_roi->WrappedUpdateWorldData(); } ThirdPersonCamera::ThirdPersonCamera() - : m_enabled(false), m_active(false), m_roiUnflipped(false), m_playerROI(nullptr), + : m_enabled(false), m_active(false), m_pendingWorldTransition(false), m_playerROI(nullptr), m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayROI(nullptr), m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true}), m_showNameBubble(true), m_orbitYaw(DEFAULT_ORBIT_YAW), m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), @@ -57,19 +57,6 @@ void ThirdPersonCamera::Disable() 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). - // Flip the native ROI (not the display clone) since TransformPointOfView - // uses it for the 1st-person camera. - LegoROI* turnAroundROI = userActor ? userActor->GetROI() : nullptr; - - if (turnAroundROI) { - FlipROIDirection(turnAroundROI); - m_roiUnflipped = true; - } - m_playerROI->SetVisibility(FALSE); VideoManager()->Get3DManager()->Remove(*m_playerROI); @@ -85,6 +72,7 @@ void ThirdPersonCamera::Disable() } m_active = false; + m_pendingWorldTransition = false; DestroyNameBubble(); DestroyDisplayClone(); m_animator.ClearRideAnimation(); @@ -104,13 +92,18 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) // even if Enable() was called after entering the vehicle. m_animator.SetCurrentVehicleType(DetectVehicleType(userActor)); - // Enter() calls TurnAround(), so any previous undo is superseded. - m_roiUnflipped = false; - if (!m_enabled) { return; } + // During a world transition, the ROI position is stale (PlaceActor hasn't + // run yet). Skip camera setup — the stale orbit view would freeze on + // screen during the ~500ms world load. ApplyOrbitCamera in the first + // Tick after PlaceActor handles camera setup naturally. + if (m_pendingWorldTransition && m_active) { + return; + } + LegoROI* newROI = userActor->GetROI(); if (!newROI) { return; @@ -135,12 +128,6 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) return; } - // Undo Enter()'s TurnAround. Vehicles are placed with ROI z opposite - // 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); m_animator.BuildRideAnimation(m_animator.GetCurrentVehicleType(), m_playerROI, 0); @@ -148,12 +135,11 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) return; } - // Non-vehicle (walking character) entry — Enter() already called TurnAround. + // Walking character entry. newROI->SetVisibility(FALSE); if (!EnsureDisplayROI()) { return; } - m_roiUnflipped = false; m_active = true; m_playerROI->SetVisibility(TRUE); @@ -181,16 +167,6 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor) // For vehicle exit, p_actor is the vehicle, not UserActor — // check m_currentVehicleType instead. if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { - // 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. m_animator.ClearRideAnimation(); m_animator.ClearAll(); @@ -212,20 +188,14 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor) void ThirdPersonCamera::OnCamAnimEnd(LegoPathActor* p_actor) { + m_pendingWorldTransition = false; + if (!m_active) { return; } - // FUN_1004b6d0's PlaceActor set the ROI with standard direction - // (z = visual forward). The 3rd person camera needs backward-z. - // Flip the ROI direction, then re-setup the camera. - // Flip the native ROI (not the display clone) since Tick() syncs the - // clone's transform from it. - LegoROI* roi = p_actor->GetROI(); - if (roi) { - FlipROIDirection(roi); - } - + // Cam anim end placed the actor via PlaceActor (forward-z). + // Restore the orbit camera. SetupCamera(p_actor); } @@ -239,8 +209,23 @@ void ThirdPersonCamera::Tick(float p_deltaTime) return; } - // Update orbit camera position each frame so it tracks the player - ApplyOrbitCamera(); + // After a world transition, PlaceActor has now run and set the ROI to + // the correct position. Clear the flag so subsequent OnActorEnter calls + // work normally. ApplyOrbitCamera below handles the camera setup. + if (m_pendingWorldTransition) { + m_pendingWorldTransition = false; + } + + // While a cam anim locks the player (actor state c_disabled), calling + // ApplyOrbitCamera would fight the cam anim each frame and, critically, + // if the cam anim is interrupted (space bar), its end handler reads + // ViewROI position to place the actor. Our orbit camera position + // (elevated, behind player) would cause the actor to be placed in the + // air. Once the player is released (first interruption resets actor + // state to c_initial), the orbit camera resumes immediately. + if (!UserActor() || UserActor()->GetActorState() != LegoPathActor::c_disabled) { + ApplyOrbitCamera(); + } m_animator.UpdateNameBubble(m_playerROI); @@ -262,8 +247,10 @@ void ThirdPersonCamera::Tick(float p_deltaTime) m_animator.SetAnimTime(m_animator.GetAnimTime() + p_deltaTime * 2000.0f); } - // Use vehicle actor's transform as base. + // Use vehicle actor's transform as base, flipped to backward-z + // so the character mesh (which faces -z) renders correctly. MxMatrix transform(actor->GetROI()->GetLocal2World()); + FlipMatrixDirection(transform); // Position character ROI at the vehicle for bone rendering. m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform); @@ -298,6 +285,8 @@ void ThirdPersonCamera::Tick(float p_deltaTime) LegoROI* nativeROI = userActor->GetROI(); if (nativeROI) { MxMatrix mat(nativeROI->GetLocal2World()); + // Native ROI uses forward-z; flip to backward-z for the mesh. + FlipMatrixDirection(mat); m_displayROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); VideoManager()->Get3DManager()->Moved(*m_displayROI); } @@ -347,13 +336,26 @@ void ThirdPersonCamera::StopClickAnimation() void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world) { - if (!m_enabled || !p_world) { + if (!p_world) { + return; + } + + if (!m_enabled) { return; } // Animation presenters may have been recreated. m_animator.ClearAll(); + // Reset orbit to default position behind the character. + ResetOrbitState(); + + // ReinitForCharacter runs BEFORE SpawnPlayer/PlaceActor, so the ROI + // position is stale. Set the flag so OnActorEnter and ReinitForCharacter + // defer camera setup. The first Tick after PlaceActor clears it, and + // ApplyOrbitCamera handles the camera naturally. + m_pendingWorldTransition = true; + ReinitForCharacter(); } @@ -362,9 +364,8 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world) if (!p_world) { return; } - m_active = false; - m_roiUnflipped = false; + m_pendingWorldTransition = false; m_playerROI = nullptr; DestroyNameBubble(); DestroyDisplayClone(); @@ -471,7 +472,8 @@ void ThirdPersonCamera::SetNameBubbleVisible(bool p_visible) void ThirdPersonCamera::ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const { // Convert spherical coordinates to camera offset in entity-local space. - // Entity local Z+ is "behind" (after TurnAround), which is where yaw=0 points. + // The ROI uses forward-z (Z+ = visual forward). The camera orbits + // behind the character, so at yaw=0 it sits at local -Z. float cosP = SDL_cosf(m_orbitPitch); float sinP = SDL_sinf(m_orbitPitch); float sinY = SDL_sinf(m_orbitYaw); @@ -480,13 +482,11 @@ void ThirdPersonCamera::ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat p_at = Mx3DPointFloat( m_orbitDistance * sinY * cosP, ORBIT_TARGET_HEIGHT + m_orbitDistance * sinP, - m_orbitDistance * cosY * cosP + -m_orbitDistance * cosY * cosP ); - // Direction points from camera toward the pivot. Since the camera sits on - // a sphere of radius m_orbitDistance, the unit direction is just the - // negated spherical unit vector. - p_dir = Mx3DPointFloat(-sinY * cosP, -sinP, -cosY * cosP); + // Direction points from camera toward the pivot (the character). + p_dir = Mx3DPointFloat(-sinY * cosP, -sinP, cosY * cosP); p_up = Mx3DPointFloat(0.0f, 1.0f, 0.0f); } @@ -543,7 +543,7 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) case SDL_EVENT_MOUSE_MOTION: if (p_event->motion.state & SDL_BUTTON_RMASK) { - m_orbitYaw += p_event->motion.xrel * 0.005f; + m_orbitYaw -= p_event->motion.xrel * 0.005f; m_orbitPitch += p_event->motion.yrel * 0.005f; ClampPitch(); } @@ -601,7 +601,7 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) // Two-finger drag for orbit float moveX = m_touch.x[idx] - oldX; float moveY = m_touch.y[idx] - oldY; - m_orbitYaw -= moveX * 2.0f; + m_orbitYaw += moveX * 2.0f; m_orbitPitch += moveY * 2.0f; ClampPitch(); } @@ -655,6 +655,7 @@ void ThirdPersonCamera::ReinitForCharacter() // Large vehicles and helicopter: stay first-person if (vehicleType == VEHICLE_HELICOPTER || (vehicleType != VEHICLE_NONE && IsLargeVehicle(vehicleType))) { m_active = false; + m_pendingWorldTransition = false; return; } @@ -671,18 +672,7 @@ void ThirdPersonCamera::ReinitForCharacter() 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; - } + m_pendingWorldTransition = false; VideoManager()->Get3DManager()->Remove(*m_playerROI); VideoManager()->Get3DManager()->Add(*m_playerROI); @@ -700,15 +690,6 @@ void ThirdPersonCamera::ReinitForCharacter() return; } - // Re-apply TurnAround if we undid it in Disable(). - // Only set the local matrix here; the subsequent Add() will propagate world data. - // Flip the native ROI (not the display clone) since Tick() syncs the - // clone's transform from it. - if (m_roiUnflipped) { - FlipROIDirection(roi); - m_roiUnflipped = false; - } - m_playerROI->SetVisibility(TRUE); // Ensure the ROI is in the 3D manager. @@ -720,6 +701,12 @@ void ThirdPersonCamera::ReinitForCharacter() m_active = true; m_animator.ApplyIdleFrame0(m_playerROI); - SetupCamera(userActor); + + // During a world transition, PlaceActor hasn't run yet — the ROI is at + // a stale position. Defer camera setup; ApplyOrbitCamera in the first + // Tick after PlaceActor handles it. + if (!m_pendingWorldTransition) { + SetupCamera(userActor); + } CreateNameBubble(); }