split MovementSystem to MovementIntentSystem and DynamicBodySystem

This commit is contained in:
2026-05-25 07:25:54 +02:00
parent f5f4453e2c
commit 0cd0529468
18 changed files with 277 additions and 154 deletions

View File

@@ -4,6 +4,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/EcsComponents.h
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h

View File

@@ -0,0 +1,25 @@
#pragma once
#include <optional>
#include <QVector2D>
struct DynamicBodyComponent
{
// --- dynamics parameters (formerly ShipDynamics) ---
float maxSpeedPerTick;
float mainAccelerationPerTick;
float maneuveringAccelerationPerTick;
float angularAccelerationPerTick;
float maxRotationSpeedPerTick;
// --- integrated state ---
QVector2D velocity;
float angularVelocity;
// --- written each tick by MovementIntentSystem, consumed by DynamicBodySystem ---
QVector2D linearAcceleration;
float angularAcceleration;
std::optional<QVector2D> snapTarget; // set when snap-to-target will fire this tick
};

View File

@@ -35,24 +35,9 @@ struct Faction
// Ship components (always present on every ship)
// ---------------------------------------------------------------------------
struct Velocity
{
QVector2D value;
};
struct Facing
{
float radians;
float rotationSpeed;
};
struct ShipDynamics
{
float maxSpeedPerTick;
float mainAccelerationPerTick;
float maneuveringAccelerationPerTick;
float angularAccelerationPerTick;
float maxRotationSpeedPerTick;
};
struct SensorRange

View File

@@ -1,5 +1,6 @@
#include "EntityAdmin.h"
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "MovementIntent.h"
@@ -33,11 +34,19 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
add<Position>(entity, Position{position});
add<Health>(entity, Health{hp, maxHp});
add<Faction>(entity, Faction{isEnemy});
add<Velocity>(entity, Velocity{QVector2D(0.0f, 0.0f)});
add<Facing>(entity, Facing{0.0f, 0.0f});
add<ShipDynamics>(entity, ShipDynamics{
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick,
angularAccelPerTick, maxRotationSpeedPerTick});
add<Facing>(entity, Facing{0.0f});
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
maxSpeedPerTick,
mainAccelPerTick,
maneuveringAccelPerTick,
angularAccelPerTick,
maxRotationSpeedPerTick,
QVector2D(0.0f, 0.0f), // velocity
0.0f, // angularVelocity
QVector2D(0.0f, 0.0f), // linearAcceleration
0.0f, // angularAcceleration
std::nullopt // snapTarget
});
add<SensorRange>(entity, SensorRange{sensorRange});
add<ShipIdentity>(entity, ShipIdentity{level, schematicId});
add<MovementIntent>(entity, MovementIntent{0, QVector2D(0.0f, 0.0f)});

View File

@@ -10,7 +10,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
@@ -25,7 +26,8 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MovementSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp

View File

@@ -0,0 +1,56 @@
#include "DynamicBodySystem.h"
#include <cmath>
#include <QVector2D>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
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 DynamicBodySystem::tick(EntityAdmin& admin)
{
admin.forEach<Position, Facing, DynamicBodyComponent>(
[](entt::entity /*e*/, Position& pos, Facing& facing, DynamicBodyComponent& body)
{
// Integrate angular velocity and advance facing.
body.angularVelocity += body.angularAcceleration;
facing.radians = wrapAngle(facing.radians + body.angularVelocity);
// Integrate linear velocity.
body.velocity += body.linearAcceleration;
// Speed cap.
const float speed = body.velocity.length();
if (speed > body.maxSpeedPerTick)
{
body.velocity = body.velocity.normalized() * body.maxSpeedPerTick;
}
// Snap to target or advance position.
if (body.snapTarget.has_value())
{
pos.value = body.snapTarget.value();
body.velocity = QVector2D(0.0f, 0.0f);
}
else
{
pos.value += body.velocity;
}
// Reset per-tick fields so stale values don't linger if the intent
// system is skipped for this entity in a future tick.
body.linearAcceleration = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f;
body.snapTarget = std::nullopt;
});
}

View File

@@ -2,7 +2,7 @@
class EntityAdmin;
class MovementSystem
class DynamicBodySystem
{
public:
void tick(EntityAdmin& admin);

View File

@@ -0,0 +1,121 @@
#include "MovementIntentSystem.h"
#include <algorithm>
#include <cmath>
#include <QVector2D>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "MovementIntent.h"
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 MovementIntentSystem::tick(EntityAdmin& admin)
{
admin.forEach<Position, Facing, DynamicBodyComponent, MovementIntent>(
[](entt::entity /*e*/, const Position& pos, const Facing& facing,
DynamicBodyComponent& body, const MovementIntent& intent)
{
if (intent.priority == 0)
{
body.velocity = QVector2D(0.0f, 0.0f);
body.angularVelocity = 0.0f;
body.linearAcceleration = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f;
body.snapTarget = std::nullopt;
return;
}
const QVector2D delta = intent.target - pos.value;
const float dist = delta.length();
if (dist < 0.001f)
{
body.velocity = QVector2D(0.0f, 0.0f);
body.linearAcceleration = QVector2D(0.0f, 0.0f);
body.snapTarget = std::nullopt;
return;
}
// --- Angular acceleration ---
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - facing.radians);
const float rotDelta = std::max(-body.angularAccelerationPerTick,
std::min(angleDiff, body.angularAccelerationPerTick));
float newAngVel = body.angularVelocity + rotDelta;
newAngVel = std::max(-body.maxRotationSpeedPerTick,
std::min(newAngVel, body.maxRotationSpeedPerTick));
const bool sameSign = (newAngVel >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(newAngVel) > std::abs(angleDiff))
{
newAngVel = angleDiff;
}
body.angularAcceleration = newAngVel - body.angularVelocity;
// --- Linear acceleration ---
// Use the projected facing (after this tick's angular integration) so
// that the main thruster aligns with where the ship will actually be
// pointing when DynamicBodySystem applies the forces — matching the
// original single-pass ordering.
const float projectedRadians = wrapAngle(facing.radians + newAngVel);
const QVector2D facingVec(std::cos(projectedRadians), std::sin(projectedRadians));
const float manAccel = body.maneuveringAccelerationPerTick;
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: body.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - body.velocity;
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, body.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied;
const QVector2D remaining = velError - mainDelta;
const float remainLen = remaining.length();
const QVector2D maneuverDelta = (remainLen > manAccel)
? remaining.normalized() * manAccel
: remaining;
body.linearAcceleration = mainDelta + maneuverDelta;
// --- Snap detection ---
// Compute the prospective velocity after integration + speed cap to
// determine whether the ship will reach (or pass) the target this tick.
QVector2D prospectiveVel = body.velocity + body.linearAcceleration;
const float prospectiveSpeed = prospectiveVel.length();
if (prospectiveSpeed > body.maxSpeedPerTick)
{
prospectiveVel = prospectiveVel.normalized() * body.maxSpeedPerTick;
}
if (dist <= prospectiveVel.length())
{
body.snapTarget = intent.target;
}
else
{
body.snapTarget = std::nullopt;
}
});
}

View File

@@ -0,0 +1,9 @@
#pragma once
class EntityAdmin;
class MovementIntentSystem
{
public:
void tick(EntityAdmin& admin);
};

View File

@@ -1,105 +0,0 @@
#include "MovementSystem.h"
#include <algorithm>
#include <cmath>
#include <QVector2D>
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "MovementIntent.h"
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 MovementSystem::tick(EntityAdmin& admin)
{
admin.forEach<Position, Velocity, Facing, ShipDynamics, MovementIntent>(
[](entt::entity /*e*/, Position& pos, Velocity& vel, Facing& facing,
ShipDynamics& dynamics, MovementIntent& intent)
{
if (intent.priority == 0)
{
vel.value = QVector2D(0.0f, 0.0f);
facing.rotationSpeed = 0.0f;
return;
}
const QVector2D delta = intent.target - pos.value;
const float dist = delta.length();
if (dist < 0.001f)
{
vel.value = QVector2D(0.0f, 0.0f);
return;
}
// Rotate toward target.
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - facing.radians);
const float rotDelta = std::max(-dynamics.angularAccelerationPerTick,
std::min(angleDiff, dynamics.angularAccelerationPerTick));
facing.rotationSpeed += rotDelta;
facing.rotationSpeed = std::max(-dynamics.maxRotationSpeedPerTick,
std::min(facing.rotationSpeed, dynamics.maxRotationSpeedPerTick));
const bool sameSign = (facing.rotationSpeed >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(facing.rotationSpeed) > std::abs(angleDiff))
{
facing.rotationSpeed = angleDiff;
}
facing.radians = wrapAngle(facing.radians + facing.rotationSpeed);
// Desired velocity (with braking near target).
const float manAccel = dynamics.maneuveringAccelerationPerTick;
const float stoppingDist = (dynamics.maxSpeedPerTick * dynamics.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: dynamics.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - vel.value;
// Main acceleration: forward only, along facing.
const QVector2D facingVec(std::cos(facing.radians), std::sin(facing.radians));
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, dynamics.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;
vel.value += mainDelta + maneuverDelta;
// Speed cap.
const float speed = vel.value.length();
if (speed > dynamics.maxSpeedPerTick)
{
vel.value = vel.value.normalized() * dynamics.maxSpeedPerTick;
}
// Snap to target or advance.
if (dist <= vel.value.length())
{
pos.value = intent.target;
vel.value = QVector2D(0.0f, 0.0f);
}
else
{
pos.value += vel.value;
}
});
}

View File

@@ -4,6 +4,7 @@
#include <map>
#include <utility>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "ModulesConfig.h"
@@ -146,7 +147,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVecto
};
Health& health = m_admin.get<Health>(entity);
ShipDynamics& dynamics = m_admin.get<ShipDynamics>(entity);
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
SensorRange& sensor = m_admin.get<SensorRange>(entity);
applyMod(health.maxHp, "hp");

View File

@@ -6,7 +6,8 @@
#include "BuildingSystem.h"
#include "EcsComponents.h"
#include "CombatSystem.h"
#include "MovementSystem.h"
#include "DynamicBodySystem.h"
#include "MovementIntentSystem.h"
#include "ScrapSystem.h"
#include "ShipSystem.h"
#include "SurfaceMask.h"
@@ -47,7 +48,8 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
@@ -114,7 +116,8 @@ void Simulation::reset(unsigned int seed)
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
@@ -181,7 +184,8 @@ void Simulation::tick()
}
// Step 10: advance ship positions
m_movementSystem->tick(m_admin);
m_movementIntentSystem->tick(m_admin);
m_dynamicBodySystem->tick(m_admin);
// Step 11: scrap despawn
m_scrapSystem->tickDespawn(m_currentTick);

View File

@@ -23,7 +23,8 @@
class AiSystem;
class BuildingSystem;
class CombatSystem;
class MovementSystem;
class DynamicBodySystem;
class MovementIntentSystem;
class ShipSystem;
class ScrapSystem;
class WaveSystem;
@@ -124,7 +125,8 @@ private:
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementSystem> m_movementSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
std::unique_ptr<ScrapSystem> m_scrapSystem;
std::unique_ptr<WaveSystem> m_waveSystem;
std::unique_ptr<CombatSystem> m_combatSystem;