extract the scroll position into WorldCamera
This commit is contained in:
@@ -15,6 +15,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
72
src/lib/core/WorldCamera.cpp
Normal file
72
src/lib/core/WorldCamera.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "WorldCamera.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Linearly blend from valueAt0 (for x <= x0) to valueAt1 (for x >= x1), clamped
|
||||
// outside [x0, x1]. A zero- or negative-width band collapses to a hard step at x1.
|
||||
float lerpClamped(float valueAt0, float valueAt1, float x0, float x1, float x)
|
||||
{
|
||||
if (x1 <= x0) { return x < x1 ? valueAt0 : valueAt1; }
|
||||
const float t = std::max(0.0f, std::min(1.0f, (x - x0) / (x1 - x0)));
|
||||
return valueAt0 + (valueAt1 - valueAt0) * t;
|
||||
}
|
||||
}
|
||||
|
||||
WorldCamera::WorldCamera(const WorldScroll& scroll, const WorldRegions& regions)
|
||||
: m_scroll(&scroll)
|
||||
, m_regions(®ions)
|
||||
, m_viewCenterXTiles(0.0f)
|
||||
{
|
||||
}
|
||||
|
||||
bool WorldCamera::advance(PanDirection direction, qint64 elapsedMs,
|
||||
ScrollBounds bounds)
|
||||
{
|
||||
const float before = m_viewCenterXTiles;
|
||||
|
||||
if (direction != PanDirection::None)
|
||||
{
|
||||
const float distance =
|
||||
getPanSpeedTilesPerSecondAt(m_viewCenterXTiles, bounds.rightTiles)
|
||||
* static_cast<float>(elapsedMs) / 1000.0f;
|
||||
m_viewCenterXTiles += (direction == PanDirection::Left) ? -distance : distance;
|
||||
}
|
||||
|
||||
m_viewCenterXTiles = std::max(bounds.leftTiles,
|
||||
std::min(m_viewCenterXTiles, bounds.rightTiles));
|
||||
return m_viewCenterXTiles != before;
|
||||
}
|
||||
|
||||
float WorldCamera::getViewCenterXTiles() const
|
||||
{
|
||||
return m_viewCenterXTiles;
|
||||
}
|
||||
|
||||
void WorldCamera::reset()
|
||||
{
|
||||
m_viewCenterXTiles = 0.0f;
|
||||
}
|
||||
|
||||
float WorldCamera::getPanSpeedTilesPerSecondAt(float viewCenterXTiles,
|
||||
float contestZoneRightEdgeTiles) const
|
||||
{
|
||||
// Slow near the asteroid/player buffer, fast across the contest zone, with a
|
||||
// linear ramp straddling each contest-zone boundary (REQ-UI-SCROLL-SPEED). The
|
||||
// contest zone spans from the player buffer's right edge to the enemy stations,
|
||||
// the latter tracked live so the ramp follows the front line as it is pushed.
|
||||
const float slow = static_cast<float>(m_scroll->panSpeedSlow_tps);
|
||||
const float fast = static_cast<float>(m_scroll->panSpeedFast_tps);
|
||||
const float half = static_cast<float>(m_scroll->panRampBandWidth_tiles) / 2.0f;
|
||||
const float leftEdge = static_cast<float>(m_regions->playerBufferWidth_tiles);
|
||||
const float rightEdge = contestZoneRightEdgeTiles;
|
||||
|
||||
// Rising ramp at the left boundary (slow -> fast) and falling ramp at the right
|
||||
// boundary (fast -> slow); their minimum yields flat-slow outside, flat-fast in
|
||||
// the middle, and — if the bands overlap in a narrow contest zone — a single peak
|
||||
// below the fast speed where the two ramps cross.
|
||||
const float leftRamp = lerpClamped(slow, fast, leftEdge - half, leftEdge + half, viewCenterXTiles);
|
||||
const float rightRamp = lerpClamped(fast, slow, rightEdge - half, rightEdge + half, viewCenterXTiles);
|
||||
return std::min(leftRamp, rightRamp);
|
||||
}
|
||||
72
src/lib/core/WorldCamera.h
Normal file
72
src/lib/core/WorldCamera.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "WorldConfig.h"
|
||||
|
||||
// Which way the player is currently panning the view (REQ-UI-SCROLL). A plain
|
||||
// direction rather than key state: the camera is deliberately agnostic about how
|
||||
// the intent was expressed, so rebindable controls would change nothing here.
|
||||
enum class PanDirection
|
||||
{
|
||||
None,
|
||||
Left,
|
||||
Right
|
||||
};
|
||||
|
||||
// The horizontal limits of the view center, in world tiles (REQ-GW-SCROLL-LIMIT):
|
||||
// the view can pan left until the asteroid's left edge is centered and right until
|
||||
// the enemy stations are. Both move as the game progresses — the left edge with
|
||||
// asteroid expansion (REQ-GW-ASTEROID-EXPAND), the right edge as stations are
|
||||
// pushed back (REQ-GW-PUSH-EXPAND) — so they are supplied per frame by the caller
|
||||
// that can see the simulation, rather than queried here. That keeps the camera a
|
||||
// value with no simulation dependency.
|
||||
struct ScrollBounds
|
||||
{
|
||||
float leftTiles;
|
||||
float rightTiles;
|
||||
};
|
||||
|
||||
// Horizontal view position for the game world (REQ-UI-SCROLL, REQ-UI-SCROLL-SPEED).
|
||||
// Works purely in world units — tiles and tiles per second, never pixels. Turning
|
||||
// the resulting position into a widget transform is WorldCoordinates' job; the two
|
||||
// meet only where the view feeds getViewCenterXTiles() into that transform.
|
||||
class WorldCamera
|
||||
{
|
||||
public:
|
||||
// Both config structs are referenced rather than copied: they live inside the
|
||||
// Simulation's GameConfig, which is assigned in place on restart
|
||||
// (REQ-CFG-RELOAD), so a camera built once still picks up reloaded tuning.
|
||||
WorldCamera(const WorldScroll& scroll, const WorldRegions& regions);
|
||||
|
||||
// Pans by `direction` for `elapsedMs` of wall-clock time, then clamps into
|
||||
// `bounds`. Wall clock rather than ticks because panning is presentation only
|
||||
// (REQ-UI-NO-ZOOM's sibling concern) and keeps working while the simulation is
|
||||
// paused. Clamping happens on every call, not just when panning, so the view
|
||||
// follows the bounds inward when they shrink.
|
||||
//
|
||||
// Returns true when the view center actually moved — including when it moved
|
||||
// only because the bounds did. Callers use that to refresh anything anchored to
|
||||
// the world under a stationary cursor, such as the box-select rectangle.
|
||||
bool advance(PanDirection direction, qint64 elapsedMs, ScrollBounds bounds);
|
||||
|
||||
// World X (tiles) at the center of the viewport.
|
||||
float getViewCenterXTiles() const;
|
||||
|
||||
// Returns the view to the start-of-run position. Deliberately does not clamp:
|
||||
// a new run's bounds are not known here, and the next advance() clamps anyway.
|
||||
void reset();
|
||||
|
||||
// Pan speed at a given view center, in tiles/s (REQ-UI-SCROLL-SPEED). Public
|
||||
// because the ramp shape is the subtle part of this class and is worth testing
|
||||
// directly; advance() uses it internally. `contestZoneRightEdgeTiles` is the
|
||||
// live right-hand boundary — the same value as ScrollBounds::rightTiles — so
|
||||
// the ramp follows the front line as it is pushed.
|
||||
float getPanSpeedTilesPerSecondAt(float viewCenterXTiles,
|
||||
float contestZoneRightEdgeTiles) const;
|
||||
|
||||
private:
|
||||
const WorldScroll* m_scroll;
|
||||
const WorldRegions* m_regions;
|
||||
float m_viewCenterXTiles;
|
||||
};
|
||||
Reference in New Issue
Block a user