Index: ps/trunk/source/simulation2/TypeList.h =================================================================== --- ps/trunk/source/simulation2/TypeList.h +++ ps/trunk/source/simulation2/TypeList.h @@ -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 @@ COMPONENT(UnitMotion) // must be after Obstruction COMPONENT(UnitMotionScripted) +INTERFACE(UnitMotionManager) +COMPONENT(UnitMotionManager) + INTERFACE(UnitRenderer) COMPONENT(UnitRenderer) Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp @@ -119,9 +119,8 @@ 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 @@ 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 @@ { switch (msg.GetType()) { - case MT_TurnStart: - { - TurnStart(); - break; - } - case MT_Update_MotionFormation: - { - if (m_FormationController) - { - m_InMotionMessage = true; - fixed dt = static_cast (msg).turnLength; - Move(dt); - m_InMotionMessage = false; - } - break; - } - case MT_Update_MotionUnit: - { - if (!m_FormationController) - { - m_InMotionMessage = true; - fixed dt = static_cast (msg).turnLength; - Move(dt); - m_InMotionMessage = false; - } - break; - } case MT_RenderSubmit: { PROFILE("UnitMotion::RenderSubmit"); @@ -361,6 +324,18 @@ PathResult(msgData.ticket, msgData.path); break; } + case MT_Create: + { + if (!ENTITY_IS_LOCAL(GetEntityId())) + CmpPtr(GetSystemEntity())->Register(GetEntityId(), m_FormationController); + break; + } + case MT_Destroy: + { + if (!ENTITY_IS_LOCAL(GetEntityId())) + CmpPtr(GetSystemEntity())->Unregister(GetEntityId()); + break; + } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); @@ -369,20 +344,15 @@ FALLTHROUGH; } case MT_OwnershipChanged: + { + OnValueModification(); + break; + } case MT_Deserialized: { - CmpPtr 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(GetSystemEntity())->Register(GetEntityId(), m_FormationController); break; } } @@ -651,17 +621,33 @@ */ void PathResult(u32 ticket, const WaypointPath& path); + void OnValueModification() + { + CmpPtr 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::TurnStart() +void CCmpUnitMotion::OnTurnStart() { if (PossiblyAtDestination()) MoveSucceeded(); @@ -926,64 +912,57 @@ } } -void CCmpUnitMotion::Move(fixed dt) +void CCmpUnitMotion::PreMove(ICmpUnitMotionManager::MotionState& state) { - 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 cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); - entity_angle_t initialAngle = cmpPosition->GetRotation().Y; + // 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; +} - // Keep track of the current unit's position and rotation during the update. - CFixedVector2D pos = initialPos; - entity_angle_t angle = initialAngle; +void CCmpUnitMotion::Move(ICmpUnitMotionManager::MotionState& state, fixed dt) +{ + PROFILE("Move"); // 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 @@ // 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 cmpUnitMotion(GetSimContext(), moveRequest.m_Entity); - bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && m_InMotionMessage; + CmpPtr cmpUnitMotionManager(GetSystemEntity()); + bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion(); if (needInterpolation && GetEntityId() < moveRequest.m_Entity) { // Add predicted movement. Index: ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp +++ ps/trunk/source/simulation2/components/CCmpUnitMotionManager.cpp @@ -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 . + */ + +#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 m_Units; + EntityMap m_FormationControllers; + + // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations). + std::vector::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 (msg).turnLength; + m_ComputingMotion = true; + MoveFormations(dt); + m_ComputingMotion = false; + break; + } + case MT_Update_MotionUnit: + { + fixed dt = static_cast (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& ents, fixed dt); +}; + +void CCmpUnitMotionManager::Register(entity_id_t ent, bool formationController) +{ + MotionState state = { + CmpPtr(GetSimContext(), ent), + CmpPtr(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::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::value_type& data : m_FormationControllers) + data.second.cmpUnitMotion->OnTurnStart(); + + for (EntityMap::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& ents, fixed dt) +{ + m_MovingUnits.clear(); + for (EntityMap::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::iterator& it : m_MovingUnits) + it->second.cmpUnitMotion->Move(it->second, dt); + + for (EntityMap::iterator& it : m_MovingUnits) + it->second.cmpUnitMotion->PostMove(it->second, dt); +} + + +REGISTER_COMPONENT_TYPE(UnitMotionManager) Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.h +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h @@ -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: /** Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp @@ -48,6 +48,14 @@ 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("MoveToPointRange", x, z, minRange, maxRange); Index: ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h +++ ps/trunk/source/simulation2/components/ICmpUnitMotionManager.h @@ -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 . + */ + +#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 cmpPosition; + CmpPtr 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 Index: ps/trunk/source/simulation2/components/ICmpUnitMotionManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotionManager.cpp +++ ps/trunk/source/simulation2/components/ICmpUnitMotionManager.cpp @@ -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 . + */ + +#include "precompiled.h" + +#include "ICmpUnitMotionManager.h" + +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(UnitMotionManager) +END_INTERFACE_WRAPPER(UnitMotionManager) Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp +++ ps/trunk/source/simulation2/system/ComponentManager.cpp @@ -677,6 +677,7 @@ 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);