mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 10:33:57 +00:00
Add free camera controls (#10)
* Add free camera orbit controls to multiplayer third-person camera Replace the fixed camera offset with dynamic orbit parameters (yaw, pitch, distance) computed from spherical coordinates. Mouse wheel controls zoom, right-click drag controls orbit, and two-finger touch gestures support pinch-zoom and orbit on touchscreens. The controls are always active when third-person mode is enabled. A single SDL event forwarding hook is added to the main event loop; all other changes are contained within the multiplayer extension. https://claude.ai/code/session_013FyPCrJSaHxiJwdfGBVnYP * Fix linker error and refactor orbit camera controls Move HandleSDLEvent extension call from ISLE (no EXTENSIONS define) into LegoInputManager::UpdateLastInputMethod in LEGO1 to fix unresolved symbol errors. DRY up orbit camera code with ClampPitch/ClampDistance/ ResetOrbitState/ApplyOrbitCamera helpers. Simplify direction vector computation. Fix mouse orbit yaw direction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fixes --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
eb6d2b8728
commit
e0a1ac781f
@ -805,6 +805,8 @@ void LegoInputManager::InitializeHaptics()
|
||||
|
||||
void LegoInputManager::UpdateLastInputMethod(SDL_Event* p_event)
|
||||
{
|
||||
Extension<MultiplayerExt>::Call(HandleSDLEvent, p_event);
|
||||
|
||||
switch (p_event->type) {
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "extensions/extensions.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
@ -63,6 +64,9 @@ class MultiplayerExt {
|
||||
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
||||
static MxBool CheckRejected();
|
||||
|
||||
// Forwards SDL events to the third-person camera for orbit controls.
|
||||
static void HandleSDLEvent(SDL_Event* p_event);
|
||||
|
||||
static void SetNetworkManager(Multiplayer::NetworkManager* p_networkManager);
|
||||
static Multiplayer::NetworkManager* GetNetworkManager();
|
||||
|
||||
@ -88,6 +92,7 @@ constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
|
||||
constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad;
|
||||
constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded;
|
||||
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
||||
constexpr auto HandleSDLEvent = &MultiplayerExt::HandleSDLEvent;
|
||||
#else
|
||||
constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
||||
@ -102,6 +107,7 @@ constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullp
|
||||
constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
||||
constexpr decltype(&MultiplayerExt::HandleSDLEvent) HandleSDLEvent = nullptr;
|
||||
#endif
|
||||
|
||||
}; // namespace Extensions
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
#include "extensions/multiplayer/animutils.h"
|
||||
#include "extensions/multiplayer/customizestate.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "mxgeometry/mxgeometry3d.h"
|
||||
#include "mxgeometry/mxmatrix.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@ -53,7 +55,17 @@ class ThirdPersonCamera {
|
||||
void OnWorldEnabled(LegoWorld* p_world);
|
||||
void OnWorldDisabled(LegoWorld* p_world);
|
||||
|
||||
// Free camera input handling
|
||||
void HandleSDLEvent(SDL_Event* p_event);
|
||||
|
||||
private:
|
||||
// Orbit camera helpers
|
||||
void ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const;
|
||||
void ApplyOrbitCamera();
|
||||
void ResetOrbitState();
|
||||
void ClampPitch();
|
||||
void ClampDistance();
|
||||
|
||||
using AnimCache = AnimUtils::AnimCache;
|
||||
|
||||
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
||||
@ -108,6 +120,28 @@ class ThirdPersonCamera {
|
||||
LegoROI* m_rideVehicleROI;
|
||||
|
||||
std::map<std::string, AnimCache> m_animCacheMap;
|
||||
|
||||
// Orbit camera state
|
||||
float m_orbitYaw;
|
||||
float m_orbitPitch;
|
||||
float m_orbitDistance;
|
||||
|
||||
// Touch gesture tracking
|
||||
struct TouchState {
|
||||
SDL_FingerID id[2];
|
||||
float x[2], y[2];
|
||||
int count;
|
||||
float initialPinchDist;
|
||||
} m_touch;
|
||||
|
||||
static constexpr float DEFAULT_ORBIT_YAW = 0.0f;
|
||||
static constexpr float DEFAULT_ORBIT_PITCH = 0.3f;
|
||||
static constexpr float DEFAULT_ORBIT_DISTANCE = 3.5f;
|
||||
static constexpr float ORBIT_TARGET_HEIGHT = 1.5f;
|
||||
static constexpr float MIN_PITCH = 0.05f;
|
||||
static constexpr float MAX_PITCH = 1.4f;
|
||||
static constexpr float MIN_DISTANCE = 1.5f;
|
||||
static constexpr float MAX_DISTANCE = 15.0f;
|
||||
};
|
||||
|
||||
} // namespace Multiplayer
|
||||
|
||||
@ -304,6 +304,13 @@ MxBool MultiplayerExt::IsClonedCharacter(const char* p_name)
|
||||
return s_networkManager->IsClonedCharacter(p_name) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
void MultiplayerExt::HandleSDLEvent(SDL_Event* p_event)
|
||||
{
|
||||
if (s_networkManager && s_networkManager->GetThirdPersonCamera().IsActive()) {
|
||||
s_networkManager->GetThirdPersonCamera().HandleSDLEvent(p_event);
|
||||
}
|
||||
}
|
||||
|
||||
MxBool MultiplayerExt::CheckRejected()
|
||||
{
|
||||
if (s_networkManager && s_networkManager->WasRejected()) {
|
||||
|
||||
@ -42,7 +42,8 @@ ThirdPersonCamera::ThirdPersonCamera()
|
||||
m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f),
|
||||
m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false),
|
||||
m_clickAnimObjectId(0), m_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
|
||||
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr)
|
||||
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_orbitYaw(DEFAULT_ORBIT_YAW),
|
||||
m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), m_touch{}
|
||||
{
|
||||
SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName));
|
||||
}
|
||||
@ -97,6 +98,8 @@ void ThirdPersonCamera::Disable()
|
||||
ClearRideAnimation();
|
||||
m_animCacheMap.clear();
|
||||
ClearAnimCaches();
|
||||
|
||||
ResetOrbitState();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
||||
@ -254,6 +257,9 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
|
||||
return;
|
||||
}
|
||||
|
||||
// Update orbit camera position each frame so it tracks the player
|
||||
ApplyOrbitCamera();
|
||||
|
||||
// Small vehicle with ride animation (like RemotePlayer)
|
||||
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||
StopClickAnimation();
|
||||
@ -544,12 +550,8 @@ void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
|
||||
return;
|
||||
}
|
||||
|
||||
// Camera behind the character; +z in ROI-local is behind the model
|
||||
// after TurnAround. Movement inversion in CalculateTransform corrects controls.
|
||||
Mx3DPointFloat at(0.0f, 2.5f, 3.0f);
|
||||
Mx3DPointFloat dir(0.0f, -0.3f, -1.0f);
|
||||
Mx3DPointFloat up(0.0f, 1.0f, 0.0f);
|
||||
|
||||
Mx3DPointFloat at, dir, up;
|
||||
ComputeOrbitVectors(at, dir, up);
|
||||
world->GetCameraController()->SetWorldTransform(at, dir, up);
|
||||
p_actor->TransformPointOfView();
|
||||
}
|
||||
@ -677,6 +679,172 @@ void ThirdPersonCamera::ApplyIdleFrame0()
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
float cosP = cosf(m_orbitPitch);
|
||||
float sinP = sinf(m_orbitPitch);
|
||||
float sinY = sinf(m_orbitYaw);
|
||||
float cosY = cosf(m_orbitYaw);
|
||||
|
||||
p_at = Mx3DPointFloat(
|
||||
m_orbitDistance * sinY * cosP,
|
||||
ORBIT_TARGET_HEIGHT + m_orbitDistance * sinP,
|
||||
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);
|
||||
|
||||
p_up = Mx3DPointFloat(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ApplyOrbitCamera()
|
||||
{
|
||||
LegoPathActor* actor = UserActor();
|
||||
LegoWorld* world = CurrentWorld();
|
||||
if (!actor || !world || !world->GetCameraController()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mx3DPointFloat at, dir, up;
|
||||
ComputeOrbitVectors(at, dir, up);
|
||||
world->GetCameraController()->SetWorldTransform(at, dir, up);
|
||||
actor->TransformPointOfView();
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ResetOrbitState()
|
||||
{
|
||||
m_orbitYaw = DEFAULT_ORBIT_YAW;
|
||||
m_orbitPitch = DEFAULT_ORBIT_PITCH;
|
||||
m_orbitDistance = DEFAULT_ORBIT_DISTANCE;
|
||||
m_touch = {};
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ClampPitch()
|
||||
{
|
||||
if (m_orbitPitch < MIN_PITCH) {
|
||||
m_orbitPitch = MIN_PITCH;
|
||||
}
|
||||
if (m_orbitPitch > MAX_PITCH) {
|
||||
m_orbitPitch = MAX_PITCH;
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ClampDistance()
|
||||
{
|
||||
if (m_orbitDistance < MIN_DISTANCE) {
|
||||
m_orbitDistance = MIN_DISTANCE;
|
||||
}
|
||||
if (m_orbitDistance > MAX_DISTANCE) {
|
||||
m_orbitDistance = MAX_DISTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event)
|
||||
{
|
||||
switch (p_event->type) {
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
m_orbitDistance -= p_event->wheel.y * 0.5f;
|
||||
ClampDistance();
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (p_event->motion.state & SDL_BUTTON_RMASK) {
|
||||
m_orbitYaw += p_event->motion.xrel * 0.005f;
|
||||
m_orbitPitch += p_event->motion.yrel * 0.005f;
|
||||
ClampPitch();
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_FINGER_DOWN: {
|
||||
if (m_touch.count < 2) {
|
||||
int idx = m_touch.count;
|
||||
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.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 = sqrtf(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_FINGER_MOTION: {
|
||||
if (m_touch.count == 2) {
|
||||
// Find which finger moved
|
||||
int idx = -1;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (m_touch.id[i] == p_event->tfinger.fingerID) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
float oldX = m_touch.x[idx];
|
||||
float oldY = m_touch.y[idx];
|
||||
m_touch.x[idx] = p_event->tfinger.x;
|
||||
m_touch.y[idx] = p_event->tfinger.y;
|
||||
|
||||
// Pinch zoom
|
||||
float dx = m_touch.x[1] - m_touch.x[0];
|
||||
float dy = m_touch.y[1] - m_touch.y[0];
|
||||
float newDist = sqrtf(dx * dx + dy * dy);
|
||||
|
||||
if (m_touch.initialPinchDist > 0.001f) {
|
||||
float pinchDelta = m_touch.initialPinchDist - newDist;
|
||||
m_orbitDistance += pinchDelta * 15.0f;
|
||||
ClampDistance();
|
||||
m_touch.initialPinchDist = newDist;
|
||||
}
|
||||
|
||||
// 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_orbitPitch += moveY * 2.0f;
|
||||
ClampPitch();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
case SDL_EVENT_FINGER_CANCELED: {
|
||||
for (int i = 0; i < m_touch.count; i++) {
|
||||
if (m_touch.id[i] == p_event->tfinger.fingerID) {
|
||||
// Shift remaining finger down
|
||||
if (i == 0 && m_touch.count == 2) {
|
||||
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.count--;
|
||||
m_touch.initialPinchDist = 0.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ThirdPersonCamera::ReinitForCharacter()
|
||||
{
|
||||
LegoPathActor* userActor = UserActor();
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
#include "misc/legostorage.h"
|
||||
#include "mxmisc.h"
|
||||
#include "mxvariable.h"
|
||||
#include "mxvariabletable.h"
|
||||
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <cstdio>
|
||||
|
||||
@ -79,4 +79,6 @@ SDL_MouseID_v: "SDL-based name"
|
||||
SDL_JoystickID_v: "SDL-based name"
|
||||
SDL_TouchID_v: "SDL-based name"
|
||||
Load: "Not a variable but function name"
|
||||
HandleCreate: "Not a variable but function name"
|
||||
HandleCreate: "Not a variable but function name"
|
||||
HandleBeforeSaveLoad: "Not a variable but function name"
|
||||
HandleSaveLoaded: "Not a variable but function name"
|
||||
Loading…
Reference in New Issue
Block a user