osb/source/game/StarMovementController.cpp
Kai Blaschke 431a9c00a5
Fixed a huge amount of Clang warnings
On Linux and macOS, using Clang to compile OpenStarbound produces about 400 MB worth of warnings during the build, making the compiler output unreadable and slowing the build down considerably.

99% of the warnings were unqualified uses of std::move and std::forward, which are now all properly qualified.

Fixed a few other minor warnings about non-virtual destructors and some uses of std::move preventing copy elision on temporary objects.

Most remaining warnings are now unused parameters.
2024-02-19 16:55:19 +01:00

1123 lines
42 KiB
C++

#include "StarMovementController.hpp"
#include "StarJsonExtra.hpp"
#include "StarDataStreamExtra.hpp"
#include "StarRoot.hpp"
#include "StarWorld.hpp"
#include "StarAssets.hpp"
#include "StarRandom.hpp"
namespace Star {
MovementParameters MovementParameters::sensibleDefaults() {
return MovementParameters(Root::singleton().assets()->json("/default_movement.config").toObject());
}
MovementParameters::MovementParameters(Json const& config) {
if (config.isNull())
return;
mass = config.optFloat("mass");
gravityMultiplier = config.optFloat("gravityMultiplier");
liquidBuoyancy = config.optFloat("liquidBuoyancy");
airBuoyancy = config.optFloat("airBuoyancy");
bounceFactor = config.optFloat("bounceFactor");
stopOnFirstBounce = config.optBool("stopOnFirstBounce");
enableSurfaceSlopeCorrection = config.optBool("enableSurfaceSlopeCorrection");
slopeSlidingFactor = config.optFloat("slopeSlidingFactor");
maxMovementPerStep = config.optFloat("maxMovementPerStep");
maximumCorrection = config.optFloat("maximumCorrection");
speedLimit = config.optFloat("speedLimit");
discontinuityThreshold = config.optFloat("discontinuityThreshold");
collisionPoly = config.opt("collisionPoly").apply(jsonToPolyF);
stickyCollision = config.optBool("stickyCollision");
stickyForce = config.optFloat("stickyForce");
airFriction = config.optFloat("airFriction");
liquidFriction = config.optFloat("liquidFriction");
groundFriction = config.optFloat("groundFriction");
collisionEnabled = config.optBool("collisionEnabled");
frictionEnabled = config.optBool("frictionEnabled");
gravityEnabled = config.optBool("gravityEnabled");
ignorePlatformCollision = config.optBool("ignorePlatformCollision");
maximumPlatformCorrection = config.optFloat("maximumPlatformCorrection");
maximumPlatformCorrectionVelocityFactor = config.optFloat("maximumPlatformCorrectionVelocityFactor");
physicsEffectCategories = config.opt("physicsEffectCategories").apply(jsonToStringSet);
restDuration = config.optInt("restDuration");
}
MovementParameters MovementParameters::merge(MovementParameters const& rhs) const {
MovementParameters merged;
merged.mass = rhs.mass.orMaybe(mass);
merged.gravityMultiplier = rhs.gravityMultiplier.orMaybe(gravityMultiplier);
merged.liquidBuoyancy = rhs.liquidBuoyancy.orMaybe(liquidBuoyancy);
merged.airBuoyancy = rhs.airBuoyancy.orMaybe(airBuoyancy);
merged.bounceFactor = rhs.bounceFactor.orMaybe(bounceFactor);
merged.stopOnFirstBounce = rhs.stopOnFirstBounce.orMaybe(stopOnFirstBounce);
merged.enableSurfaceSlopeCorrection = rhs.enableSurfaceSlopeCorrection.orMaybe(enableSurfaceSlopeCorrection);
merged.slopeSlidingFactor = rhs.slopeSlidingFactor.orMaybe(slopeSlidingFactor);
merged.maxMovementPerStep = rhs.maxMovementPerStep.orMaybe(maxMovementPerStep);
merged.maximumCorrection = rhs.maximumCorrection.orMaybe(maximumCorrection);
merged.speedLimit = rhs.speedLimit.orMaybe(speedLimit);
merged.discontinuityThreshold = rhs.discontinuityThreshold.orMaybe(discontinuityThreshold);
merged.collisionPoly = rhs.collisionPoly.orMaybe(collisionPoly);
merged.stickyCollision = rhs.stickyCollision.orMaybe(stickyCollision);
merged.stickyForce = rhs.stickyForce.orMaybe(stickyForce);
merged.airFriction = rhs.airFriction.orMaybe(airFriction);
merged.liquidFriction = rhs.liquidFriction.orMaybe(liquidFriction);
merged.groundFriction = rhs.groundFriction.orMaybe(groundFriction);
merged.collisionEnabled = rhs.collisionEnabled.orMaybe(collisionEnabled);
merged.frictionEnabled = rhs.frictionEnabled.orMaybe(frictionEnabled);
merged.gravityEnabled = rhs.gravityEnabled.orMaybe(gravityEnabled);
merged.ignorePlatformCollision = rhs.ignorePlatformCollision.orMaybe(ignorePlatformCollision);
merged.maximumPlatformCorrection = rhs.maximumPlatformCorrection.orMaybe(maximumPlatformCorrection);
merged.maximumPlatformCorrectionVelocityFactor = rhs.maximumPlatformCorrectionVelocityFactor.orMaybe(maximumPlatformCorrectionVelocityFactor);
merged.physicsEffectCategories = rhs.physicsEffectCategories.orMaybe(physicsEffectCategories);
merged.restDuration = rhs.restDuration.orMaybe(restDuration);
return merged;
}
Json MovementParameters::toJson() const {
return JsonObject{
{"mass", jsonFromMaybe(mass)},
{"gravityMultiplier", jsonFromMaybe(gravityMultiplier)},
{"liquidBuoyancy", jsonFromMaybe(liquidBuoyancy)},
{"airBuoyancy", jsonFromMaybe(airBuoyancy)},
{"stopOnFirstBounce", jsonFromMaybe(stopOnFirstBounce)},
{"enableSurfaceSlopeCorrection", jsonFromMaybe(enableSurfaceSlopeCorrection)},
{"slopeSlidingFactor", jsonFromMaybe(slopeSlidingFactor)},
{"maxMovementPerStep", jsonFromMaybe(maxMovementPerStep)},
{"maximumCorrection", jsonFromMaybe(maximumCorrection)},
{"speedLimit", jsonFromMaybe(speedLimit)},
{"discontinuityThreshold", jsonFromMaybe(discontinuityThreshold)},
{"collisionPoly", jsonFromMaybe(collisionPoly, jsonFromPolyF)},
{"stickyCollision", jsonFromMaybe(stickyCollision)},
{"stickyForce", jsonFromMaybe(stickyForce)},
{"airFriction", jsonFromMaybe(airFriction)},
{"liquidFriction", jsonFromMaybe(liquidFriction)},
{"groundFriction", jsonFromMaybe(groundFriction)},
{"collisionEnabled", jsonFromMaybe(collisionEnabled)},
{"frictionEnabled", jsonFromMaybe(frictionEnabled)},
{"gravityEnabled", jsonFromMaybe(gravityEnabled)},
{"ignorePlatformCollision", jsonFromMaybe(ignorePlatformCollision)},
{"maximumPlatformCorrection", jsonFromMaybe(maximumPlatformCorrection)},
{"maximumPlatformCorrectionVelocityFactor", jsonFromMaybe(maximumPlatformCorrectionVelocityFactor)},
{"physicsEffectCategories", jsonFromMaybe(physicsEffectCategories, jsonFromStringSet)},
{"restDuration", jsonFromMaybe(restDuration)}
};
}
DataStream& operator>>(DataStream& ds, MovementParameters& movementParameters) {
ds.read(movementParameters.mass);
ds.read(movementParameters.gravityMultiplier);
ds.read(movementParameters.liquidBuoyancy);
ds.read(movementParameters.airBuoyancy);
ds.read(movementParameters.stopOnFirstBounce);
ds.read(movementParameters.enableSurfaceSlopeCorrection);
ds.read(movementParameters.slopeSlidingFactor);
ds.read(movementParameters.maxMovementPerStep);
ds.read(movementParameters.maximumCorrection);
ds.read(movementParameters.speedLimit);
ds.read(movementParameters.discontinuityThreshold);
ds.read(movementParameters.collisionPoly);
ds.read(movementParameters.stickyCollision);
ds.read(movementParameters.stickyForce);
ds.read(movementParameters.airFriction);
ds.read(movementParameters.liquidFriction);
ds.read(movementParameters.groundFriction);
ds.read(movementParameters.collisionEnabled);
ds.read(movementParameters.frictionEnabled);
ds.read(movementParameters.gravityEnabled);
ds.read(movementParameters.ignorePlatformCollision);
ds.read(movementParameters.maximumPlatformCorrection);
ds.read(movementParameters.maximumPlatformCorrectionVelocityFactor);
ds.read(movementParameters.physicsEffectCategories);
ds.read(movementParameters.restDuration);
return ds;
}
DataStream& operator<<(DataStream& ds, MovementParameters const& movementParameters) {
ds.write(movementParameters.mass);
ds.write(movementParameters.gravityMultiplier);
ds.write(movementParameters.liquidBuoyancy);
ds.write(movementParameters.airBuoyancy);
ds.write(movementParameters.stopOnFirstBounce);
ds.write(movementParameters.enableSurfaceSlopeCorrection);
ds.write(movementParameters.slopeSlidingFactor);
ds.write(movementParameters.maxMovementPerStep);
ds.write(movementParameters.maximumCorrection);
ds.write(movementParameters.speedLimit);
ds.write(movementParameters.discontinuityThreshold);
ds.write(movementParameters.collisionPoly);
ds.write(movementParameters.stickyCollision);
ds.write(movementParameters.stickyForce);
ds.write(movementParameters.airFriction);
ds.write(movementParameters.liquidFriction);
ds.write(movementParameters.groundFriction);
ds.write(movementParameters.collisionEnabled);
ds.write(movementParameters.frictionEnabled);
ds.write(movementParameters.gravityEnabled);
ds.write(movementParameters.ignorePlatformCollision);
ds.write(movementParameters.maximumPlatformCorrection);
ds.write(movementParameters.maximumPlatformCorrectionVelocityFactor);
ds.write(movementParameters.physicsEffectCategories);
ds.write(movementParameters.restDuration);
return ds;
}
MovementController::MovementController(MovementParameters const& parameters) {
m_resting = false;
m_timeStep = GlobalTimestep;
m_liquidPercentage = 0.0f;
m_liquidId = EmptyLiquidId;
m_xPosition.setFixedPointBase(0.0125f);
m_yPosition.setFixedPointBase(0.0125f);
m_xVelocity.setFixedPointBase(0.00625f);
m_yVelocity.setFixedPointBase(0.00625f);
m_rotation.setFixedPointBase(0.01f);
m_xRelativeSurfaceMovingCollisionPosition.setFixedPointBase(0.0125f);
m_yRelativeSurfaceMovingCollisionPosition.setFixedPointBase(0.0125f);
m_xVelocity.setInterpolator(lerp<float, float>);
m_yVelocity.setInterpolator(lerp<float, float>);
m_rotation.setInterpolator(angleLerp<float, float>);
m_xRelativeSurfaceMovingCollisionPosition.setInterpolator(lerp<float, float>);
m_yRelativeSurfaceMovingCollisionPosition.setInterpolator(lerp<float, float>);
addNetElement(&m_collisionPoly);
addNetElement(&m_mass);
addNetElement(&m_xPosition);
addNetElement(&m_yPosition);
addNetElement(&m_xVelocity);
addNetElement(&m_yVelocity);
addNetElement(&m_rotation);
addNetElement(&m_colliding);
addNetElement(&m_collisionStuck);
addNetElement(&m_nullColliding);
addNetElement(&m_stickingDirection);
addNetElement(&m_onGround);
addNetElement(&m_zeroG);
addNetElement(&m_surfaceMovingCollision);
addNetElement(&m_xRelativeSurfaceMovingCollisionPosition);
addNetElement(&m_yRelativeSurfaceMovingCollisionPosition);
m_world = nullptr;
resetParameters(parameters);
}
MovementParameters const& MovementController::parameters() const {
return m_parameters;
}
void MovementController::applyParameters(MovementParameters const& parameters) {
updateParameters(m_parameters.merge(parameters));
}
void MovementController::resetParameters(MovementParameters const& parameters) {
updateParameters(MovementParameters::sensibleDefaults().merge(parameters));
}
Json MovementController::storeState() const {
return JsonObject{
{"position", jsonFromVec2F(position())},
{"velocity", jsonFromVec2F(velocity())},
{"rotation", rotation()}
};
}
void MovementController::loadState(Json const& state) {
setPosition(jsonToVec2F(state.get("position")));
setVelocity(jsonToVec2F(state.get("velocity")));
setRotation(state.getFloat("rotation"));
}
float MovementController::mass() const {
return m_mass.get();
}
PolyF const& MovementController::collisionPoly() const {
return m_collisionPoly.get();
}
void MovementController::setCollisionPoly(PolyF const& poly) {
m_collisionPoly.set(poly);
}
Vec2F MovementController::position() const {
return {m_xPosition.get(), m_yPosition.get()};
}
float MovementController::xPosition() const {
return m_xPosition.get();
}
float MovementController::yPosition() const {
return m_yPosition.get();
}
Vec2F MovementController::velocity() const {
return {m_xVelocity.get(), m_yVelocity.get()};
}
float MovementController::xVelocity() const {
return m_xVelocity.get();
}
float MovementController::yVelocity() const {
return m_yVelocity.get();
}
float MovementController::rotation() const {
return m_rotation.get();
}
PolyF MovementController::collisionBody() const {
auto poly = collisionPoly();
poly.rotate(rotation());
poly.translate(position());
return poly;
}
RectF MovementController::localBoundBox() const {
auto poly = collisionPoly();
poly.rotate(rotation());
return poly.boundBox();
}
RectF MovementController::collisionBoundBox() const {
return collisionBody().boundBox();
}
bool MovementController::isColliding() const {
return m_colliding.get();
}
bool MovementController::isNullColliding() const {
return m_nullColliding.get();
}
bool MovementController::isCollisionStuck() const {
return m_collisionStuck.get();
}
Maybe<float> MovementController::stickingDirection() const {
return m_stickingDirection.get();
}
float MovementController::liquidPercentage() const {
return m_liquidPercentage;
}
LiquidId MovementController::liquidId() const {
return m_liquidId;
}
bool MovementController::onGround() const {
return m_onGround.get();
}
bool MovementController::zeroG() const {
return m_zeroG.get();
}
bool MovementController::atWorldLimit(bool bottomOnly) const {
if (m_world) {
if (!collisionPoly().isNull()) {
auto bounds = collisionBoundBox();
return bounds.yMin() <= 0 || (!bottomOnly && bounds.yMax() >= m_world->geometry().height());
} else {
return yPosition() <= 0 || (!bottomOnly && yPosition() >= m_world->geometry().height());
}
}
return false;
}
void MovementController::setPosition(Vec2F position) {
if (m_world)
position = m_world->geometry().limit(position);
if (position[0] != m_xPosition.get() || position[1] != m_yPosition.get())
m_resting = false;
m_xPosition.set(position[0]);
m_yPosition.set(position[1]);
}
void MovementController::setXPosition(float xPosition) {
setPosition({xPosition, yPosition()});
}
void MovementController::setYPosition(float yPosition) {
setPosition({xPosition(), yPosition});
}
void MovementController::translate(Vec2F const& direction) {
setPosition(position() + direction);
}
void MovementController::setVelocity(Vec2F vel) {
if (m_parameters.speedLimit && vel.magnitude() > *m_parameters.speedLimit) {
vel.normalize();
vel *= *m_parameters.speedLimit;
}
if ((velocity() - vel).magnitude() > 0.0001)
m_resting = false;
m_xVelocity.set(vel[0]);
m_yVelocity.set(vel[1]);
}
void MovementController::setXVelocity(float xVelocity) {
setVelocity({xVelocity, yVelocity()});
}
void MovementController::setYVelocity(float yVelocity) {
setVelocity({xVelocity(), yVelocity});
}
void MovementController::addMomentum(Vec2F const& momentum) {
setVelocity(velocity() + momentum / mass());
}
void MovementController::setRotation(float rotation) {
m_resting = false;
m_rotation.set(rotation);
}
void MovementController::rotate(float rotationRate) {
if (rotationRate == 0.0)
return;
m_resting = false;
m_rotation.set(fmod(rotation() + rotationRate * m_timeStep, 2 * Constants::pi));
}
void MovementController::accelerate(Vec2F const& acceleration) {
setVelocity(velocity() + acceleration * m_timeStep);
}
void MovementController::force(Vec2F const& force) {
setVelocity(velocity() + force / mass() * m_timeStep);
}
void MovementController::approachVelocity(Vec2F const& targetVelocity, float maxControlForce) {
// Instead of applying the force directly, work backwards and figure out the
// maximum acceleration that could be achieved by the current control force,
// and maximize the change in velocity based on that.
Vec2F diff = targetVelocity - velocity();
float diffMagnitude = vmag(diff);
if (diffMagnitude == 0.0f)
return;
float maximumAcceleration = maxControlForce / mass() * m_timeStep;
float clampedMagnitude = clamp(diffMagnitude, 0.0f, maximumAcceleration);
setVelocity(velocity() + diff * (clampedMagnitude / diffMagnitude));
}
void MovementController::approachVelocityAlongAngle(float angle, float targetVelocity, float maxControlForce, bool positiveOnly) {
// Same strategy as approachVelocity, work backwards to figure out the
// maximum acceleration and apply that.
// Project the current velocity along the axis normal, the velocity
// difference is the difference between the targetVelocity and this
// projection.
Vec2F axis = Vec2F::withAngle(angle, 1.0f);
float velocityAlongAxis = velocity() * axis;
float diff = targetVelocity - velocityAlongAxis;
if (diff == 0.0f)
return;
if (positiveOnly && diff < 0)
return;
float maximumAcceleration = maxControlForce / mass() * m_timeStep;
float diffMagnitude = std::fabs(diff);
float clampedMagnitude = clamp(diffMagnitude, 0.0f, maximumAcceleration);
setVelocity(velocity() + axis * diff * (clampedMagnitude / diffMagnitude));
}
void MovementController::approachXVelocity(float targetXVelocity, float maxControlForce) {
approachVelocityAlongAngle(0.0f, targetXVelocity, maxControlForce);
}
void MovementController::approachYVelocity(float targetYVelocity, float maxControlForce) {
approachVelocityAlongAngle(Constants::pi / 2, targetYVelocity, maxControlForce);
}
void MovementController::init(World* world) {
m_world = world;
setPosition(position());
updatePositionInterpolators();
}
void MovementController::uninit() {
m_world = nullptr;
updatePositionInterpolators();
}
void MovementController::setTimestep(float dt) {
m_timeStep = dt;
}
void MovementController::tickMaster(float dt) {
setTimestep(dt);
auto geometry = world()->geometry();
m_zeroG.set(!*m_parameters.gravityEnabled || *m_parameters.gravityMultiplier == 0 || world()->gravity(position()) == 0);
Maybe<PhysicsMovingCollision> surfaceCollision;
if (auto movingCollisionId = m_surfaceMovingCollision.get()) {
if (auto physicsEntity = world()->get<PhysicsEntity>(movingCollisionId->physicsEntityId))
surfaceCollision = physicsEntity->movingCollision(movingCollisionId->collisionIndex);
}
if (surfaceCollision) {
Vec2F surfacePositionDelta = geometry.diff(surfaceCollision->position, m_surfaceMovingCollisionPosition);
setPosition(position() + surfacePositionDelta);
Vec2F newSurfaceVelocity = surfacePositionDelta / dt;
setVelocity(velocity() - m_surfaceVelocity + newSurfaceVelocity);
m_surfaceVelocity = newSurfaceVelocity;
} else {
m_surfaceMovingCollision.set({});
m_surfaceMovingCollisionPosition = {};
m_surfaceVelocity = {};
}
if (m_resting) {
m_restTicks -= 1;
if (m_restTicks < 0) {
m_resting = false;
}
}
// don't integrate velocity when resting
Vec2F relativeVelocity = m_resting ? Vec2F() : velocity();
Vec2F originalMovement = relativeVelocity * dt;
if (surfaceCollision)
relativeVelocity -= m_surfaceVelocity;
m_collisionCorrection = {};
m_surfaceSlope = Vec2F(1, 0);
m_surfaceMovingCollision.set({});
unsigned steps;
float maxMovementPerStep = *m_parameters.maxMovementPerStep;
if (maxMovementPerStep > 0.0f)
steps = std::floor(vmag(relativeVelocity) * dt / maxMovementPerStep) + 1;
else
steps = 1;
// skip collision checks when resting (there's no movement anyway)
if (m_resting)
steps = 0;
for (unsigned i = 0; i < steps; ++i) {
float dtSteps = dt / steps;
Vec2F movement = relativeVelocity * dtSteps;
if (!*m_parameters.collisionEnabled || collisionPoly().isNull()) {
setPosition(position() + movement);
m_surfaceSlope = Vec2F(1, 0);
m_surfaceVelocity = Vec2F(0, 0);
m_colliding.set(false);
m_collisionStuck.set(false);
m_nullColliding.set(false);
m_stickingDirection.set({});
m_onGround.set(false);
} else {
auto body = collisionBody();
float velocityMagnitude = vmag(relativeVelocity);
Vec2F velocityDirection = relativeVelocity / velocityMagnitude;
bool ignorePlatforms = *m_parameters.ignorePlatformCollision || relativeVelocity[1] > 0;
float maximumCorrection = *m_parameters.maximumCorrection;
float maximumPlatformCorrection = *m_parameters.maximumPlatformCorrection
+ *m_parameters.maximumPlatformCorrectionVelocityFactor * velocityMagnitude;
Vec2F bodyCenter = body.center();
RectF queryBounds = body.boundBox().padded(maximumCorrection);
queryBounds.combine(queryBounds.translated(movement));
queryCollisions(queryBounds);
auto result = collisionMove(m_workingCollisions, body, movement, ignorePlatforms, *m_parameters.enableSurfaceSlopeCorrection && !zeroG(),
maximumCorrection, maximumPlatformCorrection, bodyCenter, dtSteps);
setPosition(position() + result.movement);
if (result.collisionKind == CollisionKind::Null) {
m_nullColliding.set(true);
break;
} else {
m_nullColliding.set(false);
}
Vec2F correction = result.correction;
Vec2F normCorrection = vnorm(correction);
m_surfaceSlope = result.groundSlope;
m_surfaceMovingCollision.set(result.surfaceMovingCollisionId);
m_collisionCorrection += correction;
m_colliding.set(correction != Vec2F() || result.isStuck);
m_onGround.set(!zeroG() && result.onGround);
m_collisionStuck.set(result.isStuck);
// If we have collided, apply either sticky or normal (bouncing) collision physics
if (correction != Vec2F()) {
if (*m_parameters.stickyCollision && result.collisionKind != CollisionKind::Slippery) {
// When sticking, cancel all velocity and apply stickyForce in the
// opposite of the direction of collision correction.
relativeVelocity = -normCorrection * *m_parameters.stickyForce / mass() * dtSteps;
m_stickingDirection.set(-normCorrection.angle());
break;
} else {
m_stickingDirection.set({});
float correctionMagnitude = vmag(correction);
Vec2F correctionDirection = correction / correctionMagnitude;
if (*m_parameters.bounceFactor != 0.0f) {
Vec2F adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection));
relativeVelocity += adjustment + *m_parameters.bounceFactor * adjustment;
if (*m_parameters.stopOnFirstBounce) {
// When bouncing, stop integrating at the moment of bounce. This
// prevents the frame of contact from being missed due to multiple
// iterations per frame.
break;
}
} else {
// Only adjust the velocity to the extent that the collision was
// caused by the velocity in each axis, to eliminate collision
// induced velocity in a platformery way (each axis considered
// independently).
if (relativeVelocity[0] < 0 && correction[0] > 0)
relativeVelocity[0] = min(0.0f, relativeVelocity[0] + correction[0] / dt);
else if (relativeVelocity[0] > 0 && correction[0] < 0)
relativeVelocity[0] = max(0.0f, relativeVelocity[0] + correction[0] / dt);
if (relativeVelocity[1] < 0 && correction[1] > 0)
relativeVelocity[1] = min(0.0f, relativeVelocity[1] + correction[1] / dt);
else if (relativeVelocity[1] > 0 && correction[1] < 0)
relativeVelocity[1] = max(0.0f, relativeVelocity[1] + correction[1] / dt);
}
}
}
}
}
Vec2F newVelocity = relativeVelocity + m_surfaceVelocity;
Vec2F pos = position();
PolyF body = collisionBody();
RectF boundBox = body.boundBox();
updateLiquidPercentage();
if (auto movingCollisionId = m_surfaceMovingCollision.get()) {
if (auto physicsEntity = world()->get<PhysicsEntity>(movingCollisionId->physicsEntityId))
surfaceCollision = physicsEntity->movingCollision(movingCollisionId->collisionIndex);
}
if (surfaceCollision) {
m_surfaceMovingCollisionPosition = surfaceCollision->position;
m_xRelativeSurfaceMovingCollisionPosition.set(geometry.diff(xPosition(), m_surfaceMovingCollisionPosition[0]));
m_yRelativeSurfaceMovingCollisionPosition.set(yPosition() - m_surfaceMovingCollisionPosition[1]);
} else {
m_surfaceMovingCollisionPosition = {};
m_surfaceVelocity = {};
}
// In order to make control work accurately, passive forces need to be
// applied to velocity *after* integrating. This prevents control from
// having to account for one timestep of passive forces in order to result
// in the correct controlled movement.
if (!zeroG() && !stickingDirection()) {
float buoyancy = *m_parameters.liquidBuoyancy * m_liquidPercentage + *m_parameters.airBuoyancy * (1.0f - liquidPercentage());
float gravity = world()->gravity(position()) * *m_parameters.gravityMultiplier * (1.0f - buoyancy);
Vec2F environmentVelocity;
environmentVelocity[1] -= gravity * dt;
if (onGround() && *m_parameters.slopeSlidingFactor != 0 && m_surfaceSlope[1] != 0)
environmentVelocity += -m_surfaceSlope * (m_surfaceSlope[0] * m_surfaceSlope[1]) * *m_parameters.slopeSlidingFactor;
newVelocity += environmentVelocity;
}
// If original movement was entirely (almost) in the direction of gravity
// and was entirely (almost) cancelled by collision correction, put the
// entity into rest for restDuration
if (!m_resting &&
abs(originalMovement[0]) < 0.0001 &&
originalMovement[1] * gravity() <= 0.0 &&
abs(originalMovement[1] + m_collisionCorrection[1]) < 0.0001) {
m_resting = true;
m_restTicks = m_parameters.restDuration.value(0);
}
if (*m_parameters.frictionEnabled) {
Vec2F refVel;
float friction = liquidPercentage() * *m_parameters.liquidFriction + (1.0f - liquidPercentage()) * *m_parameters.airFriction;
if (onGround()) {
friction = max(friction, *m_parameters.groundFriction);
refVel = m_surfaceVelocity;
}
// The equation for friction here is effectively:
// frictionForce = friction * (refVel - velocity)
// but it is applied here as a multiplicitave factor from [0, 1] so it does
// not induce oscillation at very high friction and so it cannot be
// negative.
float frictionFactor = clamp(friction / mass() * dt, 0.0f, 1.0f);
newVelocity = lerp(frictionFactor, newVelocity, refVel);
}
setVelocity(newVelocity);
updateForceRegions(dt);
}
void MovementController::tickSlave(float dt) {
setTimestep(dt);
if (auto movingCollisionId = m_surfaceMovingCollision.get()) {
if (auto physicsEntity = world()->get<PhysicsEntity>(movingCollisionId->physicsEntityId)) {
if (auto collision = physicsEntity->movingCollision(movingCollisionId->collisionIndex)) {
m_xPosition.set(m_xRelativeSurfaceMovingCollisionPosition.get() + collision->position[0]);
m_yPosition.set(m_yRelativeSurfaceMovingCollisionPosition.get() + collision->position[1]);
}
}
}
LiquidLevel cll = world()->liquidLevel(collisionBody().boundBox());
m_liquidPercentage = clamp(cll.level, 0.0f, 1.0f);
m_liquidId = cll.liquid;
}
void MovementController::setIgnorePhysicsEntities(Set<EntityId> ignorePhysicsEntities) {
m_ignorePhysicsEntities = ignorePhysicsEntities;
}
void MovementController::forEachMovingCollision(RectF const& region, function<bool(MovingCollisionId id, PhysicsMovingCollision, PolyF, RectF)> callback) {
auto geometry = world()->geometry();
for (auto& physicsEntity : world()->query<PhysicsEntity>(region)) {
if (m_ignorePhysicsEntities.contains(physicsEntity->entityId()))
continue;
for (size_t i = 0; i < physicsEntity->movingCollisionCount(); ++i) {
if (auto mc = physicsEntity->movingCollision(i)) {
if (mc->categoryFilter.check(m_parameters.physicsEffectCategories.value())) {
PolyF poly = std::move(mc->collision);
poly.translate(geometry.nearestTo(region.min(), mc->position));
RectF polyBounds = poly.boundBox();
if (region.intersects(polyBounds)) {
// early exit if the callback returns false
if(callback({physicsEntity->entityId(), i}, *mc, poly, polyBounds) == false)
return;
}
}
}
}
}
}
void MovementController::updateForceRegions(float dt) {
auto geometry = world()->geometry();
auto pos = position();
auto body = collisionBody();
RectF boundBox = body.boundBox();
m_appliedForceRegion = false;
auto handleForceRegions = [&](List<PhysicsForceRegion> const& forces) {
for (auto const& force : forces) {
bool categoryCheck = force.call([myCategories = m_parameters.physicsEffectCategories.value()](auto& fr) {
return fr.categoryFilter.check(myCategories);
});
if (!categoryCheck)
continue;
bool boundsCheck = force.call([geometry, myBounds = collisionBoundBox()](auto& fr) {
return geometry.rectIntersectsRect(myBounds, fr.boundBox());
});
if (!boundsCheck)
continue;
m_appliedForceRegion = true;
if (auto directionalForceRegion = force.ptr<DirectionalForceRegion>()) {
float forceEffect = geometry.polyOverlapArea(directionalForceRegion->region, body) / body.convexArea();
if (directionalForceRegion->xTargetVelocity)
approachXVelocity(*directionalForceRegion->xTargetVelocity, directionalForceRegion->controlForce * forceEffect);
if (directionalForceRegion->yTargetVelocity)
approachYVelocity(*directionalForceRegion->yTargetVelocity, directionalForceRegion->controlForce * forceEffect);
} else if (auto radialForceRegion = force.ptr<RadialForceRegion>()) {
Vec2F direction = geometry.diff(pos, radialForceRegion->center);
float distance = vmag(direction);
if (distance > 0 && distance < radialForceRegion->outerRadius) {
float incidence = min(1.0f - (distance - radialForceRegion->innerRadius) / (radialForceRegion->outerRadius - radialForceRegion->innerRadius), distance / radialForceRegion->innerRadius);
if (radialForceRegion->targetRadialVelocity < 0)
direction = -direction;
approachVelocityAlongAngle(direction.angle(),
abs(radialForceRegion->targetRadialVelocity),
radialForceRegion->controlForce * incidence,
true);
}
} else if (auto gradientForceRegion = force.ptr<GradientForceRegion>()) {
float overlapFactor = geometry.polyOverlapArea(gradientForceRegion->region, body) / body.convexArea();
Vec2F gNorm = gradientForceRegion->gradient.direction();
Vec2F pDiff = geometry.diff(pos, gradientForceRegion->gradient.min());
float projected = pDiff[0] * gNorm[0] + pDiff[1] * gNorm[1];
float gradientFactor = 1.0 - clamp(projected / gradientForceRegion->gradient.length(), -1.0f, 1.0f);
approachVelocityAlongAngle(gradientForceRegion->gradient.angle(),
gradientForceRegion->baseTargetVelocity * overlapFactor * gradientFactor,
gradientForceRegion->baseControlForce * overlapFactor * gradientFactor,
true);
}
}
};
for (auto& physicsEntity : world()->query<PhysicsEntity>(boundBox)) {
if (m_ignorePhysicsEntities.contains(physicsEntity->entityId()))
continue;
handleForceRegions(physicsEntity->forceRegions());
}
handleForceRegions(world()->forceRegions());
}
void MovementController::updateLiquidPercentage() {
auto geometry = world()->geometry();
auto pos = position();
auto body = collisionBody();
RectF boundBox = body.boundBox();
LiquidLevel cll;
if (boundBox.isEmpty())
cll = world()->liquidLevel(Vec2I::floor(pos));
else
cll = world()->liquidLevel(boundBox);
m_liquidPercentage = clamp(cll.level, 0.0f, 1.0f);
m_liquidId = cll.liquid;
}
void MovementController::setOnGround(bool onGround) {
m_onGround.set(onGround);
}
bool MovementController::appliedForceRegion() const {
return m_appliedForceRegion;
}
Vec2F MovementController::collisionCorrection() const {
return m_collisionCorrection;
}
Vec2F MovementController::surfaceSlope() const {
return m_surfaceSlope;
}
Vec2F MovementController::surfaceVelocity() const {
return m_surfaceVelocity;
}
World* MovementController::world() {
if (!m_world)
throw MovementControllerException("MovementController not initialized!");
return m_world;
}
CollisionKind MovementController::maxOrNullCollision(CollisionKind a, CollisionKind b) {
if (a == CollisionKind::Null || b == CollisionKind::Null)
return CollisionKind::Null;
else
return max(a, b);
}
MovementController::CollisionResult MovementController::collisionMove(List<CollisionPoly>& collisionPolys, PolyF const& body, Vec2F const& movement,
bool ignorePlatforms, bool enableSurfaceSlopeCorrection, float maximumCorrection, float maximumPlatformCorrection, Vec2F sortCenter, float dt) {
unsigned const MaximumSeparationLoops = 3;
float const SlideAngle = Constants::pi / 3;
float const SlideCorrectionLimit = 0.2f;
float separationTolerance = 0.001f * (dt * 60.f);
maximumPlatformCorrection *= (dt * 60.f);
if (body.isNull())
return {movement, Vec2F(), {}, false, false, Vec2F(1, 0), CollisionKind::None};
PolyF translatedBody = body;
translatedBody.translate(movement);
PolyF checkBody = translatedBody;
Vec2F totalCorrection = Vec2F();
CollisionKind maxCollided = CollisionKind::None;
Maybe<MovingCollisionId> surfaceMovingCollisionId;
CollisionSeparation separation = {};
if (enableSurfaceSlopeCorrection) {
// First try separating with our ground sliding cheat.
separation = collisionSeparate(collisionPolys, checkBody, ignorePlatforms, maximumPlatformCorrection, sortCenter, true, separationTolerance);
totalCorrection += separation.correction;
checkBody.translate(separation.correction);
maxCollided = maxOrNullCollision(maxCollided, separation.collisionKind);
surfaceMovingCollisionId = separation.movingCollisionId;
Vec2F upwardResult = movement + separation.correction;
float upMag = upwardResult.magnitude();
// Angle off of horizontal (minimum of either direction)
float angleHoriz = std::min(Vec2F(1, 0).angleBetweenNormalized(upwardResult / upMag),
Vec2F(-1, 0).angleBetweenNormalized(upwardResult / upMag));
// We need to make sure that even if we found a solution with the sliding
// cheat, we are not beyond the angle and correction limits for the ground
// cheat correction.
if (separation.solutionFound)
separation.solutionFound = upMag < SlideCorrectionLimit || angleHoriz < SlideAngle;
if (separation.solutionFound) {
if (totalCorrection.magnitude() > maximumCorrection)
separation.solutionFound = false;
}
}
if (!separation.solutionFound) {
checkBody = translatedBody;
totalCorrection = Vec2F();
for (size_t i = 0; i < MaximumSeparationLoops; ++i) {
separation = collisionSeparate(collisionPolys, checkBody, ignorePlatforms,
maximumPlatformCorrection, sortCenter, false, separationTolerance);
totalCorrection += separation.correction;
checkBody.translate(separation.correction);
maxCollided = maxOrNullCollision(maxCollided, separation.collisionKind);
surfaceMovingCollisionId = {};
if (totalCorrection.magnitude() > maximumCorrection) {
separation.solutionFound = false;
break;
}
if (separation.solutionFound)
break;
}
}
if (!separation.solutionFound && movement != Vec2F()) {
// No collision solution found! Move checkBody back to original body before
// applying movement and try one last time to correct.
checkBody = body;
totalCorrection = -movement;
for (size_t i = 0; i < MaximumSeparationLoops; ++i) {
separation = collisionSeparate(collisionPolys, checkBody, true, maximumPlatformCorrection, sortCenter, false, separationTolerance);
totalCorrection += separation.correction;
checkBody.translate(separation.correction);
maxCollided = maxOrNullCollision(maxCollided, separation.collisionKind);
if (totalCorrection.magnitude() > maximumCorrection) {
separation.solutionFound = false;
break;
}
if (separation.solutionFound)
break;
}
}
if (separation.solutionFound) {
CollisionResult result;
result.movement = movement + totalCorrection;
result.correction = totalCorrection;
result.isStuck = false;
result.onGround = result.correction[1] > separationTolerance;
result.surfaceMovingCollisionId = surfaceMovingCollisionId;
result.collisionKind = maxCollided;
result.groundSlope = Vec2F(1, 0);
if (result.onGround) {
// If we are on the ground and need to find the ground slope, look for a
// vertex on the body being moved that is touching an edge of one of the
// collision polys. We only want a slope to be produced from an edge of
// colision geometry, not an edge of the colliding body. Pick the
// touching edge that is the most horizontally overlapped with the
// geometry, rather than off to the side.
float maxSideHorizontalOverlap = 0.0f;
RectF touchingBounds = checkBody.boundBox();
touchingBounds.pad(separationTolerance);
for (auto const& cp : collisionPolys) {
if (!cp.polyBounds.intersects(touchingBounds))
continue;
for (size_t i = 0; i < cp.poly.sides(); ++i) {
auto side = cp.poly.side(i);
RectF sideBounds = RectF::boundBoxOf(side.min(), side.max());
float thisSideHorizontalOverlap = sideBounds.overlap(touchingBounds).width();
if (thisSideHorizontalOverlap > maxSideHorizontalOverlap) {
for (auto const& bodyVertex : checkBody) {
float t = clamp(side.lineProjection(bodyVertex), 0.0f, 1.0f);
Vec2F nearPoint = side.eval(t);
if (nearPoint[1] > cp.sortPosition[1]) {
if (vmagSquared(bodyVertex - nearPoint) <= square(separationTolerance)) {
maxSideHorizontalOverlap = thisSideHorizontalOverlap;
result.groundSlope = side.diff().normalized();
if (result.groundSlope[0] < 0)
result.groundSlope *= -1;
}
}
}
}
}
}
}
return result;
} else {
return {Vec2F(), -movement, {}, true, true, Vec2F(1, 0), maxCollided};
}
}
MovementController::CollisionSeparation MovementController::collisionSeparate(List<CollisionPoly>& collisionPolys, PolyF const& poly,
bool ignorePlatforms, float maximumPlatformCorrection, Vec2F const& sortCenter, bool upward, float separationTolerance) {
CollisionSeparation separation = {};
separation.collisionKind = CollisionKind::None;
bool intersects = false;
for (auto& cp : collisionPolys)
cp.sortDistance = vmagSquared(cp.sortPosition - sortCenter);
sort(collisionPolys, [](auto const& a, auto const& b) {
return a.sortDistance < b.sortDistance;
});
PolyF::IntersectResult intersectResult;
PolyF correctedPoly = poly;
RectF correctedBoundBox = correctedPoly.boundBox();
for (auto const& cp : collisionPolys) {
if ((ignorePlatforms && cp.collisionKind == CollisionKind::Platform) || !correctedBoundBox.intersects(cp.polyBounds, false))
continue;
if (upward)
intersectResult = correctedPoly.directionalSatIntersection(cp.poly, Vec2F(0, 1), false);
else if (cp.collisionKind == CollisionKind::Platform)
intersectResult = correctedPoly.directionalSatIntersection(cp.poly, Vec2F(0, 1), true);
else
intersectResult = correctedPoly.satIntersection(cp.poly);
if (cp.collisionKind == CollisionKind::Platform && intersectResult.intersects) {
if (intersectResult.overlap[1] <= 0 || intersectResult.overlap[1] > maximumPlatformCorrection)
intersectResult.intersects = false;
}
if (intersectResult.intersects) {
intersects = true;
correctedPoly.translate(intersectResult.overlap);
correctedBoundBox = correctedPoly.boundBox();
separation.correction += intersectResult.overlap;
if (cp.movingCollisionId)
separation.movingCollisionId = cp.movingCollisionId;
separation.collisionKind = maxOrNullCollision(separation.collisionKind, cp.collisionKind);
}
}
separation.solutionFound = true;
float separationToleranceSquared = square(separationTolerance);
if (intersects) {
for (auto const& cp : collisionPolys) {
if (cp.collisionKind == CollisionKind::Platform || !correctedBoundBox.intersects(cp.polyBounds, false))
continue;
intersectResult = correctedPoly.satIntersection(cp.poly);
if (intersectResult.intersects && intersectResult.overlap.magnitudeSquared() > separationToleranceSquared) {
separation.collisionKind = maxOrNullCollision(separation.collisionKind, cp.collisionKind);
separation.solutionFound = false;
break;
}
}
}
return separation;
}
void MovementController::updateParameters(MovementParameters parameters) {
m_parameters = std::move(parameters);
m_collisionPoly.set(*m_parameters.collisionPoly);
m_mass.set(*m_parameters.mass);
updatePositionInterpolators();
}
void MovementController::updatePositionInterpolators() {
if (m_world)
m_xPosition.setInterpolator(m_world->geometry().xLerpFunction(m_parameters.discontinuityThreshold));
else
m_xPosition.setInterpolator(bind(lerpWithLimit<float, float>, m_parameters.discontinuityThreshold, _1, _2, _3));
m_yPosition.setInterpolator(bind(lerpWithLimit<float, float>, m_parameters.discontinuityThreshold, _1, _2, _3));
}
void MovementController::queryCollisions(RectF const& region) {
while (!m_workingCollisions.empty()) {
m_collisionBuffers.append(m_workingCollisions.takeLast().poly);
}
auto newCollisionPoly = [this]() -> CollisionPoly& {
if (!m_collisionBuffers.empty())
return m_workingCollisions.emplaceAppend(CollisionPoly{
m_collisionBuffers.takeLast(), {}, {}, {}, {}, {}
});
else
return m_workingCollisions.emplaceAppend(CollisionPoly{});
};
auto geometry = world()->geometry();
world()->forEachCollisionBlock(RectI::integral(region.padded(1)), [&](CollisionBlock const& block) {
if (block.kind != CollisionKind::None && !block.poly.isNull()) {
RectF polyBounds = block.polyBounds;
Vec2F basePosition = block.poly.vertex(0);
Vec2F nearTranslation = geometry.nearestTo(region.min(), basePosition) - basePosition;
polyBounds.translate(nearTranslation);
if (region.intersects(polyBounds)) {
CollisionPoly& collisionPoly = newCollisionPoly();
collisionPoly.poly = block.poly;
collisionPoly.poly.translate(nearTranslation);
collisionPoly.polyBounds = polyBounds;
collisionPoly.sortPosition = centerOfTile(block.space);
collisionPoly.movingCollisionId = {};
collisionPoly.collisionKind = block.kind;
}
}
});
forEachMovingCollision(region, [&](MovingCollisionId id, PhysicsMovingCollision mc, PolyF poly, RectF bounds) {
CollisionPoly& collisionPoly = newCollisionPoly();
collisionPoly.poly = std::move(poly);
collisionPoly.polyBounds = bounds;
collisionPoly.sortPosition = collisionPoly.poly.center();
collisionPoly.movingCollisionId = id;
collisionPoly.collisionKind = mc.collisionKind;
return true;
});
}
float MovementController::gravity() {
float buoyancy = *m_parameters.liquidBuoyancy * m_liquidPercentage + *m_parameters.airBuoyancy * (1.0f - liquidPercentage());
return world()->gravity(position()) * *m_parameters.gravityMultiplier * (1.0f - buoyancy);
}
}