change to physics based ship movement

This commit is contained in:
2026-05-19 21:53:55 +02:00
parent d397b9969a
commit 34c6dea505
10 changed files with 183 additions and 44 deletions

View File

@@ -395,7 +395,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string mPath = elemPath + ".movement";
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
toml::table& mMt = const_cast<toml::table&>(mTable);
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_formula"], file, mPath + ".main_acceleration_formula");
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_formula"], file, mPath + ".maneuvering_acceleration_formula");
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_formula"], file, mPath + ".angular_acceleration_formula");
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_formula"], file, mPath + ".max_rotation_speed_formula");
}
// Sensor

View File

@@ -30,7 +30,11 @@ struct ShipHealth
struct ShipMovement
{
Formula speedFormula; // REQ-SHP-STATS, REQ-SHP-MOVEMENT
Formula speedFormula; // max linear speed cap, tiles/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
Formula mainAccelerationFormula; // forward acceleration, tiles/s²
Formula maneuveringAccelerationFormula;// omnidirectional acceleration, tiles/s²
Formula angularAccelerationFormula; // angular acceleration, rad/s²
Formula maxRotationSpeedFormula; // angular velocity cap, rad/s
};
struct ShipSensor

View File

@@ -75,10 +75,16 @@ struct Ship
EntityId id;
QVector2D position;
QVector2D velocity;
float facing; // heading in radians (0 = east/+x)
float rotationSpeed; // angular velocity in radians per tick
float hp;
float maxHp;
float speedPerTick; // pre-evaluated from speedFormula / kTickRateHz
float sensorRange; // pre-evaluated from sensorRangeFormula (REQ-SHP-SENSOR)
float maxSpeedPerTick; // linear speed cap (tiles/tick)
float mainAccelerationPerTick; // forward acceleration (tiles/tick²)
float maneuveringAccelerationPerTick; // omnidirectional acceleration (tiles/tick²)
float angularAccelerationPerTick; // angular acceleration (rad/tick²)
float maxRotationSpeedPerTick; // angular velocity cap (rad/tick)
float sensorRange; // pre-evaluated from sensorRangeFormula (REQ-SHP-SENSOR)
int level;
std::string schematicId;

View File

@@ -2,6 +2,7 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
#include <map>
#include <utility>
@@ -55,19 +56,29 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
const double x = static_cast<double>(level);
Ship ship;
ship.id = m_allocateId();
ship.position = position;
ship.velocity = QVector2D(0.0f, 0.0f);
ship.maxHp = static_cast<float>(def->health.hpFormula.evaluate(x));
ship.hp = ship.maxHp;
ship.speedPerTick = static_cast<float>(
def->movement.speedFormula.evaluate(x))
/ static_cast<float>(kTickRateHz);
ship.sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
ship.level = level;
ship.schematicId = schematicId;
ship.isEnemy = isEnemy;
ship.intent = MovementIntent{0, QVector2D(0.0f, 0.0f)};
ship.id = m_allocateId();
ship.position = position;
ship.velocity = QVector2D(0.0f, 0.0f);
ship.facing = 0.0f;
ship.rotationSpeed = 0.0f;
ship.maxHp = static_cast<float>(def->health.hpFormula.evaluate(x));
ship.hp = ship.maxHp;
const float tickRate = static_cast<float>(kTickRateHz);
ship.maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x))
/ tickRate;
ship.mainAccelerationPerTick = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x))
/ tickRate;
ship.maneuveringAccelerationPerTick = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x))
/ tickRate;
ship.angularAccelerationPerTick = static_cast<float>(def->movement.angularAccelerationFormula.evaluate(x))
/ tickRate;
ship.maxRotationSpeedPerTick = static_cast<float>(def->movement.maxRotationSpeedFormula.evaluate(x))
/ tickRate;
ship.sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
ship.level = level;
ship.schematicId = schematicId;
ship.isEnemy = isEnemy;
ship.intent = MovementIntent{0, QVector2D(0.0f, 0.0f)};
if (def->combat)
{
@@ -150,7 +161,11 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
applyMod(ship.maxHp, "hp");
ship.hp = ship.maxHp;
applyMod(ship.speedPerTick, "speed");
applyMod(ship.maxSpeedPerTick, "speed");
applyMod(ship.mainAccelerationPerTick, "main_acceleration");
applyMod(ship.maneuveringAccelerationPerTick, "maneuvering_acceleration");
applyMod(ship.angularAccelerationPerTick, "angular_acceleration");
applyMod(ship.maxRotationSpeedPerTick, "max_rotation_speed");
applyMod(ship.sensorRange, "sensor_range");
if (ship.weapon.has_value())
{
@@ -764,25 +779,98 @@ void ShipSystem::triggerRallyDeparture()
// tickMovement (tick-order step 10)
// ---------------------------------------------------------------------------
// Reduces angle to [-π, π].
static float wrapAngle(float a)
{
constexpr float kPi = 3.14159265f;
a = std::fmod(a, 2.0f * kPi);
if (a > kPi) { a -= 2.0f * kPi; }
if (a < -kPi) { a += 2.0f * kPi; }
return a;
}
void ShipSystem::tickMovement()
{
for (Ship& s : m_ships)
{
if (s.intent.priority == 0)
{
s.velocity = QVector2D(0.0f, 0.0f);
s.rotationSpeed = 0.0f;
continue;
}
const QVector2D delta = s.intent.target - s.position;
const float dist = delta.length();
if (dist < 0.001f)
{
s.velocity = QVector2D(0.0f, 0.0f);
continue;
}
QVector2D delta = s.intent.target - s.position;
float dist = delta.length();
if (dist <= s.speedPerTick)
// ── Rotate toward target ──────────────────────────────────────────
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - s.facing);
// Clamp angular acceleration, accumulate rotation speed.
const float rotDelta = std::max(-s.angularAccelerationPerTick,
std::min(angleDiff, s.angularAccelerationPerTick));
s.rotationSpeed += rotDelta;
s.rotationSpeed = std::max(-s.maxRotationSpeedPerTick,
std::min(s.rotationSpeed, s.maxRotationSpeedPerTick));
// Prevent overshooting the desired angle this tick.
const bool sameSign = (s.rotationSpeed >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(s.rotationSpeed) > std::abs(angleDiff))
{
s.rotationSpeed = angleDiff;
}
s.facing = wrapAngle(s.facing + s.rotationSpeed);
// ── Desired velocity (with braking near target) ───────────────────
// Stopping distance using maneuvering acceleration as the worst-case brake.
const float manAccel = s.maneuveringAccelerationPerTick;
const float stoppingDist = (s.maxSpeedPerTick * s.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: s.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - s.velocity;
// ── Main acceleration: forward only, along facing ─────────────────
const QVector2D facingVec(std::cos(s.facing), std::sin(s.facing));
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, s.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied;
// ── Maneuvering acceleration: any direction, handles the remainder ─
const QVector2D remaining = velError - mainDelta;
const float remainLen = remaining.length();
const QVector2D maneuverDelta = (remainLen > manAccel)
? remaining.normalized() * manAccel
: remaining;
s.velocity += mainDelta + maneuverDelta;
// ── Speed cap ─────────────────────────────────────────────────────
const float speed = s.velocity.length();
if (speed > s.maxSpeedPerTick)
{
s.velocity = s.velocity.normalized() * s.maxSpeedPerTick;
}
// ── Snap to target or advance ─────────────────────────────────────
if (dist <= s.velocity.length())
{
s.position = s.intent.target;
s.velocity = QVector2D(0.0f, 0.0f);
}
else
{
s.velocity = delta.normalized() * s.speedPerTick;
s.position += s.velocity;
}
}