diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarMovementController.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarMovementController.cpp')
-rw-r--r-- | source/game/StarMovementController.cpp | 1109 |
1 files changed, 1109 insertions, 0 deletions
diff --git a/source/game/StarMovementController.cpp b/source/game/StarMovementController.cpp new file mode 100644 index 0000000..26e540a --- /dev/null +++ b/source/game/StarMovementController.cpp @@ -0,0 +1,1109 @@ +#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>); + m_rotation.setInterpolator(lerp<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(); +} + +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); +} + +}
\ No newline at end of file |