# Initial support for formation movement.

Support asynchronous path queries.
Allow escaping when stuck in impassable terrain tiles.
Split Update message in multiple phases, to cope with ordering
requirements.
Support automatic walk/run animation switching.

This was SVN commit r8058.
This commit is contained in:
Ykkrosh 2010-09-03 09:55:14 +00:00
parent ece6b58188
commit 2b57f4f998
37 changed files with 1783 additions and 546 deletions

View File

@ -32,6 +32,16 @@ AnimalAI.prototype.Schema =
var AnimalFsmSpec = {
"MoveCompleted": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"MoveStarted": function() {
// ignore spurious movement messages
},
"SKITTISH": {
"ResourceGather": function(msg) {
@ -58,7 +68,7 @@ var AnimalFsmSpec = {
this.SetNextState("FEEDING");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
@ -75,7 +85,7 @@ var AnimalFsmSpec = {
this.StopTimer();
},
"MoveStopped": function() { },
"MoveCompleted": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
@ -95,7 +105,7 @@ var AnimalFsmSpec = {
this.SetMoveSpeed(this.GetWalkSpeed());
},
"MoveStopped": function() {
"MoveCompleted": function() {
// When we've run far enough, go back to the roaming state
this.SetNextState("ROAMING");
},
@ -104,13 +114,6 @@ var AnimalFsmSpec = {
"PASSIVE": {
"ResourceGather": function(msg) {
// If someone's carving chunks of meat off us, then run away
// this.MoveAwayFrom(msg.gatherer, +this.template.FleeDistance);
// this.SetNextState("FLEEING");
// this.PlaySound("panic");
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
@ -128,7 +131,7 @@ var AnimalFsmSpec = {
this.SetNextState("FEEDING");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
@ -145,31 +148,12 @@ var AnimalFsmSpec = {
this.StopTimer();
},
"MoveStopped": function() { },
"MoveCompleted": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": {
"enter": function() {
// Run quickly
// var speed = this.GetRunSpeed();
// this.SelectAnimation("run", false, speed);
// this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed
this.SetMoveSpeed(this.GetWalkSpeed());
},
"MoveStopped": function() {
// When we've run far enough, go back to the roaming state
this.SetNextState("ROAMING");
},
},
},
};
@ -178,7 +162,6 @@ var AnimalFsm = new FSM(AnimalFsmSpec);
AnimalAI.prototype.Init = function()
{
this.messageQueue = [];
};
// FSM linkage functions:
@ -206,36 +189,26 @@ AnimalAI.prototype.DeferMessage = function(msg)
AnimalFsm.DeferMessage(this, msg);
};
AnimalAI.prototype.PushMessage = function(msg)
{
this.messageQueue.push(msg);
};
AnimalAI.prototype.OnUpdate = function()
{
var mq = this.messageQueue;
if (mq.length)
{
this.messageQueue = [];
for each (var msg in mq)
AnimalFsm.ProcessMessage(this, msg);
}
};
AnimalAI.prototype.OnMotionChanged = function(msg)
{
if (!msg.speed)
this.PushMessage({"type": "MoveStopped"});
if (msg.starting && !msg.error)
{
AnimalFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
}
else if (!msg.starting || msg.error)
{
AnimalFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
}
};
AnimalAI.prototype.OnResourceGather = function(msg)
{
this.PushMessage({"type": "ResourceGather", "gatherer": msg.gatherer});
AnimalFsm.ProcessMessage(this, {"type": "ResourceGather", "gatherer": msg.gatherer});
};
AnimalAI.prototype.TimerHandler = function(data, lateness)
{
this.PushMessage({"type": "Timer", "data": data, "lateness": lateness});
AnimalFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
// Functions to be called by the FSM:

View File

@ -0,0 +1,240 @@
function Formation() {}
Formation.prototype.Schema =
"<a:component type='system'/><empty/>";
Formation.prototype.Init = function()
{
this.members = [];
};
Formation.prototype.GetMemberCount = function()
{
return this.members.length;
};
/**
* Initialise the members of this formation.
* Must only be called once.
* All members must implement UnitAI.
*/
Formation.prototype.SetMembers = function(ents)
{
this.members = ents;
for each (var ent in this.members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
}
// Locate this formation controller in the middle of its members
this.MoveToMembersCenter();
this.ComputeMotionParameters();
};
/**
* Remove the given list of entities.
* The entities must already be members of this formation.
*/
Formation.prototype.RemoveMembers = function(ents)
{
this.members = this.members.filter(function(e) { return ents.indexOf(e) == -1; });
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
// If there's nobody left, destroy the formation
if (this.members.length == 0)
{
Engine.DestroyEntity(this.entity);
return;
}
this.ComputeMotionParameters();
// Rearrange the remaining members
this.MoveMembersIntoFormation();
};
/**
* Remove all members and destroy the formation.
*/
Formation.prototype.Disband = function()
{
for each (var ent in this.members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
this.members = [];
Engine.DestroyEntity(this.entity);
};
/**
* Call UnitAI.ReplaceOrder on all members.
*/
Formation.prototype.ReplaceMemberOrders = function(type, data)
{
for each (var ent in this.members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.ReplaceOrder(type, data);
}
};
/**
* Set all members to form up into the formation shape.
*/
Formation.prototype.MoveMembersIntoFormation = function()
{
var active = [];
var positions = [];
var types = { "Unknown": 0 }; // TODO: make this work so we put ranged behind infantry etc
for each (var ent in this.members)
{
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
active.push(ent);
positions.push(cmpPosition.GetPosition());
types["Unknown"] += 1; // TODO
}
var offsets = this.ComputeFormationOffsets(types);
var avgpos = this.ComputeAveragePosition(positions);
var avgoffset = this.ComputeAveragePosition(offsets);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
cmpPosition.JumpTo(avgpos.x, avgpos.z);
// TODO: assign to minimise worst-case distances or whatever
for (var i = 0; i < active.length; ++i)
{
var offset = offsets[i];
var cmpUnitAI = Engine.QueryInterface(active[i], IID_UnitAI);
cmpUnitAI.ReplaceOrder("FormationWalk", {
"target": this.entity,
"x": offset.x - avgoffset.x,
"z": offset.z - avgoffset.z
});
}
};
Formation.prototype.MoveToMembersCenter = function()
{
var positions = [];
for each (var ent in this.members)
{
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
positions.push(cmpPosition.GetPosition());
}
var avgpos = this.ComputeAveragePosition(positions);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
cmpPosition.JumpTo(avgpos.x, avgpos.z);
};
Formation.prototype.ComputeFormationOffsets = function(types)
{
var separation = 4; // TODO: don't hardcode this
var count = types["Unknown"];
// Choose a sensible width for the basic box formation
var cols;
if (count <= 4)
cols = count;
if (count <= 8)
cols = 4;
else if (count <= 16)
cols = Math.ceil(count / 2);
else
cols = 8;
var ranks = Math.ceil(count / cols);
var offsets = [];
var left = count;
for (var r = 0; r < ranks; ++r)
{
var n = Math.min(left, cols);
for (var c = 0; c < n; ++c)
{
var x = ((n-1)/2 - c) * separation;
var z = -r * separation;
offsets.push({"x": x, "z": z});
}
left -= n;
}
return offsets;
};
Formation.prototype.ComputeAveragePosition = function(positions)
{
var sx = 0;
var sz = 0;
for each (var pos in positions)
{
sx += pos.x;
sz += pos.z;
}
return { "x": sx / positions.length, "z": sz / positions.length };
};
/**
* Set formation controller's radius and speed based on its current members.
*/
Formation.prototype.ComputeMotionParameters = function()
{
var maxRadius = 0;
var minSpeed = Infinity;
for each (var ent in this.members)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
maxRadius = Math.max(maxRadius, cmpObstruction.GetUnitRadius());
var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.SetUnitRadius(maxRadius);
cmpUnitMotion.SetSpeed(minSpeed);
// TODO: we also need to do something about PassabilityClass, CostClass
};
Formation.prototype.OnGlobalOwnershipChanged = function(msg)
{
// When an entity is captured or destroyed, it should no longer be
// controlled by this formation
if (this.members.indexOf(msg.entity) != -1)
this.RemoveMembers([msg.entity]);
};
Engine.RegisterComponentType(IID_Formation, "Formation", Formation);

View File

@ -184,6 +184,8 @@ TrainingQueue.prototype.SpawnUnits = function(templateName, count)
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
var ents = [];
for (var i = 0; i < count; ++i)
{
var ent = Engine.AddEntity(templateName);
@ -205,19 +207,28 @@ TrainingQueue.prototype.SpawnUnits = function(templateName, count)
var cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
// If a rally point is set, walk towards it
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpRallyPoint)
{
var rallyPos = cmpRallyPoint.GetPosition();
if (rallyPos)
cmpUnitAI.Walk(rallyPos.x, rallyPos.z, false);
}
ents.push(ent);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects)
if (i == 0)
PlaySound("trained", ent);
}
// If a rally point is set, walk towards it (in formation)
if (cmpRallyPoint)
{
var rallyPos = cmpRallyPoint.GetPosition();
if (rallyPos)
{
ProcessCommand(cmpOwnership.GetOwner(), {
"type": "walk",
"entities": ents,
"x": rallyPos.x,
"z": rallyPos.z,
"queued": false
});
}
}
};
TrainingQueue.prototype.ProgressTimeout = function(data)

View File

@ -3,7 +3,9 @@ function UnitAI() {}
UnitAI.prototype.Schema =
"<a:help>Controls the unit's movement, attacks, etc, in response to commands from the player.</a:help>" +
"<a:example/>" +
"<empty/>";
"<element name='FormationController'>" +
"<data type='boolean'/>" +
"</element>";
// Very basic stance support (currently just for test maps where we don't want
// everyone killing each other immediately after loading)
@ -18,22 +20,214 @@ var g_Stances = {
var UnitFsmSpec = {
// Default event handlers:
"MoveCompleted": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"MoveStarted": function() {
// ignore spurious movement messages
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"Attacked": function(msg) {
// ignore attacker
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
"Order.FormationWalk": function(msg) {
this.PlaySound("walk");
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
this.SetNextState("FORMATIONMEMBER.WALKING");
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Walk": function(msg) {
this.PlaySound("walk");
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkToTarget": function(msg) {
this.PlaySound("walk");
var ok = this.MoveToTarget(this.order.data.target);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
"Order.Attack": function(msg) {
// Work out how to attack the given target
var type = this.GetBestAttack();
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.attackType = type;
// Try to move within attack range
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try attacking it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
}
},
"Order.Gather": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
}
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
}
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
},
"Order.Attack": function(msg) {
// TODO: we should move in formation towards the target,
// then break up into individuals when close enough to it
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.ReplaceMemberOrders("Attack", msg.data);
// TODO: we should wait until the target is killed, then
// move on to the next queued order.
// Don't bother now, just disband the formation immediately.
cmpFormation.Disband();
},
"Order.Repair": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.ReplaceMemberOrders("Repair", msg.data);
cmpFormation.Disband();
},
"Order.Gather": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.ReplaceMemberOrders("Gather", msg.data);
cmpFormation.Disband();
},
"IDLE": {
"enter": function() {
this.SelectAnimation("idle");
},
},
"WALKING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.MoveMembersIntoFormation();
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
return;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
this.SetNextState("INDIVIDUAL.IDLE");
},
"IDLE": {
"enter": function() {
this.SelectAnimation("idle");
},
},
"WALKING": {
"enter": function () {
this.SelectAnimation("move");
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"MoveStopped": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"Attacked": function(msg) {
// Default behaviour: attack back at our attacker
if (this.CanAttack(msg.data.attacker))
@ -42,7 +236,6 @@ var UnitFsmSpec = {
}
},
"IDLE": {
"enter": function() {
// If we entered the idle state we must have nothing better to do,
@ -75,76 +268,28 @@ var UnitFsmSpec = {
},
},
"Order.Walk": function(msg) {
var ok;
if (this.order.data.target)
ok = this.MoveToTarget(this.order.data.target);
else
ok = this.MoveToPoint(this.order.data.x, this.order.data.z);
if (ok)
{
// We've started walking to the given point
this.SetNextState("WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
"WALKING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.PlaySound("walk");
"enter": function () {
this.SelectAnimation("move");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.FinishOrder();
},
},
"Order.Attack": function(msg) {
// Work out how to attack the given target
var type = this.GetBestAttack();
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.attackType = type;
// Try to move within attack range
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
// We've started walking to the given point
this.SetNextState("COMBAT.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try attacking it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("COMBAT.ATTACKING");
}
},
"COMBAT": {
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else
// who's attacking us
},
"APPROACHING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
"enter": function () {
this.SelectAnimation("move");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
},
@ -196,41 +341,23 @@ var UnitFsmSpec = {
},
"CHASING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
"enter": function () {
this.SelectAnimation("move");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
},
},
"Order.Gather": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("GATHER.GATHERING");
}
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.PlaySound("walk");
"enter": function () {
this.SelectAnimation("move");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.SetNextState("GATHERING");
},
},
@ -302,31 +429,13 @@ var UnitFsmSpec = {
},
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
{
// We've started walking to the given point
this.SetNextState("REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.SetNextState("REPAIR.REPAIRING");
}
},
"REPAIR": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.PlaySound("walk");
"enter": function () {
this.SelectAnimation("move");
},
"MoveStopped": function() {
"MoveCompleted": function() {
this.SetNextState("REPAIRING");
},
},
@ -388,13 +497,22 @@ UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.SetStance("aggressive");
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.OnCreate = function()
{
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
if (this.IsFormationController())
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
@ -546,8 +664,14 @@ UnitAI.prototype.StopTimer = function()
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (!msg.speed)
UnitFsm.ProcessMessage(this, {"type": "MoveStopped"});
if (msg.starting && !msg.error)
{
UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
}
else if (!msg.starting || msg.error)
{
UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
}
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
@ -594,6 +718,17 @@ UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
if (!cmpVisual)
return;
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
cmpVisual.SelectMovementAnimation(runThreshold);
return;
}
var soundgroup;
if (sound)
{
@ -688,6 +823,31 @@ UnitAI.prototype.AttackVisibleEntity = function(ents)
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (queued)
@ -703,7 +863,7 @@ UnitAI.prototype.Walk = function(x, z, queued)
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("Walk", { "target": target }, queued);
this.AddOrder("WalkToTarget", { "target": target }, queued);
};
UnitAI.prototype.Attack = function(target, queued)
@ -736,16 +896,12 @@ UnitAI.prototype.Gather = function(target, queued)
UnitAI.prototype.Repair = function(target, queued)
{
// Verify that we're able to respond to Repair commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
// TODO: verify that this is a valid target
this.AddOrder("Repair", { "target": target }, queued);
};
@ -766,6 +922,11 @@ UnitAI.prototype.GetStance = function()
UnitAI.prototype.CanAttack = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
@ -778,6 +939,11 @@ UnitAI.prototype.CanAttack = function(target)
UnitAI.prototype.CanGather = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
@ -792,5 +958,22 @@ UnitAI.prototype.CanGather = function(target)
return true;
};
UnitAI.prototype.CanRepair = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpAttack)
return false;
// TODO: verify that this is a valid target
return true;
};
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);

View File

@ -0,0 +1 @@
Engine.RegisterInterface("Formation");

View File

@ -12,40 +12,28 @@ function ProcessCommand(player, cmd)
break;
case "walk":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (ai)
ai.Walk(cmd.x, cmd.z, cmd.queued);
}
var cmpUnitAI = GetFormationUnitAI(cmd.entities);
if (cmpUnitAI)
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
break;
case "attack":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (ai)
ai.Attack(cmd.target, cmd.queued);
}
var cmpUnitAI = GetFormationUnitAI(cmd.entities);
if (cmpUnitAI)
cmpUnitAI.Attack(cmd.target, cmd.queued);
break;
case "repair":
// This covers both repairing damaged buildings, and constructing unfinished foundations
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (ai)
ai.Repair(cmd.target, cmd.queued);
}
var cmpUnitAI = GetFormationUnitAI(cmd.entities);
if (cmpUnitAI)
cmpUnitAI.Repair(cmd.target, cmd.queued);
break;
case "gather":
for each (var ent in cmd.entities)
{
var ai = Engine.QueryInterface(ent, IID_UnitAI);
if (ai)
ai.Gather(cmd.target, cmd.queued);
}
var cmpUnitAI = GetFormationUnitAI(cmd.entities);
if (cmpUnitAI)
cmpUnitAI.Gather(cmd.target, cmd.queued);
break;
case "train":
@ -161,4 +149,105 @@ function ProcessCommand(player, cmd)
}
}
/**
* Get some information about the formations used by entities.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Return null or a UnitAI belonging either to the selected unit
* or to a formation entity for the selected group of units.
*/
function GetFormationUnitAI(ents)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
RemoveFromFormation(ents);
return Engine.QueryInterface(ents[0], IID_UnitAI);
}
// Find what formations the selected entities are currently in
var formation = ExtractFormations(ents);
if (formation.entities.length == 0)
{
// No units with AI - nothing to do here
return null;
}
var formationEnt = undefined;
if (formation.ids.length == 1)
{
// Selected units all belong to the same formation.
// Check that it doesn't have any other members
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.entities.length)
{
// The whole formation was selected, so reuse its controller for this command
formationEnt = +fid;
}
}
if (!formationEnt)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// Create the new controller
formationEnt = Engine.AddEntity("special/formation");
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
cmpFormation.SetMembers(formation.entities);
}
return Engine.QueryInterface(formationEnt, IID_UnitAI);
}
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);

View File

@ -218,7 +218,7 @@ FSM.prototype.SwitchToNextState = function(obj, nextStateName)
var toState = this.decompose[nextStateName];
if (!toState)
error("Tried to change to non-existent state '" + nextState + "'");
error("Tried to change to non-existent state '" + nextStateName + "'");
// Find the set of states in the hierarchy tree to leave then enter,
// to traverse from the old state to the new one.

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<Position>
<Altitude>0</Altitude>
<Anchor>upright</Anchor>
<Floating>false</Floating>
</Position>
<Formation/>
<UnitAI>
<FormationController>true</FormationController>
</UnitAI>
<UnitMotion>
<FormationController>true</FormationController>
<WalkSpeed>1.0</WalkSpeed>
<PassabilityClass>default</PassabilityClass>
<CostClass>default</CostClass>
</UnitMotion>
<!--
<VisualActor>
<Actor>props/special/common/waypoint_flag.xml</Actor>
</VisualActor>
-->
</Entity>

View File

@ -6,7 +6,9 @@
<Minimap>
<Type>unit</Type>
</Minimap>
<UnitAI/>
<UnitAI>
<FormationController>false</FormationController>
</UnitAI>
<Cost>
<Population>1</Population>
<PopulationBonus>0</PopulationBonus>
@ -37,6 +39,7 @@
<Crush>1.0</Crush>
</Armour>
<UnitMotion>
<FormationController>false</FormationController>
<WalkSpeed>7.0</WalkSpeed>
<Run>
<Speed>15.0</Speed>

View File

@ -82,10 +82,6 @@ void CUnitAnimation::ReloadUnit()
void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, float desync, bool keepSelection, const CStrW& actionSound)
{
if (name == m_State)
return;
m_State = name;
m_Looping = !once;
m_OriginalSpeed = speed;
m_Desync = desync;
@ -94,10 +90,15 @@ void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed,
m_Speed = DesyncSpeed(m_OriginalSpeed, m_Desync);
m_SyncRepeatTime = 0.f;
if (! keepSelection)
m_Unit.SetEntitySelection(name);
if (name != m_State)
{
m_State = name;
ReloadUnit();
if (! keepSelection)
m_Unit.SetEntitySelection(name);
ReloadUnit();
}
}
void CUnitAnimation::SetAnimationSyncRepeat(float repeatTime)

View File

@ -211,6 +211,16 @@ public:
{
return CFixedVector2D(Y, -X);
}
/**
* Rotate the vector by the given angle (anticlockwise).
*/
CFixedVector2D Rotate(fixed angle)
{
fixed s, c;
sincos_approx(angle, s, c);
return CFixedVector2D(X.Multiply(c) + Y.Multiply(s), Y.Multiply(c) - X.Multiply(s));
}
};
#endif // INCLUDED_FIXED_VECTOR2D

View File

@ -152,15 +152,19 @@ void CReplayPlayer::Replay()
game.GetSimulation2()->Update(turnLength, commands);
commands.clear();
std::string hash;
bool ok = game.GetSimulation2()->ComputeStateHash(hash);
debug_assert(ok);
printf("%s\n", Hexify(hash).c_str());
// std::string hash;
// bool ok = game.GetSimulation2()->ComputeStateHash(hash);
// debug_assert(ok);
// printf("%s\n", Hexify(hash).c_str());
}
else
{
printf("Unrecognised replay token %s\n", type.c_str());
}
}
std::string hash;
bool ok = game.GetSimulation2()->ComputeStateHash(hash);
debug_assert(ok);
printf("# Final state: %s\n", Hexify(hash).c_str());
}

View File

@ -252,3 +252,8 @@ template<> bool ScriptInterface::FromJSVal<std::vector<int> >(JSContext* cx, jsv
{
return FromJSVal_vector(cx, v, out);
}
template<> bool ScriptInterface::FromJSVal<std::vector<u32> >(JSContext* cx, jsval v, std::vector<u32>& out)
{
return FromJSVal_vector(cx, v, out);
}

View File

@ -24,6 +24,8 @@
#include "simulation2/helpers/Position.h"
#include "simulation2/components/ICmpPathfinder.h"
#define DEFAULT_MESSAGE_IMPL(name) \
virtual int GetType() const { return MT_##name; } \
virtual const char* GetScriptHandlerName() const { return "On" #name; } \
@ -44,6 +46,13 @@ public:
}
};
// The update process is split into a number of phases, in an attempt
// to cope with dependencies between components. Each phase is implemented
// as a separate message. Simulation2.cpp sends them in sequence.
/**
* Generic per-turn update message, for things that don't care much about ordering.
*/
class CMessageUpdate : public CMessage
{
public:
@ -57,6 +66,55 @@ public:
fixed turnLength;
};
/**
* Update phase for formation controller movement (must happen before individual
* units move to follow their formation).
*/
class CMessageUpdate_MotionFormation : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Update_MotionFormation)
CMessageUpdate_MotionFormation(fixed turnLength) :
turnLength(turnLength)
{
}
fixed turnLength;
};
/**
* Update phase for non-formation-controller unit movement.
*/
class CMessageUpdate_MotionUnit : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Update_MotionUnit)
CMessageUpdate_MotionUnit(fixed turnLength) :
turnLength(turnLength)
{
}
fixed turnLength;
};
/**
* Final update phase, after all other updates.
*/
class CMessageUpdate_Final : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Update_Final)
CMessageUpdate_Final(fixed turnLength) :
turnLength(turnLength)
{
}
fixed turnLength;
};
class CMessageInterpolate : public CMessage
{
public:
@ -169,12 +227,13 @@ class CMessageMotionChanged : public CMessage
public:
DEFAULT_MESSAGE_IMPL(MotionChanged)
CMessageMotionChanged(fixed speed) :
speed(speed)
CMessageMotionChanged(bool starting, bool error) :
starting(starting), error(error)
{
}
fixed speed; // metres per second, or 0 if not moving
bool starting; // whether this is a start or end of movement
bool error; // whether we failed to start moving (couldn't find any path)
};
/**
@ -234,4 +293,21 @@ public:
}
};
/**
* Sent by CCmpPathfinder after async path requests.
*/
class CMessagePathResult : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(PathResult)
CMessagePathResult(u32 ticket, const ICmpPathfinder::Path& path) :
ticket(ticket), path(path)
{
}
u32 ticket;
ICmpPathfinder::Path path;
};
#endif // INCLUDED_MESSAGETYPES

View File

@ -187,6 +187,9 @@ bool CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
{
fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
// TODO: the update process is pretty ugly, with lots of messages and dependencies
// between different components. Ought to work out a nicer way to do this.
CMessageTurnStart msgTurnStart;
m_ComponentManager.BroadcastMessage(msgTurnStart);
@ -200,12 +203,31 @@ bool CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
m_ComponentManager.GetScriptInterface().LoadScript(L"map startup script", m_StartupScript);
}
CmpPtr<ICmpPathfinder> cmpPathfinder(m_SimContext, SYSTEM_ENTITY);
if (!cmpPathfinder.null())
cmpPathfinder->FinishAsyncRequests();
CmpPtr<ICmpCommandQueue> cmpCommandQueue(m_SimContext, SYSTEM_ENTITY);
if (!cmpCommandQueue.null())
cmpCommandQueue->FlushTurn(commands);
CMessageUpdate msgUpdate(turnLengthFixed);
m_ComponentManager.BroadcastMessage(msgUpdate);
// Send all the update phases
{
CMessageUpdate msgUpdate(turnLengthFixed);
m_ComponentManager.BroadcastMessage(msgUpdate);
}
{
CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed);
m_ComponentManager.BroadcastMessage(msgUpdate);
}
{
CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed);
m_ComponentManager.BroadcastMessage(msgUpdate);
}
{
CMessageUpdate_Final msgUpdate(turnLengthFixed);
m_ComponentManager.BroadcastMessage(msgUpdate);
}
// Clean up any entities destroyed during the simulation update
m_ComponentManager.FlushDestroyedComponents();

View File

@ -32,6 +32,9 @@ COMPONENT(Test2Scripted)
// Message types:
MESSAGE(TurnStart)
MESSAGE(Update)
MESSAGE(Update_MotionFormation)
MESSAGE(Update_MotionUnit)
MESSAGE(Update_Final)
MESSAGE(Interpolate) // non-deterministic (use with caution)
MESSAGE(RenderSubmit) // non-deterministic (use with caution)
MESSAGE(Create)
@ -41,6 +44,7 @@ MESSAGE(PositionChanged)
MESSAGE(MotionChanged)
MESSAGE(RangeUpdate)
MESSAGE(TerrainChanged)
MESSAGE(PathResult)
// TemplateManager must come before all other (non-test) components,
// so that it is the first to be (de)serialized

View File

@ -35,7 +35,6 @@ public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_PositionChanged);
componentManager.SubscribeToMessageType(MT_MotionChanged);
componentManager.SubscribeToMessageType(MT_Destroy);
}
@ -54,6 +53,7 @@ public:
// Dynamic state:
bool m_Moving;
entity_id_t m_ControlGroup;
ICmpObstructionManager::tag_t m_Tag;
static std::string GetSchema()
@ -104,6 +104,7 @@ public:
m_Tag = ICmpObstructionManager::tag_t();
m_Moving = false;
m_ControlGroup = GetEntityId();
}
virtual void Deinit(const CSimContext& UNUSED(context))
@ -150,7 +151,7 @@ public:
if (m_Type == STATIC)
m_Tag = cmpObstructionManager->AddStaticShape(data.x, data.z, data.a, m_Size0, m_Size1);
else
m_Tag = cmpObstructionManager->AddUnitShape(data.x, data.z, m_Size0, m_Moving);
m_Tag = cmpObstructionManager->AddUnitShape(data.x, data.z, m_Size0, m_Moving, m_ControlGroup);
}
else if (!data.inWorld && m_Tag.valid())
{
@ -159,20 +160,6 @@ public:
}
break;
}
case MT_MotionChanged:
{
const CMessageMotionChanged& data = static_cast<const CMessageMotionChanged&> (msg);
m_Moving = !data.speed.IsZero();
if (m_Tag.valid() && 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.valid())
@ -219,6 +206,31 @@ public:
else
return cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Size0);
}
virtual void SetMovingFlag(bool enabled)
{
m_Moving = enabled;
if (m_Tag.valid() && m_Type == UNIT)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (!cmpObstructionManager.null())
cmpObstructionManager->SetUnitMovingFlag(m_Tag, m_Moving);
}
}
virtual void SetControlGroup(entity_id_t group)
{
m_ControlGroup = group;
if (m_Tag.valid() && m_Type == UNIT)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (!cmpObstructionManager.null())
cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
}
}
};
REGISTER_COMPONENT_TYPE(Obstruction)

View File

@ -49,6 +49,7 @@ struct UnitShape
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)
entity_id_t group; // control group (typically the owner entity, or a formation controller entity) (units ignore collisions with others in the same group)
};
/**
@ -144,9 +145,9 @@ public:
MakeDirty();
}
virtual tag_t AddUnitShape(entity_pos_t x, entity_pos_t z, entity_pos_t r, bool moving)
virtual tag_t AddUnitShape(entity_pos_t x, entity_pos_t z, entity_pos_t r, bool moving, entity_id_t group)
{
UnitShape shape = { x, z, r, moving };
UnitShape shape = { x, z, r, moving, group };
size_t id = m_UnitShapeNext++;
m_UnitShapes[id] = shape;
MakeDirtyUnits();
@ -208,6 +209,18 @@ public:
}
}
virtual void SetUnitControlGroup(tag_t tag, entity_id_t group)
{
debug_assert(TAG_IS_VALID(tag) && TAG_IS_UNIT(tag));
if (TAG_IS_UNIT(tag))
{
UnitShape& shape = m_UnitShapes[TAG_TO_INDEX(tag)];
shape.group = group;
MakeDirtyUnits();
}
}
virtual void RemoveShape(tag_t tag)
{
debug_assert(TAG_IS_VALID(tag));
@ -328,7 +341,7 @@ bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti
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))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving, it->second.group))
continue;
CFixedVector2D center(it->second.x, it->second.z);
@ -342,7 +355,7 @@ bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti
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))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false, INVALID_ENTITY))
continue;
CFixedVector2D center(it->second.x, it->second.z);
@ -374,7 +387,7 @@ bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte
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))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving, it->second.group))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -385,7 +398,7 @@ bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte
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))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false, INVALID_ENTITY))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -409,7 +422,7 @@ bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
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))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving, it->second.group))
continue;
entity_pos_t r1 = it->second.r;
@ -420,7 +433,7 @@ bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
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))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false, INVALID_ENTITY))
continue;
CFixedVector2D center1(it->second.x, it->second.z);
@ -524,7 +537,7 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
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))
if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.moving, it->second.group))
continue;
entity_pos_t r = it->second.r;
@ -541,7 +554,7 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
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))
if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), false, INVALID_ENTITY))
continue;
entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center

View File

@ -17,7 +17,7 @@
/**
* @file
* Common and setup code for CCmpPathfinder.
* Common code and setup code for CCmpPathfinder.
*/
#include "precompiled.h"
@ -45,6 +45,7 @@ void CCmpPathfinder::Init(const CSimContext& UNUSED(context), const CParamNode&
m_Grid = NULL;
m_ObstructionGrid = NULL;
m_TerrainDirty = true;
m_NextAsyncTicket = 1;
m_DebugOverlay = NULL;
m_DebugGrid = NULL;
@ -153,6 +154,8 @@ void CCmpPathfinder::RenderSubmit(const CSimContext& UNUSED(context), SceneColle
fixed CCmpPathfinder::GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, u8 costClass)
{
UpdateGrid();
u16 i, j;
NearestTile(x0, z0, i, j);
TerrainTile tileTag = m_Grid->get(i, j);
@ -189,6 +192,29 @@ u8 CCmpPathfinder::GetCostClass(const std::string& name)
return m_UnitCostClassTags[name];
}
fixed CCmpPathfinder::DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
return (pos - CFixedVector2D(goal.x, goal.z)).Length();
case CCmpPathfinder::Goal::CIRCLE:
return ((pos - CFixedVector2D(goal.x, goal.z)).Length() - goal.hw).Absolute();
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();
}
}
void CCmpPathfinder::UpdateGrid()
{
PROFILE("UpdateGrid");
@ -265,3 +291,55 @@ void CCmpPathfinder::UpdateGrid()
m_TerrainDirty = false;
}
}
//////////////////////////////////////////////////////////
// Async path requests:
u32 CCmpPathfinder::ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass, entity_id_t notify)
{
AsyncLongPathRequest req = { m_NextAsyncTicket++, x0, z0, goal, passClass, costClass, notify };
m_AsyncLongPathRequests.push_back(req);
return req.ticket;
}
u32 CCmpPathfinder::ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, u8 passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify)
{
AsyncShortPathRequest req = { m_NextAsyncTicket++, x0, z0, r, range, goal, passClass, avoidMovingUnits, group, notify };
m_AsyncShortPathRequests.push_back(req);
return req.ticket;
}
void CCmpPathfinder::FinishAsyncRequests()
{
// Save the request queue in case it gets modified while iterating
std::vector<AsyncLongPathRequest> longRequests;
m_AsyncLongPathRequests.swap(longRequests);
std::vector<AsyncShortPathRequest> shortRequests;
m_AsyncShortPathRequests.swap(shortRequests);
// TODO: we should only compute one path per entity per turn
// TODO: this computation should be done incrementally, spread
// across multiple frames (or even multiple turns)
for (size_t i = 0; i < longRequests.size(); ++i)
{
const AsyncLongPathRequest& req = longRequests[i];
Path path;
ComputePath(req.x0, req.z0, req.goal, req.passClass, req.costClass, path);
CMessagePathResult msg(req.ticket, path);
GetSimContext().GetComponentManager().PostMessage(req.notify, msg);
}
for (size_t i = 0; i < shortRequests.size(); ++i)
{
const AsyncShortPathRequest& req = shortRequests[i];
Path path;
ControlGroupObstructionFilter filter(req.avoidMovingUnits, req.group);
ComputeShortPath(filter, req.x0, req.z0, req.r, req.range, req.goal, req.passClass, path);
CMessagePathResult msg(req.ticket, path);
GetSimContext().GetComponentManager().PostMessage(req.notify, msg);
}
}

View File

@ -117,6 +117,31 @@ const int COST_CLASS_BITS = 8 - (PASS_CLASS_BITS + 1);
#define GET_COST_CLASS(item) ((item) >> (PASS_CLASS_BITS + 1))
#define COST_CLASS_TAG(id) ((id) << (PASS_CLASS_BITS + 1))
struct AsyncLongPathRequest
{
u32 ticket;
entity_pos_t x0;
entity_pos_t z0;
ICmpPathfinder::Goal goal;
u8 passClass;
u8 costClass;
entity_id_t notify;
};
struct AsyncShortPathRequest
{
u32 ticket;
entity_pos_t x0;
entity_pos_t z0;
entity_pos_t r;
entity_pos_t range;
ICmpPathfinder::Goal goal;
u8 passClass;
bool avoidMovingUnits;
entity_id_t group;
entity_id_t notify;
};
/**
* Implementation of ICmpPathfinder
*/
@ -125,6 +150,7 @@ class CCmpPathfinder : public ICmpPathfinder
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
componentManager.SubscribeToMessageType(MT_TerrainChanged);
}
@ -139,10 +165,14 @@ public:
std::vector<std::vector<u32> > m_MoveCosts; // costs[unitClass][terrainClass]
std::vector<std::vector<fixed> > m_MoveSpeeds; // speeds[unitClass][terrainClass]
std::vector<AsyncLongPathRequest> m_AsyncLongPathRequests;
std::vector<AsyncShortPathRequest> m_AsyncShortPathRequests;
u16 m_MapSize; // tiles per side
Grid<TerrainTile>* m_Grid; // terrain/passability information
Grid<u8>* m_ObstructionGrid; // cached obstruction information (TODO: we shouldn't bother storing this, it's redundant with LSBs of m_Grid)
bool m_TerrainDirty; // indicates if m_Grid has been updated since terrain changed
u32 m_NextAsyncTicket; // unique IDs for asynchronous path requests
// Debugging - output from last pathfind operation:
Grid<PathfindTile>* m_DebugGrid;
@ -186,8 +216,12 @@ public:
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass, Path& ret);
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass, entity_id_t notify);
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, u8 passClass, Path& ret);
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, u8 passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify);
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass);
virtual void ResetDebugPath();
@ -196,6 +230,10 @@ public:
virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, u8 costClass);
virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u8 passClass);
virtual void FinishAsyncRequests();
/**
* Returns the tile containing the given position
*/
@ -214,6 +252,8 @@ public:
z = entity_pos_t::FromInt(j*(int)CELL_SIZE + CELL_SIZE/2);
}
static fixed DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal);
/**
* Regenerates the grid based on the current obstruction list, if necessary
*/
@ -222,27 +262,4 @@ public:
void RenderSubmit(const CSimContext& context, SceneCollector& collector);
};
static fixed DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
return (pos - CFixedVector2D(goal.x, goal.z)).Length();
case CCmpPathfinder::Goal::CIRCLE:
return ((pos - CFixedVector2D(goal.x, goal.z)).Length() - goal.hw).Absolute();
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();
}
}
#endif // INCLUDED_CCMPPATHFINDER_COMMON

View File

@ -194,6 +194,8 @@ struct PathfinderState
Grid<PathfindTile>* tiles;
Grid<TerrainTile>* terrain;
bool ignoreImpassable; // allows us to escape if stuck in patches of impassability
u32 hBest; // heuristic of closest discovered tile to goal
u16 iBest, jBest; // closest tile
@ -215,7 +217,7 @@ static bool AtGoal(u16 i, u16 j, const ICmpPathfinder::Goal& goal)
entity_pos_t x, z;
CCmpPathfinder::TileCenter(i, j, x, z);
fixed dist = DistanceToGoal(CFixedVector2D(x, z), goal);
fixed dist = CCmpPathfinder::DistanceToGoal(CFixedVector2D(x, z), goal);
return (dist < tolerance);
}
@ -288,7 +290,7 @@ static void ProcessNeighbour(u16 pi, u16 pj, u16 i, u16 j, u32 pg, PathfinderSta
// Reject impassable tiles
TerrainTile tileTag = state.terrain->get(i, j);
if (!IS_PASSABLE(tileTag, state.passClass))
if (!IS_PASSABLE(tileTag, state.passClass) && !state.ignoreImpassable)
return;
u32 dg = CalculateCostDelta(pi, pj, i, j, state.tiles, state.moveCosts.at(GET_COST_CLASS(tileTag)));
@ -402,6 +404,10 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
state.tiles->get(i0, j0).SetPred(i0, j0, i0, j0);
state.tiles->get(i0, j0).cost = 0;
// To prevent units getting very stuck, if they start on an impassable tile
// surrounded entirely by impassable tiles, we ignore the impassability
state.ignoreImpassable = !IS_PASSABLE(state.terrain->get(i0, j0), state.passClass);
while (1)
{
++state.steps;
@ -434,6 +440,20 @@ void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& g
break;
}
// As soon as we find an escape route from the impassable terrain,
// take it and forbid any further use of impassable tiles
if (state.ignoreImpassable)
{
if (i > 0 && IS_PASSABLE(state.terrain->get(i-1, j), state.passClass))
state.ignoreImpassable = false;
else if (i < m_MapSize-1 && IS_PASSABLE(state.terrain->get(i+1, j), state.passClass))
state.ignoreImpassable = false;
else if (j > 0 && IS_PASSABLE(state.terrain->get(i, j-1), state.passClass))
state.ignoreImpassable = false;
else if (j < m_MapSize-1 && IS_PASSABLE(state.terrain->get(i, j+1), state.passClass))
state.ignoreImpassable = false;
}
u32 g = state.tiles->get(i, j).cost;
if (i > 0)
ProcessNeighbour(i, j, i-1, j, g, state);

View File

@ -827,3 +827,44 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter, enti
PROFILE_END("A*");
}
bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u8 passClass)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
if (cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r))
return false;
// Test against terrain:
// (TODO: this could probably be a tiny bit faster by not reusing all the vertex computation code)
UpdateGrid();
std::vector<Edge> edgesAA;
std::vector<Vertex> vertexes;
u16 i0, j0, i1, j1;
NearestTile(std::min(x0, x1) - r, std::min(z0, z1) - r, i0, j0);
NearestTile(std::max(x0, x1) + r, std::max(z0, z1) + r, i1, j1);
AddTerrainEdges(edgesAA, vertexes, i0, j0, i1, j1, r, passClass, *m_Grid);
CFixedVector2D a(x0, z0);
CFixedVector2D b(x1, z1);
std::vector<EdgeAA> edgesLeft;
std::vector<EdgeAA> edgesRight;
std::vector<EdgeAA> edgesBottom;
std::vector<EdgeAA> edgesTop;
SplitAAEdges(a, edgesAA, edgesLeft, edgesRight, edgesBottom, edgesTop);
bool visible =
CheckVisibilityLeft(a, b, edgesLeft) &&
CheckVisibilityRight(a, b, edgesRight) &&
CheckVisibilityBottom(a, b, edgesBottom) &&
CheckVisibilityTop(a, b, edgesTop);
return visible;
}

View File

@ -288,6 +288,17 @@ public:
return CFixedVector3D(m_RotX, m_RotY, m_RotZ);
}
virtual fixed GetDistanceTravelled()
{
if (!m_InWorld)
{
LOGERROR(L"CCmpPosition::GetDistanceTravelled called on entity when IsInWorld is false");
return fixed::Zero();
}
return CFixedVector2D(m_X - m_LastX, m_Z - m_LastZ).Length();
}
virtual void GetInterpolatedPosition2D(float frameOffset, float& x, float& z, float& rotY)
{
if (!m_InWorld)

View File

@ -31,24 +31,29 @@
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/FixedVector2D.h"
#include "ps/CLogger.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 entity_pos_t SHORT_PATH_SEARCH_RANGE = entity_pos_t::FromInt(CELL_SIZE*10);
static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(CELL_SIZE*3/2);
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);
static const CColor OVERLAY_COLOUR_DIRECT_PATH(1, 1, 0, 1);
class CCmpUnitMotion : public ICmpUnitMotion
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
componentManager.SubscribeToMessageType(MT_PathResult);
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
@ -59,30 +64,45 @@ public:
// Template state:
bool m_FormationController;
fixed m_WalkSpeed; // in metres per second
fixed m_RunSpeed;
entity_pos_t m_Radius;
u8 m_PassClass;
u8 m_CostClass;
// Dynamic state:
fixed m_Speed;
bool m_HasTarget; // whether we currently have valid paths and targets
// These values contain undefined junk if !HasTarget:
ICmpPathfinder::Path m_Path;
ICmpPathfinder::Path m_ShortPath;
entity_pos_t m_ShortTargetX, m_ShortTargetZ;
ICmpPathfinder::Goal m_FinalGoal;
entity_pos_t m_Radius;
enum
{
IDLE,
WALKING,
STOPPING
STATE_IDLE, // not moving at all
STATE_STOPPING, // will go IDLE next turn (this delay fixes animation timings)
// Units that are members of a formation:
// ('target' is m_TargetEntity plus m_TargetOffset)
STATE_FORMATIONMEMBER_DIRECT, // trying to follow a straight line to target
STATE_FORMATIONMEMBER_PATH_COMPUTING, // waiting for a path to target
STATE_FORMATIONMEMBER_PATH_FOLLOWING, // following the computed path to target
// Entities that represent individual units or formation controllers:
// ('target' is m_FinalGoal)
STATE_INDIVIDUAL_PATH_COMPUTING, // waiting for a path to target
STATE_INDIVIDUAL_PATH_FOLLOWING // following the computed path to target
};
int m_State;
u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none
entity_id_t m_TargetEntity;
entity_pos_t m_TargetOffsetX, m_TargetOffsetZ;
fixed m_Speed;
// These values contain undefined junk if !HasTarget:
ICmpPathfinder::Path m_LongPath;
ICmpPathfinder::Path m_ShortPath;
entity_pos_t m_ShortTargetX, m_ShortTargetZ;
ICmpPathfinder::Goal m_FinalGoal;
static std::string GetSchema()
{
return
@ -92,6 +112,9 @@ public:
"<PassabilityClass>default</PassabilityClass>"
"<CostClass>infantry</CostClass>"
"</a:example>"
"<element name='FormationController'>"
"<data type='boolean'/>"
"</element>"
"<element name='WalkSpeed' a:help='Basic movement speed (in metres per second)'>"
"<ref name='positiveDecimal'/>"
"</element>"
@ -120,7 +143,7 @@ public:
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_HasTarget = false;
m_FormationController = paramNode.GetChild("FormationController").ToBool();
m_WalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed();
m_Speed = m_WalkSpeed;
@ -145,7 +168,9 @@ public:
m_CostClass = cmpPathfinder->GetCostClass(paramNode.GetChild("CostClass").ToASCIIString());
}
m_State = IDLE;
m_State = STATE_IDLE;
m_ExpectedPathTicket = 0;
m_DebugOverlayEnabled = false;
}
@ -156,8 +181,8 @@ public:
virtual void Serialize(ISerializer& serialize)
{
serialize.Bool("has target", m_HasTarget);
if (m_HasTarget)
// serialize.Bool("has target", m_HasTarget);
// if (m_HasTarget)
{
// TODO: m_Path
// TODO: m_FinalTargetAngle
@ -170,8 +195,8 @@ public:
{
Init(context, paramNode);
deserialize.Bool(m_HasTarget);
if (m_HasTarget)
// deserialize.Bool(m_HasTarget);
// if (m_HasTarget)
{
}
}
@ -180,19 +205,22 @@ public:
{
switch (msg.GetType())
{
case MT_Update:
case MT_Update_MotionFormation:
{
fixed dt = static_cast<const CMessageUpdate&> (msg).turnLength;
if (m_State == STOPPING)
if (m_FormationController)
{
m_State = IDLE;
CMessageMotionChanged msg(fixed::Zero());
context.GetComponentManager().PostMessage(GetEntityId(), msg);
fixed dt = static_cast<const CMessageUpdate_MotionFormation&> (msg).turnLength;
Move(dt);
}
break;
}
case MT_Update_MotionUnit:
{
if (!m_FormationController)
{
fixed dt = static_cast<const CMessageUpdate_MotionUnit&> (msg).turnLength;
Move(dt);
}
Move(dt);
break;
}
case MT_RenderSubmit:
@ -201,6 +229,12 @@ public:
RenderSubmit(msgData.collector);
break;
}
case MT_PathResult:
{
const CMessagePathResult& msgData = static_cast<const CMessagePathResult&> (msg);
PathResult(msgData.ticket, msgData.path);
break;
}
}
}
@ -224,7 +258,7 @@ public:
m_DebugOverlayEnabled = enabled;
if (enabled)
{
RenderPath(m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
RenderPath(m_LongPath, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
}
}
@ -233,20 +267,49 @@ public:
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);
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange);
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z);
virtual void StopMoving()
{
SwitchState(IDLE);
m_State = STATE_STOPPING;
}
virtual void SetUnitRadius(fixed radius)
{
m_Radius = radius;
}
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);
bool ShouldAvoidMovingUnits()
{
return !m_FormationController;
}
void SendMessageStartFailed()
{
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
cmpObstruction->SetMovingFlag(false);
CMessageMotionChanged msg(true, true);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
void SendMessageStartSucceeded()
{
CMessageMotionChanged msg(true, false);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
void SendMessageEndSucceeded()
{
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
cmpObstruction->SetMovingFlag(false);
CMessageMotionChanged msg(false, false);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Do the per-turn movement and other updates
@ -260,18 +323,15 @@ private:
*/
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(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);
void RegeneratePath(CFixedVector2D pos);
void RegenerateShortPath(CFixedVector2D pos, const ICmpPathfinder::Goal& goal, bool avoidMovingUnits);
/**
* Maybe select a new long waypoint if we're getting too close to the
@ -299,124 +359,345 @@ private:
void RenderPath(const ICmpPathfinder::Path& path, std::vector<SOverlayLine>& lines, CColor color);
void RenderSubmit(SceneCollector& collector);
/**
* Handle the result of an asynchronous path query
*/
void PathResult(u32 ticket, const ICmpPathfinder::Path& path);
};
REGISTER_COMPONENT_TYPE(UnitMotion)
bool CCmpUnitMotion::CheckMovement(CFixedVector2D pos, CFixedVector2D target)
void CCmpUnitMotion::PathResult(u32 ticket, const ICmpPathfinder::Path& path)
{
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
// Ignore obsolete path requests
if (ticket != m_ExpectedPathTicket)
return;
NullObstructionFilter filter;
if (cmpObstructionManager->TestLine(filter, pos.X, pos.Y, target.X, target.Y, m_Radius))
m_ExpectedPathTicket = 0; // we don't expect to get this result again
if (m_State == STATE_FORMATIONMEMBER_PATH_COMPUTING)
{
// 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))
m_LongPath.m_Waypoints.clear();
m_ShortPath = path;
if (m_DebugOverlayEnabled)
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
if (m_ShortPath.m_Waypoints.empty())
{
// Oh dear, we can't find the path any more; give up
StopAndFaceGoal(pos);
return false;
// Can't find any path - switch back to DIRECT so we try again later
// when the formation's moved further on.
// TODO: We should probably wait a while before trying again
m_State = STATE_FORMATIONMEMBER_DIRECT;
return;
}
// NOTE: it's theoretically possible that we will generate a waypoint we can reach without
// colliding with anything, but multiplying the movement vector by the timestep will result
// in a line that does collide (given numerical inaccuracies), so we'll get stuck in a loop
// of generating a new path and colliding whenever we try to follow it, and the unit will
// move nowhere.
// Hopefully this isn't common.
// Wait for the next Update before we try moving again
return false;
// Now we're moving along the path
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
m_State = STATE_FORMATIONMEMBER_PATH_FOLLOWING;
SendMessageStartSucceeded();
PickNextShortWaypoint(pos, ShouldAvoidMovingUnits());
return;
}
else if (m_State == STATE_INDIVIDUAL_PATH_COMPUTING)
{
m_LongPath = path;
m_ShortPath.m_Waypoints.clear();
// NOTE: we ignore terrain here - we assume the pathfinder won't give us a path that crosses impassable
// terrain (which is a valid assumption) and that the terrain will never change (which is not).
// Probably not worth fixing since it'll happen very rarely.
if (m_DebugOverlayEnabled)
RenderPath(m_LongPath, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
return true;
// If there's no waypoints then we've stopped already, otherwise move to the first one
if (m_LongPath.m_Waypoints.empty())
{
m_State = STATE_IDLE;
SendMessageStartFailed();
}
else
{
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
if (PickNextShortWaypoint(pos, ShouldAvoidMovingUnits()))
{
m_State = STATE_INDIVIDUAL_PATH_FOLLOWING;
SendMessageStartSucceeded();
}
else
{
m_State = STATE_IDLE;
SendMessageStartFailed();
}
}
}
else
{
LOGWARNING(L"unexpected PathResult (%d %d)", GetEntityId(), m_State);
}
}
void CCmpUnitMotion::Move(fixed dt)
{
PROFILE("Move");
if (!m_HasTarget)
return;
CmpPtr<ICmpPathfinder> cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
// We want to move (at most) m_Speed*dt units from pos towards the next waypoint
while (dt > fixed::Zero())
switch (m_State)
{
CFixedVector2D target(m_ShortTargetX, m_ShortTargetZ);
CFixedVector2D offset = target - pos;
case STATE_STOPPING:
m_State = STATE_IDLE;
SendMessageEndSucceeded();
return;
// Face towards the target
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
cmpPosition->TurnTo(angle);
case STATE_IDLE:
case STATE_FORMATIONMEMBER_PATH_COMPUTING:
case STATE_INDIVIDUAL_PATH_COMPUTING:
return;
// Find the speed factor of the underlying terrain
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile)
fixed terrainSpeed = cmpPathfinder->GetMovementSpeed(pos.X, pos.Y, m_CostClass);
case STATE_FORMATIONMEMBER_DIRECT:
case STATE_FORMATIONMEMBER_PATH_FOLLOWING:
{
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
// Work out how far we can travel in dt
fixed maxdist = m_Speed.Multiply(terrainSpeed).Multiply(dt);
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return;
// If the target is close, we can move there directly
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), m_TargetEntity);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
{
if (!CheckMovement(pos, target))
return;
pos = target;
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);
// If the target has disappeared, give up on it
StopMoving();
return;
}
else
CFixedVector2D pos = cmpPosition->GetPosition2D();
// We want to move (at most) m_Speed*dt units from pos towards the next waypoint
entity_angle_t targetAngle = cmpTargetPosition->GetRotation().Y;
CFixedVector2D targetOffset = CFixedVector2D(m_TargetOffsetX, m_TargetOffsetZ).Rotate(targetAngle);
while (dt > fixed::Zero())
{
// Not close enough, so just move in the right direction
offset.Normalize(maxdist);
target = pos + offset;
CFixedVector2D target;
if (m_State == STATE_FORMATIONMEMBER_DIRECT)
{
target = cmpTargetPosition->GetPosition2D() + targetOffset;
if (!CheckMovement(pos, target))
return;
if (m_DebugOverlayEnabled)
{
// Draw a path from our current location to the target
ICmpPathfinder::Path dummyPath;
ICmpPathfinder::Waypoint dummyWaypoint;
dummyWaypoint.x = pos.X;
dummyWaypoint.z = pos.Y;
dummyPath.m_Waypoints.push_back(dummyWaypoint);
dummyWaypoint.x = target.X;
dummyWaypoint.z = target.Y;
dummyPath.m_Waypoints.push_back(dummyWaypoint);
RenderPath(dummyPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_DIRECT_PATH);
}
}
else // m_State == STATE_FORMATIONMEMBER_PATH_FOLLOWING
{
target = CFixedVector2D(m_ShortTargetX, m_ShortTargetZ);
}
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
CFixedVector2D offset = target - pos;
MaybePickNextWaypoint(pos);
// Face towards the target
if (!offset.IsZero())
{
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
cmpPosition->TurnTo(angle);
}
// Find the speed factor of the underlying terrain
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile)
fixed terrainSpeed = cmpPathfinder->GetMovementSpeed(pos.X, pos.Y, m_CostClass);
fixed maxSpeed = GetRunSpeed().Multiply(terrainSpeed); // TODO: fall back to walk speed if tired
// Work out how far we can travel in dt
fixed maxdist = maxSpeed.Multiply(dt);
// If the target is close, we can move there directly
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
ControlGroupObstructionFilter filter(true, m_TargetEntity);
if (cmpPathfinder->CheckMovement(filter, pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass))
{
// We can move directly with no obstructions
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
if (m_State == STATE_FORMATIONMEMBER_DIRECT)
{
// We've caught up with the target. Match the target direction
// (but stay in TARGET_ENTITY_DIRECT mode since
// the target might move by the next turn and we should continue
// following it)
cmpPosition->TurnTo(targetAngle);
return;
}
else // m_State == STATE_FORMATIONMEMBER_PATH_FOLLOWING
{
// We were heading towards a waypoint on an obstruction-avoiding path.
// Spend the rest of the time heading towards the next waypoint
dt = dt - (offsetLength / maxSpeed);
if (PickNextShortWaypoint(pos, false))
{
// Got a new waypoint; use it in the next iteration
continue;
}
// We reached the end of the waypoints, so try going direct again
m_State = STATE_FORMATIONMEMBER_DIRECT;
continue;
}
}
}
else
{
// Not close enough, so just move in the right direction
offset.Normalize(maxdist);
target = pos + offset;
ControlGroupObstructionFilter filter(true, m_TargetEntity);
if (cmpPathfinder->CheckMovement(filter, pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass))
{
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
return;
}
}
// Our movement was blocked before we reached the target/waypoint.
// Compute a new short path to the target, which we'll use on the next turn.
// TODO: if the target is a long way away, we should compute a long path
// or leave the formation or something.
m_State = STATE_FORMATIONMEMBER_PATH_COMPUTING;
ICmpPathfinder::Goal goal;
goal.type = ICmpPathfinder::Goal::POINT;
target = cmpTargetPosition->GetPosition2D() + targetOffset;
goal.x = target.X;
goal.z = target.Y;
RegenerateShortPath(pos, goal, true);
return;
}
return;
}
case STATE_INDIVIDUAL_PATH_FOLLOWING:
{
CmpPtr<ICmpPathfinder> cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
// We want to move (at most) m_Speed*dt units from pos towards the next waypoint
while (dt > fixed::Zero())
{
CFixedVector2D target(m_ShortTargetX, m_ShortTargetZ);
CFixedVector2D offset = target - pos;
// Face towards the target
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
cmpPosition->TurnTo(angle);
// Find the speed factor of the underlying terrain
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile)
fixed terrainSpeed = cmpPathfinder->GetMovementSpeed(pos.X, pos.Y, m_CostClass);
fixed maxSpeed = m_Speed.Multiply(terrainSpeed); // TODO: running?
// Work out how far we can travel in dt
fixed maxdist = maxSpeed.Multiply(dt);
// If the target is close, we can move there directly
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
ControlGroupObstructionFilter filter(ShouldAvoidMovingUnits(), GetEntityId());
if (cmpPathfinder->CheckMovement(filter, pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass))
{
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
// Spend the rest of the time heading towards the next waypoint
dt = dt - (offsetLength / maxSpeed);
MaybePickNextWaypoint(pos);
if (PickNextShortWaypoint(pos, ShouldAvoidMovingUnits()))
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);
target = pos + offset;
ControlGroupObstructionFilter filter(ShouldAvoidMovingUnits(), GetEntityId());
if (cmpPathfinder->CheckMovement(filter, pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass))
{
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
MaybePickNextWaypoint(pos);
return;
}
}
// We got blocked
// Try finding a new path
m_State = STATE_INDIVIDUAL_PATH_COMPUTING;
RegeneratePath(pos);
return;
}
}
}
}
void CCmpUnitMotion::StopAndFaceGoal(CFixedVector2D pos)
{
SwitchState(IDLE);
m_ExpectedPathTicket = 0;
m_State = STATE_STOPPING;
FaceTowardsPoint(pos, m_FinalGoal.x, m_FinalGoal.z);
// TODO: if the goal was a square building, we ought to point towards the
@ -438,50 +719,6 @@ void CCmpUnitMotion::FaceTowardsPoint(CFixedVector2D pos, entity_pos_t x, entity
}
}
void CCmpUnitMotion::SwitchState(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);
GetSimContext().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");
@ -491,22 +728,13 @@ bool CCmpUnitMotion::MoveToPoint(entity_pos_t x, entity_pos_t z)
return false;
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Reset any current movement
m_HasTarget = false;
ICmpPathfinder::Goal goal;
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::tag_t tag;
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
tag = cmpObstruction->GetObstruction();
SkipTagObstructionFilter filter(tag);
ControlGroupObstructionFilter filter(true, GetEntityId());
ICmpObstructionManager::ObstructionSquare obstruction;
if (cmpObstructionManager->FindMostImportantObstruction(filter, x, z, m_Radius, obstruction))
@ -531,12 +759,9 @@ bool CCmpUnitMotion::MoveToPoint(entity_pos_t x, entity_pos_t z)
goal.z = z;
}
m_State = STATE_INDIVIDUAL_PATH_COMPUTING;
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
RegeneratePath(pos);
return true;
}
@ -567,9 +792,6 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Reset any current movement
m_HasTarget = false;
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
@ -679,11 +901,9 @@ bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange
}
}
m_State = STATE_INDIVIDUAL_PATH_COMPUTING;
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
RegeneratePath(pos);
return true;
}
else
@ -736,11 +956,9 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos
goal.z = z;
goal.hw = m_Radius + goalDistance;
m_State = STATE_INDIVIDUAL_PATH_COMPUTING;
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
RegeneratePath(pos);
return true;
}
@ -814,76 +1032,92 @@ bool CCmpUnitMotion::IsInAttackRange(entity_id_t target, entity_pos_t minRange,
}
}
bool CCmpUnitMotion::RegeneratePath(CFixedVector2D pos, bool avoidMovingUnits)
void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
{
CmpPtr<ICmpPathfinder> cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return false;
m_ExpectedPathTicket = 0;
m_State = STATE_FORMATIONMEMBER_DIRECT;
m_Path.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
m_TargetEntity = target;
m_TargetOffsetX = x;
m_TargetOffsetZ = z;
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
cmpObstruction->SetMovingFlag(true);
}
void CCmpUnitMotion::RegeneratePath(CFixedVector2D pos)
{
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
cmpObstruction->SetMovingFlag(true);
// TODO: if it's close then just do a short path, not a long path
cmpPathfinder->SetDebugPath(pos.X, pos.Y, m_FinalGoal, m_PassClass, m_CostClass);
cmpPathfinder->ComputePath(pos.X, pos.Y, m_FinalGoal, m_PassClass, m_CostClass, m_Path);
m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(pos.X, pos.Y, m_FinalGoal, m_PassClass, m_CostClass, GetEntityId());
}
if (m_DebugOverlayEnabled)
RenderPath(m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
void CCmpUnitMotion::RegenerateShortPath(CFixedVector2D pos, const ICmpPathfinder::Goal& goal, bool avoidMovingUnits)
{
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
// 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);
}
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), GetEntityId());
if (!cmpObstruction.null())
cmpObstruction->SetMovingFlag(true);
m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, avoidMovingUnits, m_TargetEntity, GetEntityId());
}
void CCmpUnitMotion::MaybePickNextWaypoint(const CFixedVector2D& pos)
{
if (m_Path.m_Waypoints.empty())
if (m_LongPath.m_Waypoints.empty())
return;
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
CFixedVector2D w(m_LongPath.m_Waypoints.back().x, m_LongPath.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())
if (m_LongPath.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();
entity_pos_t targetX = m_LongPath.m_Waypoints.back().x;
entity_pos_t targetZ = m_LongPath.m_Waypoints.back().z;
m_LongPath.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())
while (!m_LongPath.m_Waypoints.empty())
{
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
CFixedVector2D w(m_LongPath.m_Waypoints.back().x, m_LongPath.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();
targetX = m_LongPath.m_Waypoints.back().x;
targetZ = m_LongPath.m_Waypoints.back().z;
m_LongPath.m_Waypoints.pop_back();
}
// Highlight the targeted waypoint
if (m_DebugOverlayEnabled)
m_DebugOverlayLines[m_Path.m_Waypoints.size()].m_Color = OVERLAY_COLOUR_PATH_ACTIVE;
m_DebugOverlayLines[m_LongPath.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())
if (m_LongPath.m_Waypoints.empty())
{
// This was the last waypoint - head for the exact goal
goal = m_FinalGoal;
@ -892,7 +1126,7 @@ bool CCmpUnitMotion::PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovin
{
// 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.hw = SHORT_PATH_GOAL_RADIUS;
goal.x = targetX;
goal.z = targetZ;
}
@ -902,15 +1136,10 @@ bool CCmpUnitMotion::PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovin
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;
ControlGroupObstructionFilter filter(avoidMovingUnits, GetEntityId());
cmpPathfinder->ComputeShortPath(*filter, pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, m_ShortPath);
// TODO: this should be async
cmpPathfinder->ComputeShortPath(filter, pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, m_ShortPath);
if (m_DebugOverlayEnabled)
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
@ -935,7 +1164,6 @@ bool CCmpUnitMotion::PickNextShortWaypoint(const CFixedVector2D& pos, bool avoid
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;
}

View File

@ -41,6 +41,7 @@ class CCmpVisualActor : public ICmpVisual
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_Interpolate);
componentManager.SubscribeToMessageType(MT_RenderSubmit);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
@ -54,6 +55,7 @@ public:
bool m_Hidden; // only valid between Interpolate and RenderSubmit
// Current animation state
float m_AnimRunThreshold; // if non-zero this is the special walk/run mode
std::string m_AnimName;
bool m_AnimOnce;
float m_AnimSpeed;
@ -150,20 +152,26 @@ public:
Init(context, paramNode);
}
virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global))
virtual void HandleMessage(const CSimContext& UNUSED(context), const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update_Final:
{
const CMessageUpdate_Final& msgData = static_cast<const CMessageUpdate_Final&> (msg);
Update(msgData.turnLength);
break;
}
case MT_Interpolate:
{
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
Interpolate(context, msgData.frameTime, msgData.offset);
Interpolate(msgData.frameTime, msgData.offset);
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
RenderSubmit(context, msgData.collector, msgData.frustum, msgData.culling);
RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
break;
}
case MT_OwnershipChanged:
@ -222,6 +230,7 @@ public:
if (!isfinite(speed) || speed < 0) // JS 'undefined' converts to NaN, which causes Bad Things
speed = 1.f;
m_AnimRunThreshold = 0.f;
m_AnimName = name;
m_AnimOnce = once;
m_AnimSpeed = speed;
@ -232,6 +241,16 @@ public:
m_Unit->GetAnimation().SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed, m_AnimDesync, false, m_SoundGroup.c_str());
}
virtual void SelectMovementAnimation(float runThreshold)
{
if (!m_Unit)
return;
m_AnimRunThreshold = runThreshold;
m_Unit->GetAnimation().SetAnimationState("walk", false, 1.f, 0.f, false, L"");
}
virtual void SetAnimationSyncRepeat(float repeattime)
{
if (!m_Unit)
@ -294,18 +313,42 @@ public:
}
private:
void Interpolate(const CSimContext& context, float frameTime, float frameOffset);
void RenderSubmit(const CSimContext& context, SceneCollector& collector, const CFrustum& frustum, bool culling);
void Update(fixed turnLength);
void Interpolate(float frameTime, float frameOffset);
void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
};
REGISTER_COMPONENT_TYPE(VisualActor)
void CCmpVisualActor::Interpolate(const CSimContext& context, float frameTime, float frameOffset)
void CCmpVisualActor::Update(fixed turnLength)
{
if (m_Unit == NULL)
return;
CmpPtr<ICmpPosition> cmpPosition(context, GetEntityId());
// If we're in the special movement mode, select an appropriate animation
if (m_AnimRunThreshold)
{
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return;
float speed = cmpPosition->GetDistanceTravelled().ToFloat() / turnLength.ToFloat();
if (speed == 0.0f)
m_Unit->GetAnimation().SetAnimationState("idle", false, 1.f, 0.f, false, L"");
else if (speed < m_AnimRunThreshold)
m_Unit->GetAnimation().SetAnimationState("walk", false, speed, 0.f, false, L"");
else
m_Unit->GetAnimation().SetAnimationState("run", false, speed, 0.f, false, L"");
}
}
void CCmpVisualActor::Interpolate(float frameTime, float frameOffset)
{
if (m_Unit == NULL)
return;
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null())
return;
@ -326,7 +369,7 @@ void CCmpVisualActor::Interpolate(const CSimContext& context, float frameTime, f
m_Unit->UpdateModel(frameTime);
}
void CCmpVisualActor::RenderSubmit(const CSimContext& UNUSED(context), SceneCollector& collector, const CFrustum& frustum, bool culling)
void CCmpVisualActor::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
{
if (m_Unit == NULL)
return;

View File

@ -22,5 +22,7 @@
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(Obstruction)
DEFINE_INTERFACE_METHOD_0("GetUnitRadius", entity_pos_t, ICmpObstruction, GetUnitRadius)
DEFINE_INTERFACE_METHOD_0("CheckCollisions", bool, ICmpObstruction, CheckCollisions)
DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t)
END_INTERFACE_WRAPPER(Obstruction)

View File

@ -40,6 +40,15 @@ public:
*/
virtual bool CheckCollisions() = 0;
virtual void SetMovingFlag(bool enabled) = 0;
/**
* Change the control group that the entity belongs to.
* Control groups are used to let units ignore collisions with other units from
* the same group. Default is the entity's own ID.
*/
virtual void SetControlGroup(entity_id_t group) = 0;
DECLARE_INTERFACE_TYPE(Obstruction)
};

View File

@ -93,7 +93,7 @@ public:
* @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;
virtual tag_t AddUnitShape(entity_pos_t x, entity_pos_t z, entity_angle_t r, bool moving, entity_id_t group) = 0;
/**
* Adjust the position and angle of an existing shape.
@ -111,6 +111,13 @@ public:
*/
virtual void SetUnitMovingFlag(tag_t tag, bool moving) = 0;
/**
* Set the control group of a unit shape.
* @param tag tag of shape (must be valid and a unit shape)
* @param group control group entity ID
*/
virtual void SetUnitControlGroup(tag_t tag, entity_id_t group) = 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)
@ -229,8 +236,9 @@ public:
* This is called for all shapes that would collide, and also for some that wouldn't.
* @param tag tag of shape being tested
* @param moving whether the shape is a moving unit
* @param group the control group (typically the shape's unit, or the unit's formation controller, or 0)
*/
virtual bool Allowed(ICmpObstructionManager::tag_t tag, bool moving) const = 0;
virtual bool Allowed(ICmpObstructionManager::tag_t tag, bool moving, entity_id_t group) const = 0;
};
/**
@ -239,7 +247,10 @@ public:
class NullObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool UNUSED(moving)) const { return true; }
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool UNUSED(moving), entity_id_t UNUSED(group)) const
{
return true;
}
};
/**
@ -248,7 +259,34 @@ public:
class StationaryObstructionFilter : public IObstructionTestFilter
{
public:
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool moving) const { return !moving; }
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool moving, entity_id_t UNUSED(group)) const
{
return !moving;
}
};
/**
* Obstruction test filter that reject shapes in a given control group,
* and optionally rejects moving shapes.
*/
class ControlGroupObstructionFilter : public IObstructionTestFilter
{
bool m_AvoidMoving;
entity_id_t m_Group;
public:
ControlGroupObstructionFilter(bool avoidMoving, entity_id_t group) :
m_AvoidMoving(avoidMoving), m_Group(group)
{
}
virtual bool Allowed(ICmpObstructionManager::tag_t UNUSED(tag), bool moving, entity_id_t group) const
{
if (group == m_Group)
return false;
if (moving && !m_AvoidMoving)
return false;
return true;
}
};
/**
@ -258,8 +296,14 @@ 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, bool UNUSED(moving)) const { return tag.n != m_Tag.n; }
SkipTagObstructionFilter(ICmpObstructionManager::tag_t tag) : m_Tag(tag)
{
}
virtual bool Allowed(ICmpObstructionManager::tag_t tag, bool UNUSED(moving), entity_id_t UNUSED(group)) const
{
return tag.n != m_Tag.n;
}
};
#endif // INCLUDED_ICMPOBSTRUCTIONMANAGER

View File

@ -94,6 +94,14 @@ public:
*/
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass, Path& ret) = 0;
/**
* Asynchronous version of ComputePath.
* The result will be sent as CMessagePathResult to 'notify'.
* Returns a unique non-zero number, which will match the 'ticket' in the result,
* so callers can recognise each individual request they make.
*/
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, u8 passClass, u8 costClass, entity_id_t notify) = 0;
/**
* If the debug overlay is enabled, render the path that will computed by ComputePath.
*/
@ -107,17 +115,37 @@ public:
*/
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, u8 passClass, Path& ret) = 0;
/**
* Asynchronous version of ComputeShortPath (using ControlGroupObstructionFilter).
* The result will be sent as CMessagePathResult to 'notify'.
* Returns a unique non-zero number, which will match the 'ticket' in the result,
* so callers can recognise each individual request they make.
*/
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, u8 passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) = 0;
/**
* Find the speed factor (typically around 1.0) for a unit of the given cost class
* at the given position.
*/
virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, u8 costClass) = 0;
/**
* Check whether the given movement line is valid and doesn't hit any obstructions
* or impassable terrain.
* Returns true if the movement is okay.
*/
virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, u8 passClass) = 0;
/**
* Toggle the storage and rendering of debug info.
*/
virtual void SetDebugOverlay(bool enabled) = 0;
/**
* Finish computing asynchronous path requests and send the CMessagePathResult messages.
*/
virtual void FinishAsyncRequests() = 0;
DECLARE_INTERFACE_TYPE(Pathfinder)
};

View File

@ -131,7 +131,11 @@ public:
*/
virtual CFixedVector3D GetRotation() = 0;
// TODO: do we want to add smooth rotation here?
/**
* Returns the distance that the unit will be interpolated over,
* i.e. the distance travelled since the start of the turn.
*/
virtual fixed GetDistanceTravelled() = 0;
/**
* Get the current interpolated 2D position and orientation, for rendering.

View File

@ -26,9 +26,11 @@ DEFINE_INTERFACE_METHOD_2("MoveToPoint", bool, ICmpUnitMotion, MoveToPoint, enti
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_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed)
DEFINE_INTERFACE_METHOD_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed)
DEFINE_INTERFACE_METHOD_0("GetRunSpeed", fixed, ICmpUnitMotion, GetRunSpeed)
DEFINE_INTERFACE_METHOD_1("SetUnitRadius", void, ICmpUnitMotion, SetUnitRadius, fixed)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion)

View File

@ -65,6 +65,12 @@ public:
*/
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Join a formation, and move towards a given offset relative to the formation controller entity.
* Continues following the formation until given a different command.
*/
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
/**
* Stop moving immediately.
*/
@ -85,6 +91,13 @@ public:
*/
virtual fixed GetRunSpeed() = 0;
/**
* Override the default obstruction radius, used for planning paths and checking for collisions.
* Bad things may happen if this entity has an active Obstruction component with a larger
* radius. (This is intended primarily for formation controllers.)
*/
virtual void SetUnitRadius(fixed radius) = 0;
/**
* Toggle the rendering of debug info.
*/

View File

@ -23,6 +23,7 @@
BEGIN_INTERFACE_WRAPPER(Visual)
DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, float, std::wstring)
DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, float)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, float)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, float)
DEFINE_INTERFACE_METHOD_4("SetShadingColour", void, ICmpVisual, SetShadingColour, fixed, fixed, fixed, fixed)

View File

@ -72,6 +72,12 @@ public:
*/
virtual void SelectAnimation(std::string name, bool once, float speed, std::wstring soundgroup) = 0;
/**
* Start playing the walk/run animations, scaled to the unit's movement speed.
* @param runThreshold movement speed at which to switch to the run animation
*/
virtual void SelectMovementAnimation(float runThreshold) = 0;
/**
* Adjust the speed of the current animation, so it can match simulation events.
* @param repeattime time for complete loop of animation, in msec

View File

@ -83,7 +83,7 @@ public:
return NULL;
}
void promote(ID id, u32 newrank)
void promote(ID id, R newrank)
{
for (size_t n = 0; n < m_Heap.size(); ++n)
{

View File

@ -64,19 +64,24 @@ CMessage* CMessageTurnStart::FromJSVal(ScriptInterface& UNUSED(scriptInterface),
////////////////////////////////
jsval CMessageUpdate::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(turnLength);
return OBJECT_TO_JSVAL(obj);
}
#define MESSAGE_1(name, t0, a0) \
jsval CMessage##name::ToJSVal(ScriptInterface& scriptInterface) const \
{ \
TOJSVAL_SETUP(); \
SET_MSG_PROPERTY(a0); \
return OBJECT_TO_JSVAL(obj); \
} \
CMessage* CMessage##name::FromJSVal(ScriptInterface& scriptInterface, jsval val) \
{ \
FROMJSVAL_SETUP(); \
GET_MSG_PROPERTY(t0, a0); \
return new CMessage##name(a0); \
}
CMessage* CMessageUpdate::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(fixed, turnLength);
return new CMessageUpdate(turnLength);
}
MESSAGE_1(Update, fixed, turnLength)
MESSAGE_1(Update_MotionFormation, fixed, turnLength)
MESSAGE_1(Update_MotionUnit, fixed, turnLength)
MESSAGE_1(Update_Final, fixed, turnLength)
////////////////////////////////
@ -177,15 +182,17 @@ CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& UNUSED(scriptInter
jsval CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(speed);
SET_MSG_PROPERTY(starting);
SET_MSG_PROPERTY(error);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(fixed, speed);
return new CMessageMotionChanged(speed);
GET_MSG_PROPERTY(bool, starting);
GET_MSG_PROPERTY(bool, error);
return new CMessageMotionChanged(starting, error);
}
////////////////////////////////
@ -216,6 +223,18 @@ CMessage* CMessageRangeUpdate::FromJSVal(ScriptInterface& UNUSED(scriptInterface
return NULL;
}
////////////////////////////////
jsval CMessagePathResult::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessagePathResult::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return NULL;
}
////////////////////////////////////////////////////////////////
CMessage* CMessageFromJSVal(int mtid, ScriptInterface& scriptingInterface, jsval val)

View File

@ -90,6 +90,7 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti
#undef INTERFACE
#undef COMPONENT
m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY);
m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY);
ResetState();