Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 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 @@ -389,6 +389,20 @@ int newVisibility; }; +/** + * Sent when then obstruction of an entity has changed in a manner + * that changes 'block movement' properties. + */ +class CMessageMovementObstructionChanged : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(MovementObstructionChanged) + + CMessageMovementObstructionChanged() + { + } +}; + /** * Sent when ObstructionManager's view of the shape of the world has changed * (changing the TILE_OUTOFBOUNDS tiles returned by Rasterise). Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -50,6 +50,7 @@ MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) MESSAGE(WaterChanged) +MESSAGE(MovementObstructionChanged) MESSAGE(ObstructionMapShapeChanged) MESSAGE(TerritoriesChanged) MESSAGE(PathResult) Index: source/simulation2/components/CCmpObstruction.cpp =================================================================== --- source/simulation2/components/CCmpObstruction.cpp +++ source/simulation2/components/CCmpObstruction.cpp @@ -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 @@ -409,6 +409,13 @@ pos.X, pos.Y, m_Clearance, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup); else AddClusterShapes(pos.X, pos.Y, cmpPosition->GetRotation().Y); + + // Used by UnitMotion to activate/deactivate pushing + if (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) + { + CMessageMovementObstructionChanged msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } } else if (!active && m_Active) { @@ -428,6 +435,13 @@ if (m_Type == CLUSTER) RemoveClusterShapes(); } + + // Used by UnitMotion to activate/deactivate pushing + if (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) + { + CMessageMovementObstructionChanged msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } } // else we didn't change the active status } @@ -462,9 +476,9 @@ } } - virtual bool GetBlockMovementFlag() const + virtual bool GetBlockMovementFlag(bool templateOnly) const { - return (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0; + return m_Active && ((templateOnly ? m_TemplateFlags : m_Flags) & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0; } virtual EObstructionType GetObstructionType() const @@ -527,6 +541,8 @@ virtual void SetUnitClearance(const entity_pos_t& clearance) { + // This doesn't send a MovementObstructionChanged message + // because it's a just a workaround init order, and used in UnitMotion directly. if (m_Type == UNIT) m_Clearance = clearance; } Index: source/simulation2/components/CCmpUnitMotion.h =================================================================== --- source/simulation2/components/CCmpUnitMotion.h +++ source/simulation2/components/CCmpUnitMotion.h @@ -55,10 +55,10 @@ * smaller ranges might miss some legitimate routes around large obstacles.) * NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic. */ -static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2; +static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14); static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); -static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 2; +static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 1; /** * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint. @@ -129,6 +129,7 @@ componentManager.SubscribeToMessageType(MT_PathResult); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); + componentManager.SubscribeToMessageType(MT_MovementObstructionChanged); componentManager.SubscribeToMessageType(MT_Deserialized); } @@ -155,7 +156,11 @@ bool m_FacePointAfterMove; - // Number of turns since we last managed to move successfully. + // Whether the unit participates in pushing. + bool m_Pushing = true; + + // Internal counter used when recovering from obstructed movement. + // Most notably, increases the search range of the vertex pathfinder. // See HandleObstructedMove() for more details. u8 m_FailedMovements = 0; @@ -258,7 +263,11 @@ CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) + { cmpObstruction->SetUnitClearance(m_Clearance); + if (!cmpObstruction->GetBlockMovementFlag(true)) + m_Pushing = false; + } } m_DebugOverlayEnabled = false; @@ -291,6 +300,7 @@ serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + serialize.Bool("pushing", m_Pushing); Serializer(serialize, "long path", m_LongPath.m_Waypoints); Serializer(serialize, "short path", m_ShortPath.m_Waypoints); @@ -341,6 +351,13 @@ CmpPtr(GetSystemEntity())->Unregister(GetEntityId()); break; } + case MT_MovementObstructionChanged: + { + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + m_Pushing = cmpObstruction->GetBlockMovementFlag(false); + break; + } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); @@ -507,11 +524,6 @@ } private: - bool ShouldAvoidMovingUnits() const - { - return !m_FormationController; - } - bool IsFormationMember() const { // TODO: this really shouldn't be what we are checking for. @@ -721,14 +733,14 @@ */ ControlGroupMovementObstructionFilter GetObstructionFilter() const { - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup()); + return ControlGroupMovementObstructionFilter(false, GetGroup()); } /** * Filter a specific tag on top of the existing control groups. */ - SkipMovingTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const + SkipTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const { - return SkipMovingTagAndControlGroupObstructionFilter(tag, GetGroup()); + return SkipTagAndControlGroupObstructionFilter(tag, false, GetGroup()); } /** @@ -917,9 +929,23 @@ void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state) { + state.ignore = !m_Pushing; + // If we were idle and will still be, no need for an update. state.needUpdate = state.cmpPosition->IsInWorld() && (m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE); + + if (state.ignore) + return; + + state.controlGroup = IsFormationMember() ? m_MoveRequest.m_Entity : INVALID_ENTITY; + + // Update moving flag, this is an internal construct used for pushing, + // so it does not really reflect whether the unit is actually moving or not. + state.isMoving = m_MoveRequest.m_Type != MoveRequest::NONE; + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + cmpObstruction->SetMovingFlag(state.isMoving); } void CCmpUnitMotion::Move(CCmpUnitMotionManager::MotionState& state, fixed dt) @@ -946,9 +972,12 @@ 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 = state.pos - state.initialPos; - state.angle = atan2_approx(offset.X, offset.Y); + // 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); // Calculate the mean speed over this past turn. @@ -960,6 +989,10 @@ else if (!state.wasObstructed && state.pos != state.initialPos) m_FailedMovements = 0; + // If we moved straight, and didn't quite finish the path, reset - we'll update it next turn if still OK. + if (state.wentStraight && !state.wasObstructed) + m_ShortPath.m_Waypoints.clear(); + // 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 (!state.wentStraight && PathingUpdateNeeded(state.pos)) @@ -1132,24 +1165,12 @@ void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed) { - CmpPtr cmpObstruction(GetEntityHandle()); CmpPtr cmpVisual(GetEntityHandle()); - // Idle this turn. - if (speed == fixed::Zero()) + if (cmpVisual) { - // Update moving flag if we moved last turn. - if (m_CurSpeed > fixed::Zero() && cmpObstruction) - cmpObstruction->SetMovingFlag(false); - if (cmpVisual) + if (speed == fixed::Zero()) cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1)); - } - // Moved this turn - else - { - // Update moving flag if we didn't move last turn. - if (m_CurSpeed == fixed::Zero() && cmpObstruction) - cmpObstruction->SetMovingFlag(true); - if (cmpVisual) + else cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed); } @@ -1298,6 +1319,12 @@ bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from) { + // Assume if we have short paths we want to follow them. + // Exception: offset movement (formations) generally have very short deltas + // and do look good we need them to walk-straight most of the time. + if (!IsFormationMember() && !m_ShortPath.m_Waypoints.empty()) + return false; + CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; @@ -1332,15 +1359,15 @@ specificIgnore = cmpTargetObstruction->GetObstruction(); } + // Check movement against units - we want to use the short pathfinder to walk around those if needed. if (specificIgnore.valid()) { - if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; } else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; - // That route is okay, so update our path m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); Index: source/simulation2/components/CCmpUnitMotionManager.h =================================================================== --- source/simulation2/components/CCmpUnitMotionManager.h +++ source/simulation2/components/CCmpUnitMotionManager.h @@ -22,6 +22,8 @@ #include "ICmpUnitMotionManager.h" #include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpTerrain.h" +#include "simulation2/helpers/Grid.h" #include "simulation2/system/EntityMap.h" class CCmpUnitMotion; @@ -31,6 +33,7 @@ public: static void ClassInit(CComponentManager& componentManager) { + componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); @@ -42,6 +45,8 @@ // Persisted state for each unit. struct MotionState { + MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion); + // Component references - these must be kept alive for the duration of motion. // NB: this is generally not something one should do, but because of the tight coupling here it's doable. CmpPtr cmpPosition; @@ -52,22 +57,35 @@ // Transient position during the movement. CFixedVector2D pos; + // Accumulated "pushing" from nearby units. + CFixedVector2D push; + fixed initialAngle; fixed angle; + // Used for formations - units with the same control group won't push at a distance. + // (this is required because formations may be tight and large units may end up never settling. + entity_id_t controlGroup = INVALID_ENTITY; + + // Meta-flag -> this entity won't push nor be pushed. + // (used for entities that have their obstruction disabled). + bool ignore = false; + // If true, the entity needs to be handled during movement. - bool needUpdate; + bool needUpdate = false; - // 'Leak' from UnitMotion. - bool wentStraight; - bool wasObstructed; + bool wentStraight = false; + bool wasObstructed = false; + + // Clone of the obstruction manager flag for efficiency + bool isMoving = false; }; EntityMap m_Units; EntityMap m_FormationControllers; - // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations). - std::vector::iterator> m_MovingUnits; + // The vectors are cleared each frame. + Grid::iterator>> m_MovingUnits; bool m_ComputingMotion; @@ -78,7 +96,6 @@ virtual void Init(const CParamNode& UNUSED(paramNode)) { - m_MovingUnits.reserve(40); } virtual void Deinit() @@ -92,12 +109,20 @@ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); + ResetSubdivisions(); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { + case MT_TerrainChanged: + { + CmpPtr cmpTerrain(GetSystemEntity()); + if (cmpTerrain->GetVerticesPerSide() != m_MovingUnits.width()) + ResetSubdivisions(); + break; + } case MT_TurnStart: { OnTurnStart(); @@ -130,13 +155,27 @@ return m_ComputingMotion; } +private: + void ResetSubdivisions(); void OnTurnStart(); void MoveUnits(fixed dt); void MoveFormations(fixed dt); void Move(EntityMap& ents, fixed dt); + + void Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt); }; +void CCmpUnitMotionManager::ResetSubdivisions() +{ + CmpPtr cmpTerrain(GetSystemEntity()); + if (!cmpTerrain) + return; + + size_t size = cmpTerrain->GetVerticesPerSide() - 1; + m_MovingUnits.resize(size * TERRAIN_TILE_SIZE / 20 + 1, size * TERRAIN_TILE_SIZE / 20 + 1); +} + REGISTER_COMPONENT_TYPE(UnitMotionManager) #endif // INCLUDED_CCMPUNITMOTIONMANAGER Index: source/simulation2/components/CCmpUnitMotion_System.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion_System.cpp +++ source/simulation2/components/CCmpUnitMotion_System.cpp @@ -28,18 +28,14 @@ // but UnitMotion needs access to MotionState (defined in UnitMotionManager). // To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here. +CCmpUnitMotionManager::MotionState::MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion) + : cmpPosition(cmpPos), cmpUnitMotion(cmpMotion) +{ +} + void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController) { - MotionState state = { - CmpPtr(GetSimContext(), ent), - component, - CFixedVector2D(), - CFixedVector2D(), - fixed::Zero(), - fixed::Zero(), - false, - false - }; + MotionState state(CmpPtr(GetSimContext(), ent), component); if (!formationController) m_Units.insert(ent, state); else @@ -80,22 +76,185 @@ void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt) { - m_MovingUnits.clear(); + PROFILE2("MotionMgr_Move"); + std::set::iterator>*> assigned; for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it) { - it->second.cmpUnitMotion->PreMove(it->second); - if (!it->second.needUpdate) + if (!it->second.cmpPosition->IsInWorld()) + { + it->second.needUpdate = false; continue; - m_MovingUnits.push_back(it); + } + else + it->second.cmpUnitMotion->PreMove(it->second); 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; + ENSURE(it->second.pos.X.ToInt_RoundToZero() / 20 < m_MovingUnits.width() && it->second.pos.Y.ToInt_RoundToZero() / 20 < m_MovingUnits.height()); + std::vector::iterator>& subdiv = m_MovingUnits.get(it->second.pos.X.ToInt_RoundToZero() / 20, it->second.pos.Y.ToInt_RoundToZero() / 20); + subdiv.emplace_back(it); + assigned.emplace(&subdiv); } - for (EntityMap::iterator& it : m_MovingUnits) + for (std::vector::iterator>* vec : assigned) + for (EntityMap::iterator& it : *vec) + if (it->second.needUpdate) + it->second.cmpUnitMotion->Move(it->second, dt); + + if (&ents == &m_Units) + { + PROFILE2("MotionMgr_Pushing"); + for (std::vector::iterator>* vec : assigned) + { + ENSURE(!vec->empty()); + + std::vector::iterator>::iterator cit1 = vec->begin(); + do + { + if ((*cit1)->second.ignore) + continue; + std::vector::iterator>::iterator cit2 = cit1; + while(++cit2 != vec->end()) + if (!(*cit2)->second.ignore) + Push(**cit1, **cit2, dt); + } + while(++cit1 != vec->end()); + } + } + + { + PROFILE2("MotionMgr_PushAdjust"); + CmpPtr cmpPathfinder(GetSystemEntity()); + for (std::vector::iterator>* vec : assigned) + { + for (EntityMap::iterator& it : *vec) + { + + if (!it->second.needUpdate || it->second.ignore) + continue; + + // Prevent pushed units from crossing uncrossable boundaries + // (we can assume that normal movement didn't push units into impassable terrain). + if ((it->second.push.X != entity_pos_t::Zero() || it->second.push.Y != entity_pos_t::Zero()) && + !cmpPathfinder->CheckMovement(it->second.cmpUnitMotion->GetObstructionFilter(), + it->second.pos.X, it->second.pos.Y, + it->second.pos.X + it->second.push.X, it->second.pos.Y + it->second.push.Y, + it->second.cmpUnitMotion->m_Clearance, + it->second.cmpUnitMotion->m_PassClass)) + { + // Mark them as obstructed - this could possibly be optimised + // perhaps it'd make more sense to mark the pushers as blocked. + it->second.wasObstructed = true; + it->second.wentStraight = false; + it->second.push = CFixedVector2D(); + } + // Only apply pushing if the effect is significant enough. + if (it->second.push.CompareLength(entity_pos_t::FromInt(1)/10) > 0) + { + // If there was an attempt at movement, and the pushed movement is in a sufficiently different direction + // (measured by an extremely arbitrary dot product) + // then mark the unit as obstructed still. + if (it->second.pos != it->second.initialPos && + (it->second.pos - it->second.initialPos).Dot(it->second.pos + it->second.push - it->second.initialPos) < entity_pos_t::FromInt(1)/2) + { + it->second.wasObstructed = true; + it->second.wentStraight = false; + // Push anyways. + } + it->second.pos += it->second.push; + } + it->second.push = CFixedVector2D(); + } + } + } + { + PROFILE2("MotionMgr_PostMove"); + for (EntityMap::value_type& data : ents) + { + if (!data.second.needUpdate) + continue; + data.second.cmpUnitMotion->PostMove(data.second, dt); + } + } + for (std::vector::iterator>* vec : assigned) + vec->clear(); +} + +// TODO: ought to better simulate in-flight pushing, e.g. if units would cross in-between turns. +void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt) +{ + // The hard problem for pushing is knowing when to actually use the pathfinder to go around unpushable obstacles. + // For simplicitly, the current logic separates moving & stopped entities: + // moving entities will push moving entities, but not stopped ones, and vice-versa. + // this still delivers most of the value of pushing, without a lot of the complexity. + int movingPush = a.second.isMoving + b.second.isMoving; + + // Exception: units in the same control group (i.e. the same formation) never push farther than themselves + // and are also allowed to push idle units (obstructions are ignored within formations, + // so pushing idle units makes one member crossing the formation look better). + if (a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup) + movingPush = 0; + + if (movingPush == 1) + return; + + // Treat the clearances as a circle - they're defined as squares, so we'll slightly overcompensate the diagonal + // (they're also full-width instead of half, so we want to divide by two. sqrt(2)/2 is about 0.71 < 5/7). + entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance) * 5 / 7; + entity_pos_t maxDist = combinedClearance; + if (movingPush) + maxDist += entity_pos_t::FromInt(1); + + + CFixedVector2D offset = a.second.pos - b.second.pos; + if (offset.CompareLength(maxDist) > 0) + return; + + entity_pos_t offsetLength = offset.Length(); + // If the offset is small enough that precision would be problematic, pick an arbitrary vector instead. + if (offsetLength <= entity_pos_t::Epsilon() * 10) + { + // Throw in some 'randomness' so that clumped units unclump more naturally. + bool dir = a.first % 2; + offset.X = entity_pos_t::FromInt(dir ? 1 : 0); + offset.Y = entity_pos_t::FromInt(dir ? 0 : 1); + offsetLength = entity_pos_t::FromInt(1); + } + else { - it->second.cmpUnitMotion->Move(it->second, dt); - it->second.cmpUnitMotion->PostMove(it->second, dt); + offset.X = offset.X / offsetLength; + offset.Y = offset.Y / offsetLength; } + + // If the units are moving in opposite direction, check if they might have phased through each other. + // If it looks like yes, move them perpendicularily so it looks like they avoid each other. + // NB: this isn't very precise, nor will it catch 100% of intersections - it's meant as a cheap improvement. + if (movingPush && (a.second.pos - a.second.initialPos).Dot(b.second.pos - b.second.initialPos) < entity_pos_t::Zero()) + // Perform some finer checking. + if (Geometry::TestRayAASquare(a.second.initialPos - b.second.initialPos, a.second.pos - b.second.initialPos, + CFixedVector2D(combinedClearance, combinedClearance)) + || + Geometry::TestRayAASquare(a.second.initialPos - b.second.pos, a.second.pos - b.second.pos, + CFixedVector2D(combinedClearance, combinedClearance))) + { + offset = offset.Perpendicular(); + offsetLength = fixed::Zero(); + } + + + + // The formula expects 'normal' pushing if the two entities edges are touching. + entity_pos_t distanceFactor = movingPush ? (maxDist - offsetLength) / (maxDist - combinedClearance) : combinedClearance - offsetLength + entity_pos_t::FromInt(1); + distanceFactor = std::clamp(distanceFactor, entity_pos_t::Zero(), entity_pos_t::FromInt(2)); + + // Mark both as needing an update so they actually get moved. + a.second.needUpdate = true; + b.second.needUpdate = true; + + CFixedVector2D pushingDir = offset.Multiply(distanceFactor); + + // Divide by an arbitrary constant to avoid pushing too much. + a.second.push += pushingDir.Multiply(movingPush ? dt : dt / 2); + b.second.push -= pushingDir.Multiply(movingPush ? dt : dt / 2); } Index: source/simulation2/components/ICmpObstruction.h =================================================================== --- source/simulation2/components/ICmpObstruction.h +++ source/simulation2/components/ICmpObstruction.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 @@ -138,7 +138,10 @@ virtual void SetDisableBlockMovementPathfinding(bool movementDisabled, bool pathfindingDisabled, int32_t shape) = 0; - virtual bool GetBlockMovementFlag() const = 0; + /** + * @param templateOnly - whether to return the raw template value or the current value. + */ + virtual bool GetBlockMovementFlag(bool templateOnly) const = 0; /** * Change the control group that the entity belongs to. Index: source/simulation2/components/ICmpObstruction.cpp =================================================================== --- source/simulation2/components/ICmpObstruction.cpp +++ source/simulation2/components/ICmpObstruction.cpp @@ -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 @@ -55,7 +55,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesDeletedUponConstruction", std::vector, ICmpObstruction, GetEntitiesDeletedUponConstruction) DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool) DEFINE_INTERFACE_METHOD_3("SetDisableBlockMovementPathfinding", void, ICmpObstruction, SetDisableBlockMovementPathfinding, bool, bool, int32_t) -DEFINE_INTERFACE_METHOD_CONST_0("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag) +DEFINE_INTERFACE_METHOD_CONST_1("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag, bool) DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t) DEFINE_INTERFACE_METHOD_CONST_0("GetControlGroup", entity_id_t, ICmpObstruction, GetControlGroup) DEFINE_INTERFACE_METHOD_1("SetControlGroup2", void, ICmpObstruction, SetControlGroup2, entity_id_t) Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.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 @@ -92,7 +92,7 @@ FLAG_BLOCK_FOUNDATION = (1 << 1), // prevents foundations being placed on this shape FLAG_BLOCK_CONSTRUCTION = (1 << 2), // prevents buildings being constructed on this shape FLAG_BLOCK_PATHFINDING = (1 << 3), // prevents the tile pathfinder choosing paths through this shape - FLAG_MOVING = (1 << 4), // indicates this unit is currently moving + FLAG_MOVING = (1 << 4), // reserved for unitMotion - indicates intention to move. FLAG_DELETE_UPON_CONSTRUCTION = (1 << 5) // this entity is deleted when construction of a building placed on top of this entity starts }; @@ -530,27 +530,30 @@ }; /** - * Obstruction test filter that reject shapes in a given control group or with the given tag (if that tag is moving), - * and rejects shapes that don't block unit movement. See D3482 for why this exists. + * Similar to ControlGroupMovementObstructionFilter, but also ignoring a specific tag. See D3482 for why this exists. */ -class SkipMovingTagAndControlGroupObstructionFilter : public IObstructionTestFilter +class SkipTagAndControlGroupObstructionFilter : public IObstructionTestFilter { entity_id_t m_Group; tag_t m_Tag; + bool m_AvoidMoving; public: - SkipMovingTagAndControlGroupObstructionFilter(tag_t tag, entity_id_t group) : - m_Tag(tag), m_Group(group) + SkipTagAndControlGroupObstructionFilter(tag_t tag, bool avoidMoving, entity_id_t group) : + m_Tag(tag), m_Group(group), m_AvoidMoving(avoidMoving) {} virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t group, entity_id_t group2) const { - if (tag.n == m_Tag.n && (flags & ICmpObstructionManager::FLAG_MOVING)) + if (tag.n == m_Tag.n) return false; if (group == m_Group || (group2 != INVALID_ENTITY && group2 == m_Group)) return false; + if ((flags & ICmpObstructionManager::FLAG_MOVING) && !m_AvoidMoving) + return false; + if (!(flags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT)) return false; Index: source/simulation2/components/tests/test_ObstructionManager.h =================================================================== --- source/simulation2/components/tests/test_ObstructionManager.h +++ source/simulation2/components/tests/test_ObstructionManager.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 @@ -47,7 +47,7 @@ virtual void SetActive(bool UNUSED(active)) { } virtual void SetMovingFlag(bool UNUSED(enabled)) { } virtual void SetDisableBlockMovementPathfinding(bool UNUSED(movementDisabled), bool UNUSED(pathfindingDisabled), int32_t UNUSED(shape)) { } - virtual bool GetBlockMovementFlag() const { return true; } + virtual bool GetBlockMovementFlag(bool) const { return true; } virtual void SetControlGroup(entity_id_t UNUSED(group)) { } virtual entity_id_t GetControlGroup() const { return INVALID_ENTITY; } virtual void SetControlGroup2(entity_id_t UNUSED(group2)) { } Index: source/simulation2/components/tests/test_RangeManager.h =================================================================== --- source/simulation2/components/tests/test_RangeManager.h +++ source/simulation2/components/tests/test_RangeManager.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 @@ -103,7 +103,7 @@ virtual void SetActive(bool) {}; virtual void SetMovingFlag(bool) {}; virtual void SetDisableBlockMovementPathfinding(bool, bool, int32_t) {}; - virtual bool GetBlockMovementFlag() const { return {}; }; + virtual bool GetBlockMovementFlag(bool) const { return {}; }; virtual void SetControlGroup(entity_id_t) {}; virtual entity_id_t GetControlGroup() const { return {}; }; virtual void SetControlGroup2(entity_id_t) {}; Index: source/simulation2/helpers/LongPathfinder.h =================================================================== --- source/simulation2/helpers/LongPathfinder.h +++ source/simulation2/helpers/LongPathfinder.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 @@ -260,9 +260,7 @@ /** * Given a path with an arbitrary collection of waypoints, updates the - * waypoints to be nicer. Calls "Testline" 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). + * waypoints to be nicer. * If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist. * In that case the distance between (x0, z0) and the first waypoint will also be made less than maxDist. */ Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -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 @@ -356,6 +356,19 @@ //////////////////////////////// +JS::Value CMessageMovementObstructionChanged::ToJSVal(const ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageMovementObstructionChanged::FromJSVal(const ScriptInterface& UNUSED(scriptInterface), JS::HandleValue UNUSED(val)) +{ + return new CMessageMovementObstructionChanged(); +} + +//////////////////////////////// + JS::Value CMessageObstructionMapShapeChanged::ToJSVal(const ScriptInterface& scriptInterface) const { TOJSVAL_SETUP();