Add trails and projections
parent
4911bfe8d5
commit
3ee599caeb
|
|
@ -1,27 +1,137 @@
|
||||||
#include "components/PhysicsComponent.hpp"
|
#include "components/PhysicsComponent.hpp"
|
||||||
|
|
||||||
#include "Entity.hpp"
|
#include "Entity.hpp"
|
||||||
|
#include "components/GravityWellComponent.hpp"
|
||||||
|
#include "components/NullZoneComponent.hpp"
|
||||||
#include "components/TransformComponent.hpp"
|
#include "components/TransformComponent.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
void PhysicsComponent::Setup() {}
|
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) {
|
void PhysicsComponent::Update(float dt) {
|
||||||
auto transform = entity->GetComponent<TransformComponent>();
|
auto transform = entity->GetComponent<TransformComponent>();
|
||||||
if (!transform) {
|
if (!transform) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float speed = std::sqrt(vx * vx + vy * vy);
|
double x = static_cast<double>(transform->get().x);
|
||||||
if (speed > speedCap && speed > 0.0f) {
|
double y = static_cast<double>(transform->get().y);
|
||||||
const float scale = speedCap / speed;
|
double vxLocal = static_cast<double>(vx);
|
||||||
vx *= scale;
|
double vyLocal = static_cast<double>(vy);
|
||||||
vy *= scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
transform->get().x += vx * dt;
|
SimulateStep(x, y, vxLocal, vyLocal, static_cast<double>(dt), false);
|
||||||
transform->get().y += vy * dt;
|
|
||||||
|
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() {}
|
void PhysicsComponent::Cleanup() {}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,38 @@ struct PhysicsComponent : public Component {
|
||||||
float vy = 0.0f;
|
float vy = 0.0f;
|
||||||
float speedCap = 400.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 Setup() override;
|
||||||
void Update(float dt) override;
|
void Update(float dt) override;
|
||||||
void Cleanup() override;
|
void Cleanup() override;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ void ProbeStateComponent::Update(float) {
|
||||||
|
|
||||||
if (transform->get().y < -20.0f ||
|
if (transform->get().y < -20.0f ||
|
||||||
transform->get().y > static_cast<float>(GetScreenHeight() + 20) ||
|
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) {
|
if (context->onPlayerDeath) {
|
||||||
context->onPlayerDeath();
|
context->onPlayerDeath();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,74 @@
|
||||||
#include "components/ProjectionComponent.hpp"
|
#include "components/ProjectionComponent.hpp"
|
||||||
|
|
||||||
void ProjectionComponent::Setup() {}
|
#include "Entity.hpp"
|
||||||
void ProjectionComponent::Update(float) {}
|
#include "components/GravityReceiverComponent.hpp"
|
||||||
void ProjectionComponent::Cleanup() {}
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,25 @@
|
||||||
|
|
||||||
#include "Component.hpp"
|
#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 {
|
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 Setup() override;
|
||||||
void Update(float dt) override;
|
void Update(float dt) override;
|
||||||
void Cleanup() override;
|
void Cleanup() override;
|
||||||
|
|
||||||
|
void Draw() const;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,94 @@
|
||||||
#include "components/TrailComponent.hpp"
|
#include "components/TrailComponent.hpp"
|
||||||
|
|
||||||
void TrailComponent::Setup() {}
|
#include "Entity.hpp"
|
||||||
void TrailComponent::Update(float) {}
|
#include "components/TransformComponent.hpp"
|
||||||
void TrailComponent::Cleanup() {}
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,37 @@
|
||||||
|
|
||||||
#include "Component.hpp"
|
#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 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 Setup() override;
|
||||||
void Update(float dt) override;
|
void Update(float dt) override;
|
||||||
void Cleanup() override;
|
void Cleanup() override;
|
||||||
|
|
||||||
|
void Draw() const;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue