2023-06-20 04:33:09 +00:00
|
|
|
#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_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>);
|
2023-06-23 09:06:05 +00:00
|
|
|
m_rotation.setInterpolator(angleLerp<float, float>);
|
2023-06-20 04:33:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
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 * WorldTimestep, 2 * Constants::pi));
|
|
|
|
}
|
|
|
|
|
|
|
|
void MovementController::accelerate(Vec2F const& acceleration) {
|
|
|
|
setVelocity(velocity() + acceleration * WorldTimestep);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MovementController::force(Vec2F const& force) {
|
|
|
|
setVelocity(velocity() + force / mass() * WorldTimestep);
|
|
|
|
}
|
|
|
|
|
|
|
|
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() * WorldTimestep;
|
|
|
|
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() * WorldTimestep;
|
|
|
|
|
|
|
|
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::tickMaster() {
|
|
|
|
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 / WorldTimestep;
|
|
|
|
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 * WorldTimestep;
|
|
|
|
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) * WorldTimestep / 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 dt = WorldTimestep / steps;
|
|
|
|
Vec2F movement = relativeVelocity * dt;
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
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() * dt;
|
|
|
|
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] / WorldTimestep);
|
|
|
|
else if (relativeVelocity[0] > 0 && correction[0] < 0)
|
|
|
|
relativeVelocity[0] = max(0.0f, relativeVelocity[0] + correction[0] / WorldTimestep);
|
|
|
|
|
|
|
|
if (relativeVelocity[1] < 0 && correction[1] > 0)
|
|
|
|
relativeVelocity[1] = min(0.0f, relativeVelocity[1] + correction[1] / WorldTimestep);
|
|
|
|
else if (relativeVelocity[1] > 0 && correction[1] < 0)
|
|
|
|
relativeVelocity[1] = max(0.0f, relativeVelocity[1] + correction[1] / WorldTimestep);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 * WorldTimestep;
|
|
|
|
|
|
|
|
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() * WorldTimestep, 0.0f, 1.0f);
|
|
|
|
newVelocity = lerp(frictionFactor, newVelocity, refVel);
|
|
|
|
}
|
|
|
|
|
|
|
|
setVelocity(newVelocity);
|
|
|
|
|
|
|
|
updateForceRegions();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MovementController::tickSlave() {
|
|
|
|
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 = 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() {
|
|
|
|
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) {
|
|
|
|
unsigned const MaximumSeparationLoops = 3;
|
|
|
|
float const SlideAngle = Constants::pi / 3;
|
|
|
|
float const SlideCorrectionLimit = 0.2f;
|
|
|
|
float const SeparationTolerance = 0.001f;
|
|
|
|
|
|
|
|
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 = 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 = 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|