1
0
forked from mirrors/0ad

smoother formation manouvres

Implement a threshold between "turning" the formation and "rearranging"
it.

Patch By: marder
Based on Patch By: alre
Comments By: Freagarach, Stan, Silier
Glanced good By: Freagarach
Differential Revision: D4294
Forum Topic:
https://wildfiregames.com/forum/topic/44848-proposals-for-formations/#comment-452917

This was SVN commit r26520.
This commit is contained in:
bb
2022-03-02 12:54:43 +00:00
parent 250b0f12c6
commit d9d4deb6ce
4 changed files with 49 additions and 34 deletions
@@ -29,6 +29,7 @@
{ "nick": "Alan", "name": "Alan Kemp" },
{ "nick": "Alex", "name": "Alexander Yakobovich" },
{ "nick": "alpha123", "name": "Peter P. Cannici" },
{ "nick": "alre" },
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
{ "name": "André Puel" },
{ "nick": "andy5995", "name": "Andy Alt" },
@@ -17,6 +17,9 @@ Formation.prototype.Schema =
"<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
"<text/>" +
"</element>" +
"<element name='MaxTurningAngle' a:help='The turning angle in radian under which the formation attempts to turn and over which the formation positions are recomputed.'>" +
"<text/>" +
"</element>" +
"<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" +
"<text/>" +
"</element>" +
@@ -67,6 +70,9 @@ Formation.prototype.Schema =
// Distance at which we'll switch between column/box formations.
var g_ColumnDistanceThreshold = 128;
// Distance under which the formation will not try to turn towards the target position.
var g_RotateDistanceThreshold = 1;
Formation.prototype.variablesToSerialize = [
"lastOrderVariant",
"members",
@@ -87,6 +93,7 @@ Formation.prototype.variablesToSerialize = [
Formation.prototype.Init = function(deserialized = false)
{
this.maxTurningAngle = +this.template.MaxTurningAngle;
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
this.shiftRows = this.template.ShiftRows == "true";
this.separationMultiplier = {
@@ -490,7 +497,6 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, varia
let active = [];
let positions = [];
let rotations = 0;
for (let ent of this.members)
{
@@ -502,29 +508,37 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, varia
// Query the 2D position as the exact height calculation isn't needed,
// but bring the position to the correct coordinates.
positions.push(cmpPosition.GetPosition2D());
rotations += cmpPosition.GetRotation().y;
}
let avgpos = Vector2D.average(positions);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Reposition the formation if we're told to or if we don't already have a position.
if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length);
this.lastOrderVariant = variant;
// Switch between column and box if necessary.
let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance();
let columnar = walkingDistance > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
{
this.columnar = columnar;
this.offsets = undefined;
const oldRotation = cmpPosition.GetRotation().y;
const avgpos = Vector2D.average(positions);
// Switch between column and box if necessary.
const cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
const columnar = cmpFormationUnitAI.ComputeWalkingDistance() > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
{
this.columnar = columnar;
this.offsets = undefined;
}
let newRotation = oldRotation;
const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0];
if (targetPosition !== undefined && avgpos.distanceToSquared(targetPosition) > g_RotateDistanceThreshold)
newRotation = avgpos.angleTo(targetPosition);
cmpPosition.TurnTo(newRotation);
if (!this.areAnglesSimilar(newRotation, oldRotation))
this.offsets = undefined;
}
this.lastOrderVariant = variant;
let offsetsChanged = false;
let newOrientation = this.GetEstimatedOrientation(avgpos);
if (!this.offsets)
{
this.offsets = this.ComputeFormationOffsets(active, positions);
@@ -801,16 +815,14 @@ Formation.prototype.ComputeFormationOffsets = function(active, positions)
});
// Query the 2D position of the formation.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let formationPos = cmpPosition.GetPosition2D();
const realPositions = this.GetRealOffsetPositions(offsets);
// Use realistic place assignment,
// every soldier searches the closest available place in the formation.
let newOffsets = [];
let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
for (let i = sortingClasses.length; i; --i)
for (const i of sortingClasses.reverse())
{
let t = types[sortingClasses[i - 1]];
const t = types[i];
if (!t.length)
continue;
let usedOffsets = offsets.splice(-t.length);
@@ -855,10 +867,14 @@ Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
/**
* Get the world positions for a list of offsets in this formation.
*/
Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
Formation.prototype.GetRealOffsetPositions = function(offsets)
{
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
const pos = cmpPosition.GetPosition2D();
const rot = cmpPosition.GetRotation().y;
const sin = Math.sin(rot);
const cos = Math.cos(rot);
let offsetPositions = [];
let { sin, cos } = this.GetEstimatedOrientation(pos);
// Calculate the world positions.
for (let o of offsets)
offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
@@ -867,19 +883,15 @@ Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
};
/**
* Calculate the estimated rotation of the formation based on the current rotation.
* Return the sine and cosine of the angle.
* Returns true if the two given angles (in radians)
* are smaller than the maximum turning angle of the formation and therfore allow
* the formation turn without reassigning positions.
*/
Formation.prototype.GetEstimatedOrientation = function(pos)
Formation.prototype.areAnglesSimilar = function(a1, a2)
{
let r = {};
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return r;
let rot = cmpPosition.GetRotation().y;
r.sin = Math.sin(rot);
r.cos = Math.cos(rot);
return r;
const d = Math.abs(a1 - a2) % 2 * Math.PI;
return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle;
};
/**
@@ -887,7 +899,6 @@ Formation.prototype.GetEstimatedOrientation = function(pos)
*/
Formation.prototype.ComputeMotionParameters = function()
{
let maxRadius = 0;
let minSpeed = Infinity;
let minAcceleration = Infinity;
@@ -233,6 +233,7 @@ function TestFormationExiting(mode)
AddMock(controller, IID_Position, {
"JumpTo": function(x, z) { this.x = x; this.z = z; },
"TurnTo": function() {},
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
@@ -407,6 +408,7 @@ function TestMoveIntoFormationWhileAttacking()
AddMock(controller, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"JumpTo": function(x, z) { this.x = x; this.z = z; },
"TurnTo": function() {},
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
"GetRotation": () => ({ "y": 0 }),
@@ -15,6 +15,7 @@
<DisabledTooltip>Requires at least 2 Soldiers or Siege Engines.</DisabledTooltip>
<SpeedMultiplier>1</SpeedMultiplier>
<FormationShape>square</FormationShape>
<MaxTurningAngle>0.785</MaxTurningAngle>
<SortingClasses>Hero Champion Cavalry Melee Ranged</SortingClasses>
<ShiftRows>false</ShiftRows>
<UnitSeparationWidthMultiplier>1</UnitSeparationWidthMultiplier>