Add LMB forward movement to third person camera

When LMB is held in 3rd person mode, the character walks forward in
the camera's facing direction. Works simultaneously with RMB camera
rotation. Transitions between 1st and 3rd person are seamless while
holding LMB. Also fixes RMB mouse cursor not reappearing when
releasing after a 3rd-to-1st person transition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-16 17:44:05 -07:00
parent 982957ee5e
commit 138cbcf6a3
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
9 changed files with 80 additions and 13 deletions

View File

@ -124,6 +124,7 @@ class LegoNavController : public MxCore {
protected:
friend class Extensions::ThirdPersonCamera::OrbitCamera;
friend class Extensions::ThirdPersonCamera::Controller;
float CalculateNewVel(float p_targetVel, float p_currentVel, float p_accel, float p_time);
float CalculateNewTargetVel(int p_pos, int p_center, float p_max);

View File

@ -11,6 +11,7 @@ class CharacterCloner;
}
namespace ThirdPersonCamera
{
class Controller;
class OrbitCamera;
}
} // namespace Extensions

View File

@ -74,6 +74,19 @@ class Controller {
bool ConsumeAutoDisable() { return m_input.ConsumeAutoDisable(); }
bool ConsumeAutoEnable() { return m_input.ConsumeAutoEnable(); }
bool IsLeftButtonHeld() const { return m_input.IsLeftButtonHeld(); }
bool IsLmbForwardEngaged() const { return m_lmbForwardEngaged; }
void SetLmbForwardEngaged(bool p_engaged) { m_lmbForwardEngaged = p_engaged; }
MxBool HandleFirstPersonForward(
LegoNavController* p_nav,
const Vector3& p_curPos,
const Vector3& p_curDir,
Vector3& p_newPos,
Vector3& p_newDir,
float p_deltaTime
);
float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); }
void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); }
void ResetTouchState() { m_input.ResetTouchState(); }
@ -106,6 +119,7 @@ class Controller {
bool m_enabled;
bool m_active;
bool m_pendingWorldTransition;
bool m_lmbForwardEngaged;
LegoROI* m_playerROI;
};

View File

@ -21,6 +21,8 @@ class InputHandler {
int GetTouchCount() const { return m_touch.count; }
SDL_FingerID GetFingerID(int p_idx) const { return m_touch.id[p_idx]; }
bool IsLeftButtonHeld() const { return m_leftButtonHeld; }
bool ConsumeAutoDisable();
bool ConsumeAutoEnable();
@ -43,6 +45,7 @@ class InputHandler {
bool m_wantsAutoDisable;
bool m_wantsAutoEnable;
bool m_rightButtonHeld;
bool m_leftButtonHeld;
float m_savedMouseX;
float m_savedMouseY;
};

View File

@ -36,7 +36,8 @@ class OrbitCamera {
Vector3& p_newPos,
Vector3& p_newDir,
float p_deltaTime,
bool p_isInMultiPartEmote
bool p_isInMultiPartEmote,
bool p_lmbHeld
);
void AdjustYaw(float p_delta) { m_absoluteYaw += p_delta; }

View File

@ -119,8 +119,15 @@ void ThirdPersonCameraExt::OnSDLEvent(SDL_Event* p_event)
s_camera->HandleSDLEventImpl(p_event);
if (p_event->type == SDL_EVENT_MOUSE_BUTTON_UP && p_event->button.button == SDL_BUTTON_LEFT) {
s_camera->SetLmbForwardEngaged(false);
}
if (s_camera->ConsumeAutoDisable()) {
s_camera->Disable(/*p_preserveTouch=*/true);
if (s_camera->IsLeftButtonHeld()) {
s_camera->SetLmbForwardEngaged(true);
}
}
else if (s_camera->ConsumeAutoEnable()) {
// Clear the movement system's touch state for camera-owned fingers only,
@ -145,6 +152,9 @@ void ThirdPersonCameraExt::OnSDLEvent(SDL_Event* p_event)
s_camera->SetOrbitDistance(ThirdPersonCamera::Controller::MIN_DISTANCE);
s_camera->Enable();
if (s_camera->IsLeftButtonHeld()) {
s_camera->SetLmbForwardEngaged(true);
}
}
}
@ -202,6 +212,9 @@ MxBool ThirdPersonCameraExt::HandleNavOverride(
}
if (!s_camera->IsActive()) {
if (s_camera->IsLmbForwardEngaged()) {
return s_camera->HandleFirstPersonForward(p_nav, p_curPos, p_curDir, p_newPos, p_newDir, p_deltaTime);
}
return FALSE;
}

View File

@ -30,7 +30,7 @@ using namespace Extensions::ThirdPersonCamera;
Controller::Controller()
: m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true, /*.propSuffix=*/0}), m_enabled(false),
m_active(false), m_pendingWorldTransition(false), m_playerROI(nullptr)
m_active(false), m_pendingWorldTransition(false), m_lmbForwardEngaged(false), m_playerROI(nullptr)
{
}
@ -59,6 +59,7 @@ void Controller::Deactivate()
m_active = false;
m_pendingWorldTransition = false;
m_lmbForwardEngaged = false;
m_animator.StopROISounds();
m_animator.StopClickAnimation();
m_display.DestroyDisplayClone();
@ -365,7 +366,8 @@ MxBool Controller::HandleCameraRelativeMovement(
p_newPos,
p_newDir,
p_deltaTime,
m_animator.IsInMultiPartEmote()
m_animator.IsInMultiPartEmote(),
m_input.IsLeftButtonHeld()
);
}
@ -374,6 +376,31 @@ void Controller::HandleSDLEventImpl(SDL_Event* p_event)
m_input.HandleSDLEvent(p_event, m_orbit, m_active);
}
MxBool Controller::HandleFirstPersonForward(
LegoNavController* p_nav,
const Vector3& p_curPos,
const Vector3& p_curDir,
Vector3& p_newPos,
Vector3& p_newDir,
float p_deltaTime
)
{
float accel = p_nav->m_maxLinearAccel;
p_nav->m_linearVel += accel * p_deltaTime;
if (p_nav->m_linearVel > p_nav->m_maxLinearVel) {
p_nav->m_linearVel = p_nav->m_maxLinearVel;
}
float speed = p_nav->m_linearVel * p_deltaTime;
p_newPos[0] = p_curPos[0] + p_curDir[0] * speed;
p_newPos[1] = p_curPos[1] + p_curDir[1] * speed;
p_newPos[2] = p_curPos[2] + p_curDir[2] * speed;
p_newDir = p_curDir;
p_nav->m_rotationalVel = 0.0f;
return TRUE;
}
void Controller::ReinitForCharacter()
{
if (!GameState() || IsRestrictedArea(GameState()->m_currentArea)) {

View File

@ -8,8 +8,8 @@
using namespace Extensions::ThirdPersonCamera;
InputHandler::InputHandler()
: m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false), m_rightButtonHeld(false), m_savedMouseX(0.0f),
m_savedMouseY(0.0f)
: m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false), m_rightButtonHeld(false),
m_leftButtonHeld(false), m_savedMouseX(0.0f), m_savedMouseY(0.0f)
{
}
@ -116,21 +116,23 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
case SDL_EVENT_MOUSE_BUTTON_UP: {
if (p_event->button.button == SDL_BUTTON_RIGHT) {
m_rightButtonHeld = p_event->button.down;
if (!p_active) {
break;
}
SDL_Window* window = SDL_GetWindowFromID(p_event->button.windowID);
if (window) {
if (m_rightButtonHeld) {
if (p_active) {
SDL_GetMouseState(&m_savedMouseX, &m_savedMouseY);
SDL_SetWindowRelativeMouseMode(window, true);
}
else {
}
else if (SDL_GetWindowRelativeMouseMode(window)) {
SDL_SetWindowRelativeMouseMode(window, false);
SDL_WarpMouseInWindow(window, m_savedMouseX, m_savedMouseY);
}
}
}
else if (p_event->button.button == SDL_BUTTON_LEFT) {
m_leftButtonHeld = p_event->button.down;
}
break;
}

View File

@ -147,7 +147,8 @@ MxBool OrbitCamera::HandleCameraRelativeMovement(
Vector3& p_newPos,
Vector3& p_newDir,
float p_deltaTime,
bool p_isInMultiPartEmote
bool p_isInMultiPartEmote,
bool p_lmbHeld
)
{
LegoInputManager* inputManager = InputManager();
@ -180,8 +181,12 @@ MxBool OrbitCamera::HandleCameraRelativeMovement(
moveDirX += camRightX;
moveDirZ += camRightZ;
}
if (p_lmbHeld) {
moveDirX += camForwardX;
moveDirZ += camForwardZ;
}
if (keyFlags == 0 && inputManager) {
if (keyFlags == 0 && !p_lmbHeld && inputManager) {
MxU32 joystickX, joystickY, povPosition;
if (inputManager->GetJoystickState(&joystickX, &joystickY, &povPosition) == SUCCESS) {
float jx = (joystickX - 50.0f) / 50.0f;