mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Fix camera flip bugs, refactor camera (#11)
* Fix 180-degree camera flip when exiting sub-worlds without cam anim When returning from sub-worlds (jukebox, hospital exterior, etc.) with 3rd person camera active, the camera/player flipped 180 degrees. This happened because SpawnPlayer calls Enter() → TurnAround() before PlaceActor(), and PlaceActor overwrites the ROI direction with the path's standard convention (z = forward). For spawn points with cam anims, OnCamAnimEnd corrected this, but spawn points with m_location=0 (like jukebox exterior) had no correction. Replace m_roiUnflipped with m_needsDirectionFlip flag that tracks world transitions. OnWorldEnabled sets the flag, and the first Tick after PlaceActor completes flips the ROI to backward-z and re-setups the camera. ReinitForCharacter now always flips the ROI direction, handling both Disable→Enable toggles and enabling 3rd person after a 1st-person spawn. https://claude.ai/code/session_01NQ9vy9Qr3aH6LNsRNLEEtY * Fix camera flip regressions for vehicle exit and world transitions The unconditional ROI flip in ReinitForCharacter caused a 180-degree flip when exiting vehicles (Enter's TurnAround follows and double-flips). Restore conditional flip using m_roiUnflipped, but now also set it in OnWorldEnabled (even when disabled) so cold-enabling 3rd person after a world transition correctly flips from PlaceActor's forward-z. Key changes: - Remove m_roiUnflipped clearing from OnActorEnter: Enter() is always followed by PlaceActor which overwrites the ROI, so clearing the flag prematurely caused the cold-enable case to miss the needed flip. - Add orbit camera override in OnActorEnter during world transitions (m_needsDirectionFlip && m_active) to suppress the 1st-person camera flash from Enter's TransformPointOfView. - Clear m_roiUnflipped in OnCamAnimEnd alongside m_needsDirectionFlip, since the cam anim's PlaceActor + flip handles the correction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Defer orbit camera setup during world transition loading freeze Remove the premature SetupCamera call from OnActorEnter's world transition path. The stale orbit camera view (computed before PlaceActor runs) would freeze on screen during the ~500ms world load, appearing as a wrong-direction flash. The Tick correction after PlaceActor now handles the initial orbit camera setup at the correct position. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Skip orbit camera while cam anim is running to prevent actor glitch When a cam anim plays with 3rd-person camera active, ApplyOrbitCamera was fighting the cam anim each frame. If the cam anim was interrupted (space bar), its end handler read the ViewROI position — which was set by our orbit camera (elevated, behind player) — and placed the actor at that position, causing it to glitch into the air. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add documentation for ROI direction conventions and camera corrections Documents the forward-z vs backward-z conventions, all code paths that require direction correction, and the flags that coordinate them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Refactor 3rd-person camera from backward-z to forward-z convention Switch the orbit camera to use forward-z (matching PlaceActor's native output), eliminating all FlipROIDirection/TurnAround corrections. The display clone flips to backward-z when syncing from the native ROI so character meshes face correctly. Use actor state (c_disabled) instead of m_animRunning to guard against cam anim conflicts, allowing the orbit camera to resume as soon as the player is released. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Reset orbit camera position on world re-entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove ShouldInvertMovement and update documentation * Remove header
This commit is contained in:
parent
37f33a91df
commit
fe5ef4f9a5
@ -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 <mxdebug.h>
|
||||
#include <vec.h>
|
||||
|
||||
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<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;
|
||||
@ -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]);
|
||||
|
||||
97
extensions/docs/multiplayer/roi-direction-conventions.md
Normal file
97
extensions/docs/multiplayer/roi-direction-conventions.md
Normal file
@ -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.
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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 <SDL3/SDL_stdinc.h>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user