Group units in clusters of separate formations when they're too far apart.

This was SVN commit r14483.
This commit is contained in:
sanderd17
2014-01-02 20:04:50 +00:00
parent 8b98e2d513
commit 8269f0cfd4
2 changed files with 242 additions and 43 deletions
@@ -16,6 +16,19 @@ Formation.prototype.Init = function()
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()
@@ -157,6 +170,39 @@ Formation.prototype.RemoveMembers = function(ents)
this.MoveMembersIntoFormation(true, true);
};
Formation.prototype.AddMembers = function(ents)
{
this.offsets = undefined;
this.inPosition = [];
for each (var ent in 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.members = this.members.concat(ents);
for each (var ent in 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.MoveMembersIntoFormation(true, true);
};
/**
* Called when the formation stops moving in order to detect
* units that have already reached their final positions.
@@ -293,6 +339,16 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force)
}
}
// 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.GetTargetOrientation(avgpos);
var dSin = Math.abs(newOrientation.sin - this.oldOrientation.sin);
var dCos = Math.abs(newOrientation.cos - this.oldOrientation.cos);
@@ -850,11 +906,45 @@ Formation.prototype.ComputeMotionParameters = function()
// TODO: we also need to do something about PassabilityClass, CostClass
};
Formation.prototype.OnUpdate_Final = function(msg)
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
this.AddMembers(cmpOtherFormation.members);
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);
// TODO do we really need to calculate the distance every turn?
var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
var columnar = walkingDistance > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
@@ -897,6 +987,26 @@ Formation.prototype.OnGlobalEntityRenamed = function(msg)
}
}
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 each (var ent in 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(formationName)
{
this.formationName = formationName;
@@ -1202,7 +1202,7 @@ function GetFormationUnitAIs(ents, player, formName)
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationEnt = undefined;
var formationUnitAIs = [];
if (formation.ids.length == 1)
{
// Selected units either belong to this formation or have no formation
@@ -1212,12 +1212,12 @@ function GetFormationUnitAIs(ents, player, formName)
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command
formationEnt = +fid;
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
}
}
if (!formationEnt)
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller
@@ -1228,52 +1228,141 @@ function GetFormationUnitAIs(ents, player, formName)
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// Create the new controller
formationEnt = Engine.AddEntity("special/formation");
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
cmpFormation.SetMembers(formation.entities);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
// If all the selected units were previously in formations of the same shape,
// then set this new formation to that shape too; otherwise use the default shape
var lastFormationName = undefined;
for each (var ent in formation.entities)
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
var formationSeparation = 60;
var clusters = ClusterEntities(formation.entities, formationSeparation);
var formationEnts = [];
for each (var cluster in clusters)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
// Create the new controller
var formationEnt = Engine.AddEntity("special/formation");
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for each (var ent in formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
// If all the selected units were previously in formations of the same shape,
// then set this new formation to that shape too; otherwise use the default shape
var lastFormationName = undefined;
for each (var ent in cluster)
{
var name = cmpUnitAI.GetLastFormationName();
if (lastFormationName === undefined)
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
lastFormationName = name;
}
else if (lastFormationName != name)
{
lastFormationName = undefined;
break;
var name = cmpUnitAI.GetLastFormationName();
if (lastFormationName === undefined)
{
lastFormationName = name;
}
else if (lastFormationName != name)
{
lastFormationName = undefined;
break;
}
}
}
}
var formationName;
if (lastFormationName)
formationName = lastFormationName;
else
formationName = "Line Closed";
var formationName;
if (lastFormationName)
formationName = lastFormationName;
else
formationName = "Line Closed";
if (CanMoveEntsIntoFormation(formation.entities, formationName))
{
cmpFormation.LoadFormation(formationName);
}
else
{
cmpFormation.LoadFormation("Scatter");
if (CanMoveEntsIntoFormation(cluster, formationName))
cmpFormation.LoadFormation(formationName);
else
cmpFormation.LoadFormation("Scatter");
}
}
return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI));
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
var clusters = [];
if (!ents.length)
return clusters;
var distSq = separationDistance * separationDistance;
var positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
var matrix = [];
for (var i = 0; i < ents.length; i++)
{
matrix[i] = [];
clusters.push([ents[i]]);
var cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
if (!cmpPosition)
{
error("Asked to cluster entities without position: "+ents[i]);
return clusters;
}
positions.push(cmpPosition.GetPosition2D());
for (var j = 0; j < i; j++)
{
var dx = positions[i].x - positions[j].x;
var dy = positions[i].y - positions[j].y;
matrix[i][j] = dx * dx + dy * dy;
}
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
var smallDist = Infinity;
var closeClusters = undefined;
for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (var j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
var distances = [];
for (var i = 0; i < clusters.length; i++)
{
if (i == closeClusters[1] || i == closeClusters[0])
continue;
var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]];
var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (var i = 0; i < matrix.length; i++)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationName)