Index: ps/trunk/binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 22567) +++ ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 22568) @@ -1,995 +1,995 @@ function Formation() {} Formation.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; var g_ColumnDistanceThreshold = 128; // distance at which we'll switch between column/box formations Formation.prototype.Init = function() { this.formationShape = this.template.FormationShape; this.sortingClasses = this.template.SortingClasses.split(/\s+/g); this.sortingOrder = this.template.SortingOrder; this.shiftRows = this.template.ShiftRows == "true"; this.separationMultiplier = { "width": +this.template.UnitSeparationWidthMultiplier, "depth": +this.template.UnitSeparationDepthMultiplier }; this.sloppyness = +this.template.Sloppyness; this.widthDepthRatio = +this.template.WidthDepthRatio; this.minColumns = +(this.template.MinColumns || 0); this.maxColumns = +(this.template.MaxColumns || 0); this.maxRows = +(this.template.MaxRows || 0); this.centerGap = +(this.template.CenterGap || 0); this.animations = []; if (this.template.Animations) { let differentAnimations = this.template.Animations.split(/\s*;\s*/); // loop over the different rectangulars that will map to different animations for (var rectAnimation of differentAnimations) { var rect, replacementAnimationName; [rect, replacementAnimationName] = rectAnimation.split(/\s*:\s*/); var rows, columns; [rows, columns] = rect.split(/\s*,\s*/); var minRow, maxRow, minColumn, maxColumn; [minRow, maxRow] = rows.split(/\s*\.\.\s*/); [minColumn, maxColumn] = columns.split(/\s*\.\.\s*/); this.animations.push({ "minRow": +minRow, "maxRow": +maxRow, "minColumn": +minColumn, "maxColumn": +maxColumn, "animation": replacementAnimationName }); } } this.members = []; // entity IDs currently belonging to this formation this.memberPositions = {}; this.maxRowsUsed = 0; this.maxColumnsUsed = []; this.inPosition = []; // entities that have reached their final position this.columnar = false; // whether we're travelling in column (vs box) formation this.rearrange = true; // whether we should rearrange all formation members this.formationMembersWithAura = []; // Members with a formation aura this.width = 0; this.depth = 0; this.oldOrientation = {"sin": 0, "cos": 0}; this.twinFormations = []; // distance from which two twin formations will merge into one. this.formationSeparation = 0; Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer) .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null); }; /** * Set the value from which two twin formations will become one. */ Formation.prototype.SetFormationSeparation = function(value) { this.formationSeparation = value; }; Formation.prototype.GetSize = function() { return {"width": this.width, "depth": this.depth}; }; Formation.prototype.GetSpeedMultiplier = function() { return +this.template.SpeedMultiplier; }; Formation.prototype.GetMemberCount = function() { return this.members.length; }; Formation.prototype.GetMembers = function() { return this.members; }; Formation.prototype.GetClosestMember = function(ent, filter) { var cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpEntPosition || !cmpEntPosition.IsInWorld()) return INVALID_ENTITY; var entPosition = cmpEntPosition.GetPosition2D(); var closestMember = INVALID_ENTITY; var closestDistance = Infinity; for (var member of this.members) { if (filter && !filter(ent)) continue; var cmpPosition = Engine.QueryInterface(member, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition2D(); var dist = entPosition.distanceToSquared(pos); if (dist < closestDistance) { closestMember = member; closestDistance = dist; } } return closestMember; }; /** * Returns the 'primary' member of this formation (typically the most * important unit type), for e.g. playing a representative sound. * Returns undefined if no members. * TODO: actually implement something like that; currently this just returns * the arbitrary first one. */ Formation.prototype.GetPrimaryMember = function() { return this.members[0]; }; /** * Get the formation animation for a certain member of this formation * @param entity The entity ID to get the animation for * @return The name of the transformed animation as defined in the template * E.g. "testudo_row1" */ Formation.prototype.GetFormationAnimation = function(entity) { var animationGroup = this.animations; if (!animationGroup.length || this.columnar || !this.memberPositions[entity]) return "formation"; var row = this.memberPositions[entity].row; var column = this.memberPositions[entity].column; for (var i = 0; i < animationGroup.length; ++i) { var minRow = animationGroup[i].minRow; if (minRow < 0) minRow += this.maxRowsUsed + 1; if (row < minRow) continue; var maxRow = animationGroup[i].maxRow; if (maxRow < 0) maxRow += this.maxRowsUsed + 1; if (row > maxRow) continue; var minColumn = animationGroup[i].minColumn; if (minColumn < 0) minColumn += this.maxColumnsUsed[row] + 1; if (column < minColumn) continue; var maxColumn = animationGroup[i].maxColumn; if (maxColumn < 0) maxColumn += this.maxColumnsUsed[row] + 1; if (column > maxColumn) continue; return animationGroup[i].animation; } return "formation"; }; /** * Permits formation members to register that they've reached their destination. */ Formation.prototype.SetInPosition = function(ent) { if (this.inPosition.indexOf(ent) != -1) return; // Rotate the entity to the right angle var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld()) cmpEntPosition.TurnTo(cmpPosition.GetRotation().y); this.inPosition.push(ent); }; /** * Called by formation members upon entering non-walking states. */ Formation.prototype.UnsetInPosition = function(ent) { var ind = this.inPosition.indexOf(ent); if (ind != -1) this.inPosition.splice(ind, 1); }; /** * Set whether we should rearrange formation members if * units are removed from the formation. */ Formation.prototype.SetRearrange = function(rearrange) { this.rearrange = rearrange; }; /** * Initialise the members of this formation. * Must only be called once. * All members must implement UnitAI. */ Formation.prototype.SetMembers = function(ents) { this.members = ents; for (var ent of this.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(this.entity); var cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); cmpAuras.ApplyFormationBonus(ents); } } this.offsets = undefined; // Locate this formation controller in the middle of its members this.MoveToMembersCenter(); // Compute the speed etc. of the formation this.ComputeMotionParameters(); }; /** * Remove the given list of entities. * The entities must already be members of this formation. */ Formation.prototype.RemoveMembers = function(ents) { this.offsets = undefined; this.members = this.members.filter(function(e) { return ents.indexOf(e) == -1; }); this.inPosition = this.inPosition.filter(function(e) { return ents.indexOf(e) == -1; }); for (var ent of ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.UpdateWorkOrders(); cmpUnitAI.SetFormationController(INVALID_ENTITY); } for (var ent of this.formationMembersWithAura) { var cmpAuras = Engine.QueryInterface(ent, IID_Auras); cmpAuras.RemoveFormationBonus(ents); // the unit with the aura is also removed from the formation if (ents.indexOf(ent) !== -1) cmpAuras.RemoveFormationBonus(this.members); } this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; }); // If there's nobody left, destroy the formation if (this.members.length == 0) { Engine.DestroyEntity(this.entity); return; } if (!this.rearrange) return; this.ComputeMotionParameters(); // Rearrange the remaining members this.MoveMembersIntoFormation(true, true); }; Formation.prototype.AddMembers = function(ents) { this.offsets = undefined; this.inPosition = []; for (let ent of this.formationMembersWithAura) { let cmpAuras = Engine.QueryInterface(ent, IID_Auras); cmpAuras.ApplyFormationBonus(ents); } this.members = this.members.concat(ents); for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(this.entity); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); cmpAuras.ApplyFormationBonus(this.members); } } this.MoveMembersIntoFormation(true, true); }; /** * Called when the formation stops moving in order to detect * units that have already reached their final positions. */ Formation.prototype.FindInPosition = function() { for (var i = 0; i < this.members.length; ++i) { var cmpUnitMotion = Engine.QueryInterface(this.members[i], IID_UnitMotion); - if (!cmpUnitMotion.IsMoving()) + if (!cmpUnitMotion.IsMoveRequested()) { // Verify that members are stopped in FORMATIONMEMBER.WALKING var cmpUnitAI = Engine.QueryInterface(this.members[i], IID_UnitAI); if (cmpUnitAI.IsWalking()) this.SetInPosition(this.members[i]); } } }; /** * Remove all members and destroy the formation. */ Formation.prototype.Disband = function() { for (var ent of this.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(INVALID_ENTITY); } for (var ent of this.formationMembersWithAura) { var cmpAuras = Engine.QueryInterface(ent, IID_Auras); cmpAuras.RemoveFormationBonus(this.members); } this.members = []; this.inPosition = []; this.formationMembersWithAura = []; this.offsets = undefined; Engine.DestroyEntity(this.entity); }; /** * Set all members to form up into the formation shape. * If moveCenter is true, the formation center will be reinitialised * to the center of the units. * If force is true, all individual orders of the formation units are replaced, * otherwise the order to walk into formation is just pushed to the front. */ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force) { if (!this.members.length) return; var active = []; var positions = []; for (var ent of this.members) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; active.push(ent); // query the 2D position as exact hight calculation isn't needed // but bring the position to the right coordinates var pos = cmpPosition.GetPosition2D(); positions.push(pos); } var avgpos = Vector2D.average(positions); // Reposition the formation if we're told to or if we don't already have a position var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var inWorld = cmpPosition.IsInWorld(); if (moveCenter || !inWorld) { cmpPosition.JumpTo(avgpos.x, avgpos.y); // Don't make the formation controller entity show up in range queries if (!inWorld) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetEntityFlag(this.entity, "normal", false); } } // Switch between column and box if necessary var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = walkingDistance > g_ColumnDistanceThreshold; if (columnar != this.columnar) { this.columnar = columnar; this.offsets = undefined; } var newOrientation = this.GetEstimatedOrientation(avgpos); var dSin = Math.abs(newOrientation.sin - this.oldOrientation.sin); var dCos = Math.abs(newOrientation.cos - this.oldOrientation.cos); // If the formation existed, only recalculate positions if the turning agle is somewhat biggish if (!this.offsets || dSin > 1 || dCos > 1) this.offsets = this.ComputeFormationOffsets(active, positions); this.oldOrientation = newOrientation; var xMax = 0; var yMax = 0; var xMin = 0; var yMin = 0; for (var i = 0; i < this.offsets.length; ++i) { var offset = this.offsets[i]; var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI); if (!cmpUnitAI) continue; var data = { "target": this.entity, "x": offset.x, "z": offset.y }; cmpUnitAI.AddOrder("FormationWalk", data, !force); xMax = Math.max(xMax, offset.x); yMax = Math.max(yMax, offset.y); xMin = Math.min(xMin, offset.x); yMin = Math.min(yMin, offset.y); } this.width = xMax - xMin; this.depth = yMax - yMin; }; Formation.prototype.MoveToMembersCenter = function() { var positions = []; for (var ent of this.members) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; positions.push(cmpPosition.GetPosition2D()); } var avgpos = Vector2D.average(positions); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var inWorld = cmpPosition.IsInWorld(); cmpPosition.JumpTo(avgpos.x, avgpos.y); // Don't make the formation controller show up in range queries if (!inWorld) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetEntityFlag(this.entity, "normal", false); } }; Formation.prototype.GetAvgFootprint = function(active) { var footprints = []; for (var ent of active) { var cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (cmpFootprint) footprints.push(cmpFootprint.GetShape()); } if (!footprints.length) return {"width":1, "depth": 1}; var r = {"width": 0, "depth": 0}; for (var shape of footprints) { if (shape.type == "circle") { r.width += shape.radius * 2; r.depth += shape.radius * 2; } else if (shape.type == "square") { r.width += shape.width; r.depth += shape.depth; } } r.width /= footprints.length; r.depth /= footprints.length; return r; }; Formation.prototype.ComputeFormationOffsets = function(active, positions) { var separation = this.GetAvgFootprint(active); separation.width *= this.separationMultiplier.width; separation.depth *= this.separationMultiplier.depth; if (this.columnar) var sortingClasses = ["Cavalry","Infantry"]; else var sortingClasses = this.sortingClasses.slice(); sortingClasses.push("Unknown"); // the entities will be assigned to positions in the formation in // the same order as the types list is ordered var types = {}; for (var i = 0; i < sortingClasses.length; ++i) types[sortingClasses[i]] = []; for (var i in active) { var cmpIdentity = Engine.QueryInterface(active[i], IID_Identity); var classes = cmpIdentity.GetClassesList(); var done = false; for (var c = 0; c < sortingClasses.length; ++c) { if (classes.indexOf(sortingClasses[c]) > -1) { types[sortingClasses[c]].push({"ent": active[i], "pos": positions[i]}); done = true; break; } } if (!done) types["Unknown"].push({"ent": active[i], "pos": positions[i]}); } var count = active.length; var shape = this.formationShape; var shiftRows = this.shiftRows; var centerGap = this.centerGap; var sortingOrder = this.sortingOrder; var offsets = []; // Choose a sensible size/shape for the various formations, depending on number of units var cols; if (this.columnar) { shape = "square"; cols = Math.min(count,3); shiftRows = false; centerGap = 0; sortingOrder = null; } else { var depth = Math.sqrt(count / this.widthDepthRatio); if (this.maxRows && depth > this.maxRows) depth = this.maxRows; cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0)); if (cols < this.minColumns) cols = Math.min(count, this.minColumns); if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth) cols = this.maxColumns; } // define special formations here if (this.template.FormationName == "Scatter") { var width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5; for (var i = 0; i < count; ++i) { var obj = new Vector2D(randFloat(0, width), randFloat(0, width)); obj.row = 1; obj.column = i + 1; offsets.push(obj); } } // For non-special formations, calculate the positions based on the number of entities this.maxColumnsUsed = []; this.maxRowsUsed = 0; if (shape != "special") { offsets = []; var r = 0; var left = count; // while there are units left, start a new row in the formation while (left > 0) { // save the position of the row var z = -r * separation.depth; // switch between the left and right side of the center to have a symmetrical distribution var side = 1; // determine the number of entities in this row of the formation if (shape == "square") { var n = cols; if (shiftRows) n -= r%2; } else if (shape == "triangle") { if (shiftRows) var n = r + 1; else var n = r * 2 + 1; } if (!shiftRows && n > left) n = left; for (var c = 0; c < n && left > 0; ++c) { // switch sides for the next entity side *= -1; if (n%2 == 0) var x = side * (Math.floor(c/2) + 0.5) * separation.width; else var x = side * Math.ceil(c/2) * separation.width; if (centerGap) { if (x == 0) // don't use the center position with a center gap continue; x += side * centerGap / 2; } var column = Math.ceil(n/2) + Math.ceil(c/2) * side; var r1 = randFloat(-1, 1) * this.sloppyness; var r2 = randFloat(-1, 1) * this.sloppyness; offsets.push(new Vector2D(x + r1, z + r2)); offsets[offsets.length - 1].row = r+1; offsets[offsets.length - 1].column = column; left--; } ++r; this.maxColumnsUsed[r] = n; } this.maxRowsUsed = r; } // make sure the average offset is zero, as the formation is centered around that // calculating offset distances without a zero average makes no sense, as the formation // will jump to a different position any time var avgoffset = Vector2D.average(offsets); offsets.forEach(function (o) {o.sub(avgoffset);}); // sort the available places in certain ways // the places first in the list will contain the heaviest units as defined by the order // of the types list if (this.sortingOrder == "fillFromTheSides") offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);}); else if (this.sortingOrder == "fillToTheCenter") offsets.sort(function(o1, o2) { return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y)); }); // query the 2D position of the formation var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var formationPos = cmpPosition.GetPosition2D(); // use realistic place assignment, // every soldier searches the closest available place in the formation var newOffsets = []; var realPositions = this.GetRealOffsetPositions(offsets, formationPos); for (var i = sortingClasses.length; i; --i) { var t = types[sortingClasses[i-1]]; if (!t.length) continue; var usedOffsets = offsets.splice(-t.length); var usedRealPositions = realPositions.splice(-t.length); for (var entPos of t) { var closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets); usedRealPositions.splice(closestOffsetId, 1); newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]); newOffsets[newOffsets.length - 1].ent = entPos.ent; } } return newOffsets; }; /** * Search the closest position in the realPositions list to the given entity * @param ent, the queried entity * @param realPositions, the world coordinates of the available offsets * @return the index of the closest offset position */ Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets) { var pos = entPos.pos; var closestOffsetId = -1; var offsetDistanceSq = Infinity; for (var i = 0; i < realPositions.length; i++) { var distSq = pos.distanceToSquared(realPositions[i]); if (distSq < offsetDistanceSq) { offsetDistanceSq = distSq; closestOffsetId = i; } } this.memberPositions[entPos.ent] = {"row": offsets[closestOffsetId].row, "column":offsets[closestOffsetId].column}; return closestOffsetId; }; /** * Get the world positions for a list of offsets in this formation */ Formation.prototype.GetRealOffsetPositions = function(offsets, pos) { var offsetPositions = []; var {sin, cos} = this.GetEstimatedOrientation(pos); // calculate the world positions for (var o of offsets) offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin)); return offsetPositions; }; /** * calculate the estimated rotation of the formation * based on the first unitAI target position when ordered to walk, * based on the current rotation in other cases * Return the sine and cosine of the angle */ Formation.prototype.GetEstimatedOrientation = function(pos) { var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var r = {"sin": 0, "cos": 1}; var unitAIState = cmpUnitAI.GetCurrentState(); if (unitAIState == "FORMATIONCONTROLLER.WALKING" || unitAIState == "FORMATIONCONTROLLER.COMBAT.APPROACHING") { var targetPos = cmpUnitAI.GetTargetPositions(); if (!targetPos.length) return r; var d = targetPos[0].sub(pos).normalize(); if (!d.x && !d.y) return r; r.cos = d.y; r.sin = d.x; } else { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition) return r; var rot = cmpPosition.GetRotation().y; r.sin = Math.sin(rot); r.cos = Math.cos(rot); } return r; }; /** * Set formation controller's speed based on its current members. */ Formation.prototype.ComputeMotionParameters = function() { var maxRadius = 0; var minSpeed = Infinity; for (var ent of this.members) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed()); } minSpeed *= this.GetSpeedMultiplier(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed()); }; Formation.prototype.ShapeUpdate = function() { // Check the distance to twin formations, and merge if when // the formations could collide for (var i = this.twinFormations.length - 1; i >= 0; --i) { // only do the check on one side if (this.twinFormations[i] <= this.entity) continue; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position); var cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation); if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation) continue; var thisPosition = cmpPosition.GetPosition2D(); var otherPosition = cmpOtherPosition.GetPosition2D(); var dx = thisPosition.x - otherPosition.x; var dy = thisPosition.y - otherPosition.y; var dist = Math.sqrt(dx * dx + dy * dy); var thisSize = this.GetSize(); var otherSize = cmpOtherFormation.GetSize(); var minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) + Math.max(otherSize.width / 2, otherSize.depth / 2) + this.formationSeparation; if (minDist < dist) continue; // merge the members from the twin formation into this one // twin formations should always have exactly the same orders let otherMembers = cmpOtherFormation.members; cmpOtherFormation.RemoveMembers(otherMembers); this.AddMembers(otherMembers); Engine.DestroyEntity(this.twinFormations[i]); this.twinFormations.splice(i,1); } // Switch between column and box if necessary var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = walkingDistance > g_ColumnDistanceThreshold; if (columnar != this.columnar) { this.offsets = undefined; this.columnar = columnar; this.MoveMembersIntoFormation(false, true); // (disable moveCenter so we can't get stuck in a loop of switching // shape causing center to change causing shape to switch back) } }; Formation.prototype.OnGlobalOwnershipChanged = function(msg) { // When an entity is captured or destroyed, it should no longer be // controlled by this formation if (this.members.indexOf(msg.entity) != -1) this.RemoveMembers([msg.entity]); }; Formation.prototype.OnGlobalEntityRenamed = function(msg) { if (this.members.indexOf(msg.entity) != -1) { this.offsets = undefined; var cmpNewUnitAI = Engine.QueryInterface(msg.newentity, IID_UnitAI); if (cmpNewUnitAI) { this.members[this.members.indexOf(msg.entity)] = msg.newentity; this.memberPositions[msg.newentity] = this.memberPositions[msg.entity]; } var cmpOldUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI); cmpOldUnitAI.SetFormationController(INVALID_ENTITY); if (cmpNewUnitAI) cmpNewUnitAI.SetFormationController(this.entity); // Because the renamed entity might have different characteristics, // (e.g. packed vs. unpacked siege), we need to recompute motion parameters this.ComputeMotionParameters(); } }; Formation.prototype.RegisterTwinFormation = function(entity) { var cmpFormation = Engine.QueryInterface(entity, IID_Formation); if (!cmpFormation) return; this.twinFormations.push(entity); cmpFormation.twinFormations.push(this.entity); }; Formation.prototype.DeleteTwinFormations = function() { for (var ent of this.twinFormations) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1); } this.twinFormations = []; }; Formation.prototype.LoadFormation = function(newTemplate) { // get the old formation info var members = this.members.slice(); var cmpThisUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var orders = cmpThisUnitAI.GetOrders().slice(); this.Disband(); var newFormation = Engine.AddEntity(newTemplate); // Apply the info from the old formation to the new one let cmpNewOwnership = Engine.QueryInterface(newFormation, IID_Ownership); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpNewOwnership) cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); var cmpNewPosition = Engine.QueryInterface(newFormation, IID_Position); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld() && cmpNewPosition) cmpNewPosition.TurnTo(cmpPosition.GetRotation().y); var cmpFormation = Engine.QueryInterface(newFormation, IID_Formation); var cmpNewUnitAI = Engine.QueryInterface(newFormation, IID_UnitAI); cmpFormation.SetMembers(members); if (orders.length) cmpNewUnitAI.AddOrders(orders); else cmpNewUnitAI.MoveIntoFormation(); Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": newFormation }); }; Engine.RegisterComponentType(IID_Formation, "Formation", Formation); Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 22567) +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 22568) @@ -1,1515 +1,1465 @@ /* Copyright (C) 2019 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 "ICmpUnitMotion.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializeTemplates.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #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, * Min/Max range to restrict short path queries to. (Larger ranges are (much) slower, * smaller ranges might miss some legitimate routes around large obstacles.) */ static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2; static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*6); static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); /** * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint. */ static const entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); /** * 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); /** * If we are this close to our target entity/point, then think about heading * for it in a straight line instead of pathfinding. */ static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** * To avoid recomputing paths too often, have some leeway for target range checks * based on our distance to the target. Increase that incertainty by one navcell * for every this many tiles of distance. */ static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); /** * When we fail more than this many path computations in a row, inform other components that the move will fail. * Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units. * However, too high means units will look idle for a long time when they are failing to move. * TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages, * this could probably be lowered. * TODO: when unit pushing is implemented, this number can probably be lowered. */ static const u8 MAX_FAILED_PATH_COMPUTATIONS = 15; /** * If we have failed path computations this many times and ComputePathToGoal is called, * always run a long-path, to avoid getting stuck sometimes (see D1424). */ static const u8 MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH = 3; static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); class CCmpUnitMotion : public ICmpUnitMotion { public: static void ClassInit(CComponentManager& componentManager) { 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); } DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) bool m_DebugOverlayEnabled; std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; // Template state: bool m_FormationController; fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier; pass_class_t m_PassClass; std::string m_PassClassName; // Dynamic state: entity_pos_t m_Clearance; // cached for efficiency fixed m_WalkSpeed, m_RunMultiplier; bool m_FacePointAfterMove; // Number of path computations that failed (in a row). // When this gets above MAX_FAILED_PATH_COMPUTATIONS, inform other components // that the move will likely fail. u8 m_FailedPathComputations = 0; // If true, PathingUpdateNeeded returns false always. // This is an optimisation against unreachable goals, where otherwise we would always // be recomputing a path. bool m_PretendLongPathIsCorrect = false; struct Ticket { u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none enum Type { SHORT_PATH, LONG_PATH } m_Type = SHORT_PATH; // Pick some default value to avoid UB. void clear() { m_Ticket = 0; } } m_ExpectedPathTicket; struct MoveRequest { enum Type { NONE, POINT, ENTITY, OFFSET } m_Type = NONE; entity_id_t m_Entity = INVALID_ENTITY; CFixedVector2D m_Position; entity_pos_t m_MinRange, m_MaxRange; // For readability CFixedVector2D GetOffset() const { return m_Position; }; MoveRequest() = default; MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {}; } m_MoveRequest; // If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier. fixed m_SpeedMultiplier; // This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience. fixed m_Speed; // 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; static std::string GetSchema() { return "Provides the unit with the ability to move around the world by itself." "" "7.0" "default" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_FacePointAfterMove = true; m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_SpeedMultiplier = fixed::FromInt(1); m_CurSpeed = fixed::Zero(); m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1); if (paramNode.GetChild("RunMultiplier").IsOk()) m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { m_PassClassName = paramNode.GetChild("PassabilityClass").ToUTF8(); m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); m_Clearance = cmpPathfinder->GetClearance(m_PassClass); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetUnitClearance(m_Clearance); } m_DebugOverlayEnabled = false; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.StringASCII("pass class", m_PassClassName, 0, 64); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket); SerializeU8_Enum()(serialize, "ticket type", m_ExpectedPathTicket.m_Type); serialize.NumberU8("failed path computations", m_FailedPathComputations, 0, 255); serialize.Bool("pretendLongPathIsCorrect", m_PretendLongPathIsCorrect); SerializeU8_Enum()(serialize, "target type", m_MoveRequest.m_Type); serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity); serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X); serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y); serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier); serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.Bool("facePointAfterMove", m_FacePointAfterMove); SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); } 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); } break; } case MT_RenderSubmit: { PROFILE("UnitMotion::RenderSubmit"); const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_PathResult: { const CMessagePathResult& msgData = static_cast (msg); PathResult(msgData.ticket, msgData.path); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"UnitMotion") break; FALLTHROUGH; } case MT_OwnershipChanged: case MT_Deserialized: { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId()); // For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier. // For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed // (in case then new m_RunMultiplier value is lower than the old). SetSpeedMultiplier(m_SpeedMultiplier); break; } } } void UpdateMessageSubscriptions() { bool needRender = m_DebugOverlayEnabled; GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() const + virtual bool IsMoveRequested() const { return m_MoveRequest.m_Type != MoveRequest::NONE; } virtual fixed GetSpeedMultiplier() const { return m_SpeedMultiplier; } virtual void SetSpeedMultiplier(fixed multiplier) { m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier); m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed()); } virtual fixed GetSpeed() const { return m_Speed; } virtual fixed GetWalkSpeed() const { return m_WalkSpeed; } virtual fixed GetRunMultiplier() const { return m_RunMultiplier; } virtual pass_class_t GetPassabilityClass() const { return m_PassClass; } virtual std::string GetPassabilityClassName() const { return m_PassClassName; } virtual void SetPassabilityClassName(const std::string& passClassName) { m_PassClassName = passClassName; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } virtual fixed GetCurrentSpeed() const { return m_CurSpeed; } virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; UpdateMessageSubscriptions(); } - 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 bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) + { + return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange)); + } + + virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) + { + return MoveTo(MoveRequest(target, minRange, maxRange)); + } + + virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) + { + MoveTo(MoveRequest(target, CFixedVector2D(x, z))); + } virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); + /** + * Clears the current MoveRequest - the unit will stop and no longer try and move. + * This should never be called from UnitMotion, since MoveToX orders are given + * by other components - these components should also decide when to stop. + */ virtual void StopMoving() { if (m_FacePointAfterMove) { CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition && cmpPosition->IsInWorld()) { CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y); } } m_MoveRequest = MoveRequest(); m_ExpectedPathTicket.clear(); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); } virtual entity_pos_t GetUnitClearance() const { return m_Clearance; } private: bool ShouldAvoidMovingUnits() const { return !m_FormationController; } bool IsFormationMember() const { // TODO: this really shouldn't be what we are checking for. return m_MoveRequest.m_Type == MoveRequest::OFFSET; } entity_id_t GetGroup() const { return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId(); } + /** + * Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target) + * This should only be called before the actual movement in a given turn, or units might both move and try to do things + * on the same turn, leading to gliding units. + */ void MoveFailed() { CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } + /** + * Warns other components that our current movement is likely over (i.e. we probably reached our destination) + * This should only be called before the actual movement in a given turn, or units might both move and try to do things + * on the same turn, leading to gliding units. + */ void MoveSucceeded() { CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** - * Update other components on our speed. - * This doesn't use messages for efficiency. - * This should only be called when speed changes. - */ - void UpdateMovementState(entity_pos_t speed) - { - CmpPtr cmpObstruction(GetEntityHandle()); - CmpPtr cmpVisual(GetEntityHandle()); - // Moved last turn, didn't this turn. - if (speed == fixed::Zero() && m_CurSpeed > fixed::Zero()) - { - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - if (cmpVisual) - cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1)); - } - // Moved this turn, didn't last turn - else if (speed > fixed::Zero() && m_CurSpeed == fixed::Zero()) - { - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - if (cmpVisual) - cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); - } - // Speed change, update the visual actor if necessary. - else if (speed != m_CurSpeed && cmpVisual) - cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); - - m_CurSpeed = speed; - } - - /** - * Increment the number of failed path and notify other components if required. + * Increment the number of failed path computations and notify other components if required. */ void IncrementFailedPathComputationAndMaybeNotify() { m_FailedPathComputations++; if (m_FailedPathComputations >= MAX_FAILED_PATH_COMPUTATIONS) { MoveFailed(); m_FailedPathComputations = 0; } } /** * If path would take us farther away from the goal than pos currently is, return false, else return true. */ bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const; /** * Handle the result of an asynchronous path query. */ void PathResult(u32 ticket, const WaypointPath& path); /** * Do the per-turn movement and other updates. */ void Move(fixed dt); /** * Returns true if we are possibly at our destination. + * Since the concept of being at destination is dependent on why the move was requested, + * UnitMotion can only ever hint about this, hence the conditional tone. */ bool PossiblyAtDestination() const; /** * Process the move the unit will do this turn. * This does not send actually change the position. * @returns true if the move was obstructed. */ bool PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const; /** - * React if our move was obstructed. - * @returns true if the obstruction required handling, false otherwise. + * Update other components on our speed. + * (For performance, this should try to avoid sending messages). */ - bool HandleObstructedMove(); + void UpdateMovementState(entity_pos_t speed); /** - * Decide whether to approximate the given range from a square target as a circle, - * rather than as a square. + * React if our move was obstructed. + * @returns true if the obstruction required handling, false otherwise. */ - bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; + bool HandleObstructedMove(); /** * Returns true if the target position is valid. False otherwise. * (this may indicate that the target is e.g. out of the world/dead). * NB: for code-writing convenience, if we have no target, this returns true. */ bool TargetHasValidPosition(const MoveRequest& moveRequest) const; bool TargetHasValidPosition() const { return TargetHasValidPosition(m_MoveRequest); } /** * 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 MoveRequest& moveRequest) const; bool ComputeTargetPosition(CFixedVector2D& out) const { return ComputeTargetPosition(out, m_MoveRequest); } /** * Attempts to replace the current path with a straight line to the target, * if it's close enough and the route is not obstructed. */ bool TryGoingStraightToTarget(const CFixedVector2D& from); /** * Returns whether our we need to recompute a path to reach our target. */ bool PathingUpdateNeeded(const CFixedVector2D& from) const; /** - * Returns whether the length of the given path, plus the distance from - * 'from' to the first waypoints, it shorter than minDistance. - */ - bool PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const; - - /** * Rotate to face towards the target point, given the current pos */ void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z); /** * Returns an appropriate obstruction filter for use with path requests. */ ControlGroupMovementObstructionFilter GetObstructionFilter() const; /** + * 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; + + /** * Create a PathGoal from a move request. * @returns true if the goal was successfully created. */ bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) 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. + * Compute a path to the given goal from the given position. + * Might go in a straight line immediately, or might start an asynchronous path request. */ - void BeginPathing(const CFixedVector2D& from, const PathGoal& goal); + void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous long path query. */ void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous short path query. */ void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); /** + * General handler for MoveTo interface functions. + */ + bool MoveTo(MoveRequest request); + + /** * Convert a path into a renderable list of lines */ void RenderPath(const WaypointPath& path, std::vector& lines, CColor color); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(UnitMotion) bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const { if (path.m_Waypoints.empty()) return false; // Reject the new path if it does not lead us closer to the target's position. if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z))) return true; return false; } void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE) return; Ticket::Type ticketType = m_ExpectedPathTicket.m_Type; m_ExpectedPathTicket.clear(); // Check that we are still able to do something with that path CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) { // We will probably fail to move so inform components but keep on trying anyways. MoveFailed(); return; } CFixedVector2D pos = cmpPosition->GetPosition2D(); PathGoal goal; // If we can't compute a goal, we'll fail in the next Move() call so do nothing special. if (!ComputeGoal(goal, m_MoveRequest)) return; if (ticketType == Ticket::LONG_PATH) { if (RejectFartherPaths(goal, path, pos)) { IncrementFailedPathComputationAndMaybeNotify(); return; } m_LongPath = path; m_PretendLongPathIsCorrect = false; // 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 over the next turns), so if we're stuck and the user clicks // close enough to the unit then we can probably get unstuck // NB: this relies on HandleObstructedMove requesting short paths if we still have long waypoints. if (m_LongPath.m_Waypoints.empty()) { IncrementFailedPathComputationAndMaybeNotify(); CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y }); } // If this new path won't put us in range, it's highly likely that we are going somewhere unreachable. // This means we will try to recompute the path every turn. // To avoid this, act as if our current path leads us to the correct destination. // (we will still fail the move when we arrive to the best possible position, and if we were blocked by // an obstruction and it goes away we will notice when getting there as having no waypoint goes through // HandleObstructedMove, so this is safe). // TODO: For now, we won't warn components straight away as that could lead to units idling earlier than expected, // but it should be done someday when the message can differentiate between different failure causes. else if (PathingUpdateNeeded(pos)) m_PretendLongPathIsCorrect = true; return; } // Reject new short paths if they were aiming at the goal directly (i.e. no long waypoints still exists). if (m_LongPath.m_Waypoints.empty() && RejectFartherPaths(goal, path, pos)) { IncrementFailedPathComputationAndMaybeNotify(); return; } m_ShortPath = path; if (!m_ShortPath.m_Waypoints.empty()) return; if (m_FailedPathComputations >= 1) { // Inform other components - we might be ordered to stop, and computeGoal will then fail and return early. CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } // Don't notify if we are a formation member - we can occasionally be stuck for a long time // if our current offset is unreachable. if (!IsFormationMember()) IncrementFailedPathComputationAndMaybeNotify(); // If there's no waypoints then we couldn't get near the target // If we're globally following a long path, try to remove the next waypoint, // it might be obstructed (e.g. by idle entities which the long-range pathfinder doesn't see). if (!m_LongPath.m_Waypoints.empty()) { m_LongPath.m_Waypoints.pop_back(); if (!m_LongPath.m_Waypoints.empty()) { // Get close enough - this will likely help the short path efficiency, and if we end up taking a wrong way // we'll easily be able to revert it using a long path. PathGoal goal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, SHORT_PATH_LONG_WAYPOINT_RANGE }; RequestShortPath(pos, goal, true); return; } } - BeginPathing(pos, goal); + ComputePathToGoal(pos, goal); } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); // If we were idle and will still be, we can return. // TODO: this will need to be removed if pushing is implemented. if (m_CurSpeed == fixed::Zero() && m_MoveRequest.m_Type == MoveRequest::NONE) return; if (PossiblyAtDestination()) MoveSucceeded(); else if (!TargetHasValidPosition()) { // Scrap waypoints - we don't know where to go. // If the move request remains unchanged and the target again has a valid position later on, // moving will be resumed. // Units may want to move to move to the target's last known position, // but that should be decided by UnitAI (handling MoveFailed), not UnitMotion. m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); MoveFailed(); } CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D initialPos = cmpPosition->GetPosition2D(); // Keep track of the current unit's position during the update CFixedVector2D pos = initialPos; // 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 bool wentStraight = TryGoingStraightToTarget(initialPos); bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos); // Update our speed over this turn so that the visual actor shows the correct animation. if (pos == initialPos) UpdateMovementState(fixed::Zero()); else { // Update the Position component after our movement (if we actually moved anywhere) CFixedVector2D offset = pos - initialPos; // Face towards the target entity_angle_t angle = atan2_approx(offset.X, offset.Y); cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); // Calculate the mean speed over this past turn. UpdateMovementState(offset.Length() / dt); } if (wasObstructed && HandleObstructedMove()) return; else if (!wasObstructed) m_FailedPathComputations = 0; // We may need to recompute our path sometimes (e.g. if our target moves). // Since we request paths asynchronously anyways, this does not need to be done before moving. if (!wentStraight && PathingUpdateNeeded(pos)) { PathGoal goal; if (ComputeGoal(goal, m_MoveRequest)) - BeginPathing(pos, goal); + ComputePathToGoal(pos, goal); } } bool CCmpUnitMotion::PossiblyAtDestination() const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); if (m_MoveRequest.m_Type == MoveRequest::POINT) return cmpObstructionManager->IsInPointRange(GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::ENTITY) return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::OFFSET) { CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); - if (cmpControllerMotion && cmpControllerMotion->IsMoving()) + if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested()) return false; CFixedVector2D targetPos; ComputeTargetPosition(targetPos); CmpPtr cmpPosition(GetEntityHandle()); return cmpObstructionManager->IsInPointRange(GetEntityId(), targetPos.X, targetPos.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); } return false; } bool CCmpUnitMotion::PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) return true; // 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. CmpPtr cmpPathfinder(GetSystemEntity()); ENSURE(cmpPathfinder); fixed basicSpeed = m_Speed; // If in formation, run to keep up; otherwise just walk if (IsFormationMember()) basicSpeed = m_Speed.Multiply(m_RunMultiplier); // 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); fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint fixed timeLeft = dt; fixed zero = fixed::Zero(); while (timeLeft > zero) { // If we ran out of path, we have to stop if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) break; CFixedVector2D target; if (shortPath.m_Waypoints.empty()) target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); else target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); CFixedVector2D offset = target - pos; // Work out how far we can travel in timeLeft fixed maxdist = maxSpeed.Multiply(timeLeft); // 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; // Spend the rest of the time heading towards the next waypoint timeLeft = (maxdist - offsetLength) / maxSpeed; if (shortPath.m_Waypoints.empty()) longPath.m_Waypoints.pop_back(); else shortPath.m_Waypoints.pop_back(); continue; } else { // Error - path was obstructed return true; } } else { // Not close enough, so just move in the right direction offset.Normalize(maxdist); target = pos + offset; if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) pos = target; else return true; break; } } return false; } +void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed) +{ + CmpPtr cmpObstruction(GetEntityHandle()); + CmpPtr cmpVisual(GetEntityHandle()); + // Moved last turn, didn't this turn. + if (speed == fixed::Zero() && m_CurSpeed > fixed::Zero()) + { + if (cmpObstruction) + cmpObstruction->SetMovingFlag(false); + if (cmpVisual) + cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1)); + } + // Moved this turn, didn't last turn + else if (speed > fixed::Zero() && m_CurSpeed == fixed::Zero()) + { + if (cmpObstruction) + cmpObstruction->SetMovingFlag(true); + if (cmpVisual) + cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); + } + // Speed change, update the visual actor if necessary. + else if (speed != m_CurSpeed && cmpVisual) + cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); + + m_CurSpeed = speed; +} + bool CCmpUnitMotion::HandleObstructedMove() { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; if (m_FailedPathComputations >= 1) { // Inform other components - we might be ordered to stop, and computeGoal will then fail and return early. CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } CFixedVector2D pos = cmpPosition->GetPosition2D(); // Oops, we hit something (very likely another unit). PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; // If close enough, just compute a short path to the goal if (goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST) { m_LongPath.m_Waypoints.clear(); RequestShortPath(pos, goal, true); return true; } // If we still have long waypoints, try and compute a short path. // Assume the next waypoint is impassable if (m_LongPath.m_Waypoints.size() > 1) m_LongPath.m_Waypoints.pop_back(); if (!m_LongPath.m_Waypoints.empty()) { // Get close enough - this will likely help the short path efficiency, and if we end up taking a wrong way // we'll easily be able to revert it using a long path. PathGoal goal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, SHORT_PATH_LONG_WAYPOINT_RANGE }; RequestShortPath(pos, goal, true); return true; } // Else, just entirely recompute - BeginPathing(pos, goal); + ComputePathToGoal(pos, goal); // potential TODO: We could switch the short-range pathfinder for something else entirely. return true; } bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const { if (moveRequest.m_Type != MoveRequest::ENTITY) return true; CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity); return cmpPosition && cmpPosition->IsInWorld(); } bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::POINT) { out = moveRequest.m_Position; return true; } CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; if (moveRequest.m_Type == MoveRequest::OFFSET) { // There is an offset, so compute it relative to orientation entity_angle_t angle = cmpPosition->GetRotation().Y; CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle); out = cmpPosition->GetPosition2D() + offset; } else { out = cmpPosition->GetPosition2D(); // If the target is moving, we might never get in range if we just try to reach its current position, // so we have to try and move to a position where we will be in-range, including their movement. // Since we request paths asynchronously a the end of our turn, we need to account for twice the movement speed. // TODO: be cleverer about this. It fixes fleeing nicely currently, but orthogonal movement should be considered, // and the overall logic could be improved upon. CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity); - if (cmpUnitMotion && cmpUnitMotion->IsMoving()) + if (cmpUnitMotion && cmpUnitMotion->IsMoveRequested()) out += (out - cmpPosition->GetPreviousPosition2D()) * 2; } return true; } bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from) { CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; // Fail if the target is too far away if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) return false; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return false; // Move the goal to match the target entity's new position PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; 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) // Find the point on the goal shape that we should head towards CFixedVector2D goalPos = goal.NearestPointOnGoal(from); // Check if there's any collisions on that route. // For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities. ICmpObstructionManager::tag_t specificIgnore; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) specificIgnore = cmpTargetObstruction->GetObstruction(); } if (specificIgnore.valid()) { if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; } else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; // That route is okay, so update our path m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); return true; } bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; if (m_PretendLongPathIsCorrect) return false; if (PossiblyAtDestination()) return false; // Get the obstruction shape and translate it where we estimate the target to be. ICmpObstructionManager::ObstructionSquare estimatedTargetShape; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape); } estimatedTargetShape.x = targetPos.X; estimatedTargetShape.z = targetPos.Y; CmpPtr cmpObstruction(GetEntityHandle()); ICmpObstructionManager::ObstructionSquare shape; if (cmpObstruction) cmpObstruction->GetObstructionSquare(shape); // Translate our own obstruction shape to our last waypoint or our current position, lacking that. if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) { shape.x = from.X; shape.z = from.Y; } else { const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front(); shape.x = lastWaypoint.x; shape.z = lastWaypoint.z; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); // Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example. entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length(); // When in straight-path distance, we want perfect detection. distance = std::max(distance - DIRECT_PATH_RANGE, entity_pos_t::Zero()); // TODO: it could be worth computing this based on time to collision instead of linear distance. entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero()); entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange : m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER; if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false)) return false; return true; } -bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const -{ - CFixedVector2D prev = from; - entity_pos_t distLeft = minDistance; - - for (ssize_t i = (ssize_t)path.m_Waypoints.size()-1; i >= 0; --i) - { - // Check if the next path segment is longer than the requested minimum - CFixedVector2D waypoint(path.m_Waypoints[i].x, path.m_Waypoints[i].z); - CFixedVector2D delta = waypoint - prev; - if (delta.CompareLength(distLeft) > 0) - return false; - - // Still short enough - prepare to check the next segment - distLeft -= delta.Length(); - prev = waypoint; - } - - // Reached the end of the path before exceeding minDistance - return true; -} - void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); FaceTowardsPointFromPos(pos, x, z); } void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z) { CFixedVector2D target(x, z); CFixedVector2D offset = target - pos; if (!offset.IsZero()) { entity_angle_t angle = atan2_approx(offset.X, offset.Y); CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return; cmpPosition->TurnTo(angle); } } ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter() const { return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup()); } // The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range. // Depending on what the best approximation is, we either pretend the target is a circle or a square. // One needs to be careful that the approximated geometry will be in the range. bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const { // 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); } bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CFixedVector2D targetPosition; if (!ComputeTargetPosition(targetPosition, moveRequest)) return false; ICmpObstructionManager::ObstructionSquare targetObstruction; if (moveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(targetObstruction); } targetObstruction.x = targetPosition.X; targetObstruction.z = targetPosition.Y; ICmpObstructionManager::ObstructionSquare obstruction; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->GetObstructionSquare(obstruction); else { obstruction.x = pos.X; obstruction.z = pos.Y; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction); out.x = targetObstruction.x; out.z = targetObstruction.z; out.hw = targetObstruction.hw; out.hh = targetObstruction.hh; out.u = targetObstruction.u; out.v = targetObstruction.v; if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() || targetObstruction.hw > fixed::Zero()) out.type = PathGoal::SQUARE; else { out.type = PathGoal::POINT; return true; } entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length(); // TODO: because we cannot move to rounded rectangles, we have to make conservative approximations. // This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range) // When going outside of the min-range or inside the max-range, the unit will still go through the correct range // but if it moves fast enough, this might not be picked up by PossiblyAtDestination(). // Fixing this involves moving to rounded rectangles, or checking more often in PerformMove(). // In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where // min-range is not 0 and max-range is not infinity. if (distance < moveRequest.m_MinRange) { // Distance checks are nearest edge to nearest edge, so we need to account for our clearance // and we must make sure diagonals also fit so multiply by slightly more than sqrt(2) entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2; if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius)) { // We are safely away from the obstruction itself if we are away from the circumscribing circle out.type = PathGoal::INVERTED_CIRCLE; out.hw = circleRadius + goalDistance; } else { out.type = PathGoal::INVERTED_SQUARE; out.hw = targetObstruction.hw + goalDistance; out.hh = targetObstruction.hh + goalDistance; } } else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange) { if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius)) { entity_pos_t goalDistance = moveRequest.m_MaxRange; // We must go in-range of the inscribed circle, not the circumscribing circle. circleRadius = std::min(targetObstruction.hw, targetObstruction.hh); out.type = PathGoal::CIRCLE; out.hw = circleRadius + goalDistance; } else { // The target is large relative to our range, so treat it as a square and // get close enough that the diagonals come within range entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2) out.type = PathGoal::SQUARE; 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 out.hw = targetObstruction.hw + delta; out.hh = targetObstruction.hh + delta; } } // Do nothing in particular in case we are already in range. return true; } -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) +void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal) { - m_ExpectedPathTicket.clear(); - #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 }); return; } #endif // If the target is 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 (m_FailedPathComputations != MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH && TryGoingStraightToTarget(from)) return; // 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. // To avoid getting stuck because the short-range pathfinder is bounded, occasionally compute a long path instead. if (m_FailedPathComputations != MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH && goal.DistanceToPoint(from) < LONG_PATH_MIN_DIST) { m_LongPath.m_Waypoints.clear(); RequestShortPath(from, goal, true); } else RequestLongPath(from, goal); } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; // 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); cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH; m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); } void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * m_FailedPathComputations; if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) searchRange = SHORT_PATH_MAX_SEARCH_RANGE; m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH; m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); } -bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) +bool CCmpUnitMotion::MoveTo(MoveRequest request) { - PROFILE("MoveToPointRange"); - - MoveRequest moveRequest(CFixedVector2D(x, z), minRange, maxRange); - - PathGoal goal; - if (!ComputeGoal(goal, moveRequest)) - return false; + PROFILE("MoveTo"); CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; - m_MoveRequest = moveRequest; - m_FailedPathComputations = 0; - m_PretendLongPathIsCorrect = false; - - BeginPathing(cmpPosition->GetPosition2D(), goal); - - return true; -} - -bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) -{ - PROFILE("MoveToTargetRange"); - - MoveRequest moveRequest(target, minRange, maxRange); - PathGoal goal; - - if (!ComputeGoal(goal, moveRequest)) - return false; - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + if (!ComputeGoal(goal, request)) return false; - m_MoveRequest = moveRequest; + m_MoveRequest = request; m_FailedPathComputations = 0; m_PretendLongPathIsCorrect = false; - BeginPathing(cmpPosition->GetPosition2D(), goal); - + ComputePathToGoal(cmpPosition->GetPosition2D(), goal); return true; } -void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) -{ - MoveRequest moveRequest(target, CFixedVector2D(x, z)); - - PathGoal goal; - if (!ComputeGoal(goal, moveRequest)) - return; - - CmpPtr cmpPosition(GetSimContext(), target); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - m_MoveRequest = moveRequest; - m_FailedPathComputations = 0; - m_PretendLongPathIsCorrect = false; - - BeginPathing(cmpPosition->GetPosition2D(), goal); -} - void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { bool floating = false; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) floating = cmpPosition->CanFloat(); lines.clear(); std::vector waypointCoords; for (size_t i = 0; i < path.m_Waypoints.size(); ++i) { float x = path.m_Waypoints[i].x.ToFloat(); float z = path.m_Waypoints[i].z.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } float x = cmpPosition->GetPosition2D().X.ToFloat(); float z = cmpPosition->GetPosition2D().Y.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating); } void CCmpUnitMotion::RenderSubmit(SceneCollector& collector) { 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]); for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) collector.Submit(&m_DebugOverlayShortPathLines[i]); } Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 22567) +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 22568) @@ -1,135 +1,135 @@ /* Copyright (C) 2019 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 "ICmpUnitMotion.h" #include "simulation2/system/InterfaceScripted.h" #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_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_CONST_0("IsMoving", bool, ICmpUnitMotion, IsMoving) +DEFINE_INTERFACE_METHOD_CONST_0("IsMoveRequested", bool, ICmpUnitMotion, IsMoveRequested) DEFINE_INTERFACE_METHOD_CONST_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetRunMultiplier", fixed, ICmpUnitMotion, GetRunMultiplier) DEFINE_INTERFACE_METHOD_1("SetSpeedMultiplier", void, ICmpUnitMotion, SetSpeedMultiplier, fixed) 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) { return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange); } virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); } virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { m_Script.CallVoid("MoveToFormationOffset", target, x, z); } virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { m_Script.CallVoid("FaceTowardsPoint", x, z); } virtual void StopMoving() { m_Script.CallVoid("StopMoving"); } virtual fixed GetCurrentSpeed() const { return m_Script.Call("GetCurrentSpeed"); } - virtual bool IsMoving() const + virtual bool IsMoveRequested() const { - return m_Script.Call("IsMoving"); + return m_Script.Call("IsMoveRequested"); } virtual fixed GetSpeed() const { return m_Script.Call("GetSpeed"); } virtual fixed GetWalkSpeed() const { return m_Script.Call("GetWalkSpeed"); } virtual fixed GetRunMultiplier() const { return m_Script.Call("GetRunMultiplier"); } virtual void SetSpeedMultiplier(fixed multiplier) { m_Script.CallVoid("SetSpeedMultiplier", multiplier); } virtual fixed GetSpeedMultiplier() const { return m_Script.Call("GetSpeedMultiplier"); } virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove); } virtual pass_class_t GetPassabilityClass() const { return m_Script.Call("GetPassabilityClass"); } virtual std::string GetPassabilityClassName() const { return m_Script.Call("GetPassabilityClassName"); } virtual entity_pos_t GetUnitClearance() const { return m_Script.Call("GetUnitClearance"); } virtual void SetDebugOverlay(bool enabled) { m_Script.CallVoid("SetDebugOverlay", enabled); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(UnitMotionScripted) Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 22567) +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 22568) @@ -1,138 +1,138 @@ /* Copyright (C) 2019 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_ICMPUNITMOTION #define INCLUDED_ICMPUNITMOTION #include "simulation2/system/Interface.h" #include "simulation2/components/ICmpPathfinder.h" // for pass_class_t #include "simulation2/components/ICmpPosition.h" // for entity_pos_t /** * 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. */ 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 cannot move anywhere at all, or if there is some other error, then returns false. * Otherwise, returns true. * If maxRange is negative, then the maximum range is treated as infinity. */ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 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 cannot move anywhere at all, or if there is some other error, then returns false. * Otherwise, returns true. * If maxRange is negative, then the maximum range is treated as infinity. */ virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** * Join a formation, and move towards a given offset relative to the formation controller entity. * Continues following the formation until given a different command. */ virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; /** * Turn to look towards the given point. */ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** * Stop moving immediately. */ virtual void StopMoving() = 0; /** * Get the distance travelled over the last turn. */ virtual fixed GetCurrentSpeed() const = 0; /** * @returns true if the unit has a destination. */ - virtual bool IsMoving() const = 0; + virtual bool IsMoveRequested() const = 0; /** * Get the unit template walk speed after modifications. */ virtual fixed GetWalkSpeed() const = 0; /** * Get the unit template running (i.e. max) speed after modifications. */ virtual fixed GetRunMultiplier() const = 0; /** * Returns the ratio of GetSpeed() / GetWalkSpeed(). */ virtual fixed GetSpeedMultiplier() const = 0; /** * Set the current movement speed. * @param speed A multiplier of GetWalkSpeed(). */ virtual void SetSpeedMultiplier(fixed multiplier) = 0; /** * Get the speed at which the unit intends to move. * (regardless of whether the unit is moving or not right now). */ virtual fixed GetSpeed() 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. */ virtual pass_class_t GetPassabilityClass() const = 0; /** * Get the passability class name (as defined in pathfinder.xml) */ virtual std::string GetPassabilityClassName() const = 0; /** * Get the unit clearance (used by the Obstruction component) */ virtual entity_pos_t GetUnitClearance() const = 0; /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; DECLARE_INTERFACE_TYPE(UnitMotion) }; #endif // INCLUDED_ICMPUNITMOTION