Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/source/simulation2/components/CCmpUnitMotion.h
/* Copyright (C) 2021 Wildfire Games. | /* Copyright (C) 2022 Wildfire Games. | ||||
* This file is part of 0 A.D. | * This file is part of 0 A.D. | ||||
* | * | ||||
* 0 A.D. is free software: you can redistribute it and/or modify | * 0 A.D. is free software: you can redistribute it and/or modify | ||||
* it under the terms of the GNU General Public License as published by | * it under the terms of the GNU General Public License as published by | ||||
* the Free Software Foundation, either version 2 of the License, or | * the Free Software Foundation, either version 2 of the License, or | ||||
* (at your option) any later version. | * (at your option) any later version. | ||||
* | * | ||||
* 0 A.D. is distributed in the hope that it will be useful, | * 0 A.D. is distributed in the hope that it will be useful, | ||||
Show All 27 Lines | |||||
#include "simulation2/serialization/SerializedTypes.h" | #include "simulation2/serialization/SerializedTypes.h" | ||||
#include "graphics/Overlay.h" | #include "graphics/Overlay.h" | ||||
#include "maths/FixedVector2D.h" | #include "maths/FixedVector2D.h" | ||||
#include "ps/CLogger.h" | #include "ps/CLogger.h" | ||||
#include "ps/Profile.h" | #include "ps/Profile.h" | ||||
#include "renderer/Scene.h" | #include "renderer/Scene.h" | ||||
#include <algorithm> | |||||
// NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager. | // NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager. | ||||
// As such, both are compiled in the same TU. | // As such, both are compiled in the same TU. | ||||
// For debugging; units will start going straight to the target | // For debugging; units will start going straight to the target | ||||
// instead of calling the pathfinder | // instead of calling the pathfinder | ||||
#define DISABLE_PATHFINDER 0 | #define DISABLE_PATHFINDER 0 | ||||
namespace | namespace | ||||
▲ Show 20 Lines • Show All 366 Lines • ▼ Show 20 Lines | virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) | ||||
case MT_OwnershipChanged: | case MT_OwnershipChanged: | ||||
{ | { | ||||
OnValueModification(); | OnValueModification(); | ||||
break; | break; | ||||
} | } | ||||
case MT_Deserialized: | case MT_Deserialized: | ||||
{ | { | ||||
OnValueModification(); | OnValueModification(); | ||||
if (!ENTITY_IS_LOCAL(GetEntityId())) | |||||
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController); | |||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
void UpdateMessageSubscriptions() | void UpdateMessageSubscriptions() | ||||
{ | { | ||||
bool needRender = m_DebugOverlayEnabled; | bool needRender = m_DebugOverlayEnabled; | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const | ||||
CFixedVector2D pos = cmpPosition->GetPosition2D(); | CFixedVector2D pos = cmpPosition->GetPosition2D(); | ||||
entity_angle_t angle = cmpPosition->GetRotation().Y; | entity_angle_t angle = cmpPosition->GetRotation().Y; | ||||
fixed speed = m_CurrentSpeed; | fixed speed = m_CurrentSpeed; | ||||
// Copy the path so we don't change it. | // Copy the path so we don't change it. | ||||
WaypointPath shortPath = m_ShortPath; | WaypointPath shortPath = m_ShortPath; | ||||
WaypointPath longPath = m_LongPath; | WaypointPath longPath = m_LongPath; | ||||
PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle); | PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle, 0); | ||||
return pos; | return pos; | ||||
} | } | ||||
virtual fixed GetAcceleration() const | virtual fixed GetAcceleration() const | ||||
{ | { | ||||
return m_Acceleration; | return m_Acceleration; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 253 Lines • ▼ Show 20 Lines | private: | ||||
*/ | */ | ||||
bool PossiblyAtDestination() const; | bool PossiblyAtDestination() const; | ||||
/** | /** | ||||
* Process the move the unit will do this turn. | * Process the move the unit will do this turn. | ||||
* This does not send actually change the position. | * This does not send actually change the position. | ||||
* @returns true if the move was obstructed. | * @returns true if the move was obstructed. | ||||
*/ | */ | ||||
bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const; | bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const; | ||||
/** | /** | ||||
* Update other components on our speed. | * Update other components on our speed. | ||||
* (For performance, this should try to avoid sending messages). | * (For performance, this should try to avoid sending messages). | ||||
*/ | */ | ||||
void UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed); | void UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed); | ||||
/** | /** | ||||
▲ Show 20 Lines • Show All 279 Lines • ▼ Show 20 Lines | |||||
{ | { | ||||
PROFILE("Move"); | PROFILE("Move"); | ||||
// If we're chasing a potentially-moving unit and are currently close | // If we're chasing a potentially-moving unit and are currently close | ||||
// enough to its current position, and we can head in a straight line | // enough to its current position, and we can head in a straight line | ||||
// to it, then throw away our current path and go straight to it. | // to it, then throw away our current path and go straight to it. | ||||
state.wentStraight = TryGoingStraightToTarget(state.initialPos, true); | state.wentStraight = TryGoingStraightToTarget(state.initialPos, true); | ||||
state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle); | state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle, state.pushingPressure); | ||||
} | } | ||||
void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt) | void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt) | ||||
{ | { | ||||
// Update our speed over this turn so that the visual actor shows the correct animation. | // Update our speed over this turn so that the visual actor shows the correct animation. | ||||
if (state.pos == state.initialPos) | if (state.pos == state.initialPos) | ||||
{ | { | ||||
if (state.angle != state.initialAngle) | if (state.angle != state.initialAngle) | ||||
state.cmpPosition->TurnTo(state.angle); | state.cmpPosition->TurnTo(state.angle); | ||||
UpdateMovementState(fixed::Zero(), fixed::Zero()); | UpdateMovementState(fixed::Zero(), fixed::Zero()); | ||||
} | } | ||||
else | else | ||||
{ | { | ||||
// Update the Position component after our movement (if we actually moved anywhere) | // Update the Position component after our movement (if we actually moved anywhere) | ||||
CFixedVector2D offset = state.pos - state.initialPos; | CFixedVector2D offset = state.pos - state.initialPos; | ||||
// When moving always set the angle in the direction of the movement, | |||||
// if we are not trying to move, assume this is pushing-related movement, | |||||
// and maintain the current angle instead. | |||||
if (IsMoveRequested()) | |||||
state.angle = atan2_approx(offset.X, offset.Y); | |||||
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle); | state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle); | ||||
// Calculate the mean speed over this past turn. | // Calculate the mean speed over this past turn. | ||||
UpdateMovementState(state.speed, offset.Length() / dt); | UpdateMovementState(state.speed, offset.Length() / dt); | ||||
} | } | ||||
if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos)) | if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos)) | ||||
return; | return; | ||||
Show All 40 Lines | if (m_MoveRequest.m_Type == MoveRequest::OFFSET) | ||||
CFixedVector2D targetPos; | CFixedVector2D targetPos; | ||||
ComputeTargetPosition(targetPos); | ComputeTargetPosition(targetPos); | ||||
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle()); | ||||
return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0; | return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0; | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const | bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const | ||||
{ | { | ||||
// If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. | // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. | ||||
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) | if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) | ||||
return true; | return true; | ||||
// Wrap the angle to (-Pi, Pi]. | // Wrap the angle to (-Pi, Pi]. | ||||
while (angle > entity_angle_t::Pi()) | while (angle > entity_angle_t::Pi()) | ||||
angle -= entity_angle_t::Pi() * 2; | angle -= entity_angle_t::Pi() * 2; | ||||
while (angle < -entity_angle_t::Pi()) | while (angle < -entity_angle_t::Pi()) | ||||
angle += entity_angle_t::Pi() * 2; | angle += entity_angle_t::Pi() * 2; | ||||
// TODO: there's some asymmetry here when units look at other | |||||
// units' positions - the result will depend on the order of execution. | |||||
// Maybe we should split the updates into multiple phases to minimise | |||||
// that problem. | |||||
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity()); | CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity()); | ||||
ENSURE(cmpPathfinder); | ENSURE(cmpPathfinder); | ||||
fixed basicSpeed = m_Speed; | fixed basicSpeed = m_Speed; | ||||
// If in formation, run to keep up; otherwise just walk. | // If in formation, run to keep up; otherwise just walk. | ||||
if (IsMovingAsFormation()) | if (IsMovingAsFormation()) | ||||
basicSpeed = m_Speed.Multiply(m_RunMultiplier); | basicSpeed = m_Speed.Multiply(m_RunMultiplier); | ||||
// If pushing pressure is applied, slow the unit down. | |||||
if (pushingPressure) | |||||
{ | |||||
// Values below this pressure don't slow the unit down (avoids slowing groups down). | |||||
constexpr int pressureMinThreshold = 10; | |||||
// Lower speed up to a floor to prevent units from getting stopped. | |||||
// This helped pushing particularly for fast units, since they'll end up slowing down. | |||||
constexpr int maxPressure = CCmpUnitMotionManager::MAX_PRESSURE - pressureMinThreshold - 80; | |||||
constexpr entity_pos_t floorSpeed = entity_pos_t::FromFraction(3, 2); | |||||
static_assert(maxPressure > 0); | |||||
uint8_t slowdown = maxPressure - std::min(maxPressure, std::max(0, pushingPressure - pressureMinThreshold)); | |||||
basicSpeed = basicSpeed.Multiply(fixed::FromInt(slowdown) / maxPressure); | |||||
// NB: lowering this too much will make the units behave a lot like viscous fluid | |||||
// when the density becomes extreme. While perhaps realistic (and kind of neat), | |||||
// it's not very helpful for gameplay. Empirically, a value of 1.5 avoids most of the effect | |||||
// while still slowing down movement significantly, and seems like a good balance. | |||||
// Min with the template speed to allow units that are explicitly absurdly slow. | |||||
basicSpeed = std::max(std::min(m_TemplateWalkSpeed, floorSpeed), basicSpeed); | |||||
} | |||||
// Find the speed factor of the underlying terrain. | // Find the speed factor of the underlying terrain. | ||||
// (We only care about the tile we start on - it doesn't matter if we're moving | // (We only care about the tile we start on - it doesn't matter if we're moving | ||||
// partially onto a much slower/faster tile). | // partially onto a much slower/faster tile). | ||||
// TODO: Terrain-dependent speeds are not currently supported. | // TODO: Terrain-dependent speeds are not currently supported. | ||||
fixed terrainSpeed = fixed::FromInt(1); | fixed terrainSpeed = fixed::FromInt(1); | ||||
fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); | fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); | ||||
Show All 17 Lines | while (timeLeft > zero) | ||||
CFixedVector2D target; | CFixedVector2D target; | ||||
if (shortPath.m_Waypoints.empty()) | if (shortPath.m_Waypoints.empty()) | ||||
target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); | target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); | ||||
else | else | ||||
target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); | target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); | ||||
CFixedVector2D offset = target - pos; | CFixedVector2D offset = target - pos; | ||||
if (turnRate > zero && !offset.IsZero()) | |||||
{ | |||||
fixed angleDiff = angle - atan2_approx(offset.X, offset.Y); | fixed angleDiff = angle - atan2_approx(offset.X, offset.Y); | ||||
fixed absoluteAngleDiff = angleDiff.Absolute(); | fixed absoluteAngleDiff = angleDiff.Absolute(); | ||||
if (absoluteAngleDiff > entity_angle_t::Pi()) | if (absoluteAngleDiff > entity_angle_t::Pi()) | ||||
absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff; | absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff; | ||||
// We only rotate to the instantTurnAngle angle. The rest we rotate during movement. | // We only rotate to the instantTurnAngle angle. The rest we rotate during movement. | ||||
if (absoluteAngleDiff > m_InstantTurnAngle) | if (absoluteAngleDiff > m_InstantTurnAngle) | ||||
{ | { | ||||
// Stop moving when rotating this far. | // Stop moving when rotating this far. | ||||
speed = zero; | speed = zero; | ||||
if (turnRate > zero && !offset.IsZero()) | |||||
{ | |||||
fixed maxRotation = turnRate.Multiply(timeLeft); | fixed maxRotation = turnRate.Multiply(timeLeft); | ||||
// Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction. | // Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction. | ||||
int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1; | int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1; | ||||
// Can't rotate far enough, just rotate in the correct direction. | // Can't rotate far enough, just rotate in the correct direction. | ||||
if (absoluteAngleDiff - m_InstantTurnAngle > maxRotation) | if (absoluteAngleDiff - m_InstantTurnAngle > maxRotation) | ||||
{ | { | ||||
angle += maxRotation * direction; | angle += maxRotation * direction; | ||||
if (angle * direction > entity_angle_t::Pi()) | if (angle * direction > entity_angle_t::Pi()) | ||||
angle -= entity_angle_t::Pi() * 2 * direction; | angle -= entity_angle_t::Pi() * 2 * direction; | ||||
break; | break; | ||||
} | } | ||||
// Rotate towards the next waypoint and continue moving. | // Rotate towards the next waypoint and continue moving. | ||||
angle = atan2_approx(offset.X, offset.Y); | angle = atan2_approx(offset.X, offset.Y); | ||||
timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate; | timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate; | ||||
} | } | ||||
} | |||||
else | else | ||||
{ | { | ||||
// Modify the speed depending on the angle difference. | // Modify the speed depending on the angle difference. | ||||
fixed sin, cos; | fixed sin, cos; | ||||
sincos_approx(angleDiff, sin, cos); | sincos_approx(angleDiff, sin, cos); | ||||
speed = speed.Multiply(cos); | speed = speed.Multiply(cos); | ||||
angle = atan2_approx(offset.X, offset.Y); | |||||
} | |||||
} | } | ||||
// Work out how far we can travel in timeLeft. | // Work out how far we can travel in timeLeft. | ||||
fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration); | fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration); | ||||
fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2; | fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2; | ||||
fixed maxdist = accelDist + maxSpeed.Multiply(timeLeft - accelTime); | fixed maxdist = accelDist + maxSpeed.Multiply(timeLeft - accelTime); | ||||
// If the target is close, we can move there directly. | // If the target is close, we can move there directly. | ||||
▲ Show 20 Lines • Show All 556 Lines • ▼ Show 20 Lines | if (extendRange) | ||||
{ | { | ||||
searchRange = dist.Length() + fixed::FromInt(1); | searchRange = dist.Length() + fixed::FromInt(1); | ||||
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) | if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) | ||||
searchRange = SHORT_PATH_MAX_SEARCH_RANGE; | searchRange = SHORT_PATH_MAX_SEARCH_RANGE; | ||||
} | } | ||||
} | } | ||||
m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH; | m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH; | ||||
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, GetGroup(), GetEntityId()); | m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, ShouldCollideWithMovingUnits(), GetGroup(), GetEntityId()); | ||||
} | } | ||||
bool CCmpUnitMotion::MoveTo(MoveRequest request) | bool CCmpUnitMotion::MoveTo(MoveRequest request) | ||||
{ | { | ||||
PROFILE("MoveTo"); | PROFILE("MoveTo"); | ||||
if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero()) | if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero()) | ||||
LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information"); | LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information"); | ||||
▲ Show 20 Lines • Show All 79 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator