Index: binaries/data/mods/public/simulation/components/Builder.js
===================================================================
--- binaries/data/mods/public/simulation/components/Builder.js
+++ binaries/data/mods/public/simulation/components/Builder.js
@@ -49,12 +49,7 @@
Builder.prototype.GetRange = function()
{
- let max = 2;
- let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
- if (cmpObstruction)
- max += cmpObstruction.GetUnitRadius();
-
- return { "max": max, "min": 0 };
+ return { "max": 1, "min": 0 };
};
Builder.prototype.GetRate = function()
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -502,7 +502,9 @@
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
- "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunSpeedMultiplier()
+ "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunSpeedMultiplier(),
+ "current": cmpUnitMotion.GetSpeed(),
+ "tryingToMove" : cmpUnitMotion.IsTryingToMove()
};
return ret;
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -143,7 +143,7 @@
// as switching states)
},
- "MoveStarted": function() {
+ "enter": function() {
// ignore spurious movement messages
},
@@ -225,16 +225,16 @@
this.FinishOrder();
return;
}
- // Move a tile outside the building
- let range = 4;
- if (this.MoveToTargetRangeExplicit(msg.data.target, range, range))
+ // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5
+ let range = 0.5;
+ if (this.MoveToTargetRangeExplicit(msg.data.target, range))
{
// 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
+ // We can't reach the target
this.FinishOrder();
}
},
@@ -283,7 +283,7 @@
if (!this.order.data.max)
this.MoveToPoint(this.order.data.x, this.order.data.z);
else
- this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max);
+ this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max) / 2.0);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
@@ -344,7 +344,7 @@
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.StopMoving();
this.FinishOrder();
}
@@ -371,16 +371,8 @@
// TODO: what if the units are on a cliff ? the ship will go below the cliff
// and the units won't be able to garrison. Should go to the nearest (accessible) shore
- if (needToMove && this.MoveToTarget(this.order.data.target))
- {
- this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
- }
- else
- {
- // We are already at the target, or can't move at all
- this.StopMoving();
- this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
- }
+ this.MoveToTarget(this.order.data.target, true);
+ this.SetNextState("INDIVIDUAL.PICKUP");
},
"Order.Guard": function(msg) {
@@ -390,7 +382,7 @@
return;
}
- if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange, true))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
@@ -400,7 +392,7 @@
// We use the distance between the entities to account for ranged attacks
var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
+ if (cmpUnitMotion.SetNewDestinationAsEntity(this.order.data.target, distance, true))
{
// We've started fleeing from the given target
if (this.IsAnimal())
@@ -410,7 +402,7 @@
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.StopMoving();
this.FinishOrder();
}
@@ -599,7 +591,7 @@
}
else
{
- // We are already at the target, or can't move at all,
+ // We can't reach the target.
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
@@ -676,7 +668,7 @@
}
else
{
- // We are already at the target, or can't move at all,
+ // We can't reach the target.
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
@@ -782,7 +774,7 @@
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
- if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
+ if (this.MoveToTargetRangeExplicit(this.order.data.target, (this.order.data.min, this.order.data.max) / 2.0))
this.SetNextState("WALKING");
else
this.FinishOrder();
@@ -796,7 +788,7 @@
},
"Order.WalkToPointRange": function(msg) {
- if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
+ if (this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min, this.order.data.max) / 2.0))
this.SetNextState("WALKING");
else
this.FinishOrder();
@@ -1012,24 +1004,25 @@
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
},
-
- "MoveStarted": function() {
- let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
- cmpFormation.SetRearrange(true);
- cmpFormation.MoveMembersIntoFormation(true, true);
- }
},
"WALKING": {
- "MoveStarted": function(msg) {
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ "enter": function(msg) {
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
- if (this.FinishOrder())
- this.CallMemberFunction("ResetFinishOrder", []);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
+ {
+ if (this.FinishOrder()) {
+ this.CallMemberFunction("ResetFinishOrder", []);
+ }
+ }
},
},
@@ -1047,15 +1040,21 @@
this.StopTimer();
},
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
- if (this.FinishOrder())
- this.CallMemberFunction("ResetFinishOrder", []);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
+ {
+ if (this.FinishOrder())
+ this.CallMemberFunction("ResetFinishOrder", []);
+ }
},
},
@@ -1088,28 +1087,34 @@
delete this.patrolStartPosOrder;
},
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function() {
- /**
- * A-B-A-B-..:
- * if the user only commands one patrol order, the patrol will be between
- * the last position and the defined waypoint
- * A-B-C-..-A-B-..:
- * otherwise, the patrol is only between the given patrol commands and the
- * last position is not included (last position = the position where the unit
- * is located at the time of the first patrol order)
- */
-
- if (this.orderQueue.length == 1)
- this.PushOrder("Patrol", this.patrolStartPosOrder);
-
- this.PushOrder(this.order.type, this.order.data);
- this.FinishOrder();
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
+ {
+ /**
+ * A-B-A-B-..:
+ * if the user only commands one patrol order, the patrol will be between
+ * the last position and the defined waypoint
+ * A-B-C-..-A-B-..:
+ * otherwise, the patrol is only between the given patrol commands and the
+ * last position is not included (last position = the position where the unit
+ * is located at the time of the first patrol order)
+ */
+
+ if (this.orderQueue.length == 1)
+ this.PushOrder("Patrol", this.patrolStartPosOrder);
+
+ this.PushOrder(this.order.type, this.order.data);
+ this.FinishOrder();
+ }
},
},
@@ -1135,13 +1140,17 @@
"APPROACHING": {
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
this.SetNextState("GARRISONING");
},
},
@@ -1161,40 +1170,49 @@
},
"FORMING": {
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, false);
},
"MoveCompleted": function(msg) {
-
- if (this.FinishOrder())
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
{
- this.CallMemberFunction("ResetFinishOrder", []);
- return;
+ if (this.FinishOrder())
+ {
+ this.CallMemberFunction("ResetFinishOrder", []);
+ return;
+ }
+ cmpFormation.FindInPosition();
}
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
-
- cmpFormation.FindInPosition();
}
},
"COMBAT": {
"APPROACHING": {
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
- "MoveCompleted": function(msg) {
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
- if (cmpAttack.CanAttackAsFormation())
- this.SetNextState("COMBAT.ATTACKING");
- else
- this.SetNextState("MEMBER");
+ "MoveCompleted": function(msg) {
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true))
+ {
+ var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
+ if (cmpAttack.CanAttackAsFormation())
+ this.SetNextState("COMBAT.ATTACKING");
+ else
+ this.SetNextState("MEMBER");
+ }
},
},
@@ -1352,25 +1370,22 @@
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpFormation && cmpVisual)
- {
- cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk"));
- cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run"));
- }
- this.SelectAnimation("move");
+ this.SetAnimationVariant(cmpFormation.GetFormationAnimation(this.entity, "walk"));
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MoveCompleted": function(msg) {
- // We can only finish this order if the move was really completed.
- if (!msg.data.error && this.FinishOrder())
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (!msg.data.error && this.order.data.target && !cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, 0, 0, true))
return;
+
+ this.StopMoving();
+ this.FinishOrder();
+
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
- {
- cmpVisual.ResetMoveAnimation("walk");
- cmpVisual.ResetMoveAnimation("run");
- }
+ this.SetDefaultAnimationVariant();
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
@@ -1541,15 +1556,12 @@
this.RespondToHealableEntities(msg.data.added);
},
- "MoveStarted": function() {
- this.SelectAnimation("move");
- },
-
- "MoveCompleted": function() {
- this.SelectAnimation("idle");
- },
-
"Timer": function(msg) {
+ // bit of a sanity check, but this happening would most likely mean a bug somewhere.
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (cmpUnitMotion && cmpUnitMotion.IsTryingToMove())
+ warn("Entity " + this.entity + " is in the idle state but trying to move");
+
if (!this.isIdle)
{
this.isIdle = true;
@@ -1559,12 +1571,18 @@
},
"WALKING": {
- "enter": function() {
- this.SelectAnimation("move");
+ "enter": function () {
},
- "MoveCompleted": function() {
- this.FinishOrder();
+ "MoveCompleted": function(msg) {
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (msg.data.error
+ || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1, true)
+ || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, 0, 1, true))
+ {
+ this.StopMoving();
+ this.FinishOrder();
+ }
},
},
@@ -1574,7 +1592,6 @@
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
- this.SelectAnimation("move");
},
"Timer": function(msg) {
@@ -1587,7 +1604,12 @@
},
"MoveCompleted": function() {
- this.FinishOrder();
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1), true)
+ {
+ this.StopMoving();
+ this.FinishOrder();
+ }
},
},
@@ -1609,7 +1631,6 @@
this.StartTimer(0, 1000);
this.SetAnimationVariant("combat");
- this.SelectAnimation("move");
},
"leave": function() {
@@ -1623,6 +1644,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
@@ -1643,7 +1665,6 @@
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
- this.SelectAnimation("move");
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
@@ -1657,6 +1678,24 @@
return;
}
this.SetHeldPositionOnEntity(this.isGuardOf);
+
+ this.SetMoveSpeedRatio(this.GetWalkSpeed());
+
+ // Adapt the speed to the one of the target if needed
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3*this.guardRange, true))
+ {
+ var cmpOtherMotion = Engine.QueryInterface(this.isGuardOf, IID_UnitMotion);
+ if (cmpOtherMotion)
+ {
+ let otherSpeed = cmpOtherMotion.GetSpeed();
+ let mySpeed = cmpUnitMotion.GetSpeed();
+ let speed = otherSpeed / mySpeed;
+ if (speed < this.GetWalkSpeed())
+ this.SetMoveSpeedRatio(speed);
+ }
+ }
},
"leave": function(msg) {
@@ -1665,7 +1704,7 @@
this.SetDefaultAnimationVariant();
},
- "MoveStarted": function(msg) {
+ "enter": function(msg) {
// Adapt the speed to the one of the target if needed
var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, true))
@@ -1681,8 +1720,9 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.ResetMoveSpeed();
- if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ if (!this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange))
this.SetNextState("GUARDING");
},
},
@@ -1710,7 +1750,8 @@
return;
}
// then check is the target has moved
- if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ // TODO: this should call isInRange, not this.
+ if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange))
this.SetNextState("ESCORTING");
else
{
@@ -1736,10 +1777,7 @@
"FLEEING": {
"enter": function() {
this.PlaySound("panic");
-
- // Run quickly
- this.SelectAnimation("move");
- this.SetMoveSpeedRatio(this.GetRunMultiplier());
+ this.ResetMoveSpeed();
},
"HealthChanged": function() {
@@ -1752,6 +1790,7 @@
"MoveCompleted": function() {
// When we've run far enough, stop fleeing
+ this.StopMoving();
this.FinishOrder();
},
@@ -1776,7 +1815,6 @@
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
- this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
@@ -1797,10 +1835,19 @@
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
+ else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
+ {
+ this.StopMoving();
+ // If the unit needs to unpack, do so
+ if (this.CanUnpack())
+ this.SetNextState("UNPACKING");
+ else
+ this.SetNextState("ATTACKING");
+ }
},
"MoveCompleted": function() {
-
+ this.StopMoving();
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
@@ -2026,13 +2073,11 @@
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
- this.SelectAnimation("move");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
- {
// Run after a fleeing target
this.SetMoveSpeedRatio(this.GetRunMultiplier());
- }
+
this.StartTimer(1000, 1000);
},
@@ -2062,9 +2107,19 @@
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
+ else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
+ {
+ this.StopMoving();
+ // If the unit needs to unpack, do so
+ if (this.CanUnpack())
+ this.SetNextState("UNPACKING");
+ else
+ this.SetNextState("ATTACKING");
+ }
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("ATTACKING");
},
},
@@ -2073,7 +2128,6 @@
"GATHER": {
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
@@ -2133,12 +2187,14 @@
}
return true;
}
+ this.StartTimer(0, 500);
return false;
},
"MoveCompleted": function(msg) {
if (msg.data.error)
{
+ this.StopMoving();
// We failed to reach the target
// remove us from the list of entities gathering from Resource.
@@ -2179,12 +2235,24 @@
this.PerformGather(oldTarget, false, false);
return;
}
+ else if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
+ {
+ this.StopMoving();
+ // We reached the target - start gathering from it now
+ this.SetNextState("GATHERING");
+ }
+ },
- // We reached the target - start gathering from it now
- this.SetNextState("GATHERING");
+ "Timer": function() {
+ if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
+ {
+ this.StopMoving();
+ this.SetNextState("GATHERING");
+ }
},
"leave": function() {
+ this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
@@ -2198,10 +2266,12 @@
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
- this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
+ if (msg.data.error)
+ this.StopMoving();
+
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
@@ -2233,7 +2303,6 @@
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
-
// No dropsites, just give up
},
},
@@ -2282,23 +2351,23 @@
return true;
}
- // Scale timing interval based on rate, and start timer
- // The offset should be at least as long as the repeat time so we use the same value for both.
- var offset = 1000/rate;
- var repeat = offset;
- this.StartTimer(offset, repeat);
-
- // We want to start the gather animation as soon as possible,
- // but only if we're actually at the target and it's still alive
- // (else it'll look like we're chopping empty air).
- // (If it's not alive, the Timer handler will deal with sending us
- // off to a different target.)
+ // Range check: if we are in-range, start the gathering animation and set the timer
+ // If we are not in-range, we'll set a different timer to avoid waiting an inordinate amount of time.
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetDefaultAnimationVariant();
+ this.FaceTowardsTarget(this.gatheringTarget);
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename);
+
+ // Scale timing interval based on rate, and start timer
+ // The offset should be at least as long as the repeat time so we use the same value for both.
+ var offset = 1000/rate;
+ var repeat = offset;
+ this.StartTimer(offset, repeat);
}
+ else
+ this.StartTimer(0, 1000);
return false;
},
@@ -2383,10 +2452,9 @@
// the old one. So try to get close to the old resource's
// last known position
- var maxRange = 8; // get close but not too close
+ var range = 4; // get close but not too close
if (this.order.data.lastPos &&
- this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
- 0, maxRange))
+ this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, range))
{
this.SetNextState("APPROACHING");
return;
@@ -2461,7 +2529,6 @@
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
@@ -2482,6 +2549,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("HEALING");
},
},
@@ -2512,6 +2580,7 @@
},
"leave": function() {
+ this.SelectAnimation("idle");
this.StopTimer();
},
@@ -2567,7 +2636,6 @@
},
"CHASING": {
"enter": function() {
- this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
@@ -2586,6 +2654,7 @@
}
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("HEALING");
},
},
@@ -2595,19 +2664,41 @@
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
+ this.StartTimer(0, 1000);
},
- "MoveCompleted": function() {
- // Switch back to idle animation to guarantee we won't
- // get stuck with the carry animation after stopping moving
- this.SelectAnimation("idle");
+ "leave": function() {
+ this.StopTimer();
+ },
+ "Timer": function() {
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
- if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
+ if (!this.CanReturnResource(this.order.data.target, true))
{
+ this.StopMoving();
+ // The dropsite was destroyed, or we couldn't reach it, or ownership changed
+ // Look for a new one.
+
+ var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
+ var genericType = cmpResourceGatherer.GetMainCarryingType();
+ var nearby = this.FindNearestDropsite(genericType);
+ if (nearby)
+ {
+ this.FinishOrder();
+ this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
+ return;
+ }
+
+ // Oh no, couldn't find any drop sites. Give up on returning.
+ this.FinishOrder();
+ return;
+ }
+ if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
+ {
+ this.StopMoving();
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
+ // this ought to be redundant with the above check.
if (cmpResourceDropsite)
{
// Dump any resources we can
@@ -2625,23 +2716,9 @@
return;
}
}
-
- // The dropsite was destroyed, or we couldn't reach it, or ownership changed
- // Look for a new one.
-
- var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
- var genericType = cmpResourceGatherer.GetMainCarryingType();
- var nearby = this.FindNearestDropsite(genericType);
- if (nearby)
- {
- this.FinishOrder();
- this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
- return;
- }
-
- // Oh no, couldn't find any drop sites. Give up on returning.
- this.FinishOrder();
},
+
+ "MoveCompleted": "Timer",
},
},
@@ -2653,18 +2730,28 @@
"APPROACHINGMARKET": {
"enter": function() {
- this.SelectAnimation("move");
+ this.StartTimer(1000, 1000);
},
- "MoveCompleted": function() {
- if (this.waypoints && this.waypoints.length)
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckTargetRange(this.order.data.target, IID_Trader))
{
- if (!this.MoveToMarket(this.order.data.target))
- this.StopTrading();
+ this.StopMoving();
+ if (this.waypoints && this.waypoints.length)
+ {
+ if (!this.MoveToMarket(this.order.data.target))
+ this.StopTrading();
+ }
+ else
+ this.PerformTradeAndMoveToNextMarket(this.order.data.target);
}
- else
- this.PerformTradeAndMoveToNextMarket(this.order.data.target);
},
+
+ "MoveCompleted": "Timer",
},
"TradingCanceled": function(msg) {
@@ -2674,17 +2761,30 @@
let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
this.StopTrading();
if (otherMarket)
- this.WalkToTarget(otherMarket);
+ this.g(otherMarket);
},
},
"REPAIR": {
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
+ this.StartTimer(0, 1000);
},
- "MoveCompleted": function() {
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckTargetRange(this.order.data.target, IID_Builder))
+ {
+ this.StopMoving();
+ this.SetNextState("REPAIRING");
+ }
+ },
+ // TODO: clean this up when MoveCompleted becomes MoveSuccesHint and MoveFailure or something
+ "MoveCompleted": function(msg) {
+ this.StopMoving();
this.SetNextState("REPAIRING");
},
},
@@ -2729,6 +2829,8 @@
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
+ this.FaceTowardsTarget(this.repairTarget);
+
this.SelectAnimation("build");
this.StartTimer(1000, 1000);
return false;
@@ -2739,6 +2841,7 @@
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
+ this.SelectAnimation("idle");
this.StopTimer();
},
@@ -2758,10 +2861,13 @@
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
- if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
- this.SetNextState("APPROACHING");
- else if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
- this.FinishOrder(); //can't approach and isn't in reach
+ if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
+ {
+ if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
+ this.SetNextState("APPROACHING");
+ else
+ this.FinishOrder(); //can't approach and isn't in reach
+ }
},
},
@@ -2878,12 +2984,39 @@
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
+ this.StartTimer(1000,1000);
},
- "MoveCompleted": function() {
- this.SetNextState("GARRISONED");
+ "leave": function() {
+ this.StopTimer();
},
+
+ "Timer": function() {
+ if (this.IsUnderAlert() && this.alertGarrisoningTarget)
+ {
+ // check that we can garrison in the building we're supposed to garrison in
+ var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
+ {
+ // Try to find another nearby building
+ var nearby = this.FindNearbyGarrisonHolder();
+ if (nearby)
+ {
+ this.alertGarrisoningTarget = nearby;
+ this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget});
+ }
+ else
+ this.FinishOrder();
+ return;
+ }
+ }
+ if (this.order.data.target && this.CheckGarrisonRange(this.order.data.target))
+ this.SetNextState("GARRISONED");
+ else if (this.alertGarrisoningTarget && this.CheckGarrisonRange(this.alertGarrisoningTarget))
+ this.SetNextState("GARRISONED");
+ },
+
+ "MoveCompleted": "Timer",
},
"GARRISONED": {
@@ -2971,7 +3104,7 @@
}
}
- if (this.MoveToTarget(target))
+ if (this.MoveToTarget(target, true))
{
this.SetNextState("APPROACHING");
return false;
@@ -3002,6 +3135,7 @@
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
+ this.SelectAnimation("idle");
},
"Timer": function(msg) {
@@ -3046,36 +3180,19 @@
},
"PICKUP": {
- "APPROACHING": {
- "enter": function() {
- this.SelectAnimation("move");
- },
-
- "MoveCompleted": function() {
- this.SetNextState("LOADING");
- },
-
- "PickupCanceled": function() {
- this.StopMoving();
+ "enter": function() {
+ var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
+ {
this.FinishOrder();
- },
+ return true;
+ }
+ return false;
},
- "LOADING": {
- "enter": function() {
- this.SelectAnimation("idle");
- var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
- if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
- {
- this.FinishOrder();
- return true;
- }
- return false;
- },
-
- "PickupCanceled": function() {
- this.FinishOrder();
- },
+ "PickupCanceled": function() {
+ this.StopMoving();
+ this.FinishOrder();
},
},
},
@@ -3100,16 +3217,16 @@
},
"Order.LeaveFoundation": function(msg) {
- // Move a tile outside the building
- var range = 4;
- if (this.MoveToTargetRangeExplicit(msg.data.target, range, range))
+ // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5
+ var range = 0.5;
+ if (this.MoveToTargetRangeExplicit(msg.data.target, range))
{
// We've started walking to the given point
this.SetNextState("WALKING");
}
else
{
- // We are already at the target, or can't move at all
+ // We cannot reach the target.
this.FinishOrder();
}
},
@@ -3127,8 +3244,6 @@
"ROAMING": {
"enter": function() {
// Walk in a random direction
- this.SelectAnimation("walk", false, 1);
- this.SetFacePointAfterMove(false);
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
@@ -3136,7 +3251,6 @@
"leave": function() {
this.StopTimer();
- this.SetFacePointAfterMove(true);
},
"LosRangeUpdate": function(msg) {
@@ -3166,6 +3280,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.MoveRandomly(+this.template.RoamDistance);
},
},
@@ -3198,7 +3313,7 @@
}
},
- "MoveCompleted": function() { },
+ "MoveCompleted": function() { this.StopMoving(); },
"Timer": function(msg) {
this.SetNextState("ROAMING");
@@ -3569,6 +3684,9 @@
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
+ // Safety net, in general it's better if unitAI states handle this properly.
+ this.StopMoving();
+
this.orderQueue.shift();
this.order = this.orderQueue[0];
@@ -3718,6 +3836,7 @@
this.UpdateWorkOrders(type);
}
+ this.StopMoving();
let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null;
// Special cases of orders that shouldn't be replaced:
@@ -3905,12 +4024,15 @@
//// Message handlers /////
-UnitAI.prototype.OnMotionChanged = function(msg)
+UnitAI.prototype.OnMovePaused = function(msg)
{
- if (msg.starting && !msg.error)
- this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
- else if (!msg.starting || msg.error)
- this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
+ // TODO: change this. Doesn't matter if UnitAI thinks it's completed for now since anyways the states do range checks.
+ this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : false }});
+};
+
+UnitAI.prototype.OnMoveFailure = function(msg)
+{
+ this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }});
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
@@ -4216,6 +4338,12 @@
return;
}
+ // Set default values if unspecified
+ if (once === undefined)
+ once = false;
+ if (speed === undefined)
+ speed = 1.0;
+
cmpVisual.SelectAnimation(name, once, speed);
};
@@ -4232,31 +4360,34 @@
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- cmpUnitMotion.StopMoving();
+ cmpUnitMotion.DiscardMove();
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
+ cmpUnitMotion.SetAbortIfStuck(30);
+ return cmpUnitMotion.SetNewDestinationAsPosition(x, z, 0, true);
};
-UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
+UnitAI.prototype.MoveToPointRange = function(x, z, range, evenUnreachable = false)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
+ cmpUnitMotion.SetAbortIfStuck(30);
+ return cmpUnitMotion.SetNewDestinationAsPosition(x, z, range, evenUnreachable);
};
-UnitAI.prototype.MoveToTarget = function(target)
+UnitAI.prototype.MoveToTarget = function(target, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, 0, evenUnreachable);
};
-UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
+UnitAI.prototype.MoveToTargetRange = function(target, iid, type, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
@@ -4267,7 +4398,10 @@
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ // generally speaking, try to aim for the middle of a range.
+ //
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable);
};
/**
@@ -4275,7 +4409,7 @@
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
-UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
+UnitAI.prototype.MoveToTargetAttackRange = function(target, type, evenUnreachable = false)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
@@ -4290,7 +4424,7 @@
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
- return this.MoveToTargetRange(target, IID_Attack, type);
+ return this.MoveToTargetRange(target, IID_Attack, type, evenUnreachable);
if (!this.CheckTargetVisible(target))
return false;
@@ -4322,24 +4456,27 @@
// the parabole changes while walking, take something in the middle
var guessedMaxRange = (range.max + parabolicMaxRange)/2;
+// TODO: here we should give the desired range based on unit speed, our own desire to walk, and so on.
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
+ cmpUnitMotion.SetAbortIfStuck(9);
+ if (cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + guessedMaxRange)/2.0, false))
return true;
// if that failed, try closer
- return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + Math.min(range.max, parabolicMaxRange))/2.0, evenUnreachable);
};
-UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
+UnitAI.prototype.MoveToTargetRangeExplicit = function(target, range, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, min, max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable);
};
-UnitAI.prototype.MoveToGarrisonRange = function(target)
+UnitAI.prototype.MoveToGarrisonRange = function(target, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target))
return false;
@@ -4350,12 +4487,13 @@
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
- var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, true);
};
@@ -4366,7 +4504,7 @@
return false;
var range = cmpRanged.GetRange(type);
- var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true);
};
@@ -4415,13 +4553,13 @@
if (maxRangeSq < 0)
return false;
- var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq), true);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
- var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, true);
};
@@ -4436,7 +4574,7 @@
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
- var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true);
};
@@ -4992,7 +5130,7 @@
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
- this.MoveToTargetRange(target, IID_Heal);
+ this.MoveToTargetRange(target, IID_Heal, true);
else
this.WalkToTarget(target, queued);
return;
@@ -5867,21 +6005,9 @@
else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
this.roamAngle *= randBool() ? 1 : -1;
- let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
- // First half rotation to decrease the impression of immediate rotation
- ang += halfDelta;
- cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
- // Then second half of the rotation
- ang += halfDelta;
+ ang += randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4) * 2.0;
let dist = randFloat(0.5, 1.5) * distance;
- cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, dist);
-};
-
-UnitAI.prototype.SetFacePointAfterMove = function(val)
-{
- var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpMotion)
- cmpMotion.SetFacePointAfterMove(val);
+ cmpUnitMotion.SetNewDestinationAsPosition(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, true);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitMotionFlying.js
+++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js
@@ -203,7 +203,7 @@
if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange)
{
this.reachedTarget = true;
- Engine.PostMessage(this.entity, MT_MotionChanged, { "starting": false, "error": false });
+ Engine.PostMessage(this.entity, MT_MovePaused, {});
}
// If we're facing away from the target, and are still fairly close to it,
@@ -245,33 +245,43 @@
cmpPosition.MoveTo(pos.x, pos.z);
};
-UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
+UnitMotionFlying.prototype.SetNewDestinationAsPosition = function(x, z, range)
{
this.hasTarget = true;
this.landing = false;
this.reachedTarget = false;
this.targetX = x;
this.targetZ = z;
- this.targetMinRange = minRange;
- this.targetMaxRange = maxRange;
+ this.targetMinRange = 0;
+ this.targetMaxRange = range;
+
+ // we'll tell the visual actor to set our animation here.
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (cmpVisual)
+ cmpVisual.SetMovingSpeed(this.speed);
return true;
};
-UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
+UnitMotionFlying.prototype.SetNewDestinationAsEntity = function(target, range)
{
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return false;
+ // we'll tell the visual actor to set our animation here.
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (cmpVisual)
+ cmpVisual.SetMovingSpeed(this.speed);
+
var targetPos = cmpTargetPosition.GetPosition2D();
this.hasTarget = true;
this.reachedTarget = false;
this.targetX = targetPos.x;
this.targetZ = targetPos.y;
- this.targetMinRange = minRange;
- this.targetMaxRange = maxRange;
+ this.targetMinRange = 0;
+ this.targetMaxRange = range;
return true;
};
@@ -306,22 +316,17 @@
return this.template.PassabilityClass;
};
-UnitMotionFlying.prototype.GetPassabilityClass = function()
+UnitMotionFlying.prototype.IsTryingToMove = function()
{
- return this.passabilityClass;
-};
+ return false;
+}
UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
{
// Ignore this - angle is controlled by the target-seeking code instead
};
-UnitMotionFlying.prototype.SetFacePointAfterMove = function()
-{
- // Ignore this - angle is controlled by the target-seeking code instead
-};
-
-UnitMotionFlying.prototype.StopMoving = function()
+UnitMotionFlying.prototype.DiscardMove = function()
{
//Invert
if (!this.waterDeath)
Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
+++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
@@ -63,9 +63,6 @@
GetEnemies: function() { return []; },
});
- AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
- IsInTargetRange: function(ent, target, min, max, opposite) { return true; }
- });
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
@@ -88,6 +85,7 @@
AddMock(unit, IID_UnitMotion, {
GetWalkSpeed: function() { return 1; },
MoveToFormationOffset: function(target, x, z) { },
+ IsInTargetRange: function(target, min, max) { return true; },
MoveToTargetRange: function(target, min, max) { },
StopMoving: function() { },
GetPassabilityClassName: function() { return "default"; },
@@ -139,8 +137,7 @@
});
AddMock(controller, IID_UnitMotion, {
- GetWalkSpeed: function() { return 1; },
- SetSpeedRatio: function(speed) { },
+ SetSpeed: function(speed) { },
MoveToPointRange: function(x, z, minRange, maxRange) { },
GetPassabilityClassName: function() { return "default"; },
});
@@ -207,10 +204,6 @@
GetNumPlayers: function() { return 2; },
});
- AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
- IsInTargetRange: function(ent, target, min, max) { return true; }
- });
-
AddMock(playerEntity, IID_Player, {
IsAlly: function() { return false; },
IsEnemy: function() { return true; },
@@ -243,6 +236,7 @@
AddMock(unit + i, IID_UnitMotion, {
GetWalkSpeed: function() { return 1; },
MoveToFormationOffset: function(target, x, z) { },
+ IsInTargetRange: function(target, min, max) { return true; },
MoveToTargetRange: function(target, min, max) { },
StopMoving: function() { },
GetPassabilityClassName: function() { return "default"; },
@@ -286,8 +280,7 @@
});
AddMock(controller, IID_UnitMotion, {
- GetWalkSpeed: function() { return 1; },
- SetSpeedRatio: function(speed) { },
+ SetSpeed: function(speed) { },
MoveToPointRange: function(x, z, minRange, maxRange) { },
IsInTargetRange: function(target, min, max) { return true; },
StopMoving: function() { },
Index: binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js
+++ binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js
@@ -35,7 +35,6 @@
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClassName(), "unrestricted");
-TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClass(), 1 << 8);
AddMock(entity, IID_Position, {
"IsInWorld": () => true,
@@ -78,8 +77,8 @@
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0);
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedRatio(), 0);
-TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToTargetRange(target, 0, 10), true);
-TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToPointRange(100, 200, 0, 20), true);
+TS_ASSERT_EQUALS(cmpUnitMotionFlying.SetNewDestinationAsEntity(target, 10), true);
+TS_ASSERT_EQUALS(cmpUnitMotionFlying.SetNewDestinationAsPosition(100, 200, 20), true);
// Take Off
cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 });
@@ -111,7 +110,7 @@
TS_ASSERT_EQUALS(height, 105);
// Land
-cmpUnitMotionFlying.StopMoving();
+cmpUnitMotionFlying.DiscardMove();
cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 });
TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1);
TS_ASSERT_EQUALS(height, 105);
Index: binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit.xml
+++ binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -69,7 +69,7 @@
- 2.0
+ 1.0
1.0
1
Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
@@ -56,7 +56,7 @@
150
- 2.0
+ 1.6
1.0
5
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
@@ -75,7 +75,6 @@
100
- 2.0
1.0
0.5
Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
@@ -50,7 +50,6 @@
- 2.0
1.0
1
Index: source/simulation2/MessageTypes.h
===================================================================
--- source/simulation2/MessageTypes.h
+++ source/simulation2/MessageTypes.h
@@ -317,21 +317,31 @@
};
/**
- * Sent by CCmpUnitMotion during Update, whenever the motion status has changed
- * since the previous update.
+ * Sent by CCmpUnitMotion as a hint whenever the unit stops moving
+ * Thus it will be sent if the unit's path is blocked, or if it's reached its destination.
*/
-class CMessageMotionChanged : public CMessage
+class CMessageMovePaused : public CMessage
{
public:
- DEFAULT_MESSAGE_IMPL(MotionChanged)
+ DEFAULT_MESSAGE_IMPL(MovePaused)
- CMessageMotionChanged(bool starting, bool error) :
- starting(starting), error(error)
+ CMessageMovePaused()
{
}
+};
+
+/**
+ * Sent by CCmpUnitMotion when a unit has determined it has no chance
+ * of ever reaching its assigned destination. This is a catastrophic error.
+ */
+class CMessageMoveFailure : public CMessage
+{
+public:
+ DEFAULT_MESSAGE_IMPL(MoveFailure)
- bool starting; // whether this is a start or end of movement
- bool error; // whether we failed to start moving (couldn't find any path)
+ CMessageMoveFailure()
+ {
+ }
};
/**
Index: source/simulation2/TypeList.h
===================================================================
--- source/simulation2/TypeList.h
+++ source/simulation2/TypeList.h
@@ -45,7 +45,8 @@
MESSAGE(PositionChanged)
MESSAGE(InterpolatedPositionChanged)
MESSAGE(TerritoryPositionChanged)
-MESSAGE(MotionChanged)
+MESSAGE(MovePaused)
+MESSAGE(MoveFailure)
MESSAGE(RangeUpdate)
MESSAGE(TerrainChanged)
MESSAGE(VisibilityChanged)
@@ -187,6 +188,9 @@
COMPONENT(UnitMotion) // must be after Obstruction
COMPONENT(UnitMotionScripted)
+INTERFACE(UnitMotionManager)
+COMPONENT(UnitMotionManager)
+
INTERFACE(UnitRenderer)
COMPONENT(UnitRenderer)
Index: source/simulation2/components/CCmpUnitMotion.cpp
===================================================================
--- source/simulation2/components/CCmpUnitMotion.cpp
+++ source/simulation2/components/CCmpUnitMotion.cpp
@@ -27,6 +27,8 @@
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpValueModificationManager.h"
+#include "simulation2/components/ICmpVisual.h"
+#include "simulation2/components/ICmpUnitMotionManager.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/MessageTypes.h"
@@ -39,217 +41,214 @@
#include "ps/Profile.h"
#include "renderer/Scene.h"
-// For debugging; units will start going straight to the target
-// instead of calling the pathfinder
-#define DISABLE_PATHFINDER 0
-
-/**
- * When advancing along the long path, and picking a new waypoint to move
- * towards, we'll pick one that's up to this far from the unit's current
- * position (to minimise the effects of grid-constrained movement)
- */
-static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*8);
-
/**
- * Min/Max range to restrict short path queries to. (Larger ranges are slower,
- * smaller ranges might miss some legitimate routes around large obstacles.)
+ * Minimal / usual / maximal range for a short path query.
+ * If the desired range for a short path goes beyond Max range, it gets hotswapped into a long range first
+ * as otherwise we can get large lag spikes.
*/
static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
-static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*9);
-
-/**
- * Minimum distance to goal for a long path request
- */
-static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
+static const entity_pos_t SHORT_PATH_NORMAL_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*7);
+static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16);
/**
- * When short-pathing, and the short-range pathfinder failed to return a path,
- * Assume we are at destination if we are closer than this distance to the target
- * And we have no target entity.
- * This is somewhat arbitrary, but setting a too big distance means units might lose sight of their end goal too much;
+ * Below this distance to the goal, if we're getting obstructed, we will recreate a brand new Goal for our short-pathfinder
+ * Instead of using the one given to us by RecomputeGoalPosition.
+ * This is unsafe from a shot/long pathfinder compatibility POV, so it should not be too big.
*/
-static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
+static const entity_pos_t SHORT_PATH_GOAL_REDUX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3);
/**
- * If we are this close to our target entity/point, then think about heading
- * for it in a straight line instead of pathfinding.
+ * When we run in an obstruction and request a new short path, we'll drop waypoints that are
+ * close to our position, as defined by this constant.
+ * This is to avoid paths getting increasingly large and tangled and weird when units get repeatedly stuck.
+ * This would not be as necessary if we had pushing and could differentiate between unit/unit and unit/static collisions
+ * and oculd also be mitigated by checking (somehow) our path has grown larger than initially anticipated.
+ * This should probably be lower or at least cose to SHORT_PATH_GOAL_REDUX_DIST
*/
-static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
+static const entity_pos_t DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3);
-/**
- * If we're following a target entity,
- * we will recompute our path if the target has moved
- * more than this distance from where we last pathed to.
+/*
+ * How far ahead we'll check for collisions, to try and anticipate problems.
+ * This should be lowered whenever MP turns are lowered.
*/
-static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
+static const entity_pos_t LOOKAHEAD_DISTANCE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3);
/**
- * If we're following as part of a formation,
- * but can't move to our assigned target point in a straight line,
- * we will recompute our path if the target has moved
- * more than this distance from where we last pathed to.
+ * Minimum distance to goal for a long path request
+ * Disabled, see note in RequestNewPath.
*/
-static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
+static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0);
/**
- * If we're following something but it's more than this distance away along
- * our path, then don't bother trying to repath regardless of how much it has
- * moved, until we get this close to the end of our old path.
+ * If we are this close to our target entity/point, then think about heading
+ * for it in a straight line instead of pathfinding.
+ * TODO: this should probably be reintroduced
*/
-static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16);
+static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
/**
- * If we're following something and the angle between the (straight-line) directions to its previous target
- * position and its present target position is greater than a given angle, recompute the path even far away
- * (i.e. even if CHECK_TARGET_MOVEMENT_AT_MAX_DIST condition is not fulfilled). The actual check is done
- * on the cosine of this angle, with a PI/6 angle.
+ * See unitmotion logic for details. Higher means units will retry more often before potentially failing.
*/
-static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000;
+static const size_t MAX_PATH_REATTEMPTS = 8;
-static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
-static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
+static const CColor OVERLAY_COLOR_PATH(1, 0, 0, 1);
+static const CColor OVERLAY_COLOR_ALTERNATIVE_PATH(0,1,0,1);
class CCmpUnitMotion : public ICmpUnitMotion
{
-public:
- static void ClassInit(CComponentManager& componentManager)
+private:
+ struct SMotionGoal
{
- componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
- componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
- componentManager.SubscribeToMessageType(MT_PathResult);
- componentManager.SubscribeToMessageType(MT_OwnershipChanged);
- componentManager.SubscribeToMessageType(MT_ValueModification);
- componentManager.SubscribeToMessageType(MT_Deserialized);
- }
+ private:
+ bool m_Valid = false;
- DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
+ entity_pos_t m_Range;
+ entity_id_t m_Entity;
+ CFixedVector2D m_Position;
- bool m_DebugOverlayEnabled;
- std::vector m_DebugOverlayLongPathLines;
- std::vector m_DebugOverlayShortPathLines;
+ public:
+ SMotionGoal() : m_Valid(false) {};
- // Template state:
+ SMotionGoal(CFixedVector2D position, entity_pos_t range)
+ {
+ m_Entity = INVALID_ENTITY;
+ m_Range = range;
+ m_Position = position;
- bool m_FormationController;
+ m_Valid = true;
+ }
- fixed m_TemplateWalkSpeed, m_TemplateRunSpeedMultiplier;
- pass_class_t m_PassClass;
- std::string m_PassClassName;
+ SMotionGoal(entity_id_t target, entity_pos_t range)
+ {
+ m_Entity = target;
+ m_Range = range;
+ m_Position = CFixedVector2D(fixed::Zero(), fixed::Zero());
- // Dynamic state:
+ m_Valid = true;
+ }
- entity_pos_t m_Clearance;
+ // For formations only
+ SMotionGoal(entity_id_t target, entity_pos_t x, entity_pos_t z)
+ {
+ m_Entity = target;
+ m_Range = fixed::Zero();
- // cached for efficiency
- fixed m_WalkSpeed, m_RunSpeedMultiplier;
+ m_Position = CFixedVector2D(x,z);
- bool m_Moving;
- bool m_FacePointAfterMove;
+ m_Valid = true;
+ }
- enum State
- {
- /*
- * Not moving at all.
- */
- STATE_IDLE,
+ template
+ void SerializeCommon(S& serialize)
+ {
+ serialize.Bool("valid", m_Valid);
- /*
- * Not moving at all. Will go to IDLE next turn.
- * (This one-turn delay is a hack to fix animation timings.)
- */
- STATE_STOPPING,
+ serialize.NumberFixed_Unbounded("range", m_Range);
- /*
- * Member of a formation.
- * Pathing to the target (depending on m_PathState).
- * Target is m_TargetEntity plus m_TargetOffset.
- */
- STATE_FORMATIONMEMBER_PATH,
+ serialize.NumberU32_Unbounded("entity", m_Entity);
- /*
- * Individual unit or formation controller.
- * Pathing to the target (depending on m_PathState).
- * Target is m_TargetPos, m_TargetMinRange, m_TargetMaxRange;
- * if m_TargetEntity is not INVALID_ENTITY then m_TargetPos is tracking it.
- */
- STATE_INDIVIDUAL_PATH,
+ serialize.NumberFixed_Unbounded("x", m_Position.X);
+ serialize.NumberFixed_Unbounded("y", m_Position.Y);
+ }
- STATE_MAX
- };
- u8 m_State;
+ bool IsEntity() const { return m_Entity != INVALID_ENTITY; }
+ entity_id_t GetEntity() const { return m_Entity; }
- enum PathState
- {
- /*
- * There is no path.
- * (This should only happen in IDLE and STOPPING.)
- */
- PATHSTATE_NONE,
+ bool Valid() const { return m_Valid; }
+ void Clear() { m_Valid = false; }
- /*
- * We have an outstanding long path request.
- * No paths are usable yet, so we can't move anywhere.
- */
- PATHSTATE_WAITING_REQUESTING_LONG,
+ bool IsFormationGoal() const { return m_Valid && IsEntity() && !m_Position.IsZero(); }
+ entity_pos_t Range() const { return m_Range; };
- /*
- * We have an outstanding short path request.
- * m_LongPath is valid.
- * m_ShortPath is not yet valid, so we can't move anywhere.
- */
- PATHSTATE_WAITING_REQUESTING_SHORT,
+ CFixedVector2D GetPosition() const { return m_Position; }
+ };
- /*
- * We are following our path, and have no path requests.
- * m_LongPath and m_ShortPath are valid.
- */
- PATHSTATE_FOLLOWING,
+public:
+ static void ClassInit(CComponentManager& componentManager)
+ {
+ componentManager.SubscribeToMessageType(MT_PathResult);
+ componentManager.SubscribeToMessageType(MT_OwnershipChanged);
+ componentManager.SubscribeToMessageType(MT_ValueModification);
+ componentManager.SubscribeToMessageType(MT_Deserialized);
+ }
- /*
- * We are following our path, and have an outstanding long path request.
- * (This is because our target moved a long way and we need to recompute
- * the whole path).
- * m_LongPath and m_ShortPath are valid.
- */
- PATHSTATE_FOLLOWING_REQUESTING_LONG,
+ DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
- /*
- * We are following our path, and have an outstanding short path request.
- * (This is because our target moved and we've got a new long path
- * which we need to follow).
- * m_LongPath is valid; m_ShortPath is valid but obsolete.
- */
- PATHSTATE_FOLLOWING_REQUESTING_SHORT,
+ bool m_DebugOverlayEnabled;
+ std::vector m_DebugOverlayPathLines,m_DebugOverlayPathLines1;
- PATHSTATE_MAX
- };
- u8 m_PathState;
+ // Template state, never changed after init.
+ fixed m_TemplateWalkSpeed, m_TemplateRunSpeedMultiplier;
+ pass_class_t m_PassClass;
+ std::string m_PassClassName;
+ entity_pos_t m_Clearance;
+ bool m_FormationController;
- u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none
+ // cached for efficiency
+ fixed m_WalkSpeed, m_RunSpeedMultiplier;
- entity_id_t m_TargetEntity;
- CFixedVector2D m_TargetPos;
- CFixedVector2D m_TargetOffset;
- entity_pos_t m_TargetMinRange;
- entity_pos_t m_TargetMaxRange;
+ // TARGET
+ // As long as we have a valid destination, the unit is seen as trying to move
+ // It may not be actually moving for a variety of reasons (no path, blocked path)…
+ // this is our final destination
+ SMotionGoal m_Destination;
+ // this is a "temporary" destination. Most of the time it will be the same as m_Destination,
+ // but it doesn't have to be.
+ // Can be used to temporarily re-route or pause a unit from a given component, for whatever reason.
+ // Should also probably be used whenever we implement step-by-step long paths using the hierarchical pathfinder.
+ SMotionGoal m_CurrentGoal;
+
+ // Pathfinder-compliant goal. Should be reachable (at least when it's recomputed).
+ PathGoal m_Goal;
+
+ // MOTION PLANNING
+ // We will abort if we are stuck after X tries, where X is the value in m_AbortIfStuck
+ u8 m_AbortIfStuck;
+ // actual unit speed, after technology and ratio
- // Actual unit speed, after technology and ratio
fixed m_Speed;
// Convenience variable to avoid recomputing the ratio every time. Synchronised.
fixed m_SpeedRatio;
- // Current mean speed (over the last turn).
- fixed m_CurSpeed;
-
- // Currently active paths (storing waypoints in reverse order).
- // The last item in each path is the point we're currently heading towards.
- WaypointPath m_LongPath;
- WaypointPath m_ShortPath;
-
- // Motion planning
- u8 m_Tries; // how many tries we've done to get to our current Final Goal.
-
- PathGoal m_FinalGoal;
+ // Metadata about our path requests.
+ struct SPathRequest
+ {
+ // 0 if none, otherwise the ID of the path request to drop outdated requests should that happen.
+ u32 expectedPathTicket;
+
+ // If true, then the existing path will be scrapped.
+ bool dumpExistingPath;
+ // if true, some sanity checks will be performed on the returned path
+ bool runShortPathValidation;
+ // if true, this new path will not be added to the current path but instead to the alternative path, which may or may not get used.
+ bool isAlternativePath;
+ };
+ SPathRequest m_PathRequest;
+
+ // Currently active path (storing waypoints in reverse order).
+ WaypointPath m_Path;
+ // Alternative path. If the alternative path exists (ie not empty) and we run into an obstruction
+ // along the current path, we will in the same turn try the alternative path. If that fails too, we go through
+ // the normal "path-fixing" methods. If it succeeds, however, we swap the current path and the alternative path.
+ // this allows one-turn anticipation of obstacles without breaking everything and with as little downtime as possible.
+ // The alternative path is currently kept alive for one turn only.
+ WaypointPath m_AlternativePath;
+
+ // used for the short pathfinder, incremented on each unsuccessful try.
+ u8 m_Tries;
+ // Turns to wait before a certain action. Works in concert with m_AbortIfStuck.
+ u8 m_WaitingTurns;
+ // if we actually started moving at some point.
+ bool m_StartedMoving;
+
+ // Speed over the last turn
+ // cached so we can tell the visual actor when it changes
+ fixed m_ActualSpeed;
+
+ // Movement metadata.
+ // Valid after TryMoving until the end of the turn.
+ // Would be external but it's costly to move it around.
+ MovementMetadata m_MoveMetadata;
static std::string GetSchema()
{
@@ -277,14 +276,11 @@
virtual void Init(const CParamNode& paramNode)
{
- m_FormationController = paramNode.GetChild("FormationController").ToBool();
-
- m_Moving = false;
- m_FacePointAfterMove = true;
-
m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
+ m_ActualSpeed = fixed::Zero();
+
m_SpeedRatio = fixed::FromInt(1);
- m_CurSpeed = fixed::Zero();
+ m_Speed = fixed::Zero();
m_RunSpeedMultiplier = m_TemplateRunSpeedMultiplier = fixed::FromInt(1);
if (paramNode.GetChild("RunMultiplier").IsOk())
@@ -302,55 +298,59 @@
cmpObstruction->SetUnitClearance(m_Clearance);
}
- m_State = STATE_IDLE;
- m_PathState = PATHSTATE_NONE;
-
- m_ExpectedPathTicket = 0;
+ m_PathRequest.expectedPathTicket = 0;
+ m_PathRequest.dumpExistingPath = false;
+ m_PathRequest.runShortPathValidation = false;
+ m_PathRequest.isAlternativePath = false;
m_Tries = 0;
+ m_WaitingTurns = 0;
+
+ m_DebugOverlayEnabled = false;
+ m_AbortIfStuck = 0;
- m_TargetEntity = INVALID_ENTITY;
+ CmpPtr cmpUnitMotionManager(GetSystemEntity());
- m_FinalGoal.type = PathGoal::POINT;
+ m_FormationController = paramNode.GetChild("FormationController").ToBool();
- m_DebugOverlayEnabled = false;
+ if (m_FormationController)
+ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Update_MotionFormation, this, true);
+ if (cmpUnitMotionManager && !paramNode.GetChild("FormationController").ToBool())
+ cmpUnitMotionManager->RegisterUnit(GetEntityId());
}
virtual void Deinit()
{
+ CmpPtr cmpUnitMotionManager(GetSystemEntity());
+
+ if (cmpUnitMotionManager)
+ cmpUnitMotionManager->UnregisterUnit(GetEntityId());
}
template
void SerializeCommon(S& serialize)
{
- serialize.NumberU8("state", m_State, 0, STATE_MAX-1);
- serialize.NumberU8("path state", m_PathState, 0, PATHSTATE_MAX-1);
-
- serialize.StringASCII("pass class", m_PassClassName, 0, 64);
-
- serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket);
-
- serialize.NumberU32_Unbounded("target entity", m_TargetEntity);
- serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X);
- serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y);
- serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X);
- serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y);
- serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange);
- serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange);
-
+ serialize.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255);
serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio);
- serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
+ serialize.NumberU32_Unbounded("path ticket", m_PathRequest.expectedPathTicket);
+ serialize.Bool("dump path", m_PathRequest.dumpExistingPath);
+ serialize.Bool("short path validation", m_PathRequest.runShortPathValidation);
+ serialize.Bool("alternative path", m_PathRequest.isAlternativePath);
- serialize.Bool("moving", m_Moving);
- serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
+ SerializeVector()(serialize, "path", m_Path.m_Waypoints);
serialize.NumberU8("tries", m_Tries, 0, 255);
+ serialize.NumberU8("waiting turns", m_WaitingTurns, 0, 255);
- SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints);
- SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints);
+ serialize.Bool("started moving", m_StartedMoving);
- SerializeGoal()(serialize, "goal", m_FinalGoal);
+ // strictly speaking this doesn't need to be serialized since it's graphics-only, but it's nicer to.
+ serialize.NumberFixed_Unbounded("actual speed", m_ActualSpeed);
+
+ m_Destination.SerializeCommon(serialize);
+ m_CurrentGoal.SerializeCommon(serialize);
+ SerializeGoal()(serialize, "goal", m_Goal);
}
virtual void Serialize(ISerializer& serialize)
@@ -369,26 +369,19 @@
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
}
+ // TODO: would be nice to listen to entity renamed messages, but those have no C++ interface so far.
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update_MotionFormation:
{
- if (m_FormationController)
- {
- fixed dt = static_cast (msg).turnLength;
- Move(dt);
- }
- break;
- }
- case MT_Update_MotionUnit:
- {
- if (!m_FormationController)
- {
- fixed dt = static_cast (msg).turnLength;
- Move(dt);
- }
+ fixed dt = static_cast (msg).turnLength;
+ ValidateCurrentPath();
+ TryMoving(dt);
+ PerformActualMovement(dt);
+ HandleMoveFailures();
+ AnticipatePathingNeeds();
break;
}
case MT_RenderSubmit:
@@ -411,8 +404,8 @@
break;
FALLTHROUGH;
}
- case MT_OwnershipChanged:
case MT_Deserialized:
+ case MT_OwnershipChanged:
{
CmpPtr cmpValueModificationManager(GetSystemEntity());
if (!cmpValueModificationManager)
@@ -436,9 +429,14 @@
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
}
- virtual bool IsMoving() const
+ virtual bool IsActuallyMoving()
+ {
+ return m_StartedMoving;
+ }
+
+ virtual bool IsTryingToMove()
{
- return m_Moving;
+ return m_Destination.Valid();
}
virtual fixed GetSpeedRatio() const
@@ -467,6 +465,18 @@
return m_WalkSpeed;
}
+ // convenience wrapper
+ void SetActualSpeed(fixed newRealSpeed)
+ {
+ if (m_ActualSpeed == newRealSpeed)
+ return;
+
+ m_ActualSpeed = newRealSpeed;
+ CmpPtr cmpVisualActor(GetEntityHandle());
+ if (cmpVisualActor)
+ cmpVisualActor->SetMovingSpeed(m_ActualSpeed);
+ }
+
virtual pass_class_t GetPassabilityClass() const
{
return m_PassClass;
@@ -477,7 +487,7 @@
return m_PassClassName;
}
- virtual void SetPassabilityClassName(const std::string& passClassName)
+ virtual void SetPassabilityClassName(std::string passClassName)
{
m_PassClassName = passClassName;
CmpPtr cmpPathfinder(GetSystemEntity());
@@ -485,170 +495,165 @@
m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
}
- virtual fixed GetCurrentSpeed() const
+ virtual void SetDebugOverlay(bool enabled)
{
- return m_CurSpeed;
+ m_DebugOverlayEnabled = enabled;
+ UpdateMessageSubscriptions();
}
- virtual void SetFacePointAfterMove(bool facePointAfterMove)
+ virtual entity_pos_t GetUnitClearance() const
{
- m_FacePointAfterMove = facePointAfterMove;
+ return m_Clearance;
}
- virtual void SetDebugOverlay(bool enabled)
- {
- m_DebugOverlayEnabled = enabled;
- UpdateMessageSubscriptions();
- }
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable);
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable);
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange);
- virtual bool MoveToTargetRange(entity_id_t target, 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 FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
+ virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range);
+ virtual bool GoBackToOriginalDestination();
- virtual void StopMoving()
- {
- m_Moving = false;
- m_ExpectedPathTicket = 0;
- m_State = STATE_STOPPING;
- m_PathState = PATHSTATE_NONE;
- m_LongPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.clear();
- }
+ // transform a motion goal into a corresponding PathGoal
+ // called by RecomputeGoalPosition
+ PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal);
- virtual entity_pos_t GetUnitClearance() const
- {
- return m_Clearance;
- }
+ // take an arbitrary path goal and convert it to a 2D point goal, assign it to m_Goal.
+ bool RecomputeGoalPosition(PathGoal& goal);
-private:
- bool ShouldAvoidMovingUnits() const
- {
- return !m_FormationController;
- }
+ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
+ virtual void FaceTowardsEntity(entity_id_t ent);
- bool IsFormationMember() const
+ virtual void SetAbortIfStuck(u8 shouldAbort)
{
- return m_State == STATE_FORMATIONMEMBER_PATH;
+ m_AbortIfStuck = shouldAbort;
}
- entity_id_t GetGroup() const
+ void StartMoving()
{
- return IsFormationMember() ? m_TargetEntity : GetEntityId();
- }
+ m_StartedMoving = true;
- bool HasValidPath() const
- {
- return m_PathState == PATHSTATE_FOLLOWING
- || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG
- || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT;
+ CmpPtr cmpObstruction(GetEntityHandle());
+ if (cmpObstruction)
+ cmpObstruction->SetMovingFlag(true);
}
- void StartFailed()
+ void StopMoving()
{
- StopMoving();
- m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started
+ m_StartedMoving = false;
+
+ SetActualSpeed(fixed::Zero());
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->SetMovingFlag(false);
+ }
- CMessageMotionChanged msg(true, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ void ResetPathfinding()
+ {
+ m_Tries = 0;
+ m_WaitingTurns = 0;
+
+ m_PathRequest.expectedPathTicket = 0;
+ m_Path.m_Waypoints.clear();
}
- void MoveFailed()
+ virtual void DiscardMove()
{
StopMoving();
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
+ ResetPathfinding();
- CMessageMotionChanged msg(false, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ m_CurrentGoal.Clear();
+ m_Destination.Clear();
}
- void StartSucceeded()
+ void MoveWillFail()
{
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
-
- m_Moving = true;
-
- CMessageMotionChanged msg(true, false);
+ CMessageMoveFailure msg;
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
- void MoveSucceeded()
+ void MoveHasPaused()
{
- m_Moving = false;
+ CMessageMovePaused msg;
+ GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ }
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
+ virtual bool HasValidPath()
+ {
+ return !m_Path.m_Waypoints.empty();
+ }
- // No longer moving, so speed is 0.
- m_CurSpeed = fixed::Zero();
+ virtual CFixedVector2D GetReachableGoalPosition()
+ {
+ if (!IsTryingToMove())
+ return CFixedVector2D(fixed::Zero(),fixed::Zero());
- CMessageMotionChanged msg(false, false);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ return CFixedVector2D(m_Goal.x, m_Goal.z);
}
- bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target);
+ virtual void ValidateCurrentPath();
- /**
- * Handle the result of an asynchronous path query.
- */
- void PathResult(u32 ticket, const WaypointPath& path);
+ virtual void TryMoving(fixed dt);
- /**
- * Do the per-turn movement and other updates.
- */
- void Move(fixed dt);
+ virtual void PerformActualMovement(fixed dt);
- /**
- * Decide whether to approximate the given range from a square target as a circle,
- * rather than as a square.
- */
- bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const;
+ virtual void HandleMoveFailures();
- /**
- * Computes the current location of our target entity (plus offset).
- * Returns false if no target entity or no valid position.
- */
- bool ComputeTargetPosition(CFixedVector2D& out) const;
+ virtual void AnticipatePathingNeeds();
- /**
- * Attempts to replace the current path with a straight line to the goal,
- * if this goal is a point, is close enough and the route is not obstructed.
- */
- bool TryGoingStraightToGoalPoint(const CFixedVector2D& from);
+private:
+ CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const
+ {
+ ENSURE (goal.Valid());
- /**
- * Attempts to replace the current path with a straight line to the target
- * entity, if it's close enough and the route is not obstructed.
- */
- bool TryGoingStraightToTargetEntity(const CFixedVector2D& from);
+ if (goal.IsEntity())
+ {
+ CmpPtr cmpPosition(GetSimContext(), goal.GetEntity());
+ ENSURE(cmpPosition);
+ ENSURE(cmpPosition->IsInWorld()); // TODO: do something? Like return garrisoned building or such?
+ return cmpPosition->GetPosition2D() + goal.GetPosition(); // use position as an offset for formations.
+ }
+ else
+ return goal.GetPosition();
+ }
+
+ bool CurrentGoalHasValidPosition()
+ {
+ if (!m_CurrentGoal.Valid())
+ return false;
+
+ if (m_CurrentGoal.IsEntity())
+ {
+ CmpPtr cmpPosition(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+ return true;
+ }
+ else
+ return true;
+ }
+
+ bool IsFormationMember() const;
/**
- * Returns whether the target entity has moved more than minDelta since our
- * last path computations, and we're close enough to it to care.
+ * Handle the result of an asynchronous path query.
*/
- bool CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta);
+ void PathResult(u32 ticket, const WaypointPath& path);
+ void ActuallyValidateCurrentPath();
+
/**
- * Update goal position if moving target
+ * take a 2D position and return an updated one based on target position, taking velocity into account.
*/
- void UpdateFinalGoal();
+ inline void UpdatePositionForTarget(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty);
/**
* Returns whether we are close enough to the target to assume it's a good enough
* position to stop.
*/
- bool ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from);
+ bool ShouldConsiderOurselvesAtDestination(SMotionGoal& goal);
/**
* Returns whether the length of the given path, plus the distance from
@@ -663,17 +668,14 @@
/**
* Returns an appropriate obstruction filter for use with path requests.
- * noTarget is true only when used inside tryGoingStraightToTargetEntity,
- * in which case we do not want the target obstruction otherwise it would always fail
*/
- ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const;
+ ControlGroupMovementObstructionFilter GetObstructionFilter(bool evenMovingUnits = true) const;
/**
- * Start moving to the given goal, from our current position 'from'.
- * Might go in a straight line immediately, or might start an asynchronous
- * path request.
+ * Dumps current path and request a new one asynchronously.
+ * Inside of UnitMotion, do not set evenUnreachable to false unless you REALLY know what you're doing.
*/
- void BeginPathing(const CFixedVector2D& from, const PathGoal& goal);
+ bool RequestNewPath(bool evenUnreachable = true);
/**
* Start an asynchronous long path query.
@@ -683,7 +685,12 @@
/**
* Start an asynchronous short path query.
*/
- void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits);
+ void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath = false);
+
+ /**
+ * Drop waypoints that are close to us. Used by Move() before calling RequestShortPath and for anticipating obstructions.
+ */
+ void DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints);
/**
* Convert a path into a renderable list of lines
@@ -697,534 +704,501 @@
void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
{
- // reset our state for sanity.
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
-
- m_Moving = false;
-
// Ignore obsolete path requests
- if (ticket != m_ExpectedPathTicket)
+ if (ticket != m_PathRequest.expectedPathTicket)
return;
- m_ExpectedPathTicket = 0; // we don't expect to get this result again
+ m_PathRequest.expectedPathTicket = 0; // we don't expect to get this result again
- // Check that we are still able to do something with that path
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
+ if (m_PathRequest.dumpExistingPath)
{
- if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT)
- StartFailed();
- else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT)
- StopMoving();
- return;
+ if (m_PathRequest.isAlternativePath)
+ m_AlternativePath.m_Waypoints.clear();
+ else
+ m_Path.m_Waypoints.clear();
+ m_PathRequest.dumpExistingPath = false;
}
- if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG)
- {
- m_LongPath = path;
-
- // If we are following a path, leave the old m_ShortPath so we can carry on following it
- // until a new short path has been computed
- if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG)
- m_ShortPath.m_Waypoints.clear();
+ if (!m_Destination.Valid())
+ return;
- // If there's no waypoints then we couldn't get near the target.
- // Sort of hack: Just try going directly to the goal point instead
- // (via the short pathfinder), so if we're stuck and the user clicks
- // close enough to the unit then we can probably get unstuck
- if (m_LongPath.m_Waypoints.empty())
- m_LongPath.m_Waypoints.emplace_back(Waypoint{ m_FinalGoal.x, m_FinalGoal.z });
+ if (path.m_Waypoints.empty())
+ {
+ // no waypoints, path failed.
+ // if we have some room, pop waypoint
+ // TODO: this isn't particularly bright.
+ if (!m_Path.m_Waypoints.empty())
+ m_Path.m_Waypoints.pop_back();
- if (!HasValidPath())
- StartSucceeded();
+ // we will then deal with this on the next Move() call.
+ return;
+ }
- m_PathState = PATHSTATE_FOLLOWING;
+ // if this is a short path, verify some things
+ // Namely reject any path that takes us in another global region
+ // and any waypoint that's not passable/next to a passable cell.
+ // this will ensure minimal changes of long/short rance pathfinder discrepancies
+ if (m_PathRequest.runShortPathValidation)
+ {
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ ENSURE (cmpPathfinder);
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
+ CmpPtr cmpPosition(GetEntityHandle());
+ ENSURE(cmpPosition);
- m_Moving = true;
- }
- else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT)
- {
- m_ShortPath = path;
+ u16 i0, j0;
+ cmpPathfinder->FindNearestPassableNavcell(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, i0, j0, m_PassClass);
- // If there's no waypoints then we couldn't get near the target
- if (m_ShortPath.m_Waypoints.empty())
+ m_PathRequest.runShortPathValidation = false;
+ for (const Waypoint& wpt : path.m_Waypoints)
{
- // If we're globally following a long path, try to remove the next waypoint, it might be obstructed
- // If not, and we are not in a formation, retry
- // unless we are close to our target and we don't have a target entity.
- // This makes sure that units don't clump too much when they are not in a formation and tasked to move.
- if (m_LongPath.m_Waypoints.size() > 1)
- m_LongPath.m_Waypoints.pop_back();
- else if (IsFormationMember())
+ u16 i1, j1;
+ u32 dist = cmpPathfinder->FindNearestPassableNavcell(wpt.x, wpt.z, i1, j1, m_PassClass);
+ if (dist > 2 || !cmpPathfinder->NavcellIsReachable(i0, j0, i1, j1, m_PassClass))
{
- m_Moving = false;
- CMessageMotionChanged msg(true, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ MoveWillFail();
+ // we will then deal with this on the next Move() call.
return;
}
+ }
+ }
- CMessageMotionChanged msg(false, false);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
-
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
-
- CFixedVector2D pos = cmpPosition->GetPosition2D();
+ std::vector& targetPath = m_PathRequest.isAlternativePath ? m_AlternativePath.m_Waypoints : m_Path.m_Waypoints;
- if (ShouldConsiderOurselvesAtDestination(pos))
- return;
+ if (m_PathRequest.isAlternativePath)
+ m_PathRequest.isAlternativePath = false;
- UpdateFinalGoal();
- RequestLongPath(pos, m_FinalGoal);
- m_PathState = PATHSTATE_WAITING_REQUESTING_LONG;
+ // if we're currently moving, we have a path, so check if the first waypoint can be removed
+ // it's not impossible that we've actually reached it already.
+ if (IsActuallyMoving() && path.m_Waypoints.size() >= 2)
+ {
+ CmpPtr cmpPosition(GetEntityHandle());
+ CFixedVector2D nextWp = CFixedVector2D(path.m_Waypoints.back().x,path.m_Waypoints.back().z) - cmpPosition->GetPosition2D();
+ if (nextWp.CompareLength(GetSpeed()/2) <= 0)
+ {
+ targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()-1);
return;
}
+ }
+ targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end());
+}
- // else we could, so reset our number of tries.
- m_Tries = 0;
-
- // Now we've got a short path that we can follow
- if (!HasValidPath())
- StartSucceeded();
+/**
+ * So long as they are handled properly, it is OK to send messages here.
+ */
+void CCmpUnitMotion::ValidateCurrentPath()
+{
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
+ return;
- m_PathState = PATHSTATE_FOLLOWING;
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return;
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
+ ActuallyValidateCurrentPath();
- m_Moving = true;
- }
- else
- LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState);
+ // validate our state following the messages potentially sent above.
+ if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition())
+ // One of the messages we sent UnitAI caused us to stop moving entirely.
+ // Tell the visual actor we're not moving this turn to avoid gliding.
+ SetActualSpeed(fixed::Zero());
}
-void CCmpUnitMotion::Move(fixed dt)
+void CCmpUnitMotion::ActuallyValidateCurrentPath()
{
- PROFILE("Move");
+ // this should be kept in sync with RequestNewPath otherwise we'll spend our whole life repathing.
- if (m_State == STATE_STOPPING)
- {
- m_State = STATE_IDLE;
- MoveSucceeded();
- return;
- }
+ CmpPtr cmpPosition(GetEntityHandle());
- if (m_State == STATE_IDLE)
+ m_MoveMetadata = MovementMetadata{cmpPosition->GetPosition2D(), cmpPosition->GetPosition2D(), true, false};
+
+ // don't validate points, they never change
+ if (!m_CurrentGoal.IsEntity())
return;
- switch (m_PathState)
- {
- case PATHSTATE_NONE:
+ // TODO: figure out what to do when the goal dies.
+ // for now we'll keep on keeping on, but reset as if our goal was a position
+ // and send a failure message to UnitAI in case it wants to do something
+ // use position as a proxy for existence
+ CmpPtr cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
{
- // If we're not pathing, do nothing
+ // TODO: this should call a custom function for this
+ SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range());
+ m_Destination = newGoal;
+ m_CurrentGoal = newGoal;
+ RequestNewPath();
+ MoveWillFail();
return;
}
- case PATHSTATE_WAITING_REQUESTING_LONG:
- case PATHSTATE_WAITING_REQUESTING_SHORT:
- {
- // If we're waiting for a path and don't have one yet, do nothing
+ // don't validate if no path.
+ if (!HasValidPath())
return;
- }
- case PATHSTATE_FOLLOWING:
- case PATHSTATE_FOLLOWING_REQUESTING_SHORT:
- case PATHSTATE_FOLLOWING_REQUESTING_LONG:
- {
- // TODO: there's some asymmetry here when units look at other
- // units' positions - the result will depend on the order of execution.
- // Maybe we should split the updates into multiple phases to minimise
- // that problem.
+ // if we have an alternative path, follow it, it's likely to be better.
+ if (!m_AlternativePath.m_Waypoints.empty())
+ // switch both paths. Presumably the alternative one is newer and better, so don't switch back if that one fails too.
+ m_Path.m_Waypoints.swap(m_AlternativePath.m_Waypoints);
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return;
+ // clear alternative paths whatever happened.
+ m_AlternativePath.m_Waypoints.clear();
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
+ // TODO: check LOS here (instead of in UnitAI like we do now).
- CFixedVector2D initialPos = cmpPosition->GetPosition2D();
+ // if our goal can move, then perhaps it has.
+ CmpPtr cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpTargetUnitMotion)
+ return;
- // If we're chasing a potentially-moving unit and are currently close
- // enough to its current position, and we can head in a straight line
- // to it, then throw away our current path and go straight to it
- if (m_PathState == PATHSTATE_FOLLOWING)
- TryGoingStraightToTargetEntity(initialPos);
+ // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible.
- // Keep track of the current unit's position during the update
- CFixedVector2D pos = initialPos;
+ // TODO: this will probably be called every turn if the entity tries to go to an unreachable unit
+ // In those cases, UnitAI should be warned that the unit is unreachable and tell us to do something else.
- fixed basicSpeed = m_Speed;
+ CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
+ fixed certainty = (m_Clearance + cmpTargetUnitMotion->GetUnitClearance()) * 3 / 2;
+ UpdatePositionForTarget(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty);
- // 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)
- // TODO: Terrain-dependent speeds are not currently supported
- fixed terrainSpeed = fixed::FromInt(1);
+ CmpPtr cmpObstructionManager(GetSystemEntity());
+ if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty))
+ RequestNewPath();
+}
- fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
+void CCmpUnitMotion::UpdatePositionForTarget(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty)
+{
+ CmpPtr cmpTargetPosition(GetSimContext(), ent);
+ CmpPtr cmpTargetUnitMotion(GetSimContext(), ent);
+ if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld() || !cmpTargetUnitMotion)
+ return;
- bool wasObstructed = false;
- // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint
+ if (!cmpTargetUnitMotion->IsActuallyMoving() || IsFormationMember())
+ {
+ // go directly towards the unit.
+ x = cmpTargetPosition->GetPosition2D().X;
+ z = cmpTargetPosition->GetPosition2D().Y;
+ certainty = fixed::Zero();
+ return;
+ }
- fixed timeLeft = dt;
- fixed zero = fixed::Zero();
+ // So here we'll try to estimate where the unit will be by the time we reach it.
+ // This can be done perfectly but I cannot think of a non-iterative process and this seems complicated for our purposes here
+ // so just get our direct distance and do some clever things, we'll correct later on anyhow so it doesn't matter.
+ CmpPtr cmpPosition(GetEntityHandle());
- while (timeLeft > zero)
- {
- // If we ran out of path, we have to stop
- if (m_ShortPath.m_Waypoints.empty() && m_LongPath.m_Waypoints.empty())
- break;
+ // try to estimate in how much time we'll reach it.
+ fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length();
- CFixedVector2D target;
- if (m_ShortPath.m_Waypoints.empty())
- target = CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z);
- else
- target = CFixedVector2D(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z);
+ if (GetSpeed() < fixed::Epsilon())
+ return;
- CFixedVector2D offset = target - pos;
+ fixed time = std::min(distance / GetSpeed(), fixed::FromInt(5)); // don't try from too far away or this is just dumb.
- // Work out how far we can travel in timeLeft
- fixed maxdist = maxSpeed.Multiply(timeLeft);
+ CFixedVector2D travelVector = (cmpTargetPosition->GetPosition2D() - cmpTargetPosition->GetPreviousPosition2D()).Multiply(time) * 2;
+ x += travelVector.X;
+ z += travelVector.Y;
- // If the target is close, we can move there directly
- fixed offsetLength = offset.Length();
- if (offsetLength <= maxdist)
- {
- if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
- {
- pos = target;
+ certainty += time * 2;
+}
- // Spend the rest of the time heading towards the next waypoint
- timeLeft = timeLeft - (offsetLength / maxSpeed);
+/**
+ * Sending messages in this function is _very_ forbidden
+ */
+void CCmpUnitMotion::TryMoving(fixed dt)
+{
+ PROFILE("TryMoving");
- if (m_ShortPath.m_Waypoints.empty())
- m_LongPath.m_Waypoints.pop_back();
- else
- m_ShortPath.m_Waypoints.pop_back();
+ if (!IsTryingToMove())
+ return;
- continue;
- }
- else
- {
- // Error - path was obstructed
- wasObstructed = true;
- break;
- }
- }
- else
- {
- // Not close enough, so just move in the right direction
- offset.Normalize(maxdist);
- target = pos + offset;
+ CmpPtr cmpPathfinder(GetSystemEntity());
- if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
- pos = target;
- else
- wasObstructed = true; // Error - path was obstructed
+ // We want to move (at most) m_Speed*dt units from pos towards the next waypoint
+ fixed timeLeft = dt;
- break;
- }
- }
+ // TODO: I think this may be a little buggy if we want to compute it several times per turn.
+ while (timeLeft > fixed::Zero())
+ {
+ // If we ran out of path, we have to stop there (move is still OK though).
+ if (!HasValidPath())
+ break;
- // Update the Position component after our movement (if we actually moved anywhere)
- if (pos != initialPos)
- {
- CFixedVector2D offset = pos - initialPos;
+ CFixedVector2D target;
+ target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
- // Face towards the target
- entity_angle_t angle = atan2_approx(offset.X, offset.Y);
- cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle);
+ CFixedVector2D offset = target - m_MoveMetadata.currentPos;
+ fixed offsetLength = offset.Length();
+ // Work out how far we can travel in timeLeft
+ fixed maxdist = m_Speed.Multiply(timeLeft);
- // Calculate the mean speed over this past turn.
- m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt;
+ CFixedVector2D destination;
+ if (offsetLength <= maxdist)
+ destination = target;
+ else
+ {
+ offset.Normalize(maxdist);
+ destination = m_MoveMetadata.currentPos + offset;
}
- if (wasObstructed)
+ // TODO: try moving as much as we can still?
+ // TODO: get more information about what blocked us.
+ if (cmpPathfinder->CheckMovement(GetObstructionFilter(), m_MoveMetadata.currentPos.X, m_MoveMetadata.currentPos.Y, destination.X, destination.Y, m_Clearance, m_PassClass))
{
- // Oops, we hit something (very likely another unit).
- // This is when we might easily get stuck wrongly.
+ m_MoveMetadata.currentPos = destination;
- // check if we've arrived.
- if (ShouldConsiderOurselvesAtDestination(pos))
- return;
+ timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed;
- // If we still have long waypoints, try and compute a short path
- // This will get us around units, amongst others.
- // However in some cases a long waypoint will be in located in the obstruction of
- // an idle unit. In that case, we need to scrap that waypoint or we might never be able to reach it.
- // I am not sure why this happens but the following code seems to work.
- if (!m_LongPath.m_Waypoints.empty())
- {
- CmpPtr cmpObstructionManager(GetSystemEntity());
- if (cmpObstructionManager)
- {
- // create a fake obstruction to represent our waypoint.
- ICmpObstructionManager::ObstructionSquare square;
- square.hh = m_Clearance;
- square.hw = m_Clearance;
- square.u = CFixedVector2D(entity_pos_t::FromInt(1),entity_pos_t::FromInt(0));
- square.v = CFixedVector2D(entity_pos_t::FromInt(0),entity_pos_t::FromInt(1));
- square.x = m_LongPath.m_Waypoints.back().x;
- square.z = m_LongPath.m_Waypoints.back().z;
- std::vector unitOnGoal;
- // don't ignore moving units as those might be units like us, ie not really moving.
- cmpObstructionManager->GetUnitsOnObstruction(square, unitOnGoal, GetObstructionFilter(), true);
- if (!unitOnGoal.empty())
- m_LongPath.m_Waypoints.pop_back();
- }
- if (!m_LongPath.m_Waypoints.empty())
- {
- PathGoal goal;
- if (m_LongPath.m_Waypoints.size() > 1 || m_FinalGoal.DistanceToPoint(pos) > LONG_PATH_MIN_DIST)
- goal = { PathGoal::POINT, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z };
- else
- {
- UpdateFinalGoal();
- goal = m_FinalGoal;
- m_LongPath.m_Waypoints.clear();
- CFixedVector2D target = goal.NearestPointOnGoal(pos);
- m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y });
- }
- RequestShortPath(pos, goal, true);
- m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT;
- return;
- }
- }
- // Else, just entirely recompute
- UpdateFinalGoal();
- BeginPathing(pos, m_FinalGoal);
-
- // potential TODO: We could switch the short-range pathfinder for something else entirely.
+ if (destination == target)
+ m_Path.m_Waypoints.pop_back();
+ continue;
+ }
+ else
+ {
+ // Error - path was obstructed
+ m_MoveMetadata.wasObstructed = true;
+ m_MoveMetadata.moveWentOK = false;
return;
}
+ }
- // We successfully moved along our path, until running out of
- // waypoints or time.
+ m_Tries = 0;
+ m_WaitingTurns = 0;
+}
- if (m_PathState == PATHSTATE_FOLLOWING)
- {
- // If we're not currently computing any new paths:
- if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty())
- {
- if (IsFormationMember())
- {
- // We've reached our assigned position. If the controller
- // is idle, send a notification in case it should disband,
- // otherwise continue following the formation next turn.
- CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity);
- if (cmpUnitMotion && !cmpUnitMotion->IsMoving())
- {
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
-
- m_Moving = false;
- CMessageMotionChanged msg(false, false);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
- }
- }
- else
- {
- // check if target was reached in case of a moving target
- CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity);
- if (cmpUnitMotion && cmpUnitMotion->IsMoving() &&
- MoveToTargetRange(m_TargetEntity, m_TargetMinRange, m_TargetMaxRange))
- return;
-
- // Not in formation, so just finish moving
- StopMoving();
- m_State = STATE_IDLE;
- MoveSucceeded();
-
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(pos, m_FinalGoal.x, m_FinalGoal.z);
- // TODO: if the goal was a square building, we ought to point towards the
- // nearest point on the square, not towards its center
- }
- }
+void CCmpUnitMotion::PerformActualMovement(fixed dt)
+{
+ if (m_MoveMetadata.currentPos == m_MoveMetadata.initialPos)
+ {
+ // We didn't actually move, for whatever reason, despite wanting to.
+ // Dispatch to components that we no longer actually move.
+ if (m_StartedMoving)
+ StopMoving();
- // If we have a target entity, and we're not miles away from the end of
- // our current path, and the target moved enough, then recompute our
- // whole path
- if (IsFormationMember())
- CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION);
- else
- CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA);
- }
- }
+ return;
}
+
+ CFixedVector2D offset = m_MoveMetadata.currentPos - m_MoveMetadata.initialPos;
+
+ if (!m_StartedMoving)
+ // tell other components (visual actor,...) we are now moving.
+ StartMoving();
+
+ CmpPtr cmpPosition(GetEntityHandle());
+
+ // Move facing towards the target.
+ entity_angle_t angle = atan2_approx(offset.X, offset.Y);
+ cmpPosition->MoveAndTurnTo(m_MoveMetadata.currentPos.X, m_MoveMetadata.currentPos.Y, angle);
+
+ // Calculate the mean speed over this past turn.
+ // TODO: this is often just a little different from our actual top speed
+ // so we end up changing the actual speed quite often, which is a little silly.
+ SetActualSpeed(cmpPosition->GetDistanceTravelled() / dt);
}
-bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) const
+/**
+ * So long as they are handled properly, it is OK to send messages here.
+ */
+void CCmpUnitMotion::HandleMoveFailures()
{
- if (m_TargetEntity == INVALID_ENTITY)
- return false;
+ if (m_MoveMetadata.moveWentOK)
+ return;
- CmpPtr cmpPosition(GetSimContext(), m_TargetEntity);
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ CmpPtr cmpPosition(GetEntityHandle());
+
+ CFixedVector2D pos = cmpPosition->GetPosition2D();
- if (m_TargetOffset.IsZero())
+ // We've stopped moving if we're here.
+ if (m_StartedMoving)
{
- // No offset, just return the position directly
- out = cmpPosition->GetPosition2D();
+ StopMoving();
+
+ MoveHasPaused();
+ // Validate that the MoveHasPaused message hint has not caused us to be in an invalid state.
+ if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition())
+ return;
}
- else
+
+ if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal))
+ // If we're out of path (ie not moving) but have a valid destination (IsTryingToMove()), we'll end up here every turn.
+ // We should not repath if we actually are where we want to be (ie at destination).
+ // That we do this means that the range check performed by whoever is calling us must be more permissive than this one though
+ // otherwise we'll never reach our target.
+ return;
+
+ // Oops, we have a problem. Either we were obstructed, we plan to be obstructed soon, or we ran out of path (but still have a goal).
+ // Handle it.
+ // Failure to handle it will result in stuckness and players complaining.
+
+ if (m_PathRequest.expectedPathTicket != 0)
+ // wait until we get our path to see where that leads us.
+ return;
+
+ // TODO: it would be nice to react differently to different problems
+ // eg running into a static obstruction can be handled by the long-range pathfinder
+ // but running in a unit cannot.
+ if (m_WaitingTurns == 0)
{
- // There is an offset, so compute it relative to orientation
- entity_angle_t angle = cmpPosition->GetRotation().Y;
- CFixedVector2D offset = m_TargetOffset.Rotate(angle);
- out = cmpPosition->GetPosition2D() + offset;
+ if (HasValidPath())
+ m_WaitingTurns = MAX_PATH_REATTEMPTS; // You can add some "waiting" turns by bumping this up, but for now let's not do it.
+ else
+ m_WaitingTurns = 3;
}
- return true;
-}
-bool CCmpUnitMotion::TryGoingStraightToGoalPoint(const CFixedVector2D& from)
-{
- // Make sure the goal is a point (and not a point-like target like a formation controller)
- if (m_FinalGoal.type != PathGoal::POINT || m_TargetEntity != INVALID_ENTITY)
- return false;
+ --m_WaitingTurns;
- // Fail if the goal is too far away
- CFixedVector2D goalPos(m_FinalGoal.x, m_FinalGoal.z);
- if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
- return false;
+ // MAX_PATH_REATTEMPTS and above: we wait.
+ if (m_WaitingTurns >= MAX_PATH_REATTEMPTS)
+ return;
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return false;
+ // If we're here we want to compute a short path.
+ if (m_WaitingTurns >= 3)
+ {
+ if (m_Path.m_Waypoints.empty())
+ {
+ RequestNewPath();
+ return;
+ }
+ /**
+ * Here there are two cases:
+ * 1) We are somewhat far away from the goal
+ * 2) We're really close to the goal.
+ * If it's (2) it's likely that we are running into units that are currently doing the same thing we want to do (gathering from the same tree…)
+ * Since the initial call to MakeGoalReachable gave us a specific 2D coordinate, and we can't reach it,
+ * We have a relatively high chance of never being able to reach that particular point.
+ * So we need to recreate the actual goal for this entity. This is a little dangerous in terms of short/long pathfinder compatibility
+ * So we'll run sanity checks on the output to try and not get stuck/go where we shouldn't.
+ */
+ PathGoal goal;
- // Check if there's any collisions on that route
- if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
- return false;
+ CFixedVector2D endWptPos(m_Path.m_Waypoints.front().x, m_Path.m_Waypoints.front().z);
+ bool redraw = true;
+ if ((endWptPos - pos).CompareLength(SHORT_PATH_GOAL_REDUX_DIST) > 0)
+ {
+ DropCloseWaypoints(pos, m_Path.m_Waypoints);
- // That route is okay, so update our path
- m_LongPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
+ if (!m_Path.m_Waypoints.empty())
+ {
+ redraw = false;
+ goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z };
+ }
+ }
+ // sanity check on the right, but the code above should never allow that to happen.
+ if (redraw || m_Path.m_Waypoints.empty())
+ {
+ goal = CreatePathGoalFromMotionGoal(m_CurrentGoal);
+ m_PathRequest.dumpExistingPath = true;
+ }
- return true;
-}
+ RequestShortPath(pos, goal, false);
+ return;
+ }
-bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from)
-{
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return false;
+ // Last resort, compute a long path
+ if (m_WaitingTurns == 2)
+ {
+ if (m_Path.m_Waypoints.empty())
+ {
+ RequestNewPath();
+ return;
+ }
+ PathGoal goal;
+ goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z };
+ m_Path.m_Waypoints.pop_back();
- // Fail if the target is too far away
- if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
- return false;
+ RequestLongPath(pos, goal);
+ return;
+ }
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return false;
- // Move the goal to match the target entity's new position
- PathGoal goal = m_FinalGoal;
- goal.x = targetPos.X;
- goal.z = targetPos.Y;
- // (we ignore changes to the target's rotation, since only buildings are
- // square and buildings don't move)
+ // m_waitingTurns == 1 here
- // Find the point on the goal shape that we should head towards
- CFixedVector2D goalPos = goal.NearestPointOnGoal(from);
+ // we tried getting a renewed path and still got stuck
+ if (m_AbortIfStuck == 0)
+ {
+ // TODO: inform the player maybe?
+ // TODO: this should be sent to UnitAI with a stronger "definitely failed" parameter.
+ MoveWillFail();
+ // fall through, we don't want to discard the move on our own.
+ }
- // Check if there's any collisions on that route
- if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
- return false;
+ --m_AbortIfStuck;
- // That route is okay, so update our path
- m_FinalGoal = goal;
- m_LongPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
+ // Recompute a new path, but wait a few turns first
+ m_WaitingTurns = 4 + MAX_PATH_REATTEMPTS;
- return true;
+ return;
}
-bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta)
+void CCmpUnitMotion::AnticipatePathingNeeds()
{
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return false;
-
- // Fail unless the target has moved enough
- CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z);
- if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0)
- return false;
-
+ CmpPtr cmpPathfinder(GetSystemEntity());
CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+
+ // don't rely on the move metadata, our position may well have changed by now
+ // (we could have garrisoned…)
CFixedVector2D pos = cmpPosition->GetPosition2D();
- CFixedVector2D oldDir = (oldTargetPos - pos);
- CFixedVector2D newDir = (targetPos - pos);
- oldDir.Normalize();
- newDir.Normalize();
-
- // Fail unless we're close enough to the target to care about its movement
- // and the angle between the (straight-line) directions of the previous and new target positions is small
- if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST))
- return false;
- // Fail if the target is no longer visible to this entity's owner
- // (in which case we'll continue moving to its last known location,
- // unless it comes back into view before we reach that location)
- CmpPtr cmpOwnership(GetEntityHandle());
- if (cmpOwnership)
+ bool pathWillBeObstructed = !m_MoveMetadata.wasObstructed && HasValidPath() && !m_Path.m_Waypoints.empty();
+ if (!pathWillBeObstructed)
+ return;
+
+ pathWillBeObstructed = false;
+ // check any waypoint closer to us than LOOKAHEAD_DISTANCE.
+ ssize_t index = m_Path.m_Waypoints.size() - 1;
+ CFixedVector2D checkPos = pos;
+ fixed distance = (CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z) - checkPos).Length();
+ while (index >= 0 && distance < LOOKAHEAD_DISTANCE)
{
- CmpPtr cmpRangeManager(GetSystemEntity());
- if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN)
- return false;
+ if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true),
+ checkPos.X, checkPos.Y, m_Path.m_Waypoints[index].x, m_Path.m_Waypoints[index].z,
+ m_Clearance, m_PassClass))
+ {
+ pathWillBeObstructed = true;
+ break;
+ }
+ CFixedVector2D wptPos = CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z);
+ index--;
+ distance += (wptPos - checkPos).Length();
+ checkPos = wptPos;
}
+ if (!pathWillBeObstructed)
+ return;
- // The target moved and we need to update our current path;
- // change the goal here and expect our caller to start the path request
- m_FinalGoal.x = targetPos.X;
- m_FinalGoal.z = targetPos.Y;
- RequestLongPath(from, m_FinalGoal);
- m_PathState = PATHSTATE_FOLLOWING_REQUESTING_LONG;
+ std::vector alternativePath = m_Path.m_Waypoints;
- return true;
-}
+ DropCloseWaypoints(pos, alternativePath);
+ PathGoal goal;
+ if (!alternativePath.empty())
+ goal = { PathGoal::POINT, alternativePath.back().x, alternativePath.back().z };
+ else
+ {
+ goal = CreatePathGoalFromMotionGoal(m_CurrentGoal);
+ m_PathRequest.dumpExistingPath = true;
+ }
+ // request short path as alternative path.
+ RequestShortPath(pos, goal, true, true);
-void CCmpUnitMotion::UpdateFinalGoal()
-{
- if (m_TargetEntity == INVALID_ENTITY)
- return;
- CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity);
- if (!cmpUnitMotion)
- return;
- if (IsFormationMember())
- return;
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return;
- m_FinalGoal.x = targetPos.X;
- m_FinalGoal.z = targetPos.Y;
+ // save waypoints kept so far.
+ m_AlternativePath.m_Waypoints = alternativePath;
}
-bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from)
+// Only used to send a "hint" to unitAI.
+bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(SMotionGoal& goal)
{
- if (m_TargetEntity != INVALID_ENTITY || m_FinalGoal.DistanceToPoint(from) > SHORT_PATH_GOAL_RADIUS)
- return false;
+ if (HasValidPath())
+ return false; // wait until we're done.
- StopMoving();
- MoveSucceeded();
+ CmpPtr cmpObstructionManager(GetSystemEntity());
+ if (!cmpObstructionManager)
+ return true; // what's a sane default here?
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(from, m_FinalGoal.x, m_FinalGoal.z);
- return true;
+ if (goal.IsEntity())
+ return cmpObstructionManager->IsInTargetRange(GetEntityId(), goal.GetEntity(), goal.Range(), goal.Range(), true);
+ else
+ return cmpObstructionManager->IsInPointRange(GetEntityId(), goal.GetPosition().X, goal.GetPosition().Y, goal.Range(), goal.Range(), true);
}
bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const
@@ -1274,385 +1248,356 @@
}
}
-ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const
-{
- entity_id_t group = noTarget ? m_TargetEntity : GetGroup();
- return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group);
-}
-
-
-
-void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal)
+void CCmpUnitMotion::FaceTowardsEntity(entity_id_t ent)
{
- // reset our state for sanity.
- m_ExpectedPathTicket = 0;
-
- CmpPtr cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
-
- m_Moving = false;
-
- m_PathState = PATHSTATE_NONE;
-
-#if DISABLE_PATHFINDER
- {
- CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
- CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from);
- m_LongPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
- m_PathState = PATHSTATE_FOLLOWING;
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
return;
- }
-#endif
- // If we're aiming at a target entity and it's close and we can reach
- // it in a straight line, then we'll just go along the straight line
- // instead of computing a path.
- if (TryGoingStraightToTargetEntity(from))
- {
- if (!HasValidPath())
- StartSucceeded();
- m_PathState = PATHSTATE_FOLLOWING;
+ CmpPtr cmpTargetPosition(GetSimContext(), ent);
+ if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
return;
- }
- // Same thing applies to non-entity points
- if (TryGoingStraightToGoalPoint(from))
+ CFixedVector2D pos = cmpPosition->GetPosition2D();
+ CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
+
+ CFixedVector2D offset = targetPos - pos;
+ if (!offset.IsZero())
{
- if (!HasValidPath())
- StartSucceeded();
- m_PathState = PATHSTATE_FOLLOWING;
- return;
+ entity_angle_t angle = atan2_approx(offset.X, offset.Y);
+ cmpPosition->TurnTo(angle);
}
- // Otherwise we need to compute a path.
+}
- // If it's close then just do a short path, not a long path
- // TODO: If it's close on the opposite side of a river then we really
- // need a long path, so we shouldn't simply check linear distance
- // the check is arbitrary but should be a reasonably small distance.
- if (goal.DistanceToPoint(from) < LONG_PATH_MIN_DIST)
- {
- // add our final goal as a long range waypoint so we don't forget
- // where we are going if the short-range pathfinder returns
- // an aborted path.
- m_LongPath.m_Waypoints.clear();
- CFixedVector2D target = m_FinalGoal.NearestPointOnGoal(from);
- m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y });
- m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT;
- RequestShortPath(from, goal, true);
- }
+ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool evenMovingUnits) const
+{
+ if (IsFormationMember())
+ return ControlGroupMovementObstructionFilter(evenMovingUnits, m_Destination.GetEntity());
else
- {
- m_PathState = PATHSTATE_WAITING_REQUESTING_LONG;
- RequestLongPath(from, goal);
- }
+ return ControlGroupMovementObstructionFilter(evenMovingUnits, GetEntityId());
}
-void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
+// TODO: this can be improved, it's a little limited
+// e.g. use of hierarchical pathfinder,…
+// Also adding back the "straight-line if close enough" test could be good.
+bool CCmpUnitMotion::RequestNewPath(bool evenUnreachable)
{
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return;
+ ENSURE(m_PathRequest.isAlternativePath || m_PathRequest.expectedPathTicket == 0);
- // this is by how much our waypoints will be apart at most.
- // this value here seems sensible enough.
- PathGoal improvedGoal = goal;
- improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
+ ENSURE(CurrentGoalHasValidPosition());
- cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
+ CmpPtr cmpPosition(GetEntityHandle());
+ ENSURE (cmpPosition);
- m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
-}
+ CFixedVector2D position = cmpPosition->GetPosition2D();
-void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits)
-{
- CmpPtr cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return;
+ m_PathRequest.dumpExistingPath = true;
+ m_PathRequest.isAlternativePath = false;
- // wrapping around on m_Tries isn't really a problem so don't check for overflow.
- fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * ++m_Tries, goal.DistanceToPoint(from));
- if (goal.type != PathGoal::POINT && searchRange < goal.hw && searchRange < SHORT_PATH_MIN_SEARCH_RANGE * 2)
- searchRange = std::min(goal.hw, SHORT_PATH_MIN_SEARCH_RANGE * 2);
- if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
- searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
+ bool reachable = RecomputeGoalPosition(m_Goal);
- m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId());
-}
+ if (!reachable && !evenUnreachable)
+ {
+ // Do not submit a path request if we've been told it's not going to be used anyhow.
+ DiscardMove();
+ return false;
+ }
-bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
-{
- return MoveToPointRange(x, z, minRange, maxRange, INVALID_ENTITY);
+ ENSURE(m_Goal.x >= fixed::Zero());
+
+ /**
+ * A (long) note on short vs long range pathfinder, their conflict, and stuck units.
+ * A long-standing issue with 0 A.D.'s pathfinding has been that the short-range pathfinder is "better" than the long-range
+ * Indeed it can find paths that the long-range one cannot, since the grid is coarser than the real vector representation.
+ * This leads to units going where they shouldn't go, notably impassable and "unreachable" areas.
+ * This has been a -real- plague. Made worse by the facts that groups of trees tended to trigger it, leading to stuck gatherers…
+ * Thus, in general, we'd want the short-range and the long-range pathfinder to coincide. But making the short-range pathfinder
+ * register all impassable navcells as edges would just be way too slow, so we can't do that, so we -cannot- fix the issue
+ * by just changing the pathfinders' behavior.
+ *
+ * All hope is not lost, however.
+ *
+ * A big part of the problem is that before the unitMotion rewrite, UnitMotion requested a path to the goal, and then the pathfinder
+ * made that goal "reachable" by calling MakeGoalReachable, which uses the same grid as the long-range pathfinder. Thus, over short ranges,
+ * the pathfinder entirely short-circuited this. Since UnitMotion now calls MakeGoalReachable on its own, it only ever requests
+ * paths to points that are indeed supposed to be reachable. This does fix a number of cases.
+ *
+ * But then, why set LONG_PATH_MIN_DIST to 0 and disable the use of short paths here? Well it turns out you still had a few edge cases.
+ *
+ * Imagine two houses next to each other, with a space between them just wide enough that there are no passable navcells,
+ * but enough space for the short-range pathfinder to return a path through them (make them in a test map if you have to).
+ * If you ask a unit to cross there, the goal won't change: it's reachable by the long-range pathfinder by going around the house.
+ * However, the distance is < LONG_PATH_MIN_DIST, so the short-range pathfinder is called, so it goes through the house. Edge case.
+ * There's a variety of similar cases that can be imagined around the idea that there exists a shorter path visible only by the short-range pathfinder.
+ * If we never use the short-pathfinder in RequestNewPath, we can safely avoid those edge cases.
+ *
+ * However, we still call the short-pathfinder when running into an obstruction to avoid units. Can't that get us stuck too?
+ * Well, it probably can. But there's a few things to consider:
+ * -It's harder to trigger it if you actually have to run into a unit
+ * -In those cases, UnitMotion requests a path to the next existing waypoint (if there are none, it calls requestnewPath to get those)
+ * and the next existing waypoint has -necessarily- been given to us by the long-range pathfinder since we're using it here
+ * -We are running sanity checks on the output (see PathResult).
+ * Thus it's far less likely that the short-range pathfinder will return us an impassable path.
+ * It -is- not entirely impossible. A freak construction with many units strategically positionned could probably reveal the bug.
+ * But it's in my opinion rare enough that this discrepancy can be considered fixed.
+ */
+
+ if (m_Goal.DistanceToPoint(position) < LONG_PATH_MIN_DIST)
+ RequestShortPath(position, m_Goal, true);
+ else
+ RequestLongPath(position, m_Goal);
+
+ return reachable;
}
-bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target)
+PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal)
{
- PROFILE("MoveToPointRange");
+ PathGoal goal = PathGoal();
+ goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy
CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ ENSURE(cmpPosition);
CFixedVector2D pos = cmpPosition->GetPosition2D();
- PathGoal goal;
- goal.x = x;
- goal.z = z;
+ // The point of this function is to get a reachable navcell where we want to go.
+ // It calls the hierarchical pathfinder's MakeGoalReachable function directly
+ // and analyzes to result to return something acceptable.
+ // "acceptable" means that if there is a path, once the unit has reached its destination,
+ // the ObstructionManager's "IsInPointRange/IsInTargetRange" should consider it in range.
+ // So we need to make sure MakeGoalReachable will return something in sync.
- if (minRange.IsZero() && maxRange.IsZero())
- {
- // Non-ranged movement:
+ // defaut to point at position
+ goal.type = PathGoal::POINT;
+ goal.x = GetGoalPosition(motionGoal).X;
+ goal.z = GetGoalPosition(motionGoal).Y;
- // Head directly for the goal
- goal.type = PathGoal::POINT;
- }
- else
+ // few cases to consider.
+ if (motionGoal.IsEntity())
{
- // Ranged movement:
+ CmpPtr cmpObstruction(GetSimContext(), motionGoal.GetEntity());
+ if (cmpObstruction)
+ {
+ ICmpObstructionManager::ObstructionSquare obstruction;
+ bool hasObstruction = cmpObstruction->GetObstructionSquare(obstruction);
+ if (hasObstruction)
+ {
+ fixed certainty;
+ UpdatePositionForTarget(motionGoal.GetEntity(), obstruction.x, obstruction.z, certainty);
- entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length();
+ goal.type = PathGoal::CIRCLE;
+ goal.x = obstruction.x;
+ goal.z = obstruction.z;
+ goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance;
- if (distance < minRange)
- {
- // Too close to target - move outwards to a circle
- // that's slightly larger than the min range
- goal.type = PathGoal::INVERTED_CIRCLE;
- goal.hw = minRange + Pathfinding::GOAL_DELTA;
- }
- else if (maxRange >= entity_pos_t::Zero() && distance > maxRange)
- {
- // Too far from target - move inwards to a circle
- // that's slightly smaller than the max range
- goal.type = PathGoal::CIRCLE;
- goal.hw = maxRange - Pathfinding::GOAL_DELTA;
-
- // If maxRange was abnormally small,
- // collapse the circle into a point
- if (goal.hw <= entity_pos_t::Zero())
- goal.type = PathGoal::POINT;
- }
- else
- {
- // We're already in range - no need to move anywhere
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(pos, x, z);
- return false;
+ // if not a unit, treat as a square
+ if (cmpObstruction->GetUnitRadius() == fixed::Zero())
+ {
+ goal.type = PathGoal::SQUARE;
+ goal.hh = obstruction.hh + motionGoal.Range() + m_Clearance;
+ goal.u = obstruction.u;
+ goal.v = obstruction.v;
+
+ fixed distance = Geometry::DistanceToSquare(pos - CFixedVector2D(goal.x,goal.z), goal.u, goal.v, CFixedVector2D(goal.hw, goal.hh), true);
+ if (distance == fixed::Zero())
+ goal.type = PathGoal::INVERTED_SQUARE;
+ }
+ else if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0)
+ goal.type = PathGoal::INVERTED_CIRCLE;
+ }
}
+ // if no obstruction, keep treating as a point
+ }
+ if (goal.type == PathGoal::POINT && motionGoal.Range() > fixed::Zero())
+ {
+ goal.type = PathGoal::CIRCLE;
+ goal.hw = motionGoal.Range();
+ if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0)
+ goal.type = PathGoal::INVERTED_CIRCLE;
}
- m_State = STATE_INDIVIDUAL_PATH;
- m_TargetEntity = target;
- m_TargetOffset = CFixedVector2D();
- m_TargetMinRange = minRange;
- m_TargetMaxRange = maxRange;
- m_FinalGoal = goal;
- m_Tries = 0;
-
- BeginPathing(pos, goal);
-
- return true;
+ return goal;
}
-bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const
+bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal)
{
- // Given a square, plus a target range we should reach, the shape at that distance
- // is a round-cornered square which we can approximate as either a circle or as a square.
- // Previously, we used the shape that minimized the worst-case error.
- // However that is unsage in some situations. So let's be less clever and
- // just check if our range is at least three times bigger than the circleradius
- return (range > circleRadius*3);
-}
+ if (!CurrentGoalHasValidPosition())
+ return false; // we're not going anywhere
-bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
-{
- PROFILE("MoveToTargetRange");
+ goal = CreatePathGoalFromMotionGoal(m_CurrentGoal);
+
+ // We now have a correct goal.
+ // Make it reachable
+
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ ENSURE(cmpPathfinder);
CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ ENSURE(cmpPosition);
CFixedVector2D pos = cmpPosition->GetPosition2D();
- CmpPtr cmpObstructionManager(GetSystemEntity());
- if (!cmpObstructionManager)
- return false;
+ bool reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass);
- bool hasObstruction = false;
- ICmpObstructionManager::ObstructionSquare obstruction;
- CmpPtr cmpObstruction(GetSimContext(), target);
- if (cmpObstruction)
- hasObstruction = cmpObstruction->GetObstructionSquare(obstruction);
+ // TODO: ought to verify that the returned navcell is in range if it's reachable as a sanity check
- if (!hasObstruction)
- {
- // The target didn't have an obstruction or obstruction shape, so treat it as a point instead
+ m_Goal = goal;
- CmpPtr cmpTargetPosition(GetSimContext(), target);
- if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
- return false;
+ return reachable;
+}
- CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
+void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
+{
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
+ return;
- return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange);
- }
+ m_PathRequest.runShortPathValidation = false;
+ m_PathRequest.isAlternativePath = false;
- /*
- * If we're starting outside the maxRange, we need to move closer in.
- * If we're starting inside the minRange, we need to move further out.
- * These ranges are measured from the center of this entity to the edge of the target;
- * we add the goal range onto the size of the target shape to get the goal shape.
- * (Then we extend it outwards/inwards by a little bit to be sure we'll end up
- * within the right range, in case of minor numerical inaccuracies.)
- *
- * There's a bit of a problem with large square targets:
- * the pathfinder only lets us move to goals that are squares, but the points an equal
- * distance from the target make a rounded square shape instead.
- *
- * When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely
- * within the desired rounded square, but that gives an unfair advantage to attackers who approach
- * the target diagonally.
- *
- * If the target is small relative to the range (e.g. archers attacking anything),
- * then we cheat and pretend the target is actually a circle.
- * (TODO: that probably looks rubbish for things like walls?)
- *
- * If the target is large relative to the range (e.g. melee units attacking buildings),
- * then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough.
- * (Those units should set minRange to 0 so they'll never be considered *too* close.)
- */
-
- CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
- PathGoal goal;
- goal.x = obstruction.x;
- goal.z = obstruction.z;
+ // this is by how much our waypoints will be apart at most.
+ // this value here seems sensible enough.
+ PathGoal improvedGoal = goal;
+ improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
- entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true);
+ cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
- // Compare with previous obstruction
- ICmpObstructionManager::ObstructionSquare previousObstruction;
- cmpObstruction->GetPreviousObstructionSquare(previousObstruction);
- entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true);
+ m_PathRequest.expectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
+}
- bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero();
- if ((distance < minRange && previousDistance < minRange) || inside)
- {
- // Too close to the square - need to move away
+void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath)
+{
+ CmpPtr cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
+ return;
- // Circumscribe the square
- entity_pos_t circleRadius = halfSize.Length();
+ // restrict the search space as much as possible, bumping it up on increasing m_tries if it wasn't enough to find a path.
+ // TODO: this might not be very useful following my unitMotion rewrite, and could probably be removed somewhat safely.
+ fixed distanceToGoal = goal.DistanceToPoint(from) + std::max(fixed::FromInt(TERRAIN_TILE_SIZE), goal.hw);
- entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA;
+ // wrapping around on m_Tries isn't really a problem so don't check for overflow.
+ fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * (++m_Tries + 1), distanceToGoal);
+ fixed upperBound = std::max(SHORT_PATH_NORMAL_SEARCH_RANGE, distanceToGoal);
- if (ShouldTreatTargetAsCircle(minRange, circleRadius))
- {
- // The target is small relative to our range, so pretend it's a circle
- goal.type = PathGoal::INVERTED_CIRCLE;
- goal.hw = circleRadius + goalDistance;
- }
- else
- {
- goal.type = PathGoal::INVERTED_SQUARE;
- goal.u = obstruction.u;
- goal.v = obstruction.v;
- goal.hw = obstruction.hw + goalDistance;
- goal.hh = obstruction.hh + goalDistance;
- }
- }
- else if (maxRange < entity_pos_t::Zero() || distance < maxRange || previousDistance < maxRange)
+ if (searchRange > upperBound)
+ searchRange = upperBound;
+
+ if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE && alternativePath)
{
- // We're already in range - no need to move anywhere
- FaceTowardsPointFromPos(pos, goal.x, goal.z);
- return false;
+ // this is too far away, trash our path
+ if (m_PathRequest.expectedPathTicket != 0)
+ return;
+
+ m_PathRequest.isAlternativePath = false;
+ RequestNewPath();
+ return;
}
- else
- {
- // We might need to move closer:
- // Circumscribe the square
- entity_pos_t circleRadius = halfSize.Length();
+ m_PathRequest.runShortPathValidation = true;
+ m_PathRequest.isAlternativePath = alternativePath;
- if (ShouldTreatTargetAsCircle(maxRange, circleRadius))
- {
- // The target is small relative to our range, so pretend it's a circle
+ m_PathRequest.expectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetEntityId(), GetEntityId());
+}
- // Note that the distance to the circle will always be less than
- // the distance to the square, so the previous "distance < maxRange"
- // check is still valid (though not sufficient)
- entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
- entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius;
+void CCmpUnitMotion::DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints)
+{
+ CmpPtr cmpPathfinder(GetSystemEntity());
- if (circleDistance < maxRange || previousCircleDistance < maxRange)
- {
- // We're already in range - no need to move anywhere
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(pos, goal.x, goal.z);
- return false;
- }
+ // we're far away, just drop waypoints up to
+ // Check wether our next waypoint is obstructed in which case skip it.
+ // TODO: would be good to have a faster function here.
+ if (!cmpPathfinder->CheckMovement(GetObstructionFilter(false), waypoints.back().x, waypoints.back().z, waypoints.back().x, waypoints.back().z, m_Clearance, m_PassClass))
+ waypoints.pop_back();
- entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA;
+ // let's drop waypoints until we have dropped enough or are out of waypoints.
+ fixed droppedDistance = fixed::Zero();
+ CFixedVector2D currentPos = position;
+ while (!waypoints.empty() && droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED)
+ {
+ CFixedVector2D nextWpt(waypoints.back().x, waypoints.back().z);
+ droppedDistance += (nextWpt - currentPos).Length();
+ currentPos = nextWpt;
+ if (droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED)
+ waypoints.pop_back();
+ }
+}
- goal.type = PathGoal::CIRCLE;
- goal.hw = circleRadius + goalDistance;
- }
- else
- {
- // The target is large relative to our range, so treat it as a square and
- // get close enough that the diagonals come within range
+bool CCmpUnitMotion::SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable)
+{
+ // This sets up a new destination, scrap whatever came before.
+ DiscardMove();
- entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2)
+ m_Destination = SMotionGoal(CFixedVector2D(x, z), range);
+ m_CurrentGoal = m_Destination;
- goal.type = PathGoal::SQUARE;
- goal.u = obstruction.u;
- goal.v = obstruction.v;
- entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself
- goal.hw = obstruction.hw + delta;
- goal.hh = obstruction.hh + delta;
- }
- }
+ bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition
- m_State = STATE_INDIVIDUAL_PATH;
- m_TargetEntity = target;
- m_TargetOffset = CFixedVector2D();
- m_TargetMinRange = minRange;
- m_TargetMaxRange = maxRange;
- m_FinalGoal = goal;
- m_Tries = 0;
+ return reachable;
+}
- BeginPathing(pos, goal);
+bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable)
+{
+ // This sets up a new destination, scrap whatever came before.
+ DiscardMove();
- return true;
+ // validate entity's existence.
+ CmpPtr cmpPosition(GetSimContext(), ent);
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+
+ m_Destination = SMotionGoal(ent, range);
+ m_CurrentGoal = m_Destination;
+
+ bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition
+
+ return reachable;
+}
+
+bool CCmpUnitMotion::IsFormationMember() const {
+ return m_Destination.IsFormationGoal();
}
void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
{
+ // This sets up a new destination, scrap whatever came before.
+ DiscardMove();
+
+ // validate entity's existence.
CmpPtr cmpPosition(GetSimContext(), target);
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
- CFixedVector2D pos = cmpPosition->GetPosition2D();
-
- PathGoal goal;
- goal.type = PathGoal::POINT;
- goal.x = pos.X;
- goal.z = pos.Y;
-
- m_State = STATE_FORMATIONMEMBER_PATH;
- m_TargetEntity = target;
- m_TargetOffset = CFixedVector2D(x, z);
- m_TargetMinRange = entity_pos_t::Zero();
- m_TargetMaxRange = entity_pos_t::Zero();
- m_FinalGoal = goal;
- m_Tries = 0;
+ m_Destination = SMotionGoal(target, x, z);
+ m_CurrentGoal = m_Destination;
- BeginPathing(pos, goal);
+ RequestNewPath(true); // calls RecomputeGoalPosition
}
+bool CCmpUnitMotion::TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range)
+{
+ // Does not reset destination, this is a temporary rerouting.
+ StopMoving();
+ ResetPathfinding();
+ m_CurrentGoal = SMotionGoal(CFixedVector2D(x, z), range);
+ // we must set evenUnreachable to true otherwise if the path is unreachable we'll DiscardMove() and we don't want to do that.
+ bool reachable = RequestNewPath(true); // calls RecomputeGoalPosition
+ // if this is false, whoever called this function should probably un-reroute us and let us go on our way.
+ return reachable;
+}
+bool CCmpUnitMotion::GoBackToOriginalDestination()
+{
+ SMotionGoal original = m_Destination;
+
+ // assume evenUnreachable, since if it was false originally we'd have stopped by now.
+ if (original.IsEntity())
+ return SetNewDestinationAsEntity(original.GetEntity(), original.Range(), true);
+ else
+ return SetNewDestinationAsPosition(original.GetPosition().X,original.GetPosition().Y, original.Range(), true);
+}
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color)
{
@@ -1673,8 +1618,24 @@
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
}
- float x = cmpPosition->GetPosition2D().X.ToFloat();
- float z = cmpPosition->GetPosition2D().Y.ToFloat();
+
+ if (CurrentGoalHasValidPosition())
+ {
+ float x = GetGoalPosition(m_CurrentGoal).X.ToFloat();
+ float z = GetGoalPosition(m_CurrentGoal).Y.ToFloat();
+ lines.push_back(SOverlayLine());
+ lines.back().m_Color = CColor(0.0f, 1.0f, 0.0f, 1.0f);
+ SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.4f, lines.back(), floating);
+ }
+
+ float x = m_Goal.x.ToFloat();
+ float z = m_Goal.z.ToFloat();
+ lines.push_back(SOverlayLine());
+ lines.back().m_Color = CColor(0.0f, 1.0f, 1.0f, 1.0f);
+ SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
+
+ x = cmpPosition->GetPosition2D().X.ToFloat();
+ z = cmpPosition->GetPosition2D().Y.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
@@ -1688,12 +1649,11 @@
if (!m_DebugOverlayEnabled)
return;
- RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
- RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
-
- for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayLongPathLines[i]);
+ RenderPath(m_Path, m_DebugOverlayPathLines, OVERLAY_COLOR_PATH);
+ RenderPath(m_AlternativePath, m_DebugOverlayPathLines1, OVERLAY_COLOR_ALTERNATIVE_PATH);
- for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayShortPathLines[i]);
+ for (size_t i = 0; i < m_DebugOverlayPathLines1.size(); ++i)
+ collector.Submit(&m_DebugOverlayPathLines1[i]);
+ for (size_t i = 0; i < m_DebugOverlayPathLines.size(); ++i)
+ collector.Submit(&m_DebugOverlayPathLines[i]);
}
Index: source/simulation2/components/CCmpUnitMotionManager.cpp
===================================================================
--- /dev/null
+++ source/simulation2/components/CCmpUnitMotionManager.cpp
@@ -0,0 +1,135 @@
+/* Copyright (C) 2018 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "simulation2/system/Component.h"
+#include "simulation2/system/EntityMap.h"
+#include "ICmpUnitMotionManager.h"
+
+#include "ICmpUnitMotion.h"
+
+#include "simulation2/MessageTypes.h"
+
+/**
+
+ */
+class CCmpUnitMotionManager : public ICmpUnitMotionManager
+{
+public:
+ static void ClassInit(CComponentManager& componentManager)
+ {
+ componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
+ }
+
+ DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
+
+ static std::string GetSchema()
+ {
+ return "";
+ }
+
+ EntityMap m_Units;
+
+ virtual void Init(const CParamNode& UNUSED(paramNode))
+ {
+ }
+
+ virtual void Deinit()
+ {
+ }
+
+ virtual void Serialize(ISerializer& UNUSED(serialize))
+ {
+ }
+
+ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
+ {
+ Init(paramNode);
+ }
+
+ virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
+ {
+ switch (msg.GetType())
+ {
+ case MT_Update_MotionUnit:
+ {
+ fixed dt = static_cast (msg).turnLength;
+ ValidateAllUnits();
+ MoveAllUnits(dt);
+ HandleAllFailuresToMove();
+ break;
+ }
+ }
+ }
+
+ virtual void RegisterUnit(entity_id_t ent)
+ {
+ m_Units.insert(ent, ent);
+ }
+
+ virtual void UnregisterUnit(entity_id_t ent)
+ {
+ m_Units.erase(ent);
+ }
+
+ void ValidateAllUnits();
+ void MoveAllUnits(fixed dt);
+ void HandleAllFailuresToMove();
+};
+
+void CCmpUnitMotionManager::ValidateAllUnits()
+{
+ for (auto entity : m_Units)
+ {
+ CmpPtr cmpUnitMotion(GetSimContext(), entity.first);
+ // no need to check for cmpUnitMotion's existence here.
+ cmpUnitMotion->ValidateCurrentPath();
+ }
+}
+
+void CCmpUnitMotionManager::MoveAllUnits(fixed dt)
+{
+ /**
+ * First pass: try moving units along their path
+ * Second pass: handle consequences of that
+ * This architecture will let us easily add pushing to the first pass,
+ * and would help threading it in the future (as TryMoving never sends messages).
+ */
+ for (auto entity : m_Units)
+ {
+ CmpPtr cmpUnitMotion(GetSimContext(), entity.first);
+ // no need to check for cmpUnitMotion's existence here.
+
+ cmpUnitMotion->TryMoving(dt);
+ }
+ for (auto entity : m_Units)
+ {
+ CmpPtr cmpUnitMotion(GetSimContext(), entity.first);
+ cmpUnitMotion->PerformActualMovement(dt);
+ cmpUnitMotion->HandleMoveFailures();
+ cmpUnitMotion->AnticipatePathingNeeds();
+ }
+
+}
+
+void CCmpUnitMotionManager::HandleAllFailuresToMove()
+{
+
+}
+
+REGISTER_COMPONENT_TYPE(UnitMotionManager)
Index: source/simulation2/components/CCmpVisualActor.cpp
===================================================================
--- source/simulation2/components/CCmpVisualActor.cpp
+++ source/simulation2/components/CCmpVisualActor.cpp
@@ -55,7 +55,6 @@
public:
static void ClassInit(CComponentManager& componentManager)
{
- componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
@@ -72,18 +71,18 @@
fixed m_R, m_G, m_B; // shading color
- std::map m_AnimOverride;
-
// Current animation state
- fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode
std::string m_AnimName;
bool m_AnimOnce;
fixed m_AnimSpeed;
std::wstring m_SoundGroup;
fixed m_AnimDesync;
- fixed m_AnimSyncRepeatTime; // 0.0 if not synced
+ fixed m_AnimSyncRepeatTime;
fixed m_AnimSyncOffsetTime;
+ std::string m_MovingPrefix;
+ fixed m_MovingSpeed;
+
std::map m_VariantSelections;
u32 m_Seed; // seed used for random variations
@@ -191,6 +190,7 @@
{
m_Unit = NULL;
m_R = m_G = m_B = fixed::FromInt(1);
+ m_MovingSpeed = fixed::FromInt(1);
m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk();
@@ -226,9 +226,6 @@
serialize.NumberFixed_Unbounded("g", m_G);
serialize.NumberFixed_Unbounded("b", m_B);
- SerializeMap()(serialize, "anim overrides", m_AnimOverride);
-
- serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold);
serialize.StringASCII("anim name", m_AnimName, 0, 256);
serialize.Bool("anim once", m_AnimOnce);
serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed);
@@ -237,6 +234,9 @@
serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime);
serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime);
+ serialize.NumberFixed_Unbounded("anim moving speed", m_MovingSpeed);
+ serialize.StringASCII("anim moving prefix", m_MovingPrefix, 0, 256);
+
SerializeMap()(serialize, "variation", m_VariantSelections);
serialize.NumberU32_Unbounded("seed", m_Seed);
@@ -283,12 +283,6 @@
{
switch (msg.GetType())
{
- case MT_Update_Final:
- {
- const CMessageUpdate_Final& msgData = static_cast (msg);
- Update(msgData.turnLength);
- break;
- }
case MT_OwnershipChanged:
{
if (!m_Unit)
@@ -439,7 +433,6 @@
virtual void SelectAnimation(const std::string& name, bool once = false, fixed speed = fixed::FromInt(1))
{
- m_AnimRunThreshold = fixed::Zero();
m_AnimName = name;
m_AnimOnce = once;
m_AnimSpeed = speed;
@@ -448,7 +441,12 @@
m_AnimSyncRepeatTime = fixed::Zero();
m_AnimSyncOffsetTime = fixed::Zero();
- SetVariant("animation", m_AnimName);
+ // TODO: change this once we support walk/run-anims
+ std::string animName = name;
+ /*if (!m_MovingPrefix.empty() && m_AnimName != "idle")
+ animName = m_MovingPrefix + "-" + m_AnimName;
+ else */if (!m_MovingPrefix.empty())
+ animName = m_MovingPrefix;
if (!m_Unit || !m_Unit->GetAnimation() || !m_Unit->GetID())
return;
@@ -457,24 +455,29 @@
if (cmpSound)
m_SoundGroup = cmpSound->GetSoundGroup(wstring_from_utf8(m_AnimName));
- m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
- }
+ SetVariant("animation", animName);
- virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace)
- {
- m_AnimOverride[name] = replace;
+ if (m_Unit && m_Unit->GetAnimation())
+ m_Unit->GetAnimation()->SetAnimationState(animName, m_AnimOnce, m_MovingSpeed.Multiply(m_AnimSpeed).ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
}
- virtual void ResetMoveAnimation(const std::string& name)
+ virtual void SetMovingSpeed(fixed movingSpeed)
{
- std::map::const_iterator it = m_AnimOverride.find(name);
- if (it != m_AnimOverride.end())
- m_AnimOverride.erase(name);
- }
+ // TODO: don't copy strings for fun.
+ std::string prefix;
+ if (movingSpeed.IsZero())
+ prefix = "";
+ else
+ {
+ CmpPtr cmpUnitMotion(GetEntityHandle());
+ if (!cmpUnitMotion)
+ return;
+ prefix = cmpUnitMotion->GetSpeedRatio() <= fixed::FromInt(1) ? "walk" : "run";
+ }
+ m_MovingPrefix = prefix;
+ m_MovingSpeed = movingSpeed.IsZero() ? fixed::FromInt(1) : movingSpeed;
- virtual void SelectMovementAnimation(fixed runThreshold)
- {
- m_AnimRunThreshold = runThreshold;
+ SelectAnimation(m_AnimName, m_AnimOnce, m_AnimSpeed);
}
virtual void SetAnimationSyncRepeat(fixed repeattime)
@@ -555,8 +558,6 @@
// ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical.
// It is also used by ReloadActor.
void ReloadUnitAnimation();
-
- void Update(fixed turnLength);
};
REGISTER_COMPONENT_TYPE(VisualActor)
@@ -762,45 +763,3 @@
m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat());
}
-void CCmpVisualActor::Update(fixed UNUSED(turnLength))
-{
- // This function is currently only used to update the animation if the speed in
- // CCmpUnitMotion changes. This also only happens in the "special movement mode"
- // triggered by SelectMovementAnimation.
-
- // TODO: This should become event based, in order to save performance and to make the code
- // far less hacky. We should also take into account the speed when the animation is different
- // from the "special movement mode" walking animation.
-
- // If we're not in the special movement mode, nothing to do.
- if (m_AnimRunThreshold.IsZero())
- return;
-
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
-
- CmpPtr cmpUnitMotion(GetEntityHandle());
- if (!cmpUnitMotion)
- return;
-
- fixed speed = cmpUnitMotion->GetCurrentSpeed();
- std::string name;
-
- if (speed.IsZero())
- {
- speed = fixed::FromFloat(1.f);
- name = "idle";
- }
- else
- name = speed < m_AnimRunThreshold ? "walk" : "run";
-
- std::map::const_iterator it = m_AnimOverride.find(name);
- if (it != m_AnimOverride.end())
- name = it->second;
-
- // Selecting the animation is going to reset the anim run threshold, so save it
- fixed runThreshold = m_AnimRunThreshold;
- SelectAnimation(name, false, speed);
- m_AnimRunThreshold = runThreshold;
-}
Index: source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- source/simulation2/components/ICmpUnitMotion.h
+++ source/simulation2/components/ICmpUnitMotion.h
@@ -27,62 +27,83 @@
* Motion interface for entities with complex movement capabilities.
* (Simpler motion is handled by ICmpMotion instead.)
*
- * It should eventually support different movement speeds, moving to areas
- * instead of points, moving as part of a group, moving as part of a formation,
- * etc.
+ * This component is designed to handle clever individual movement,
+ * not movement as part of an integrated group (formation, bataillon…)
+ * and another component should probably be designed for that.
*/
class ICmpUnitMotion : public IComponent
{
public:
/**
- * Attempt to walk into range of a to a given point, or as close as possible.
- * The range is measured from the center of the unit.
- * If the unit is already in range, or cannot move anywhere at all, or if there is
- * some other error, then returns false.
- * Otherwise, returns true and sends a MotionChanged message after starting to move,
- * and sends another MotionChanged after finishing moving.
- * If maxRange is negative, then the maximum range is treated as infinity.
+ * Resets motion and assigns a new 2D position as destination.
+ * Returns false if the position is unreachable (or if the move could not be completed for any other reason).
+ * Otherwise, returns true.
+ * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving.
+ * Otherwise, the unit will try to go to another position as close as possible to the destination.
*/
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) = 0;
/**
- * Attempt to walk into range of a given target entity, or as close as possible.
- * The range is measured between approximately the edges of the unit and the target, so that
- * maxRange=0 is not unreachably close to the target.
- * If the unit is already in range, or cannot move anywhere at all, or if there is
- * some other error, then returns false.
- * Otherwise, returns true and sends a MotionChanged message after starting to move,
- * and sends another MotionChanged after finishing moving.
- * If maxRange is negative, then the maximum range is treated as infinity.
+ * Resets motion and assigns a new entity as destination.
+ * Returns false if the entity is unreachable (or if the move could not be completed for any other reason).
+ * Otherwise, returns true.
+ * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving.
+ * Otherwise, the unit will try to go to another position as close as possible to the destination.
*/
- virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable) = 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.
+ * Acts like SetNewDestinationAsEntity with an offset.
*/
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
+ /**
+ * Set m_CurrentGoal to the given position. This does not affect m_Destination.
+ * This does not reset the current move or send any specific message.
+ */
+ virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range) = 0;
+
+ /**
+ * Resets our pathfinding towards the original m_Destination.
+ * If it wasn't modified by a TemporaryRerouteToPosition, this does nothing (except potentially stop the unit for one turn).
+ * returns the same as the original SetNewDestinationAsXYZ call, with evenUnreachable set to true.
+ */
+ virtual bool GoBackToOriginalDestination() = 0;
+
/**
* Turn to look towards the given point.
*/
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0;
/**
- * Stop moving immediately.
+ * Turn to look towards the given entity.
+ */
+ virtual void FaceTowardsEntity(entity_id_t ent) = 0;
+
+ /**
+ * Determine whether to abort or retry X times if pathing fails.
+ * Generally safer to let it abort and inform us.
+ */
+ virtual void SetAbortIfStuck(u8 shouldAbort) = 0;
+
+ /**
+ * Stop moving, clear any destination, path, and ticket pending.
+ * Basically resets the unit's motion.
+ * Won't send any message.
*/
- virtual void StopMoving() = 0;
+ virtual void DiscardMove() = 0;
/**
- * Get the current movement speed.
+ * Returns the reachable goal of the unit, ie the goal it's actually moving towards.
+ * If the caller knows the original goal, it can more intelligently decide to stop or continue walking order.
*/
- virtual fixed GetCurrentSpeed() const = 0;
+ virtual CFixedVector2D GetReachableGoalPosition() = 0;
/**
- * Get whether the unit is moving.
+ * Asks wether the unit has a path to follow
*/
- virtual bool IsMoving() const = 0;
+ virtual bool HasValidPath() = 0;
/**
* Get how much faster/slower we are than normal.
@@ -100,6 +121,17 @@
*/
virtual void SetSpeedRatio(fixed ratio) = 0;
+ /**
+ * Get whether the unit is actually moving on the map this turn.
+ */
+ virtual bool IsActuallyMoving() = 0;
+
+ /**
+ * Get whether a unit is trying to go somewhere
+ * NB: this does not mean its position is actually changing right now.
+ */
+ virtual bool IsTryingToMove() = 0;
+
/**
* Get the unit theoretical speed in metres per second.
* This is affected by SetSpeedRatio.
@@ -111,10 +143,6 @@
* Calls to SetSpeedRatio have no effect on this (as that affects actual speed, not template).
*/
virtual fixed GetWalkSpeed() const = 0;
- /**
- * Set whether the unit will turn to face the target point after finishing moving.
- */
- virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0;
/**
* Get the unit's passability class.
@@ -136,6 +164,48 @@
*/
virtual void SetDebugOverlay(bool enabled) = 0;
+ /**
+ * Quick note on how clever UnitMotion should be: UnitMotion should try to reach the current target (m_CurrentGoal)
+ * as well as it can. But it should not take any particular guess on wether something CAN or SHOULD be reached.
+ * Examples: chasing a unit that's faster than us is probably stupid. This is not UnitMotion's to say, UnitMotion should try.
+ * Likewise when requesting a new path, even if it's unreachable unitMotion must try its best (but can inform unitAI that it's being obtuse)
+ * However, if a chased unit is moving, we should try to anticipate its moves by any means possible.
+ */
+
+ /**
+ * Check that we still want to move, and whether our path is sensible if yes.
+ */
+ virtual void ValidateCurrentPath() = 0;
+
+ struct MovementMetadata
+ {
+ CFixedVector2D currentPos;
+ CFixedVector2D initialPos;
+ bool moveWentOK;
+ bool wasObstructed;
+ };
+
+ /**
+ * "Virtually" move the unit along its current path. May be obstructed...
+ * Sets m_MoveMetadata for the current turn.
+ */
+ virtual void TryMoving(fixed dt) = 0;
+
+ /**
+ * Actually move the position of the unit, by updating cmpPosition.
+ */
+ virtual void PerformActualMovement(fixed dt) = 0;
+
+ /**
+ * If we failed to move, we should act upon it.
+ */
+ virtual void HandleMoveFailures() = 0;
+
+ /**
+ * Request new paths if these seem useful.
+ */
+ virtual void AnticipatePathingNeeds() = 0;
+
DECLARE_INTERFACE_TYPE(UnitMotion)
};
Index: source/simulation2/components/ICmpUnitMotion.cpp
===================================================================
--- source/simulation2/components/ICmpUnitMotion.cpp
+++ source/simulation2/components/ICmpUnitMotion.cpp
@@ -23,41 +23,55 @@
#include "simulation2/scripting/ScriptComponent.h"
BEGIN_INTERFACE_WRAPPER(UnitMotion)
-DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
-DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t)
+DEFINE_INTERFACE_METHOD_4("SetNewDestinationAsPosition", bool, ICmpUnitMotion, SetNewDestinationAsPosition, entity_pos_t, entity_pos_t, entity_pos_t, bool)
+DEFINE_INTERFACE_METHOD_3("SetNewDestinationAsEntity", bool, ICmpUnitMotion, SetNewDestinationAsEntity, entity_id_t, entity_pos_t, bool)
DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t)
-DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
-DEFINE_INTERFACE_METHOD_CONST_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed)
+DEFINE_INTERFACE_METHOD_1("FaceTowardsEntity", void, ICmpUnitMotion, FaceTowardsEntity, entity_id_t)
+DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8)
+DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove)
+DEFINE_INTERFACE_METHOD_0("GetReachableGoalPosition", CFixedVector2D, ICmpUnitMotion, GetReachableGoalPosition)
+DEFINE_INTERFACE_METHOD_0("HasValidPath", bool, ICmpUnitMotion, HasValidPath)
DEFINE_INTERFACE_METHOD_CONST_0("GetRunSpeedMultiplier", fixed, ICmpUnitMotion, GetRunSpeedMultiplier)
DEFINE_INTERFACE_METHOD_1("SetSpeedRatio", void, ICmpUnitMotion, SetSpeedRatio, fixed)
-DEFINE_INTERFACE_METHOD_CONST_0("IsMoving", bool, ICmpUnitMotion, IsMoving)
+DEFINE_INTERFACE_METHOD_0("IsActuallyMoving", bool, ICmpUnitMotion, IsActuallyMoving)
+DEFINE_INTERFACE_METHOD_0("IsTryingToMove", bool, ICmpUnitMotion, IsTryingToMove)
DEFINE_INTERFACE_METHOD_CONST_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed)
DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed)
DEFINE_INTERFACE_METHOD_CONST_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName)
DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance)
-DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion)
+
class CCmpUnitMotionScripted : public ICmpUnitMotion
{
public:
DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted)
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool UNUSED(evenUnreachable))
{
- return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange);
+ return m_Script.Call("SetNewDestinationAsPosition", x, z, range, true);
}
- virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool UNUSED(evenUnreachable))
{
- return m_Script.Call("MoveToTargetRange", target, minRange, maxRange);
+ return m_Script.Call("SetNewDestinationAsEntity", target, range, true);
}
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
{
- m_Script.CallVoid("MoveToFormationOffset", target, x, z);
+ return m_Script.CallVoid("MoveToFormationOffset", target, x, z);
+ }
+
+ virtual bool TemporaryRerouteToPosition(entity_pos_t, entity_pos_t, entity_pos_t)
+ {
+ return false;
+ }
+
+ virtual bool GoBackToOriginalDestination()
+ {
+ return true;
}
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
@@ -65,14 +79,24 @@
m_Script.CallVoid("FaceTowardsPoint", x, z);
}
- virtual void StopMoving()
+ virtual void FaceTowardsEntity(entity_id_t ent)
+ {
+ m_Script.CallVoid("FaceTowardsEntity", ent);
+ }
+
+ virtual void DiscardMove()
{
- m_Script.CallVoid("StopMoving");
+ m_Script.CallVoid("DiscardMove");
}
- virtual fixed GetCurrentSpeed() const
+ virtual void SetAbortIfStuck(u8 shouldAbort)
{
- return m_Script.Call("GetCurrentSpeed");
+ m_Script.CallVoid("SetAbortIfStuck", shouldAbort);
+ }
+
+ virtual fixed GetActualSpeed()
+ {
+ return m_Script.Call("GetActualSpeed");
}
virtual void SetSpeedRatio(fixed ratio)
@@ -85,9 +109,24 @@
return m_Script.Call("GetRunSpeedMultiplier");
}
- virtual bool IsMoving() const
+ virtual CFixedVector2D GetReachableGoalPosition()
+ {
+ return m_Script.Call("GetReachableGoalPosition");
+ }
+
+ virtual bool HasValidPath()
{
- return m_Script.Call("IsMoving");
+ return m_Script.Call("HasValidPath");
+ }
+
+ virtual bool IsActuallyMoving()
+ {
+ return m_Script.Call("IsActuallyMoving");
+ }
+
+ virtual bool IsTryingToMove()
+ {
+ return m_Script.Call("IsTryingToMove");
}
virtual fixed GetSpeed() const
@@ -100,11 +139,6 @@
return m_Script.Call("GetWalkSpeed");
}
- virtual void SetFacePointAfterMove(bool facePointAfterMove)
- {
- m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove);
- }
-
virtual pass_class_t GetPassabilityClass() const
{
return m_Script.Call("GetPassabilityClass");
@@ -130,6 +164,30 @@
m_Script.CallVoid("SetDebugOverlay", enabled);
}
+ virtual void ValidateCurrentPath()
+ {
+ m_Script.CallVoid("ValidateCurrentPath");
+ }
+
+ virtual void TryMoving(fixed dt)
+ {
+ m_Script.CallVoid("TryMoving", dt);
+ }
+
+ virtual void PerformActualMovement(fixed dt)
+ {
+ m_Script.CallVoid("PerformActualMovement", dt);
+ }
+
+ virtual void HandleMoveFailures()
+ {
+ m_Script.CallVoid("HandleMoveFailures");
+ }
+
+ virtual void AnticipatePathingNeeds()
+ {
+ m_Script.CallVoid("AnticipatePathingNeeds");
+ }
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(UnitMotionScripted)
Index: source/simulation2/components/ICmpUnitMotionManager.h
===================================================================
--- /dev/null
+++ source/simulation2/components/ICmpUnitMotionManager.h
@@ -0,0 +1,34 @@
+/* Copyright (C) 2018 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_ICMPUNITMOTIONMANAGER
+#define INCLUDED_ICMPUNITMOTIONMANAGER
+
+#include "simulation2/system/Interface.h"
+
+class ICmpUnitMotionManager : public IComponent
+{
+public:
+ DECLARE_INTERFACE_TYPE(UnitMotionManager)
+
+ virtual void RegisterUnit(entity_id_t ent) = 0;
+
+ virtual void UnregisterUnit(entity_id_t ent) = 0;
+
+};
+
+#endif // INCLUDED_ICMPUNITMOTIONMANAGER
Index: source/simulation2/components/ICmpUnitMotionManager.cpp
===================================================================
--- /dev/null
+++ source/simulation2/components/ICmpUnitMotionManager.cpp
@@ -0,0 +1,25 @@
+/* Copyright (C) 2018 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "ICmpUnitMotionManager.h"
+
+#include "simulation2/system/InterfaceScripted.h"
+
+BEGIN_INTERFACE_WRAPPER(UnitMotionManager)
+END_INTERFACE_WRAPPER(UnitMotionManager)
Index: source/simulation2/components/ICmpVisual.h
===================================================================
--- source/simulation2/components/ICmpVisual.h
+++ source/simulation2/components/ICmpVisual.h
@@ -106,25 +106,10 @@
virtual void SelectAnimation(const std::string& name, bool once, fixed speed) = 0;
/**
- * Replaces a specified animation with another. Only affects the special speed-based
- * animation determination behaviour.
- * @param name Animation to match.
- * @param replace Animation that should replace the matched animation.
+ * Tell the visual actor that the unit is currently moving at the given speed.
+ * If speed is 0, the unit will become idle.
*/
- virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) = 0;
-
- /**
- * Ensures that the given animation will be used when it normally would be,
- * removing reference to any animation that might replace it.
- * @param name Animation name to remove from the replacement map.
- */
- virtual void ResetMoveAnimation(const std::string& name) = 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(fixed runThreshold) = 0;
+ virtual void SetMovingSpeed(fixed movingSpeed) = 0;
/**
* Adjust the speed of the current animation, so it can match simulation events.
Index: source/simulation2/components/ICmpVisual.cpp
===================================================================
--- source/simulation2/components/ICmpVisual.cpp
+++ source/simulation2/components/ICmpVisual.cpp
@@ -27,13 +27,11 @@
DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileActor", std::wstring, ICmpVisual, GetProjectileActor)
DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileLaunchPoint", CFixedVector3D, ICmpVisual, GetProjectileLaunchPoint)
DEFINE_INTERFACE_METHOD_3("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed)
-DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, fixed)
-DEFINE_INTERFACE_METHOD_1("ResetMoveAnimation", void, ICmpVisual, ResetMoveAnimation, std::string)
-DEFINE_INTERFACE_METHOD_2("ReplaceMoveAnimation", void, ICmpVisual, ReplaceMoveAnimation, std::string, std::string)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, fixed)
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, fixed)
DEFINE_INTERFACE_METHOD_4("SetShadingColor", void, ICmpVisual, SetShadingColor, fixed, fixed, fixed, fixed)
DEFINE_INTERFACE_METHOD_2("SetVariable", void, ICmpVisual, SetVariable, std::string, float)
+DEFINE_INTERFACE_METHOD_1("SetMovingSpeed", void, ICmpVisual, SetMovingSpeed, fixed)
DEFINE_INTERFACE_METHOD_CONST_0("GetActorSeed", u32, ICmpVisual, GetActorSeed)
DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32)
DEFINE_INTERFACE_METHOD_CONST_0("HasConstructionPreview", bool, ICmpVisual, HasConstructionPreview)
Index: source/simulation2/helpers/Pathfinding.h
===================================================================
--- source/simulation2/helpers/Pathfinding.h
+++ source/simulation2/helpers/Pathfinding.h
@@ -129,6 +129,7 @@
* between translation units.
* TODO: figure out whether this is actually needed. It was added back in r8751 (in 2010) for unclear reasons
* and it does not seem to really improve behavior today
+ * Note by Wraitii to wraitii: you just removed this in UnitMotion, delete it if it ends up being unecessary as expected.
*/
const entity_pos_t GOAL_DELTA = NAVCELL_SIZE/8;
Index: source/simulation2/helpers/Rasterize.cpp
===================================================================
--- source/simulation2/helpers/Rasterize.cpp
+++ source/simulation2/helpers/Rasterize.cpp
@@ -40,6 +40,10 @@
// A side effect is that the basic clearance has been set to 0.8, so removing this constant should be done
// in parallel with setting clearance back to 1 for the default passability class (though this isn't strictly necessary).
// Also: the code detecting foundation obstruction in CcmpObstructionManager had to be changed similarly.
+ //
+ // NB: because of this, if a entity wants to move very close to the actual obstruction of another entity
+ // it may never be able to get there, as there could be 1 extra navcell in between the actual square shape and the raster.
+ // So while this stands, max ranges should not be too small, or units might get stuck.
entity_pos_t rasterClearance = clearance + Pathfinding::CLEARANCE_EXTENSION_RADIUS;
// Get the bounds of cells that might possibly be within the shape
Index: source/simulation2/scripting/MessageTypeConversions.cpp
===================================================================
--- source/simulation2/scripting/MessageTypeConversions.cpp
+++ source/simulation2/scripting/MessageTypeConversions.cpp
@@ -266,20 +266,30 @@
////////////////////////////////
-JS::Value CMessageMotionChanged::ToJSVal(const ScriptInterface& scriptInterface) const
+JS::Value CMessageMovePaused::ToJSVal(const ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
- SET_MSG_PROPERTY(starting);
- SET_MSG_PROPERTY(error);
return JS::ObjectValue(*obj);
}
-CMessage* CMessageMotionChanged::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val)
+CMessage* CMessageMovePaused::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val)
{
FROMJSVAL_SETUP();
- GET_MSG_PROPERTY(bool, starting);
- GET_MSG_PROPERTY(bool, error);
- return new CMessageMotionChanged(starting, error);
+ return new CMessageMovePaused();
+}
+
+////////////////////////////////
+
+JS::Value CMessageMoveFailure::ToJSVal(const ScriptInterface& scriptInterface) const
+{
+ TOJSVAL_SETUP();
+ return JS::ObjectValue(*obj);
+}
+
+CMessage* CMessageMoveFailure::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val)
+{
+ FROMJSVAL_SETUP();
+ return new CMessageMoveFailure();
}
////////////////////////////////
Index: source/simulation2/system/ComponentManager.cpp
===================================================================
--- source/simulation2/system/ComponentManager.cpp
+++ source/simulation2/system/ComponentManager.cpp
@@ -727,6 +727,7 @@
AddComponent(m_SystemEntity, CID_Terrain, noParam);
AddComponent(m_SystemEntity, CID_TerritoryManager, noParam);
AddComponent(m_SystemEntity, CID_UnitRenderer, noParam);
+ AddComponent(m_SystemEntity, CID_UnitMotionManager, noParam);
AddComponent(m_SystemEntity, CID_WaterManager, noParam);
// Add scripted system components: