Implement a Motion Manager around UnitMotion.

This new MotionManager handles movement for UnitMotion components (not
UnitMotionFlying).
This is a first step towards unit pushing, by giving a central place for
the relevant units to collide.

One important side-effect is that movement is effectively synchronous -
the positions are not actually updated until all units have moved for a
turn (refs 6a66fb8205).

As a side-effect, it's an optimisation: fewer messages are being sent
overall, which leads to a slight speedup (negligible without a lot of
units though).

This is a first step - ideally, the movement functions called from
UnitMotionManager would actually be moved there.

Differential Revision: https://code.wildfiregames.com/D3509
This was SVN commit r25071.
This commit is contained in:
wraitii 2021-03-17 17:04:51 +00:00
parent ae07dcb4ff
commit bae258f9a1
8 changed files with 367 additions and 89 deletions

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -196,6 +196,9 @@ INTERFACE(UnitMotion)
COMPONENT(UnitMotion) // must be after Obstruction
COMPONENT(UnitMotionScripted)
INTERFACE(UnitMotionManager)
COMPONENT(UnitMotionManager)
INTERFACE(UnitRenderer)
COMPONENT(UnitRenderer)

View File

@ -119,9 +119,8 @@ class CCmpUnitMotion : public ICmpUnitMotion
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
componentManager.SubscribeToMessageType(MT_Create);
componentManager.SubscribeToMessageType(MT_Destroy);
componentManager.SubscribeToMessageType(MT_PathResult);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
@ -207,15 +206,6 @@ public:
WaypointPath m_LongPath;
WaypointPath m_ShortPath;
// Hack - units move one-at-a-time, so they may need to interplate their target position.
// However, some computations are not doing during the motion messages, and those shouldn't (e.g. turn start).
// This is true if and only if the calls take place during handling of the entity's MT_Motion* messages.
// NB: this won't be true if we end up in UnitMotion because of another entity's motion messages,
// but I think it fixes the issue of interpolating target position OK for current needs,
// without having to add parameters everywhere.
// No need for serialisation, it's just a transient boolean.
bool m_InMotionMessage = false;
static std::string GetSchema()
{
return
@ -321,33 +311,6 @@ public:
{
switch (msg.GetType())
{
case MT_TurnStart:
{
TurnStart();
break;
}
case MT_Update_MotionFormation:
{
if (m_FormationController)
{
m_InMotionMessage = true;
fixed dt = static_cast<const CMessageUpdate_MotionFormation&> (msg).turnLength;
Move(dt);
m_InMotionMessage = false;
}
break;
}
case MT_Update_MotionUnit:
{
if (!m_FormationController)
{
m_InMotionMessage = true;
fixed dt = static_cast<const CMessageUpdate_MotionUnit&> (msg).turnLength;
Move(dt);
m_InMotionMessage = false;
}
break;
}
case MT_RenderSubmit:
{
PROFILE("UnitMotion::RenderSubmit");
@ -361,6 +324,18 @@ public:
PathResult(msgData.ticket, msgData.path);
break;
}
case MT_Create:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(GetEntityId(), m_FormationController);
break;
}
case MT_Destroy:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Unregister(GetEntityId());
break;
}
case MT_ValueModification:
{
const CMessageValueModification& msgData = static_cast<const CMessageValueModification&> (msg);
@ -369,20 +344,15 @@ public:
FALLTHROUGH;
}
case MT_OwnershipChanged:
{
OnValueModification();
break;
}
case MT_Deserialized:
{
CmpPtr<ICmpValueModificationManager> cmpValueModificationManager(GetSystemEntity());
if (!cmpValueModificationManager)
break;
m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId());
m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId());
// For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier.
// For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed
// (in case then new m_RunMultiplier value is lower than the old).
SetSpeedMultiplier(m_SpeedMultiplier);
OnValueModification();
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Register(GetEntityId(), m_FormationController);
break;
}
}
@ -651,17 +621,33 @@ private:
*/
void PathResult(u32 ticket, const WaypointPath& path);
void OnValueModification()
{
CmpPtr<ICmpValueModificationManager> cmpValueModificationManager(GetSystemEntity());
if (!cmpValueModificationManager)
return;
m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId());
m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId());
// For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier.
// For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed
// (in case then new m_RunMultiplier value is lower than the old).
SetSpeedMultiplier(m_SpeedMultiplier);
}
/**
* Check if we are at destination early in the turn, this both lets units react faster
* and ensure that distance comparisons are done while units are not being moved
* (otherwise they won't be commutative).
*/
void TurnStart();
virtual void OnTurnStart();
/**
* Do the per-turn movement and other updates.
*/
void Move(fixed dt);
virtual void PreMove(ICmpUnitMotionManager::MotionState& state);
virtual void Move(ICmpUnitMotionManager::MotionState& state, fixed dt);
virtual void PostMove(ICmpUnitMotionManager::MotionState& state, fixed dt);
/**
* Returns true if we are possibly at our destination.
@ -908,7 +894,7 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
}
}
void CCmpUnitMotion::TurnStart()
void CCmpUnitMotion::OnTurnStart()
{
if (PossiblyAtDestination())
MoveSucceeded();
@ -926,64 +912,57 @@ void CCmpUnitMotion::TurnStart()
}
}
void CCmpUnitMotion::Move(fixed dt)
void CCmpUnitMotion::PreMove(ICmpUnitMotionManager::MotionState& state)
{
// If we were idle and will still be, no need for an update.
state.needUpdate = m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE;
}
void CCmpUnitMotion::Move(ICmpUnitMotionManager::MotionState& state, fixed dt)
{
PROFILE("Move");
// If we were idle and will still be, we can return.
// TODO: this will need to be removed if pushing is implemented.
if (m_CurSpeed == fixed::Zero() && m_MoveRequest.m_Type == MoveRequest::NONE)
return;
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CFixedVector2D initialPos = cmpPosition->GetPosition2D();
entity_angle_t initialAngle = cmpPosition->GetRotation().Y;
// Keep track of the current unit's position and rotation during the update.
CFixedVector2D pos = initialPos;
entity_angle_t angle = initialAngle;
// 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
// to it, then throw away our current path and go straight to it
bool wentStraight = TryGoingStraightToTarget(initialPos);
// to it, then throw away our current path and go straight to it.
state.wentStraight = TryGoingStraightToTarget(state.initialPos);
bool wasObstructed = PerformMove(dt, cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, pos, angle);
state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.angle);
}
void CCmpUnitMotion::PostMove(ICmpUnitMotionManager::MotionState& state, fixed dt)
{
// Update our speed over this turn so that the visual actor shows the correct animation.
if (pos == initialPos)
if (state.pos == state.initialPos)
{
if (angle != initialAngle)
cmpPosition->TurnTo(angle);
if (state.angle != state.initialAngle)
state.cmpPosition->TurnTo(state.angle);
UpdateMovementState(fixed::Zero());
}
else
{
// Update the Position component after our movement (if we actually moved anywhere)
// When moving always set the angle in the direction of the movement.
CFixedVector2D offset = pos - initialPos;
angle = atan2_approx(offset.X, offset.Y);
cmpPosition->MoveAndTurnTo(pos.X, pos.Y, angle);
CFixedVector2D offset = state.pos - state.initialPos;
state.angle = atan2_approx(offset.X, offset.Y);
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
// Calculate the mean speed over this past turn.
UpdateMovementState(offset.Length() / dt);
}
if (wasObstructed && HandleObstructedMove(pos != initialPos))
if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
return;
else if (!wasObstructed && pos != initialPos)
else if (!state.wasObstructed && state.pos != state.initialPos)
m_FailedMovements = 0;
// We may need to recompute our path sometimes (e.g. if our target moves).
// Since we request paths asynchronously anyways, this does not need to be done before moving.
if (!wentStraight && PathingUpdateNeeded(pos))
if (!state.wentStraight && PathingUpdateNeeded(state.pos))
{
PathGoal goal;
if (ComputeGoal(goal, m_MoveRequest))
ComputePathToGoal(pos, goal);
ComputePathToGoal(state.pos, goal);
}
else if (m_FollowKnownImperfectPathCountdown > 0)
--m_FollowKnownImperfectPathCountdown;
@ -1276,7 +1255,8 @@ bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveReques
// If our entity ID is higher, the target has already moved, so we can just use the position directly.
// TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
CmpPtr<ICmpUnitMotion> cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && m_InMotionMessage;
CmpPtr<ICmpUnitMotionManager> cmpUnitMotionManager(GetSystemEntity());
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
if (needInterpolation && GetEntityId() < moveRequest.m_Entity)
{
// Add predicted movement.

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 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
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ICmpUnitMotionManager.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpUnitMotion.h"
#include "simulation2/system/EntityMap.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
class CCmpUnitMotionManager : public ICmpUnitMotionManager
{
protected:
EntityMap<MotionState> m_Units;
EntityMap<MotionState> m_FormationControllers;
// Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
std::vector<EntityMap<MotionState>::iterator> m_MovingUnits;
bool m_ComputingMotion;
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_MovingUnits.reserve(40);
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& UNUSED(serialize))
{
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
{
Init(paramNode);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_TurnStart:
{
OnTurnStart();
break;
}
case MT_Update_MotionFormation:
{
fixed dt = static_cast<const CMessageUpdate_MotionFormation&> (msg).turnLength;
m_ComputingMotion = true;
MoveFormations(dt);
m_ComputingMotion = false;
break;
}
case MT_Update_MotionUnit:
{
fixed dt = static_cast<const CMessageUpdate_MotionUnit&> (msg).turnLength;
m_ComputingMotion = true;
MoveUnits(dt);
m_ComputingMotion = false;
break;
}
}
}
virtual void Register(entity_id_t ent, bool formationController);
virtual void Unregister(entity_id_t ent);
virtual bool ComputingMotion() const
{
return m_ComputingMotion;
}
void OnTurnStart();
void MoveUnits(fixed dt);
void MoveFormations(fixed dt);
void Move(EntityMap<MotionState>& ents, fixed dt);
};
void CCmpUnitMotionManager::Register(entity_id_t ent, bool formationController)
{
MotionState state = {
CmpPtr<ICmpPosition>(GetSimContext(), ent),
CmpPtr<ICmpUnitMotion>(GetSimContext(), ent),
CFixedVector2D(),
CFixedVector2D(),
fixed::Zero(),
fixed::Zero(),
false,
false
};
if (!formationController)
m_Units.insert(ent, state);
else
m_FormationControllers.insert(ent, state);
}
void CCmpUnitMotionManager::Unregister(entity_id_t ent)
{
EntityMap<MotionState>::iterator it = m_Units.find(ent);
if (it != m_Units.end())
{
m_Units.erase(it);
return;
}
it = m_FormationControllers.find(ent);
if (it != m_FormationControllers.end())
m_FormationControllers.erase(it);
}
void CCmpUnitMotionManager::OnTurnStart()
{
for (EntityMap<MotionState>::value_type& data : m_FormationControllers)
data.second.cmpUnitMotion->OnTurnStart();
for (EntityMap<MotionState>::value_type& data : m_Units)
data.second.cmpUnitMotion->OnTurnStart();
}
void CCmpUnitMotionManager::MoveUnits(fixed dt)
{
Move(m_Units, dt);
}
void CCmpUnitMotionManager::MoveFormations(fixed dt)
{
Move(m_FormationControllers, dt);
}
void CCmpUnitMotionManager::Move(EntityMap<MotionState>& ents, fixed dt)
{
m_MovingUnits.clear();
for (EntityMap<MotionState>::iterator it = ents.begin(); it != ents.end(); ++it)
{
it->second.cmpUnitMotion->PreMove(it->second);
if (!it->second.needUpdate)
continue;
m_MovingUnits.push_back(it);
it->second.initialPos = it->second.cmpPosition->GetPosition2D();
it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
it->second.pos = it->second.initialPos;
it->second.angle = it->second.initialAngle;
}
for (EntityMap<MotionState>::iterator& it : m_MovingUnits)
it->second.cmpUnitMotion->Move(it->second, dt);
for (EntityMap<MotionState>::iterator& it : m_MovingUnits)
it->second.cmpUnitMotion->PostMove(it->second, dt);
}
REGISTER_COMPONENT_TYPE(UnitMotionManager)

View File

@ -48,6 +48,14 @@ class CCmpUnitMotionScripted : public ICmpUnitMotion
public:
DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted)
private:
virtual void OnTurnStart() {};
virtual void PreMove(ICmpUnitMotionManager::MotionState&) {};
virtual void Move(ICmpUnitMotionManager::MotionState&, fixed) {};
virtual void PostMove(ICmpUnitMotionManager::MotionState&, fixed) {};
public:
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call<bool>("MoveToPointRange", x, z, minRange, maxRange);

View File

@ -22,6 +22,9 @@
#include "simulation2/components/ICmpPathfinder.h" // for pass_class_t
#include "simulation2/components/ICmpPosition.h" // for entity_pos_t
#include "simulation2/components/ICmpUnitMotionManager.h"
class CCmpUnitMotionManager;
/**
* Motion interface for entities with complex movement capabilities.
@ -33,6 +36,17 @@
*/
class ICmpUnitMotion : public IComponent
{
friend class CCmpUnitMotionManager;
protected:
/**
* This external interface is used by the Unit Motion Manager.
* Components that do not register there do not need to implement these.
*/
virtual void OnTurnStart() = 0;
virtual void PreMove(ICmpUnitMotionManager::MotionState& state) = 0;
virtual void Move(ICmpUnitMotionManager::MotionState& state, fixed dt) = 0;
virtual void PostMove(ICmpUnitMotionManager::MotionState& state, fixed dt) = 0;
public:
/**

View File

@ -0,0 +1,25 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 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
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ICmpUnitMotionManager.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(UnitMotionManager)
END_INTERFACE_WRAPPER(UnitMotionManager)

View File

@ -0,0 +1,63 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 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
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_ICMPUNITMOTIONMANAGER
#define INCLUDED_ICMPUNITMOTIONMANAGER
#include "simulation2/system/Interface.h"
class ICmpPosition;
class ICmpUnitMotion;
class ICmpUnitMotionManager : public IComponent
{
public:
// Persisted state for each unit.
struct MotionState
{
// Component references - these must be kept alive for the duration of motion.
CmpPtr<ICmpPosition> cmpPosition;
CmpPtr<ICmpUnitMotion> cmpUnitMotion;
// Position before units start moving
CFixedVector2D initialPos;
// Transient position during the movement.
CFixedVector2D pos;
fixed initialAngle;
fixed angle;
// If true, the entity needs to be handled during movement.
bool needUpdate;
// 'Leak' from UnitMotion.
bool wentStraight;
bool wasObstructed;
};
virtual void Register(entity_id_t ent, bool formationController) = 0;
virtual void Unregister(entity_id_t ent) = 0;
/**
* True if entities are currently in the "Move" phase.
*/
virtual bool ComputingMotion() const = 0;
DECLARE_INTERFACE_TYPE(UnitMotionManager)
};
#endif // INCLUDED_ICMPUNITMOTIONMANAGER

View File

@ -677,6 +677,7 @@ void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool sk
AddComponent(m_SystemEntity, CID_SoundManager, noParam);
AddComponent(m_SystemEntity, CID_Terrain, noParam);
AddComponent(m_SystemEntity, CID_TerritoryManager, noParam);
AddComponent(m_SystemEntity, CID_UnitMotionManager, noParam);
AddComponent(m_SystemEntity, CID_UnitRenderer, noParam);
AddComponent(m_SystemEntity, CID_WaterManager, noParam);