Add black hole obstacle

master
John Montagu, the 4th Earl of Sandvich 2026-03-16 02:33:49 -07:00
parent f25a796073
commit fb506f6f05
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
7 changed files with 105 additions and 33 deletions

View File

@ -70,6 +70,43 @@ std::shared_ptr<Entity> CreateGravityWell() {
return e;
}
std::shared_ptr<Entity> CreateBlackHole(float x, float y) {
auto e = std::make_shared<Entity>();
auto &transform = e->AddComponent<TransformComponent>();
transform.x = x;
transform.y = y;
auto &scrollable = e->AddComponent<ScrollableComponent>();
scrollable.worldX = x;
auto &well = e->AddComponent<GravityWellComponent>();
well.mass = static_cast<float>(1 << 21);
well.minDist = 24.0f;
well.controlledByMouse = false;
well.alwaysActive = true;
auto &collider = e->AddComponent<ColliderComponent>();
collider.radius = 18.0f;
e->AddComponent<HazardComponent>();
auto &render = e->AddComponent<RenderComponent>();
render.draw = [e]() {
auto transform = e->GetComponent<TransformComponent>();
if (!transform) {
return;
}
const int cx = static_cast<int>(transform->get().x);
const int cy = static_cast<int>(transform->get().y);
DrawCircle(cx, cy, 18.0f, Color{8, 10, 20, 255});
DrawCircleLines(cx, cy, 18.0f, Color{70, 95, 170, 240});
DrawCircleLines(cx, cy, 24.0f, Color{95, 120, 200, 120});
};
return e;
}
std::shared_ptr<Entity> CreateStar(float x, float y) {
auto e = std::make_shared<Entity>();
auto &t = e->AddComponent<TransformComponent>();

View File

@ -6,6 +6,7 @@
std::shared_ptr<Entity> CreateProbe();
std::shared_ptr<Entity> CreateGravityWell();
std::shared_ptr<Entity> CreateBlackHole(float x, float y);
std::shared_ptr<Entity> CreateStar(float x, float y);
std::shared_ptr<Entity> CreateAsteroid(float x, float y);
std::shared_ptr<Entity> CreateDebris(float x, float y, float vx, float vy, float ttl = 1.25f);

View File

@ -15,6 +15,11 @@ void GravityWellComponent::Update(float dt) {
return;
}
if (!controlledByMouse) {
active = alwaysActive;
return;
}
active = IsMouseButtonDown(MOUSE_BUTTON_LEFT);
const Vector2 mouse = GetMousePosition();

View File

@ -23,6 +23,16 @@ struct GravityWellComponent : public Component {
*/
bool active = false;
/**
* If true, active state is driven by left mouse button and this well follows the cursor.
*/
bool controlledByMouse = true;
/**
* If true and not mouse-controlled, this well applies gravity every frame.
*/
bool alwaysActive = false;
/**
* Lerp factor for how quickly the gravity well follows the mouse position.
*/

View File

@ -67,30 +67,20 @@ bool PhysicsComponent::IsInsideNullZone(double xPos) const {
return false;
}
bool PhysicsComponent::ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx,
double &dvy, bool ignoreWellActive) const {
bool PhysicsComponent::ComputeGravityDeltaVelocity(double px, double py, double dt, double &dvx,
double &dvy, bool ignoreWellActive) const {
dvx = 0.0;
dvy = 0.0;
if (!context || !context->wellEntity) {
if (!context || !context->entities) {
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;
}
// Probe cannot receive well acceleration when meter is depleted.
bool probeMeterDepleted = false;
if (context && entity == context->probeEntity && context->statsEntity) {
auto stats = context->statsEntity->GetComponent<StatsComponent>();
if (stats && stats->get().value <= 0.0f) {
return false;
probeMeterDepleted = true;
}
}
@ -98,25 +88,49 @@ bool PhysicsComponent::ComputeWellDeltaVelocity(double px, double py, double dt,
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;
bool applied = false;
for (auto &other : *context->entities) {
if (!other || other.get() == entity) {
continue;
}
auto wellTransform = other->GetComponent<TransformComponent>();
auto wellGravity = other->GetComponent<GravityWellComponent>();
if (!wellTransform || !wellGravity) {
continue;
}
if (probeMeterDepleted && wellGravity->get().controlledByMouse) {
continue;
}
if (!ignoreWellActive && !wellGravity->get().active) {
continue;
}
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) {
continue;
}
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;
applied = true;
}
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;
return applied;
}
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);
ComputeGravityDeltaVelocity(xRef, yRef, dt, dvx, dvy, ignoreWellActive);
vxRef += dvx;
vyRef += dvy;

View File

@ -29,11 +29,11 @@ struct PhysicsComponent : public Component {
bool IsInsideNullZone(double xPos) const;
/**
* Computes velocity delta from well gravity for a position.
* Returns false when gravity should not apply.
* Computes summed velocity delta from all gravity sources for a position.
* Returns false when no gravity should apply.
*/
bool ComputeWellDeltaVelocity(double px, double py, double dt, double &dvx, double &dvy,
bool ignoreWellActive = false) const;
bool ComputeGravityDeltaVelocity(double px, double py, double dt, double &dvx, double &dvy,
bool ignoreWellActive = false) const;
/**
* Performs one projected simulation step using well/null-zone rules,

View File

@ -33,21 +33,26 @@ void SpawnComponent::Update(float) {
while (cursorWX < spawnLimit) {
const float r = static_cast<float>(GetRandomValue(0, 99));
if (r < 55.0f) {
if (r < 50.0f) {
const float y = static_cast<float>(GetRandomValue(48, GetScreenHeight() - 48));
auto star = CreateStar(cursorWX, y);
star->SetContext(context);
context->entities->push_back(star);
} else if (r < 88.0f) {
} else if (r < 78.0f) {
const float y = static_cast<float>(GetRandomValue(42, GetScreenHeight() - 42));
auto asteroid = CreateAsteroid(cursorWX, y);
asteroid->SetContext(context);
context->entities->push_back(asteroid);
} else {
} else if (r < 95.0f) {
const float width = static_cast<float>(GetRandomValue(70, 140));
auto zone = CreateNullZone(cursorWX, width);
zone->SetContext(context);
context->entities->push_back(zone);
} else {
const float y = static_cast<float>(GetRandomValue(42, GetScreenHeight() - 42));
auto blackHole = CreateBlackHole(cursorWX, y);
blackHole->SetContext(context);
context->entities->push_back(blackHole);
}
const float gap =