mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-14 06:55:36 +00:00
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:
@@ -29,6 +29,7 @@
|
|||||||
{ "nick": "Alan", "name": "Alan Kemp" },
|
{ "nick": "Alan", "name": "Alan Kemp" },
|
||||||
{ "nick": "Alex", "name": "Alexander Yakobovich" },
|
{ "nick": "Alex", "name": "Alexander Yakobovich" },
|
||||||
{ "nick": "alpha123", "name": "Peter P. Cannici" },
|
{ "nick": "alpha123", "name": "Peter P. Cannici" },
|
||||||
|
{ "nick": "alre" },
|
||||||
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
|
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
|
||||||
{ "name": "André Puel" },
|
{ "name": "André Puel" },
|
||||||
{ "nick": "andy5995", "name": "Andy Alt" },
|
{ "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.'>" +
|
"<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
|
||||||
"<text/>" +
|
"<text/>" +
|
||||||
"</element>" +
|
"</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.'>" +
|
"<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" +
|
||||||
"<text/>" +
|
"<text/>" +
|
||||||
"</element>" +
|
"</element>" +
|
||||||
@@ -67,6 +70,9 @@ Formation.prototype.Schema =
|
|||||||
// Distance at which we'll switch between column/box formations.
|
// Distance at which we'll switch between column/box formations.
|
||||||
var g_ColumnDistanceThreshold = 128;
|
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 = [
|
Formation.prototype.variablesToSerialize = [
|
||||||
"lastOrderVariant",
|
"lastOrderVariant",
|
||||||
"members",
|
"members",
|
||||||
@@ -87,6 +93,7 @@ Formation.prototype.variablesToSerialize = [
|
|||||||
|
|
||||||
Formation.prototype.Init = function(deserialized = false)
|
Formation.prototype.Init = function(deserialized = false)
|
||||||
{
|
{
|
||||||
|
this.maxTurningAngle = +this.template.MaxTurningAngle;
|
||||||
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
|
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
|
||||||
this.shiftRows = this.template.ShiftRows == "true";
|
this.shiftRows = this.template.ShiftRows == "true";
|
||||||
this.separationMultiplier = {
|
this.separationMultiplier = {
|
||||||
@@ -490,7 +497,6 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, varia
|
|||||||
|
|
||||||
let active = [];
|
let active = [];
|
||||||
let positions = [];
|
let positions = [];
|
||||||
let rotations = 0;
|
|
||||||
|
|
||||||
for (let ent of this.members)
|
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,
|
// Query the 2D position as the exact height calculation isn't needed,
|
||||||
// but bring the position to the correct coordinates.
|
// but bring the position to the correct coordinates.
|
||||||
positions.push(cmpPosition.GetPosition2D());
|
positions.push(cmpPosition.GetPosition2D());
|
||||||
rotations += cmpPosition.GetRotation().y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let avgpos = Vector2D.average(positions);
|
|
||||||
|
|
||||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
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.
|
// Reposition the formation if we're told to or if we don't already have a position.
|
||||||
if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
|
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;
|
const oldRotation = cmpPosition.GetRotation().y;
|
||||||
this.offsets = undefined;
|
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 offsetsChanged = false;
|
||||||
let newOrientation = this.GetEstimatedOrientation(avgpos);
|
|
||||||
if (!this.offsets)
|
if (!this.offsets)
|
||||||
{
|
{
|
||||||
this.offsets = this.ComputeFormationOffsets(active, positions);
|
this.offsets = this.ComputeFormationOffsets(active, positions);
|
||||||
@@ -801,16 +815,14 @@ Formation.prototype.ComputeFormationOffsets = function(active, positions)
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Query the 2D position of the formation.
|
// Query the 2D position of the formation.
|
||||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
const realPositions = this.GetRealOffsetPositions(offsets);
|
||||||
let formationPos = cmpPosition.GetPosition2D();
|
|
||||||
|
|
||||||
// Use realistic place assignment,
|
// Use realistic place assignment,
|
||||||
// every soldier searches the closest available place in the formation.
|
// every soldier searches the closest available place in the formation.
|
||||||
let newOffsets = [];
|
let newOffsets = [];
|
||||||
let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
|
for (const i of sortingClasses.reverse())
|
||||||
for (let i = sortingClasses.length; i; --i)
|
|
||||||
{
|
{
|
||||||
let t = types[sortingClasses[i - 1]];
|
const t = types[i];
|
||||||
if (!t.length)
|
if (!t.length)
|
||||||
continue;
|
continue;
|
||||||
let usedOffsets = offsets.splice(-t.length);
|
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.
|
* 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 offsetPositions = [];
|
||||||
let { sin, cos } = this.GetEstimatedOrientation(pos);
|
|
||||||
// Calculate the world positions.
|
// Calculate the world positions.
|
||||||
for (let o of offsets)
|
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));
|
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.
|
* Returns true if the two given angles (in radians)
|
||||||
* Return the sine and cosine of the angle.
|
* 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 = {};
|
const d = Math.abs(a1 - a2) % 2 * Math.PI;
|
||||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle;
|
||||||
if (!cmpPosition)
|
|
||||||
return r;
|
|
||||||
let rot = cmpPosition.GetRotation().y;
|
|
||||||
r.sin = Math.sin(rot);
|
|
||||||
r.cos = Math.cos(rot);
|
|
||||||
return r;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -887,7 +899,6 @@ Formation.prototype.GetEstimatedOrientation = function(pos)
|
|||||||
*/
|
*/
|
||||||
Formation.prototype.ComputeMotionParameters = function()
|
Formation.prototype.ComputeMotionParameters = function()
|
||||||
{
|
{
|
||||||
let maxRadius = 0;
|
|
||||||
let minSpeed = Infinity;
|
let minSpeed = Infinity;
|
||||||
let minAcceleration = Infinity;
|
let minAcceleration = Infinity;
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ function TestFormationExiting(mode)
|
|||||||
|
|
||||||
AddMock(controller, IID_Position, {
|
AddMock(controller, IID_Position, {
|
||||||
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
||||||
|
"TurnTo": function() {},
|
||||||
"GetTurretParent": function() { return INVALID_ENTITY; },
|
"GetTurretParent": function() { return INVALID_ENTITY; },
|
||||||
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
|
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
|
||||||
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
|
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
|
||||||
@@ -407,6 +408,7 @@ function TestMoveIntoFormationWhileAttacking()
|
|||||||
AddMock(controller, IID_Position, {
|
AddMock(controller, IID_Position, {
|
||||||
"GetTurretParent": () => INVALID_ENTITY,
|
"GetTurretParent": () => INVALID_ENTITY,
|
||||||
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
"JumpTo": function(x, z) { this.x = x; this.z = z; },
|
||||||
|
"TurnTo": function() {},
|
||||||
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
|
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
|
||||||
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
|
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
|
||||||
"GetRotation": () => ({ "y": 0 }),
|
"GetRotation": () => ({ "y": 0 }),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<DisabledTooltip>Requires at least 2 Soldiers or Siege Engines.</DisabledTooltip>
|
<DisabledTooltip>Requires at least 2 Soldiers or Siege Engines.</DisabledTooltip>
|
||||||
<SpeedMultiplier>1</SpeedMultiplier>
|
<SpeedMultiplier>1</SpeedMultiplier>
|
||||||
<FormationShape>square</FormationShape>
|
<FormationShape>square</FormationShape>
|
||||||
|
<MaxTurningAngle>0.785</MaxTurningAngle>
|
||||||
<SortingClasses>Hero Champion Cavalry Melee Ranged</SortingClasses>
|
<SortingClasses>Hero Champion Cavalry Melee Ranged</SortingClasses>
|
||||||
<ShiftRows>false</ShiftRows>
|
<ShiftRows>false</ShiftRows>
|
||||||
<UnitSeparationWidthMultiplier>1</UnitSeparationWidthMultiplier>
|
<UnitSeparationWidthMultiplier>1</UnitSeparationWidthMultiplier>
|
||||||
|
|||||||
Reference in New Issue
Block a user