Changeset 25182

Timestamp:
Apr 2, 2021, 6:30:59 PM (3 years ago)
Author:
wraitii
Message:

Add a simple 'pushing' logic to unit motion to improve movement.

This implements a form of crowd movement that I've generally called 'unit pushing' in the last few years.
Essentially, any two units will push each other away when they're too close. This makes it possible to ignore unit-unit obstructions, and thus makes movement much smoother in crowds.
This first iteration of this system only allows pushing between idle units and between moving units (i.e. a moving unit does not affect an idle one).
This is because the unitMotion logic to detect it is stuck & needs to use the pathfinders starts breaking: units can fail to move because they are pushed away from their intended movement, and the current logic fails to handle this gracefully.
Thankfully, the most value of this patch in terms of player experience is found in the improvements to group movements and shuttling.

Other impacts:

  • As the short pathfinder is called less often, we can increase the starting search range & reduce the # of max turns, both improving collision recovery.
  • The performance of idle units is slightly worsened, as they must be checked for idle-idle collisions. If needed a 'sleeping' system, as used in physics engine, could be implemented.
  • In general, however, expect slight performance improvements, as fewer short paths are computed.
  • Gathering efficiency should increase slightly, since shuttling times are likely reduced slightly.
  • As a sanity change to improve some edge cases (units that say they're moving, i.e. pushable, but don't actually move), the 'going straight' logic is turned off if a short path has been computed. This requires a few cascading changes to work correctly.

Technical notes:

  • To reduce the cost of the n2 comparisons that pushing requires, units are only compared within a small square on a grid which is lazily reconstructed each turn. The overhead seems rather small, and this is much simpler than keeping an up-to-date grid.
  • The design is intended to be parallelisable if needed someday.
  • The pathfinder's CheckMovement ignores moving units in UnitMotion, as that is now the spec. Idle units are not ignored, which is required for the 'collision' detection to work correctly (see above).

Refs #3442 (not fixed - idle units are not pushed by moving units).
Fixes #5084 (the overlap can still happen, but units will push each other away).

Differential Revision: https://code.wildfiregames.com/D1490

Location:
ps/trunk
Files:
2 added
15 edited

Legend:

Unmodified
Added
Removed
  • ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js

    r25089 r25182  
    304304        // (via CCmpTemplateManager). Now we need to remove that temporary
    305305        // blocker-disabling, so that we'll perform standard unit blocking instead.
    306         if (cmpObstruction && cmpObstruction.GetBlockMovementFlag())
     306        if (cmpObstruction)
    307307            cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
    308308
  • ps/trunk/binaries/data/mods/public/simulation/templates/special/formations/testudo.xml

    r23694 r25182  
    99    <FormationShape>square</FormationShape>
    1010    <UnitSeparationWidthMultiplier>0.50</UnitSeparationWidthMultiplier>
    11     <UnitSeparationDepthMultiplier>0.35</UnitSeparationDepthMultiplier>
     11    <UnitSeparationDepthMultiplier>0.</UnitSeparationDepthMultiplier>
    1212    <SortingOrder>fillFromTheSides</SortingOrder>
    1313    <WidthDepthRatio>0.8</WidthDepthRatio>
  • ps/trunk/source/simulation2/MessageTypes.h

    r24511 r25182  
    1 /* Copyright (C) 2019 Wildfire Games.
     1/* Copyright (C) 20 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    391391
    392392/**
     393
     394
     395
     396
     397
     398
     399
     400
     401
     402
     403
     404
     405
     406
    393407 * Sent when ObstructionManager's view of the shape of the world has changed
    394408 * (changing the TILE_OUTOFBOUNDS tiles returned by Rasterise).
  • ps/trunk/source/simulation2/TypeList.h

    r25071 r25182  
    5151MESSAGE(VisibilityChanged)
    5252MESSAGE(WaterChanged)
     53
    5354MESSAGE(ObstructionMapShapeChanged)
    5455MESSAGE(TerritoriesChanged)
  • ps/trunk/source/simulation2/components/CCmpObstruction.cpp

    r24894 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    410410            else
    411411                AddClusterShapes(pos.X, pos.Y, cmpPosition->GetRotation().Y);
     412
     413
     414
     415
     416
     417
     418
    412419        }
    413420        else if (!active && m_Active)
     
    428435                if (m_Type == CLUSTER)
    429436                    RemoveClusterShapes();
     437
     438
     439
     440
     441
     442
     443
    430444            }
    431445        }
     
    463477    }
    464478
    465     virtual bool GetBlockMovementFlag() const
    466     {
    467         return (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0;
     479    virtual bool GetBlockMovementFlag() const
     480    {
     481        return & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0;
    468482    }
    469483
     
    528542    virtual void SetUnitClearance(const entity_pos_t& clearance)
    529543    {
     544
     545
    530546        if (m_Type == UNIT)
    531547            m_Clearance = clearance;
  • ps/trunk/source/simulation2/components/CCmpUnitMotion.h

    r25150 r25182  
    5656 * NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic.
    5757 */
    58 static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2;
     58static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3);
    5959static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14);
    6060static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
    61 static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 2;
     61static const u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = ;
    6262
    6363/**
     
    101101 * TODO: when unit pushing is implemented, this number can probably be lowered.
    102102 */
    103 static const u8 MAX_FAILED_MOVEMENTS = 40;
     103static const u8 MAX_FAILED_MOVEMENTS = ;
    104104
    105105/**
     
    130130        componentManager.SubscribeToMessageType(MT_OwnershipChanged);
    131131        componentManager.SubscribeToMessageType(MT_ValueModification);
     132
    132133        componentManager.SubscribeToMessageType(MT_Deserialized);
    133134    }
     
    156157    bool m_FacePointAfterMove;
    157158
    158     // Number of turns since we last managed to move successfully.
     159    // Whether the unit participates in pushing.
     160    bool m_Pushing = true;
     161
     162    // Internal counter used when recovering from obstructed movement.
     163    // Most notably, increases the search range of the vertex pathfinder.
    159164    // See HandleObstructedMove() for more details.
    160165    u8 m_FailedMovements = 0;
     
    259264            CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
    260265            if (cmpObstruction)
     266
    261267                cmpObstruction->SetUnitClearance(m_Clearance);
     268
     269
     270
    262271        }
    263272
     
    292301
    293302        serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
     303
    294304
    295305        Serializer(serialize, "long path", m_LongPath.m_Waypoints);
     
    340350            if (!ENTITY_IS_LOCAL(GetEntityId()))
    341351                CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Unregister(GetEntityId());
     352
     353
     354
     355
     356
     357
     358
    342359            break;
    343360        }
     
    508525
    509526private:
    510     bool ShouldAvoidMovingUnits() const
    511     {
    512         return !m_FormationController;
    513     }
    514 
    515527    bool IsFormationMember() const
    516528    {
     
    722734    ControlGroupMovementObstructionFilter GetObstructionFilter() const
    723735    {
    724         return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup());
     736        return ControlGroupMovementObstructionFilter(, GetGroup());
    725737    }
    726738    /**
    727739     * Filter a specific tag on top of the existing control groups.
    728740     */
    729     SkipMovingTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
    730     {
    731         return SkipMovingTagAndControlGroupObstructionFilter(tag, GetGroup());
     741    SkipTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
     742    {
     743        return Skip, GetGroup());
    732744    }
    733745
     
    918930void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
    919931{
     932
     933
    920934    // If we were idle and will still be, no need for an update.
    921935    state.needUpdate = state.cmpPosition->IsInWorld() &&
    922936        (m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
     937
     938
     939
     940
     941
     942
     943
     944
     945
     946
     947
     948
    923949}
    924950
     
    947973    {
    948974        // Update the Position component after our movement (if we actually moved anywhere)
    949         // When moving always set the angle in the direction of the movement.
    950975        CFixedVector2D offset = state.pos - state.initialPos;
    951         state.angle = atan2_approx(offset.X, offset.Y);
     976        // When moving always set the angle in the direction of the movement,
     977        // if we are not trying to move, assume this is pushing-related movement,
     978        // and maintain the current angle instead.
     979        if (IsMoveRequested())
     980            state.angle = atan2_approx(offset.X, offset.Y);
    952981        state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
    953982
     
    960989    else if (!state.wasObstructed && state.pos != state.initialPos)
    961990        m_FailedMovements = 0;
     991
     992
     993
     994
    962995
    963996    // We may need to recompute our path sometimes (e.g. if our target moves).
     
    11331166void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed)
    11341167{
    1135     CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
    11361168    CmpPtr<ICmpVisual> cmpVisual(GetEntityHandle());
    1137     // Idle this turn.
    1138     if (speed == fixed::Zero())
    1139     {
    1140         // Update moving flag if we moved last turn.
    1141         if (m_CurSpeed > fixed::Zero() && cmpObstruction)
    1142             cmpObstruction->SetMovingFlag(false);
    1143         if (cmpVisual)
     1169    if (cmpVisual)
     1170    {
     1171        if (speed == fixed::Zero())
    11441172            cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
    1145     }
    1146     // Moved this turn
    1147     else
    1148     {
    1149         // Update moving flag if we didn't move last turn.
    1150         if (m_CurSpeed == fixed::Zero() && cmpObstruction)
    1151             cmpObstruction->SetMovingFlag(true);
    1152         if (cmpVisual)
     1173        else
    11531174            cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed);
    11541175    }
     
    12991320bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from)
    13001321{
     1322
     1323
     1324
     1325
     1326
     1327
    13011328    CFixedVector2D targetPos;
    13021329    if (!ComputeTargetPosition(targetPos))
     
    13331360    }
    13341361
     1362
    13351363    if (specificIgnore.valid())
    13361364    {
    1337         if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
     1365        if (!cmpPathfinder->CheckMovement(ObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
    13381366            return false;
    13391367    }
    13401368    else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
    13411369        return false;
    1342 
    13431370
    13441371    // That route is okay, so update our path
  • ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h

    r25125 r25182  
    2323
    2424#include "simulation2/MessageTypes.h"
     25
     26
    2527#include "simulation2/system/EntityMap.h"
    2628
     
    3234    static void ClassInit(CComponentManager& componentManager)
    3335    {
     36
    3437        componentManager.SubscribeToMessageType(MT_TurnStart);
    3538        componentManager.SubscribeToMessageType(MT_Update_Final);
     
    4346    struct MotionState
    4447    {
     48
     49
    4550        // Component references - these must be kept alive for the duration of motion.
    4651        // NB: this is generally not something one should do, but because of the tight coupling here it's doable.
     
    5358        CFixedVector2D pos;
    5459
     60
     61
     62
    5563        fixed initialAngle;
    5664        fixed angle;
    5765
     66
     67
     68
     69
     70
     71
     72
     73
    5874        // If true, the entity needs to be handled during movement.
    59         bool needUpdate;
     75        bool needUpdate;
    6076
    61         // 'Leak' from UnitMotion.
    62         bool wentStraight;
    63         bool wasObstructed;
     77        bool wentStraight = false;
     78        bool wasObstructed = false;
     79
     80        // Clone of the obstruction manager flag for efficiency
     81        bool isMoving = false;
    6482    };
    6583
     
    6785    EntityMap<MotionState> m_FormationControllers;
    6886
    69     // Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
    70     std::vector<EntityMap<MotionState>::iterator> m_MovingUnits;
     87    // T.
     88    > m_MovingUnits;
    7189
    7290    bool m_ComputingMotion;
     
    7997    virtual void Init(const CParamNode& UNUSED(paramNode))
    8098    {
    81         m_MovingUnits.reserve(40);
    8299    }
    83100
     
    93110    {
    94111        Init(paramNode);
     112
    95113    }
    96114
     
    99117        switch (msg.GetType())
    100118        {
     119
     120
     121
     122
     123
     124
     125
    101126            case MT_TurnStart:
    102127            {
     
    131156    }
    132157
     158
     159
    133160    void OnTurnStart();
    134161
     
    136163    void MoveFormations(fixed dt);
    137164    void Move(EntityMap<MotionState>& ents, fixed dt);
     165
     166
    138167};
     168
     169
     170
     171
     172
     173
     174
     175
     176
     177
    139178
    140179REGISTER_COMPONENT_TYPE(UnitMotionManager)
  • ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp

    r25126 r25182  
    2121#include "CCmpUnitMotionManager.h"
    2222
     23
    2324#include "ps/CLogger.h"
    2425#include "ps/Profile.h"
     26
     27
    2528
    2629// NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple.
     
    2932// To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here.
    3033
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
    3162void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
    3263{
    33     MotionState state = {
    34         CmpPtr<ICmpPosition>(GetSimContext(), ent),
    35         component,
    36         CFixedVector2D(),
    37         CFixedVector2D(),
    38         fixed::Zero(),
    39         fixed::Zero(),
    40         false,
    41         false
    42     };
     64    MotionState state(CmpPtr<ICmpPosition>(GetSimContext(), ent), component);
    4365    if (!formationController)
    4466        m_Units.insert(ent, state);
     
    81103void CCmpUnitMotionManager::Move(EntityMap<MotionState>& ents, fixed dt)
    82104{
    83     m_MovingUnits.clear();
     105    PROFILE2("MotionMgr_Move");
     106    std::unordered_set<std::vector<EntityMap<MotionState>::iterator>*> assigned;
    84107    for (EntityMap<MotionState>::iterator it = ents.begin(); it != ents.end(); ++it)
    85108    {
    86         it->second.cmpUnitMotion->PreMove(it->second);
    87         if (!it->second.needUpdate)
     109        if (!it->second.cmpPosition->IsInWorld())
     110        {
     111            it->second.needUpdate = false;
    88112            continue;
    89         m_MovingUnits.push_back(it);
     113        }
     114        else
     115            it->second.cmpUnitMotion->PreMove(it->second);
    90116        it->second.initialPos = it->second.cmpPosition->GetPosition2D();
    91117        it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
    92118        it->second.pos = it->second.initialPos;
    93119        it->second.angle = it->second.initialAngle;
    94     }
    95 
    96     for (EntityMap<MotionState>::iterator& it : m_MovingUnits)
    97     {
    98         it->second.cmpUnitMotion->Move(it->second, dt);
    99         it->second.cmpUnitMotion->PostMove(it->second, dt);
    100     }
    101 }
     120        ENSURE(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.width() &&
     121               it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.height());
     122        std::vector<EntityMap<MotionState>::iterator>& subdiv = m_MovingUnits.get(
     123            it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE,
     124            it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE
     125        );
     126        subdiv.emplace_back(it);
     127        assigned.emplace(&subdiv);
     128    }
     129
     130    for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
     131        for (EntityMap<MotionState>::iterator& it : *vec)
     132            if (it->second.needUpdate)
     133                it->second.cmpUnitMotion->Move(it->second, dt);
     134
     135    if (&ents == &m_Units)
     136    {
     137        PROFILE2("MotionMgr_Pushing");
     138        for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
     139        {
     140            ENSURE(!vec->empty());
     141
     142            std::vector<EntityMap<MotionState>::iterator>::iterator cit1 = vec->begin();
     143            do
     144            {
     145                if ((*cit1)->second.ignore)
     146                    continue;
     147                std::vector<EntityMap<MotionState>::iterator>::iterator cit2 = cit1;
     148                while(++cit2 != vec->end())
     149                    if (!(*cit2)->second.ignore)
     150                        Push(**cit1, **cit2, dt);
     151            }
     152            while(++cit1 != vec->end());
     153        }
     154    }
     155
     156    {
     157        PROFILE2("MotionMgr_PushAdjust");
     158        CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
     159        for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
     160        {
     161            for (EntityMap<MotionState>::iterator& it : *vec)
     162            {
     163
     164                if (!it->second.needUpdate || it->second.ignore)
     165                    continue;
     166
     167                // Prevent pushed units from crossing uncrossable boundaries
     168                // (we can assume that normal movement didn't push units into impassable terrain).
     169                if ((it->second.push.X != entity_pos_t::Zero() || it->second.push.Y != entity_pos_t::Zero()) &&
     170                    !cmpPathfinder->CheckMovement(it->second.cmpUnitMotion->GetObstructionFilter(),
     171                        it->second.pos.X, it->second.pos.Y,
     172                        it->second.pos.X + it->second.push.X, it->second.pos.Y + it->second.push.Y,
     173                        it->second.cmpUnitMotion->m_Clearance,
     174                        it->second.cmpUnitMotion->m_PassClass))
     175                {
     176                    // Mark them as obstructed - this could possibly be optimised
     177                    // perhaps it'd make more sense to mark the pushers as blocked.
     178                    it->second.wasObstructed = true;
     179                    it->second.wentStraight = false;
     180                    it->second.push = CFixedVector2D();
     181                }
     182                // Only apply pushing if the effect is significant enough.
     183                if (it->second.push.CompareLength(MINIMAL_PUSHING) > 0)
     184                {
     185                    // If there was an attempt at movement, and the pushed movement is in a sufficiently different direction
     186                    // (measured by an extremely arbitrary dot product)
     187                    // then mark the unit as obstructed still.
     188                    if (it->second.pos != it->second.initialPos &&
     189                        (it->second.pos - it->second.initialPos).Dot(it->second.pos + it->second.push - it->second.initialPos) < entity_pos_t::FromInt(1)/2)
     190                    {
     191                        it->second.wasObstructed = true;
     192                        it->second.wentStraight = false;
     193                        // Push anyways.
     194                    }
     195                    it->second.pos += it->second.push;
     196                }
     197                it->second.push = CFixedVector2D();
     198            }
     199        }
     200    }
     201    {
     202        PROFILE2("MotionMgr_PostMove");
     203        for (EntityMap<MotionState>::value_type& data : ents)
     204        {
     205            if (!data.second.needUpdate)
     206                continue;
     207            data.second.cmpUnitMotion->PostMove(data.second, dt);
     208        }
     209    }
     210    for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
     211        vec->clear();
     212}
     213
     214// TODO: ought to better simulate in-flight pushing, e.g. if units would cross in-between turns.
     215void CCmpUnitMotionManager::Push(EntityMap<MotionState>::value_type& a, EntityMap<MotionState>::value_type& b, fixed dt)
     216{
     217    // The hard problem for pushing is knowing when to actually use the pathfinder to go around unpushable obstacles.
     218    // For simplicitly, the current logic separates moving & stopped entities:
     219    // moving entities will push moving entities, but not stopped ones, and vice-versa.
     220    // this still delivers most of the value of pushing, without a lot of the complexity.
     221    int movingPush = a.second.isMoving + b.second.isMoving;
     222
     223    // Exception: units in the same control group (i.e. the same formation) never push farther than themselves
     224    // and are also allowed to push idle units (obstructions are ignored within formations,
     225    // so pushing idle units makes one member crossing the formation look better).
     226    if (a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup)
     227        movingPush = 0;
     228
     229    if (movingPush == 1)
     230        return;
     231
     232    // Treat the clearances as a circle - they're defined as squares, so we'll slightly overcompensate the diagonal
     233    // (they're also full-width instead of half, so we want to divide by two. sqrt(2)/2 is about 0.71 < 5/7).
     234    entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance) * 5 / 7;
     235    entity_pos_t maxDist = combinedClearance;
     236    if (movingPush)
     237        maxDist += PUSHING_MOVING_INFLUENCE_EXTENSION;
     238
     239    CFixedVector2D offset = a.second.pos - b.second.pos;
     240    if (offset.CompareLength(maxDist) > 0)
     241        return;
     242
     243    entity_pos_t offsetLength = offset.Length();
     244    // If the offset is small enough that precision would be problematic, pick an arbitrary vector instead.
     245    if (offsetLength <= entity_pos_t::Epsilon() * 10)
     246    {
     247        // Throw in some 'randomness' so that clumped units unclump more naturally.
     248        bool dir = a.first % 2;
     249        offset.X = entity_pos_t::FromInt(dir ? 1 : 0);
     250        offset.Y = entity_pos_t::FromInt(dir ? 0 : 1);
     251        offsetLength = entity_pos_t::FromInt(1);
     252    }
     253    else
     254    {
     255        offset.X = offset.X / offsetLength;
     256        offset.Y = offset.Y / offsetLength;
     257    }
     258
     259    // If the units are moving in opposite direction, check if they might have phased through each other.
     260    // If it looks like yes, move them perpendicularily so it looks like they avoid each other.
     261    // NB: this isn't very precise, nor will it catch 100% of intersections - it's meant as a cheap improvement.
     262    if (movingPush && (a.second.pos - a.second.initialPos).Dot(b.second.pos - b.second.initialPos) < entity_pos_t::Zero())
     263        // Perform some finer checking.
     264        if (Geometry::TestRayAASquare(a.second.initialPos - b.second.initialPos, a.second.pos - b.second.initialPos,
     265                                  CFixedVector2D(combinedClearance, combinedClearance))
     266            ||
     267            Geometry::TestRayAASquare(a.second.initialPos - b.second.pos, a.second.pos - b.second.pos,
     268                                      CFixedVector2D(combinedClearance, combinedClearance)))
     269        {
     270            offset = offset.Perpendicular();
     271            offsetLength = fixed::Zero();
     272        }
     273
     274
     275
     276    // The formula expects 'normal' pushing if the two entities edges are touching.
     277    entity_pos_t distanceFactor = movingPush ? (maxDist - offsetLength) / (maxDist - combinedClearance) : combinedClearance - offsetLength + entity_pos_t::FromInt(1);
     278    distanceFactor = Clamp(distanceFactor, entity_pos_t::Zero(), entity_pos_t::FromInt(2));
     279
     280    // Mark both as needing an update so they actually get moved.
     281    a.second.needUpdate = true;
     282    b.second.needUpdate = true;
     283
     284    CFixedVector2D pushingDir = offset.Multiply(distanceFactor);
     285
     286    // Divide by an arbitrary constant to avoid pushing too much.
     287    a.second.push += pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR);
     288    b.second.push -= pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR);
     289}
  • ps/trunk/source/simulation2/components/ICmpObstruction.cpp

    r24010 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    5656DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool)
    5757DEFINE_INTERFACE_METHOD_3("SetDisableBlockMovementPathfinding", void, ICmpObstruction, SetDisableBlockMovementPathfinding, bool, bool, int32_t)
    58 DEFINE_INTERFACE_METHOD_CONST_0("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag)
     58DEFINE_INTERFACE_METHOD_CONST_)
    5959DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t)
    6060DEFINE_INTERFACE_METHOD_CONST_0("GetControlGroup", entity_id_t, ICmpObstruction, GetControlGroup)
  • ps/trunk/source/simulation2/components/ICmpObstruction.h

    r24010 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    139139    virtual void SetDisableBlockMovementPathfinding(bool movementDisabled, bool pathfindingDisabled, int32_t shape) = 0;
    140140
    141     virtual bool GetBlockMovementFlag() const = 0;
     141    /**
     142     * @param templateOnly - whether to return the raw template value or the current value.
     143     */
     144    virtual bool GetBlockMovementFlag(bool templateOnly) const = 0;
    142145
    143146    /**
  • ps/trunk/source/simulation2/components/ICmpObstructionManager.h

    r24798 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    9393        FLAG_BLOCK_CONSTRUCTION       = (1 << 2), // prevents buildings being constructed on this shape
    9494        FLAG_BLOCK_PATHFINDING        = (1 << 3), // prevents the tile pathfinder choosing paths through this shape
    95         FLAG_MOVING                   = (1 << 4), // indicates this unit is currently moving
     95        FLAG_MOVING                   = (1 << 4), //
    9696        FLAG_DELETE_UPON_CONSTRUCTION = (1 << 5)  // this entity is deleted when construction of a building placed on top of this entity starts
    9797    };
     
    531531
    532532/**
    533  * Obstruction test filter that reject shapes in a given control group or with the given tag (if that tag is moving),
    534  * and rejects shapes that don't block unit movement. See D3482 for why this exists.
    535  */
    536 class SkipMovingTagAndControlGroupObstructionFilter : public IObstructionTestFilter
     533 * Similar to ControlGroupMovementObstructionFilter, but also ignoring a specific tag. See D3482 for why this exists.
     534 */
     535class SkipTagAndControlGroupObstructionFilter : public IObstructionTestFilter
    537536{
    538537    entity_id_t m_Group;
    539538    tag_t m_Tag;
    540 
    541 public:
    542     SkipMovingTagAndControlGroupObstructionFilter(tag_t tag, entity_id_t group) :
    543     m_Tag(tag), m_Group(group)
     539    bool m_AvoidMoving;
     540
     541public:
     542    SkipTagAndControlGroupObstructionFilter(tag_t tag, bool avoidMoving,  entity_id_t group) :
     543    m_Tag(tag), m_Group(group), m_AvoidMoving(avoidMoving)
    544544    {}
    545545
    546546    virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t group, entity_id_t group2) const
    547547    {
    548         if (tag.n == m_Tag.n && (flags & ICmpObstructionManager::FLAG_MOVING))
     548        if (tag.n == m_Tag.n)
    549549            return false;
    550550
    551551        if (group == m_Group || (group2 != INVALID_ENTITY && group2 == m_Group))
     552
     553
     554
    552555            return false;
    553556
  • ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h

    r24268 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    4848    virtual void SetMovingFlag(bool UNUSED(enabled)) { }
    4949    virtual void SetDisableBlockMovementPathfinding(bool UNUSED(movementDisabled), bool UNUSED(pathfindingDisabled), int32_t UNUSED(shape)) { }
    50     virtual bool GetBlockMovementFlag() const { return true; }
     50    virtual bool GetBlockMovementFlag() const { return true; }
    5151    virtual void SetControlGroup(entity_id_t UNUSED(group)) { }
    5252    virtual entity_id_t GetControlGroup() const { return INVALID_ENTITY; }
  • ps/trunk/source/simulation2/components/tests/test_RangeManager.h

    r24980 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    104104    virtual void SetMovingFlag(bool) {};
    105105    virtual void SetDisableBlockMovementPathfinding(bool, bool, int32_t) {};
    106     virtual bool GetBlockMovementFlag() const { return {}; };
     106    virtual bool GetBlockMovementFlag() const { return {}; };
    107107    virtual void SetControlGroup(entity_id_t) {};
    108108    virtual entity_id_t GetControlGroup() const { return {}; };
  • ps/trunk/source/simulation2/helpers/LongPathfinder.h

    r24352 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    261261    /**
    262262     * Given a path with an arbitrary collection of waypoints, updates the
    263      * waypoints to be nicer. Calls "Testline" between waypoints
    264      * so that bended paths can become straight if there's nothing in between
    265      * (this happens because A* is 8-direction, and the map isn't actually a grid).
     263     * waypoints to be nicer.
    266264     * If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist.
    267265     * In that case the distance between (x0, z0) and the first waypoint will also be made less than maxDist.
  • ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp

    r24511 r25182  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    357357////////////////////////////////
    358358
     359
     360
     361
     362
     363
     364
     365
     366
     367
     368
     369
     370
     371
    359372JS::Value CMessageObstructionMapShapeChanged::ToJSVal(const ScriptInterface& scriptInterface) const
    360373{
Note: See TracChangeset for help on using the changeset viewer.