Add trails and projections

master
John Montagu, the 4th Earl of Sandvich 2026-03-15 22:05:41 -07:00
parent 4911bfe8d5
commit 3ee599caeb
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
7 changed files with 358 additions and 17 deletions

View File

@ -1,27 +1,137 @@
#include "components/PhysicsComponent.hpp"
#include "Entity.hpp"
#include "components/GravityWellComponent.hpp"
#include "components/NullZoneComponent.hpp"
#include "components/TransformComponent.hpp"
#include <algorithm>
#include <cmath>
void PhysicsComponent::Setup() {}
void PhysicsComponent::ClampVelocity(float &vxRef, float &vyRef) const {
const float speed = std::sqrt(vxRef * vxRef + vyRef * vyRef);
if (speed > speedCap && speed > 0.0f) {
const float scale = speedCap / speed;
vxRef *= scale;
vyRef *= scale;
}
}
void PhysicsComponent::ClampVelocity(double &vxRef, double &vyRef) const {
const double speed = std::sqrt(vxRef * vxRef + vyRef * vyRef);
if (speed > static_cast<double>(speedCap) && speed > 0.0) {
const double scale = static_cast<double>(speedCap) / speed;
vxRef *= scale;
vyRef *= scale;
}
}
void PhysicsComponent::IntegratePosition(float &xRef, float &yRef, float vxValue, float vyValue,
float dt) {
xRef += vxValue * dt;
yRef += vyValue * dt;
}
void PhysicsComponent::IntegratePosition(double &xRef, double &yRef, double vxValue, double vyValue,
double dt) {
xRef += vxValue * dt;
yRef += vyValue * dt;
}
bool PhysicsComponent::IsInsideNullZone(double xPos) const {
if (!context || !context->entities) {
return false;
}
for (auto &other : *context->entities) {
if (!other || other.get() == entity) {
continue;
}
auto zone = other->GetComponent<NullZoneComponent>();
auto zoneTransform = other->GetComponent<TransformComponent>();
if (!zone || !zoneTransform) {
continue;
}
const double left = static_cast<double>(zoneTransform->get().x);
const double right = left + static_cast<double>(zone->get().width);
if (xPos >= left && xPos <= right) {
return true;
}
}
return false;
}
bool PhysicsComponent::ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx,
double &dvy, bool ignoreWellActive) const {
dvx = 0.0;
dvy = 0.0;
if (!context || !context->wellEntity) {
return false;
}
auto wellTransform = context->wellEntity->GetComponent<TransformComponent>();
auto wellGravity = context->wellEntity->GetComponent<GravityWellComponent>();
if (!wellTransform || !wellGravity) {
return false;
}
if (!ignoreWellActive && !wellGravity->get().active) {
return false;
}
if (IsInsideNullZone(px)) {
return false;
}
const double dx = static_cast<double>(wellTransform->get().x) - px;
const double dy = static_cast<double>(wellTransform->get().y) - py;
const double dist = std::sqrt(dx * dx + dy * dy);
if (dist <= 0.0001) {
return false;
}
const double clampedDist = std::max(dist, static_cast<double>(wellGravity->get().minDist));
const double force = static_cast<double>(wellGravity->get().mass) / (clampedDist * clampedDist);
dvx = (dx / dist) * force * dt;
dvy = (dy / dist) * force * dt;
return true;
}
void PhysicsComponent::SimulateStep(double &xRef, double &yRef, double &vxRef, double &vyRef,
double dt, bool ignoreWellActive) const {
double dvx = 0.0;
double dvy = 0.0;
ComputeWellDeltaVelocity(xRef, yRef, dt, dvx, dvy, ignoreWellActive);
vxRef += dvx;
vyRef += dvy;
ClampVelocity(vxRef, vyRef);
IntegratePosition(xRef, yRef, vxRef, vyRef, dt);
}
void PhysicsComponent::Update(float dt) {
auto transform = entity->GetComponent<TransformComponent>();
if (!transform) {
return;
}
const float speed = std::sqrt(vx * vx + vy * vy);
if (speed > speedCap && speed > 0.0f) {
const float scale = speedCap / speed;
vx *= scale;
vy *= scale;
}
double x = static_cast<double>(transform->get().x);
double y = static_cast<double>(transform->get().y);
double vxLocal = static_cast<double>(vx);
double vyLocal = static_cast<double>(vy);
transform->get().x += vx * dt;
transform->get().y += vy * dt;
SimulateStep(x, y, vxLocal, vyLocal, static_cast<double>(dt), false);
transform->get().x = static_cast<float>(x);
transform->get().y = static_cast<float>(y);
vx = static_cast<float>(vxLocal);
vy = static_cast<float>(vyLocal);
}
void PhysicsComponent::Cleanup() {}

View File

@ -10,6 +10,38 @@ struct PhysicsComponent : public Component {
float vy = 0.0f;
float speedCap = 400.0f;
/**
* Clamps the provided velocity vector to this component's speed cap.
*/
void ClampVelocity(float &vxRef, float &vyRef) const;
void ClampVelocity(double &vxRef, double &vyRef) const;
/**
* Integrates a position by velocity over dt.
*/
static void IntegratePosition(float &xRef, float &yRef, float vxValue, float vyValue, float dt);
static void IntegratePosition(double &xRef, double &yRef, double vxValue, double vyValue,
double dt);
/**
* Returns true when the given X position is inside a null zone.
*/
bool IsInsideNullZone(double xPos) const;
/**
* Computes velocity delta from well gravity for a position.
* Returns false when gravity should not apply.
*/
bool ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx, double &dvy,
bool ignoreWellActive = false) const;
/**
* Performs one projected simulation step using well/null-zone rules,
* velocity clamp, and position integration.
*/
void SimulateStep(double &xRef, double &yRef, double &vxRef, double &vyRef, double dt,
bool ignoreWellActive = false) const;
void Setup() override;
void Update(float dt) override;
void Cleanup() override;

View File

@ -19,7 +19,8 @@ void ProbeStateComponent::Update(float) {
if (transform->get().y < -20.0f ||
transform->get().y > static_cast<float>(GetScreenHeight() + 20) ||
transform->get().x < -20.0f) {
transform->get().x < -20.0f ||
transform->get().x > static_cast<float>(GetScreenWidth() + 20)) {
if (context->onPlayerDeath) {
context->onPlayerDeath();
}

View File

@ -1,5 +1,74 @@
#include "components/ProjectionComponent.hpp"
void ProjectionComponent::Setup() {}
void ProjectionComponent::Update(float) {}
void ProjectionComponent::Cleanup() {}
#include "Entity.hpp"
#include "components/GravityReceiverComponent.hpp"
#include "components/GravityWellComponent.hpp"
#include "components/PhysicsComponent.hpp"
#include "components/TransformComponent.hpp"
void ProjectionComponent::Setup() { points.clear(); }
void ProjectionComponent::Update(float) {
points.clear();
highlightActive = false;
if (!context || !context->probeEntity || entity != context->probeEntity) {
return;
}
auto transform = entity->GetComponent<TransformComponent>();
auto physics = entity->GetComponent<PhysicsComponent>();
auto receiver = entity->GetComponent<GravityReceiverComponent>();
if (!transform || !physics || !receiver) {
return;
}
Entity *wellEntity = receiver->get().well ? receiver->get().well : context->wellEntity;
if (!wellEntity) {
return;
}
auto wellTransform = wellEntity->GetComponent<TransformComponent>();
auto wellGravity = wellEntity->GetComponent<GravityWellComponent>();
if (!wellTransform || !wellGravity) {
return;
}
// only highlight if the well is active, inactive will be a lighter color to show the potential
// projection if the player were to activate it
highlightActive = wellGravity->get().active;
double px = static_cast<double>(transform->get().x);
double py = static_cast<double>(transform->get().y);
double vx = static_cast<double>(physics->get().vx);
double vy = static_cast<double>(physics->get().vy);
points.reserve(static_cast<size_t>(steps));
for (int i = 0; i < steps; ++i) {
physics->get().SimulateStep(px, py, vx, vy, static_cast<double>(stepDt), true);
points.push_back({static_cast<float>(px), static_cast<float>(py)});
}
Draw();
}
void ProjectionComponent::Cleanup() { points.clear(); }
void ProjectionComponent::Draw() const {
if (points.empty()) {
return;
}
const Color base = highlightActive ? activeColor : inactiveColor;
for (size_t i = 0; i < points.size(); ++i) {
// t goes from 0 to 1 across the points, used for fading effect
const float t = (points.size() <= 1) ? 0.0f : (float)i / (points.size() - 1);
Color c = base;
float alphaScale = 1.0f - t;
// multiply alpha to make it fade towards the end
c.a = (unsigned char)((float)(base.a) * alphaScale);
DrawCircleV(points[i], pointRadius, c);
}
}

View File

@ -2,11 +2,25 @@
#include "Component.hpp"
#include "raylib.h"
#include <vector>
/**
* Placeholder for future trajectory projection behavior.
* Predicts and renders the probe's near-future trajectory.
*/
struct ProjectionComponent : public Component {
std::vector<Vector2> points;
int steps = 30;
float stepDt = 1.0f / 60.0f;
float pointRadius = 2.4f;
bool highlightActive = false;
Color inactiveColor = Color{125, 146, 178, 120};
Color activeColor = Color{160, 206, 255, 220};
void Setup() override;
void Update(float dt) override;
void Cleanup() override;
void Draw() const;
};

View File

@ -1,5 +1,94 @@
#include "components/TrailComponent.hpp"
void TrailComponent::Setup() {}
void TrailComponent::Update(float) {}
void TrailComponent::Cleanup() {}
#include "Entity.hpp"
#include "components/TransformComponent.hpp"
#include <algorithm>
#include <cmath>
void TrailComponent::Setup() {
points.clear();
elapsed = 0.0f;
lastSampleAt = 0.0f;
auto transform = entity->GetComponent<TransformComponent>();
if (!transform) {
return;
}
points.push_back({transform->get().x, transform->get().y, elapsed});
}
void TrailComponent::Update(float dt) {
elapsed += dt;
auto transform = entity->GetComponent<TransformComponent>();
if (!transform) {
return;
}
while (!points.empty() && (elapsed - points.front().createdAt) > pointLifetime) {
points.pop_front();
}
const float x = transform->get().x;
const float y = transform->get().y;
if (points.empty()) {
points.push_back({x, y, elapsed});
lastSampleAt = elapsed;
return;
}
const Point &last = points.back();
const float dx = x - last.x;
const float dy = y - last.y;
// check if we've moved far enough from the last point or if enough time has passed to warrant a
// new sample, for smoother trails when the probe is moving slowly
const bool movedEnough = (dx * dx + dy * dy) >= (sampleDistance * sampleDistance);
const bool waitedEnough = (elapsed - lastSampleAt) >= sampleInterval;
if (movedEnough || waitedEnough) {
points.push_back({x, y, elapsed});
lastSampleAt = elapsed;
} else {
points.back().x = x;
points.back().y = y;
points.back().createdAt = elapsed;
}
if (points.size() > maxPoints) {
while (points.size() > maxPoints) {
points.pop_front();
}
}
Draw();
}
void TrailComponent::Cleanup() {
points.clear();
elapsed = 0.0f;
lastSampleAt = 0.0f;
}
void TrailComponent::Draw() const {
if (points.size() < 2 || pointLifetime <= 0.0f) {
return;
}
for (size_t i = 1; i < points.size(); ++i) {
const Point &a = points[i - 1];
const Point &b = points[i];
const float ageA = elapsed - a.createdAt;
const float ageB = elapsed - b.createdAt;
const float lifeNormA = std::clamp(1.0f - (ageA / pointLifetime), 0.0f, 1.0f);
const float lifeNormB = std::clamp(1.0f - (ageB / pointLifetime), 0.0f, 1.0f);
const float lifeNorm = 0.5f * (lifeNormA + lifeNormB);
Color c = color;
c.a = (unsigned char)((float)(color.a) * lifeNorm);
DrawLineEx({a.x, a.y}, {b.x, b.y}, lineWidth, c);
}
}

View File

@ -2,11 +2,37 @@
#include "Component.hpp"
#include "raylib.h"
#include <cstddef>
#include <deque>
/**
* Placeholder for future probe trail visualization.
* Stores and renders a fading positional trail for moving entities.
*
* Points are timestamped and retired from the front of a queue once they
* exceed `pointLifetime`, so updates avoid touching every point each frame.
*/
struct TrailComponent : public Component {
struct Point {
float x = 0.0f;
float y = 0.0f;
float createdAt = 0.0f;
};
std::deque<Point> points;
float elapsed = 0.0f;
float lastSampleAt = 0.0f;
float sampleDistance = 5.0f;
float sampleInterval = 1.0f / 30.0f;
float pointLifetime = 0.85f;
float lineWidth = 2.0f;
std::size_t maxPoints = 96;
Color color = Color{120, 210, 255, 180};
void Setup() override;
void Update(float dt) override;
void Cleanup() override;
void Draw() const;
};