1
0
forked from 0ad/0ad

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 n^2 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
This was SVN commit r25182.
This commit is contained in:
wraitii 2021-04-02 16:30:59 +00:00
parent 8ed1bc2fd1
commit 592453c62f
17 changed files with 768 additions and 83 deletions

View File

@ -0,0 +1,380 @@
const REG_UNIT_TEMPLATE = "units/athen/infantry_spearman_b";
const FAST_UNIT_TEMPLATE = "units/athen/cavalry_swordsman_b";
const LARGE_UNIT_TEMPLATE = "units/brit/siege_ram";
const ELE_TEMPLATE = "units/cart/champion_elephant";
const ATTACKER = 2;
var QuickSpawn = function(x, z, template, owner = 1)
{
let ent = Engine.AddEntity(template);
let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpEntOwnership)
cmpEntOwnership.SetOwner(owner);
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
cmpEntPosition.JumpTo(x, z);
return ent;
};
var Rotate = function(angle, ent)
{
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
cmpEntPosition.SetYRotation(angle);
return ent;
};
var WalkTo = function(x, z, queued, ent, owner=1)
{
ProcessCommand(owner, {
"type": "walk",
"entities": Array.isArray(ent) ? ent : [ent],
"x": x,
"z": z,
"queued": queued,
"force": true,
});
return ent;
};
var FormationWalkTo = function(x, z, queued, ent, owner=1)
{
ProcessCommand(owner, {
"type": "walk",
"entities": Array.isArray(ent) ? ent : [ent],
"x": x,
"z": z,
"queued": queued,
"force": true,
"formation": "special/formations/box"
});
return ent;
};
var Attack = function(target, ent)
{
let comm = {
"type": "attack",
"entities": Array.isArray(ent) ? ent : [ent],
"target": target,
"queued": true,
"force": true,
};
ProcessCommand(ATTACKER, comm);
return ent;
};
var Do = function(name, data, ent, owner = 1)
{
let comm = {
"type": name,
"entities": Array.isArray(ent) ? ent : [ent],
"queued": false
};
for (let k in data)
comm[k] = data[k];
ProcessCommand(owner, comm);
};
var experiments = {};
experiments.units_sparse_forest_of_units = {
"spawn": (gx, gy) => {
for (let i = -16; i <= 16; i += 8)
for (let j = -16; j <= 16; j += 8)
QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE);
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE));
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE));
}
};
experiments.units_dense_forest_of_units = {
"spawn": (gx, gy) => {
for (let i = -16; i <= 16; i += 4)
for (let j = -16; j <= 16; j += 4)
QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE);
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE));
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE));
}
};
experiments.units_superdense_forest_of_units = {
"spawn": (gx, gy) => {
for (let i = -6; i <= 6; i += 2)
for (let j = -6; j <= 6; j += 2)
QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE);
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE));
WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE));
}
};
experiments.building = {
"spawn": (gx, gy) => {
let target = QuickSpawn(gx + 20, gy + 20, "foundation|structures/athen/storehouse");
for (let i = 0; i < 8; ++i)
Do("repair", { "target": target }, QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE));
let cmpFoundation = Engine.QueryInterface(target, IID_Foundation);
cmpFoundation.InitialiseConstruction(1, "structures/athen/storehouse");
}
};
experiments.collecting_tree = {
"spawn": (gx, gy) => {
let target = QuickSpawn(gx + 10, gy + 10, "gaia/tree/acacia");
let storehouse = QuickSpawn(gx - 10, gy - 10, "structures/athen/storehouse");
for (let i = 0; i < 8; ++i)
Do("gather", { "target": target }, QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE));
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
// Make that tree essentially infinite.
cmpModifiersManager.AddModifiers("inf_tree", {
"ResourceSupply/Max": [{ "affects": ["Tree"], "replace": 50000 }],
}, target);
let cmpSupply = Engine.QueryInterface(target, IID_ResourceSupply);
cmpSupply.SetAmount(50000);
// Make the storehouse a territory root
cmpModifiersManager.AddModifiers("root", {
"TerritoryInfluence/Root": [{ "affects": ["Structure"], "replace": true }],
}, storehouse);
// Make units gather instantly
cmpModifiersManager.AddModifiers("gatherrate", {
"ResourceGatherer/BaseSpeed": [{ "affects": ["Unit"], "replace": 100 }],
}, 3); // Player 1 is ent 3
}
};
experiments.sep1 = {
"spawn": (gx, gy) => {}
};
experiments.battle = {
"spawn": (gx, gy) => {
for (let i = 0; i < 4; ++i)
for (let j = 0; j < 8; ++j)
{
QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE);
QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE, ATTACKER);
}
}
};
experiments.sep2 = {
"spawn": (gx, gy) => {}
};
experiments.overlapping = {
"spawn": (gx, gy) => {
for (let i = 0; i < 20; ++i)
QuickSpawn(gx, gy, REG_UNIT_TEMPLATE);
}
};
experiments.multicrossing = {
"spawn": (gx, gy) => {
for (let i = 0; i < 20; i += 2)
for (let j = 0; j < 20; j += 2)
WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE));
for (let i = 0; i < 20; i += 2)
for (let j = 0; j < 20; j += 2)
WalkTo(gx+10, gy, false, QuickSpawn(gx + i, gy + j + 70, REG_UNIT_TEMPLATE));
}
};
experiments.elephant_formation = {
"spawn": (gx, gy) => {
let ents = [];
for (let i = 0; i < 20; i += 4)
for (let j = 0; j < 20; j += 4)
ents.push(QuickSpawn(gx + i, gy + j, ELE_TEMPLATE));
FormationWalkTo(gx, gy+10, false, ents);
}
};
var perf_experiments = {};
// Perf check: put units everywhere, not moving.
perf_experiments.Idle = {
"spawn": () => {
const spacing = 12;
for (let x = 0; x < 20*16*4 - 20; x += spacing)
for (let z = 0; z < 20*16*4 - 20; z += spacing)
QuickSpawn(x, z, REG_UNIT_TEMPLATE);
}
};
// Perf check: put units everywhere, moving.
perf_experiments.MovingAround = {
"spawn": () => {
const spacing = 24;
for (let x = 0; x < 20*16*4 - 20; x += spacing)
for (let z = 0; z < 20*16*4 - 20; z += spacing)
{
let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
for (let i = 0; i < 5; ++i)
{
WalkTo(x + 4, z, true, ent);
WalkTo(x + 4, z + 4, true, ent);
WalkTo(x, z + 4, true, ent);
WalkTo(x, z, true, ent);
}
}
}
};
// Perf check: fewer units moving more.
perf_experiments.LighterMovingAround = {
"spawn": () => {
const spacing = 48;
for (let x = 0; x < 20*16*4 - 20; x += spacing)
for (let z = 0; z < 20*16*4 - 20; z += spacing)
{
let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
for (let i = 0; i < 5; ++i)
{
WalkTo(x + 20, z, true, ent);
WalkTo(x + 20, z + 20, true, ent);
WalkTo(x, z + 20, true, ent);
WalkTo(x, z, true, ent);
}
}
}
};
// Perf check: rows of units crossing each other.
perf_experiments.BunchaCollisions = {
"spawn": () => {
const spacing = 64;
for (let x = 0; x < 20*16*4 - 20; x += spacing)
for (let z = 0; z < 20*16*4 - 20; z += spacing)
{
for (let i = 0; i < 10; ++i)
{
// Add a little variation to the spawning, or all clusters end up identical.
let ent = QuickSpawn(x + i + randFloat(-0.5, 0.5), z + 20 * (i%2) + randFloat(-0.5, 0.5), REG_UNIT_TEMPLATE);
for (let ii = 0; ii < 5; ++ii)
{
WalkTo(x + i, z + 20, true, ent);
WalkTo(x + i, z, true, ent);
}
}
}
}
};
// Massive moshpit of pushing.
perf_experiments.LotsaLocalCollisions = {
"spawn": () => {
const spacing = 3;
for (let x = 100; x < 200; x += spacing)
for (let z = 100; z < 200; z += spacing)
{
let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE);
for (let ii = 0; ii < 20; ++ii)
WalkTo(randFloat(100, 200), randFloat(100, 200), true, ent);
}
}
};
var woodcutting = (gx, gy) => {
let dropsite = QuickSpawn(gx + 50, gy, "structures/athen/storehouse");
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.AddModifiers("root", {
"TerritoryInfluence/Root": [{ "affects": ["Structure"], "replace": true }],
}, dropsite);
// Make units gather faster
cmpModifiersManager.AddModifiers("gatherrate", {
"ResourceGatherer/BaseSpeed": [{ "affects": ["Unit"], "multiply": 3 }],
}, 3); // Player 1 is ent 3
for (let i = 20; i <= 80; i += 10)
for (let j = 20; j <= 50; j += 10)
QuickSpawn(gx + i, gy + j, "gaia/tree/acacia");
for (let i = 10; i <= 90; i += 5)
Do("gather-near-position", { "x": gx + i, "z": gy + 10, "resourceType": { "generic": "wood", "specific": "tree" }, "resourceTemplate": "gaia/tree/acacia" },
QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE));
};
perf_experiments.WoodCutting = {
"spawn": () => {
for (let i = 0; i < 8; i++)
for (let j = 0; j < 8; j++)
{
woodcutting(20 + i*100, 20 + j*100);
}
}
};
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
Trigger.prototype.Setup = function()
{
let start = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// /*
let gx = 100;
let gy = 100;
for (let key in experiments)
{
experiments[key].spawn(gx, gy);
gx += 60;
if (gx > 20*16*4-20)
{
gx = 20;
gy += 100;
}
}
/**/
/*
let time = 0;
for (let key in perf_experiments)
{
cmpTrigger.DoAfterDelay(1000 + time * 10000, "RunExperiment", { "exp": key });
time++;
}
/**/
};
Trigger.prototype.Cleanup = function()
{
warn("cleanup");
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let ents = cmpRangeManager.GetEntitiesByPlayer(1).concat(cmpRangeManager.GetEntitiesByPlayer(2));
for (let ent of ents)
Engine.DestroyEntity(ent);
};
Trigger.prototype.RunExperiment = function(data)
{
warn("Start of " + data.exp);
perf_experiments[data.exp].spawn();
cmpTrigger.DoAfterDelay(9500, "Cleanup", {});
};
Trigger.prototype.EndGame = function()
{
Engine.QueryInterface(4, IID_Player).SetState("defeated", "trigger");
Engine.QueryInterface(3, IID_Player).SetState("won", "trigger");
};
/*
var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
// Reduce player 1 vision range (or patrolling units reacct)
cmpModifiersManager.AddModifiers("no_promotion", {
"Vision/Range": [{ "affects": ["Unit"], "replace": 5 }],
}, 3); // player 1 is ent 3
// Prevent promotions, messes up things.
cmpModifiersManager.AddModifiers("no_promotion_A", {
"Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
}, 3);
cmpModifiersManager.AddModifiers("no_promotion_B", {
"Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
}, 4); // player 2 is ent 4
*/
cmpTrigger.DoAfterDelay(4000, "Setup", {});
cmpTrigger.DoAfterDelay(300000, "EndGame", {});

Binary file not shown.

View File

@ -303,7 +303,7 @@ Foundation.prototype.Build = function(builderEnt, work)
// but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary
// blocker-disabling, so that we'll perform standard unit blocking instead.
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag())
if (cmpObstruction)
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
// Call the related trigger event

View File

@ -8,7 +8,7 @@
<FormationName>Testudo</FormationName>
<FormationShape>square</FormationShape>
<UnitSeparationWidthMultiplier>0.50</UnitSeparationWidthMultiplier>
<UnitSeparationDepthMultiplier>0.35</UnitSeparationDepthMultiplier>
<UnitSeparationDepthMultiplier>0.4</UnitSeparationDepthMultiplier>
<SortingOrder>fillFromTheSides</SortingOrder>
<WidthDepthRatio>0.8</WidthDepthRatio>
<MinColumns>8</MinColumns>

View File

@ -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 @@ public:
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).

View File

@ -50,6 +50,7 @@ MESSAGE(RangeUpdate)
MESSAGE(TerrainChanged)
MESSAGE(VisibilityChanged)
MESSAGE(WaterChanged)
MESSAGE(MovementObstructionChanged)
MESSAGE(ObstructionMapShapeChanged)
MESSAGE(TerritoriesChanged)
MESSAGE(PathResult)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -409,6 +409,13 @@ public:
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 @@ public:
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 @@ public:
}
}
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 @@ public:
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;
}

View File

@ -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.
@ -100,7 +100,7 @@ static const u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
* this could probably be lowered.
* TODO: when unit pushing is implemented, this number can probably be lowered.
*/
static const u8 MAX_FAILED_MOVEMENTS = 40;
static const u8 MAX_FAILED_MOVEMENTS = 35;
/**
* When computing paths but failing to move, we want to occasionally alternate pathfinder systems
@ -129,6 +129,7 @@ public:
componentManager.SubscribeToMessageType(MT_PathResult);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
componentManager.SubscribeToMessageType(MT_MovementObstructionChanged);
componentManager.SubscribeToMessageType(MT_Deserialized);
}
@ -155,7 +156,11 @@ public:
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 @@ public:
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
if (cmpObstruction)
{
cmpObstruction->SetUnitClearance(m_Clearance);
if (!cmpObstruction->GetBlockMovementFlag(true))
m_Pushing = false;
}
}
m_DebugOverlayEnabled = false;
@ -291,6 +300,7 @@ public:
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 @@ public:
CmpPtr<ICmpUnitMotionManager>(GetSystemEntity())->Unregister(GetEntityId());
break;
}
case MT_MovementObstructionChanged:
{
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
if (cmpObstruction)
m_Pushing = cmpObstruction->GetBlockMovementFlag(false);
break;
}
case MT_ValueModification:
{
const CMessageValueModification& msgData = static_cast<const CMessageValueModification&> (msg);
@ -507,11 +524,6 @@ public:
}
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 @@ private:
*/
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::OnTurnStart()
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<ICmpObstruction> cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->SetMovingFlag(state.isMoving);
}
void CCmpUnitMotion::Move(CCmpUnitMotionManager::MotionState& state, fixed dt)
@ -946,8 +972,11 @@ void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed d
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;
// 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);
@ -960,6 +989,10 @@ void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed d
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 @@ bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath&
void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed)
{
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
CmpPtr<ICmpVisual> cmpVisual(GetEntityHandle());
// Idle this turn.
if (cmpVisual)
{
if (speed == fixed::Zero())
{
// Update moving flag if we moved last turn.
if (m_CurSpeed > fixed::Zero() && cmpObstruction)
cmpObstruction->SetMovingFlag(false);
if (cmpVisual)
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)
cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed);
}
@ -1298,6 +1319,12 @@ bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveReques
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 to 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 @@ bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from)
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();

View File

@ -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 @@ class CCmpUnitMotionManager : public ICmpUnitMotionManager
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 @@ public:
// Persisted state for each unit.
struct MotionState
{
MotionState(CmpPtr<ICmpPosition> 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<ICmpPosition> cmpPosition;
@ -52,22 +57,35 @@ public:
// Transient position during the movement.
CFixedVector2D pos;
// Accumulated "pushing" from nearby units.
CFixedVector2D push;
fixed initialAngle;
fixed angle;
// If true, the entity needs to be handled during movement.
bool needUpdate;
// 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;
// 'Leak' from UnitMotion.
bool wentStraight;
bool wasObstructed;
// 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 = false;
bool wentStraight = false;
bool wasObstructed = false;
// Clone of the obstruction manager flag for efficiency
bool isMoving = false;
};
EntityMap<MotionState> m_Units;
EntityMap<MotionState> m_FormationControllers;
// Temporary vector, reconstructed each turn (stored here to avoid memory reallocations).
std::vector<EntityMap<MotionState>::iterator> m_MovingUnits;
// The vectors are cleared each frame.
Grid<std::vector<EntityMap<MotionState>::iterator>> m_MovingUnits;
bool m_ComputingMotion;
@ -78,7 +96,6 @@ public:
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_MovingUnits.reserve(40);
}
virtual void Deinit()
@ -92,12 +109,20 @@ public:
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<ICmpTerrain> cmpTerrain(GetSystemEntity());
if (cmpTerrain->GetVerticesPerSide() != m_MovingUnits.width())
ResetSubdivisions();
break;
}
case MT_TurnStart:
{
OnTurnStart();
@ -130,13 +155,27 @@ public:
return m_ComputingMotion;
}
private:
void ResetSubdivisions();
void OnTurnStart();
void MoveUnits(fixed dt);
void MoveFormations(fixed dt);
void Move(EntityMap<MotionState>& ents, fixed dt);
void Push(EntityMap<MotionState>::value_type& a, EntityMap<MotionState>::value_type& b, fixed dt);
};
void CCmpUnitMotionManager::ResetSubdivisions()
{
CmpPtr<ICmpTerrain> 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

View File

@ -20,26 +20,48 @@
#include "CCmpUnitMotion.h"
#include "CCmpUnitMotionManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include <unordered_set>
// NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple.
// In practice, UnitMotionManager functions need access to the full implementation of UnitMotion,
// but UnitMotion needs access to MotionState (defined in UnitMotionManager).
// To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here.
namespace {
/**
* Units push only within their own grid square. This is the size of each square (in arbitrary units).
* TODO: check other values.
*/
static const int PUSHING_GRID_SIZE = 20;
/**
* Pushing is ignored if the combined push force has lower magnitude than this.
*/
static const entity_pos_t MINIMAL_PUSHING = entity_pos_t::FromInt(1)/10;
/**
* When moving, units extert a pushing influence at a greater distance.
*/
static const entity_pos_t PUSHING_MOVING_INFLUENCE_EXTENSION = entity_pos_t::FromInt(1);
/**
* Arbitrary constant used to reduce pushing to levels that won't break physics for our turn length.
*/
static const int PUSHING_REDUCTION_FACTOR = 2;
}
CCmpUnitMotionManager::MotionState::MotionState(CmpPtr<ICmpPosition> cmpPos, CCmpUnitMotion* cmpMotion)
: cmpPosition(cmpPos), cmpUnitMotion(cmpMotion)
{
}
void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
{
MotionState state = {
CmpPtr<ICmpPosition>(GetSimContext(), ent),
component,
CFixedVector2D(),
CFixedVector2D(),
fixed::Zero(),
fixed::Zero(),
false,
false
};
MotionState state(CmpPtr<ICmpPosition>(GetSimContext(), ent), component);
if (!formationController)
m_Units.insert(ent, state);
else
@ -80,22 +102,188 @@ void CCmpUnitMotionManager::MoveFormations(fixed dt)
void CCmpUnitMotionManager::Move(EntityMap<MotionState>& ents, fixed dt)
{
m_MovingUnits.clear();
PROFILE2("MotionMgr_Move");
std::unordered_set<std::vector<EntityMap<MotionState>::iterator>*> assigned;
for (EntityMap<MotionState>::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() / PUSHING_GRID_SIZE < m_MovingUnits.width() &&
it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.height());
std::vector<EntityMap<MotionState>::iterator>& subdiv = m_MovingUnits.get(
it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE,
it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE
);
subdiv.emplace_back(it);
assigned.emplace(&subdiv);
}
for (EntityMap<MotionState>::iterator& it : m_MovingUnits)
{
for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
for (EntityMap<MotionState>::iterator& it : *vec)
if (it->second.needUpdate)
it->second.cmpUnitMotion->Move(it->second, dt);
it->second.cmpUnitMotion->PostMove(it->second, dt);
if (&ents == &m_Units)
{
PROFILE2("MotionMgr_Pushing");
for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
{
ENSURE(!vec->empty());
std::vector<EntityMap<MotionState>::iterator>::iterator cit1 = vec->begin();
do
{
if ((*cit1)->second.ignore)
continue;
std::vector<EntityMap<MotionState>::iterator>::iterator cit2 = cit1;
while(++cit2 != vec->end())
if (!(*cit2)->second.ignore)
Push(**cit1, **cit2, dt);
}
while(++cit1 != vec->end());
}
}
{
PROFILE2("MotionMgr_PushAdjust");
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
for (std::vector<EntityMap<MotionState>::iterator>* vec : assigned)
{
for (EntityMap<MotionState>::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(MINIMAL_PUSHING) > 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<MotionState>::value_type& data : ents)
{
if (!data.second.needUpdate)
continue;
data.second.cmpUnitMotion->PostMove(data.second, dt);
}
}
for (std::vector<EntityMap<MotionState>::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<MotionState>::value_type& a, EntityMap<MotionState>::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 += PUSHING_MOVING_INFLUENCE_EXTENSION;
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
{
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 = 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 / PUSHING_REDUCTION_FACTOR);
b.second.push -= pushingDir.Multiply(movingPush ? dt : dt / PUSHING_REDUCTION_FACTOR);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -55,7 +55,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesBlockingConstruction", std::vector<e
DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesDeletedUponConstruction", std::vector<entity_id_t>, 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)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -138,7 +138,10 @@ public:
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.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -92,7 +92,7 @@ public:
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 - see usage there.
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 @@ public:
};
/**
* 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;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -47,7 +47,7 @@ public:
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)) { }

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -103,7 +103,7 @@ public:
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) {};

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -260,9 +260,7 @@ private:
/**
* 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.
*/

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020 Wildfire Games.
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -356,6 +356,19 @@ CMessage* CMessageWaterChanged::FromJSVal(const ScriptInterface& UNUSED(scriptIn
////////////////////////////////
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();