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'
friend class Extensions::MultiplayerExt;
friend class Extensions::ThirdPersonCameraExt;
private:
void InitializeHaptics();

View File

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

View File

@ -26,7 +26,7 @@ class Controller {
Controller();
void Enable();
void Disable();
void Disable(bool p_preserveTouch = false);
bool IsEnabled() const { return m_enabled; }
bool IsActive() const { return m_active; }
@ -77,10 +77,13 @@ class Controller {
float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); }
void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); }
void ResetTouchState() { m_input.ResetTouchState(); }
void SuppressGestures() { m_input.SuppressGestures(); }
bool TryClaimFinger(const SDL_TouchFingerEvent& event) { return m_input.TryClaimFinger(event, m_active); }
bool TryReleaseFinger(SDL_FingerID id) { return m_input.TryReleaseFinger(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 UnfreezeDisplayActor() { m_display.UnfreezeDisplayActor(); }

View File

@ -18,20 +18,26 @@ class InputHandler {
bool TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_active);
bool TryReleaseFinger(SDL_FingerID p_id);
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 ConsumeAutoEnable();
void ResetTouchState() { m_touch = {}; }
void SuppressGestures();
static constexpr float CAMERA_ZONE_X = 0.5f;
static constexpr float PINCH_TRANSITION_THRESHOLD = 0.03f;
private:
struct TouchState {
SDL_FingerID id[2];
float x[2], y[2];
bool synced[2];
int count;
float initialPinchDist;
float gesturePinchDist;
} m_touch;
bool m_wantsAutoDisable;

View File

@ -75,6 +75,15 @@ MxResult NetworkManager::Tickle()
if (cameraEnabled != m_lastCameraEnabled) {
m_lastCameraEnabled = 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

View File

@ -4,6 +4,7 @@
#include "extensions/thirdpersoncamera/controller.h"
#include "islepathactor.h"
#include "legoeventnotificationparam.h"
#include "legoinputmanager.h"
#include "legonavcontroller.h"
#include "legopathactor.h"
#include "legovideomanager.h"
@ -118,10 +119,29 @@ void ThirdPersonCameraExt::OnSDLEvent(SDL_Event* p_event)
s_camera->HandleSDLEventImpl(p_event);
if (s_camera->ConsumeAutoDisable()) {
s_camera->Disable();
s_camera->Disable(/*p_preserveTouch=*/true);
}
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->Enable();
}

View File

@ -39,7 +39,7 @@ void Controller::Enable()
ReinitForCharacter();
}
void Controller::Disable()
void Controller::Disable(bool p_preserveTouch)
{
m_enabled = false;
@ -60,7 +60,9 @@ void Controller::Disable()
m_animator.ClearAll();
m_orbit.ResetOrbitState();
m_input.ResetTouchState();
if (!p_preserveTouch) {
m_input.ResetTouchState();
}
}
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.x[idx] = p_event.x;
m_touch.y[idx] = p_event.y;
m_touch.synced[idx] = true;
m_touch.count++;
if (m_touch.count == 2) {
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;
}
return true;
@ -40,9 +42,11 @@ bool InputHandler::TryReleaseFinger(SDL_FingerID p_id)
m_touch.id[0] = m_touch.id[1];
m_touch.x[0] = m_touch.x[1];
m_touch.y[0] = m_touch.y[1];
m_touch.synced[0] = m_touch.synced[1];
}
m_touch.count--;
m_touch.initialPinchDist = 0.0f;
m_touch.gesturePinchDist = 0.0f;
return true;
}
}
@ -69,6 +73,14 @@ bool InputHandler::ConsumeAutoEnable()
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)
{
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.x[idx] = p_event->tfinger.x;
m_touch.y[idx] = p_event->tfinger.y;
m_touch.synced[idx] = true;
m_touch.count++;
if (m_touch.count == 2) {
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;
@ -134,6 +148,13 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
break;
}
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 oldY = m_touch.y[0];
m_touch.x[0] = p_event->tfinger.x;
@ -158,6 +179,20 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
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 oldY = m_touch.y[idx];
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;
if (!p_active) {
if (pinchDelta > 0) {
float totalDelta = m_touch.gesturePinchDist - newDist;
if (totalDelta > PINCH_TRANSITION_THRESHOLD) {
m_wantsAutoEnable = true;
m_touch.gesturePinchDist = newDist;
}
m_touch.initialPinchDist = newDist;
break;
}
if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE && pinchDelta < 0) {
m_wantsAutoDisable = true;
m_touch.initialPinchDist = newDist;
break;
if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE) {
float totalDelta = newDist - m_touch.gesturePinchDist;
if (totalDelta > PINCH_TRANSITION_THRESHOLD) {
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();
m_touch.initialPinchDist = newDist;
}