X-Git-Url: http://git.localhorst.tv/?a=blobdiff_plain;f=src%2Fcreature%2Fcreature.cpp;h=e118451147fa0e3d88919ecd2a4b96e96698924d;hb=01cc65097f27ad657130fb58a10813e8d5c09fe7;hp=7333fdc42978693728e2d2bf599c9e3c159669b1;hpb=0e061ce526fe46dd3e894223e5d646eb2e30f826;p=blobs.git diff --git a/src/creature/creature.cpp b/src/creature/creature.cpp index 7333fdc..e118451 100644 --- a/src/creature/creature.cpp +++ b/src/creature/creature.cpp @@ -6,10 +6,12 @@ #include "Situation.hpp" #include "Steering.hpp" +#include "AttackGoal.hpp" #include "BlobBackgroundTask.hpp" #include "Goal.hpp" #include "IdleGoal.hpp" #include "../app/Assets.hpp" +#include "../graphics/color.hpp" #include "../math/const.hpp" #include "../ui/string.hpp" #include "../world/Body.hpp" @@ -25,13 +27,19 @@ #include #include +using blobs::graphics::hsl2rgb; +using blobs::graphics::rgb2hsl; + namespace blobs { namespace creature { -Composition::Composition() -: components() -, total_mass(0.0) { +Composition::Composition(const world::Set &resources) +: resources(resources) +, components() +, total_mass(0.0) +, total_volume(0.0) +, state_mass{0.0} { } Composition::~Composition() { @@ -49,6 +57,7 @@ void Composition::Add(int res, double amount) { if (c->resource == res) { c->value += amount; if (c->value <= 0.0) { + amount += c->value; components.erase(c); } found = true; @@ -59,7 +68,9 @@ void Composition::Add(int res, double amount) { components.emplace_back(res, amount); } std::sort(components.begin(), components.end(), CompositionCompare); + state_mass[resources[res].state] += amount; total_mass += amount; + total_volume += amount / resources[res].density; } bool Composition::Has(int res) const noexcept { @@ -80,13 +91,46 @@ double Composition::Get(int res) const noexcept { return 0.0; } +double Composition::Proportion(int res) const noexcept { + return Get(res) / TotalMass(); +} + +double Composition::StateProportion(int res) const noexcept { + return Get(res) / StateMass(resources[res].state); +} + +double Composition::Compatibility(int res) const noexcept { + if (Has(res)) { + return StateProportion(res); + } + double max_compat = -1.0; + double min_compat = 1.0; + for (const auto &c : components) { + double prop = c.value / StateMass(resources[res].state); + for (const auto &compat : resources[c.resource].compatibility) { + double value = compat.second * prop; + if (value > max_compat) { + max_compat = value; + } + if (value < min_compat) { + min_compat = value; + } + } + } + if (min_compat < 0.0) { + return min_compat; + } else { + return max_compat; + } +} + Creature::Creature(world::Simulation &sim) : sim(sim) , name() , genome() , properties() -, composition() +, composition(sim.Resources()) , base_color(1.0) , highlight_color(0.0, 0.0, 0.0, 1.0) , mass(1.0) @@ -102,6 +146,13 @@ Creature::Creature(world::Simulation &sim) , goals() , situation() , steering(*this) +, heading_target(0.0, 0.0, -1.0) +, heading_manual(false) +, perception_range(1.0) +, perception_range_squared(1.0) +, perception_omni_range(1.0) +, perception_omni_range_squared(1.0) +, perception_field(1.0) , vao() { sim.SetAlive(this); // all creatures avoid each other for now @@ -116,8 +167,8 @@ void Creature::AddMass(int res, double amount) { double nonsolid = 0.0; double volume = 0.0; for (const auto &c : composition) { - volume += c.value / sim.Assets().data.resources[c.resource].density; - if (sim.Assets().data.resources[c.resource].state != world::Resource::SOLID) { + volume += c.value / sim.Resources()[c.resource].density; + if (sim.Resources()[c.resource].state != world::Resource::SOLID) { nonsolid += c.value; } } @@ -131,22 +182,21 @@ void Creature::HighlightColor(const glm::dvec3 &c) noexcept { } void Creature::Ingest(int res, double amount) noexcept { - // TODO: check foreign materials if (sim.Resources()[res].state == world::Resource::SOLID) { - // 15% of solids stays in body - AddMass(res, amount * 0.15); + // 30% of solids stays in body + AddMass(res, amount * 0.3 * composition.Compatibility(res)); } else { // 10% of fluids stays in body - AddMass(res, amount * 0.05); + AddMass(res, amount * 0.1 * composition.Compatibility(res)); } math::GaloisLFSR &random = sim.Assets().random; if (random.UNorm() < AdaptChance()) { // change color to be slightly more like resource glm::dvec3 color(rgb2hsl(sim.Resources()[res].base_color)); // solids affect base color, others highlight - double p = sim.Resources()[res].state == world::Resource::SOLID ? 0 : 1; - double q = random.UInt(3); // hue, sat, or val - double r = random.UInt(2); // mean or deviation + int p = sim.Resources()[res].state == world::Resource::SOLID ? 0 : 1; + int q = random.UInt(3); // hue, sat, or val + int r = random.UInt(2); // mean or deviation math::Distribution *d = nullptr; double ref = 0.0; if (p == 0) { @@ -180,16 +230,30 @@ void Creature::Ingest(int res, double amount) noexcept { } else if (diff > 0.5) { diff -= 1.0; } - // move ±15% of distance - d->Mean(std::fmod(d->Mean() + diff * random.SNorm() * 0.15, 1.0)); + // move 0-15% of distance + d->Mean(std::fmod(d->Mean() + diff * random.UNorm() * 0.15, 1.0)); } else { - d->Mean(glm::clamp(d->Mean() + diff * random.SNorm() * 0.15, 0.0, 1.0)); + d->Mean(glm::clamp(d->Mean() + diff * random.UNorm() * 0.15, 0.0, 1.0)); } } else { // scale by ±15%, enforce bounds d->StandardDeviation(glm::clamp(d->StandardDeviation() * (1.0 + random.SNorm() * 0.15), 0.0001, 0.5)); } } + if (sim.Resources()[res].state == world::Resource::LIQUID && random.UNorm() < AdaptChance()) { + // change texture randomly + // TODO: make change depending on surroundings and/or resource + int p = random.UInt(2); // back or side + int q = random.UInt(2); // mean or deviation + math::Distribution &d = p ? genome.skin_side : genome.skin_back; + if (q == 0) { + // move ± one standard deviation + d.Mean(d.Mean() + (random.SNorm() * d.StandardDeviation())); + } else { + // scale by ±10%, enforce bounds + d.StandardDeviation(glm::clamp(d.StandardDeviation() * (1.0 + random.SNorm() * 0.1), 0.0001, 0.5)); + } + } } void Creature::DoWork(double amount) noexcept { @@ -266,7 +330,7 @@ void Creature::AddParent(Creature &p) { } double Creature::Age() const noexcept { - return sim.Time() - birth; + return Dead() ? death - birth : sim.Time() - birth; } double Creature::AgeFactor(double peak) const noexcept { @@ -294,18 +358,38 @@ double Creature::Strength() const noexcept { return properties.Strength() * ExhaustionFactor() * AgeFactor(0.25); } +double Creature::StrengthFactor() const noexcept { + double str = Strength(); + return str / (str + 1.0); +} + double Creature::Stamina() const noexcept { return properties.Stamina() * ExhaustionFactor() * AgeFactor(0.25); } +double Creature::StaminaFactor() const noexcept { + double stm = Stamina(); + return stm / (stm + 1.0); +} + double Creature::Dexerty() const noexcept { return properties.Dexerty() * ExhaustionFactor() * AgeFactor(0.25); } +double Creature::DexertyFactor() const noexcept { + double dex = Dexerty(); + return dex / (dex + 1.0); +} + double Creature::Intelligence() const noexcept { return properties.Intelligence() * FatigueFactor() * AgeFactor(0.25); } +double Creature::IntelligenceFactor() const noexcept { + double intl = Intelligence(); + return intl / (intl + 1.0); +} + double Creature::Lifetime() const noexcept { return properties.Lifetime(); } @@ -327,25 +411,23 @@ double Creature::OffspringMass() const noexcept { } double Creature::PerceptionRange() const noexcept { - return 3.0 * (Dexerty() / (Dexerty() + 1)) + Size(); + return perception_range; } double Creature::PerceptionOmniRange() const noexcept { - return 0.5 * (Dexerty() / (Dexerty() + 1)) + Size(); + return perception_omni_range; } double Creature::PerceptionField() const noexcept { - // this is the cosine of half the angle, so 1.0 is none, -1.0 is perfect - return 0.8 - (Dexerty() / (Dexerty() + 1)); + return perception_field; } bool Creature::PerceptionTest(const glm::dvec3 &p) const noexcept { const glm::dvec3 diff(p - situation.Position()); - double omni_range = PerceptionOmniRange(); - if (glm::length2(diff) < omni_range * omni_range) return true; - double range = PerceptionRange(); - if (glm::length2(diff) > range * range) return false; - return glm::dot(glm::normalize(diff), situation.Heading()) > PerceptionField(); + double ldiff = glm::length2(diff); + if (ldiff < perception_omni_range_squared) return true; + if (ldiff > perception_range_squared) return false; + return glm::dot(diff / std::sqrt(ldiff), situation.Heading()) > perception_field; } double Creature::OffspringChance() const noexcept { @@ -368,6 +450,14 @@ void Creature::AddGoal(std::unique_ptr &&g) { goals.emplace_back(std::move(g)); } +void Creature::SetBackgroundTask(std::unique_ptr &&g) { + bg_task = std::move(g); +} + +Goal &Creature::BackgroundTask() { + return *bg_task; +} + namespace { bool GoalCompare(const std::unique_ptr &a, const std::unique_ptr &b) { @@ -377,11 +467,22 @@ bool GoalCompare(const std::unique_ptr &a, const std::unique_ptr &b) } void Creature::Tick(double dt) { + Cache(); TickState(dt); TickStats(dt); TickBrain(dt); } +void Creature::Cache() noexcept { + double dex_fact = DexertyFactor(); + perception_range = 3.0 * dex_fact + size; + perception_range_squared = perception_range * perception_range; + perception_omni_range = 0.5 * dex_fact + size; + perception_omni_range_squared = perception_omni_range * perception_omni_range; + // this is the cosine of half the angle, so 1.0 is none, -1.0 is perfect + perception_field = 0.8 - dex_fact; +} + void Creature::TickState(double dt) { steering.MaxSpeed(Dexerty()); steering.MaxForce(Strength()); @@ -397,42 +498,49 @@ void Creature::TickState(double dt) { state.pos += f.vel * dt; state.vel += f.acc * dt; situation.EnforceConstraints(state); - if (glm::length2(state.vel) > 0.000001) { - glm::dvec3 nvel(glm::normalize(state.vel)); - double ang = glm::angle(nvel, state.dir); - double turn_rate = PI * 0.75 * dt; - if (ang < turn_rate) { - state.dir = glm::normalize(state.vel); - } else if (std::abs(ang - PI) < 0.001) { - state.dir = glm::rotate(state.dir, turn_rate, situation.GetPlanet().NormalAt(state.pos)); - } else { - state.dir = glm::rotate(state.dir, turn_rate, glm::normalize(glm::cross(state.dir, nvel))); + + if (!heading_manual && glm::length2(state.vel) > 0.000001) { + const glm::dvec3 normal(situation.GetPlanet().NormalAt(state.pos)); + const glm::dvec3 tangent(state.vel - (normal * glm::dot(state.vel, normal))); + if (glm::length2(tangent) > 0.000001) { + heading_target = glm::normalize(tangent); } } + double ang = glm::angle(heading_target, state.dir); + double turn_rate = PI * 0.75 * dt; + if (ang < turn_rate) { + state.dir = heading_target; + heading_manual = false; + } else { + state.dir = glm::rotate(state.dir, turn_rate, glm::normalize(glm::cross(state.dir, heading_target))); + } + situation.SetState(state); // work is force times distance - DoWork(glm::length(f.acc) * Mass() * glm::length(f.vel) * dt); + // keep 10% of gravity as a kind of background burn + DoWork(glm::length(f.acc - (0.9 * situation.GetPlanet().GravityAt(state.pos))) * Mass() * glm::length(f.vel) * dt); } Situation::Derivative Creature::Step(const Situation::Derivative &ds, double dt) const noexcept { Situation::State s = situation.GetState(); s.pos += ds.vel * dt; s.vel += ds.acc * dt; + situation.EnforceConstraints(s); glm::dvec3 force(steering.Force(s)); // gravity = antinormal * mass * Gm / r² - double elevation = situation.GetPlanet().DistanceAt(s.pos); glm::dvec3 normal(situation.GetPlanet().NormalAt(s.pos)); force += glm::dvec3( -normal - * Mass() * situation.GetPlanet().GravitationalParameter() - / (elevation * elevation)); + * (Mass() * situation.GetPlanet().GravitationalParameter() + / glm::length2(s.pos))); // if net force is applied and in contact with surface - if (!allzero(force) && std::abs(std::abs(elevation) - situation.GetPlanet().Radius()) < 0.001) { - // apply friction = -|normal force| * tangential force * coefficient + if (!allzero(force) && !allzero(s.vel) && glm::length2(s.pos) < (situation.GetPlanet().Radius() + 0.01) * (situation.GetPlanet().Radius() + 0.01)) { + // apply friction glm::dvec3 fn(normal * glm::dot(force, normal)); + // TODO: friction somehow bigger than force? glm::dvec3 ft(force - fn); double u = 0.4; - glm::dvec3 friction(-glm::length(fn) * ft * u); + glm::dvec3 friction(-glm::clamp(glm::length(ft), 0.0, glm::length(fn) * u) * glm::normalize(s.vel)); force += friction; } return { @@ -498,7 +606,7 @@ void Creature::TickBrain(double dt) { } } -math::AABB Creature::CollisionBox() const noexcept { +math::AABB Creature::CollisionBounds() const noexcept { return { glm::dvec3(size * -0.5), glm::dvec3(size * 0.5) }; } @@ -518,6 +626,10 @@ glm::dmat4 Creature::CollisionTransform() const noexcept { * glm::translate(glm::dvec3(0.0, half_size, 0.0)); } +void Creature::OnCollide(Creature &other) { + memory.TrackCollision(other); +} + glm::dmat4 Creature::LocalTransform() noexcept { const double half_size = size * 0.5; return CollisionTransform() @@ -537,7 +649,16 @@ void Creature::BuildVAO() { vao->ReserveAttributes(6 * 4, GL_STATIC_DRAW); { auto attrib = vao->MapAttributes(GL_WRITE_ONLY); - const float offset = 1.0f; + constexpr float offset = 1.0f; + constexpr float max_tex = 5.999f; + const float tex[6] = { + 0.0f, // face + float(std::floor(skin_side * max_tex)), // left + float(std::floor(skin_back * max_tex)), // top + float(std::floor(skin_back * max_tex)), // back + float(std::floor(skin_side * max_tex)), // right + 0.0f, // bottom + }; for (int surface = 0; surface < 6; ++surface) { const float tex_u_begin = surface < 3 ? 1.0f : 0.0f; const float tex_u_end = surface < 3 ? 0.0f : 1.0f; @@ -550,7 +671,7 @@ void Creature::BuildVAO() { attrib[4 * surface + 0].normal[(surface + 2) % 3] = surface < 3 ? 1.0f : -1.0f; attrib[4 * surface + 0].texture.x = tex_u_begin; attrib[4 * surface + 0].texture.y = 1.0f; - attrib[4 * surface + 0].texture.z = surface; + attrib[4 * surface + 0].texture.z = tex[surface]; attrib[4 * surface + 1].position[(surface + 0) % 3] = -offset; attrib[4 * surface + 1].position[(surface + 1) % 3] = offset; @@ -560,7 +681,7 @@ void Creature::BuildVAO() { attrib[4 * surface + 1].normal[(surface + 2) % 3] = surface < 3 ? 1.0f : -1.0f; attrib[4 * surface + 1].texture.x = tex_u_end; attrib[4 * surface + 1].texture.y = 1.0f; - attrib[4 * surface + 1].texture.z = surface; + attrib[4 * surface + 1].texture.z = tex[surface]; attrib[4 * surface + 2].position[(surface + 0) % 3] = offset; attrib[4 * surface + 2].position[(surface + 1) % 3] = -offset; @@ -570,7 +691,7 @@ void Creature::BuildVAO() { attrib[4 * surface + 2].normal[(surface + 2) % 3] = surface < 3 ? 1.0f : -1.0f; attrib[4 * surface + 2].texture.x = tex_u_begin; attrib[4 * surface + 2].texture.y = 0.0f; - attrib[4 * surface + 2].texture.z = surface; + attrib[4 * surface + 2].texture.z = tex[surface]; attrib[4 * surface + 3].position[(surface + 0) % 3] = offset; attrib[4 * surface + 3].position[(surface + 1) % 3] = offset; @@ -580,7 +701,7 @@ void Creature::BuildVAO() { attrib[4 * surface + 3].normal[(surface + 2) % 3] = surface < 3 ? 1.0f : -1.0f; attrib[4 * surface + 3].texture.x = tex_u_end; attrib[4 * surface + 3].texture.y = 0.0f; - attrib[4 * surface + 3].texture.z = surface; + attrib[4 * surface + 3].texture.z = tex[surface]; } } vao->BindElements(); @@ -622,6 +743,7 @@ void Spawn(Creature &c, world::Planet &p) { p.AddCreature(&c); c.GetSituation().SetPlanetSurface(p, glm::dvec3(0.0, 0.0, p.Radius())); c.GetSituation().Heading(glm::dvec3(1.0, 0.0, 0.0)); + c.HeadingTarget(glm::dvec3(1.0, 0.0, 0.0)); // probe surrounding area for common resources int start = p.SideLength() / 2 - 2; @@ -664,7 +786,7 @@ void Spawn(Creature &c, world::Planet &p) { double color_divisor = 0.0; if (p.HasAtmosphere()) { - c.AddMass(p.Atmosphere(), 0.01); + c.AddMass(p.Atmosphere(), 0.005); color_avg += c.GetSimulation().Resources()[p.Atmosphere()].base_color * 0.1; color_divisor += 0.1; } @@ -691,6 +813,9 @@ void Spawn(Creature &c, world::Planet &p) { genome.highlight_saturation = { 1.0 - hsl.y, 0.01 }; genome.highlight_lightness = { 1.0 - hsl.z, 0.01 }; + genome.skin_side = { 0.5, 0.01 }; + genome.skin_back = { 0.5, 0.01 }; + genome.Configure(c); } @@ -722,6 +847,8 @@ void Genome::Configure(Creature &c) const { ); c.BaseColor(hsl2rgb(base_color)); c.HighlightColor(hsl2rgb(highlight_color)); + c.BackSkin(glm::clamp(skin_back.FakeNormal(random.SNorm()), 0.0, 1.0)); + c.SideSkin(glm::clamp(skin_side.FakeNormal(random.SNorm()), 0.0, 1.0)); c.SetBackgroundTask(std::unique_ptr(new BlobBackgroundTask(c))); c.AddGoal(std::unique_ptr(new IdleGoal(c))); } @@ -734,13 +861,16 @@ void Split(Creature &c) { a->Name(c.GetSimulation().Assets().name.Sequential()); c.GetGenome().Configure(*a); for (const auto &cmp : c.GetComposition()) { - a->AddMass(cmp.resource, cmp.value * 0.5); + // require at least 0.1% + if (cmp.value > 0.002) { + a->AddMass(cmp.resource, cmp.value * 0.5); + } } s.GetPlanet().AddCreature(a); // TODO: duplicate situation somehow a->GetSituation().SetPlanetSurface( s.GetPlanet(), - s.Position() + glm::dvec3(0.0, 0.55 * a->Size(), 0.0)); + s.Position() + glm::rotate(s.Heading() * a->Size() * 0.86, PI * 0.5, s.SurfaceNormal())); a->BuildVAO(); c.GetSimulation().Log() << a->Name() << " was born" << std::endl; @@ -749,12 +879,15 @@ void Split(Creature &c) { b->Name(c.GetSimulation().Assets().name.Sequential()); c.GetGenome().Configure(*b); for (const auto &cmp : c.GetComposition()) { - b->AddMass(cmp.resource, cmp.value * 0.5); + // require at least 0.1% + if (cmp.value > 0.002) { + b->AddMass(cmp.resource, cmp.value * 0.5); + } } s.GetPlanet().AddCreature(b); b->GetSituation().SetPlanetSurface( s.GetPlanet(), - s.Position() - glm::dvec3(0.0, 0.55 * b->Size(), 0.0)); + s.Position() + glm::rotate(s.Heading() * b->Size() * 0.86, PI * -0.5, s.SurfaceNormal())); b->BuildVAO(); c.GetSimulation().Log() << b->Name() << " was born" << std::endl; @@ -771,6 +904,53 @@ Memory::~Memory() { void Memory::Erase() { known_types.clear(); + known_creatures.clear(); +} + +bool Memory::RememberLocation(const Composition &accept, glm::dvec3 &pos) const noexcept { + double best_rating = -1.0; + for (const auto &k : known_types) { + const world::TileType &t = c.GetSimulation().TileTypes()[k.first]; + auto entry = t.FindBestResource(accept); + if (entry != t.resources.end()) { + double rating = entry->ubiquity / std::max(0.125, 0.25 * glm::length2(c.GetSituation().Position() - k.second.first_loc.position)); + if (rating > best_rating) { + best_rating = rating; + pos = k.second.first_loc.position; + } + rating = entry->ubiquity / std::max(0.125, 0.25 * glm::length2(c.GetSituation().Position() - k.second.last_loc.position)); + if (rating > best_rating) { + best_rating = rating; + pos = k.second.last_loc.position; + } + } + } + if (best_rating > 0.0) { + glm::dvec3 error( + c.GetSimulation().Assets().random.SNorm(), + c.GetSimulation().Assets().random.SNorm(), + c.GetSimulation().Assets().random.SNorm()); + pos += error * (4.0 * (1.0 - c.IntelligenceFactor())); + pos = glm::normalize(pos) * c.GetSituation().GetPlanet().Radius(); + return true; + } else { + return false; + } +} + +void Memory::TrackCollision(Creature &other) { + // TODO: find out whose fault it was + // TODO: source values from personality + Profile &p = known_creatures[&other]; + p.annoyance += 0.1; + const double annoy_fact = p.annoyance / (p.annoyance + 1.0); + if (c.GetSimulation().Assets().random.UNorm() > annoy_fact * 0.1 * (1.0 - c.GetStats().Damage().value)) { + AttackGoal *g = new AttackGoal(c, other); + g->SetDamageTarget(annoy_fact); + g->Urgency(annoy_fact); + c.AddGoal(std::unique_ptr(g)); + p.annoyance *= 0.5; + } } void Memory::Tick(double dt) { @@ -778,6 +958,7 @@ void Memory::Tick(double dt) { if (s.OnSurface()) { TrackStay({ &s.GetPlanet(), s.Position() }, dt); } + // TODO: forget } void Memory::TrackStay(const Location &l, double t) { @@ -843,6 +1024,14 @@ bool Situation::OnSurface() const noexcept { return type == PLANET_SURFACE; } +bool Situation::OnGround() const noexcept { + return OnSurface() && glm::length2(state.pos) < (planet->Radius() + 0.05) * (planet->Radius() + 0.05); +} + +glm::dvec3 Situation::SurfaceNormal() const noexcept { + return planet->NormalAt(state.pos); +} + world::Tile &Situation::GetTile() const noexcept { return planet->TileAt(state.pos); } @@ -861,11 +1050,13 @@ void Situation::Accelerate(const glm::dvec3 &dv) noexcept { EnforceConstraints(state); } -void Situation::EnforceConstraints(State &s) noexcept { +void Situation::EnforceConstraints(State &s) const noexcept { if (OnSurface()) { double r = GetPlanet().Radius(); if (glm::length2(s.pos) < r * r) { - s.pos = glm::normalize(s.pos) * r; + const glm::dvec3 normal(GetPlanet().NormalAt(s.pos)); + s.pos = normal * r; + s.vel -= normal * glm::dot(normal, s.vel); } } } @@ -955,8 +1146,8 @@ glm::dvec3 Steering::Force(const Situation::State &s) const noexcept { result += repulse; } if (halting) { - // break twice as hard - result += -2.0 * s.vel * force; + // brake hard + result += -5.0 * s.vel * force; } if (seeking) { glm::dvec3 diff = target - s.pos; @@ -971,6 +1162,10 @@ glm::dvec3 Steering::Force(const Situation::State &s) const noexcept { result += TargetVelocity(s, diff * std::min(dist * force, speed) / dist, force); } } + // remove vertical component, if any + const glm::dvec3 normal(c.GetSituation().GetPlanet().NormalAt(s.pos)); + result -= normal * glm::dot(normal, result); + // clamp to max if (glm::length2(result) > max_force * max_force) { result = glm::normalize(result) * max_force; }