split MovementSystem to MovementIntentSystem and DynamicBodySystem
This commit is contained in:
@@ -12,7 +12,8 @@
|
|||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "MovementSystem.h"
|
#include "DynamicBodySystem.h"
|
||||||
|
#include "MovementIntentSystem.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -45,7 +46,8 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
|
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>();
|
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_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
||||||
|
|
||||||
@@ -255,7 +257,8 @@ void ArenaSimulation::tick()
|
|||||||
tickDeaths();
|
tickDeaths();
|
||||||
|
|
||||||
// Movement (tick step 10).
|
// Movement (tick step 10).
|
||||||
m_movementSystem->tick(m_admin);
|
m_movementIntentSystem->tick(m_admin);
|
||||||
|
m_dynamicBodySystem->tick(m_admin);
|
||||||
|
|
||||||
// Scrap despawn (tick step 11).
|
// Scrap despawn (tick step 11).
|
||||||
m_scrapSystem->tickDespawn(m_currentTick);
|
m_scrapSystem->tickDespawn(m_currentTick);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
class AiSystem;
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
class MovementSystem;
|
class DynamicBodySystem;
|
||||||
|
class MovementIntentSystem;
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
|
|
||||||
@@ -92,7 +93,8 @@ private:
|
|||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<AiSystem> m_aiSystem;
|
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<CombatSystem> m_combatSystem;
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
@@ -314,9 +315,9 @@ void ArenaView::drawStations(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawShips(QPainter& painter)
|
void ArenaView::drawShips(QPainter& painter)
|
||||||
{
|
{
|
||||||
m_sim->admin().forEach<ShipIdentity, Position, Velocity, Faction>(
|
m_sim->admin().forEach<ShipIdentity, Position, DynamicBodyComponent, Faction>(
|
||||||
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos,
|
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos,
|
||||||
const Velocity& vel, const Faction& fac)
|
const DynamicBodyComponent& body, const Faction& fac)
|
||||||
{
|
{
|
||||||
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e);
|
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e);
|
||||||
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e);
|
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e);
|
||||||
@@ -326,8 +327,8 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
if (it == m_visuals->ships.end()) { return; }
|
if (it == m_visuals->ships.end()) { return; }
|
||||||
|
|
||||||
const QPointF center = worldToWidget(pos.value);
|
const QPointF center = worldToWidget(pos.value);
|
||||||
const QVector2D dir = (vel.value.length() > 0.0001f)
|
const QVector2D dir = (body.velocity.length() > 0.0001f)
|
||||||
? vel.value.normalized()
|
? body.velocity.normalized()
|
||||||
: QVector2D(1.0f, 0.0f);
|
: QVector2D(1.0f, 0.0f);
|
||||||
const QVector2D perp(-dir.y(), dir.x());
|
const QVector2D perp(-dir.y(), dir.x());
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EcsComponents.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EcsComponents.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||||
|
|||||||
25
src/lib/core/DynamicBodyComponent.h
Normal file
25
src/lib/core/DynamicBodyComponent.h
Normal 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
|
||||||
|
};
|
||||||
|
|
||||||
@@ -35,24 +35,9 @@ struct Faction
|
|||||||
// Ship components (always present on every ship)
|
// Ship components (always present on every ship)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
struct Velocity
|
|
||||||
{
|
|
||||||
QVector2D value;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Facing
|
struct Facing
|
||||||
{
|
{
|
||||||
float radians;
|
float radians;
|
||||||
float rotationSpeed;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ShipDynamics
|
|
||||||
{
|
|
||||||
float maxSpeedPerTick;
|
|
||||||
float mainAccelerationPerTick;
|
|
||||||
float maneuveringAccelerationPerTick;
|
|
||||||
float angularAccelerationPerTick;
|
|
||||||
float maxRotationSpeedPerTick;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SensorRange
|
struct SensorRange
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "MovementIntent.h"
|
#include "MovementIntent.h"
|
||||||
|
|
||||||
@@ -33,11 +34,19 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
|||||||
add<Position>(entity, Position{position});
|
add<Position>(entity, Position{position});
|
||||||
add<Health>(entity, Health{hp, maxHp});
|
add<Health>(entity, Health{hp, maxHp});
|
||||||
add<Faction>(entity, Faction{isEnemy});
|
add<Faction>(entity, Faction{isEnemy});
|
||||||
add<Velocity>(entity, Velocity{QVector2D(0.0f, 0.0f)});
|
add<Facing>(entity, Facing{0.0f});
|
||||||
add<Facing>(entity, Facing{0.0f, 0.0f});
|
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||||
add<ShipDynamics>(entity, ShipDynamics{
|
maxSpeedPerTick,
|
||||||
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick,
|
mainAccelPerTick,
|
||||||
angularAccelPerTick, maxRotationSpeedPerTick});
|
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<SensorRange>(entity, SensorRange{sensorRange});
|
||||||
add<ShipIdentity>(entity, ShipIdentity{level, schematicId});
|
add<ShipIdentity>(entity, ShipIdentity{level, schematicId});
|
||||||
add<MovementIntent>(entity, MovementIntent{0, QVector2D(0.0f, 0.0f)});
|
add<MovementIntent>(entity, MovementIntent{0, QVector2D(0.0f, 0.0f)});
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.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}/ScrapSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
||||||
@@ -25,7 +26,8 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.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}/ScrapSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
||||||
|
|||||||
56
src/lib/sim/DynamicBodySystem.cpp
Normal file
56
src/lib/sim/DynamicBodySystem.cpp
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
|
|
||||||
class MovementSystem
|
class DynamicBodySystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void tick(EntityAdmin& admin);
|
void tick(EntityAdmin& admin);
|
||||||
121
src/lib/sim/MovementIntentSystem.cpp
Normal file
121
src/lib/sim/MovementIntentSystem.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/lib/sim/MovementIntentSystem.h
Normal file
9
src/lib/sim/MovementIntentSystem.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class EntityAdmin;
|
||||||
|
|
||||||
|
class MovementIntentSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void tick(EntityAdmin& admin);
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "ModulesConfig.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);
|
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);
|
SensorRange& sensor = m_admin.get<SensorRange>(entity);
|
||||||
|
|
||||||
applyMod(health.maxHp, "hp");
|
applyMod(health.maxHp, "hp");
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "MovementSystem.h"
|
#include "DynamicBodySystem.h"
|
||||||
|
#include "MovementIntentSystem.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
@@ -47,7 +48,8 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>();
|
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_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||||
@@ -114,7 +116,8 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>();
|
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_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||||
@@ -181,7 +184,8 @@ void Simulation::tick()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 10: advance ship positions
|
// Step 10: advance ship positions
|
||||||
m_movementSystem->tick(m_admin);
|
m_movementIntentSystem->tick(m_admin);
|
||||||
|
m_dynamicBodySystem->tick(m_admin);
|
||||||
|
|
||||||
// Step 11: scrap despawn
|
// Step 11: scrap despawn
|
||||||
m_scrapSystem->tickDespawn(m_currentTick);
|
m_scrapSystem->tickDespawn(m_currentTick);
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
class AiSystem;
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
class MovementSystem;
|
class DynamicBodySystem;
|
||||||
|
class MovementIntentSystem;
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
class WaveSystem;
|
class WaveSystem;
|
||||||
@@ -124,7 +125,8 @@ private:
|
|||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<AiSystem> m_aiSystem;
|
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<ScrapSystem> m_scrapSystem;
|
||||||
std::unique_ptr<WaveSystem> m_waveSystem;
|
std::unique_ptr<WaveSystem> m_waveSystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
|
#include "DynamicBodySystem.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "MovementSystem.h"
|
#include "MovementIntentSystem.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
@@ -39,7 +41,8 @@ struct Fixture
|
|||||||
BuildingSystem buildings;
|
BuildingSystem buildings;
|
||||||
ShipSystem ships;
|
ShipSystem ships;
|
||||||
AiSystem ai;
|
AiSystem ai;
|
||||||
MovementSystem movement;
|
MovementIntentSystem movementIntent;
|
||||||
|
DynamicBodySystem dynamicBody;
|
||||||
ScrapSystem scraps;
|
ScrapSystem scraps;
|
||||||
Tick tick;
|
Tick tick;
|
||||||
|
|
||||||
@@ -68,7 +71,8 @@ struct Fixture
|
|||||||
ai.tickThreatResponseBehavior(admin, buildings);
|
ai.tickThreatResponseBehavior(admin, buildings);
|
||||||
ai.tickRepairBehavior(admin, buildings);
|
ai.tickRepairBehavior(admin, buildings);
|
||||||
ai.tickSalvageBehavior(admin, scraps, buildings);
|
ai.tickSalvageBehavior(admin, scraps, buildings);
|
||||||
movement.tick(admin);
|
movementIntent.tick(admin);
|
||||||
|
dynamicBody.tick(admin);
|
||||||
++tick;
|
++tick;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -115,9 +119,10 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
const float speed = f.admin.get<ShipDynamics>(e).maxSpeedPerTick;
|
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick;
|
||||||
f.admin.get<MovementIntent>(e) = MovementIntent{1, QVector2D(100.0f, 0.0f)};
|
f.admin.get<MovementIntent>(e) = MovementIntent{1, QVector2D(100.0f, 0.0f)};
|
||||||
f.movement.tick(f.admin);
|
f.movementIntent.tick(f.admin);
|
||||||
|
f.dynamicBody.tick(f.admin);
|
||||||
|
|
||||||
REQUIRE(pos(f.admin, e).value.x() == Approx(speed));
|
REQUIRE(pos(f.admin, e).value.x() == Approx(speed));
|
||||||
REQUIRE(pos(f.admin, e).value.y() == Approx(0.0f));
|
REQUIRE(pos(f.admin, e).value.y() == Approx(0.0f));
|
||||||
@@ -129,10 +134,11 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
const float speed = f.admin.get<ShipDynamics>(e).maxSpeedPerTick;
|
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick;
|
||||||
const QVector2D target(speed * 0.5f, 0.0f);
|
const QVector2D target(speed * 0.5f, 0.0f);
|
||||||
f.admin.get<MovementIntent>(e) = MovementIntent{1, target};
|
f.admin.get<MovementIntent>(e) = MovementIntent{1, target};
|
||||||
f.movement.tick(f.admin);
|
f.movementIntent.tick(f.admin);
|
||||||
|
f.dynamicBody.tick(f.admin);
|
||||||
|
|
||||||
REQUIRE(pos(f.admin, e).value.x() == Approx(target.x()));
|
REQUIRE(pos(f.admin, e).value.x() == Approx(target.x()));
|
||||||
REQUIRE(pos(f.admin, e).value.y() == Approx(target.y()));
|
REQUIRE(pos(f.admin, e).value.y() == Approx(target.y()));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EcsComponents.h"
|
#include "EcsComponents.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
@@ -83,7 +84,7 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTi
|
|||||||
|
|
||||||
// speed_formula = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30
|
// speed_formula = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30
|
||||||
const float expected = 200.0f / static_cast<float>(kTickRateHz);
|
const float expected = 200.0f / static_cast<float>(kTickRateHz);
|
||||||
REQUIRE(admin.get<ShipDynamics>(e).maxSpeedPerTick == Approx(expected));
|
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeedPerTick == Approx(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user