fix mutually canceling orbits
This commit is contained in:
@@ -11,4 +11,7 @@ struct MovementIntentComponent
|
|||||||
bool active = false;
|
bool active = false;
|
||||||
QVector2D target; // straight-line destination, or orbit center when orbitRadius_tiles > 0
|
QVector2D target; // straight-line destination, or orbit center when orbitRadius_tiles > 0
|
||||||
float orbitRadius_tiles = 0.0f; // 0 ⇒ go straight to target; >0 ⇒ orbit target at this radius
|
float orbitRadius_tiles = 0.0f; // 0 ⇒ go straight to target; >0 ⇒ orbit target at this radius
|
||||||
|
QVector2D orbitCenterVelocity_tpt; // velocity of the orbit center (0 for a static center); the orbit
|
||||||
|
// sense is resolved relative to this so a moving target's own motion
|
||||||
|
// does not bias it
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
|||||||
QVector2D destination = intent.target;
|
QVector2D destination = intent.target;
|
||||||
if (intent.orbitRadius_tiles > 0.0f)
|
if (intent.orbitRadius_tiles > 0.0f)
|
||||||
{
|
{
|
||||||
const float sign = OrbitMath::resolveOrbitSign(pos.value, intent.target,
|
const float sign = OrbitMath::resolveOrbitSign(
|
||||||
body.velocity_tpt);
|
pos.value, intent.target, body.velocity_tpt,
|
||||||
|
intent.orbitCenterVelocity_tpt);
|
||||||
destination = OrbitMath::computeOrbitDestination(
|
destination = OrbitMath::computeOrbitDestination(
|
||||||
pos.value, intent.target, intent.orbitRadius_tiles, sign);
|
pos.value, intent.target, intent.orbitRadius_tiles, sign);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "AttackBehavior.h"
|
#include "AttackBehavior.h"
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentComponent.h"
|
#include "MovementIntentComponent.h"
|
||||||
@@ -27,12 +28,17 @@ void AttackExecutor::execute(EntityAdmin& admin)
|
|||||||
const entt::entity t = *attack.currentTarget;
|
const entt::entity t = *attack.currentTarget;
|
||||||
QVector2D center = pos.value;
|
QVector2D center = pos.value;
|
||||||
float radius = 0.0f;
|
float radius = 0.0f;
|
||||||
|
QVector2D centerVelocity;
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||||
{
|
{
|
||||||
center = admin.get<PositionComponent>(t).value;
|
center = admin.get<PositionComponent>(t).value;
|
||||||
radius = attack.orbitRadius_tiles;
|
radius = attack.orbitRadius_tiles;
|
||||||
|
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||||
|
{
|
||||||
|
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
intent = MovementIntentComponent{true, center, radius};
|
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Weapons: assign the behavior target only if it is within this weapon's range.
|
// Weapons: assign the behavior target only if it is within this weapon's range.
|
||||||
|
|||||||
@@ -18,24 +18,32 @@ namespace OrbitMath
|
|||||||
constexpr float kOrbitLeadAngle_rad = 0.6f;
|
constexpr float kOrbitLeadAngle_rad = 0.6f;
|
||||||
|
|
||||||
// Returns the orbit sense (+1 counter-clockwise, -1 clockwise) that matches
|
// Returns the orbit sense (+1 counter-clockwise, -1 clockwise) that matches
|
||||||
// the ship's current movement around `center`, so steering reinforces the
|
// the ship's movement around `center`, so steering reinforces the motion the
|
||||||
// motion the ship already has. When the velocity is nearly radial or near
|
// ship already has. The sense is taken from the ship's velocity *relative to
|
||||||
// zero (e.g. a head-on approach or a freshly spawned ship) the sense is
|
// the center* (`centerVelocity`): for a moving target this both removes the
|
||||||
|
// target's own motion from the decision and dissolves the degenerate case
|
||||||
|
// where two ships orbiting each other translate in a straight line — there
|
||||||
|
// their shared velocity cancels, leaving ~zero relative velocity. When the
|
||||||
|
// relative velocity is nearly radial or near zero (a head-on approach, a
|
||||||
|
// freshly spawned ship, or that mutual-translation case) the sense is
|
||||||
// ill-defined; this is an unstable point the ship leaves within a tick or
|
// ill-defined; this is an unstable point the ship leaves within a tick or
|
||||||
// two, so a deterministic fallback of +1 is returned.
|
// two, so a deterministic fallback of +1 is returned.
|
||||||
inline float resolveOrbitSign(const QVector2D& shipPos, const QVector2D& center,
|
inline float resolveOrbitSign(const QVector2D& shipPos, const QVector2D& center,
|
||||||
const QVector2D& velocity)
|
const QVector2D& velocity,
|
||||||
|
const QVector2D& centerVelocity = QVector2D())
|
||||||
{
|
{
|
||||||
const QVector2D radial = shipPos - center;
|
const QVector2D radial = shipPos - center;
|
||||||
|
const QVector2D relativeVelocity = velocity - centerVelocity;
|
||||||
const float radialLength = radial.length();
|
const float radialLength = radial.length();
|
||||||
const float velocityLength = velocity.length();
|
const float velocityLength = relativeVelocity.length();
|
||||||
if (radialLength < 1.0e-4f || velocityLength < 1.0e-4f)
|
if (radialLength < 1.0e-4f || velocityLength < 1.0e-4f)
|
||||||
{
|
{
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// z-component of radial x velocity, normalised to sin(angle) between them.
|
// z-component of radial x relativeVelocity, normalised to sin(angle).
|
||||||
const float cross = radial.x() * velocity.y() - radial.y() * velocity.x();
|
const float cross = radial.x() * relativeVelocity.y()
|
||||||
|
- radial.y() * relativeVelocity.x();
|
||||||
const float sinAngle = cross / (radialLength * velocityLength);
|
const float sinAngle = cross / (radialLength * velocityLength);
|
||||||
|
|
||||||
constexpr float kRadialEpsilon = 1.0e-3f;
|
constexpr float kRadialEpsilon = 1.0e-3f;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "RepairExecutor.h"
|
#include "RepairExecutor.h"
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentComponent.h"
|
#include "MovementIntentComponent.h"
|
||||||
@@ -27,12 +28,17 @@ void RepairExecutor::execute(EntityAdmin& admin)
|
|||||||
const entt::entity t = *repair.currentTarget;
|
const entt::entity t = *repair.currentTarget;
|
||||||
QVector2D center = pos.value;
|
QVector2D center = pos.value;
|
||||||
float radius = 0.0f;
|
float radius = 0.0f;
|
||||||
|
QVector2D centerVelocity;
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||||
{
|
{
|
||||||
center = admin.get<PositionComponent>(t).value;
|
center = admin.get<PositionComponent>(t).value;
|
||||||
radius = repair.orbitRadius_tiles;
|
radius = repair.orbitRadius_tiles;
|
||||||
|
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||||
|
{
|
||||||
|
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
intent = MovementIntentComponent{true, center, radius};
|
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Repair tools: prefer the behavior target if it is within tool range.
|
// Repair tools: prefer the behavior target if it is within tool range.
|
||||||
|
|||||||
@@ -1260,3 +1260,22 @@ TEST_CASE("OrbitMath: orbit sign follows the ship's tangential velocity",
|
|||||||
// Zero velocity (e.g. freshly spawned) → fallback +1.
|
// Zero velocity (e.g. freshly spawned) → fallback +1.
|
||||||
REQUIRE(OrbitMath::resolveOrbitSign(shipPos, center, QVector2D(0.0f, 0.0f)) == Approx(1.0f));
|
REQUIRE(OrbitMath::resolveOrbitSign(shipPos, center, QVector2D(0.0f, 0.0f)) == Approx(1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("OrbitMath: orbit sense uses velocity relative to a moving center",
|
||||||
|
"[orbit]")
|
||||||
|
{
|
||||||
|
const QVector2D center(0.0f, 0.0f);
|
||||||
|
const QVector2D shipPos(5.0f, 0.0f); // radial points +x
|
||||||
|
|
||||||
|
// Two ships orbiting each other can translate in parallel: the ship and the
|
||||||
|
// center share the same velocity, so relative velocity cancels → fallback +1
|
||||||
|
// (both ships agree on the sign and break into a real mutual orbit).
|
||||||
|
const QVector2D sharedVelocity(0.0f, 3.0f);
|
||||||
|
REQUIRE(OrbitMath::resolveOrbitSign(shipPos, center, sharedVelocity, sharedVelocity)
|
||||||
|
== Approx(1.0f));
|
||||||
|
|
||||||
|
// A moving center's own motion is removed: the ship is stationary while the
|
||||||
|
// center drifts +y, so relative motion is -y → clockwise → -1.
|
||||||
|
REQUIRE(OrbitMath::resolveOrbitSign(shipPos, center, QVector2D(0.0f, 0.0f),
|
||||||
|
QVector2D(0.0f, 3.0f)) == Approx(-1.0f));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user