forked from 0ad/0ad
# 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:
parent
5daac34ef9
commit
cfae58928f
@ -14,5 +14,7 @@
|
||||
<Circle radius="4"/>
|
||||
<Height>1.0</Height>
|
||||
</Footprint>
|
||||
<Obstruction/>
|
||||
<Obstruction>
|
||||
<Unit radius="4"/>
|
||||
</Obstruction>
|
||||
</Entity>
|
||||
|
@ -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();
|
||||
|
||||
|
||||
|
@ -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 -->
|
||||
|
@ -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?
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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];
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -4,5 +4,4 @@
|
||||
<Civ>gaia</Civ>
|
||||
<GenericName>Gaia</GenericName>
|
||||
</Identity>
|
||||
<Obstruction/>
|
||||
</Entity>
|
||||
|
@ -7,4 +7,7 @@
|
||||
<Circle radius="1.5"/>
|
||||
<Height>10.0</Height>
|
||||
</Footprint>
|
||||
<Obstruction>
|
||||
<Static width="3.0" depth="3.0"/>
|
||||
</Obstruction>
|
||||
</Entity>
|
||||
|
@ -7,4 +7,7 @@
|
||||
<Circle radius="1.0"/>
|
||||
<Height>1.0</Height>
|
||||
</Footprint>
|
||||
<Obstruction>
|
||||
<Static width="2.0" depth="2.0"/>
|
||||
</Obstruction>
|
||||
</Entity>
|
||||
|
@ -7,4 +7,7 @@
|
||||
<Circle radius="3.5"/>
|
||||
<Height>3.5</Height>
|
||||
</Footprint>
|
||||
<Obstruction>
|
||||
<Static width="7.0" depth="7.0"/>
|
||||
</Obstruction>
|
||||
</Entity>
|
||||
|
@ -24,5 +24,4 @@
|
||||
<Pierce>42.0</Pierce>
|
||||
<Crush>10.0</Crush>
|
||||
</Armour>
|
||||
<Obstruction/>
|
||||
</Entity>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -29,5 +29,4 @@
|
||||
<Square width="10.0" depth="10.0"/>
|
||||
<Height>8.0</Height>
|
||||
</Footprint>
|
||||
<Obstruction disable=""/>
|
||||
</Entity>
|
||||
|
@ -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>
|
||||
|
@ -33,4 +33,7 @@
|
||||
<Circle radius="0.5"/>
|
||||
<Height>2.5</Height>
|
||||
</Footprint>
|
||||
<Obstruction>
|
||||
<Unit radius="1.0"/>
|
||||
</Obstruction>
|
||||
</Entity>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -100,7 +100,7 @@ INTERFACE(Terrain)
|
||||
COMPONENT(Terrain)
|
||||
|
||||
INTERFACE(UnitMotion)
|
||||
COMPONENT(UnitMotion)
|
||||
COMPONENT(UnitMotion) // must be after Obstruction
|
||||
|
||||
INTERFACE(Visual)
|
||||
COMPONENT(VisualActor)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
288
source/simulation2/helpers/Geometry.cpp
Normal file
288
source/simulation2/helpers/Geometry.cpp
Normal 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;
|
||||
}
|
51
source/simulation2/helpers/Geometry.h
Normal file
51
source/simulation2/helpers/Geometry.h
Normal 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
|
@ -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();
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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}) {
|
||||
|
Loading…
Reference in New Issue
Block a user