# New unit movement system, which does a far better job of approaching targets and avoiding obstacles.

Add short-range vertex-based pathfinder.
Integrate new pathfinder into unit motion code.
Change obstruction system to get rid of circles, and differentiate
structures from units.
Make PositionChanged messages synchronous.
Try to prevent some accidental float->int conversions.

This was SVN commit r7484.
This commit is contained in:
Ykkrosh 2010-04-29 23:36:05 +00:00
parent 5daac34ef9
commit cfae58928f
60 changed files with 2396 additions and 806 deletions

View File

@ -14,5 +14,7 @@
<Circle radius="4"/>
<Height>1.0</Height>
</Footprint>
<Obstruction/>
<Obstruction>
<Unit radius="4"/>
</Obstruction>
</Entity>

View File

@ -7,10 +7,16 @@ function _setHighlight(ents, colour)
Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities":ents, "colour":colour });
}
function _setMotionOverlay(ents, enabled)
{
Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities":ents, "enabled":enabled });
}
function EntitySelection()
{
this.selected = {}; // { id: 1, id: 1, ... } for each selected entity ID 'id'
this.highlighted = {}; // { id: 1, ... } for mouseover-highlighted entity IDs
this.motionDebugOverlay = false;
}
EntitySelection.prototype.toggle = function(ent)
@ -18,11 +24,13 @@ EntitySelection.prototype.toggle = function(ent)
if (this.selected[ent])
{
_setHighlight([ent], g_InactiveSelectionColour);
_setMotionOverlay([ent], false);
delete this.selected[ent];
}
else
{
_setHighlight([ent], g_ActiveSelectionColour);
_setMotionOverlay([ent], this.motionDebugOverlay);
this.selected[ent] = 1;
}
};
@ -39,11 +47,13 @@ EntitySelection.prototype.addList = function(ents)
}
}
_setHighlight(added, g_ActiveSelectionColour);
_setMotionOverlay(added, this.motionDebugOverlay);
};
EntitySelection.prototype.reset = function()
{
_setHighlight(this.toList(), g_InactiveSelectionColour);
_setMotionOverlay(this.toList(), false);
this.selected = {};
};
@ -82,7 +92,10 @@ EntitySelection.prototype.setHighlightList = function(ents)
this.highlighted[ent] = 1;
};
EntitySelection.prototype.SetMotionDebugOverlay = function(enabled)
{
this.motionDebugOverlay = enabled;
_setMotionOverlay(this.toList(), enabled);
};
var g_Selection = new EntitySelection();

View File

@ -42,7 +42,7 @@
</object>
<!-- Dev/cheat commands -->
<object size="100%-170 32 100%-16 96" type="image" sprite="devCommandsBackground">
<object size="100%-170 32 100%-16 112" type="image" sprite="devCommandsBackground">
<object size="0 0 100%-18 16" type="text" style="devCommandsText">Control all units</object>
<object size="100%-16 0 100% 16" type="checkbox" name="devControlAll" style="wheatCrossBox"/>
@ -58,6 +58,11 @@
<object size="100%-16 48 100% 64" type="checkbox" style="wheatCrossBox">
<action on="Press">Engine.GuiInterfaceCall("SetObstructionDebugOverlay", this.checked);</action>
</object>
<object size="0 64 100%-18 80" type="text" style="devCommandsText">Unit motion overlay</object>
<object size="100%-16 64 100% 80" type="checkbox" style="wheatCrossBox">
<action on="Press">g_Selection.SetMotionDebugOverlay(this.checked);</action>
</object>
</object>
<!-- Debug text -->

View File

@ -33,7 +33,7 @@ Builder.prototype.GetEntitiesList = function()
Builder.prototype.GetRange = function()
{
return { "max": 16, "min": 0 };
return { "max": 2, "min": 0 };
// maybe this should depend on the unit or target or something?
}

View File

@ -229,6 +229,16 @@ GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
cmpObstructionManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for each (var ent in data.entities)
{
var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
@ -241,7 +251,8 @@ var exposedFunctions = {
"SetSelectionHighlight": 1,
"SetBuildingPlacementPreview": 1,
"SetPathfinderDebugOverlay": 1,
"SetObstructionDebugOverlay": 1
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)

View File

@ -36,7 +36,8 @@ Timer.prototype.OnUpdate = function(msg)
try {
cmp[t[2]](t[4]);
} catch (e) {
print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e+"\n");
var stack = e.stack.trimRight().replace(/^/mg, ' '); // indent the stack trace
print("Error in timer on entity "+t[0]+", IID "+t[1]+", function "+t[2]+": "+e+"\n"+stack+"\n");
// TODO: should report in an error log
}
delete this.timers[id];

View File

@ -187,6 +187,8 @@ TrainingQueue.prototype.ProgressTimeout = function(data)
// with items that take fractions of a second)
var time = g_ProgressInterval;
time *= 10; // XXX: this is a hack to make testing easier
while (time > 0 && this.queue.length)
{
var item = this.queue[0];

View File

@ -70,12 +70,15 @@ UnitAI.prototype.Walk = function(x, z)
if (!cmpMotion)
return;
this.SelectAnimation("walk", false, cmpMotion.GetSpeed());
PlaySound("walk", this.entity);
cmpMotion.MoveToPoint(x, z, 0, 0);
this.state = STATE_WALKING;
if (cmpMotion.MoveToPoint(x, z))
{
this.state = STATE_WALKING;
PlaySound("walk", this.entity);
}
else
{
this.state = STATE_IDLE;
}
};
UnitAI.prototype.Attack = function(target)
@ -92,8 +95,14 @@ UnitAI.prototype.Attack = function(target)
// Remember the target, and start moving towards it
this.attackTarget = target;
this.MoveToTarget(target, cmpAttack.GetRange());
this.state = STATE_ATTACKING;
if (!this.MoveToTarget(target, cmpAttack.GetRange()))
{
// We're in range already, do the attack
// (TODO: this could also happen if we couldn't move anywhere)
this.StartAttack();
}
// else we've started moving and the attack will start in OnMotionChanged
};
UnitAI.prototype.Repair = function(target)
@ -110,8 +119,14 @@ UnitAI.prototype.Repair = function(target)
// Remember the target, and start moving towards it
this.repairTarget = target;
this.MoveToTarget(target, cmpBuilder.GetRange());
this.state = STATE_REPAIRING;
if (!this.MoveToTarget(target, cmpBuilder.GetRange()))
{
// We're in range already, do the repairing
// (TODO: this could also happen if we couldn't move anywhere)
this.StartRepair();
}
// else we've started moving and the repair will start in OnMotionChanged
};
UnitAI.prototype.Gather = function(target)
@ -128,8 +143,14 @@ UnitAI.prototype.Gather = function(target)
// Remember the target, and start moving towards it
this.gatherTarget = target;
this.MoveToTarget(target, cmpResourceGatherer.GetRange());
this.state = STATE_GATHERING;
if (!this.MoveToTarget(target, cmpResourceGatherer.GetRange()))
{
// We're in range already, do the gathering
// (TODO: this could also happen if we couldn't move anywhere)
this.StartGather();
}
// else we've started moving and the gather will start in OnMotionChanged
};
//// Message handlers ////
@ -161,97 +182,79 @@ UnitAI.prototype.OnMotionChanged = function(msg)
// We were attacking, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoAttackRange())
if (this.MoveIntoRange(IID_Attack, this.attackTarget))
return;
// In range, so perform the attack,
// after the prepare time but not before the previous attack's recharge
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timers = cmpAttack.GetTimers();
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {});
// Start the attack animation and sound, but synced to the timers
this.SelectAnimation("melee", false, 1.0, "attack");
this.SetAnimationSync(time, timers.repeat);
// TODO: this drifts since the sim is quantised to sim turns and these timers aren't
// TODO: we should probably only bother syncing projectile attacks, not melee
// In range, so perform the attack
this.StartAttack();
}
else if (this.state == STATE_REPAIRING)
{
// We were repairing, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoRepairRange())
if (this.MoveIntoRange(IID_Builder, this.repairTarget))
return;
// In range, so perform the repairing
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
// Start the repair/build animation and sound
this.SelectAnimation("build", false, 1.0, "build");
this.StartRepair();
}
else if (this.state == STATE_GATHERING)
{
// We were gathering, and have stopped moving
// => check if we can still reach the target now
if (!this.MoveIntoGatherRange())
if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget))
return;
// In range, so perform the gathering
var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
// Get the animation/sound type name
var type = cmpResourceSupply.GetType();
var typename = "gather_" + (type.specific || type.generic);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {"typename": typename});
// Start the gather animation and sound
this.SelectAnimation(typename, false, 1.0, typename);
this.StartGather();
}
}
};
//// Private functions ////
function hypot2(x, y)
UnitAI.prototype.StartAttack = function()
{
return x*x + y*y;
}
// Perform the attack after the prepare time but not before the previous attack's recharge
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
UnitAI.prototype.CheckRange = function(target, range)
var timers = cmpAttack.GetTimers();
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {});
// Start the attack animation and sound, but synced to the timers
this.SelectAnimation("melee", false, 1.0, "attack");
this.SetAnimationSync(time, timers.repeat);
// TODO: this drifts since the sim is quantised to sim turns and these timers aren't
// TODO: we should probably only bother syncing projectile attacks, not melee
};
UnitAI.prototype.StartRepair = function()
{
// Target must be in the world
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
return { "error": "not-in-world" };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
// We must be in the world
var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld())
return { "error": "not-in-world" };
// Start the repair/build animation and sound
this.SelectAnimation("build", false, 1.0, "build");
};
// Target must be within range
var posTarget = cmpPositionTarget.GetPosition();
var posSelf = cmpPositionSelf.GetPosition();
var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z);
// TODO: ought to be distance to closest point in footprint, not to center
// The +4 is a hack to give a ~1 tile tolerance, because the pathfinder doesn't
// always get quite close enough to the target
if (dist2 > (range.max+4)*(range.max+4))
return { "error": "out-of-range" };
UnitAI.prototype.StartGather = function()
{
var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
return {};
}
// Get the animation/sound type name
var type = cmpResourceSupply.GetType();
var typename = "gather_" + (type.specific || type.generic);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {"typename": typename});
// Start the gather animation and sound
this.SelectAnimation(typename, false, 1.0, typename);
};
UnitAI.prototype.CancelTimers = function()
{
@ -278,89 +281,30 @@ UnitAI.prototype.CancelTimers = function()
};
/**
* Tries to move into range of the attack target.
* Returns true if it's already in range.
* Tries to move into range of the target.
* Returns true if the unit has started walking or on pathing failure, false if already in range.
*/
UnitAI.prototype.MoveIntoAttackRange = function()
UnitAI.prototype.MoveIntoRange = function(iid, target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange();
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = cmpRanged.GetRange();
var rangeStatus = this.CheckRange(this.attackTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.attackTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion.IsInAttackRange(target, range.min, range.max))
return false;
}
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
if (this.MoveToTarget(target, range))
return true;
// If it's impossible to reach the target, give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return true;
};
UnitAI.prototype.MoveIntoRepairRange = function()
{
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
var range = cmpBuilder.GetRange();
var rangeStatus = this.CheckRange(this.repairTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.repairTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return false;
}
return true;
};
UnitAI.prototype.MoveIntoGatherRange = function()
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var range = cmpResourceGatherer.GetRange();
var rangeStatus = this.CheckRange(this.gatherTarget, range);
if (rangeStatus.error)
{
if (rangeStatus.error == "out-of-range")
{
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
this.MoveToTarget(this.gatherTarget, range);
return false;
}
// Otherwise it's impossible to reach the target, so give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return false;
}
return true;
};
// TODO: refactor all this repetitive code
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
@ -387,16 +331,15 @@ UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
cmpVisual.SetAnimationSync(actiontime, repeattime);
};
/**
* Tries to move to the specified range of the target.
* This might synchronously trigger a MotionChanged message.
* Returns true if the unit has started walking, false on error or if already in range.
*/
UnitAI.prototype.MoveToTarget = function(target, range)
{
var cmpPositionTarget = Engine.QueryInterface(target, IID_Position);
if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld())
return;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
var pos = cmpPositionTarget.GetPosition();
cmpMotion.MoveToPoint(pos.x, pos.z, range.min, range.max);
return cmpMotion.MoveToAttackRange(target, range.min, range.max);
};
UnitAI.prototype.AttackTimeout = function(data)
@ -406,7 +349,7 @@ UnitAI.prototype.AttackTimeout = function(data)
return;
// Check if we can still reach the target
if (!this.MoveIntoAttackRange())
if (this.MoveIntoRange(IID_Attack, this.attackTarget))
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
@ -430,7 +373,7 @@ UnitAI.prototype.RepairTimeout = function(data)
return;
// Check if we can still reach the target
if (!this.MoveIntoRepairRange())
if (this.MoveIntoRange(IID_Builder, this.repairTarget))
return;
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
@ -459,7 +402,7 @@ UnitAI.prototype.GatherTimeout = function(data)
return;
// Check if we can still reach the target
if (!this.MoveIntoGatherRange())
if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget))
return;
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);

View File

@ -4,5 +4,4 @@
<Civ>gaia</Civ>
<GenericName>Gaia</GenericName>
</Identity>
<Obstruction/>
</Entity>

View File

@ -7,4 +7,7 @@
<Circle radius="1.5"/>
<Height>10.0</Height>
</Footprint>
<Obstruction>
<Static width="3.0" depth="3.0"/>
</Obstruction>
</Entity>

View File

@ -7,4 +7,7 @@
<Circle radius="1.0"/>
<Height>1.0</Height>
</Footprint>
<Obstruction>
<Static width="2.0" depth="2.0"/>
</Obstruction>
</Entity>

View File

@ -7,4 +7,7 @@
<Circle radius="3.5"/>
<Height>3.5</Height>
</Footprint>
<Obstruction>
<Static width="7.0" depth="7.0"/>
</Obstruction>
</Entity>

View File

@ -24,5 +24,4 @@
<Pierce>42.0</Pierce>
<Crush>10.0</Crush>
</Armour>
<Obstruction/>
</Entity>

View File

@ -29,4 +29,7 @@
<Square width="32.0" depth="32.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="32.0" depth="32.0"/>
</Obstruction>
</Entity>

View File

@ -26,6 +26,9 @@
<Square width="9.0" depth="9.0"/>
<Height>5.0</Height>
</Footprint>
<Obstruction>
<Static width="9.0" depth="9.0"/>
</Obstruction>
<TrainingQueue>
<Entities>
units/{civ}_support_female_citizen

View File

@ -26,6 +26,9 @@
<Square width="12.0" depth="12.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="12.0" depth="12.0"/>
</Obstruction>
<TrainingQueue>
<Entities>
units/{civ}_support_healer

View File

@ -26,4 +26,7 @@
<Square width="6.0" depth="6.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="6.0" depth="6.0"/>
</Obstruction>
</Entity>

View File

@ -24,4 +24,7 @@
<Square width="6.0" depth="6.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="6.0" depth="6.0"/>
</Obstruction>
</Entity>

View File

@ -24,4 +24,7 @@
<Square width="6.0" depth="6.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="6.0" depth="6.0"/>
</Obstruction>
</Entity>

View File

@ -24,4 +24,7 @@
<Square width="6.0" depth="6.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="6.0" depth="6.0"/>
</Obstruction>
</Entity>

View File

@ -25,4 +25,7 @@
<Square width="12.0" depth="12.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="12.0" depth="12.0"/>
</Obstruction>
</Entity>

View File

@ -25,6 +25,9 @@
<Square width="17.0" depth="17.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="17.0" depth="17.0"/>
</Obstruction>
<TrainingQueue>
<Entities>
units/{civ}_support_trader

View File

@ -25,4 +25,7 @@
<Square width="12.0" depth="12.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="12.0" depth="12.0"/>
</Obstruction>
</Entity>

View File

@ -14,4 +14,7 @@
<Square width="32.0" depth="32.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="32.0" depth="32.0"/>
</Obstruction>
</Entity>

View File

@ -27,4 +27,7 @@
<Square width="17.0" depth="17.0"/>
<Height>5.0</Height>
</Footprint>
<Obstruction>
<Static width="17.0" depth="17.0"/>
</Obstruction>
</Entity>

View File

@ -26,4 +26,7 @@
<Square width="18.0" depth="18.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="18.0" depth="18.0"/>
</Obstruction>
</Entity>

View File

@ -27,4 +27,7 @@
<Square width="24.0" depth="24.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="24.0" depth="24.0"/>
</Obstruction>
</Entity>

View File

@ -29,4 +29,7 @@
<Square width="9.5" depth="19.75"/>
<Height>5.0</Height>
</Footprint>
<Obstruction>
<Static width="9.5" depth="19.75"/>
</Obstruction>
</Entity>

View File

@ -29,5 +29,4 @@
<Square width="10.0" depth="10.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction disable=""/>
</Entity>

View File

@ -14,4 +14,7 @@
<Square width="24.0" depth="24.0"/>
<Height>8.0</Height>
</Footprint>
<Obstruction>
<Static width="24.0" depth="24.0"/>
</Obstruction>
</Entity>

View File

@ -33,4 +33,7 @@
<Circle radius="0.5"/>
<Height>2.5</Height>
</Footprint>
<Obstruction>
<Unit radius="1.0"/>
</Obstruction>
</Entity>

View File

@ -22,6 +22,7 @@
#include "precompiled.h"
#include "lib/res/graphics/ogl_tex.h"
#include "lib/sysdep/cpu.h"
#include "renderer/Renderer.h"
#include "renderer/WaterManager.h"
@ -150,9 +151,9 @@ void CTerrain::CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) cons
height = m_Heightmap[j*m_MapSize + i];
else
height = 0;
pos.X = CFixed_23_8::FromInt(i)*CELL_SIZE;
pos.Y = CFixed_23_8::FromInt(height)/HEIGHT_UNITS_PER_METRE;
pos.Z = CFixed_23_8::FromInt(j)*CELL_SIZE;
pos.X = CFixed_23_8::FromInt(i)*(int)CELL_SIZE;
pos.Y = CFixed_23_8::FromInt(height)/(int)HEIGHT_UNITS_PER_METRE;
pos.Z = CFixed_23_8::FromInt(j)*(int)CELL_SIZE;
}
@ -343,13 +344,13 @@ float CTerrain::GetExactGroundLevel(float x, float z) const
CFixed_23_8 CTerrain::GetExactGroundLevelFixed(CFixed_23_8 x, CFixed_23_8 z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = clamp((ssize_t)(x/CELL_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const ssize_t zi = clamp((ssize_t)(z/CELL_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const ssize_t xi = clamp((ssize_t)(x / (int)CELL_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const ssize_t zi = clamp((ssize_t)(z / (int)CELL_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const CFixed_23_8 one = CFixed_23_8::FromInt(1);
const CFixed_23_8 xf = clamp((x/CELL_SIZE)-CFixed_23_8::FromInt(xi), CFixed_23_8::FromInt(0), one);
const CFixed_23_8 zf = clamp((z/CELL_SIZE)-CFixed_23_8::FromInt(zi), CFixed_23_8::FromInt(0), one);
const CFixed_23_8 xf = clamp((x / (int)CELL_SIZE) - CFixed_23_8::FromInt(xi), CFixed_23_8::FromInt(0), one);
const CFixed_23_8 zf = clamp((z / (int)CELL_SIZE) - CFixed_23_8::FromInt(zi), CFixed_23_8::FromInt(0), one);
u16 h00 = m_Heightmap[zi*m_MapSize + xi];
u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
@ -357,7 +358,7 @@ CFixed_23_8 CTerrain::GetExactGroundLevelFixed(CFixed_23_8 x, CFixed_23_8 z) con
u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
// Linearly interpolate
return ((one - zf).Multiply((one - xf) * h00 + xf * h10)
+ zf.Multiply((one - xf) * h01 + xf * h11)) / HEIGHT_UNITS_PER_METRE;
+ zf.Multiply((one - xf) * h01 + xf * h11)) / (int)HEIGHT_UNITS_PER_METRE;
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -25,7 +25,6 @@
#include "maths/Vector3D.h"
#include "maths/Fixed.h"
#include "graphics/SColor.h"
#include "lib/sysdep/cpu.h"
class HEntity;
class CEntity;

View File

@ -32,6 +32,7 @@
#include "lib/external_libraries/sdl.h"
#include "lib/bits.h"
#include "lib/timer.h"
#include "lib/sysdep/cpu.h"
#include "network/NetMessage.h"
#include "ps/Game.h"
#include "ps/Interact.h"

View File

@ -120,6 +120,12 @@ public:
/// Subtract a CFixed. Might overflow.
CFixed operator-(CFixed n) const { return CFixed(value - n.value); }
/// Add a CFixed. Might overflow.
CFixed& operator+=(CFixed n) { value += n.value; return *this; }
/// Subtract a CFixed. Might overflow.
CFixed& operator-=(CFixed n) { value -= n.value; return *this; }
/// Negate a CFixed.
CFixed operator-() const { return CFixed(-value); }
@ -155,6 +161,11 @@ public:
u32 s = isqrt64(value);
return CFixed((u64)s << (fract_bits / 2));
}
private:
// Prevent dangerous accidental implicit conversions of floats to ints in certain operations
CFixed operator*(float n) const;
CFixed operator/(float n) const;
};
/**

View File

@ -39,6 +39,12 @@ public:
return (X == v.X && Y == v.Y);
}
/// Vector inequality
bool operator!=(const CFixedVector2D& v) const
{
return (X != v.X || Y != v.Y);
}
/// Vector addition
CFixedVector2D operator+(const CFixedVector2D& v) const
{
@ -102,6 +108,11 @@ public:
return r;
}
bool IsZero() const
{
return (X.IsZero() && Y.IsZero());
}
/**
* Normalize the vector so that length is close to 1.
* If length is 0, does nothing.
@ -110,14 +121,36 @@ public:
*/
void Normalize()
{
fixed l = Length();
if (!l.IsZero())
if (!IsZero())
{
fixed l = Length();
X = X / l;
Y = Y / l;
}
}
/**
* Normalize the vector so that length is close to n.
* If length is 0, does nothing.
*/
void Normalize(fixed n)
{
if (n.IsZero())
{
X = Y = fixed::FromInt(0);
return;
}
fixed l = Length();
// TODO: work out whether this is giving decent precision
fixed d = l / n;
if (!d.IsZero())
{
X = X / d;
Y = Y / d;
}
}
/**
* Compute the dot product of this vector with another.
*/
@ -130,6 +163,11 @@ public:
ret.SetInternalValue((i32)(sum >> fixed::fract_bits));
return ret;
}
CFixedVector2D Perpendicular()
{
return CFixedVector2D(Y, -X);
}
};
#endif // INCLUDED_FIXED_VECTOR2D

View File

@ -166,13 +166,13 @@ CStr CProfileNodeTable::GetCellText(size_t row, size_t col)
}
if (col == 2)
sprintf_s(buf, sizeof(buf), "%.3f", unlogged * 1000.0f);
sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", unlogged * 1000.0f);
else if (col == 3)
sprintf_s(buf, sizeof(buf), "%.1f", unlogged / g_Profiler.GetRoot()->GetFrameTime());
sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", unlogged / g_Profiler.GetRoot()->GetFrameTime());
else if (col == 4)
sprintf_s(buf, sizeof(buf), "%.1f", unlogged * 100.0f / g_Profiler.GetRoot()->GetFrameTime());
sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", unlogged * 100.0f / g_Profiler.GetRoot()->GetFrameTime());
else if (col == 5)
sprintf_s(buf, sizeof(buf), "%ld", unlogged_mallocs);
sprintf_s(buf, ARRAY_SIZE(buf), "%ld", unlogged_mallocs);
return CStr(buf);
}
@ -187,7 +187,7 @@ CStr CProfileNodeTable::GetCellText(size_t row, size_t col)
#ifdef PROFILE_AMORTIZE
sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", child->GetFrameCalls());
#else
sprintf_s(buf, sizeof(buf), "%d", child->GetFrameCalls());
sprintf_s(buf, ARRAY_SIZE(buf), "%d", child->GetFrameCalls());
#endif
break;
case 2:

View File

@ -87,98 +87,6 @@ TerrainOverlay::~TerrainOverlay()
}
#if 0
//initial test to draw out entity boundaries
//it shows how to retrieve object boundary postions for triangulation
//NOTE: it's a test to see how to retrieve bounadry locations for static objects on the terrain.
//disabled
void TerrainOverlay::RenderEntityEdges()
{
//Kai: added for line drawing
//use a function to encapsulate all the entity boundaries
std::vector<CEntity*> results;
g_EntityManager.GetExtant(results);
glColor3f( 1, 1, 1 ); // Colour outline with player colour
for(size_t i =0 ; i < results.size(); i++)
{
glBegin(GL_LINE_LOOP);
CEntity* tempHandle = results[i];
debug_printf(L"Entity position: %f %f %f\n", tempHandle->m_position.X,tempHandle->m_position.Y,tempHandle->m_position.Z);
CVector2D p, q;
CVector2D u, v;
q.x = tempHandle->m_position.X;
q.y = tempHandle->m_position.Z;
float d = ((CBoundingBox*)tempHandle->m_bounds)->m_d;
float w = ((CBoundingBox*)tempHandle->m_bounds)->m_w;
u.x = sin( tempHandle->m_graphics_orientation.Y );
u.y = cos( tempHandle->m_graphics_orientation.Y );
v.x = u.y;
v.y = -u.x;
CBoundingObject* m_bounds = tempHandle->m_bounds;
switch( m_bounds->m_type )
{
case CBoundingObject::BOUND_CIRCLE:
{
break;
}
case CBoundingObject::BOUND_OABB:
{
//glVertex3f( tempHandle->m_position.X, tempHandle->GetAnchorLevel( tempHandle->m_position.X, tempHandle->m_position.Z ) + 10.0f, tempHandle->m_position.Z ); // lower left vertex
//glVertex3f( 5, tempHandle->GetAnchorLevel( 5, 5 ) + 0.25f, 5 ); // upper vertex
p = q + u * d + v * w;
glVertex3f( p.x, tempHandle->GetAnchorLevel( p.x, p.y ) + + 10.0f, p.y );
p = q - u * d + v * w ;
glVertex3f( p.x, tempHandle->GetAnchorLevel( p.x, p.y ) + + 10.0f, p.y );
p = q - u * d - v * w;
glVertex3f( p.x, tempHandle->GetAnchorLevel( p.x, p.y ) + + 10.0f, p.y );
p = q + u * d - v * w;
glVertex3f( p.x, tempHandle->GetAnchorLevel( p.x, p.y ) + + 10.0f, p.y );
break;
}
}//end switch
glEnd();
}//end for loop
CTerrain* m_Terrain = g_Game->GetWorld()->GetTerrain();
CEntity* tempHandle = results[0];
glBegin(GL_LINE_LOOP);
int width = m_Terrain->GetVerticesPerSide()*4;
glVertex3f( 0, tempHandle->GetAnchorLevel( 0, 0 ) + 0.25f, 0 );
glVertex3f( width, tempHandle->GetAnchorLevel( width, 0 ) + 0.25f, 0 );
glVertex3f( width, tempHandle->GetAnchorLevel(width,width ) + 0.25f,width );
glVertex3f( 0, tempHandle->GetAnchorLevel( 0, width ) + 0.25f, width );
glEnd();
//----------------------
}
#endif
void TerrainOverlay::RenderOverlays()
{
if (g_TerrainOverlayList.size() == 0)

View File

@ -100,7 +100,7 @@ INTERFACE(Terrain)
COMPONENT(Terrain)
INTERFACE(UnitMotion)
COMPONENT(UnitMotion)
COMPONENT(UnitMotion) // must be after Obstruction
INTERFACE(Visual)
COMPONENT(VisualActor)

View File

@ -20,6 +20,7 @@
#include "simulation2/system/Component.h"
#include "ICmpFootprint.h"
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "simulation2/MessageTypes.h"
@ -122,6 +123,11 @@ public:
virtual CFixedVector3D PickSpawnPoint(entity_id_t spawned)
{
// Try to find a free space around the building's footprint.
// (Note that we use the footprint, not the obstruction shape - this might be a bit dodgy
// because the footprint might be inside the obstruction, but it hopefully gives us a nicer
// shape.)
CFixedVector3D error(CFixed_23_8::FromInt(-1), CFixed_23_8::FromInt(-1), CFixed_23_8::FromInt(-1));
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
@ -132,28 +138,15 @@ public:
if (cmpObstructionManager.null())
return error;
// Always approximate the spawned entity as a circle, so we're orientation-independent
CFixed_23_8 spawnedRadius;
entity_pos_t spawnedRadius;
CmpPtr<ICmpFootprint> cmpSpawnedFootprint(*m_Context, spawned);
if (!cmpSpawnedFootprint.null())
{
EShape shape;
CFixed_23_8 size0, size1, height;
cmpSpawnedFootprint->GetShape(shape, size0, size1, height);
if (shape == CIRCLE)
spawnedRadius = size0;
else
spawnedRadius = std::max(size0, size1); // safe overapproximation of the correct sqrt((size0/2)^2 + (size1/2)^2)
}
else
{
// No footprint - weird but let's just pretend it's a point
spawnedRadius = CFixed_23_8::FromInt(0);
}
CmpPtr<ICmpObstruction> cmpSpawnedObstruction(*m_Context, spawned);
if (!cmpSpawnedObstruction.null())
spawnedRadius = cmpSpawnedObstruction->GetUnitRadius();
// else zero
// The spawn point should be far enough from this footprint to fit the unit, plus a little gap
CFixed_23_8 clearance = spawnedRadius + CFixed_23_8::FromInt(2);
CFixed_23_8 clearance = spawnedRadius + entity_pos_t::FromInt(2);
CFixedVector3D initialPos = cmpPosition->GetPosition();
entity_angle_t initialAngle = cmpPosition->GetRotation().Y;
@ -166,7 +159,7 @@ public:
const ssize_t numPoints = 31;
for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2]
{
entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/numPoints);
entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/(int)numPoints);
CFixed_23_8 s, c;
sincos_approx(angle, s, c);
@ -174,7 +167,7 @@ public:
CFixedVector3D pos (initialPos.X + s.Multiply(radius), CFixed_23_8::Zero(), initialPos.Z + c.Multiply(radius));
SkipTagObstructionFilter filter(spawned); // ignore collisions with the spawned entity
if (cmpObstructionManager->TestCircle(filter, pos.X, pos.Z, spawnedRadius))
if (!cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Z, spawnedRadius))
return pos; // this position is okay, so return it
}
}
@ -217,14 +210,14 @@ public:
CFixedVector2D center;
center.X = initialPos.X + (-dir.Y).Multiply(sy/2 + clearance);
center.Y = initialPos.Z + dir.X.Multiply(sy/2 + clearance);
dir = dir.Multiply((sx + clearance*2) / (numPoints-1));
dir = dir.Multiply((sx + clearance*2) / (int)(numPoints-1));
for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2]
{
CFixedVector2D pos (center + dir*i);
SkipTagObstructionFilter filter(spawned); // ignore collisions with the spawned entity
if (cmpObstructionManager->TestCircle(filter, pos.X, pos.Y, spawnedRadius))
if (!cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, spawnedRadius))
return CFixedVector3D(pos.X, CFixed_23_8::Zero(), pos.Y); // this position is okay, so return it
}
}

View File

@ -20,8 +20,8 @@
#include "simulation2/system/Component.h"
#include "ICmpObstruction.h"
#include "ICmpFootprint.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "simulation2/MessageTypes.h"
@ -35,6 +35,7 @@ public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_PositionChanged);
componentManager.SubscribeToMessageType(MT_MotionChanged);
componentManager.SubscribeToMessageType(MT_Destroy);
}
@ -42,8 +43,19 @@ public:
const CSimContext* m_Context;
// Template state:
enum {
STATIC,
UNIT
} m_Type;
entity_pos_t m_Size0; // radius or width
entity_pos_t m_Size1; // radius or depth
bool m_Active; // whether the obstruction is obstructing or just an inactive placeholder
// Dynamic state:
bool m_Moving;
ICmpObstructionManager::tag_t m_Tag;
static std::string GetSchema()
@ -51,6 +63,21 @@ public:
return
"<a:example/>"
"<a:help>Causes this entity's footprint to obstruct the motion of other units.</a:help>"
"<choice>"
"<element name='Static'>"
"<attribute name='width'>"
"<ref name='positiveDecimal'/>"
"</attribute>"
"<attribute name='depth'>"
"<ref name='positiveDecimal'/>"
"</attribute>"
"</element>"
"<element name='Unit'>"
"<attribute name='radius'>"
"<ref name='positiveDecimal'/>"
"</attribute>"
"</element>"
"</choice>"
"<optional>"
"<element name='Inactive' a:help='If this element is present, this entity will be ignored in collision tests by other units but can still perform its own collision tests'>"
"<empty/>"
@ -62,12 +89,25 @@ public:
{
m_Context = &context;
if (paramNode.GetChild("Unit").IsOk())
{
m_Type = UNIT;
m_Size0 = m_Size1 = paramNode.GetChild("Unit").GetChild("@radius").ToFixed();
}
else
{
m_Type = STATIC;
m_Size0 = paramNode.GetChild("Static").GetChild("@width").ToFixed();
m_Size1 = paramNode.GetChild("Static").GetChild("@depth").ToFixed();
}
if (paramNode.GetChild("Inactive").IsOk())
m_Active = false;
else
m_Active = true;
m_Tag = 0;
m_Moving = false;
}
virtual void Deinit(const CSimContext& UNUSED(context))
@ -111,19 +151,10 @@ public:
else if (data.inWorld && !m_Tag)
{
// Need to create a new pathfinder shape:
CmpPtr<ICmpFootprint> cmpFootprint(context, GetEntityId());
if (cmpFootprint.null())
break;
ICmpFootprint::EShape shape;
entity_pos_t size0, size1, height;
cmpFootprint->GetShape(shape, size0, size1, height);
if (shape == ICmpFootprint::SQUARE)
m_Tag = cmpObstructionManager->AddSquare(data.x, data.z, data.a, size0, size1);
if (m_Type == STATIC)
m_Tag = cmpObstructionManager->AddStaticShape(data.x, data.z, data.a, m_Size0, m_Size1);
else
m_Tag = cmpObstructionManager->AddCircle(data.x, data.z, size0);
m_Tag = cmpObstructionManager->AddUnitShape(data.x, data.z, m_Size0, m_Moving);
}
else if (!data.inWorld && m_Tag)
{
@ -132,6 +163,20 @@ public:
}
break;
}
case MT_MotionChanged:
{
const CMessageMotionChanged& data = static_cast<const CMessageMotionChanged&> (msg);
m_Moving = !data.speed.IsZero();
if (m_Tag && m_Type == UNIT)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(context, SYSTEM_ENTITY);
if (cmpObstructionManager.null())
break;
cmpObstructionManager->SetUnitMovingFlag(m_Tag, m_Moving);
}
break;
}
case MT_Destroy:
{
if (m_Tag)
@ -148,31 +193,35 @@ public:
}
}
virtual ICmpObstructionManager::tag_t GetObstruction()
{
return m_Tag;
}
virtual entity_pos_t GetUnitRadius()
{
if (m_Type == UNIT)
return m_Size0;
else
return entity_pos_t::Zero();
}
virtual bool CheckCollisions()
{
CmpPtr<ICmpFootprint> cmpFootprint(*m_Context, GetEntityId());
if (cmpFootprint.null())
return false;
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null())
return false;
ICmpFootprint::EShape shape;
entity_pos_t size0, size1, height;
cmpFootprint->GetShape(shape, size0, size1, height);
CFixedVector3D pos = cmpPosition->GetPosition();
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
SkipTagObstructionFilter filter(m_Tag); // ignore collisions with self
if (shape == ICmpFootprint::SQUARE)
return !cmpObstructionManager->TestSquare(filter, pos.X, pos.Z, cmpPosition->GetRotation().Y, size0, size1);
if (m_Type == STATIC)
return cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Z, cmpPosition->GetRotation().Y, m_Size0, m_Size1);
else
return !cmpObstructionManager->TestCircle(filter, pos.X, pos.Z, size0);
return cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Z, m_Size0);
}
};

View File

@ -21,11 +21,11 @@
#include "ICmpObstructionManager.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/FixedVector2D.h"
#include "maths/MathUtil.h"
#include "ps/Overlay.h"
#include "ps/Profile.h"
@ -34,28 +34,30 @@
// Externally, tags are opaque non-zero positive integers.
// Internally, they are tagged (by shape) indexes into shape lists.
// idx must be non-zero.
#define TAG_IS_CIRCLE(tag) (((tag) & 1) == 0)
#define TAG_IS_SQUARE(tag) (((tag) & 1) == 1)
#define CIRCLE_INDEX_TO_TAG(idx) (((idx) << 1) | 0)
#define SQUARE_INDEX_TO_TAG(idx) (((idx) << 1) | 1)
#define TAG_IS_UNIT(tag) (((tag) & 1) == 0)
#define TAG_IS_STATIC(tag) (((tag) & 1) == 1)
#define UNIT_INDEX_TO_TAG(idx) (((idx) << 1) | 0)
#define STATIC_INDEX_TO_TAG(idx) (((idx) << 1) | 1)
#define TAG_TO_INDEX(tag) ((tag) >> 1)
/**
* Internal representation of circle shapes
* Internal representation of axis-aligned sometimes-square sometimes-circle shapes for moving units
*/
struct Circle
struct UnitShape
{
entity_pos_t x, z, r;
entity_pos_t x, z;
entity_pos_t r; // radius of circle, or half width of square
bool moving; // whether it's currently mobile (and should be generally ignored when pathing)
};
/**
* Internal representation of square shapes
* Internal representation of arbitrary-rotation static square shapes for buildings
*/
struct Square
struct StaticShape
{
entity_pos_t x, z;
entity_angle_t a;
entity_pos_t w, h;
entity_pos_t x, z; // world-space coordinates
CFixedVector2D u, v; // orthogonal unit vectors - axes of local coordinate space
entity_pos_t hw, hh; // half width/height in local coordinate space
};
class CCmpObstructionManager : public ICmpObstructionManager
@ -73,10 +75,10 @@ public:
std::vector<SOverlayLine> m_DebugOverlayLines;
// TODO: using std::map is a bit inefficient; is there a better way to store these?
std::map<u32, Circle> m_Circles;
std::map<u32, Square> m_Squares;
u32 m_CircleNext; // next allocated id
u32 m_SquareNext;
std::map<u32, UnitShape> m_UnitShapes;
std::map<u32, StaticShape> m_StaticShapes;
u32 m_UnitShapeNext; // next allocated id
u32 m_StaticShapeNext;
static std::string GetSchema()
{
@ -88,8 +90,8 @@ public:
m_DebugOverlayEnabled = false;
m_DebugOverlayDirty = true;
m_CircleNext = 1;
m_SquareNext = 1;
m_UnitShapeNext = 1;
m_StaticShapeNext = 1;
m_DirtyID = 1; // init to 1 so default-initialised grids are considered dirty
}
@ -125,62 +127,106 @@ public:
}
}
virtual tag_t AddCircle(entity_pos_t x, entity_pos_t z, entity_pos_t r)
virtual tag_t AddUnitShape(entity_pos_t x, entity_pos_t z, entity_pos_t r, bool moving)
{
Circle c = { x, z, r };
size_t id = m_CircleNext++;
m_Circles[id] = c;
UnitShape shape = { x, z, r, moving };
size_t id = m_UnitShapeNext++;
m_UnitShapes[id] = shape;
MakeDirty();
return CIRCLE_INDEX_TO_TAG(id);
return UNIT_INDEX_TO_TAG(id);
}
virtual tag_t AddSquare(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h)
virtual tag_t AddStaticShape(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h)
{
Square s = { x, z, a, w, h };
size_t id = m_SquareNext++;
m_Squares[id] = s;
CFixed_23_8 s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
StaticShape shape = { x, z, u, v, w/2, h/2 };
size_t id = m_StaticShapeNext++;
m_StaticShapes[id] = shape;
MakeDirty();
return SQUARE_INDEX_TO_TAG(id);
return STATIC_INDEX_TO_TAG(id);
}
virtual void MoveShape(tag_t tag, entity_pos_t x, entity_pos_t z, entity_angle_t a)
{
debug_assert(tag);
if (TAG_IS_CIRCLE(tag))
if (TAG_IS_UNIT(tag))
{
Circle& c = m_Circles[TAG_TO_INDEX(tag)];
c.x = x;
c.z = z;
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
shape.x = x;
shape.z = z;
}
else
{
Square& s = m_Squares[TAG_TO_INDEX(tag)];
s.x = x;
s.z = z;
s.a = a;
CFixed_23_8 s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
shape.x = x;
shape.z = z;
shape.u = u;
shape.v = v;
}
MakeDirty();
}
virtual void SetUnitMovingFlag(tag_t tag, bool moving)
{
debug_assert(tag && TAG_IS_UNIT(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
shape.moving = moving;
}
}
virtual void RemoveShape(tag_t tag)
{
debug_assert(tag);
if (TAG_IS_CIRCLE(tag))
m_Circles.erase(TAG_TO_INDEX(tag));
if (TAG_IS_UNIT(tag))
m_UnitShapes.erase(TAG_TO_INDEX(tag));
else
m_Squares.erase(TAG_TO_INDEX(tag));
m_StaticShapes.erase(TAG_TO_INDEX(tag));
MakeDirty();
}
virtual ObstructionSquare GetObstruction(tag_t tag)
{
debug_assert(tag);
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare o = { shape.x, shape.z, u, v, shape.r, shape.r };
return o;
}
else
{
StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
ObstructionSquare o = { shape.x, shape.z, shape.u, shape.v, shape.hw, shape.hh };
return o;
}
}
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r);
virtual bool TestCircle(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r);
virtual bool TestSquare(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h);
virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h);
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r);
virtual bool Rasterise(Grid<u8>& grid);
virtual void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares);
virtual bool FindMostImportantObstruction(entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square);
virtual void SetDebugOverlay(bool enabled)
{
@ -220,96 +266,103 @@ private:
REGISTER_COMPONENT_TYPE(ObstructionManager)
// Detect intersection between ray (0,0)-L and circle with center M radius r
// (Only counts intersections from the outside to the inside)
static bool IntersectRayCircle(CFixedVector2D l, CFixedVector2D m, entity_pos_t r)
{
// TODO: this should all be checked and tested etc, it's just a rough first attempt for now...
// Intersections at (t * l.X - m.X)^2 * (t * l.Y - m.Y) = r^2
// so solve the quadratic for t:
#define DOT(u, v) ( ((i64)u.X.GetInternalValue()*(i64)v.X.GetInternalValue()) + ((i64)u.Y.GetInternalValue()*(i64)v.Y.GetInternalValue()) )
i64 a = DOT(l, l);
if (a == 0)
return false; // avoid divide-by-zero later
i64 b = DOT(l, m)*-2;
i64 c = DOT(m, m) - r.GetInternalValue()*r.GetInternalValue();
i64 d = b*b - 4*a*c; // TODO: overflow breaks stuff here
if (d < 0) // no solutions
return false;
// Find the time of first intersection (entering the circle)
i64 t2a = (-b - isqrt64(d)); // don't divide by 2a explicitly, to avoid rounding errors
if ((a > 0 && t2a < 0) || (a < 0 && t2a > 0)) // if t2a/2a < 0 then intersection was before the ray
return false;
if (t2a >= 2*a) // intersection was after the ray
return false;
// printf("isct (%f,%f) (%f,%f) %f a=%lld b=%lld c=%lld d=%lld t2a=%lld\n", l.X.ToDouble(), l.Y.ToDouble(), m.X.ToDouble(), m.Y.ToDouble(), r.ToDouble(), a, b, c, d, t2a);
return true;
}
bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r)
{
PROFILE("TestLine");
// TODO: this is all very inefficient, it should use some kind of spatial data structures
// Ray-circle intersections
for (std::map<u32, Circle>::iterator it = m_Circles.begin(); it != m_Circles.end(); ++it)
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(CIRCLE_INDEX_TO_TAG(it->first)))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving))
continue;
if (IntersectRayCircle(CFixedVector2D(x1 - x0, z1 - z0), CFixedVector2D(it->second.x - x0, it->second.z - z0), it->second.r + r))
return false;
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D halfSize(it->second.r + r, it->second.r + r);
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, u, v, halfSize))
return true;
// If this is slow we could use a specialised TestRayAlignedSquare for axis-aligned squares
}
// Ray-square intersections
for (std::map<u32, Square>::iterator it = m_Squares.begin(); it != m_Squares.end(); ++it)
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(SQUARE_INDEX_TO_TAG(it->first)))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false))
continue;
// XXX need some kind of square intersection code
if (IntersectRayCircle(CFixedVector2D(x1 - x0, z1 - z0), CFixedVector2D(it->second.x - x0, it->second.z - z0), it->second.w/2 + r))
return false;
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r);
if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize))
return true;
}
return true;
return false;
}
bool CCmpObstructionManager::TestCircle(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r)
bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h)
{
PROFILE("TestCircle");
PROFILE("TestStaticShape");
// Circle-circle intersections
for (std::map<u32, Circle>::iterator it = m_Circles.begin(); it != m_Circles.end(); ++it)
CFixed_23_8 s, c;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s);
CFixedVector2D v(s, c);
CFixedVector2D center(x, z);
CFixedVector2D halfSize(w/2, h/2);
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(CIRCLE_INDEX_TO_TAG(it->first)))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving))
continue;
if (CFixedVector2D(it->second.x - x, it->second.z - z).Length() <= it->second.r + r)
return false;
CFixedVector2D center1(it->second.x, it->second.z);
if (Geometry::PointIsInSquare(center1 - center, u, v, CFixedVector2D(halfSize.X + it->second.r, halfSize.Y + it->second.r)))
return true;
}
// Circle-square intersections
for (std::map<u32, Square>::iterator it = m_Squares.begin(); it != m_Squares.end(); ++it)
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(SQUARE_INDEX_TO_TAG(it->first)))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false))
continue;
// XXX need some kind of square intersection code
if (CFixedVector2D(it->second.x - x, it->second.z - z).Length() <= it->second.w/2 + r)
return false;
CFixedVector2D center1(it->second.x, it->second.z);
CFixedVector2D halfSize1(it->second.hw, it->second.hh);
if (Geometry::TestSquareSquare(center, u, v, halfSize, center1, it->second.u, it->second.v, halfSize1))
return true;
}
return true;
return false;
}
bool CCmpObstructionManager::TestSquare(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h)
bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r)
{
// XXX need to implement this
return TestCircle(filter, x, z, w/2);
PROFILE("TestUnitShape");
CFixedVector2D center(x, z);
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving))
continue;
entity_pos_t r1 = it->second.r;
if (!(it->second.x + r1 < x - r || it->second.x - r1 > x + r || it->second.z + r1 < z - r || it->second.z - r1 > z + r))
return true;
}
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
if (Geometry::PointIsInSquare(center1 - center, it->second.u, it->second.v, CFixedVector2D(it->second.hw + r, it->second.hh + r)))
return true;
}
return false;
}
/**
@ -317,8 +370,17 @@ bool CCmpObstructionManager::TestSquare(const IObstructionTestFilter& filter, en
*/
static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
{
i = clamp((x / CELL_SIZE).ToInt_RoundToZero(), 0, w-1);
j = clamp((z / CELL_SIZE).ToInt_RoundToZero(), 0, h-1);
i = clamp((x / (int)CELL_SIZE).ToInt_RoundToZero(), 0, w-1);
j = clamp((z / (int)CELL_SIZE).ToInt_RoundToZero(), 0, h-1);
}
/**
* Returns the position of the center of the given tile
*/
static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i*(int)CELL_SIZE + CELL_SIZE/2);
z = entity_pos_t::FromInt(j*(int)CELL_SIZE + CELL_SIZE/2);
}
bool CCmpObstructionManager::Rasterise(Grid<u8>& grid)
@ -334,56 +396,132 @@ bool CCmpObstructionManager::Rasterise(Grid<u8>& grid)
grid.reset();
for (std::map<u32, Circle>::iterator it = m_Circles.begin(); it != m_Circles.end(); ++it)
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
// TODO: need to handle larger circles (r != 0)
u16 i, j;
NearestTile(it->second.x, it->second.z, i, j, grid.m_W, grid.m_H);
grid.set(i, j, 1);
}
CFixedVector2D center(it->second.x, it->second.z);
// Since we only count tiles whose centers are inside the square,
// we maybe want to expand the square a bit so we're less likely to think there's
// free space between buildings when there isn't. But this is just a random guess
// and needs to be tweaked until everything works nicely.
entity_pos_t expand = entity_pos_t::FromInt(CELL_SIZE / 2);
CFixedVector2D halfSize(it->second.hw + expand, it->second.hh + expand);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, halfSize);
for (std::map<u32, Square>::iterator it = m_Squares.begin(); it != m_Squares.end(); ++it)
{
// TODO: need to handle rotations (a != 0)
entity_pos_t x0 = it->second.x - it->second.w/2;
entity_pos_t z0 = it->second.z - it->second.h/2;
entity_pos_t x1 = it->second.x + it->second.w/2;
entity_pos_t z1 = it->second.z + it->second.h/2;
u16 i0, j0, i1, j1;
NearestTile(x0, z0, i0, j0, grid.m_W, grid.m_H); // TODO: should be careful about rounding on edges
NearestTile(x1, z1, i1, j1, grid.m_W, grid.m_H);
NearestTile(center.X - halfBound.X, center.Y - halfBound.Y, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + halfBound.X, center.Y + halfBound.Y, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
grid.set(i, j, 1);
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x, z) - center, it->second.u, it->second.v, halfSize))
grid.set(i, j, 1);
}
}
}
return true;
}
void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares)
{
// TODO: this should be made faster with quadtrees or whatever
PROFILE("GetObstructionsInRange");
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving))
continue;
entity_pos_t r = it->second.r;
// Skip this object if it's completely outside the requested range
if (it->second.x + r < x0 || it->second.x - r > x1 || it->second.z + r < z0 || it->second.z - r > z1)
continue;
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare s = { it->second.x, it->second.z, u, v, r, r };
squares.push_back(s);
}
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false))
continue;
entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center
// Skip this object if its overestimated bounding box is completely outside the requested range
if (it->second.x + r < x0 || it->second.x - r > x1 || it->second.z + r < z0 || it->second.z - r > z1)
continue;
// TODO: maybe we should use Geometry::GetHalfBoundingBox to be more precise?
ObstructionSquare s = { it->second.x, it->second.z, it->second.u, it->second.v, it->second.hw, it->second.hh };
squares.push_back(s);
}
}
bool CCmpObstructionManager::FindMostImportantObstruction(entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square)
{
std::vector<ObstructionSquare> squares;
CFixedVector2D center(x, z);
// First look for obstructions that are covering the exact target point
NullObstructionFilter filter;
GetObstructionsInRange(filter, x, z, x, z, squares);
// Building squares are more important but returned last, so check backwards
for (std::vector<ObstructionSquare>::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
{
CFixedVector2D halfSize(it->hw, it->hh);
if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
{
square = *it;
return true;
}
}
// Then look for obstructions that cover the target point when expanded by r
// (i.e. if the target is not inside an object but closer than we can get to it)
// TODO: actually do that
// (This might matter when you tell a unit to walk too close to the edge of a building)
return false;
}
void CCmpObstructionManager::RenderSubmit(const CSimContext& context, SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
CColor defaultColour(0, 0, 1, 1);
CColor movingColour(1, 0, 1, 1);
// If the shapes have changed, then regenerate all the overlays
if (m_DebugOverlayDirty)
{
m_DebugOverlayLines.clear();
for (std::map<u32, Circle>::iterator it = m_Circles.begin(); it != m_Circles.end(); ++it)
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = defaultColour;
SimRender::ConstructCircleOnGround(context, it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.r.ToFloat(), m_DebugOverlayLines.back());
m_DebugOverlayLines.back().m_Color = (it->second.moving ? movingColour : defaultColour);
SimRender::ConstructSquareOnGround(context, it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.r.ToFloat()*2, it->second.r.ToFloat()*2, 0, m_DebugOverlayLines.back());
}
for (std::map<u32, Square>::iterator it = m_Squares.begin(); it != m_Squares.end(); ++it)
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
{
m_DebugOverlayLines.push_back(SOverlayLine());
m_DebugOverlayLines.back().m_Color = defaultColour;
SimRender::ConstructSquareOnGround(context, it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.w.ToFloat(), it->second.h.ToFloat(), it->second.a.ToFloat(), m_DebugOverlayLines.back());
float a = atan2(it->second.v.X.ToFloat(), it->second.v.Y.ToFloat());
SimRender::ConstructSquareOnGround(context, it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back());
}
m_DebugOverlayDirty = false;

View File

@ -24,12 +24,23 @@
#include "ICmpObstructionManager.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/FixedVector2D.h"
#include "maths/MathUtil.h"
#include "ps/Overlay.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
#include "renderer/TerrainOverlay.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/helpers/Geometry.h"
/*
* Note this file contains two separate pathfinding implementations, the 'normal' tile-based
* one and the precise vertex-based 'short' pathfinder.
* They share a priority queue implementation but have independent A* implementations
* (with slightly different characteristics).
*/
#ifdef NDEBUG
#define PATHFIND_DEBUG 0
@ -42,6 +53,8 @@
class CCmpPathfinder;
struct PathfindTile;
typedef CFixed_23_8 fixed;
/**
* Terrain overlay for pathfinder debugging.
* Renders a representation of the most recent pathfinding operation.
@ -67,8 +80,9 @@ public:
class CCmpPathfinder : public ICmpPathfinder
{
public:
static void ClassInit(CComponentManager& UNUSED(componentManager))
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
}
DEFAULT_COMPONENT_ALLOCATOR(Pathfinder)
@ -84,6 +98,8 @@ public:
Path* m_DebugPath;
PathfinderOverlay* m_DebugOverlay;
std::vector<SOverlayLine> m_DebugOverlayShortPathLines;
static std::string GetSchema()
{
return "<a:component type='system'/><empty/>";
@ -123,10 +139,23 @@ public:
// TODO
}
virtual bool CanMoveStraight(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u32& cost);
virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
RenderSubmit(context, msgData.collector);
break;
}
}
}
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, Path& ret);
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, Path& ret);
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal)
{
if (!m_DebugOverlay)
@ -157,17 +186,17 @@ public:
*/
void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j)
{
i = clamp((x / CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
j = clamp((z / CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
i = clamp((x / (int)CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
j = clamp((z / (int)CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
}
/**
* Returns the position of the center of the given tile
*/
void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i*CELL_SIZE + CELL_SIZE/2);
z = entity_pos_t::FromInt(j*CELL_SIZE + CELL_SIZE/2);
x = entity_pos_t::FromInt(i*(int)CELL_SIZE + CELL_SIZE/2);
z = entity_pos_t::FromInt(j*(int)CELL_SIZE + CELL_SIZE/2);
}
/**
@ -191,6 +220,8 @@ public:
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
cmpObstructionManager->Rasterise(*m_Grid);
}
void RenderSubmit(const CSimContext& context, SceneCollector& collector);
};
REGISTER_COMPONENT_TYPE(Pathfinder)
@ -198,21 +229,6 @@ REGISTER_COMPONENT_TYPE(Pathfinder)
const u32 g_CostPerTile = 256; // base cost to move between adjacent tiles
bool CCmpPathfinder::CanMoveStraight(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u32& cost)
{
// Test whether there's a straight path
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
NullObstructionFilter filter;
if (!cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r))
return false;
// Calculate the exact movement cost
// (TODO: this needs to care about terrain costs etc)
cost = (CFixedVector2D(x1 - x0, z1 - z0).Length() * g_CostPerTile).ToInt_RoundToZero();
return true;
}
/**
* Tile data for A* computation
*/
@ -271,15 +287,10 @@ void PathfinderOverlay::ProcessTile(ssize_t i, ssize_t j)
* to the implementation shouldn't affect that interface much.
*/
struct QueueItem
{
u16 i, j;
u32 rank; // g+h (estimated total cost of path through here)
};
template <typename Item>
struct QueueItemPriority
{
bool operator()(const QueueItem& a, const QueueItem& b)
bool operator()(const Item& a, const Item& b)
{
if (a.rank > b.rank) // higher costs are lower priority
return true;
@ -287,13 +298,9 @@ struct QueueItemPriority
return false;
// Need to tie-break to get a consistent ordering
// TODO: Should probably tie-break on g or h or something, but don't bother for now
if (a.i < b.i)
if (a.id < b.id)
return true;
if (a.i > b.i)
return false;
if (a.j < b.j)
return true;
if (a.j > b.j)
if (b.id < a.id)
return false;
#if PATHFIND_DEBUG
debug_warn(L"duplicate tiles in queue");
@ -307,48 +314,55 @@ struct QueueItemPriority
* This is quite dreadfully slow in MSVC's debug STL implementation,
* so we shouldn't use it unless we reimplement the heap functions more efficiently.
*/
template <typename ID, typename R>
class PriorityQueueHeap
{
public:
void push(const QueueItem& item)
struct Item
{
ID id;
R rank; // f = g+h (estimated total cost of path through here)
};
void push(const Item& item)
{
m_Heap.push_back(item);
push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority());
push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item>());
}
QueueItem* find(u16 i, u16 j)
Item* find(ID id)
{
for (size_t n = 0; n < m_Heap.size(); ++n)
{
if (m_Heap[n].i == i && m_Heap[n].j == j)
if (m_Heap[n].id == id)
return &m_Heap[n];
}
return NULL;
}
void promote(u16 i, u16 j, u32 newrank)
void promote(ID id, u32 newrank)
{
for (size_t n = 0; n < m_Heap.size(); ++n)
{
if (m_Heap[n].i == i && m_Heap[n].j == j)
if (m_Heap[n].id == id)
{
#if PATHFIND_DEBUG
debug_assert(m_Heap[n].rank > newrank);
#endif
m_Heap[n].rank = newrank;
push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority());
push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority<Item>());
return;
}
}
}
QueueItem pop()
Item pop()
{
#if PATHFIND_DEBUG
debug_assert(m_Heap.size());
#endif
QueueItem r = m_Heap.front();
pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority());
Item r = m_Heap.front();
pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item>());
m_Heap.pop_back();
return r;
}
@ -363,7 +377,7 @@ public:
return m_Heap.size();
}
std::vector<QueueItem> m_Heap;
std::vector<Item> m_Heap;
};
/**
@ -373,41 +387,48 @@ public:
* It seems fractionally slower than a binary heap in optimised builds, but is
* much simpler and less susceptible to MSVC's painfully slow debug STL.
*/
template <typename ID, typename R>
class PriorityQueueList
{
public:
void push(const QueueItem& item)
struct Item
{
ID id;
R rank; // f = g+h (estimated total cost of path through here)
};
void push(const Item& item)
{
m_List.push_back(item);
}
QueueItem* find(u16 i, u16 j)
Item* find(ID id)
{
for (size_t n = 0; n < m_List.size(); ++n)
{
if (m_List[n].i == i && m_List[n].j == j)
if (m_List[n].id == id)
return &m_List[n];
}
return NULL;
}
void promote(u16 i, u16 j, u32 newrank)
void promote(ID id, R newrank)
{
find(i, j)->rank = newrank;
find(id)->rank = newrank;
}
QueueItem pop()
Item pop()
{
#if PATHFIND_DEBUG
debug_assert(m_List.size());
#endif
// Loop backwards looking for the best (it's most likely to be one
// we've recently pushed, so going backwards saves a bit of copying)
QueueItem best = m_List.back();
Item best = m_List.back();
size_t bestidx = m_List.size()-1;
for (ssize_t i = (ssize_t)bestidx-1; i >= 0; --i)
{
if (QueueItemPriority()(best, m_List[i]))
if (QueueItemPriority<Item>()(best, m_List[i]))
{
bestidx = i;
best = m_List[i];
@ -429,17 +450,17 @@ public:
return m_List.size();
}
std::vector<QueueItem> m_List;
std::vector<Item> m_List;
};
typedef PriorityQueueList PriorityQueue;
typedef PriorityQueueList<std::pair<u16, u16>, u32> PriorityQueue;
#define USE_DIAGONAL_MOVEMENT
// Calculate heuristic cost from tile i,j to destination
// (This ought to be an underestimate for correctness)
static u32 CalculateHeuristic(u16 i, u16 j, u16 iGoal, u16 jGoal, u16 rGoal, bool aimingInwards)
static u32 CalculateHeuristic(u16 i, u16 j, u16 iGoal, u16 jGoal, u16 rGoal)
{
#ifdef USE_DIAGONAL_MOVEMENT
CFixedVector2D pos (CFixed_23_8::FromInt(i), CFixed_23_8::FromInt(j));
@ -448,12 +469,9 @@ static u32 CalculateHeuristic(u16 i, u16 j, u16 iGoal, u16 jGoal, u16 rGoal, boo
// TODO: the heuristic could match the costs better - it's not really Euclidean movement
CFixed_23_8 rdist = dist - CFixed_23_8::FromInt(rGoal);
if (!aimingInwards)
rdist = -rdist;
rdist = rdist.Absolute();
if (rdist < CFixed_23_8::FromInt(0))
return 0;
return (rdist * g_CostPerTile).ToInt_RoundToZero();
return (rdist * (int)g_CostPerTile).ToInt_RoundToZero();
#else
return (abs((int)i - (int)iGoal) + abs((int)j - (int)jGoal)) * g_CostPerTile;
@ -497,7 +515,6 @@ struct PathfinderState
u16 iGoal, jGoal; // goal tile
u16 rGoal; // radius of goal (around tile center)
bool aimingInwards; // whether we're moving towards the goal or away
PriorityQueue open;
// (there's no explicit closed list; it's encoded in PathfindTile::status)
@ -538,7 +555,7 @@ static void ProcessNeighbour(u16 pi, u16 pj, u16 i, u16 j, u32 pg, PathfinderSta
// If this is a new tile, compute the heuristic distance
if (n.status == PathfindTile::STATUS_UNEXPLORED)
{
n.h = CalculateHeuristic(i, j, state.iGoal, state.jGoal, state.rGoal, state.aimingInwards);
n.h = CalculateHeuristic(i, j, state.iGoal, state.jGoal, state.rGoal);
// Remember the best tile we've seen so far, in case we never actually reach the target
if (n.h < state.hBest)
{
@ -564,7 +581,7 @@ static void ProcessNeighbour(u16 pi, u16 pj, u16 i, u16 j, u32 pg, PathfinderSta
n.pi = pi;
n.pj = pj;
n.step = state.steps;
state.open.promote(i, j, g + n.h);
state.open.promote(std::make_pair(i, j), g + n.h);
#if PATHFIND_STATS
state.numImproveOpen++;
#endif
@ -588,27 +605,46 @@ static void ProcessNeighbour(u16 pi, u16 pj, u16 i, u16 j, u32 pg, PathfinderSta
n.pi = pi;
n.pj = pj;
n.step = state.steps;
QueueItem t = { i, j, g + n.h };
PriorityQueue::Item t = { std::make_pair(i, j), g + n.h };
state.open.push(t);
#if PATHFIND_STATS
state.numAddToOpen++;
#endif
}
static bool AtGoal(u16 i, u16 j, u16 iGoal, u16 jGoal, u16 rGoal, bool aimingInwards)
static fixed DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
// If we're aiming towards a point, stop when we get there
if (aimingInwards && rGoal == 0)
return (i == iGoal && j == jGoal);
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
return (pos - CFixedVector2D(goal.x, goal.z)).Length();
// Otherwise compute the distance and compare to desired radius
i32 dist2 = ((i32)i-iGoal)*((i32)i-iGoal) + ((i32)j-jGoal)*((i32)j-jGoal);
if (aimingInwards && (dist2 <= rGoal*rGoal))
return true;
if (!aimingInwards && (dist2 >= rGoal*rGoal))
return true;
case CCmpPathfinder::Goal::CIRCLE:
return ((pos - CFixedVector2D(goal.x, goal.z)).Length() - goal.hw).Absolute();
return false;
case CCmpPathfinder::Goal::SQUARE:
{
CFixedVector2D halfSize(goal.hw, goal.hh);
CFixedVector2D d(pos.X - goal.x, pos.Y - goal.z);
return Geometry::DistanceToSquare(d, goal.u, goal.v, halfSize);
}
default:
debug_warn(L"invalid type");
return fixed::Zero();
}
}
static bool AtGoal(u16 i, u16 j, const ICmpPathfinder::Goal& goal)
{
// Allow tiles slightly more than sqrt(2) from the actual goal,
// i.e. adjacent diagonally to the target tile
fixed tolerance = entity_pos_t::FromInt(CELL_SIZE*3/2);
entity_pos_t x, z;
CCmpPathfinder::TileCenter(i, j, x, z);
fixed dist = DistanceToGoal(CFixedVector2D(x, z), goal);
return (dist < tolerance);
}
void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, Path& path)
@ -624,33 +660,22 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
NearestTile(x0, z0, i0, j0);
NearestTile(goal.x, goal.z, state.iGoal, state.jGoal);
// If we start closer than min radius, aim for the min radius
// If we start further than max radius, aim for the max radius
// Otherwise we're there already
CFixed_23_8 initialDist = (CFixedVector2D(x0, z0) - CFixedVector2D(goal.x, goal.z)).Length();
if (initialDist < goal.minRadius)
{
state.aimingInwards = false;
state.rGoal = (goal.minRadius / CELL_SIZE).ToInt_RoundToZero(); // TODO: what rounding mode is appropriate?
}
else if (initialDist > goal.maxRadius)
{
state.aimingInwards = true;
state.rGoal = (goal.maxRadius / CELL_SIZE).ToInt_RoundToZero(); // TODO: what rounding mode is appropriate?
}
else
{
return;
}
// If we're already at the goal tile, then move directly to the exact goal coordinates
if (AtGoal(i0, j0, state.iGoal, state.jGoal, state.rGoal, state.aimingInwards))
if (AtGoal(i0, j0, goal))
{
Waypoint w = { goal.x, goal.z, 0 };
Waypoint w = { goal.x, goal.z };
path.m_Waypoints.push_back(w);
return;
}
// If the target is a circle, we want to aim for the edge of it (so e.g. if we're inside
// a large circle then the heuristics will aim us directly outwards);
// otherwise just aim at the center point. (We'll never try moving outwards to a square shape.)
if (goal.type == Goal::CIRCLE)
state.rGoal = (goal.hw / (int)CELL_SIZE).ToInt_RoundToZero();
else
state.rGoal = 0;
state.steps = 0;
state.tiles = new Grid<PathfindTile>(m_MapSize, m_MapSize);
@ -658,9 +683,9 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
state.iBest = i0;
state.jBest = j0;
state.hBest = CalculateHeuristic(i0, j0, state.iGoal, state.jGoal, state.rGoal, state.aimingInwards);
state.hBest = CalculateHeuristic(i0, j0, state.iGoal, state.jGoal, state.rGoal);
QueueItem start = { i0, j0, 0 };
PriorityQueue::Item start = { std::make_pair(i0, j0), 0 };
state.open.push(start);
state.tiles->get(i0, j0).status = PathfindTile::STATUS_OPEN;
state.tiles->get(i0, j0).pi = i0;
@ -685,27 +710,29 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
#endif
// Move best tile from open to closed
QueueItem curr = state.open.pop();
state.tiles->get(curr.i, curr.j).status = PathfindTile::STATUS_CLOSED;
PriorityQueue::Item curr = state.open.pop();
u16 i = curr.id.first;
u16 j = curr.id.second;
state.tiles->get(i, j).status = PathfindTile::STATUS_CLOSED;
// If we've reached the destination, stop
if (AtGoal(curr.i, curr.j, state.iGoal, state.jGoal, state.rGoal, state.aimingInwards))
if (AtGoal(i, j, goal))
{
state.iBest = curr.i;
state.jBest = curr.j;
state.iBest = i;
state.jBest = j;
state.hBest = 0;
break;
}
u32 g = state.tiles->get(curr.i, curr.j).cost;
if (curr.i > 0)
ProcessNeighbour(curr.i, curr.j, curr.i-1, curr.j, g, state);
if (curr.i < m_MapSize-1)
ProcessNeighbour(curr.i, curr.j, curr.i+1, curr.j, g, state);
if (curr.j > 0)
ProcessNeighbour(curr.i, curr.j, curr.i, curr.j-1, g, state);
if (curr.j < m_MapSize-1)
ProcessNeighbour(curr.i, curr.j, curr.i, curr.j+1, g, state);
u32 g = state.tiles->get(i, j).cost;
if (i > 0)
ProcessNeighbour(i, j, i-1, j, g, state);
if (i < m_MapSize-1)
ProcessNeighbour(i, j, i+1, j, g, state);
if (j > 0)
ProcessNeighbour(i, j, i, j-1, g, state);
if (j < m_MapSize-1)
ProcessNeighbour(i, j, i, j+1, g, state);
}
// Reconstruct the path (in reverse)
@ -713,18 +740,9 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
while (ip != i0 || jp != j0)
{
PathfindTile& n = state.tiles->get(ip, jp);
// Pick the exact point if it's the goal tile, else the tile's centre
entity_pos_t x, z;
if (ip == state.iGoal && jp == state.jGoal)
{
x = goal.x;
z = goal.z;
}
else
{
TileCenter(ip, jp, x, z);
}
Waypoint w = { x, z, n.cost };
TileCenter(ip, jp, x, z);
Waypoint w = { x, z };
path.m_Waypoints.push_back(w);
// Follow the predecessor link
@ -741,3 +759,359 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
printf("PATHFINDER: steps=%d avgo=%d proc=%d impc=%d impo=%d addo=%d\n", state.steps, state.sumOpenSize/state.steps, state.numProcessed, state.numImproveClosed, state.numImproveOpen, state.numAddToOpen);
#endif
}
//////////////////////////////////////////////////////////
struct Vertex
{
CFixedVector2D p;
fixed g, h;
u16 pred;
enum
{
UNEXPLORED,
OPEN,
CLOSED,
} status;
};
struct Edge
{
CFixedVector2D p0, p1;
};
/**
* Check whether a ray from 'a' to 'b' crosses any of the edges.
* (Edges are one-sided so it's only considered a cross if going from front to back.)
*/
static bool CheckVisibility(CFixedVector2D a, CFixedVector2D b, const std::vector<Edge>& edges)
{
CFixedVector2D abn = (b - a).Perpendicular();
for (size_t i = 0; i < edges.size(); ++i)
{
CFixedVector2D d = (edges[i].p1 - edges[i].p0).Perpendicular();
// If 'a' is behind the edge, we can't cross
fixed q = (a - edges[i].p0).Dot(d);
if (q < fixed::Zero())
continue;
// If 'b' is in front of the edge, we can't cross
fixed r = (b - edges[i].p0).Dot(d);
if (r > fixed::Zero())
continue;
// The ray is crossing the infinitely-extended edge from in front to behind.
// If the edge's points are the same side of the infinitely-extended ray
// then the finite lines can't intersect, otherwise they're crossing
fixed s = (edges[i].p0 - a).Dot(abn);
fixed t = (edges[i].p1 - a).Dot(abn);
if ((s <= fixed::Zero() && t >= fixed::Zero()) || (s >= fixed::Zero() && t <= fixed::Zero()))
return false;
}
return true;
}
static CFixedVector2D NearestPointOnGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
CFixedVector2D g(goal.x, goal.z);
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
{
return g;
}
case CCmpPathfinder::Goal::CIRCLE:
{
CFixedVector2D d = pos - g;
if (d.IsZero())
d = CFixedVector2D(fixed::FromInt(1), fixed::Zero()); // some arbitrary direction
d.Normalize(goal.hw);
return g + d;
}
case CCmpPathfinder::Goal::SQUARE:
{
CFixedVector2D halfSize(goal.hw, goal.hh);
CFixedVector2D d = pos - g;
return g + Geometry::NearestPointOnSquare(d, goal.u, goal.v, halfSize);
}
default:
debug_warn(L"invalid type");
return CFixedVector2D();
}
}
typedef PriorityQueueList<u16, fixed> ShortPathPriorityQueue;
void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, Path& path)
{
PROFILE("ComputeShortPath");
m_DebugOverlayShortPathLines.clear();
if (m_DebugOverlay)
{
// Render the goal shape
m_DebugOverlayShortPathLines.push_back(SOverlayLine());
m_DebugOverlayShortPathLines.back().m_Color = CColor(1, 0, 0, 1);
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
{
SimRender::ConstructCircleOnGround(*m_Context, goal.x.ToFloat(), goal.z.ToFloat(), 0.2f, m_DebugOverlayShortPathLines.back());
break;
}
case CCmpPathfinder::Goal::CIRCLE:
{
SimRender::ConstructCircleOnGround(*m_Context, goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat(), m_DebugOverlayShortPathLines.back());
break;
}
case CCmpPathfinder::Goal::SQUARE:
{
float a = atan2(goal.v.X.ToFloat(), goal.v.Y.ToFloat());
SimRender::ConstructSquareOnGround(*m_Context, goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat()*2, goal.hh.ToFloat()*2, a, m_DebugOverlayShortPathLines.back());
break;
}
}
}
// List of collision edges - paths must never cross these.
// (Edges are one-sided so intersections are fine in one direction, but not the other direction.)
std::vector<Edge> edges;
// Create impassable edges at the max-range boundary, so we can't escape the region
// where we're meant to be searching
fixed rangeXMin = x0 - range;
fixed rangeXMax = x0 + range;
fixed rangeZMin = z0 - range;
fixed rangeZMax = z0 + range;
{
// (The edges are the opposite direction to usual, so it's an inside-out square)
Edge e0 = { CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) };
Edge e1 = { CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) };
Edge e2 = { CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) };
Edge e3 = { CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) };
edges.push_back(e0);
edges.push_back(e1);
edges.push_back(e2);
edges.push_back(e3);
}
CFixedVector2D goalVec(goal.x, goal.z);
// List of obstruction vertexes (plus start/end points); we'll try to find paths through
// the graph defined by these vertexes
std::vector<Vertex> vertexes;
// Add the start point to the graph
Vertex start = { CFixedVector2D(x0, z0), fixed::Zero(), (CFixedVector2D(x0, z0) - goalVec).Length(), 0, Vertex::OPEN };
vertexes.push_back(start);
const size_t START_VERTEX_ID = 0;
// Add the goal vertex to the graph.
// Since the goal isn't always a point, this a special magic virtual vertex which moves around - whenever
// we look at it from another vertex, it is moved to be the closest point on the goal shape to that vertex.
Vertex end = { CFixedVector2D(goal.x, goal.z), fixed::Zero(), fixed::Zero(), 0, Vertex::UNEXPLORED };
vertexes.push_back(end);
const size_t GOAL_VERTEX_ID = 1;
// Find all the obstruction squares that might affect us
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
std::vector<ICmpObstructionManager::ObstructionSquare> squares;
cmpObstructionManager->GetObstructionsInRange(filter, rangeXMin - r, rangeZMin - r, rangeXMax + r, rangeZMax + r, squares);
// Resize arrays to reduce reallocations
vertexes.reserve(vertexes.size() + squares.size()*4);
edges.reserve(edges.size() + squares.size()*4);
// Convert each obstruction square into collision edges and search graph vertexes
for (size_t i = 0; i < squares.size(); ++i)
{
CFixedVector2D center(squares[i].x, squares[i].z);
CFixedVector2D u = squares[i].u;
CFixedVector2D v = squares[i].v;
// Expand the vertexes by the moving unit's collision radius, to find the
// closest we can get to it
entity_pos_t delta = entity_pos_t::FromInt(1)/4;
// add a small delta so that the vertexes of an edge don't get interpreted
// as crossing the edge (given minor numerical inaccuracies)
CFixedVector2D hd0(squares[i].hw + r + delta, squares[i].hh + r + delta);
CFixedVector2D hd1(squares[i].hw + r + delta, -(squares[i].hh + r + delta));
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v); vertexes.push_back(vert);
vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v); vertexes.push_back(vert);
vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v); vertexes.push_back(vert);
vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v); vertexes.push_back(vert);
// Add the four edges
CFixedVector2D h0(squares[i].hw + r, squares[i].hh + r);
CFixedVector2D h1(squares[i].hw + r, -(squares[i].hh + r));
CFixedVector2D ev0(center.X - h0.Dot(u), center.Y + h0.Dot(v));
CFixedVector2D ev1(center.X - h1.Dot(u), center.Y + h1.Dot(v));
CFixedVector2D ev2(center.X + h0.Dot(u), center.Y - h0.Dot(v));
CFixedVector2D ev3(center.X + h1.Dot(u), center.Y - h1.Dot(v));
Edge e0 = { ev0, ev1 };
Edge e1 = { ev1, ev2 };
Edge e2 = { ev2, ev3 };
Edge e3 = { ev3, ev0 };
edges.push_back(e0);
edges.push_back(e1);
edges.push_back(e2);
edges.push_back(e3);
// TODO: should clip out vertexes and edges that are outside the range,
// to reduce the search space
}
debug_assert(vertexes.size() < 65536); // we store array indexes as u16
if (m_DebugOverlay)
{
// Render the obstruction edges
for (size_t i = 0; i < edges.size(); ++i)
{
m_DebugOverlayShortPathLines.push_back(SOverlayLine());
m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1);
std::vector<float> xz;
xz.push_back(edges[i].p0.X.ToFloat());
xz.push_back(edges[i].p0.Y.ToFloat());
xz.push_back(edges[i].p1.X.ToFloat());
xz.push_back(edges[i].p1.Y.ToFloat());
SimRender::ConstructLineOnGround(*m_Context, xz, m_DebugOverlayShortPathLines.back());
}
}
// Do an A* search over the vertex/visibility graph:
// Since we are just measuring Euclidean distance the heuristic is admissible,
// so we never have to re-examine a node once it's been moved to the closed set.
// To save time in common cases, we don't precompute a graph of valid edges between vertexes;
// we do it lazily instead. When the search algorithm reaches a vertex, we examine every other
// vertex and see if we can reach it without hitting any collision edges, and ignore the ones
// we can't reach. Since the algorithm can only reach a vertex once (and then it'll be marked
// as closed), we won't be doing any redundant visibility computations.
PROFILE_START("A*");
ShortPathPriorityQueue open;
ShortPathPriorityQueue::Item qiStart = { START_VERTEX_ID, start.h };
open.push(qiStart);
u16 idBest = START_VERTEX_ID;
fixed hBest = start.h;
while (!open.empty())
{
// Move best tile from open to closed
ShortPathPriorityQueue::Item curr = open.pop();
vertexes[curr.id].status = Vertex::CLOSED;
// If we've reached the destination, stop
if (curr.id == GOAL_VERTEX_ID)
{
idBest = curr.id;
break;
}
for (size_t n = 0; n < vertexes.size(); ++n)
{
if (vertexes[n].status == Vertex::CLOSED)
continue;
// If this is the magical goal vertex, move it to near the current vertex
CFixedVector2D npos;
if (n == GOAL_VERTEX_ID)
npos = NearestPointOnGoal(vertexes[curr.id].p, goal);
else
npos = vertexes[n].p;
bool visible = CheckVisibility(vertexes[curr.id].p, npos, edges);
/*
// Render the edges that we examine
m_DebugOverlayShortPathLines.push_back(SOverlayLine());
m_DebugOverlayShortPathLines.back().m_Color = visible ? CColor(0, 1, 0, 1) : CColor(0, 0, 0, 1);
std::vector<float> xz;
xz.push_back(vertexes[curr.id].p.X.ToFloat());
xz.push_back(vertexes[curr.id].p.Y.ToFloat());
xz.push_back(npos.X.ToFloat());
xz.push_back(npos.Y.ToFloat());
SimRender::ConstructLineOnGround(*m_Context, xz, m_DebugOverlayShortPathLines.back());
//*/
if (visible)
{
fixed g = vertexes[curr.id].g + (vertexes[curr.id].p - npos).Length();
// If this is a new tile, compute the heuristic distance
if (vertexes[n].status == Vertex::UNEXPLORED)
{
// Add it to the open list:
vertexes[n].status = Vertex::OPEN;
vertexes[n].g = g;
vertexes[n].h = DistanceToGoal(npos, goal);
vertexes[n].pred = curr.id;
if (n == GOAL_VERTEX_ID)
vertexes[n].p = npos; // remember the new best goal position
ShortPathPriorityQueue::Item t = { n, g + vertexes[n].h };
open.push(t);
// Remember the heuristically best vertex we've seen so far, in case we never actually reach the target
if (vertexes[n].h < hBest)
{
idBest = n;
hBest = vertexes[n].h;
}
}
else // must be OPEN
{
// If we've already seen this tile, and the new path to this tile does not have a
// better cost, then stop now
if (g >= vertexes[n].g)
continue;
// Otherwise, we have a better path, so replace the old one with the new cost/parent
vertexes[n].g = g;
vertexes[n].pred = curr.id;
if (n == GOAL_VERTEX_ID)
vertexes[n].p = npos; // remember the new best goal position
open.promote((u16)n, g + vertexes[n].h);
continue;
}
}
}
}
// Reconstruct the path (in reverse)
for (u16 id = idBest; id != START_VERTEX_ID; id = vertexes[id].pred)
{
Waypoint w = { vertexes[id].p.X, vertexes[id].p.Y };
path.m_Waypoints.push_back(w);
}
PROFILE_END("A*");
}
//////////////////////////////////////////////////////////
void CCmpPathfinder::RenderSubmit(const CSimContext& context, SceneCollector& collector)
{
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
}

View File

@ -73,8 +73,6 @@ public:
entity_angle_t m_RotX, m_RotY, m_RotZ;
float m_InterpolatedRotY; // not serialized
bool m_Dirty; // true if position/rotation has changed since last TurnStart
static std::string GetSchema()
{
return
@ -120,8 +118,6 @@ public:
m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0);
m_InterpolatedRotY = 0;
m_Dirty = false;
}
virtual void Deinit(const CSimContext& UNUSED(context))
@ -144,7 +140,6 @@ public:
serialize.NumberFixed_Unbounded("rot y", m_RotY);
serialize.NumberFixed_Unbounded("rot z", m_RotZ);
serialize.NumberFixed_Unbounded("altitude", m_YOffset);
serialize.Bool("dirty", m_Dirty);
if (serialize.IsDebug())
{
@ -176,7 +171,6 @@ public:
deserialize.NumberFixed_Unbounded(m_RotY);
deserialize.NumberFixed_Unbounded(m_RotZ);
deserialize.NumberFixed_Unbounded(m_YOffset);
deserialize.Bool(m_Dirty);
// TODO: should there be range checks on all these values?
m_InterpolatedRotY = m_RotY.ToFloat();
@ -191,7 +185,7 @@ public:
{
m_InWorld = false;
m_Dirty = true;
AdvertisePositionChanges();
}
virtual void MoveTo(entity_pos_t x, entity_pos_t z)
@ -206,7 +200,7 @@ public:
m_LastZ = m_Z;
}
m_Dirty = true;
AdvertisePositionChanges();
}
virtual void JumpTo(entity_pos_t x, entity_pos_t z)
@ -215,14 +209,14 @@ public:
m_LastZ = m_Z = z;
m_InWorld = true;
m_Dirty = true;
AdvertisePositionChanges();
}
virtual void SetHeightOffset(entity_pos_t dy)
{
m_YOffset = dy;
m_Dirty = true;
AdvertisePositionChanges();
}
virtual entity_pos_t GetHeightOffset()
@ -256,7 +250,7 @@ public:
{
m_RotY = y;
m_Dirty = true;
AdvertisePositionChanges();
}
virtual void SetYRotation(entity_angle_t y)
@ -264,7 +258,7 @@ public:
m_RotY = y;
m_InterpolatedRotY = m_RotY.ToFloat();
m_Dirty = true;
AdvertisePositionChanges();
}
virtual void SetXZRotation(entity_angle_t x, entity_angle_t z)
@ -272,7 +266,7 @@ public:
m_RotX = x;
m_RotZ = z;
m_Dirty = true;
AdvertisePositionChanges();
}
virtual CFixedVector3D GetRotation()
@ -366,25 +360,26 @@ public:
{
m_LastX = m_X;
m_LastZ = m_Z;
if (m_Dirty)
{
if (m_InWorld)
{
CMessagePositionChanged msg(true, m_X, m_Z, m_RotY);
context.GetComponentManager().PostMessage(GetEntityId(), msg);
}
else
{
CMessagePositionChanged msg(false, entity_pos_t(), entity_pos_t(), entity_angle_t());
context.GetComponentManager().PostMessage(GetEntityId(), msg);
}
m_Dirty = false;
}
break;
}
}
}
private:
void AdvertisePositionChanges()
{
if (m_InWorld)
{
CMessagePositionChanged msg(true, m_X, m_Z, m_RotY);
m_Context->GetComponentManager().PostMessage(GetEntityId(), msg);
}
else
{
CMessagePositionChanged msg(false, entity_pos_t(), entity_pos_t(), entity_angle_t());
m_Context->GetComponentManager().PostMessage(GetEntityId(), msg);
}
}
};
REGISTER_COMPONENT_TYPE(Position)

View File

@ -59,7 +59,7 @@ public:
virtual CFixedVector3D CalcNormal(entity_pos_t x, entity_pos_t z)
{
CFixedVector3D normal;
m_Terrain->CalcNormalFixed((x / CELL_SIZE).ToInt_RoundToZero(), (z / CELL_SIZE).ToInt_RoundToZero(), normal);
m_Terrain->CalcNormalFixed((x / (int)CELL_SIZE).ToInt_RoundToZero(), (z / (int)CELL_SIZE).ToInt_RoundToZero(), normal);
return normal;
}

View File

@ -20,9 +20,27 @@
#include "simulation2/system/Component.h"
#include "ICmpUnitMotion.h"
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "ICmpPathfinder.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/FixedVector2D.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
static const entity_pos_t WAYPOINT_ADVANCE_MIN = entity_pos_t::FromInt(CELL_SIZE*4);
static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(CELL_SIZE*8);
static const entity_pos_t SHORT_PATH_SEARCH_RANGE = entity_pos_t::FromInt(CELL_SIZE*12);
static const CColor OVERLAY_COLOUR_PATH(1, 1, 1, 1);
static const CColor OVERLAY_COLOUR_PATH_ACTIVE(1, 1, 0, 1);
static const CColor OVERLAY_COLOUR_SHORT_PATH(1, 0, 0, 1);
class CCmpUnitMotion : public ICmpUnitMotion
{
@ -30,23 +48,30 @@ public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
const CSimContext* m_Context;
bool m_DebugOverlayEnabled;
std::vector<SOverlayLine> m_DebugOverlayLines;
std::vector<SOverlayLine> m_DebugOverlayShortPathLines;
// Template state:
CFixed_23_8 m_Speed; // in units per second
entity_pos_t m_Radius;
// Dynamic state:
bool m_HasTarget;
ICmpPathfinder::Path m_Path;
bool m_HasTarget; // whether we currently have valid paths and targets
// These values contain undefined junk if !HasTarget:
entity_pos_t m_TargetX, m_TargetZ; // currently-selected waypoint
entity_pos_t m_FinalTargetX, m_FinalTargetZ; // final target center (used to face towards it)
ICmpPathfinder::Path m_Path;
ICmpPathfinder::Path m_ShortPath;
entity_pos_t m_ShortTargetX, m_ShortTargetZ;
ICmpPathfinder::Goal m_FinalGoal;
enum
{
@ -75,7 +100,13 @@ public:
m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
CmpPtr<ICmpObstruction> cmpObstruction(context, GetEntityId());
if (!cmpObstruction.null())
m_Radius = cmpObstruction->GetUnitRadius();
m_State = IDLE;
m_DebugOverlayEnabled = false;
}
virtual void Deinit(const CSimContext& UNUSED(context))
@ -88,8 +119,6 @@ public:
if (m_HasTarget)
{
// TODO: m_Path
serialize.NumberFixed_Unbounded("target x", m_TargetX);
serialize.NumberFixed_Unbounded("target z", m_TargetZ);
// TODO: m_FinalTargetAngle
}
@ -103,8 +132,6 @@ public:
deserialize.Bool(m_HasTarget);
if (m_HasTarget)
{
deserialize.NumberFixed_Unbounded(m_TargetX);
deserialize.NumberFixed_Unbounded(m_TargetZ);
}
}
@ -127,92 +154,12 @@ public:
break;
}
}
}
void SwitchState(const CSimContext& context, int state)
{
debug_assert(state == IDLE || state == WALKING);
// IDLE -> IDLE -- no change
// IDLE -> WALKING -- send a MotionChanged message
// WALKING -> IDLE -- set to STOPPING, so we'll send MotionChanged in the next Update
// WALKING -> WALKING -- no change
// STOPPING -> IDLE -- stay in STOPPING
// STOPPING -> WALKING -- set to WALKING, send no messages
if (m_State == IDLE && state == WALKING)
case MT_RenderSubmit:
{
CMessageMotionChanged msg(m_Speed);
context.GetComponentManager().PostMessage(GetEntityId(), msg);
m_State = WALKING;
return;
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
RenderSubmit(context, msgData.collector);
break;
}
if (m_State == WALKING && state == IDLE)
{
m_State = STOPPING;
return;
}
if (m_State == STOPPING && state == IDLE)
{
return;
}
if (m_State == STOPPING && state == WALKING)
{
m_State = WALKING;
return;
}
}
virtual void MoveToPoint(entity_pos_t x, entity_pos_t z, entity_pos_t minRadius, entity_pos_t maxRadius)
{
CmpPtr<ICmpPathfinder> cmpPathfinder (*m_Context, SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null())
return;
SwitchState(*m_Context, WALKING);
CFixedVector3D pos = cmpPosition->GetPosition();
m_Path.m_Waypoints.clear();
// u32 cost;
// entity_pos_t r = entity_pos_t::FromInt(0); // TODO: should get this from the entity's size
// if (cmpPathfinder->CanMoveStraight(pos.X, pos.Z, x, z, r, cost))
// {
// m_TargetX = x;
// m_TargetZ = z;
// m_HasTarget = true;
// }
// else
{
ICmpPathfinder::Goal goal;
goal.x = x;
goal.z = z;
goal.minRadius = minRadius;
goal.maxRadius = maxRadius;
cmpPathfinder->SetDebugPath(pos.X, pos.Z, goal);
cmpPathfinder->ComputePath(pos.X, pos.Z, goal, m_Path);
// If there's no waypoints then we've stopped already, otherwise move to the first one
if (m_Path.m_Waypoints.empty())
{
m_HasTarget = false;
SwitchState(*m_Context, IDLE);
}
else
{
m_FinalTargetX = x;
m_FinalTargetZ = z;
PickNextWaypoint(pos);
}
}
}
@ -221,16 +168,121 @@ public:
return m_Speed;
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
if (enabled)
{
RenderPath(*m_Context, m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
RenderPath(*m_Context, m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
}
}
virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z);
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
private:
/**
* Check whether moving from pos to target is safe (won't hit anything).
* If safe, returns true (the caller should do cmpPosition->MoveTo).
* Otherwise returns false, and either computes a new path to use on the
* next turn or makes the unit stop.
*/
bool CheckMovement(CFixedVector2D pos, CFixedVector2D target);
/**
* Do the per-turn movement and other updates
*/
void Move(const CSimContext& context, CFixed_23_8 dt);
void PickNextWaypoint(const CFixedVector3D& pos);
void StopAndFaceGoal(CFixedVector2D pos);
/**
* Rotate to face towards the target point, given the current pos
*/
void FaceTowardsPoint(CFixedVector2D pos, entity_pos_t x, entity_pos_t z);
/**
* Change between idle/walking states; automatically sends MotionChanged messages when appropriate
*/
void SwitchState(const CSimContext& context, int state);
bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius);
/**
* Recompute the whole path to the current goal.
* Returns false on error or if the unit can't move anywhere at all.
*/
bool RegeneratePath(CFixedVector2D pos, bool avoidMovingUnits);
/**
* Maybe select a new long waypoint if we're getting too close to the
* current one.
*/
void MaybePickNextWaypoint(const CFixedVector2D& pos);
/**
* Select a next long waypoint, given the current unit position.
* Also recomputes the short path to use that waypoint.
* Returns false on error, or if there is no waypoint to pick.
*/
bool PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits);
/**
* Select a new short waypoint as the current target,
* which possibly involves first selecting a new long waypoint.
* Returns false on error, or if there is no waypoint to pick.
*/
bool PickNextShortWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits);
/**
* Convert a path into a renderable list of lines
*/
void RenderPath(const CSimContext& context, const ICmpPathfinder::Path& path, std::vector<SOverlayLine>& lines, CColor color);
void RenderSubmit(const CSimContext& context, SceneCollector& collector);
};
REGISTER_COMPONENT_TYPE(UnitMotion)
bool CCmpUnitMotion::CheckMovement(CFixedVector2D pos, CFixedVector2D target)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
NullObstructionFilter filter;
entity_pos_t delta = entity_pos_t::FromInt(1) / 8;
// add a small delta so that we don't get so close that the pathfinder thinks
// we've actually crossed the edge (given minor numerical inaccuracies)
// TODO: keep this in sync with CCmpPathfinder::ComputeShortPath delta
// (this value needs to be smaller)
// TODO: work out what this value should actually be, rather than just guessing
if (cmpObstructionManager->TestLine(filter, pos.X, pos.Y, target.X, target.Y, m_Radius + delta))
{
// Oops, hit something
// TODO: we ought to wait for obstructions to move away instead of immediately throwing away the whole path
// TODO: actually a whole proper collision resolution thing needs to be designed and written
if (!RegeneratePath(pos, true))
{
// Oh dear, we can't find the path any more; give up
StopAndFaceGoal(pos);
return false;
}
// Wait for the next Update before we try moving again
return false;
}
return true;
}
void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
{
PROFILE("Move");
if (!m_HasTarget)
return;
@ -238,86 +290,591 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt)
if (cmpPosition.null())
return;
CFixedVector3D pos = cmpPosition->GetPosition();
pos.Y = CFixed_23_8::FromInt(0); // remove Y so it doesn't influence our distance calculations
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// We want to move (at most) m_Speed*dt units from pos towards the next waypoint
while (dt > CFixed_23_8::FromInt(0))
{
CFixedVector3D target(m_TargetX, CFixed_23_8::FromInt(0), m_TargetZ);
CFixedVector3D offset = target - pos;
CFixedVector2D target(m_ShortTargetX, m_ShortTargetZ);
CFixedVector2D offset = target - pos;
// Face towards the target
entity_angle_t angle = atan2_approx(offset.X, offset.Z);
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
cmpPosition->TurnTo(angle);
// Work out how far we can travel in dt
CFixed_23_8 maxdist = m_Speed.Multiply(dt);
// If the target is close, we can move there directly
if (offset.Length() <= maxdist)
CFixed_23_8 offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
// If we've reached the last waypoint, stop
if (m_Path.m_Waypoints.empty())
{
cmpPosition->MoveTo(target.X, target.Z);
// If we didn't reach the final goal, point towards it now
if (target.X != m_FinalTargetX || target.Z != m_FinalTargetZ)
{
CFixedVector3D final(m_FinalTargetX, CFixed_23_8::FromInt(0), m_FinalTargetZ);
CFixedVector3D finalOffset = final - target;
entity_angle_t angle = atan2_approx(finalOffset.X, finalOffset.Z);
cmpPosition->TurnTo(angle);
}
m_HasTarget = false;
SwitchState(context, IDLE);
if (!CheckMovement(pos, target))
return;
}
// Otherwise, spend the rest of the time heading towards the next waypoint
dt = dt - (offset.Length() / m_Speed);
pos = target;
PickNextWaypoint(pos);
continue;
cmpPosition->MoveTo(pos.X, pos.Y);
// Spend the rest of the time heading towards the next waypoint
dt = dt - (offset.Length() / m_Speed);
MaybePickNextWaypoint(pos);
if (PickNextShortWaypoint(pos, false))
continue;
// We ran out of usable waypoints, so stop now
StopAndFaceGoal(pos);
return;
}
else
{
// Not close enough, so just move in the right direction
offset.Normalize(maxdist);
pos += offset;
cmpPosition->MoveTo(pos.X, pos.Z);
target = pos + offset;
if (!CheckMovement(pos, target))
return;
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
MaybePickNextWaypoint(pos);
return;
}
}
}
void CCmpUnitMotion::PickNextWaypoint(const CFixedVector3D& pos)
void CCmpUnitMotion::StopAndFaceGoal(CFixedVector2D pos)
{
// We can always pick the immediate next waypoint
debug_assert(!m_Path.m_Waypoints.empty());
m_TargetX = m_Path.m_Waypoints.back().x;
m_TargetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
m_HasTarget = true;
SwitchState(*m_Context, IDLE);
FaceTowardsPoint(pos, m_FinalGoal.x, m_FinalGoal.z);
// To smooth the motion and avoid grid-constrained motion, we could try picking some
// subsequent waypoints instead, if we can reach them without hitting any obstacles
// TODO: if the goal was a square building, we ought to point towards the
// nearest point on the square, not towards its center
}
void CCmpUnitMotion::FaceTowardsPoint(CFixedVector2D pos, entity_pos_t x, entity_pos_t z)
{
CFixedVector2D target(x, z);
CFixedVector2D offset = target - pos;
if (!offset.IsZero())
{
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null())
return;
cmpPosition->TurnTo(angle);
}
}
void CCmpUnitMotion::SwitchState(const CSimContext& context, int state)
{
debug_assert(state == IDLE || state == WALKING);
if (state == IDLE)
m_HasTarget = false;
// IDLE -> IDLE -- no change
// IDLE -> WALKING -- send a MotionChanged(speed) message
// WALKING -> IDLE -- set to STOPPING, so we'll send MotionChanged(0) in the next Update
// WALKING -> WALKING -- send a MotionChanged(speed) message
// STOPPING -> IDLE -- stay in STOPPING
// STOPPING -> WALKING -- set to WALKING, send MotionChanged(speed)
if (state == WALKING)
{
CMessageMotionChanged msg(m_Speed);
context.GetComponentManager().PostMessage(GetEntityId(), msg);
}
if (m_State == IDLE && state == WALKING)
{
m_State = WALKING;
return;
}
if (m_State == WALKING && state == IDLE)
{
m_State = STOPPING;
return;
}
if (m_State == STOPPING && state == IDLE)
{
return;
}
if (m_State == STOPPING && state == WALKING)
{
m_State = WALKING;
return;
}
}
bool CCmpUnitMotion::MoveToPoint(entity_pos_t x, entity_pos_t z)
{
PROFILE("MoveToPoint");
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// Reset any current movement
m_HasTarget = false;
ICmpPathfinder::Goal goal;
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::ObstructionSquare obstruction;
if (cmpObstructionManager->FindMostImportantObstruction(x, z, m_Radius, obstruction))
{
// If we're aiming inside a building, then aim for the outline of the building instead
// TODO: if we're aiming at a unit then maybe a circle would look nicer?
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.x = obstruction.x;
goal.z = obstruction.z;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = entity_pos_t::FromInt(1) / 4; // nudge the goal outwards so it doesn't intersect the building itself
goal.hw = obstruction.hw + m_Radius + delta;
goal.hh = obstruction.hh + m_Radius + delta;
}
else
{
// Unobstructed - head directly for the goal
goal.type = ICmpPathfinder::Goal::POINT;
goal.x = x;
goal.z = z;
}
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(*m_Context, WALKING);
return true;
}
bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius)
{
// Given a square, plus a target range we should reach, the shape at that distance
// is a round-cornered square which we can approximate as either a circle or as a square.
// Choose the shape that will minimise the worst-case error:
// For a square, error is (sqrt(2)-1) * range at the corners
entity_pos_t errSquare = (entity_pos_t::FromInt(4142)/10000).Multiply(range);
// For a circle, error is radius-hw at the sides and radius-hh at the top/bottom
entity_pos_t errCircle = circleRadius - std::min(hw, hh);
return (errCircle < errSquare);
}
bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
PROFILE("MoveToAttackRange");
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// Reset any current movement
m_HasTarget = false;
ICmpPathfinder::Goal goal;
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::tag_t tag = 0;
CmpPtr<ICmpObstruction> cmpObstruction(*m_Context, target);
if (!cmpObstruction.null())
tag = cmpObstruction->GetObstruction();
/*
* If we're starting outside the maxRange, we need to move closer in.
* If we're starting inside the minRange, we need to move further out.
* These ranges are measured from the center of this entity to the edge of the target;
* we add the goal range onto the size of the target shape to get the goal shape.
* (Then we extend it outwards/inwards by a little bit to be sure we'll end up
* within the right range, in case of minor numerical inaccuracies.)
*
* There's a bit of a problem with large square targets:
* the pathfinder only lets us move to goals that are squares, but the points an equal
* distance from the target make a rounded square shape instead.
*
* When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely
* within the desired rounded square, but that gives an unfair advantage to attackers who approach
* the target diagonally.
*
* If the target is small relative to the range (e.g. archers attacking anything),
* then we cheat and pretend the target is actually a circle.
* (TODO: that probably looks rubbish for things like walls?)
*
* If the target is large relative to the range (e.g. melee units attacking buildings),
* then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough.
* (Those units should set minRange to 0 so they'll never be considered *too* close.)
*/
const entity_pos_t goalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
if (tag)
{
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
goal.x = obstruction.x;
goal.z = obstruction.z;
entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize);
if (distance < minRange)
{
// Too close to the square - need to move away
entity_pos_t goalDistance = minRange + goalDelta;
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself
goal.hw = obstruction.hw + delta;
goal.hh = obstruction.hh + delta;
}
else if (distance < maxRange)
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
else
{
// We might need to move closer:
// Circumscribe the square
entity_pos_t circleRadius = halfSize.Length();
if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius))
{
// The target is small relative to our range, so pretend it's a circle
// Note that the distance to the circle will always be less than
// the distance to the square, so the previous "distance < maxRange"
// check is still valid (though not sufficient)
entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
if (circleDistance < maxRange)
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
entity_pos_t goalDistance = maxRange - goalDelta;
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.hw = circleRadius + goalDistance;
}
else
{
// The target is large relative to our range, so treat it as a square and
// get close enough that the diagonals come within range
entity_pos_t goalDistance = (maxRange - goalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2)
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself
goal.hw = obstruction.hw + delta;
goal.hh = obstruction.hh + delta;
}
}
}
else
{
// The target didn't have an obstruction or obstruction shape, so treat it as a point instead
CmpPtr<ICmpPosition> cmpTargetPosition(*m_Context, target);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
return false;
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length();
entity_pos_t goalDistance;
if (distance < minRange)
{
goalDistance = minRange + goalDelta;
}
else if (distance > maxRange)
{
goalDistance = maxRange - goalDelta;
}
else
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
// TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target)
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.x = targetPos.X;
goal.z = targetPos.Z;
goal.hw = m_Radius + goalDistance;
}
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(*m_Context, WALKING);
return true;
}
bool CCmpUnitMotion::IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
// This function closely mirrors MoveToAttackRange - it needs to return true
// after that Move has completed
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::tag_t tag = 0;
CmpPtr<ICmpObstruction> cmpObstruction(*m_Context, target);
if (!cmpObstruction.null())
tag = cmpObstruction->GetObstruction();
entity_pos_t distance;
if (tag)
{
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize);
// See if we're too close to the target square
if (distance < minRange)
return false;
// See if we're close enough to the target square
if (distance <= maxRange)
return true;
entity_pos_t circleRadius = halfSize.Length();
if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius))
{
// The target is small relative to our range, so pretend it's a circle
// and see if we're close enough to that
entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
if (circleDistance <= maxRange)
return true;
}
return false;
}
else
{
CmpPtr<ICmpPosition> cmpTargetPosition(*m_Context, target);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
return false;
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length();
if (minRange <= distance && distance <= maxRange)
return true;
return false;
}
}
bool CCmpUnitMotion::RegeneratePath(CFixedVector2D pos, bool avoidMovingUnits)
{
CmpPtr<ICmpPathfinder> cmpPathfinder (*m_Context, SYSTEM_ENTITY);
if (cmpPathfinder.null())
return false;
m_Path.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
// TODO: if it's close then just do a short path, not a long path
cmpPathfinder->SetDebugPath(pos.X, pos.Y, m_FinalGoal);
cmpPathfinder->ComputePath(pos.X, pos.Y, m_FinalGoal, m_Path);
if (m_DebugOverlayEnabled)
RenderPath(*m_Context, m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
// If there's no waypoints then we've stopped already, otherwise move to the first one
if (m_Path.m_Waypoints.empty())
{
m_HasTarget = false;
return false;
}
else
{
return PickNextShortWaypoint(pos, avoidMovingUnits);
}
}
void CCmpUnitMotion::MaybePickNextWaypoint(const CFixedVector2D& pos)
{
if (m_Path.m_Waypoints.empty())
return;
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
if ((w - pos).Length() < WAYPOINT_ADVANCE_MIN)
PickNextWaypoint(pos, false); // TODO: handle failures?
}
bool CCmpUnitMotion::PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits)
{
if (m_Path.m_Waypoints.empty())
return false;
// First try to get the immediate next waypoint
entity_pos_t targetX = m_Path.m_Waypoints.back().x;
entity_pos_t targetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
// To smooth the motion and avoid grid-constrained movement and allow dynamic obstacle avoidance,
// try skipping some more waypoints if they're close enough
while (!m_Path.m_Waypoints.empty())
{
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
if ((w - pos).Length() > WAYPOINT_ADVANCE_MAX)
break;
targetX = m_Path.m_Waypoints.back().x;
targetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
}
// Highlight the targeted waypoint
if (m_DebugOverlayEnabled)
m_DebugOverlayLines[m_Path.m_Waypoints.size()].m_Color = OVERLAY_COLOUR_PATH_ACTIVE;
// Now we need to recompute a short path to the waypoint
m_ShortPath.m_Waypoints.clear();
ICmpPathfinder::Goal goal;
if (m_Path.m_Waypoints.empty())
{
// This was the last waypoint - head for the exact goal
goal = m_FinalGoal;
}
else
{
// Head for somewhere near the waypoint (but allow some leeway in case it's obstructed)
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.hw = entity_pos_t::FromInt(CELL_SIZE*3/2);
goal.x = targetX;
goal.z = targetZ;
}
CmpPtr<ICmpPathfinder> cmpPathfinder (*m_Context, SYSTEM_ENTITY);
if (cmpPathfinder.null())
return false;
// Set up the filter to avoid/ignore moving units
NullObstructionFilter filterNull;
StationaryObstructionFilter filterStationary;
const IObstructionTestFilter* filter;
if (avoidMovingUnits)
filter = &filterNull;
else
filter = &filterStationary;
cmpPathfinder->ComputeShortPath(*filter, pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_ShortPath);
if (m_DebugOverlayEnabled)
RenderPath(*m_Context, m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
return true;
}
bool CCmpUnitMotion::PickNextShortWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits)
{
// If we don't have a short path now
if (m_ShortPath.m_Waypoints.empty())
{
// Try to pick a new long waypoint (which will also recompute the short path)
if (!PickNextWaypoint(pos, avoidMovingUnits))
return false; // no waypoints left
if (m_ShortPath.m_Waypoints.empty())
return false; // we can't reach the next long waypoint or are already there
}
// Head towards the next short waypoint
m_ShortTargetX = m_ShortPath.m_Waypoints.back().x;
m_ShortTargetZ = m_ShortPath.m_Waypoints.back().z;
m_ShortPath.m_Waypoints.pop_back();
m_HasTarget = true;
return true;
}
void CCmpUnitMotion::RenderPath(const CSimContext& context, const ICmpPathfinder::Path& path, std::vector<SOverlayLine>& lines, CColor color)
{
lines.clear();
std::vector<float> waypointCoords;
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
{
float x = path.m_Waypoints[i].x.ToFloat();
float z = path.m_Waypoints[i].z.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(context, x, z, 1.0f, 1.0f, 0.0f, lines.back());
}
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructLineOnGround(context, waypointCoords, lines.back());
}
void CCmpUnitMotion::RenderSubmit(const CSimContext& UNUSED(context), SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
for (size_t i = 0; i < 3 && !m_Path.m_Waypoints.empty(); ++i)
{
u32 cost;
entity_pos_t r = entity_pos_t::FromInt(0); // TODO: should get this from the entity's size
if (!cmpPathfinder->CanMoveStraight(pos.X, pos.Z, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, r, cost))
break;
m_TargetX = m_Path.m_Waypoints.back().x;
m_TargetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
}
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_DebugOverlayLines[i]);
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
}

View File

@ -21,6 +21,8 @@
#include "simulation2/system/InterfaceScripted.h"
#include "maths/FixedVector3D.h"
BEGIN_INTERFACE_WRAPPER(Footprint)
DEFINE_INTERFACE_METHOD_1("PickSpawnPoint", CFixedVector3D, ICmpFootprint, PickSpawnPoint, entity_id_t)
END_INTERFACE_WRAPPER(Footprint)

View File

@ -21,7 +21,8 @@
#include "simulation2/system/Interface.h"
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector3D.h"
class CFixedVector3D;
/**
* Footprints - an approximation of the entity's shape, used for collision detection and for

View File

@ -20,7 +20,7 @@
#include "simulation2/system/Interface.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpObstructionManager.h"
/**
* Flags an entity as obstructing movement for other units,
@ -29,6 +29,11 @@
class ICmpObstruction : public IComponent
{
public:
virtual ICmpObstructionManager::tag_t GetObstruction() = 0;
virtual entity_pos_t GetUnitRadius() = 0;
/**
* Test whether this entity's footprint is colliding with any other's.
* @return true if there is a collision

View File

@ -23,13 +23,20 @@
#include "simulation2/helpers/Grid.h"
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector2D.h"
class IObstructionTestFilter;
/**
* Obstruction manager: provides efficient spatial queries over objects in the world.
*
* The class exposes the abstraction of "shapes", which represent circles or squares
* with certain properties.
* The class deals with two types of shape:
* "static" shapes, typically representing buildings, which are rectangles with a given
* width and height and angle;
* and "unit" shapes, representing units that can move around the world, which have a
* radius and no rotation. (Units sometimes act as axis-aligned squares, sometimes
* as approximately circles, due to the algorithm used by the short pathfinder.)
*
* Other classes (particularly ICmpObstruction) register shapes with this interface
* and keep them updated.
*
@ -38,8 +45,11 @@ class IObstructionTestFilter;
* The functions accept an IObstructionTestFilter argument, which can restrict the
* set of shapes that are counted as collisions.
*
* Units can be marked as either moving or stationary, which simply determines whether
* certain filters include or exclude them.
*
* The @c Rasterise function approximates the current set of shapes onto a 2D grid,
* primarily for pathfinding.
* for use with tile-based pathfinding.
*/
class ICmpObstructionManager : public IComponent
{
@ -50,16 +60,7 @@ public:
typedef u32 tag_t;
/**
* Register a circle.
* @param x X coordinate of center, in world space
* @param z Z coordinate of center, in world space
* @param r radius
* @return a valid tag for manipulating the shape
*/
virtual tag_t AddCircle(entity_pos_t x, entity_pos_t z, entity_pos_t r) = 0;
/**
* Register a square.
* Register a static shape.
* @param x X coordinate of center, in world space
* @param z Z coordinate of center, in world space
* @param a angle of rotation (clockwise from +Z direction)
@ -67,17 +68,34 @@ public:
* @param h height (size along Z axis)
* @return a valid tag for manipulating the shape
*/
virtual tag_t AddSquare(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h) = 0;
virtual tag_t AddStaticShape(entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h) = 0;
/**
* Register a unit shape.
* @param x X coordinate of center, in world space
* @param z Z coordinate of center, in world space
* @param r radius (half the unit's width/height)
* @param moving whether the unit is currently moving through the world or is stationary
* @return a valid tag for manipulating the shape
*/
virtual tag_t AddUnitShape(entity_pos_t x, entity_pos_t z, entity_angle_t r, bool moving) = 0;
/**
* Adjust the position and angle of an existing shape.
* @param tag tag of shape (must be valid)
* @param x X coordinate of center, in world space
* @param z Z coordinate of center, in world space
* @param a angle of rotation (clockwise from +Z direction); ignored for circles
* @param a angle of rotation (clockwise from +Z direction); ignored for unit shapes
*/
virtual void MoveShape(tag_t tag, entity_pos_t x, entity_pos_t z, entity_angle_t a) = 0;
/**
* Set whether a unit shape is moving or stationary.
* @param tag tag of shape (must be valid and a unit shape)
* @param moving whether the unit is currently moving through the world or is stationary
*/
virtual void SetUnitMovingFlag(tag_t tag, bool moving) = 0;
/**
* Remove an existing shape. The tag will be made invalid and must not be used after this.
* @param tag tag of shape (must be valid)
@ -86,37 +104,39 @@ public:
/**
* Collision test a flat-ended thick line against the current set of shapes.
* The line caps extend by @p r beyond the end points.
* Only intersections going from outside to inside a shape are counted.
* @param filter filter to restrict the shapes that are counted
* @param x0 X coordinate of line's first point
* @param z0 Z coordinate of line's first point
* @param x1 X coordinate of line's second point
* @param z1 Z coordinate of line's second point
* @param r radius (half width) of line
* @return false if there is a collision
* @return true if there is a collision
*/
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) = 0;
/**
* Collision test a circle against the current set of shapes.
* @param filter filter to restrict the shapes that are counted
* @param x X coordinate of center
* @param z Z coordinate of center
* @param r radius of circle
* @return false if there is a collision
*/
virtual bool TestCircle(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r) = 0;
/**
* Collision test a square against the current set of shapes.
* Collision test a static square shape against the current set of shapes.
* @param filter filter to restrict the shapes that are counted
* @param x X coordinate of center
* @param z Z coordinate of center
* @param a angle of rotation (clockwise from +Z direction)
* @param w width (size along X axis)
* @param h height (size along Z axis)
* @return false if there is a collision
* @return true if there is a collision
*/
virtual bool TestSquare(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h) = 0;
virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h) = 0;
/**
* Collision test a unit shape against the current set of shapes.
* @param filter filter to restrict the shapes that are counted
* @param x X coordinate of center
* @param z Z coordinate of center
* @param r radius (half the unit's width/height)
* @return true if there is a collision
*/
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r) = 0;
/**
* Convert the current set of shapes onto a grid.
@ -128,6 +148,41 @@ public:
*/
virtual bool Rasterise(Grid<u8>& grid) = 0;
/**
* Standard representation for all types of shapes, for use with geometry processing code.
*/
struct ObstructionSquare
{
entity_pos_t x, z; // position of center
CFixedVector2D u, v; // 'horizontal' and 'vertical' orthogonal unit vectors, representing orientation
entity_pos_t hw, hh; // half width, half height of square
};
/**
* Find all the obstructions that are inside (or partially inside) the given range.
* @param filter filter to restrict the shapes that are counted
* @param x0 X coordinate of left edge of range
* @param z0 Z coordinate of bottom edge of range
* @param x1 X coordinate of right edge of range
* @param z1 Z coordinate of top edge of range
* @param squares output list of obstructions
*/
virtual void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares) = 0;
/**
* Find a single obstruction that blocks a unit at the given point with the given radius.
* Static obstructions (buildings) are more important than unit obstructions, and
* obstructions that cover the given point are more important than those that only cover
* the point expanded by the radius.
*/
virtual bool FindMostImportantObstruction(entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square) = 0;
/**
* Get the obstruction square representing the given shape.
* @param tag tag of shape (must be valid)
*/
virtual ObstructionSquare GetObstruction(tag_t tag) = 0;
/**
* Toggle the rendering of debug info.
*/
@ -149,7 +204,7 @@ public:
* This is called for all shapes that would collide, and also for some that wouldn't.
* @param tag tag of shape being tested
*/
virtual bool Allowed(ICmpObstructionManager::tag_t tag) const = 0;
virtual bool Allowed(ICmpObstructionManager::tag_t tag, bool moving) const = 0;
};
/**
@ -158,7 +213,16 @@ public:
class NullObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag)) const { return true; }
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool UNUSED(moving)) const { return true; }
};
/**
* Obstruction test filter that accepts all non-moving shapes.
*/
class StationaryObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool moving) const { return !moving; }
};
/**
@ -169,7 +233,7 @@ class SkipTagObstructionFilter : public IObstructionTestFilter
ICmpObstructionManager::tag_t m_Tag;
public:
SkipTagObstructionFilter(ICmpObstructionManager::tag_t tag) : m_Tag(tag) {}
virtual bool Allowed(ICmpObstructionManager::tag_t tag) const { return tag != m_Tag; }
virtual bool Allowed(ICmpObstructionManager::tag_t tag, bool UNUSED(moving)) const { return tag != m_Tag; }
};
#endif // INCLUDED_ICMPOBSTRUCTIONMANAGER

View File

@ -22,36 +22,43 @@
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector2D.h"
#include <vector>
class IObstructionTestFilter;
/**
* Pathfinder algorithm.
* Pathfinder algorithms.
*
* The pathfinder itself does not depend on other components. Instead, it contains an abstract
* view of the game world, based a series of collision shapes (circles and squares), which is
* updated by calls from other components (typically CCmpObstruction).
* There are two different modes: a tile-based pathfinder that works over long distances and
* accounts for terrain costs but ignore units, and a 'short' vertex-based pathfinder that
* provides precise paths and avoids other units.
*
* Internally it quantises the shapes onto a grid and computes paths over the grid, but the interface
* does not expose that detail.
* Both use the same concept of a Goal: either a point, circle or square.
* (If the starting point is inside the goal shape then the path will move outwards
* to reach the shape's outline.)
*
* The output is a list of waypoints.
*/
class ICmpPathfinder : public IComponent
{
public:
struct Goal
{
entity_pos_t x, z;
entity_pos_t minRadius, maxRadius;
enum {
POINT,
CIRCLE,
SQUARE
} type;
entity_pos_t x, z; // position of center
CFixedVector2D u, v; // if SQUARE, then orthogonal unit axes
entity_pos_t hw, hh; // if SQUARE, then half width & height; if CIRCLE, then hw is radius
};
/**
* Returned paths are currently represented as a series of waypoints.
* These happen to correspond to the centers of horizontally/vertically adjacent tiles
* along the path, but it's probably best not to rely on that.
*/
struct Waypoint
{
entity_pos_t x, z;
u32 cost; // currently a meaningless number
};
/**
@ -64,24 +71,25 @@ public:
};
/**
* Determine whether a unit (of radius r) can move between the given points in a straight line,
* without hitting any obstacles.
* This is based on the exact list of obtruction shapes, not the grid approximation.
* This should be used as a shortcut to avoid using the pathfinding algorithm in simple cases,
* and for more refined movement along the found paths.
*/
virtual bool CanMoveStraight(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u32& cost) = 0;
/**
* Compute a path between the given points, and return the set of waypoints.
* Compute a tile-based path from the given point to the goal, and return the set of waypoints.
* The waypoints correspond to the centers of horizontally/vertically adjacent tiles
* along the path.
*/
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, Path& ret) = 0;
/**
* Compute a path between the given points, and draw the latest such path as a terrain overlay.
* If the debug overlay is enabled, render the path that will computed by ComputePath.
*/
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal) = 0;
/**
* Compute a precise path from the given point to the goal, and return the set of waypoints.
* The path is based on the full set of obstructions that pass the filter, such that
* a unit of radius 'r' will be able to follow the path with no collisions.
* The path is restricted to a box of radius 'range' from the starting point.
*/
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, Path& ret) = 0;
/**
* Toggle the storage and rendering of debug info.
*/

View File

@ -22,6 +22,9 @@
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD_4("MoveToPoint", void, ICmpUnitMotion, MoveToPoint, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_2("MoveToPoint", bool, ICmpUnitMotion, MoveToPoint, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("IsInAttackRange", bool, ICmpUnitMotion, IsInAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToAttackRange", bool, ICmpUnitMotion, MoveToAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("GetSpeed", CFixed_23_8, ICmpUnitMotion, GetSpeed)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion)

View File

@ -34,10 +34,42 @@
class ICmpUnitMotion : public IComponent
{
public:
virtual void MoveToPoint(entity_pos_t x, entity_pos_t z, entity_pos_t minRadius, entity_pos_t maxRadius) = 0;
/**
* Attempt to walk to a given point, or as close as possible.
* If the unit cannot move anywhere at all, or if there is some other error, then
* returns false.
* Otherwise, sends a MotionChanged message and returns true; it will send another
* MotionChanged message (with speed 0) once it has reached the target or otherwise
* given up trying to reach it.
*/
virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z) = 0;
/**
* Determine whether the target is within the given range, using the same measurement
* as MoveToAttackRange.
*/
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Attempt to walk into range of a given target, or as close as possible.
* If the unit is already in range, or cannot move anywhere at all, or if there is
* some other error, then returns false.
* Otherwise, sends a MotionChanged message and returns true; it will send another
* MotionChanged message (with speed 0) once it has reached the target range (such that
* IsInAttackRange should return true) or otherwise given up trying to reach it.
*/
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Get the default speed that this unit will have when walking.
*/
virtual CFixed_23_8 GetSpeed() = 0;
/**
* Toggle the rendering of debug info.
*/
virtual void SetDebugOverlay(bool enabled) = 0;
DECLARE_INTERFACE_TYPE(UnitMotion)
};

View File

@ -106,7 +106,7 @@ public:
entity_pos_t z0 = entity_pos_t::FromInt(rand() % 512);
entity_pos_t x1 = entity_pos_t::FromInt(rand() % 512);
entity_pos_t z1 = entity_pos_t::FromInt(rand() % 512);
ICmpPathfinder::Goal goal = { x1, z1, entity_pos_t::FromInt(0), entity_pos_t::FromInt(0) };
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 };
ICmpPathfinder::Path path;
cmp->ComputePath(x0, z0, goal, path);

View File

@ -0,0 +1,288 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "Geometry.h"
#include "maths/FixedVector2D.h"
using namespace Geometry;
// TODO: all of these things could be optimised quite easily
bool Geometry::PointIsInSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
CFixed_23_8 du = point.Dot(u);
if (-halfSize.X <= du && du <= halfSize.X)
{
CFixed_23_8 dv = point.Dot(v);
if (-halfSize.Y <= dv && dv <= halfSize.Y)
return true;
}
return false;
}
CFixedVector2D Geometry::GetHalfBoundingBox(CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
return CFixedVector2D(
u.X.Multiply(halfSize.X).Absolute() + v.X.Multiply(halfSize.Y).Absolute(),
u.Y.Multiply(halfSize.X).Absolute() + v.Y.Multiply(halfSize.Y).Absolute()
);
}
Geometry::fixed Geometry::DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
/*
* Relative to its own coordinate system, we have a square like:
*
* A : B : C
* : :
* - - ########### - -
* # #
* # I #
* D # 0 # E v
* # # ^
* # # |
* - - ########### - - -->u
* : :
* F : G : H
*
* where 0 is the center, u and v are unit axes,
* and the square is hw*2 by hh*2 units in size.
*
* Points in the BIG regions should check distance to horizontal edges.
* Points in the DIE regions should check distance to vertical edges.
* Points in the ACFH regions should check distance to the corresponding corner.
*
* So we just need to check all of the regions to work out which calculations to apply.
*
*/
// du, dv are the location of the point in the square's coordinate system
fixed du = point.Dot(u);
fixed dv = point.Dot(v);
fixed hw = halfSize.X;
fixed hh = halfSize.Y;
// TODO: I haven't actually tested this
if (-hw < du && du < hw) // regions B, I, G
{
fixed closest = (dv.Absolute() - hh).Absolute(); // horizontal edges
if (-hh < dv && dv < hh) // region I
closest = std::min(closest, (du.Absolute() - hw).Absolute()); // vertical edges
return closest;
}
else if (-hh < dv && dv < hh) // regions D, E
{
return (du.Absolute() - hw).Absolute(); // vertical edges
}
else // regions A, C, F, H
{
CFixedVector2D corner;
if (du < fixed::Zero()) // A, F
corner -= u.Multiply(hw);
else // C, H
corner += u.Multiply(hw);
if (dv < fixed::Zero()) // F, H
corner -= v.Multiply(hh);
else // A, C
corner += v.Multiply(hh);
return (corner - point).Length();
}
}
CFixedVector2D Geometry::NearestPointOnSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
/*
* Relative to its own coordinate system, we have a square like:
*
* A : : C
* : :
* - - #### B #### - -
* #\ /#
* # \ / #
* D --0-- E v
* # / \ # ^
* #/ \# |
* - - #### G #### - - -->u
* : :
* F : : H
*
* where 0 is the center, u and v are unit axes,
* and the square is hw*2 by hh*2 units in size.
*
* Points in the BDEG regions are nearest to the corresponding edge.
* Points in the ACFH regions are nearest to the corresponding corner.
*
* So we just need to check all of the regions to work out which calculations to apply.
*
*/
// du, dv are the location of the point in the square's coordinate system
fixed du = point.Dot(u);
fixed dv = point.Dot(v);
fixed hw = halfSize.X;
fixed hh = halfSize.Y;
if (-hw < du && du < hw) // regions B, G; or regions D, E inside the square
{
if (-hh < dv && dv < hh && (du.Absolute() - hw).Absolute() < (dv.Absolute() - hh).Absolute()) // regions D, E
{
if (du >= fixed::Zero()) // E
return u.Multiply(hw) + v.Multiply(dv);
else // D
return -u.Multiply(hw) + v.Multiply(dv);
}
else // B, G
{
if (dv >= fixed::Zero()) // B
return v.Multiply(hh) + u.Multiply(du);
else // G
return -v.Multiply(hh) + u.Multiply(du);
}
}
else if (-hh < dv && dv < hh) // regions D, E outside the square
{
if (du >= fixed::Zero()) // E
return u.Multiply(hw) + v.Multiply(dv);
else // D
return -u.Multiply(hw) + v.Multiply(dv);
}
else // regions A, C, F, H
{
CFixedVector2D corner;
if (du < fixed::Zero()) // A, F
corner -= u.Multiply(hw);
else // C, H
corner += u.Multiply(hw);
if (dv < fixed::Zero()) // F, H
corner -= v.Multiply(hh);
else // A, C
corner += v.Multiply(hh);
return corner;
}
}
bool Geometry::TestRaySquare(CFixedVector2D a, CFixedVector2D b, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
/*
* We only consider collisions to be when the ray goes from outside to inside the shape (and possibly out again).
* Various cases to consider:
* 'a' inside, 'b' inside -> no collision
* 'a' inside, 'b' outside -> no collision
* 'a' outside, 'b' inside -> collision
* 'a' outside, 'b' outside -> depends; use separating axis theorem:
* if the ray's bounding box is outside the square -> no collision
* if the whole square is on the same side of the ray -> no collision
* otherwise -> collision
* (Points on the edge are considered 'inside'.)
*/
fixed hw = halfSize.X;
fixed hh = halfSize.Y;
fixed au = a.Dot(u);
fixed av = a.Dot(v);
if (-hw <= au && au <= hw && -hh <= av && av <= hh)
return false; // a is inside
fixed bu = b.Dot(u);
fixed bv = b.Dot(v);
if (-hw <= bu && bu <= hw && -hh <= bv && bv <= hh) // TODO: isn't this subsumed by the next checks?
return true; // a is outside, b is inside
if ((au < -hw && bu < -hw) || (au > hw && bu > hw) || (av < -hh && bv < -hh) || (av > hh && bv > hh))
return false; // ab is entirely above/below/side the square
CFixedVector2D abp = (b - a).Perpendicular();
fixed s0 = abp.Dot((u.Multiply(hw) + v.Multiply(hh)) - a);
fixed s1 = abp.Dot((u.Multiply(hw) - v.Multiply(hh)) - a);
fixed s2 = abp.Dot((-u.Multiply(hw) - v.Multiply(hh)) - a);
fixed s3 = abp.Dot((-u.Multiply(hw) + v.Multiply(hh)) - a);
if (s0.IsZero() || s1.IsZero() || s2.IsZero() || s3.IsZero())
return true; // ray intersects the corner
bool sign = (s0 < fixed::Zero());
if ((s1 < fixed::Zero()) != sign || (s2 < fixed::Zero()) != sign || (s3 < fixed::Zero()) != sign)
return true; // ray cuts through the square
return false;
}
/**
* Separating axis test; returns true if the square defined by u/v/halfSize at the origin
* is not entirely on the clockwise side of a line in direction 'axis' passing through 'a'
*/
static bool SquareSAT(CFixedVector2D a, CFixedVector2D axis, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
{
fixed hw = halfSize.X;
fixed hh = halfSize.Y;
CFixedVector2D p = axis.Perpendicular();
if (p.Dot((u.Multiply(hw) + v.Multiply(hh)) - a) <= fixed::Zero())
return true;
if (p.Dot((u.Multiply(hw) - v.Multiply(hh)) - a) <= fixed::Zero())
return true;
if (p.Dot((-u.Multiply(hw) - v.Multiply(hh)) - a) <= fixed::Zero())
return true;
if (p.Dot((-u.Multiply(hw) + v.Multiply(hh)) - a) <= fixed::Zero())
return true;
return false;
}
bool Geometry::TestSquareSquare(
CFixedVector2D c0, CFixedVector2D u0, CFixedVector2D v0, CFixedVector2D halfSize0,
CFixedVector2D c1, CFixedVector2D u1, CFixedVector2D v1, CFixedVector2D halfSize1)
{
// TODO: need to test this carefully
CFixedVector2D corner0a = c0 + u0.Multiply(halfSize0.X) + v0.Multiply(halfSize0.Y);
CFixedVector2D corner0b = c0 - u0.Multiply(halfSize0.X) - v0.Multiply(halfSize0.Y);
CFixedVector2D corner1a = c1 + u1.Multiply(halfSize1.X) + v1.Multiply(halfSize1.Y);
CFixedVector2D corner1b = c1 - u1.Multiply(halfSize1.X) - v1.Multiply(halfSize1.Y);
// Do a SAT test for each square vs each edge of the other square
if (!SquareSAT(corner0a - c1, -u0, u1, v1, halfSize1))
return false;
if (!SquareSAT(corner0a - c1, v0, u1, v1, halfSize1))
return false;
if (!SquareSAT(corner0b - c1, u0, u1, v1, halfSize1))
return false;
if (!SquareSAT(corner0b - c1, -v0, u1, v1, halfSize1))
return false;
if (!SquareSAT(corner1a - c0, -u1, u0, v0, halfSize0))
return false;
if (!SquareSAT(corner1a - c0, v1, u0, v0, halfSize0))
return false;
if (!SquareSAT(corner1b - c0, u1, u0, v0, halfSize0))
return false;
if (!SquareSAT(corner1b - c0, -v1, u0, v0, halfSize0))
return false;
return true;
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_HELPER_GEOMETRY
#define INCLUDED_HELPER_GEOMETRY
/**
* @file
* Helper functions related to geometry algorithms
*/
#include "maths/Fixed.h"
class CFixedVector2D;
namespace Geometry
{
typedef CFixed_23_8 fixed;
bool PointIsInSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
CFixedVector2D GetHalfBoundingBox(CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
fixed DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
CFixedVector2D NearestPointOnSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
bool TestRaySquare(CFixedVector2D a, CFixedVector2D b, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
bool TestSquareSquare(
CFixedVector2D c0, CFixedVector2D u0, CFixedVector2D v0, CFixedVector2D halfSize0,
CFixedVector2D c1, CFixedVector2D u1, CFixedVector2D v1, CFixedVector2D halfSize1);
} // namespace
#endif // INCLUDED_HELPER_GEOMETRY

View File

@ -26,6 +26,27 @@
static const size_t RENDER_CIRCLE_POINTS = 16;
static const float RENDER_HEIGHT_DELTA = 0.25f; // distance above terrain
void SimRender::ConstructLineOnGround(const CSimContext& context, std::vector<float> xz, SOverlayLine& overlay)
{
overlay.m_Coords.clear();
CmpPtr<ICmpTerrain> cmpTerrain(context, SYSTEM_ENTITY);
if (cmpTerrain.null())
return;
overlay.m_Coords.reserve(xz.size()/2 * 3);
for (size_t i = 0; i < xz.size(); i += 2)
{
float px = xz[i];
float pz = xz[i+1];
float py = cmpTerrain->GetGroundLevel(px, pz) + RENDER_HEIGHT_DELTA;
overlay.m_Coords.push_back(px);
overlay.m_Coords.push_back(py);
overlay.m_Coords.push_back(pz);
}
}
void SimRender::ConstructCircleOnGround(const CSimContext& context, float x, float z, float radius, SOverlayLine& overlay)
{
overlay.m_Coords.clear();

View File

@ -29,6 +29,11 @@ struct SOverlayLine;
namespace SimRender
{
/**
* Updates @p overlay so that it represents the given line (a list of x, z coordinate pairs), flattened on the terrain.
*/
void ConstructLineOnGround(const CSimContext& context, std::vector<float> xz, SOverlayLine& overlay);
/**
* Updates @p overlay so that it represents the given circle, flattened on the terrain.
*/

View File

@ -84,7 +84,7 @@ public:
const CParamNode* previewobstruct = tempMan->LoadTemplate(ent2, L"preview|unitobstruct", -1);
TS_ASSERT(previewobstruct != NULL);
TS_ASSERT_WSTR_EQUALS(previewobstruct->ToXML(), L"<Footprint><Circle radius=\"4\"></Circle><Height>1.0</Height></Footprint><Obstruction><Inactive></Inactive></Obstruction><Position><Altitude>0</Altitude><Anchor>upright</Anchor><Floating>false</Floating></Position><VisualActor><Actor>example</Actor></VisualActor>");
TS_ASSERT_WSTR_EQUALS(previewobstruct->ToXML(), L"<Footprint><Circle radius=\"4\"></Circle><Height>1.0</Height></Footprint><Obstruction><Inactive></Inactive><Unit radius=\"4\"></Unit></Obstruction><Position><Altitude>0</Altitude><Anchor>upright</Anchor><Floating>false</Floating></Position><VisualActor><Actor>example</Actor></VisualActor>");
const CParamNode* previewactor = tempMan->LoadTemplate(ent2, L"preview|actor|example2", -1);
TS_ASSERT(previewactor != NULL);

View File

@ -253,14 +253,26 @@ sub convert {
$out .= qq{$i$i<Height>$data->{Traits}[0]{Footprint}[0]{Height}[0]</Height>\n};
}
$out .= qq{$i</Footprint>\n};
if ($name =~ /^template_(structure|gaia)_/ and $name !~ /^template_structure_resource_field$/) {
my ($w, $d);
if ($data->{Traits}[0]{Footprint}[0]{Radius}) {
$w = $d = sprintf '%.1f', 2*$data->{Traits}[0]{Footprint}[0]{Radius}[0];
}
if ($data->{Traits}[0]{Footprint}[0]{Width}) {
$w = $data->{Traits}[0]{Footprint}[0]{Width}[0];
$d = $data->{Traits}[0]{Footprint}[0]{Depth}[0];
}
$out .= qq{$i<Obstruction>\n};
$out .= qq{$i$i<Static width="$w" depth="$d"/>\n};
$out .= qq{$i</Obstruction>\n};
}
}
if ($name =~ /^template_(structure|gaia)$/) {
$out .= qq{$i<Obstruction/>\n};
}
if ($name =~ /^template_structure_resource_field$/) {
$out .= qq{$i<Obstruction disable=""/>\n};
if ($name eq 'template_unit') {
$out .= qq{$i<Obstruction>\n};
$out .= qq{$i$i<Unit radius="1.0"/>\n};
$out .= qq{$i</Obstruction>\n};
}
if ($data->{Actions}[0]{Create}[0]{List}[0]{StructCiv} or $data->{Actions}[0]{Create}[0]{List}[0]{StructMil}) {