Cleanup / Functioning formation

This commit is contained in:
Anuken
2020-05-02 17:58:12 -04:00
parent 3e87fff9db
commit dce8b8faa1
63 changed files with 165 additions and 4007 deletions

View File

@@ -0,0 +1,44 @@
package mindustry.ai.formations;
import arc.struct.*;
/**
* {@code BoundedSlotAssignmentStrategy} is an abstract implementation of {@link SlotAssignmentStrategy} that supports roles.
* Generally speaking, there are hard and soft roles. Hard roles cannot be broken, soft roles can.
* <p>
* This abstract class provides an implementation of the {@link #calculateNumberOfSlots(Array) calculateNumberOfSlots} method that
* is more general (and costly) than the simplified implementation in {@link FreeSlotAssignmentStrategy}. It scans the assignment
* list to find the number of filled slots, which is the highest slot number in the assignments.
* @author davebaol
*/
public abstract class BoundedSlotAssignmentStrategy implements SlotAssignmentStrategy{
@Override
public abstract void updateSlotAssignments(Array<SlotAssignment> assignments);
@Override
public int calculateNumberOfSlots(Array<SlotAssignment> assignments){
// Find the number of filled slots: it will be the
// highest slot number in the assignments
int filledSlots = -1;
for(int i = 0; i < assignments.size; i++){
SlotAssignment assignment = assignments.get(i);
if(assignment.slotNumber >= filledSlots) filledSlots = assignment.slotNumber;
}
// Add one to go from the index of the highest slot to the number of slots needed.
return filledSlots + 1;
}
@Override
public void removeSlotAssignment(Array<SlotAssignment> assignments, int index){
int sn = assignments.get(index).slotNumber;
for(int i = 0; i < assignments.size; i++){
SlotAssignment sa = assignments.get(i);
if(sa.slotNumber >= sn) sa.slotNumber--;
}
assignments.remove(index);
}
}

View File

@@ -0,0 +1,212 @@
package mindustry.ai.formations;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
/**
* A {@code Formation} coordinates the movement of a group of characters so that they retain some group organization. Characters
* belonging to a formation must implement the {@link FormationMember} interface. At its simplest, a formation can consist of
* moving in a fixed geometric pattern such as a V or line abreast, but it is not limited to that. Formations can also make use of
* the environment. Squads of characters can move between cover points using formation steering with only minor modifications, for
* example.
* <p>
* Formation motion is used in team sports games, squad-based games, real-time strategy games, and sometimes in first-person
* shooters, driving games, and action adventures too. It is a simple and flexible technique that is much quicker to write and
* execute and can produce much more stable behavior than collaborative tactical decision making.
* @author davebaol
*/
public class Formation{
/** A list of slots assignments. */
Array<SlotAssignment> slotAssignments;
/** The anchor point of this formation. */
public Vec3 anchor;
/** The formation pattern */
public FormationPattern pattern;
/** The strategy used to assign a member to his slot */
public SlotAssignmentStrategy slotAssignmentStrategy;
/** The formation motion moderator */
public FormationMotionModerator motionModerator;
private final Vec2 positionOffset;
private final Mat orientationMatrix = new Mat();
/** The location representing the drift offset for the currently filled slots. */
private final Vec3 driftOffset;
/**
* Creates a {@code Formation} for the specified {@code pattern} using a {@link FreeSlotAssignmentStrategy} and no motion
* moderator.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Vec3 anchor, FormationPattern pattern){
this(anchor, pattern, new FreeSlotAssignmentStrategy(), null);
}
/**
* Creates a {@code Formation} for the specified {@code pattern} and {@code slotAssignmentStrategy} using no motion moderator.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy){
this(anchor, pattern, slotAssignmentStrategy, null);
}
/**
* Creates a {@code Formation} for the specified {@code pattern}, {@code slotAssignmentStrategy} and {@code moderator}.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
* @param motionModerator the motion moderator. Can be {@code null} if moderation is not needed
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy,
FormationMotionModerator motionModerator){
if(anchor == null) throw new IllegalArgumentException("The anchor point cannot be null");
this.anchor = anchor;
this.pattern = pattern;
this.slotAssignmentStrategy = slotAssignmentStrategy;
this.motionModerator = motionModerator;
this.slotAssignments = new Array<>();
this.driftOffset = new Vec3();
this.positionOffset = new Vec2(anchor.x, anchor.y).cpy();
}
/** Updates the assignment of members to slots */
public void updateSlotAssignments(){
// Apply the strategy to update slot assignments
slotAssignmentStrategy.updateSlotAssignments(slotAssignments);
// Set the newly calculated number of slots
pattern.slots = slotAssignmentStrategy.calculateNumberOfSlots(slotAssignments);
// Update the drift offset if a motion moderator is set
if(motionModerator != null) motionModerator.calculateDriftOffset(driftOffset, slotAssignments, pattern);
}
/**
* Changes the pattern of this formation and updates slot assignments if the number of member is supported by the given
* pattern.
* @param pattern the pattern to set
* @return {@code true} if the pattern has effectively changed; {@code false} otherwise.
*/
public boolean changePattern(FormationPattern pattern){
// Find out how many slots we have occupied
int occupiedSlots = slotAssignments.size;
// Check if the pattern supports one more slot
if(pattern.supportsSlots(occupiedSlots)){
this.pattern = pattern;
// Update the slot assignments and return success
updateSlotAssignments();
return true;
}
return false;
}
/**
* Adds a new member to the first available slot and updates slot assignments if the number of member is supported by the
* current pattern.
* @param member the member to add
* @return {@code false} if no more slots are available; {@code true} otherwise.
*/
public boolean addMember(FormationMember member){
// Find out how many slots we have occupied
int occupiedSlots = slotAssignments.size;
// Check if the pattern supports one more slot
if(pattern.supportsSlots(occupiedSlots + 1)){
// Add a new slot assignment
slotAssignments.add(new SlotAssignment(member, occupiedSlots));
// Update the slot assignments and return success
updateSlotAssignments();
return true;
}
return false;
}
/**
* Removes a member from its slot and updates slot assignments.
* @param member the member to remove
*/
public void removeMember(FormationMember member){
// Find the member's slot
int slot = findMemberSlot(member);
// Make sure we've found a valid result
if(slot >= 0){
// Remove the slot
// slotAssignments.removeIndex(slot);
slotAssignmentStrategy.removeSlotAssignment(slotAssignments, slot);
// Update the assignments
updateSlotAssignments();
}
}
private int findMemberSlot(FormationMember member){
for(int i = 0; i < slotAssignments.size; i++){
if(slotAssignments.get(i).member == member) return i;
}
return -1;
}
// debug
public SlotAssignment getSlotAssignmentAt(int index){
return slotAssignments.get(index);
}
// debug
public int getSlotAssignmentCount(){
return slotAssignments.size;
}
/** Writes new slot locations to each member */
public void updateSlots(){
positionOffset.set(anchor);
float orientationOffset = anchor.z;
if(motionModerator != null){
positionOffset.sub(driftOffset);
orientationOffset -= driftOffset.z;
}
// Get the orientation of the anchor point as a matrix
orientationMatrix.idt().rotate(anchor.z);
// Go through each member in turn
for(int i = 0; i < slotAssignments.size; i++){
SlotAssignment slotAssignment = slotAssignments.get(i);
// Retrieve the location reference of the formation member to calculate the new value
Vec3 relativeLoc = slotAssignment.member.formationPos();
float z = relativeLoc.z;
// Ask for the location of the slot relative to the anchor point
pattern.calculateSlotLocation(relativeLoc, slotAssignment.slotNumber);
// Transform it by the anchor point's position and orientation
relativeLoc.mul(orientationMatrix);
// Add the anchor and drift components
relativeLoc.add(positionOffset.x, positionOffset.y, 0);
relativeLoc.z = z + orientationOffset;
}
// Possibly reset the anchor point if a moderator is set
if(motionModerator != null){
motionModerator.updateAnchorPoint(anchor);
}
}
}

View File

@@ -0,0 +1,14 @@
package mindustry.ai.formations;
import arc.math.geom.*;
/**
* Game characters coordinated by a {@link Formation} must implement this interface. Any {@code FormationMember} has a target
* location which is the place where it should be in order to stay in formation. This target location is calculated by the
* formation itself.
* @author davebaol
*/
public interface FormationMember{
/** Returns the target location of this formation member. */
Vec3 formationPos();
}

View File

@@ -0,0 +1,52 @@
package mindustry.ai.formations;
import arc.math.geom.*;
import arc.struct.*;
/**
* A {@code FormationMotionModerator} moderates the movement of the formation based on the current positions of the members in its
* slots: in effect to keep the anchor point on a leash. If the members in the slots are having trouble reaching their targets,
* then the formation as a whole should be held back to give them a chance to catch up.
* @author davebaol
*/
public abstract class FormationMotionModerator{
private Vec3 tempLocation;
/**
* Update the anchor point to moderate formation motion. This method is called at each frame.
* @param anchor the anchor point
*/
public abstract void updateAnchorPoint(Vec3 anchor);
/**
* Calculates the drift offset when members are in the given set of slots for the specified pattern.
* @param centerOfMass the output location set to the calculated drift offset
* @param slotAssignments the set of slots
* @param pattern the pattern
* @return the given location for chaining.
*/
public Vec3 calculateDriftOffset(Vec3 centerOfMass, Array<SlotAssignment> slotAssignments, FormationPattern pattern){
// Clear the center of mass
centerOfMass.x = centerOfMass.y = 0;
float centerOfMassOrientation = 0;
// Make sure tempLocation is instantiated
if(tempLocation == null) tempLocation = new Vec3();
// Go through each assignment and add its contribution to the center
float numberOfAssignments = slotAssignments.size;
for(int i = 0; i < numberOfAssignments; i++){
pattern.calculateSlotLocation(tempLocation, slotAssignments.get(i).slotNumber);
centerOfMass.add(tempLocation);
centerOfMassOrientation += tempLocation.z;
}
// Divide through to get the drift offset.
centerOfMass.scl(1f / numberOfAssignments);
centerOfMassOrientation /= numberOfAssignments;
centerOfMass.z = centerOfMassOrientation;
return centerOfMass;
}
}

View File

@@ -0,0 +1,27 @@
package mindustry.ai.formations;
import arc.math.geom.*;
/**
* The {@code FormationPattern} interface represents the shape of a formation and generates the slot offsets, relative to its
* anchor point. Since formations can be scalable the pattern must be able to determine if a given number of slots is supported.
* <p>
* Each particular pattern (such as a V, wedge, circle) needs its own instance of a class that implements this
* {@code FormationPattern} interface.
* @author davebaol
*/
public abstract class FormationPattern{
public int slots;
/** Returns the location of the given slot index. */
public abstract Vec3 calculateSlotLocation(Vec3 out, int slot);
/**
* Returns true if the pattern can support the given number of slots
* @param slotCount the number of slots
* @return {@code true} if this pattern can support the given number of slots; {@code false} othervwise.
*/
public boolean supportsSlots(int slotCount){
return true;
}
}

View File

@@ -0,0 +1,33 @@
package mindustry.ai.formations;
import arc.struct.*;
/**
* {@code FreeSlotAssignmentStrategy} is the simplest implementation of {@link SlotAssignmentStrategy}. It simply go through
* each assignment in the list and assign sequential slot numbers. The number of slots is just the length of the list.
* <p>
* Because each member can occupy any slot this implementation does not support roles.
* @author davebaol
*/
public class FreeSlotAssignmentStrategy implements SlotAssignmentStrategy{
@Override
public void updateSlotAssignments(Array<SlotAssignment> assignments){
// A very simple assignment algorithm: we simply go through
// each assignment in the list and assign sequential slot numbers
for(int i = 0; i < assignments.size; i++)
assignments.get(i).slotNumber = i;
}
@Override
public int calculateNumberOfSlots(Array<SlotAssignment> assignments){
return assignments.size;
}
@Override
public void removeSlotAssignment(Array<SlotAssignment> assignments, int index){
assignments.remove(index);
}
}

View File

@@ -0,0 +1,29 @@
package mindustry.ai.formations;
/**
* A {@code SlotAssignment} instance represents the assignment of a single {@link FormationMember} to its slot in the
* {@link Formation}.
* @author davebaol
*/
public class SlotAssignment{
public FormationMember member;
public int slotNumber;
/**
* Creates a {@code SlotAssignment} for the given {@code member}.
* @param member the member of this slot assignment
*/
public SlotAssignment(FormationMember member){
this(member, 0);
}
/**
* Creates a {@code SlotAssignment} for the given {@code member} and {@code slotNumber}.
* @param member the member of this slot assignment
*/
public SlotAssignment(FormationMember member, int slotNumber){
this.member = member;
this.slotNumber = slotNumber;
}
}

View File

@@ -0,0 +1,20 @@
package mindustry.ai.formations;
import arc.struct.*;
/**
* This interface defines how each {@link FormationMember} is assigned to a slot in the {@link Formation}.
* @author davebaol
*/
public interface SlotAssignmentStrategy{
/** Updates the assignment of members to slots */
void updateSlotAssignments(Array<SlotAssignment> assignments);
/** Calculates the number of slots from the assignment data. */
int calculateNumberOfSlots(Array<SlotAssignment> assignments);
/** Removes the slot assignment at the specified index. */
void removeSlotAssignment(Array<SlotAssignment> assignments, int index);
}

View File

@@ -0,0 +1,166 @@
package mindustry.ai.formations;
import arc.struct.*;
import arc.util.*;
/**
* {@code SoftRoleSlotAssignmentStrategy} is a concrete implementation of {@link BoundedSlotAssignmentStrategy} that supports soft
* roles, i.e. roles that can be broken. Rather than a member having a list of roles it can fulfill, it has a set of values
* representing how difficult it would find it to fulfill every role. The value is known as the slot cost. To make a slot
* impossible for a member to fill, its slot cost should be infinite (you can even set a threshold to ignore all slots whose cost
* is too high; this will reduce computation time when several costs are exceeding). To make a slot ideal for a member, its slot
* cost should be zero. We can have different levels of unsuitable assignment for one member.
* <p>
* Slot costs do not necessarily have to depend only on the member and the slot roles. They can be generalized to include any
* difficulty a member might have in taking up a slot. If a formation is spread out, for example, a member may choose a slot that
* is close by over a more distant slot. Distance can be directly used as a slot cost.
* <p>
* <b>IMPORTANVec2 NOTES:</b>
* <ul>
* <li>In order for the algorithm to work properly the slot costs can not be negative.</li>
* <li>This algorithm is often not fast enough to be used regularly. However, slot assignment happens relatively seldom (when the
* player selects a new pattern, for example, or adds a member to the formation, or a member is removed from the formation).</li>
* </ul>
* @author davebaol
*/
public class SoftRoleSlotAssignmentStrategy extends BoundedSlotAssignmentStrategy{
protected SlotCostProvider slotCostProvider;
protected float costThreshold;
private BooleanArray filledSlots;
/**
* Creates a {@code SoftRoleSlotAssignmentStrategy} with the given slot cost provider and no cost threshold.
* @param slotCostProvider the slot cost provider
*/
public SoftRoleSlotAssignmentStrategy(SlotCostProvider slotCostProvider){
this(slotCostProvider, Float.POSITIVE_INFINITY);
}
/**
* Creates a {@code SoftRoleSlotAssignmentStrategy} with the given slot cost provider and cost threshold.
* @param slotCostProvider the slot cost provider
* @param costThreshold is a slot-cost limit, beyond which a slot is considered to be too expensive to consider occupying.
*/
public SoftRoleSlotAssignmentStrategy(SlotCostProvider slotCostProvider, float costThreshold){
this.slotCostProvider = slotCostProvider;
this.costThreshold = costThreshold;
this.filledSlots = new BooleanArray();
}
@Override
public void updateSlotAssignments(Array<SlotAssignment> assignments){
// Holds a list of member and slot data for each member.
Array<MemberAndSlots> memberData = new Array<>();
// Compile the member data
int numberOfAssignments = assignments.size;
for(int i = 0; i < numberOfAssignments; i++){
SlotAssignment assignment = assignments.get(i);
// Create a new member datum, and fill it
MemberAndSlots datum = new MemberAndSlots(assignment.member);
// Add each valid slot to it
for(int j = 0; j < numberOfAssignments; j++){
// Get the cost of the slot
float cost = slotCostProvider.getCost(assignment.member, j);
// Make sure the slot is valid
if(cost >= costThreshold) continue;
SlotAssignment slot = assignments.get(j);
// Store the slot information
CostAndSlot slotDatum = new CostAndSlot(cost, slot.slotNumber);
datum.costAndSlots.add(slotDatum);
// Add it to the member's ease of assignment
datum.assignmentEase += 1f / (1f + cost);
}
// Add member datum
memberData.add(datum);
}
// Reset the array to keep track of which slots we have already filled.
if(numberOfAssignments > filledSlots.size) filledSlots.ensureCapacity(numberOfAssignments - filledSlots.size);
filledSlots.size = numberOfAssignments;
for(int i = 0; i < numberOfAssignments; i++)
filledSlots.set(i, false);
// Arrange members in order of ease of assignment, with the least easy first.
memberData.sort();
MEMBER_LOOP:
for(int i = 0; i < memberData.size; i++){
MemberAndSlots memberDatum = memberData.get(i);
// Choose the first slot in the list that is still empty (non-filled)
memberDatum.costAndSlots.sort();
int m = memberDatum.costAndSlots.size;
for(int j = 0; j < m; j++){
int slotNumber = memberDatum.costAndSlots.get(j).slotNumber;
// Check if this slot is valid
if(!filledSlots.get(slotNumber)){
// Fill this slot
SlotAssignment slot = assignments.get(slotNumber);
slot.member = memberDatum.member;
slot.slotNumber = slotNumber;
// Reserve the slot
filledSlots.set(slotNumber, true);
// Go to the next member
continue MEMBER_LOOP;
}
}
// If we reach here, it's because a member has no valid assignment.
//
// TODO
// Some sensible action should be taken, such as reporting to the player.
throw new ArcRuntimeException("SoftRoleSlotAssignmentStrategy cannot find valid slot assignment for member " + memberDatum.member);
}
}
static class CostAndSlot implements Comparable<CostAndSlot>{
float cost;
int slotNumber;
public CostAndSlot(float cost, int slotNumber){
this.cost = cost;
this.slotNumber = slotNumber;
}
@Override
public int compareTo(CostAndSlot other){
return Float.compare(cost, other.cost);
}
}
static class MemberAndSlots implements Comparable<MemberAndSlots>{
FormationMember member;
float assignmentEase;
Array<CostAndSlot> costAndSlots;
public MemberAndSlots(FormationMember member){
this.member = member;
this.assignmentEase = 0f;
this.costAndSlots = new Array<>();
}
@Override
public int compareTo(MemberAndSlots other){
return Float.compare(assignmentEase, other.assignmentEase);
}
}
public interface SlotCostProvider{
float getCost(FormationMember member, int slotNumber);
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.ai.formations.patterns;
import arc.math.*;
import arc.math.geom.*;
import mindustry.ai.formations.*;
public class CircleFormation extends FormationPattern{
/** The radius of one member. This is needed to determine how close we can pack a given number of members around circle. */
public float memberRadius;
/** Angle offset. */
public float angleOffset = 0;
public CircleFormation(float memberRadius){
this.memberRadius = memberRadius;
}
@Override
public Vec3 calculateSlotLocation(Vec3 outLocation, int slotNumber){
if(slots > 1){
float angle = (360f * slotNumber) / slots;
float radius = memberRadius / (float)Math.sin(180f / slots * Mathf.degRad);
outLocation.set(Angles.trnsx(angle, radius), Angles.trnsy(angle, radius), angle);
}else{
outLocation.set(0, 0, 360f * slotNumber);
}
outLocation.z += angleOffset;
return outLocation;
}
}

View File

@@ -0,0 +1,26 @@
package mindustry.ai.formations.patterns;
import arc.math.*;
import arc.math.geom.*;
import mindustry.ai.formations.*;
public class SquareFormation extends FormationPattern{
public float spacing = 20;
@Override
public Vec3 calculateSlotLocation(Vec3 out, int slot){
//side of each square of formation
int side = Mathf.ceil(Mathf.sqrt(slots + 1));
int cx = slot % side, cy = slot / side;
//don't hog the middle spot
if(cx == side /2 && cy == side/2 && (side%2)==1){
slot = slots;
cx = slot % side;
cy = slot / side;
}
return out.set(cx - (side/2f - 0.5f), cy - (side/2f - 0.5f), 0).scl(spacing);
}
}