Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -193,6 +193,9 @@ COMPONENT(UnitMotion) // must be after Obstruction COMPONENT(UnitMotionScripted) +INTERFACE(UnitMotionManager) +COMPONENT(UnitMotionManager) + INTERFACE(UnitRenderer) COMPONENT(UnitRenderer) Index: source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- source/simulation2/components/CCmpObstructionManager.cpp +++ source/simulation2/components/CCmpObstructionManager.cpp @@ -479,6 +479,7 @@ virtual bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const; virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false) const; + virtual bool TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const; virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out) const; virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out) const; @@ -905,6 +906,37 @@ return false; } +bool CCmpObstructionManager::TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const +{ + PROFILE("TestStaticLine"); + + // Check that both end points are within the world (which means the whole line must be) + if (!IsInWorld(x0, z0, r) || !IsInWorld(x1, z1, r)) + return true; + + CFixedVector2D posMin (std::min(x0, x1) - r, std::min(z0, z1) - r); + CFixedVector2D posMax (std::max(x0, x1) + r, std::max(z0, z1) + r); + + std::vector staticShapes; + m_StaticSubdivision.GetInRange(staticShapes, posMin, posMax); + for (const entity_id_t& shape : staticShapes) + { + std::map::const_iterator it = m_StaticShapes.find(shape); + ENSURE(it != m_StaticShapes.end()); + + if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2)) + continue; + + CFixedVector2D center(it->second.x, it->second.z); + CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r); + if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize)) + return true; + } + + return false; +} + + bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out) const Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -894,10 +894,8 @@ PROFILE2_IFSPIKE("Check Movement", 0.001); // Test against obstructions first. filter may discard pathfinding-blocking obstructions. - // Use more permissive version of TestLine to allow unit-unit collisions to overlap slightly. - // This makes movement smoother and more natural for units, overall. CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager || cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true)) + if (!cmpObstructionManager || cmpObstructionManager->TestStaticLine(filter, x0, z0, x1, z1, r)) return false; // Then test against the terrain grid. This should not be necessary Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -26,6 +26,7 @@ #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpUnitMotionManager.h" #include "simulation2/components/ICmpValueModificationManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" @@ -303,24 +304,6 @@ { switch (msg.GetType()) { - case MT_Update_MotionFormation: - { - if (m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } - break; - } - case MT_Update_MotionUnit: - { - if (!m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } - break; - } case MT_RenderSubmit: { PROFILE("UnitMotion::RenderSubmit"); @@ -339,9 +322,35 @@ const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"UnitMotion") break; - FALLTHROUGH; + + 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); + + break; } case MT_OwnershipChanged: + { + const CMessageOwnershipChanged& msgData = static_cast (msg); + + if (!ENTITY_IS_LOCAL(GetEntityId())) + { + CmpPtr cmpUnitMotionManager(GetSystemEntity()); + if (msgData.from == INVALID_PLAYER && msgData.to != INVALID_PLAYER) + cmpUnitMotionManager->Register(GetEntityId(), m_FormationController); + else if (msgData.to == INVALID_PLAYER && msgData.from != INVALID_PLAYER) + cmpUnitMotionManager->Unregister(GetEntityId()); + } + FALLTHROUGH; + } case MT_Deserialized: { CmpPtr cmpValueModificationManager(GetSystemEntity()); @@ -602,7 +611,9 @@ /** * Do the per-turn movement and other updates. */ - void Move(fixed dt); + void PreMove(MotionState& state, fixed dt); + void Move(MotionState& state, fixed dt); + void PostMove(MotionState& state, fixed dt); /** * Returns true if we are possibly at our destination. @@ -849,14 +860,9 @@ ComputePathToGoal(pos, goal); } -void CCmpUnitMotion::Move(fixed dt) +void CCmpUnitMotion::PreMove(MotionState& state, fixed UNUSED(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; + PROFILE("PreMove"); if (PossiblyAtDestination()) MoveSucceeded(); @@ -874,49 +880,57 @@ } CmpPtr cmpPosition(GetEntityHandle()); + state.cmpPosition = cmpPosition; if (!cmpPosition || !cmpPosition->IsInWorld()) return; - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; + state.initialPos = cmpPosition->GetPosition2D(); + state.pos = state.initialPos; // 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); + state.wentStraight = TryGoingStraightToTarget(state.initialPos); +} - bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos); +void CCmpUnitMotion::Move(MotionState& state, fixed dt) +{ + PROFILE("Move"); + state.wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, state.pos); +} +void CCmpUnitMotion::PostMove(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) UpdateMovementState(fixed::Zero()); else { // Update the Position component after our movement (if we actually moved anywhere) - CFixedVector2D offset = pos - initialPos; + CFixedVector2D offset = state.pos - state.initialPos; // Face towards the target entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); + + state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, angle); // Calculate the mean speed over this past turn. UpdateMovementState(offset.Length() / dt); } - if (wasObstructed && HandleObstructedMove()) + if (state.wasObstructed && HandleObstructedMove()) return; - else if (!wasObstructed) + else if (!state.wasObstructed) m_FailedPathComputations = 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; Index: source/simulation2/components/CCmpUnitMotionManager.cpp =================================================================== --- /dev/null +++ source/simulation2/components/CCmpUnitMotionManager.cpp @@ -0,0 +1,200 @@ +/* Copyright (C) 2020 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/helpers/Spatial.h" +#include "simulation2/system/EntityMap.h" + +class CCmpUnitMotionManager : public ICmpUnitMotionManager +{ +protected: + using MotionState = ICmpUnitMotion::MotionState; + EntityMap m_Units; + EntityMap m_FormationControllers; + +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_Update_MotionUnit); + componentManager.SubscribeToMessageType(MT_Update_MotionFormation); + } + + DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager) + + virtual void Init(const CParamNode& UNUSED(paramNode)) + { + } + + 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_Update_MotionFormation: + { + fixed dt = static_cast (msg).turnLength; + MoveFormations(dt); + break; + } + case MT_Update_MotionUnit: + { + fixed dt = static_cast (msg).turnLength; + MoveUnits(dt); + break; + } + } + } + + void Register(entity_id_t ent, bool formationController); + void Unregister(entity_id_t ent); + + void MoveUnits(fixed dt); + void MoveFormations(fixed dt); + + void Push(EntityMap::value_type& a, EntityMap::value_type& b); +}; + +void CCmpUnitMotionManager::Register(entity_id_t ent, bool formationController) +{ + MotionState state = { + CFixedVector2D(), + CFixedVector2D(), + CFixedVector2D(), + CmpPtr(GetSimContext(), ent), + CmpPtr(GetSimContext(), ent), + 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::MoveUnits(fixed dt) +{ + std::set drop; + for (EntityMap::value_type& data : m_Units) + { + // Check that cmpUnitMotion still exists + CmpPtr cmpUnitMotion(GetSimContext(), data.first); + if (!cmpUnitMotion) + drop.insert(data.first); + else + { + data.second.cmpUnitMotion = cmpUnitMotion; + data.second.cmpUnitMotion->PreMove(data.second, dt); + if (!data.second.cmpPosition || !data.second.cmpPosition->IsInWorld()) + drop.insert(data.first); + } + } + + for (EntityMap::value_type& data : m_Units) + data.second.cmpUnitMotion->Move(data.second, dt); + + for (EntityMap::value_type& data1 : m_Units) + for (EntityMap::value_type& data2 : m_Units) + if (data1.first < data2.first && drop.count(data1.first) == 0 && drop.count(data2.first) == 0) + Push(data1, data2); + + for (EntityMap::value_type& data : m_Units) + { + if (data.second.push.CompareLength(fixed::FromInt(2)) >= 0) + { + data.second.wasObstructed = true; + data.second.wentStraight = false; + data.second.pos = data.second.initialPos + (data.second.pos - data.second.initialPos) / 2 + data.second.push / 2; + } + else + { + data.second.pos += data.second.push; + } + data.second.push = CFixedVector2D(); + data.second.cmpUnitMotion->PostMove(data.second, dt); + } +} + +void CCmpUnitMotionManager::MoveFormations(fixed dt) +{ + for (EntityMap::value_type& data : m_FormationControllers) + { + // Check that cmpUnitMotion still exists + CmpPtr cmpUnitMotion(GetSimContext(), data.first); + if (cmpUnitMotion) + { + data.second.cmpUnitMotion = cmpUnitMotion; + data.second.cmpUnitMotion->PreMove(data.second, dt); + } + } + + for (EntityMap::value_type& data : m_FormationControllers) + data.second.cmpUnitMotion->Move(data.second, dt); + + for (EntityMap::value_type& data : m_FormationControllers) + data.second.cmpUnitMotion->PostMove(data.second, dt); +} + +void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMap::value_type& b) +{ + CFixedVector2D offset = a.second.pos - b.second.pos; + if (offset.CompareLength(fixed::FromInt(3)) > 0) + return; + CFixedVector2D initial_offset = a.second.initialPos - b.second.initialPos; + if (initial_offset.Dot(offset) < fixed::Zero()) + { + CFixedVector2D delta = offset.Perpendicular(); + a.second.push += delta; + b.second.push -= delta; + return; + } + offset = offset.Multiply(std::max(fixed::Zero(), fixed::FromInt(2) - offset.Length())); + a.second.push += offset; + b.second.push -= offset; +} + + +REGISTER_COMPONENT_TYPE(UnitMotionManager) Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.h @@ -241,6 +241,7 @@ */ virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) const = 0; + virtual bool TestStaticLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) const = 0; /** * Collision test a static square shape against the current set of shapes. * @param filter filter to restrict the shapes that are being tested against Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -23,6 +23,10 @@ #include "simulation2/components/ICmpPathfinder.h" // for pass_class_t #include "simulation2/components/ICmpPosition.h" // for entity_pos_t +class ICmpPosition; +class ICmpUnitMotion; +class ICmpUnitMotionManager; + /** * Motion interface for entities with complex movement capabilities. * (Simpler motion is handled by ICmpMotion instead.) @@ -33,6 +37,24 @@ */ class ICmpUnitMotion : public IComponent { + friend class CCmpUnitMotionManager; +protected: + // Persisted state for the motion manager, shared by the move functions. + struct MotionState + { + CFixedVector2D initialPos; + CFixedVector2D pos; + CFixedVector2D push; + CmpPtr cmpPosition; + CmpPtr cmpUnitMotion; + // This is 'leaky' from CCmpUnitMotion but it's mostly easier/faster to handle this way. + bool wentStraight; + bool wasObstructed; + }; + + virtual void PreMove(MotionState& state, fixed dt) = 0; + virtual void Move(MotionState& state, fixed dt) = 0; + virtual void PostMove(MotionState& state, fixed dt) = 0; public: /** Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -47,6 +47,21 @@ public: DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted) + virtual void PreMove(MotionState&, fixed dt) + { + m_Script.CallVoid("PreMove", dt); + } + + virtual void Move(MotionState&, fixed dt) + { + m_Script.CallVoid("Move", dt); + } + + virtual void PostMove(MotionState&, fixed dt) + { + m_Script.CallVoid("PostMove", dt); + } + 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: source/simulation2/components/ICmpUnitMotionManager.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpUnitMotionManager.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2020 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 ICmpUnitMotionManager : public IComponent +{ +public: + virtual void Register(entity_id_t ent, bool formationController) = 0; + virtual void Unregister(entity_id_t ent) = 0; + + DECLARE_INTERFACE_TYPE(UnitMotionManager) +}; + +#endif // INCLUDED_ICMPUNITMOTIONMANAGER Index: source/simulation2/components/ICmpUnitMotionManager.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpUnitMotionManager.cpp @@ -0,0 +1,25 @@ +/* Copyright (C) 2020 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: source/simulation2/helpers/LongPathfinder.h =================================================================== --- source/simulation2/helpers/LongPathfinder.h +++ source/simulation2/helpers/LongPathfinder.h @@ -258,7 +258,7 @@ /** * Given a path with an arbitrary collection of waypoints, updates the - * waypoints to be nicer. Calls "Testline" between waypoints + * waypoints to be nicer. Calls "CheckMovement" between waypoints * so that bended paths can become straight if there's nothing in between * (this happens because A* is 8-direction, and the map isn't actually a grid). * If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist. Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ source/simulation2/system/ComponentManager.cpp @@ -715,6 +715,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);