Fix mobile camera zoom/transition and name bubble issues

- Reduce pinch zoom sensitivity (15x → 6x multiplier)
- Add cumulative deadzone threshold for 1st/3rd person transitions
  to prevent accidental mode switches from slight finger movement
- Preserve camera touch tracking through 3rd→1st transition so the
  same fingers can pinch back without lifting (seamless round-trip)
- On 1st→3rd transition, selectively clear only camera-owned fingers
  from LegoInputManager's touch scheme state, preserving any active
  left-side movement finger
- Suppress camera gesture processing until finger positions re-sync
  after transition to prevent camera jumps from stale coordinates
- Hide local player name bubble when transitioning to 1st person,
  restore on transition back to 3rd person

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-13 19:10:49 -07:00
parent 577fa09a3b
commit 74271aa189
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
8 changed files with 94 additions and 11 deletions

View File

@ -180,6 +180,7 @@ class LegoInputManager : public MxPresenter {
// LegoInputManager::`scalar deleting destructor' // LegoInputManager::`scalar deleting destructor'
friend class Extensions::MultiplayerExt; friend class Extensions::MultiplayerExt;
friend class Extensions::ThirdPersonCameraExt;
private: private:
void InitializeHaptics(); void InitializeHaptics();

View File

@ -4,6 +4,7 @@
namespace Extensions namespace Extensions
{ {
class MultiplayerExt; class MultiplayerExt;
class ThirdPersonCameraExt;
namespace Common namespace Common
{ {
class CharacterCloner; class CharacterCloner;

View File

@ -26,7 +26,7 @@ class Controller {
Controller(); Controller();
void Enable(); void Enable();
void Disable(); void Disable(bool p_preserveTouch = false);
bool IsEnabled() const { return m_enabled; } bool IsEnabled() const { return m_enabled; }
bool IsActive() const { return m_active; } bool IsActive() const { return m_active; }
@ -77,10 +77,13 @@ class Controller {
float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); } float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); }
void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); } void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); }
void ResetTouchState() { m_input.ResetTouchState(); } void ResetTouchState() { m_input.ResetTouchState(); }
void SuppressGestures() { m_input.SuppressGestures(); }
bool TryClaimFinger(const SDL_TouchFingerEvent& event) { return m_input.TryClaimFinger(event, m_active); } bool TryClaimFinger(const SDL_TouchFingerEvent& event) { return m_input.TryClaimFinger(event, m_active); }
bool TryReleaseFinger(SDL_FingerID id) { return m_input.TryReleaseFinger(id); } bool TryReleaseFinger(SDL_FingerID id) { return m_input.TryReleaseFinger(id); }
bool IsFingerTracked(SDL_FingerID id) const { return m_input.IsFingerTracked(id); } bool IsFingerTracked(SDL_FingerID id) const { return m_input.IsFingerTracked(id); }
int GetTouchCount() const { return m_input.GetTouchCount(); }
SDL_FingerID GetFingerID(int idx) const { return m_input.GetFingerID(idx); }
void FreezeDisplayActor() { m_display.FreezeDisplayActor(); } void FreezeDisplayActor() { m_display.FreezeDisplayActor(); }
void UnfreezeDisplayActor() { m_display.UnfreezeDisplayActor(); } void UnfreezeDisplayActor() { m_display.UnfreezeDisplayActor(); }

View File

@ -18,20 +18,26 @@ class InputHandler {
bool TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_active); bool TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_active);
bool TryReleaseFinger(SDL_FingerID p_id); bool TryReleaseFinger(SDL_FingerID p_id);
bool IsFingerTracked(SDL_FingerID p_id) const; bool IsFingerTracked(SDL_FingerID p_id) const;
int GetTouchCount() const { return m_touch.count; }
SDL_FingerID GetFingerID(int p_idx) const { return m_touch.id[p_idx]; }
bool ConsumeAutoDisable(); bool ConsumeAutoDisable();
bool ConsumeAutoEnable(); bool ConsumeAutoEnable();
void ResetTouchState() { m_touch = {}; } void ResetTouchState() { m_touch = {}; }
void SuppressGestures();
static constexpr float CAMERA_ZONE_X = 0.5f; static constexpr float CAMERA_ZONE_X = 0.5f;
static constexpr float PINCH_TRANSITION_THRESHOLD = 0.03f;
private: private:
struct TouchState { struct TouchState {
SDL_FingerID id[2]; SDL_FingerID id[2];
float x[2], y[2]; float x[2], y[2];
bool synced[2];
int count; int count;
float initialPinchDist; float initialPinchDist;
float gesturePinchDist;
} m_touch; } m_touch;
bool m_wantsAutoDisable; bool m_wantsAutoDisable;

View File

@ -75,6 +75,15 @@ MxResult NetworkManager::Tickle()
if (cameraEnabled != m_lastCameraEnabled) { if (cameraEnabled != m_lastCameraEnabled) {
m_lastCameraEnabled = cameraEnabled; m_lastCameraEnabled = cameraEnabled;
NotifyThirdPersonChanged(cameraEnabled); NotifyThirdPersonChanged(cameraEnabled);
if (m_localNameBubble) {
if (!cameraEnabled) {
m_localNameBubble->SetVisible(false);
}
else if (m_showNameBubbles) {
m_localNameBubble->SetVisible(true);
}
}
} }
// Create local name bubble when display ROI becomes available // Create local name bubble when display ROI becomes available

View File

@ -4,6 +4,7 @@
#include "extensions/thirdpersoncamera/controller.h" #include "extensions/thirdpersoncamera/controller.h"
#include "islepathactor.h" #include "islepathactor.h"
#include "legoeventnotificationparam.h" #include "legoeventnotificationparam.h"
#include "legoinputmanager.h"
#include "legonavcontroller.h" #include "legonavcontroller.h"
#include "legopathactor.h" #include "legopathactor.h"
#include "legovideomanager.h" #include "legovideomanager.h"
@ -118,10 +119,29 @@ void ThirdPersonCameraExt::OnSDLEvent(SDL_Event* p_event)
s_camera->HandleSDLEventImpl(p_event); s_camera->HandleSDLEventImpl(p_event);
if (s_camera->ConsumeAutoDisable()) { if (s_camera->ConsumeAutoDisable()) {
s_camera->Disable(); s_camera->Disable(/*p_preserveTouch=*/true);
} }
else if (s_camera->ConsumeAutoEnable()) { else if (s_camera->ConsumeAutoEnable()) {
s_camera->ResetTouchState(); // Clear the movement system's touch state for camera-owned fingers only,
// so any virtual thumbstick input from 1st-person mode is zeroed while
// leaving a left-side movement finger intact.
LegoInputManager* im = InputManager();
if (im) {
for (int i = 0; i < s_camera->GetTouchCount(); i++) {
SDL_FingerID fid = s_camera->GetFingerID(i);
if (im->m_touchFinger == fid) {
im->m_touchFinger = 0;
im->m_touchVirtualThumb = {0, 0};
im->m_touchVirtualThumbOrigin = {0, 0};
}
im->m_touchFlags.erase(fid);
}
}
// Suppress camera gestures until finger positions re-sync to avoid
// a camera jump from stale positions carried through the transition.
s_camera->SuppressGestures();
s_camera->SetOrbitDistance(ThirdPersonCamera::Controller::MIN_DISTANCE); s_camera->SetOrbitDistance(ThirdPersonCamera::Controller::MIN_DISTANCE);
s_camera->Enable(); s_camera->Enable();
} }

View File

@ -39,7 +39,7 @@ void Controller::Enable()
ReinitForCharacter(); ReinitForCharacter();
} }
void Controller::Disable() void Controller::Disable(bool p_preserveTouch)
{ {
m_enabled = false; m_enabled = false;
@ -60,7 +60,9 @@ void Controller::Disable()
m_animator.ClearAll(); m_animator.ClearAll();
m_orbit.ResetOrbitState(); m_orbit.ResetOrbitState();
m_input.ResetTouchState(); if (!p_preserveTouch) {
m_input.ResetTouchState();
}
} }
void Controller::OnActorEnter(IslePathActor* p_actor) void Controller::OnActorEnter(IslePathActor* p_actor)

View File

@ -21,12 +21,14 @@ bool InputHandler::TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_ac
m_touch.id[idx] = p_event.fingerID; m_touch.id[idx] = p_event.fingerID;
m_touch.x[idx] = p_event.x; m_touch.x[idx] = p_event.x;
m_touch.y[idx] = p_event.y; m_touch.y[idx] = p_event.y;
m_touch.synced[idx] = true;
m_touch.count++; m_touch.count++;
if (m_touch.count == 2) { if (m_touch.count == 2) {
float dx = m_touch.x[1] - m_touch.x[0]; float dx = m_touch.x[1] - m_touch.x[0];
float dy = m_touch.y[1] - m_touch.y[0]; float dy = m_touch.y[1] - m_touch.y[0];
m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy);
m_touch.gesturePinchDist = m_touch.initialPinchDist;
} }
return true; return true;
@ -40,9 +42,11 @@ bool InputHandler::TryReleaseFinger(SDL_FingerID p_id)
m_touch.id[0] = m_touch.id[1]; m_touch.id[0] = m_touch.id[1];
m_touch.x[0] = m_touch.x[1]; m_touch.x[0] = m_touch.x[1];
m_touch.y[0] = m_touch.y[1]; m_touch.y[0] = m_touch.y[1];
m_touch.synced[0] = m_touch.synced[1];
} }
m_touch.count--; m_touch.count--;
m_touch.initialPinchDist = 0.0f; m_touch.initialPinchDist = 0.0f;
m_touch.gesturePinchDist = 0.0f;
return true; return true;
} }
} }
@ -69,6 +73,14 @@ bool InputHandler::ConsumeAutoEnable()
return std::exchange(m_wantsAutoEnable, false); return std::exchange(m_wantsAutoEnable, false);
} }
void InputHandler::SuppressGestures()
{
m_touch.synced[0] = false;
m_touch.synced[1] = false;
m_touch.initialPinchDist = 0.0f;
m_touch.gesturePinchDist = 0.0f;
}
void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool p_active) void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool p_active)
{ {
switch (p_event->type) { switch (p_event->type) {
@ -117,12 +129,14 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
m_touch.id[idx] = p_event->tfinger.fingerID; m_touch.id[idx] = p_event->tfinger.fingerID;
m_touch.x[idx] = p_event->tfinger.x; m_touch.x[idx] = p_event->tfinger.x;
m_touch.y[idx] = p_event->tfinger.y; m_touch.y[idx] = p_event->tfinger.y;
m_touch.synced[idx] = true;
m_touch.count++; m_touch.count++;
if (m_touch.count == 2) { if (m_touch.count == 2) {
float dx = m_touch.x[1] - m_touch.x[0]; float dx = m_touch.x[1] - m_touch.x[0];
float dy = m_touch.y[1] - m_touch.y[0]; float dy = m_touch.y[1] - m_touch.y[0];
m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy);
m_touch.gesturePinchDist = m_touch.initialPinchDist;
} }
} }
break; break;
@ -134,6 +148,13 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
break; break;
} }
if (m_touch.id[0] == p_event->tfinger.fingerID) { if (m_touch.id[0] == p_event->tfinger.fingerID) {
if (!m_touch.synced[0]) {
m_touch.x[0] = p_event->tfinger.x;
m_touch.y[0] = p_event->tfinger.y;
m_touch.synced[0] = true;
break;
}
float oldX = m_touch.x[0]; float oldX = m_touch.x[0];
float oldY = m_touch.y[0]; float oldY = m_touch.y[0];
m_touch.x[0] = p_event->tfinger.x; m_touch.x[0] = p_event->tfinger.x;
@ -158,6 +179,20 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
break; break;
} }
if (!m_touch.synced[idx]) {
m_touch.x[idx] = p_event->tfinger.x;
m_touch.y[idx] = p_event->tfinger.y;
m_touch.synced[idx] = true;
if (m_touch.synced[0] && m_touch.synced[1]) {
float dx = m_touch.x[1] - m_touch.x[0];
float dy = m_touch.y[1] - m_touch.y[0];
m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy);
m_touch.gesturePinchDist = m_touch.initialPinchDist;
}
break;
}
float oldX = m_touch.x[idx]; float oldX = m_touch.x[idx];
float oldY = m_touch.y[idx]; float oldY = m_touch.y[idx];
m_touch.x[idx] = p_event->tfinger.x; m_touch.x[idx] = p_event->tfinger.x;
@ -171,20 +206,26 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
float pinchDelta = m_touch.initialPinchDist - newDist; float pinchDelta = m_touch.initialPinchDist - newDist;
if (!p_active) { if (!p_active) {
if (pinchDelta > 0) { float totalDelta = m_touch.gesturePinchDist - newDist;
if (totalDelta > PINCH_TRANSITION_THRESHOLD) {
m_wantsAutoEnable = true; m_wantsAutoEnable = true;
m_touch.gesturePinchDist = newDist;
} }
m_touch.initialPinchDist = newDist; m_touch.initialPinchDist = newDist;
break; break;
} }
if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE && pinchDelta < 0) { if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE) {
m_wantsAutoDisable = true; float totalDelta = newDist - m_touch.gesturePinchDist;
m_touch.initialPinchDist = newDist; if (totalDelta > PINCH_TRANSITION_THRESHOLD) {
break; m_wantsAutoDisable = true;
m_touch.initialPinchDist = newDist;
m_touch.gesturePinchDist = newDist;
break;
}
} }
p_orbit.AdjustDistance(pinchDelta * 15.0f); p_orbit.AdjustDistance(pinchDelta * 6.0f);
p_orbit.ClampDistance(); p_orbit.ClampDistance();
m_touch.initialPinchDist = newDist; m_touch.initialPinchDist = newDist;
} }