Added steering AI classes

This commit is contained in:
Anuken
2020-05-02 14:13:47 -04:00
parent 25f07e7bcb
commit b9aa8edf78
68 changed files with 6363 additions and 19 deletions

View File

@@ -21,7 +21,6 @@ import java.util.*;
import static mindustry.Vars.*;
/** Class used for indexing special target blocks for AI. */
@SuppressWarnings("unchecked")
public class BlockIndexer{
/** Size of one quadrant. */
private final static int quadrantSize = 16;
@@ -36,9 +35,9 @@ public class BlockIndexer{
private GridBits[] structQuadrants;
/** Stores all damaged tile entities by team. */
private TileArray[] damagedTiles = new TileArray[Team.all().length];
/**All ores available on this map.*/
/** All ores available on this map. */
private ObjectSet<Item> allOres = new ObjectSet<>();
/**Stores teams that are present here as tiles.*/
/** Stores teams that are present here as tiles. */
private Array<Team> activeTeams = new Array<>();
/** Maps teams to a map of flagged tiles by type. */
private TileArray[][] flagMap = new TileArray[Team.all().length][BlockFlag.all.length];
@@ -126,7 +125,7 @@ public class BlockIndexer{
}
}
/** @return whether this item is present on this map.*/
/** @return whether this item is present on this map. */
public boolean hasOre(Item item){
return allOres.contains(item);
}
@@ -236,7 +235,7 @@ public class BlockIndexer{
public Tilec findTile(Team team, float x, float y, float range, Boolf<Tilec> pred, boolean usePriority){
Tilec closest = null;
float dst = 0;
float range2 = range*range;
float range2 = range * range;
for(int rx = Math.max((int)((x - range) / tilesize / quadrantSize), 0); rx <= (int)((x + range) / tilesize / quadrantSize) && rx < quadWidth(); rx++){
for(int ry = Math.max((int)((y - range) / tilesize / quadrantSize), 0); ry <= (int)((y + range) / tilesize / quadrantSize) && ry < quadHeight(); ry++){
@@ -254,10 +253,10 @@ public class BlockIndexer{
float ndst = e.dst2(x, y);
if(ndst < range2 && (closest == null ||
//this one is closer, and it is at least of equal priority
(ndst < dst && (!usePriority || closest.block().priority.ordinal() <= e.block().priority.ordinal())) ||
//priority is used, and new block has higher priority regardless of range
(usePriority && closest.block().priority.ordinal() < e.block().priority.ordinal()))){
//this one is closer, and it is at least of equal priority
(ndst < dst && (!usePriority || closest.block().priority.ordinal() <= e.block().priority.ordinal())) ||
//priority is used, and new block has higher priority regardless of range
(usePriority && closest.block().priority.ordinal() < e.block().priority.ordinal()))){
dst = ndst;
closest = e;
}
@@ -369,7 +368,7 @@ public class BlockIndexer{
for(int y = quadrantY * quadrantSize; y < world.height() && y < (quadrantY + 1) * quadrantSize; y++){
Tilec result = world.ent(x, y);
//when a targetable block is found, mark this quadrant as occupied and stop searching
if(result!= null && result.team() == team){
if(result != null && result.team() == team){
bits.set(quadrantX, quadrantY);
break outer;
}

View File

@@ -24,7 +24,7 @@ public class Pathfinder implements Runnable{
/** tile data, see PathTileStruct */
private int[][] tiles;
/** unordered array of path data for iteration only. DO NOT iterate ot access this in the main thread.*/
/** unordered array of path data for iteration only. DO NOT iterate ot access this in the main thread. */
private Array<PathData> list = new Array<>();
/** Maps teams + flags to a valid path to get to that flag for that team. */
private PathData[][] pathMap = new PathData[Team.all().length][PathTarget.all.length];
@@ -182,7 +182,7 @@ public class Pathfinder implements Runnable{
return current;
}
/** @return whether a tile can be passed through by this team. Pathfinding thread only.*/
/** @return whether a tile can be passed through by this team. Pathfinding thread only. */
private boolean passable(int x, int y, Team team){
int tile = tiles[x][y];
return PathTile.passable(tile) || (PathTile.team(tile) != team.id && PathTile.team(tile) != (int)Team.derelict.id);
@@ -229,8 +229,10 @@ public class Pathfinder implements Runnable{
updateFrontier(createPath(team, target, target.getTargets(team, new IntArray())), -1);
}
/** Created a new flowfield that aims to get to a certain target for a certain team.
* Pathfinding thread only. */
/**
* Created a new flowfield that aims to get to a certain target for a certain team.
* Pathfinding thread only.
*/
private PathData createPath(Team team, PathTarget target, IntArray targets){
PathData path = new PathData(team, target, world.width(), world.height());
@@ -292,7 +294,7 @@ public class Pathfinder implements Runnable{
}
}
/** A path target defines a set of targets for a path.*/
/** A path target defines a set of targets for a path. */
public enum PathTarget{
enemyCores((team, out) -> {
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
@@ -320,7 +322,7 @@ public class Pathfinder implements Runnable{
this.targeter = targeter;
}
/** Get targets. This must run on the main thread.*/
/** Get targets. This must run on the main thread. */
public IntArray getTargets(Team team, IntArray out){
targeter.get(team, out);
return out;

View File

@@ -87,7 +87,7 @@ public class WaveSpawner{
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam) && !state.teams.playerCores().isEmpty()){
Tilec firstCore = state.teams.playerCores().first();
for(Tilec core : state.rules.waveTeam.cores()){
Tmp.v1.set(firstCore).sub(core).limit(coreMargin + core.block().size*tilesize);
Tmp.v1.set(firstCore).sub(core).limit(coreMargin + core.block().size * tilesize);
cons.accept(core.x() + Tmp.v1.x, core.y() + Tmp.v1.y, false);
}
}
@@ -95,7 +95,7 @@ public class WaveSpawner{
private void eachFlyerSpawn(Floatc2 cons){
for(Tile tile : spawns){
float angle = Angles.angle(tile.x, tile.y, world.width()/2, world.height()/2);
float angle = Angles.angle(tile.x, tile.y, world.width() / 2, world.height() / 2);
float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize;
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin);

View File

@@ -0,0 +1,44 @@
package mindustry.ai.ai.fma;
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,278 @@
package mindustry.ai.ai.fma;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* 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. */
protected Location anchor;
/** The formation pattern */
protected FormationPattern pattern;
/** The strategy used to assign a member to his slot */
protected SlotAssignmentStrategy slotAssignmentStrategy;
/** The formation motion moderator */
protected 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 Location 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, usually a {@link Steerable}. Cannot be {@code null}.
* @param pattern the pattern of this formation
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Location 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, usually a {@link Steerable}. 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(Location 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, usually a {@link Steerable}. 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(Location 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 = anchor.newLocation();
this.positionOffset = anchor.getPosition().cpy();
}
/**
* Returns the current anchor point of the formation. This can be the location (i.e. position and orientation) of a leader
* member, a modified center of mass of the members in the formation, or an invisible but steered anchor point for a two-level
* steering system.
*/
public Location getAnchorPoint(){
return anchor;
}
/**
* Sets the anchor point of the formation.
* @param anchor the anchor point to set
*/
public void setAnchorPoint(Location anchor){
this.anchor = anchor;
}
/** @return the pattern of this formation */
public FormationPattern getPattern(){
return pattern;
}
/**
* Sets the pattern of this formation
* @param pattern the pattern to set
*/
public void setPattern(FormationPattern pattern){
this.pattern = pattern;
}
/** @return the slot assignment strategy of this formation */
public SlotAssignmentStrategy getSlotAssignmentStrategy(){
return slotAssignmentStrategy;
}
/**
* Sets the slot assignment strategy of this formation
* @param slotAssignmentStrategy the slot assignment strategy to set
*/
public void setSlotAssignmentStrategy(SlotAssignmentStrategy slotAssignmentStrategy){
this.slotAssignmentStrategy = slotAssignmentStrategy;
}
/** @return the motion moderator of this formation */
public FormationMotionModerator getMotionModerator(){
return motionModerator;
}
/**
* Sets the motion moderator of this formation
* @param motionModerator the motion moderator to set
*/
public void setMotionModerator(FormationMotionModerator motionModerator){
this.motionModerator = motionModerator;
}
/** 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.setNumberOfSlots(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)){
setPattern(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(){
// Find the anchor point
Location anchor = getAnchorPoint();
positionOffset.set(anchor.getPosition());
float orientationOffset = anchor.getOrientation();
if(motionModerator != null){
positionOffset.sub(driftOffset.getPosition());
orientationOffset -= driftOffset.getOrientation();
}
// Get the orientation of the anchor point as a matrix
orientationMatrix.idt().rotateRad(anchor.getOrientation());
// 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
Location relativeLoc = slotAssignment.member.getTargetLocation();
// Ask for the location of the slot relative to the anchor point
pattern.calculateSlotLocation(relativeLoc, slotAssignment.slotNumber);
Vec2 relativeLocPosition = relativeLoc.getPosition();
// Transform it by the anchor point's position and orientation
//relativeLocPosition.mul(orientationMatrix).add(anchor.position);
relativeLocPosition.mul(orientationMatrix);
// Add the anchor and drift components
relativeLocPosition.add(positionOffset);
relativeLoc.setOrientation(relativeLoc.getOrientation() + orientationOffset);
}
// Possibly reset the anchor point if a moderator is set
if(motionModerator != null){
motionModerator.updateAnchorPoint(anchor);
}
}
}

View File

@@ -0,0 +1,15 @@
package mindustry.ai.ai.fma;
import mindustry.ai.ai.utils.*;
/**
* 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. */
Location getTargetLocation();
}

View File

@@ -0,0 +1,59 @@
package mindustry.ai.ai.fma;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.ai.ai.utils.*;
/**
* 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 Location 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(Location 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 Location calculateDriftOffset(Location centerOfMass, Array<SlotAssignment> slotAssignments,
FormationPattern pattern){
// Clear the center of mass
centerOfMass.getPosition().setZero();
float centerOfMassOrientation = 0;
// Make sure tempLocation is instantiated
if(tempLocation == null) tempLocation = centerOfMass.newLocation();
Vec2 centerOfMassPos = centerOfMass.getPosition();
Vec2 tempLocationPos = tempLocation.getPosition();
// 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);
centerOfMassPos.add(tempLocationPos);
centerOfMassOrientation += tempLocation.getOrientation();
}
// Divide through to get the drift offset.
centerOfMassPos.scl(1f / numberOfAssignments);
centerOfMassOrientation /= numberOfAssignments;
centerOfMass.setOrientation(centerOfMassOrientation);
return centerOfMass;
}
}

View File

@@ -0,0 +1,30 @@
package mindustry.ai.ai.fma;
import mindustry.ai.ai.utils.*;
/**
* 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 interface FormationPattern{
/**
* Sets the number of slots.
* @param numberOfSlots the number of slots to set
*/
void setNumberOfSlots(int numberOfSlots);
/** Returns the location of the given slot index. */
Location calculateSlotLocation(Location outLocation, int slotNumber);
/**
* 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.
*/
boolean supportsSlots(int slotCount);
}

View File

@@ -0,0 +1,33 @@
package mindustry.ai.ai.fma;
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.ai.fma;
/**
* 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.ai.fma;
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.ai.fma;
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,63 @@
package mindustry.ai.ai.fma.patterns;
import arc.math.*;
import mindustry.ai.ai.fma.*;
import mindustry.ai.ai.utils.*;
/**
* The defensive circle posts members around the circumference of a circle, so their backs are to the center of the circle. The
* circle can consist of any number of members. Although a huge number of members might look silly, this implementation doesn't
* put any fixed limit.
* @author davebaol
*/
public class DefensiveCircleFormationPattern implements FormationPattern{
/** The number of slots currently in the pattern. */
int numberOfSlots;
/** The radius of one member. This is needed to determine how close we can pack a given number of members around circle. */
float memberRadius;
/**
* Creates a {@code DefensiveCircleFormationPattern}
*/
public DefensiveCircleFormationPattern(float memberRadius){
this.memberRadius = memberRadius;
}
@Override
public void setNumberOfSlots(int numberOfSlots){
this.numberOfSlots = numberOfSlots;
}
@Override
public Location calculateSlotLocation(Location outLocation, int slotNumber){
if(numberOfSlots > 1){
// Place the slot around the circle based on its slot number
float angleAroundCircle = (Mathf.PI2 * slotNumber) / numberOfSlots;
// The radius depends on the radius of the member,
// and the number of members in the circle:
// we want there to be no gap between member's shoulders.
float radius = memberRadius / (float)Math.sin(Math.PI / numberOfSlots);
// Fill location components based on the angle around circle.
outLocation.angleToVector(outLocation.getPosition(), angleAroundCircle).scl(radius);
// The members should be facing out
outLocation.setOrientation(angleAroundCircle);
}else{
outLocation.getPosition().setZero();
outLocation.setOrientation(Mathf.PI2 * slotNumber);
}
// Return the slot location
return outLocation;
}
@Override
public boolean supportsSlots(int slotCount){
// In this case we support any number of slots.
return true;
}
}

View File

@@ -0,0 +1,28 @@
package mindustry.ai.ai.fma.patterns;
import arc.math.*;
import mindustry.ai.ai.utils.*;
/**
* The offensive circle posts members around the circumference of a circle, so their fronts are to the center of the circle. The
* circle can consist of any number of members. Although a huge number of members might look silly, this implementation doesn't
* put any fixed limit.
* @author davebaol
*/
public class OffensiveCircleFormationPattern extends DefensiveCircleFormationPattern{
/**
* Creates a {@code OffensiveCircleFormationPattern}
*/
public OffensiveCircleFormationPattern(float memberRadius){
super(memberRadius);
}
@Override
public Location calculateSlotLocation(Location outLocation, int slotNumber){
super.calculateSlotLocation(outLocation, slotNumber);
outLocation.setOrientation(outLocation.getOrientation() + Mathf.PI);
return outLocation;
}
}

View File

@@ -0,0 +1,39 @@
package mindustry.ai.ai.steer;
import mindustry.ai.ai.steer.Proximity.*;
/**
* {@code GroupBehavior} is the base class for the steering behaviors that take into consideration the agents in the game world
* that are within the immediate area of the owner. This immediate area is defined by a {@link Proximity} that is in charge of
* finding and processing the owner's neighbors through the given {@link ProximityCallback}.
* @author davebaol
*/
public abstract class GroupBehavior extends SteeringBehavior{
/** The proximity decides which agents are considered neighbors. */
protected Proximity proximity;
/**
* Creates a GroupBehavior for the specified owner and proximity.
* @param owner the owner of this behavior.
* @param proximity the proximity to detect the owner's neighbors
*/
public GroupBehavior(Steerable owner, Proximity proximity){
super(owner);
this.proximity = proximity;
}
/** Returns the proximity of this group behavior */
public Proximity getProximity(){
return proximity;
}
/**
* Sets the proximity of this group behavior
* @param proximity the proximity to set
*/
public void setProximity(Proximity proximity){
this.proximity = proximity;
}
}

View File

@@ -0,0 +1,44 @@
package mindustry.ai.ai.steer;
/**
* A {@code Limiter} provides the maximum magnitudes of speed and acceleration for both linear and angular components.
* @author davebaol
*/
public interface Limiter{
/**
* Returns the threshold below which the linear speed can be considered zero. It must be a small positive value near to zero.
* Usually it is used to avoid updating the orientation when the velocity vector has a negligible length.
*/
float getZeroLinearSpeedThreshold();
/**
* Sets the threshold below which the linear speed can be considered zero. It must be a small positive value near to zero.
* Usually it is used to avoid updating the orientation when the velocity vector has a negligible length.
*/
void setZeroLinearSpeedThreshold(float value);
/** Returns the maximum linear speed. */
float getMaxLinearSpeed();
/** Sets the maximum linear speed. */
void setMaxLinearSpeed(float maxLinearSpeed);
/** Returns the maximum linear acceleration. */
float getMaxLinearAcceleration();
/** Sets the maximum linear acceleration. */
void setMaxLinearAcceleration(float maxLinearAcceleration);
/** Returns the maximum angular speed. */
float getMaxAngularSpeed();
/** Sets the maximum angular speed. */
void setMaxAngularSpeed(float maxAngularSpeed);
/** Returns the maximum angular acceleration. */
float getMaxAngularAcceleration();
/** Sets the maximum angular acceleration. */
void setMaxAngularAcceleration(float maxAngularAcceleration);
}

View File

@@ -0,0 +1,77 @@
package mindustry.ai.ai.steer;
import mindustry.ai.ai.steer.behaviors.*;
import mindustry.ai.ai.utils.Timepiece;
/**
* A {@code Proximity} defines an area that is used by group behaviors to find and process the owner's neighbors.
* <p>
* Typically (but not necessarily) different group behaviors share the same {@code Proximity} for a given owner. This allows you to
* combine group behaviors so as to get a more complex behavior also known as emergent behavior. Emergent behavior is behavior
* that looks complex and/or purposeful to the observer but is actually derived spontaneously from fairly simple rules. The
* lower-level agents following the rules have no idea of the bigger picture; they are only aware of themselves and maybe a few of
* their neighbors. A typical example of emergence is flocking behavior which is a combination of three group behaviors:
* {@link Separation separation}, {@link Alignment alignment}, and {@link Cohesion cohesion}. The three behaviors are typically
* combined through a {@link BlendedSteering blended steering}. This works okay but, because of the limited view distance of a
* character, it's possible for an agent to become isolated from its flock. If this happens, it will just sit still and do
* nothing. To prevent this from happening, you usually add in the {@link Wander wander} behavior too. This way, all the agents
* keep moving all the time. Tweaking the magnitudes of each of the contributing behaviors will give you different effects such as
* shoals of fish, loose swirling flocks of birds, or bustling close-knit herds of sheep.
* <p>
* Before a steering acceleration can be calculated for a combination of group behaviors, the neighbors must be determined and
* processed. This is done by the {@link #findNeighbors} method and its callback argument.
* <p>
* Notes:
* <ul>
* <li>Sharing a {@code Proximity} instance among group behaviors having the same owner can save a little time determining the
* neighbors only once from inside the {@code findNeighbors} method. Especially, {@code Proximity} implementation classes can use
* {@link mindustry.ai.ai.utils.Timepiece#getTime() GdxAI.getTimepiece().getTime()} to calculate neighbors only once per frame (assuming delta time is
* always greater than 0, if time has changed the frame has changed too). This means that
* <ul>
* <li>if you forget to {@link Timepiece#update(float) update the timepiece} on each frame the proximity instance will be
* calculated only the very first time, which is not what you want of course.</li>
* <li>ideally the timepiece should be updated before the proximity is updated by the {@link #findNeighbors(ProximityCallback)}
* method.</li>
* </ul>
* </li>
* <li>If you want to make sure a Proximity doesn't use as a neighbor a given agent from the list, for example the evader or the
* owner itself, you have to implement a callback that prevents it from being considered by returning {@code false} from the method
* {@link ProximityCallback#reportNeighbor(Steerable) reportNeighbor}.</li>
* <li>If there is some efficient way of pruning potential neighbors before they are processed, the overall performance in time
* will improve. Spatial data structures such as multi-resolution maps, quad-trees, oct-trees, and binary space partition (BSP)
* trees can be used to get potential neighbors more efficiently. Spatial partitioning techniques are crucial when you have to
* deal with lots of agents. Especially, if you're using Bullet or Box2d in your game, it's recommended to implement proximities
* that exploit their methods to query the world. Both Bullet and Box2d internally use some kind of spatial partitioning.</li>
* </ul>
* @author davebaol
*/
public interface Proximity{
/** Returns the owner of this proximity. */
Steerable getOwner();
/** Sets the owner of this proximity. */
void setOwner(Steerable owner);
/**
* Finds the agents that are within the immediate area of the owner. Each of those agents is passed to the
* {@link ProximityCallback#reportNeighbor(Steerable) reportNeighbor} method of the specified callback.
* @return the number of neighbors found.
*/
int findNeighbors(ProximityCallback callback);
/**
* The callback object used by a proximity to report the owner's neighbor.
* @author davebaol
*/
interface ProximityCallback{
/**
* The callback method used to report a neighbor.
* @param neighbor the reported neighbor.
* @return {@code true} if the given neighbor is valid; {@code false} otherwise.
*/
boolean reportNeighbor(Steerable neighbor);
}
}

View File

@@ -0,0 +1,33 @@
package mindustry.ai.ai.steer;
import arc.math.geom.*;
import mindustry.ai.ai.utils.*;
/**
* A {@code Steerable} is a {@link Location} that gives access to the character's data required by steering system.
* <p>
* Notice that there is nothing to connect the direction that a Steerable is moving and the direction it is facing. For
* instance, a character can be oriented along the x-axis but be traveling directly along the y-axis.
* @author davebaol
*/
public interface Steerable extends Location, Limiter{
/** Returns the vector indicating the linear velocity of this Steerable. */
Vec2 getLinearVelocity();
/** Returns the float value indicating the the angular velocity in radians of this Steerable. */
float getAngularVelocity();
/** Returns the bounding radius of this Steerable. */
float getBoundingRadius();
/** Returns {@code true} if this Steerable is tagged; {@code false} otherwise. */
boolean isTagged();
/**
* Tag/untag this Steerable. This is a generic flag utilized in a variety of ways.
* @param tagged the boolean value to set
*/
void setTagged(boolean tagged);
}

View File

@@ -0,0 +1,111 @@
package mindustry.ai.ai.steer;
import arc.math.geom.*;
import mindustry.ai.ai.utils.*;
/**
* An adapter class for {@link Steerable}. You can derive from this and only override what you are interested in. For example,
* this comes in handy when you have to create on the fly a target for a particular behavior.
* @author davebaol
*/
public class SteerableAdapter implements Steerable{
@Override
public float getZeroLinearSpeedThreshold(){
return 0.001f;
}
@Override
public void setZeroLinearSpeedThreshold(float value){
}
@Override
public float getMaxLinearSpeed(){
return 0;
}
@Override
public void setMaxLinearSpeed(float maxLinearSpeed){
}
@Override
public float getMaxLinearAcceleration(){
return 0;
}
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration){
}
@Override
public float getMaxAngularSpeed(){
return 0;
}
@Override
public void setMaxAngularSpeed(float maxAngularSpeed){
}
@Override
public float getMaxAngularAcceleration(){
return 0;
}
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration){
}
@Override
public Vec2 getPosition(){
return null;
}
@Override
public float getOrientation(){
return 0;
}
@Override
public void setOrientation(float orientation){
}
@Override
public Vec2 getLinearVelocity(){
return null;
}
@Override
public float getAngularVelocity(){
return 0;
}
@Override
public float getBoundingRadius(){
return 0;
}
@Override
public boolean isTagged(){
return false;
}
@Override
public void setTagged(boolean tagged){
}
@Override
public Location newLocation(){
return null;
}
@Override
public float vectorToAngle(Vec2 vector){
return 0;
}
@Override
public Vec2 angleToVector(Vec2 outVector, float angle){
return null;
}
}

View File

@@ -0,0 +1,95 @@
package mindustry.ai.ai.steer;
import arc.math.geom.*;
/**
* {@code SteeringAcceleration} is a movement requested by the steering system. It is made up of two components, linear and angular
* acceleration.
* @author davebaol
*/
public class SteeringAcceleration{
/** The linear component of this steering acceleration. */
public Vec2 linear;
/** The angular component of this steering acceleration. */
public float angular;
/**
* Creates a {@code SteeringAcceleration} with the given linear acceleration and zero angular acceleration.
* @param linear The initial linear acceleration to give this SteeringAcceleration.
*/
public SteeringAcceleration(Vec2 linear){
this(linear, 0f);
}
/**
* Creates a {@code SteeringAcceleration} with the given linear and angular components.
* @param linear The initial linear acceleration to give this SteeringAcceleration.
* @param angular The initial angular acceleration to give this SteeringAcceleration.
*/
public SteeringAcceleration(Vec2 linear, float angular){
if(linear == null) throw new IllegalArgumentException("Linear acceleration cannot be null");
this.linear = linear;
this.angular = angular;
}
/** Returns {@code true} if both linear and angular components of this steering acceleration are zero; {@code false} otherwise. */
public boolean isZero(){
return angular == 0 && linear.isZero();
}
/**
* Zeros the linear and angular components of this steering acceleration.
* @return this steering acceleration for chaining
*/
public SteeringAcceleration setZero(){
linear.setZero();
angular = 0f;
return this;
}
/**
* Adds the given steering acceleration to this steering acceleration.
* @param steering the steering acceleration
* @return this steering acceleration for chaining
*/
public SteeringAcceleration add(SteeringAcceleration steering){
linear.add(steering.linear);
angular += steering.angular;
return this;
}
/**
* Scales this steering acceleration by the specified scalar.
* @param scalar the scalar
* @return this steering acceleration for chaining
*/
public SteeringAcceleration scl(float scalar){
linear.scl(scalar);
angular *= scalar;
return this;
}
/**
* First scale a supplied steering acceleration, then add it to this steering acceleration.
* @param steering the steering acceleration
* @param scalar the scalar
* @return this steering acceleration for chaining
*/
public SteeringAcceleration mulAdd(SteeringAcceleration steering, float scalar){
linear.mulAdd(steering.linear, scalar);
angular += steering.angular * scalar;
return this;
}
/** Returns the square of the magnitude of this steering acceleration. This includes the angular component. */
public float calculateSquareMagnitude(){
return linear.len2() + angular * angular;
}
/** Returns the magnitude of this steering acceleration. This includes the angular component. */
public float calculateMagnitude(){
return (float)Math.sqrt(calculateSquareMagnitude());
}
}

View File

@@ -0,0 +1,138 @@
package mindustry.ai.ai.steer;
import arc.math.geom.*;
import mindustry.ai.ai.utils.*;
/**
* A {@code SteeringBehavior} calculates the linear and/or angular accelerations to be applied to its owner.
* @author davebaol
*/
public abstract class SteeringBehavior{
/** The owner of this steering behavior */
protected Steerable owner;
/** The limiter of this steering behavior */
protected Limiter limiter;
/** A flag indicating whether this steering behavior is enabled or not. */
protected boolean enabled;
/**
* Creates a {@code SteeringBehavior} for the specified owner. The behavior is enabled and has no explicit limiter, meaning
* that the owner is used instead.
* @param owner the owner of this steering behavior
*/
public SteeringBehavior(Steerable owner){
this(owner, null, true);
}
/**
* Creates a {@code SteeringBehavior} for the specified owner and limiter. The behavior is enabled.
* @param owner the owner of this steering behavior
* @param limiter the limiter of this steering behavior
*/
public SteeringBehavior(Steerable owner, Limiter limiter){
this(owner, limiter, true);
}
/**
* Creates a {@code SteeringBehavior} for the specified owner and activation flag. The behavior has no explicit limiter,
* meaning that the owner is used instead.
* @param owner the owner of this steering behavior
* @param enabled a flag indicating whether this steering behavior is enabled or not
*/
public SteeringBehavior(Steerable owner, boolean enabled){
this(owner, null, enabled);
}
/**
* Creates a {@code SteeringBehavior} for the specified owner, limiter and activation flag.
* @param owner the owner of this steering behavior
* @param limiter the limiter of this steering behavior
* @param enabled a flag indicating whether this steering behavior is enabled or not
*/
public SteeringBehavior(Steerable owner, Limiter limiter, boolean enabled){
this.owner = owner;
this.limiter = limiter;
this.enabled = enabled;
}
/**
* If this behavior is enabled calculates the steering acceleration and writes it to the given steering output. If it is
* disabled the steering output is set to zero.
* @param steering the steering acceleration to be calculated.
* @return the calculated steering acceleration for chaining.
*/
public SteeringAcceleration calculateSteering(SteeringAcceleration steering){
return isEnabled() ? calculateRealSteering(steering) : steering.setZero();
}
/**
* Calculates the steering acceleration produced by this behavior and writes it to the given steering output.
* <p>
* This method is called by {@link #calculateSteering(SteeringAcceleration)} when this steering behavior is enabled.
* @param steering the steering acceleration to be calculated.
* @return the calculated steering acceleration for chaining.
*/
protected abstract SteeringAcceleration calculateRealSteering(SteeringAcceleration steering);
/** Returns the owner of this steering behavior. */
public Steerable getOwner(){
return owner;
}
/**
* Sets the owner of this steering behavior.
* @return this behavior for chaining.
*/
public SteeringBehavior setOwner(Steerable owner){
this.owner = owner;
return this;
}
/** Returns the limiter of this steering behavior. */
public Limiter getLimiter(){
return limiter;
}
/**
* Sets the limiter of this steering behavior.
* @return this behavior for chaining.
*/
public SteeringBehavior setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
/** Returns true if this steering behavior is enabled; false otherwise. */
public boolean isEnabled(){
return enabled;
}
/**
* Sets this steering behavior on/off.
* @return this behavior for chaining.
*/
public SteeringBehavior setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/** Returns the actual limiter of this steering behavior. */
protected Limiter getActualLimiter(){
return limiter == null ? owner : limiter;
}
/**
* Utility method that creates a new vector.
* <p>
* This method is used internally to instantiate vectors of the correct type parameter {@code T}. This technique keeps the API
* simple and makes the API easier to use with the GWVec2 backend because avoids the use of reflection.
* @param location the location whose position is used to create the new vector
* @return the newly created vector
*/
protected Vec2 newVector(Location location){
return location.getPosition().cpy().setZero();
}
}

View File

@@ -0,0 +1,83 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.Proximity.*;
/**
* {@code Alignment} is a group behavior producing a linear acceleration that attempts to keep the owner aligned with the agents in
* its immediate area defined by the given {@link Proximity}. The acceleration is calculated by first iterating through all the
* neighbors and averaging their linear velocity vectors. This value is the desired direction, so we just subtract the owner's
* linear velocity to get the steering output.
* <p>
* Cars moving along roads demonstrate {@code Alignment} type behavior. They also demonstrate {@link Separation} as they try to
* keep a minimum distance from each other.
* @author davebaol
*/
public class Alignment extends GroupBehavior implements ProximityCallback{
private Vec2 averageVelocity;
/**
* Creates an {@code Alignment} behavior for the specified owner and proximity.
* @param owner the owner of this behavior
* @param proximity the proximity
*/
public Alignment(Steerable owner, Proximity proximity){
super(owner, proximity);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
steering.setZero();
averageVelocity = steering.linear;
int neighborCount = proximity.findNeighbors(this);
if(neighborCount > 0){
// Average the accumulated velocities
averageVelocity.scl(1f / neighborCount);
// Match the average velocity.
// Notice that steering.linear and averageVelocity are the same vector here.
averageVelocity.sub(owner.getLinearVelocity()).limit(getActualLimiter().getMaxLinearAcceleration());
}
return steering;
}
@Override
public boolean reportNeighbor(Steerable neighbor){
// Accumulate neighbor velocity
averageVelocity.add(neighbor.getLinearVelocity());
return true;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Alignment setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Alignment setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Alignment setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,180 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code Arrive} behavior moves the agent towards a target position. It is similar to seek but it attempts to arrive at the target
* position with a zero velocity.
* <p>
* {@code Arrive} behavior uses two radii. The {@code arrivalTolerance} lets the owner get near enough to the target without
* letting small errors keep it in motion. The {@code decelerationRadius}, usually much larger than the previous one, specifies
* when the incoming character will begin to slow down. The algorithm calculates an ideal speed for the owner. At the slowing-down
* radius, this is equal to its maximum linear speed. At the target point, it is zero (we want to have zero speed when we arrive).
* In between, the desired speed is an interpolated intermediate value, controlled by the distance from the target.
* <p>
* The direction toward the target is calculated and combined with the desired speed to give a target velocity. The algorithm
* looks at the current velocity of the character and works out the acceleration needed to turn it into the target velocity. We
* can't immediately change velocity, however, so the acceleration is calculated based on reaching the target velocity in a fixed
* time scale known as {@code timeToTarget}. This is usually a small value; it defaults to 0.1 seconds which is a good starting
* point.
* @author davebaol
*/
public class Arrive extends SteeringBehavior{
/** The target to arrive to. */
protected Location target;
/**
* The tolerance for arriving at the target. It lets the owner get near enough to the target without letting small errors keep
* it in motion.
*/
protected float arrivalTolerance;
/** The radius for beginning to slow down */
protected float decelerationRadius;
/** The time over which to achieve target speed */
protected float timeToTarget = 0.1f;
/**
* Creates an {@code Arrive} behavior for the specified owner.
* @param owner the owner of this behavior
*/
public Arrive(Steerable owner){
this(owner, null);
}
/**
* Creates an {@code Arrive} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target of this behavior
*/
public Arrive(Steerable owner, Location target){
super(owner);
this.target = target;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
return arrive(steering, target.getPosition());
}
protected SteeringAcceleration arrive(SteeringAcceleration steering, Vec2 targetPosition){
// Get the direction and distance to the target
Vec2 toTarget = steering.linear.set(targetPosition).sub(owner.getPosition());
float distance = toTarget.len();
// Check if we are there, return no steering
if(distance <= arrivalTolerance) return steering.setZero();
Limiter actualLimiter = getActualLimiter();
// Go max speed
float targetSpeed = actualLimiter.getMaxLinearSpeed();
// If we are inside the slow down radius calculate a scaled speed
if(distance <= decelerationRadius) targetSpeed *= distance / decelerationRadius;
// Target velocity combines speed and direction
Vec2 targetVelocity = toTarget.scl(targetSpeed / distance); // Optimized code for: toTarget.nor().scl(targetSpeed)
// Acceleration tries to get to the target velocity without exceeding max acceleration
// Notice that steering.linear and targetVelocity are the same vector
targetVelocity.sub(owner.getLinearVelocity()).scl(1f / timeToTarget).limit(actualLimiter.getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0f;
// Output the steering
return steering;
}
/** Returns the target to arrive to. */
public Location getTarget(){
return target;
}
/**
* Sets the target to arrive to.
* @return this behavior for chaining.
*/
public Arrive setTarget(Location target){
this.target = target;
return this;
}
/**
* Returns the tolerance for arriving at the target. It lets the owner get near enough to the target without letting small
* errors keep it in motion.
*/
public float getArrivalTolerance(){
return arrivalTolerance;
}
/**
* Sets the tolerance for arriving at the target. It lets the owner get near enough to the target without letting small errors
* keep it in motion.
* @return this behavior for chaining.
*/
public Arrive setArrivalTolerance(float arrivalTolerance){
this.arrivalTolerance = arrivalTolerance;
return this;
}
/** Returns the radius for beginning to slow down. */
public float getDecelerationRadius(){
return decelerationRadius;
}
/**
* Sets the radius for beginning to slow down.
* @return this behavior for chaining.
*/
public Arrive setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
/** Returns the time over which to achieve target speed. */
public float getTimeToTarget(){
return timeToTarget;
}
/**
* Sets the time over which to achieve target speed.
* @return this behavior for chaining.
*/
public Arrive setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Arrive setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Arrive setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear speed and
* acceleration.
* @return this behavior for chaining.
*/
@Override
public Arrive setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,181 @@
package mindustry.ai.ai.steer.behaviors;
import arc.struct.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.limiters.*;
/**
* This combination behavior simply sums up all the behaviors, applies their weights, and truncates the result before returning.
* There are no constraints on the blending weights; they don't have to sum to one, for example, and rarely do. Don't think of
* {@code BlendedSteering} as a weighted mean, because it's not.
* <p>
* With {@code BlendedSteering} you can combine multiple behaviors to get a more complex behavior. It can work fine, but the
* trade-off is that it comes with a few problems:
* <ul>
* <li>Since every active behavior is calculated every time step, it can be a costly method to process.</li>
* <li>Behavior weights can be difficult to tweak. There have been research projects that have tried to evolve the steering
* weights using genetic algorithms or neural networks. Results have not been encouraging, however, and manual experimentation
* still seems to be the most sensible approach.</li>
* <li>It's problematic with conflicting forces. For instance, a common scenario is where an agent is backed up against a wall by
* several other agents. In this example, the separating forces from the neighboring agents can be greater than the repulsive
* force from the wall and the agent can end up being pushed through the wall boundary. This is almost certainly not going to be
* favorable. Sure you can make the weights for the wall avoidance huge, but then your agent may behave strangely next time it
* finds itself alone and next to a wall.</li>
* </ul>
* @author davebaol
*/
public class BlendedSteering extends SteeringBehavior{
/** The list of behaviors and their corresponding blending weights. */
protected Array<BehaviorAndWeight> list;
private SteeringAcceleration steering;
/**
* Creates a {@code BlendedSteering} for the specified {@code owner}, {@code maxLinearAcceleration} and
* {@code maxAngularAcceleration}.
* @param owner the owner of this behavior.
*/
public BlendedSteering(Steerable owner){
super(owner);
this.list = new Array<>();
this.steering = new SteeringAcceleration(newVector(owner));
}
/**
* Adds a steering behavior and its weight to the list.
* @param behavior the steering behavior to add
* @param weight the weight of the behavior
* @return this behavior for chaining.
*/
public BlendedSteering add(SteeringBehavior behavior, float weight){
return add(new BehaviorAndWeight(behavior, weight));
}
/**
* Adds a steering behavior and its weight to the list.
* @param item the steering behavior and its weight
* @return this behavior for chaining.
*/
public BlendedSteering add(BehaviorAndWeight item){
item.behavior.setOwner(owner);
list.add(item);
return this;
}
/**
* Removes a steering behavior from the list.
* @param item the steering behavior to remove
*/
public void remove(BehaviorAndWeight item){
list.remove(item, true);
}
/**
* Removes a steering behavior from the list.
* @param behavior the steering behavior to remove
*/
public void remove(SteeringBehavior behavior){
for(int i = 0; i < list.size; i++){
if(list.get(i).behavior == behavior){
list.remove(i);
return;
}
}
}
/**
* Returns the weighted behavior at the specified index.
* @param index the index of the weighted behavior to return
*/
public BehaviorAndWeight get(int index){
return list.get(index);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration blendedSteering){
// Clear the output to start with
blendedSteering.setZero();
// Go through all the behaviors
int len = list.size;
for(int i = 0; i < len; i++){
BehaviorAndWeight bw = list.get(i);
// Calculate the behavior's steering
bw.behavior.calculateSteering(steering);
// Scale and add the steering to the accumulator
blendedSteering.mulAdd(steering, bw.weight);
}
Limiter actualLimiter = getActualLimiter();
// Crop the result
blendedSteering.linear.limit(actualLimiter.getMaxLinearAcceleration());
if(blendedSteering.angular > actualLimiter.getMaxAngularAcceleration())
blendedSteering.angular = actualLimiter.getMaxAngularAcceleration();
return blendedSteering;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public BlendedSteering setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public BlendedSteering setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear and angular
* accelerations. You can use {@link NullLimiter#NEUTRAL_LIMITER} to avoid all truncations.
* @return this behavior for chaining.
*/
@Override
public BlendedSteering setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
//
// Nested classes
//
public static class BehaviorAndWeight{
protected SteeringBehavior behavior;
protected float weight;
public BehaviorAndWeight(SteeringBehavior behavior, float weight){
this.behavior = behavior;
this.weight = weight;
}
public SteeringBehavior getBehavior(){
return behavior;
}
public void setBehavior(SteeringBehavior behavior){
this.behavior = behavior;
}
public float getWeight(){
return weight;
}
public void setWeight(float weight){
this.weight = weight;
}
}
}

View File

@@ -0,0 +1,82 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.Proximity.*;
/**
* {@code Cohesion} is a group behavior producing a linear acceleration that attempts to move the agent towards the center of mass
* of the agents in its immediate area defined by the given {@link Proximity}. The acceleration is calculated by first iterating
* through all the neighbors and averaging their position vectors. This gives us the center of mass of the neighbors, the place
* the agents wants to get to, so it seeks to that position.
* <p>
* A sheep running after its flock is demonstrating cohesive behavior. Use this behavior to keep a group of agents together.
* @author davebaol
*/
public class Cohesion extends GroupBehavior implements ProximityCallback{
private Vec2 centerOfMass;
/**
* Creates a {@code Cohesion} for the specified owner and proximity.
* @param owner the owner of this behavior.
* @param proximity the proximity to detect the owner's neighbors
*/
public Cohesion(Steerable owner, Proximity proximity){
super(owner, proximity);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
steering.setZero();
centerOfMass = steering.linear;
int neighborCount = proximity.findNeighbors(this);
if(neighborCount > 0){
// The center of mass is the average of the sum of positions
centerOfMass.scl(1f / neighborCount);
// Now seek towards that position.
centerOfMass.sub(owner.getPosition()).nor().scl(getActualLimiter().getMaxLinearAcceleration());
}
return steering;
}
@Override
public boolean reportNeighbor(Steerable neighbor){
// Accumulate neighbor position
centerOfMass.add(neighbor.getPosition());
return true;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Cohesion setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Cohesion setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Cohesion setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,146 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.Proximity.*;
/**
* {@code CollisionAvoidance} behavior steers the owner to avoid obstacles lying in its path. An obstacle is any object that can be
* approximated by a circle (or sphere, if you are working in 3D).
* <p>
* This implementation uses collision prediction working out the closest approach of two agents and determining if their distance
* at this point is less than the sum of their bounding radius. For avoiding groups of characters, averaging positions and
* velocities do not work well with this approach. Instead, the algorithm needs to search for the character whose closest approach
* will occur first and to react to this character only. Once this imminent collision is avoided, the steering behavior can then
* react to more distant characters.
* <p>
* This algorithm works well with small and/or moving obstacles whose shape can be approximately represented by a center and a
* radius.
* @author davebaol
*/
public class CollisionAvoidance extends GroupBehavior implements ProximityCallback{
private float shortestTime;
private Steerable firstNeighbor;
private float firstMinSeparation;
private float firstDistance;
private Vec2 firstRelativePosition;
private Vec2 firstRelativeVelocity;
private Vec2 relativePosition;
private Vec2 relativeVelocity;
/**
* Creates a {@code CollisionAvoidance} behavior for the specified owner and proximity.
* @param owner the owner of this behavior
* @param proximity the proximity of this behavior.
*/
public CollisionAvoidance(Steerable owner, Proximity proximity){
super(owner, proximity);
this.firstRelativePosition = newVector(owner);
this.firstRelativeVelocity = newVector(owner);
this.relativeVelocity = newVector(owner);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
shortestTime = Float.POSITIVE_INFINITY;
firstNeighbor = null;
firstMinSeparation = 0;
firstDistance = 0;
relativePosition = steering.linear;
// Take into consideration each neighbor to find the most imminent collision.
int neighborCount = proximity.findNeighbors(this);
// If we have no target, then return no steering acceleration
//
// NOTE: You might think that the condition below always evaluates to true since
// firstNeighbor has been set to null when entering this method. In fact, we have just
// executed findNeighbors(this) that has possibly set firstNeighbor to a non null value
// through the method reportNeighbor defined below.
if(neighborCount == 0 || firstNeighbor == null) return steering.setZero();
// If we're going to hit exactly, or if we're already
// colliding, then do the steering based on current position.
if(firstMinSeparation <= 0 || firstDistance < owner.getBoundingRadius() + firstNeighbor.getBoundingRadius()){
relativePosition.set(firstNeighbor.getPosition()).sub(owner.getPosition());
}else{
// Otherwise calculate the future relative position
relativePosition.set(firstRelativePosition).mulAdd(firstRelativeVelocity, shortestTime);
}
// Avoid the target
// Notice that steerling.linear and relativePosition are the same vector
relativePosition.nor().scl(-getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0f;
// Output the steering
return steering;
}
@Override
public boolean reportNeighbor(Steerable neighbor){
// Calculate the time to collision
relativePosition.set(neighbor.getPosition()).sub(owner.getPosition());
relativeVelocity.set(neighbor.getLinearVelocity()).sub(owner.getLinearVelocity());
float relativeSpeed2 = relativeVelocity.len2();
// Collision can't happen when the agents have the same linear velocity.
// Also, note that timeToCollision would be NaN due to the indeterminate form 0/0 and,
// since any comparison involving NaN returns false, it would become the shortestTime,
// so defeating the algorithm.
if(relativeSpeed2 == 0) return false;
float timeToCollision = -relativePosition.dot(relativeVelocity) / relativeSpeed2;
// If timeToCollision is negative, i.e. the owner is already moving away from the the neighbor,
// or it's not the most imminent collision then no action needs to be taken.
if(timeToCollision <= 0 || timeToCollision >= shortestTime) return false;
// Check if it is going to be a collision at all
float distance = relativePosition.len();
float minSeparation = distance - (float)Math.sqrt(relativeSpeed2) * timeToCollision /* shortestTime */;
if(minSeparation > owner.getBoundingRadius() + neighbor.getBoundingRadius()) return false;
// Store most imminent collision data
shortestTime = timeToCollision;
firstNeighbor = neighbor;
firstMinSeparation = minSeparation;
firstDistance = distance;
firstRelativePosition.set(relativePosition);
firstRelativeVelocity.set(relativeVelocity);
return true;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public CollisionAvoidance setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public CollisionAvoidance setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public CollisionAvoidance setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,70 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
/**
* {@code Evade} behavior is almost the same as {@link Pursue} except that the agent flees from the estimated future position of
* the pursuer. Indeed, reversing the acceleration is all we have to do.
* @author davebaol
*/
public class Evade extends Pursue{
/**
* Creates a {@code Evade} behavior for the specified owner and target. Maximum prediction time defaults to 1 second.
* @param owner the owner of this behavior
* @param target the target of this behavior, typically a pursuer.
*/
public Evade(Steerable owner, Steerable target){
this(owner, target, 1);
}
/**
* Creates a {@code Evade} behavior for the specified owner and pursuer.
* @param owner the owner of this behavior
* @param target the target of this behavior, typically a pursuer
* @param maxPredictionTime the max time used to predict the pursuer's position assuming it continues to move with its current
* velocity.
*/
public Evade(Steerable owner, Steerable target, float maxPredictionTime){
super(owner, target, maxPredictionTime);
}
@Override
protected float getActualMaxLinearAcceleration(){
// Simply return the opposite of the max linear acceleration so to evade the target
return -getActualLimiter().getMaxLinearAcceleration();
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Evade setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Evade setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Evade setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public Evade setTarget(Steerable target){
this.target = target;
return this;
}
}

View File

@@ -0,0 +1,101 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code Face} behavior makes the owner look at its target. It delegates to the {@link ReachOrientation} behavior to perform the
* rotation but calculates the target orientation first based on target and owner position.
* @author davebaol
*/
public class Face extends ReachOrientation{
/**
* Creates a {@code Face} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public Face(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code Face} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target of this behavior.
*/
public Face(Steerable owner, Location target){
super(owner, target);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
return face(steering, target.getPosition());
}
protected SteeringAcceleration face(SteeringAcceleration steering, Vec2 targetPosition){
// Get the direction to target
Vec2 toTarget = steering.linear.set(targetPosition).sub(owner.getPosition());
// Check for a zero direction, and return no steering if so
if(toTarget.isZero(getActualLimiter().getZeroLinearSpeedThreshold())) return steering.setZero();
// Calculate the orientation to face the target
float orientation = owner.vectorToAngle(toTarget);
// Delegate to ReachOrientation
return reachOrientation(steering, orientation);
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Face setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Face setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum angular speed and
* acceleration.
* @return this behavior for chaining.
*/
@Override
public Face setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public Face setTarget(Location target){
this.target = target;
return this;
}
@Override
public Face setAlignTolerance(float alignTolerance){
this.alignTolerance = alignTolerance;
return this;
}
@Override
public Face setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public Face setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,75 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code Flee} behavior does the opposite of {@link Seek}. It produces a linear steering force that moves the agent away from a
* target position.
* @author davebaol
*/
public class Flee extends Seek{
/**
* Creates a {@code Flee} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public Flee(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code Flee} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target agent of this behavior.
*/
public Flee(Steerable owner, Location target){
super(owner, target);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// We just do the opposite of seek, i.e. (owner.getPosition() - target.getPosition())
// instead of (target.getPosition() - owner.getPosition())
steering.linear.set(owner.getPosition()).sub(target.getPosition()).nor().scl(getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Flee setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Flee setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Flee setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public Flee setTarget(Location target){
this.target = target;
return this;
}
}

View File

@@ -0,0 +1,153 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
/**
* The {@code FollowFlowField} behavior produces a linear acceleration that tries to align the motion of the owner with the local
* tangent of a flow field. The flow field defines a mapping from a location in space to a flow vector. Any flow field can be used
* as the basis of this steering behavior, although it is sensitive to discontinuities in the field.
* <p>
* For instance, flow fields can be used for simulating various effects, such as magnetic fields, an irregular gust of wind or the
* meandering path of a river. They can be generated by a simple random algorithm, a Perlin noise or a complicated image
* processing. And of course flow fields can be dynamic. The only limit is your imagination.
* <p>
* Like {@link FollowPath}, this behavior can work in a predictive manner when its {@code predictionTime} is greater than 0.
* @author davebaol
*/
public class FollowFlowField extends SteeringBehavior{
/** The flow field to follow. */
protected FlowField flowField;
/** The time in the future to predict the owner's position. Set it to 0 for non-predictive flow field following. */
protected float predictionTime;
/**
* Creates a non-predictive {@code FollowFlowField} for the specified owner.
* @param owner the owner of this behavior
*/
public FollowFlowField(Steerable owner){
this(owner, null);
}
/**
* Creates a non-predictive {@code FollowFlowField} for the specified owner and flow field. Prediction time defaults to 0.
* @param owner the owner of this behavior
* @param flowField the flow field to follow
*/
public FollowFlowField(Steerable owner, FlowField flowField){
this(owner, flowField, 0);
}
/**
* Creates a {@code FollowFlowField} with the specified owner, flow field and prediction time.
* @param owner the owner of this behavior
* @param flowField the flow field to follow
* @param predictionTime the time in the future to predict the owner's position. Can be 0 for non-predictive flow field
* following.
*/
public FollowFlowField(Steerable owner, FlowField flowField, float predictionTime){
super(owner);
this.flowField = flowField;
this.predictionTime = predictionTime;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Predictive or non-predictive behavior?
Vec2 location = (predictionTime == 0) ?
// Use the current position of the owner
owner.getPosition()
:
// Calculate the predicted future position of the owner. We're reusing steering.linear here.
steering.linear.set(owner.getPosition()).mulAdd(owner.getLinearVelocity(), predictionTime);
// Retrieve the flow vector at the specified location
Vec2 flowVector = flowField.lookup(location);
// Clear both linear and angular components
steering.setZero();
if(flowVector != null && !flowVector.isZero()){
Limiter actualLimiter = getActualLimiter();
// Calculate linear acceleration
steering.linear.mulAdd(flowVector, actualLimiter.getMaxLinearSpeed()).sub(owner.getLinearVelocity())
.limit(actualLimiter.getMaxLinearAcceleration());
}
// Output steering
return steering;
}
/** Returns the flow field of this behavior */
public FlowField getFlowField(){
return flowField;
}
/**
* Sets the flow field of this behavior
* @param flowField the flow field to set
* @return this behavior for chaining
*/
public FollowFlowField setFlowField(FlowField flowField){
this.flowField = flowField;
return this;
}
/** Returns the prediction time. */
public float getPredictionTime(){
return predictionTime;
}
/**
* Sets the prediction time. Set it to 0 for non-predictive flow field following.
* @param predictionTime the predictionTime to set
* @return this behavior for chaining.
*/
public FollowFlowField setPredictionTime(float predictionTime){
this.predictionTime = predictionTime;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public FollowFlowField setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public FollowFlowField setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear speed and
* acceleration.
* @return this behavior for chaining.
*/
@Override
public FollowFlowField setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
/**
* A {@code FlowField} defines a mapping from a location in space to a flow vector. Typically flow fields are implemented as a
* multidimensional array representing a grid of cells. In each cell of the grid lives a flow vector.
* @author davebaol
*/
public interface FlowField{
/**
* Returns the flow vector for the specified position in space.
* @param position the position to map
*/
Vec2 lookup(Vec2 position);
}
}

View File

@@ -0,0 +1,247 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.utils.Path;
import mindustry.ai.ai.steer.utils.Path.*;
import mindustry.ai.ai.utils.*;
/**
* {@code FollowPath} behavior produces a linear acceleration that moves the agent along the given path. First it calculates the
* agent location based on the specified prediction time. Then it works out the position of the internal target based on the
* location just calculated and the shape of the path. It finally uses {@link Seek seek} behavior to move the owner towards the
* internal target position. However, if the path is open {@link Arrive arrive} behavior is used to approach path's extremities
* when they are far less than the {@link FollowPath#decelerationRadius deceleration radius} from the internal target position.
* <p>
* For complex paths with sudden changes of direction the predictive behavior (i.e., with prediction time greater than 0) can
* appear smoother than the non-predictive one (i.e., with no prediction time). However, predictive path following has the
* downside of cutting corners when some sections of the path come close together. This cutting-corner attitude can make the
* character miss a whole section of the path. This might not be what you want if, for example, the path represents a patrol
* route.
* @param <P> Type of path parameter implementing the {@link PathParam} interface
* @author davebaol
*/
public class FollowPath<P extends PathParam> extends Arrive{
/** The path to follow */
protected Path<P> path;
/** The distance along the path to generate the target. Can be negative if the owner has to move along the reverse direction. */
protected float pathOffset;
/** The current position on the path */
protected P pathParam;
/** The flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. It defaults to {@code true}. */
protected boolean arriveEnabled;
/** The time in the future to predict the owner's position. Set it to 0 for non-predictive path following. */
protected float predictionTime;
private Vec2 internalTargetPosition;
/**
* Creates a non-predictive {@code FollowPath} behavior for the specified owner and path.
* @param owner the owner of this behavior
* @param path the path to be followed by the owner.
*/
public FollowPath(Steerable owner, Path<P> path){
this(owner, path, 0);
}
/**
* Creates a non-predictive {@code FollowPath} behavior for the specified owner, path and path offset.
* @param owner the owner of this behavior
* @param path the path to be followed by the owner
* @param pathOffset the distance along the path to generate the target. Can be negative if the owner is to move along the
* reverse direction.
*/
public FollowPath(Steerable owner, Path<P> path, float pathOffset){
this(owner, path, pathOffset, 0);
}
/**
* Creates a {@code FollowPath} behavior for the specified owner, path, path offset, maximum linear acceleration and prediction
* time.
* @param owner the owner of this behavior
* @param path the path to be followed by the owner
* @param pathOffset the distance along the path to generate the target. Can be negative if the owner is to move along the
* reverse direction.
* @param predictionTime the time in the future to predict the owner's position. Can be 0 for non-predictive path following.
*/
public FollowPath(Steerable owner, Path<P> path, float pathOffset, float predictionTime){
super(owner);
this.path = path;
this.pathParam = path.createParam();
this.pathOffset = pathOffset;
this.predictionTime = predictionTime;
this.arriveEnabled = true;
this.internalTargetPosition = newVector(owner);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Predictive or non-predictive behavior?
Vec2 location = (predictionTime == 0) ?
// Use the current position of the owner
owner.getPosition()
:
// Calculate the predicted future position of the owner. We're reusing steering.linear here.
steering.linear.set(owner.getPosition()).mulAdd(owner.getLinearVelocity(), predictionTime);
// Find the distance from the start of the path
float distance = path.calculateDistance(location, pathParam);
// Offset it
float targetDistance = distance + pathOffset;
// Calculate the target position
path.calculateTargetPosition(internalTargetPosition, pathParam, targetDistance);
if(arriveEnabled && path.isOpen()){
if(pathOffset >= 0){
// Use Arrive to approach the last point of the path
if(targetDistance > path.getLength() - decelerationRadius) return arrive(steering, internalTargetPosition);
}else{
// Use Arrive to approach the first point of the path
if(targetDistance < decelerationRadius) return arrive(steering, internalTargetPosition);
}
}
// Seek the target position
steering.linear.set(internalTargetPosition).sub(owner.getPosition()).nor()
.scl(getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
/** Returns the path to follow */
public Path<P> getPath(){
return path;
}
/**
* Sets the path followed by this behavior.
* @param path the path to set
* @return this behavior for chaining.
*/
public FollowPath<P> setPath(Path<P> path){
this.path = path;
return this;
}
/** Returns the path offset. */
public float getPathOffset(){
return pathOffset;
}
/** Returns the flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. */
public boolean isArriveEnabled(){
return arriveEnabled;
}
/** Returns the prediction time. */
public float getPredictionTime(){
return predictionTime;
}
/**
* Sets the prediction time. Set it to 0 for non-predictive path following.
* @param predictionTime the predictionTime to set
* @return this behavior for chaining.
*/
public FollowPath<P> setPredictionTime(float predictionTime){
this.predictionTime = predictionTime;
return this;
}
/**
* Sets the flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. It defaults to
* {@code true}.
* @param arriveEnabled the flag value to set
* @return this behavior for chaining.
*/
public FollowPath<P> setArriveEnabled(boolean arriveEnabled){
this.arriveEnabled = arriveEnabled;
return this;
}
/**
* Sets the path offset to generate the target. Can be negative if the owner has to move along the reverse direction.
* @param pathOffset the pathOffset to set
* @return this behavior for chaining.
*/
public FollowPath<P> setPathOffset(float pathOffset){
this.pathOffset = pathOffset;
return this;
}
/** Returns the current path parameter. */
public P getPathParam(){
return pathParam;
}
/** Returns the current position of the internal target. This method is useful for debug purpose. */
public Vec2 getInternalTargetPosition(){
return internalTargetPosition;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public FollowPath<P> setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public FollowPath<P> setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear speed and
* acceleration. However the maximum linear speed is not required for a closed path.
* @return this behavior for chaining.
*/
@Override
public FollowPath<P> setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public FollowPath<P> setTarget(Location target){
this.target = target;
return this;
}
@Override
public FollowPath<P> setArrivalTolerance(float arrivalTolerance){
this.arrivalTolerance = arrivalTolerance;
return this;
}
@Override
public FollowPath<P> setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public FollowPath<P> setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,209 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.Proximity.*;
import mindustry.ai.ai.steer.proximities.*;
import mindustry.ai.ai.utils.*;
/**
* This behavior attempts to position a owner so that an obstacle is always between itself and the agent (the hunter) it's trying
* to hide from. First the distance to each of these obstacles is determined. Then the owner uses the arrive behavior to steer
* toward the closest one. If no appropriate obstacles can be found, no steering is returned.
* <p>
* You can use this behavior not only for situations where you require a non-player character (NPC) to hide from the player, like
* find cover when fired at, but also in situations where you would like an NPC to sneak up on a player. For example, you can
* create an NPC capable of stalking a player through a gloomy forest, darting from tree to tree.
* <p>
* It's worth mentioning that since this behavior can produce no steering acceleration it is commonly used with
* {@link PrioritySteering}. For instance, to make the owner go away from the target if there are no obstacles nearby to hide
* behind, just use {@link Hide} and {@link Evade} behaviors with this priority order.
* <p>
* There are a few interesting modifications you might want to make to this behavior:
* <ul>
* <li>With {@link FieldOfViewProximity} you can allow the owner to hide only if the target is within its field of view. This
* tends to produce unsatisfactory performance though, because the owner starts to act like a child hiding from monsters beneath
* the bed sheets, something like "if you can't see it, then it can't see you" effect making the owner look dumb. This can be
* countered slightly though by adding in a time effect so that the owner will hide if the target is visible or if it has seen the
* target within the last {@code N} seconds. This gives it a sort of memory and produces reasonable-looking behavior.</li>
* <li>The same as above, but this time the owner only tries to hide if the owner can see the target and the target can see the
* owner.
* <li>It might be desirable to produce a force that steers the owner so that it always favors hiding positions that are to the
* side or rear of the pursuer. This can be achieved easily using the dot product to bias the distances returned from the method
* {@link #getHidingPosition}.</li>
* <li>At the beginning of any of the methods a check can be made to test if the target is within a "threat distance" before
* proceeding with any further calculations. If the target is not a threat, then the method can return immediately with zero
* steering.</li>
* </ul>
* @author davebaol
*/
public class Hide extends Arrive implements ProximityCallback{
/** The proximity to find nearby obstacles. */
protected Proximity proximity;
/** The distance from the boundary of the obstacle behind which to hide. */
protected float distanceFromBoundary;
private Vec2 toObstacle;
private Vec2 bestHidingSpot;
private float distance2ToClosest;
/**
* Creates an {@code Hide} behavior for the specified owner.
* @param owner the owner of this behavior
*/
public Hide(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code Hide} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target of this behavior
*/
public Hide(Steerable owner, Location target){
this(owner, target, null);
}
/**
* Creates a {@code Hide} behavior for the specified owner, target and proximity.
* @param owner the owner of this behavior
* @param target the target of this behavior
* @param proximity the proximity to find nearby obstacles
*/
public Hide(Steerable owner, Location target, Proximity proximity){
super(owner, target);
this.proximity = proximity;
this.bestHidingSpot = newVector(owner);
this.toObstacle = null; // Set to null since we'll reuse steering.linear for this vector
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Initialize member variables used by the callback
this.distance2ToClosest = Float.POSITIVE_INFINITY;
this.toObstacle = steering.linear;
// Find neighbors (the obstacles) using this behavior as callback
int neighborsCount = proximity.findNeighbors(this);
// If no suitable obstacles found return no steering otherwise use Arrive on the hiding spot
return neighborsCount == 0 ? steering.setZero() : arrive(steering, bestHidingSpot);
}
@Override
public boolean reportNeighbor(Steerable neighbor){
// Calculate the position of the hiding spot for this obstacle
Vec2 hidingSpot = getHidingPosition(neighbor.getPosition(), neighbor.getBoundingRadius(), target.getPosition());
// Work in distance-squared space to find the closest hiding
// spot to the owner
float distance2 = hidingSpot.dst2(owner.getPosition());
if(distance2 < distance2ToClosest){
distance2ToClosest = distance2;
bestHidingSpot.set(hidingSpot);
return true;
}
return false;
}
/** Returns the proximity used to find nearby obstacles. */
public Proximity getProximity(){
return proximity;
}
/**
* Sets the proximity used to find nearby obstacles.
* @param proximity the proximity to set
* @return this behavior for chaining.
*/
public Hide setProximity(Proximity proximity){
this.proximity = proximity;
return this;
}
/** Returns the distance from the boundary of the obstacle behind which to hide. */
public float getDistanceFromBoundary(){
return distanceFromBoundary;
}
/**
* Sets the distance from the boundary of the obstacle behind which to hide.
* @param distanceFromBoundary the distance to set
* @return this behavior for chaining.
*/
public Hide setDistanceFromBoundary(float distanceFromBoundary){
this.distanceFromBoundary = distanceFromBoundary;
return this;
}
/**
* Given the position of a target and the position and radius of an obstacle, this method calculates a position
* {@code distanceFromBoundary} away from the object's bounding radius and directly opposite the target. It does this by scaling
* the normalized "to obstacle" vector by the required distance away from the center of the obstacle and then adding the result
* to the obstacle's position.
* @return the hiding position behind the obstacle.
*/
protected Vec2 getHidingPosition(Vec2 obstaclePosition, float obstacleRadius, Vec2 targetPosition){
// Calculate how far away the agent is to be from the chosen
// obstacle's bounding radius
float distanceAway = obstacleRadius + distanceFromBoundary;
// Calculate the normalized vector toward the obstacle from the target
toObstacle.set(obstaclePosition).sub(targetPosition).nor();
// Scale it to size and add to the obstacle's position to get
// the hiding spot.
return toObstacle.scl(distanceAway).add(obstaclePosition);
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Hide setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Hide setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
@Override
public Hide setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public Hide setTarget(Location target){
this.target = target;
return this;
}
@Override
public Hide setArrivalTolerance(float arrivalTolerance){
this.arrivalTolerance = arrivalTolerance;
return this;
}
@Override
public Hide setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public Hide setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,182 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code Interpose} behavior produces a steering force that moves the owner to a point along the imaginary line connecting two
* other agents. A bodyguard taking a bullet for his employer or a soccer player intercepting a pass are examples of this type of
* behavior. Like {@code Pursue}, the owner must estimate where the two agents are going to be located at a time {@code t} in the
* future. It can then steer toward that position using the {@link Arrive} behavior. But how do we know what the best value of
* {@code t} is to use? The answer is, we don't, so we make a calculated guess instead.
* <p>
* The first step is to determine a point along the imaginary line connecting the positions of the agents at the current time
* step. This point is found taking into account the {@code interpositionRatio}, a number between 0 and 1 where 0 is the position
* of the first agent (agentA) and 1 is the position of the second agent (agentB). Values in between are interpolated intermediate
* locations.
* <p>
* Then the distance from this point is computed and the value divided by the owner's maximum speed to give the time {@code t}
* required to travel the distance.
* <p>
* Using the time {@code t}, the agents' positions are extrapolated into the future. The target position in between of these
* predicted positions is determined and finally the owner uses the Arrive behavior to steer toward that point.
* @author davebaol
*/
public class Interpose extends Arrive{
protected Steerable agentA;
protected Steerable agentB;
protected float interpositionRatio;
private Vec2 internalTargetPosition;
/**
* Creates an {@code Interpose} behavior for the specified owner and agents using the midpoint between agents as the target.
* @param owner the owner of this behavior
* @param agentA the first agent
* @param agentB the other agent
*/
public Interpose(Steerable owner, Steerable agentA, Steerable agentB){
this(owner, agentA, agentB, 0.5f);
}
/**
* Creates an {@code Interpose} behavior for the specified owner and agents using the the given interposing ratio.
* @param owner the owner of this behavior
* @param agentA the first agent
* @param agentB the other agent
* @param interpositionRatio a number between 0 and 1 indicating the percentage of the distance between the 2 agents that the
* owner should reach, where 0 is agentA position and 1 is agentB position.
*/
public Interpose(Steerable owner, Steerable agentA, Steerable agentB, float interpositionRatio){
super(owner);
this.agentA = agentA;
this.agentB = agentB;
this.interpositionRatio = interpositionRatio;
this.internalTargetPosition = newVector(owner);
}
/** Returns the first agent. */
public Steerable getAgentA(){
return agentA;
}
/**
* Sets the first agent.
* @return this behavior for chaining.
*/
public Interpose setAgentA(Steerable agentA){
this.agentA = agentA;
return this;
}
/** Returns the second agent. */
public Steerable getAgentB(){
return agentB;
}
/**
* Sets the second agent.
* @return this behavior for chaining.
*/
public Interpose setAgentB(Steerable agentB){
this.agentB = agentB;
return this;
}
/** Returns the interposition ratio. */
public float getInterpositionRatio(){
return interpositionRatio;
}
/**
* Sets the interposition ratio.
* @param interpositionRatio a number between 0 and 1 indicating the percentage of the distance between the 2 agents that the
* owner should reach. Especially, 0 is the position of agentA and 1 is the position of agentB.
* @return this behavior for chaining.
*/
public Interpose setInterpositionRatio(float interpositionRatio){
this.interpositionRatio = interpositionRatio;
return this;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// First we need to figure out where the two agents are going to be at
// time Vec2 in the future. This is approximated by determining the time
// taken by the owner to reach the desired point between the 2 agents
// at the current time at the max speed. This desired point P is given by
// P = posA + interpositionRatio * (posB - posA)
internalTargetPosition.set(agentB.getPosition()).sub(agentA.getPosition()).scl(interpositionRatio)
.add(agentA.getPosition());
float timeToTargetPosition = owner.getPosition().dst(internalTargetPosition) / getActualLimiter().getMaxLinearSpeed();
// Now we have the time, we assume that agent A and agent B will continue on a
// straight trajectory and extrapolate to get their future positions.
// Note that here we are reusing steering.linear vector as agentA future position
// and targetPosition as agentB future position.
steering.linear.set(agentA.getPosition()).mulAdd(agentA.getLinearVelocity(), timeToTargetPosition);
internalTargetPosition.set(agentB.getPosition()).mulAdd(agentB.getLinearVelocity(), timeToTargetPosition);
// Calculate the target position between these predicted positions
internalTargetPosition.sub(steering.linear).scl(interpositionRatio).add(steering.linear);
// Finally delegate to Arrive
return arrive(steering, internalTargetPosition);
}
/** Returns the current position of the internal target. This method is useful for debug purpose. */
public Vec2 getInternalTargetPosition(){
return internalTargetPosition;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Interpose setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Interpose setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
@Override
public Interpose setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
@Override
public Interpose setTarget(Location target){
this.target = target;
return this;
}
@Override
public Interpose setArrivalTolerance(float arrivalTolerance){
this.arrivalTolerance = arrivalTolerance;
return this;
}
@Override
public Interpose setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public Interpose setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,411 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
/**
* First the {@code Jump} behavior calculates the linear velocity required to achieve the jump. If the calculated velocity doesn't
* exceed the maximum linear velocity the jump is achievable; otherwise it's not. In either cases, the given callback gets
* informed through the {@link JumpCallback#reportAchievability(boolean) reportAchievability} method. Also, if the jump is
* achievable the run up phase begins and the {@code Jump} behavior will start to produce the linear acceleration required to match
* the calculated velocity. Once the jump point and the linear velocity are reached with a precision within the given tolerance
* the callback is told to jump through the {@link JumpCallback#takeoff(float, float) takeoff} method.
* @author davebaol
*/
public class Jump extends MatchVelocity{
/** The jump descriptor to use */
protected JumpDescriptor jumpDescriptor;
/**
* The gravity vector to use. Notice that this behavior only supports gravity along a single axis, which must be the one
* returned by the {@link GravityComponentHandler#getComponent(Vec2)} method.
*/
protected Vec2 gravity;
protected GravityComponentHandler gravityComponentHandler;
protected JumpCallback callback;
protected float takeoffPositionTolerance;
protected float takeoffVelocityTolerance;
/** The maximum vertical component of jump velocity, where "vertical" stands for the axis where gravity operates. */
protected float maxVerticalVelocity;
/** Keeps track of whether the jump is achievable */
private boolean isJumpAchievable;
protected float airborneTime = 0;
private JumpTarget jumpTarget;
private Vec2 planarVelocity;
/**
* Creates a {@code Jump} behavior.
* @param owner the owner of this behavior
* @param jumpDescriptor the descriptor of the jump to make
* @param gravity the gravity vector
* @param gravityComponentHandler the handler giving access to the vertical axis
* @param callback the callback that gets informed about jump achievability and when to jump
*/
public Jump(Steerable owner, JumpDescriptor jumpDescriptor, Vec2 gravity,
GravityComponentHandler gravityComponentHandler, JumpCallback callback){
super(owner);
this.gravity = gravity;
this.gravityComponentHandler = gravityComponentHandler;
setJumpDescriptor(jumpDescriptor);
this.callback = callback;
this.jumpTarget = new JumpTarget(owner);
this.planarVelocity = newVector(owner);
}
@Override
public SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Check if we have a trajectory, and create one if not.
if(target == null){
target = calculateTarget();
callback.reportAchievability(isJumpAchievable);
}
// If the trajectory is zero, return no steering acceleration
if(!isJumpAchievable) return steering.setZero();
// Check if the owner has reached target position and velocity with acceptable tolerance
if(owner.getPosition().epsilonEquals(target.getPosition(), takeoffPositionTolerance)){
if(owner.getLinearVelocity().epsilonEquals(target.getLinearVelocity(), takeoffVelocityTolerance)){
isJumpAchievable = false;
// Perform the jump, and return no steering (the owner is airborne, no need to steer).
callback.takeoff(maxVerticalVelocity, airborneTime);
return steering.setZero();
}
}
// Delegate to MatchVelocity
return super.calculateRealSteering(steering);
}
/** Works out the trajectory calculation. */
private Steerable calculateTarget(){
this.jumpTarget.position = jumpDescriptor.takeoffPosition;
this.airborneTime = calculateAirborneTimeAndVelocity(jumpTarget.linearVelocity, jumpDescriptor, getActualLimiter()
.getMaxLinearSpeed());
this.isJumpAchievable = airborneTime >= 0;
return jumpTarget;
}
/**
* Returns the airborne time and sets the {@code outVelocity} vector to the airborne planar velocity required to achieve the
* jump. If the jump is not achievable -1 is returned and the {@code outVelocity} vector remains unchanged.
* <p>
* Be aware that you should avoid using unlimited or very high max velocity, because this might produce a time of flight close
* to 0. Actually, the motion equation for Vec2 has 2 solutions and Jump always try to use the fastest time.
* @param outVelocity the output vector where the airborne planar velocity is calculated
* @param jumpDescriptor the jump descriptor
* @param maxLinearSpeed the maximum linear speed that can be used to achieve the jump
* @return the time of flight or -1 if the jump is not achievable using the given max linear speed.
*/
public float calculateAirborneTimeAndVelocity(Vec2 outVelocity, JumpDescriptor jumpDescriptor, float maxLinearSpeed){
float g = gravityComponentHandler.getComponent(gravity);
// Calculate the first jump time, see time of flight at http://hyperphysics.phy-astr.gsu.edu/hbase/traj.html
// Notice that the equation has 2 solutions. We'd ideally like to achieve the jump in the fastest time
// possible, so we want to use the smaller of the two values. However, this time value might give us
// an impossible launch velocity (required speed greater than the max), so we need to check and
// use the higher value if necessary.
float sqrtTerm = (float)Math.sqrt(2f * g * gravityComponentHandler.getComponent(jumpDescriptor.delta)
+ maxVerticalVelocity * maxVerticalVelocity);
float time = (-maxVerticalVelocity + sqrtTerm) / g;
// Check if we can use it
if(!checkAirborneTimeAndCalculateVelocity(outVelocity, time, jumpDescriptor, maxLinearSpeed)){
// Otherwise try the other time
time = (-maxVerticalVelocity - sqrtTerm) / g;
if(!checkAirborneTimeAndCalculateVelocity(outVelocity, time, jumpDescriptor, maxLinearSpeed)){
return -1f; // Unachievable jump
}
}
return time; // Achievable jump
}
private boolean checkAirborneTimeAndCalculateVelocity(Vec2 outVelocity, float time, JumpDescriptor jumpDescriptor,
float maxLinearSpeed){
// Calculate the planar velocity
planarVelocity.set(jumpDescriptor.delta).scl(1f / time);
gravityComponentHandler.setComponent(planarVelocity, 0f);
// Check the planar linear speed
if(planarVelocity.len2() < maxLinearSpeed * maxLinearSpeed){
// We have a valid solution, so store it by merging vertical and non-vertical axes
float verticalValue = gravityComponentHandler.getComponent(outVelocity);
gravityComponentHandler.setComponent(outVelocity.set(planarVelocity), verticalValue);
return true;
}
return false;
}
/** Returns the jump descriptor. */
public JumpDescriptor getJumpDescriptor(){
return jumpDescriptor;
}
/**
* Sets the jump descriptor to use.
* @param jumpDescriptor the jump descriptor to set
* @return this behavior for chaining.
*/
public Jump setJumpDescriptor(JumpDescriptor jumpDescriptor){
this.jumpDescriptor = jumpDescriptor;
this.target = null;
this.isJumpAchievable = false;
return this;
}
/** Returns the gravity vector. */
public Vec2 getGravity(){
return gravity;
}
/**
* Sets the gravity vector.
* @param gravity the gravity to set
* @return this behavior for chaining.
*/
public Jump setGravity(Vec2 gravity){
this.gravity = gravity;
return this;
}
/** Returns the maximum vertical component of jump velocity, where "vertical" stands for the axis where gravity operates. */
public float getMaxVerticalVelocity(){
return maxVerticalVelocity;
}
/**
* Sets the maximum vertical component of jump velocity, where "vertical" stands for the axis where gravity operates.
* @param maxVerticalVelocity the maximum vertical velocity to set
* @return this behavior for chaining.
*/
public Jump setMaxVerticalVelocity(float maxVerticalVelocity){
this.maxVerticalVelocity = maxVerticalVelocity;
return this;
}
/** Returns the tolerance used to check if the owner has reached the takeoff location. */
public float getTakeoffPositionTolerance(){
return takeoffPositionTolerance;
}
/**
* Sets the tolerance used to check if the owner has reached the takeoff location.
* @param takeoffPositionTolerance the takeoff position tolerance to set
* @return this behavior for chaining.
*/
public Jump setTakeoffPositionTolerance(float takeoffPositionTolerance){
this.takeoffPositionTolerance = takeoffPositionTolerance;
return this;
}
/** Returns the tolerance used to check if the owner has reached the takeoff velocity. */
public float getTakeoffVelocityTolerance(){
return takeoffVelocityTolerance;
}
/**
* Sets the tolerance used to check if the owner has reached the takeoff velocity.
* @param takeoffVelocityTolerance the takeoff velocity tolerance to set
* @return this behavior for chaining.
*/
public Jump setTakeoffVelocityTolerance(float takeoffVelocityTolerance){
this.takeoffVelocityTolerance = takeoffVelocityTolerance;
return this;
}
/**
* Sets the the tolerance used to check if the owner has reached the takeoff location with the required velocity.
* @param takeoffTolerance the takeoff tolerance for both position and velocity
* @return this behavior for chaining.
*/
public Jump setTakeoffTolerance(float takeoffTolerance){
setTakeoffPositionTolerance(takeoffTolerance);
setTakeoffVelocityTolerance(takeoffTolerance);
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Jump setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Jump setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration and
* speed.
* @return this behavior for chaining.
*/
@Override
public Jump setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
/**
* Sets the target whose velocity should be matched. Notice that this method is inherited from {@link MatchVelocity}. Usually
* with {@code Jump} you should never call it because a specialized internal target has already been created implicitly.
* @param target the target to set
* @return this behavior for chaining.
*/
@Override
public Jump setTarget(Steerable target){
this.target = target;
return this;
}
@Override
public Jump setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
//
// Nested classes and interfaces
//
private static class JumpTarget extends SteerableAdapter{
Vec2 position;
Vec2 linearVelocity;
public JumpTarget(Steerable other){
this.position = null;
this.linearVelocity = other.getPosition().cpy().setZero();
}
@Override
public Vec2 getPosition(){
return position;
}
@Override
public Vec2 getLinearVelocity(){
return linearVelocity;
}
}
/**
* A {@code JumpDescriptor} contains jump information like the take-off and the landing position.
* @author davebaol
*/
public static class JumpDescriptor{
/** The position of the takeoff pad */
public Vec2 takeoffPosition;
/** The position of the landing pad */
public Vec2 landingPosition;
/** The change in position from takeoff to landing. This is calculated from the other values. */
public Vec2 delta;
/**
* Creates a {@code JumpDescriptor} with the given takeoff and landing positions.
* @param takeoffPosition the position of the takeoff pad
* @param landingPosition the position of the landing pad
*/
public JumpDescriptor(Vec2 takeoffPosition, Vec2 landingPosition){
this.takeoffPosition = takeoffPosition;
this.landingPosition = landingPosition;
this.delta = landingPosition.cpy();
set(takeoffPosition, landingPosition);
}
/**
* Sets this {@code JumpDescriptor} from the given takeoff and landing positions.
* @param takeoffPosition the position of the takeoff pad
* @param landingPosition the position of the landing pad
*/
public void set(Vec2 takeoffPosition, Vec2 landingPosition){
this.takeoffPosition.set(takeoffPosition);
this.landingPosition.set(landingPosition);
this.delta.set(landingPosition).sub(takeoffPosition);
}
}
/**
* A {@code GravityComponentHandler} is aware of the axis along which the gravity acts.
* @author davebaol
*/
public interface GravityComponentHandler{
/**
* Returns the component of the given vector along which the gravity operates.
* <p>
* Assuming a 3D coordinate system where the gravity is acting along the y-axis, this method will be implemented as follows:
*
* <pre>
* public float getComponent (Vector3 vector) {
* return vector.y;
* }
* </pre>
* <p>
* Of course, the equivalent 2D implementation will use Vector2 instead of Vector3.
* @param vector the vector
* @return the value of the component affected by gravity.
*/
float getComponent(Vec2 vector);
/**
* Sets the component of the given vector along which the gravity operates.
* <p>
* Assuming a 3D coordinate system where the gravity is acting along the y-axis, this method will be implemented as follows:
*
* <pre>
* public void setComponent (Vector3 vector, float value) {
* vector.y = value;
* }
* </pre>
* <p>
* Of course, the equivalent 2D implementation will use Vector2 instead of Vector3.
* @param vector the vector
* @param value the value of the component affected by gravity
*/
void setComponent(Vec2 vector, float value);
}
/**
* The {@code JumpCallback} allows you to know whether a jump is achievable and when to jump.
* @author davebaol
*/
public interface JumpCallback{
/**
* Reports whether the jump is achievable or not.
* <p>
* A jump is not achievable when the character's maximum linear velocity is not enough, in which case the jump behavior
* won't produce any acceleration; you might want to use pathfinding to plan a new path.
* <p>
* If the jump is achievable the run up phase will start immediately and the character will try to match the target velocity
* toward the takeoff point. This is the right moment to start the run up animation, if needed.
* @param achievable whether the jump is achievable or not.
*/
void reportAchievability(boolean achievable);
/**
* This method is called to notify that both the position and velocity of the character are good enough to jump.
* @param maxVerticalVelocity the velocity to set along the vertical axis to achieve the jump
* @param time the duration of the jump
*/
void takeoff(float maxVerticalVelocity, float time);
}
}

View File

@@ -0,0 +1,99 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* The entire steering framework assumes that the direction a character is facing does not have to be its direction of motion. In
* many cases, however, you would like the character to face in the direction it is moving. To do this you can manually align the
* orientation of the character to its linear velocity on each frame update or you can use the {@code LookWhereYouAreGoing}
* behavior.
* <p>
* {@code LookWhereYouAreGoing} behavior gives the owner angular acceleration to make it face in the direction it is moving. In
* this way the owner changes facing gradually, which can look more natural, especially for aerial vehicles such as helicopters or
* for human characters that can move sideways.
* <p>
* This is a process similar to the {@code Face} behavior. The target orientation is calculated using the current velocity of the
* owner. If there is no velocity, then the target orientation is set to the current orientation. We have no preference in this
* situation for any orientation.
* @author davebaol
*/
public class LookWhereYouAreGoing extends ReachOrientation{
/**
* Creates a {@code LookWhereYouAreGoing} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public LookWhereYouAreGoing(Steerable owner){
super(owner);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Check for a zero direction, and return no steering if so
if(owner.getLinearVelocity().isZero(getActualLimiter().getZeroLinearSpeedThreshold())) return steering.setZero();
// Calculate the orientation based on the velocity of the owner
float orientation = owner.vectorToAngle(owner.getLinearVelocity());
// Delegate to ReachOrientation
return reachOrientation(steering, orientation);
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public LookWhereYouAreGoing setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public LookWhereYouAreGoing setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum angular speed and
* acceleration.
* @return this behavior for chaining.
*/
@Override
public LookWhereYouAreGoing setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
/**
* Sets the target to align to. Notice that this method is inherited from {@link ReachOrientation}, but is completely useless
* for {@code LookWhereYouAreGoing} because the target orientation is determined by the velocity of the owner itself.
* @return this behavior for chaining.
*/
@Override
public LookWhereYouAreGoing setTarget(Location target){
this.target = target;
return this;
}
@Override
public LookWhereYouAreGoing setAlignTolerance(float alignTolerance){
this.alignTolerance = alignTolerance;
return this;
}
@Override
public LookWhereYouAreGoing setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public LookWhereYouAreGoing setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,117 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
/**
* This steering behavior produces a linear acceleration trying to match target's velocity. It does not produce any angular
* acceleration.
* @author davebaol
*/
public class MatchVelocity extends SteeringBehavior{
/** The target of this behavior */
protected Steerable target;
/** The time over which to achieve target speed */
protected float timeToTarget;
/**
* Creates a {@code MatchVelocity} behavior for the given owner. No target is set. The maxLinearAcceleration is set to 100. The
* timeToTarget is set to 0.1 seconds.
* @param owner the owner of this behavior.
*/
public MatchVelocity(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code MatchVelocity} behavior for the given owner and target. The timeToTarget is set to 0.1 seconds.
* @param owner the owner of this behavior
* @param target the target of this behavior.
*/
public MatchVelocity(Steerable owner, Steerable target){
this(owner, target, 0.1f);
}
/**
* Creates a {@code MatchVelocity} behavior for the given owner, target and timeToTarget.
* @param owner the owner of this behavior
* @param target the target of this behavior
* @param timeToTarget the time over which to achieve target speed.
*/
public MatchVelocity(Steerable owner, Steerable target, float timeToTarget){
super(owner);
this.target = target;
this.timeToTarget = timeToTarget;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Acceleration tries to get to the target velocity without exceeding max acceleration
steering.linear.set(target.getLinearVelocity()).sub(owner.getLinearVelocity()).scl(1f / timeToTarget)
.limit(getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
/** Returns the target whose velocity should be matched. */
public Steerable getTarget(){
return target;
}
/**
* Sets the target whose velocity should be matched.
* @param target the target to set
* @return this behavior for chaining.
*/
public MatchVelocity setTarget(Steerable target){
this.target = target;
return this;
}
/** Returns the time over which to achieve target speed. */
public float getTimeToTarget(){
return timeToTarget;
}
/**
* Sets the time over which to achieve target speed.
* @param timeToTarget the time to set
* @return this behavior for chaining.
*/
public MatchVelocity setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public MatchVelocity setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public MatchVelocity setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public MatchVelocity setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,163 @@
package mindustry.ai.ai.steer.behaviors;
import arc.struct.*;
import mindustry.ai.ai.steer.*;
/**
* The {@code PrioritySteering} behavior iterates through the behaviors and returns the first non zero steering. It makes sense
* since certain steering behaviors only request an acceleration in particular conditions. Unlike {@link Seek} or {@link Evade},
* which always produce an acceleration, {@link RaycastObstacleAvoidance}, {@link CollisionAvoidance}, {@link Separation},
* {@link Hide} and {@link Arrive} will suggest no acceleration in many cases. But when these behaviors do suggest an
* acceleration, it is unwise to ignore it. An obstacle avoidance behavior, for example, should be honored immediately to avoid
* the crash.
* <p>
* Typically the behaviors of a {@code PrioritySteering} are arranged in groups with regular blending weights, see
* {@link BlendedSteering}. These groups are then placed in priority order to let the steering system consider each group in turn.
* It blends the steering behaviors in the current group together. If the total result is very small (less than some small, but
* adjustable, parameter), then it is ignored and the next group is considered. It is best not to check against zero directly,
* because numerical instability in calculations can mean that a zero value is never reached for some steering behaviors. Using a
* small constant value (conventionally called {@code epsilon}) avoids this problem. When a group is found with a result that isn't
* small, its result is used to steer the agent.
* <p>
* For instance, a pursuing agent working in a team may have three priorities:
* <ul>
* <li>a collision avoidance group that contains behaviors for obstacle avoidance, wall avoidance, and avoiding other characters.</li>
* <li>a separation behavior used to avoid getting too close to other members of the chasing pack.</li>
* <li>a pursuit behavior to chase the target.</li>
* </ul>
* If the character is far from any interference, the collision avoidance group will return with no desired acceleration. The
* separation behavior will then be considered but will also return with no action. Finally, the pursuit behavior will be
* considered, and the acceleration needed to continue the chase will be used. If the current motion of the character is perfect
* for the pursuit, this behavior may also return with no acceleration. In this case, there are no more behaviors to consider, so
* the character will have no acceleration, just as if they'd been exclusively controlled by the pursuit behavior.
* <p>
* In a different scenario, if the character is about to crash into a wall, the first group will return an acceleration that will
* help avoid the crash. The character will carry out this acceleration immediately, and the steering behaviors in the other
* groups won't be considered.
* <p>
* Usually {@code PrioritySteering} gives you a good compromise between speed and accuracy.
* @author davebaol
*/
public class PrioritySteering extends SteeringBehavior{
/** The threshold of the steering acceleration magnitude below which a steering behavior is considered to have given no output. */
protected float epsilon;
/**
* The list of steering behaviors in priority order. The first item in the list is tried first, the subsequent entries are only
* considered if the first one does not return a result.
*/
protected Array<SteeringBehavior> behaviors = new Array<>();
/** The index of the behavior whose acceleration has been returned by the last evaluation of this priority steering. */
protected int selectedBehaviorIndex;
/**
* Creates a {@code PrioritySteering} behavior for the specified owner. The threshold is set to 0.001.
* @param owner the owner of this behavior
*/
public PrioritySteering(Steerable owner){
this(owner, 0.001f);
}
/**
* Creates a {@code PrioritySteering} behavior for the specified owner and threshold.
* @param owner the owner of this behavior
* @param epsilon the threshold of the steering acceleration magnitude below which a steering behavior is considered to have
* given no output
*/
public PrioritySteering(Steerable owner, float epsilon){
super(owner);
this.epsilon = epsilon;
}
/**
* Adds the specified behavior to the priority list.
* @param behavior the behavior to add
* @return this behavior for chaining.
*/
public PrioritySteering add(SteeringBehavior behavior){
behaviors.add(behavior);
return this;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// We'll need epsilon squared later.
float epsilonSquared = epsilon * epsilon;
// Go through the behaviors until one has a large enough acceleration
int n = behaviors.size;
selectedBehaviorIndex = -1;
for(int i = 0; i < n; i++){
selectedBehaviorIndex = i;
SteeringBehavior behavior = behaviors.get(i);
// Calculate the behavior's steering
behavior.calculateSteering(steering);
// If we're above the threshold return the current steering
if(steering.calculateSquareMagnitude() > epsilonSquared) return steering;
}
// If we get here, it means that no behavior had a large enough acceleration,
// so return the small acceleration from the final behavior or zero if there are
// no behaviors in the list.
return n > 0 ? steering : steering.setZero();
}
/**
* Returns the index of the behavior whose acceleration has been returned by the last evaluation of this priority steering; -1
* otherwise.
*/
public int getSelectedBehaviorIndex(){
return selectedBehaviorIndex;
}
/**
* Returns the threshold of the steering acceleration magnitude below which a steering behavior is considered to have given no
* output.
*/
public float getEpsilon(){
return epsilon;
}
/**
* Sets the threshold of the steering acceleration magnitude below which a steering behavior is considered to have given no
* output.
* @param epsilon the epsilon to set
* @return this behavior for chaining.
*/
public PrioritySteering setEpsilon(float epsilon){
this.epsilon = epsilon;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public PrioritySteering setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public PrioritySteering setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. However, {@code PrioritySteering} needs no limiter at all as it simply returns
* the first non zero steering acceleration.
* @return this behavior for chaining.
*/
@Override
public PrioritySteering setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,144 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
/**
* {@code Pursue} behavior produces a force that steers the agent towards the evader (the target). Actually it predicts where an
* agent will be in time @{code t} and seeks towards that point to intercept it. We did this naturally playing tag as children,
* which is why the most difficult tag players to catch were those who kept switching direction, foiling our predictions.
* <p>
* This implementation performs the prediction by assuming the target will continue moving with the same velocity it currently
* has. This is a reasonable assumption over short distances, and even over longer distances it doesn't appear too stupid. The
* algorithm works out the distance between character and target and works out how long it would take to get there, at maximum
* speed. It uses this time interval as its prediction lookahead. It calculates the position of the target if it continues to move
* with its current velocity. This new position is then used as the target of a standard seek behavior.
* <p>
* If the character is moving slowly, or the target is a long way away, the prediction time could be very large. The target is
* less likely to follow the same path forever, so we'd like to set a limit on how far ahead we aim. The algorithm has a
* {@code maxPredictionTime} for this reason. If the prediction time is beyond this, then the maximum time is used.
* @author davebaol
*/
public class Pursue extends SteeringBehavior{
/** The target */
protected Steerable target;
/** The maximum prediction time */
protected float maxPredictionTime;
/**
* Creates a {@code Pursue} behavior for the specified owner and target. Maximum prediction time defaults to 1 second.
* @param owner the owner of this behavior.
* @param target the target of this behavior.
*/
public Pursue(Steerable owner, Steerable target){
this(owner, target, 1);
}
/**
* Creates a {@code Pursue} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target of this behavior
* @param maxPredictionTime the max time used to predict the target's position assuming it continues to move with its current
* velocity.
*/
public Pursue(Steerable owner, Steerable target, float maxPredictionTime){
super(owner);
this.target = target;
this.maxPredictionTime = maxPredictionTime;
}
/**
* Returns the actual linear acceleration to be applied. This method is overridden by the {@link Evade} behavior to invert the
* maximum linear acceleration in order to evade the target.
*/
protected float getActualMaxLinearAcceleration(){
return getActualLimiter().getMaxLinearAcceleration();
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
Vec2 targetPosition = target.getPosition();
// Get the square distance to the evader (the target)
float squareDistance = steering.linear.set(targetPosition).sub(owner.getPosition()).len2();
// Work out our current square speed
float squareSpeed = owner.getLinearVelocity().len2();
float predictionTime = maxPredictionTime;
if(squareSpeed > 0){
// Calculate prediction time if speed is not too small to give a reasonable value
float squarePredictionTime = squareDistance / squareSpeed;
if(squarePredictionTime < maxPredictionTime * maxPredictionTime)
predictionTime = (float)Math.sqrt(squarePredictionTime);
}
// Calculate and seek/flee the predicted position of the target
steering.linear.set(targetPosition).mulAdd(target.getLinearVelocity(), predictionTime).sub(owner.getPosition()).nor()
.scl(getActualMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
/** Returns the target. */
public Steerable getTarget(){
return target;
}
/**
* Sets the target.
* @return this behavior for chaining.
*/
public Pursue setTarget(Steerable target){
this.target = target;
return this;
}
/** Returns the maximum prediction time. */
public float getMaxPredictionTime(){
return maxPredictionTime;
}
/**
* Sets the maximum prediction time.
* @return this behavior for chaining.
*/
public Pursue setMaxPredictionTime(float maxPredictionTime){
this.maxPredictionTime = maxPredictionTime;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Pursue setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Pursue setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Pursue setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,221 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.utils.*;
import mindustry.ai.ai.steer.utils.rays.*;
import mindustry.ai.ai.utils.Ray;
import mindustry.ai.ai.utils.*;
/**
* With the {@code RaycastObstacleAvoidance} the moving agent (the owner) casts one or more rays out in the direction of its
* motion. If these rays collide with an obstacle, then a target is created that will avoid the collision, and the owner does a
* basic seek on this target. Typically, the rays extend a short distance ahead of the character (usually a distance corresponding
* to a few seconds of movement).
* <p>
* This behavior is especially suitable for large-scale obstacles like walls.
* <p>
* You should use the {@link RayConfiguration} more suitable for your game environment. Some basic ray configurations are provided
* by the framework: {@link SingleRayConfiguration}, {@link ParallelSideRayConfiguration} and
* {@link CentralRayWithWhiskersConfiguration}. There are no hard and fast rules as to which configuration is better. Each has its
* own particular idiosyncrasies. A single ray with short whiskers is often the best initial configuration to try but can make it
* impossible for the character to move down tight passages. The single ray configuration is useful in concave environments but
* grazes convex obstacles. The parallel configuration works well in areas where corners are highly obtuse but is very susceptible
* to the corner trap.
* <p>
* <a name="cornerTrap">
* <h2>The corner trap</h2></a> All the basic configurations for multi-ray obstacle avoidance can suffer from a crippling problem
* with acute angled corners (any convex corner, in fact, but it is more prevalent with acute angles). Consider a character with
* two parallel rays that is going towards a corner. As soon as its left ray is colliding with the wall near the corner, the
* steering behavior will turn it to the left to avoid the collision. Immediately, the right ray will then be colliding the other
* side of the corner, and the steering behavior will turn the character to the right. The character will repeatedly collide both
* sides of the corner in rapid succession. It will appear to home into the corner directly, until it slams into the wall. It will
* be unable to free itself from the trap.
* <p>
* The fan structure, with a wide enough fan angle, alleviates this problem. Often, there is a trade-off, however, between
* avoiding the corner trap with a large fan angle and keeping the angle small to allow the character to access small passages. At
* worst, with a fan angle near PI radians, the character will not be able to respond quickly enough to collisions detected on its
* side rays and will still graze against walls. There are two approaches that work well and represent the most practical
* solutions to the problem:
* <ul>
* <li><b>Adaptive fan angles:</b> If the character is moving successfully without a collision, then the fan angle is narrowed. If
* a collision is detected, then the fan angle is widened. If the character detects many collisions on successive frames, then the
* fan angle will continue to widen, reducing the chance that the character is trapped in a corner.</li>
* <li><b>Winner ray:</b> If a corner trap is detected, then one of the rays is considered to have won, and the collisions
* detected by other rays are ignored for a while.</li>
* </ul>
* It seems that the most practical solution is to use adaptive fan angles, with one long ray cast and two shorter whiskers.
* @author davebaol
*/
public class RaycastObstacleAvoidance extends SteeringBehavior{
/** The inputRay configuration */
protected RayConfiguration rayConfiguration;
/** The collision detector */
protected RaycastCollisionDetector raycastCollisionDetector;
/** The minimum distance to a wall, i.e. how far to avoid collision. */
protected float distanceFromBoundary;
private Collision outputCollision;
private Collision minOutputCollision;
/**
* Creates a {@code RaycastObstacleAvoidance} behavior.
* @param owner the owner of this behavior
*/
public RaycastObstacleAvoidance(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code RaycastObstacleAvoidance} behavior.
* @param owner the owner of this behavior
* @param rayConfiguration the ray configuration
*/
public RaycastObstacleAvoidance(Steerable owner, RayConfiguration rayConfiguration){
this(owner, rayConfiguration, null);
}
/**
* Creates a {@code RaycastObstacleAvoidance} behavior.
* @param owner the owner of this behavior
* @param rayConfiguration the ray configuration
* @param raycastCollisionDetector the collision detector
*/
public RaycastObstacleAvoidance(Steerable owner, RayConfiguration rayConfiguration,
RaycastCollisionDetector raycastCollisionDetector){
this(owner, rayConfiguration, raycastCollisionDetector, 0);
}
/**
* Creates a {@code RaycastObstacleAvoidance} behavior.
* @param owner the owner of this behavior
* @param rayConfiguration the ray configuration
* @param raycastCollisionDetector the collision detector
* @param distanceFromBoundary the minimum distance to a wall (i.e., how far to avoid collision).
*/
public RaycastObstacleAvoidance(Steerable owner, RayConfiguration rayConfiguration,
RaycastCollisionDetector raycastCollisionDetector, float distanceFromBoundary){
super(owner);
this.rayConfiguration = rayConfiguration;
this.raycastCollisionDetector = raycastCollisionDetector;
this.distanceFromBoundary = distanceFromBoundary;
this.outputCollision = new Collision(newVector(owner), newVector(owner));
this.minOutputCollision = new Collision(newVector(owner), newVector(owner));
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
Vec2 ownerPosition = owner.getPosition();
float minDistanceSquare = Float.POSITIVE_INFINITY;
// Get the updated rays
Ray[] inputRays = rayConfiguration.updateRays();
// Process rays
for(Ray inputRay : inputRays){
// Find the collision with current ray
boolean collided = raycastCollisionDetector.findCollision(outputCollision, inputRay);
if(collided){
float distanceSquare = ownerPosition.dst2(outputCollision.point);
if(distanceSquare < minDistanceSquare){
minDistanceSquare = distanceSquare;
// Swap collisions
Collision tmpCollision = outputCollision;
outputCollision = minOutputCollision;
minOutputCollision = tmpCollision;
}
}
}
// Return zero steering if no collision has occurred
if(minDistanceSquare == Float.POSITIVE_INFINITY) return steering.setZero();
// Calculate and seek the target position
steering.linear.set(minOutputCollision.point)
.mulAdd(minOutputCollision.normal, owner.getBoundingRadius() + distanceFromBoundary).sub(owner.getPosition()).nor()
.scl(getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
/** Returns the ray configuration of this behavior. */
public RayConfiguration getRayConfiguration(){
return rayConfiguration;
}
/**
* Sets the ray configuration of this behavior.
* @param rayConfiguration the ray configuration to set
* @return this behavior for chaining.
*/
public RaycastObstacleAvoidance setRayConfiguration(RayConfiguration rayConfiguration){
this.rayConfiguration = rayConfiguration;
return this;
}
/** Returns the raycast collision detector of this behavior. */
public RaycastCollisionDetector getRaycastCollisionDetector(){
return raycastCollisionDetector;
}
/**
* Sets the raycast collision detector of this behavior.
* @param raycastCollisionDetector the raycast collision detector to set
* @return this behavior for chaining.
*/
public RaycastObstacleAvoidance setRaycastCollisionDetector(RaycastCollisionDetector raycastCollisionDetector){
this.raycastCollisionDetector = raycastCollisionDetector;
return this;
}
/** Returns the distance from boundary, i.e. the minimum distance to an obstacle. */
public float getDistanceFromBoundary(){
return distanceFromBoundary;
}
/**
* Sets the distance from boundary, i.e. the minimum distance to an obstacle.
* @param distanceFromBoundary the distanceFromBoundary to set
* @return this behavior for chaining.
*/
public RaycastObstacleAvoidance setDistanceFromBoundary(float distanceFromBoundary){
this.distanceFromBoundary = distanceFromBoundary;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public RaycastObstacleAvoidance setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public RaycastObstacleAvoidance setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public RaycastObstacleAvoidance setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,183 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code ReachOrientation} tries to align the owner to the target. It pays no attention to the position or velocity of the owner
* or target. This steering behavior does not produce any linear acceleration; it only responds by turning.
* <p>
* {@code ReachOrientation} behaves in a similar way to {@link Arrive} since it tries to reach the target orientation and tries to
* have zero rotation when it gets there. Like arrive, it uses two radii: {@code decelerationRadius} for slowing down and
* {@code alignTolerance} to make orientations near the target acceptable without letting small errors keep the owner swinging.
* Because we are dealing with a single scalar value, rather than a 2D or 3D vector, the radius acts as an interval.
* <p>
* Similarly to {@code Arrive}, there is a {@code timeToTarget} that defaults to 0.1 seconds.
* @author davebaol
*/
public class ReachOrientation extends SteeringBehavior{
/** The target to align to. */
protected Location target;
/** The tolerance for aligning to the target without letting small errors keep the owner swinging. */
protected float alignTolerance;
/** The radius for beginning to slow down */
protected float decelerationRadius;
/** The time over which to achieve target rotation speed */
protected float timeToTarget = 0.1f;
/**
* Creates a {@code ReachOrientation} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public ReachOrientation(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code ReachOrientation} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target.
*/
public ReachOrientation(Steerable owner, Location target){
super(owner);
this.target = target;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
return reachOrientation(steering, target.getOrientation());
}
/**
* Produces a steering that tries to align the owner to the target orientation. This method is called by subclasses that want
* to align to a certain orientation.
* @param steering the steering to be calculated.
* @param targetOrientation the target orientation you want to align to.
* @return the calculated steering for chaining.
*/
protected SteeringAcceleration reachOrientation(SteeringAcceleration steering, float targetOrientation){
// Get the rotation direction to the target wrapped to the range [-PI, PI]
float rotation = Mathf.wrapAngleAroundZero(targetOrientation - owner.getOrientation());
// Absolute rotation
float rotationSize = rotation < 0f ? -rotation : rotation;
// Check if we are there, return no steering
if(rotationSize <= alignTolerance) return steering.setZero();
Limiter actualLimiter = getActualLimiter();
// Use maximum rotation
float targetRotation = actualLimiter.getMaxAngularSpeed();
// If we are inside the slow down radius, then calculate a scaled rotation
if(rotationSize <= decelerationRadius) targetRotation *= rotationSize / decelerationRadius;
// The final target rotation combines
// speed (already in the variable) and direction
targetRotation *= rotation / rotationSize;
// Acceleration tries to get to the target rotation
steering.angular = (targetRotation - owner.getAngularVelocity()) / timeToTarget;
// Check if the absolute acceleration is too great
float angularAcceleration = steering.angular < 0f ? -steering.angular : steering.angular;
if(angularAcceleration > actualLimiter.getMaxAngularAcceleration())
steering.angular *= actualLimiter.getMaxAngularAcceleration() / angularAcceleration;
// No linear acceleration
steering.linear.setZero();
// Output the steering
return steering;
}
/** Returns the target to align to. */
public Location getTarget(){
return target;
}
/**
* Sets the target to align to.
* @return this behavior for chaining.
*/
public ReachOrientation setTarget(Location target){
this.target = target;
return this;
}
/** Returns the tolerance for aligning to the target without letting small errors keep the owner swinging. */
public float getAlignTolerance(){
return alignTolerance;
}
/**
* Sets the tolerance for aligning to the target without letting small errors keep the owner swinging.
* @return this behavior for chaining.
*/
public ReachOrientation setAlignTolerance(float alignTolerance){
this.alignTolerance = alignTolerance;
return this;
}
/** Returns the radius for beginning to slow down */
public float getDecelerationRadius(){
return decelerationRadius;
}
/**
* Sets the radius for beginning to slow down
* @return this behavior for chaining.
*/
public ReachOrientation setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
/** Returns the time over which to achieve target rotation speed */
public float getTimeToTarget(){
return timeToTarget;
}
/**
* Sets the time over which to achieve target rotation speed
* @return this behavior for chaining.
*/
public ReachOrientation setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public ReachOrientation setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public ReachOrientation setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum angular speed and
* acceleration.
* @return this behavior for chaining.
*/
@Override
public ReachOrientation setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,87 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@code Seek} behavior moves the owner towards the target position. Given a target, this behavior calculates the linear steering
* acceleration which will direct the agent towards the target as fast as possible.
* @author davebaol
*/
public class Seek extends SteeringBehavior{
/** The target to seek */
protected Location target;
/**
* Creates a {@code Seek} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public Seek(Steerable owner){
this(owner, null);
}
/**
* Creates a {@code Seek} behavior for the specified owner and target.
* @param owner the owner of this behavior
* @param target the target agent of this behavior.
*/
public Seek(Steerable owner, Location target){
super(owner);
this.target = target;
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Try to match the position of the character with the position of the target by calculating
// the direction to the target and by moving toward it as fast as possible.
steering.linear.set(target.getPosition()).sub(owner.getPosition()).nor().scl(getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
/** Returns the target to seek. */
public Location getTarget(){
return target;
}
/**
* Sets the target to seek.
* @return this behavior for chaining.
*/
public Seek setTarget(Location target){
this.target = target;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Seek setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Seek setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Seek setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,108 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.Proximity.*;
/**
* {@code Separation} is a group behavior producing a steering acceleration repelling from the other neighbors which are the agents
* in the immediate area defined by the given {@link Proximity}. The acceleration is calculated by iterating through all the
* neighbors, examining each one. The vector to each agent under consideration is normalized, multiplied by a strength decreasing
* according to the inverse square law in relation to distance, and accumulated.
* @author davebaol
*/
public class Separation extends GroupBehavior implements ProximityCallback{
/**
* The constant coefficient of decay for the inverse square law force. It controls how fast the separation strength decays with
* distance.
*/
float decayCoefficient = 1f;
private Vec2 toAgent;
private Vec2 linear;
/**
* Creates a {@code Separation} behavior for the specified owner and proximity.
* @param owner the owner of this behavior
* @param proximity the proximity to detect the owner's neighbors
*/
public Separation(Steerable owner, Proximity proximity){
super(owner, proximity);
this.toAgent = newVector(owner);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
steering.setZero();
linear = steering.linear;
proximity.findNeighbors(this);
return steering;
}
@Override
public boolean reportNeighbor(Steerable neighbor){
toAgent.set(owner.getPosition()).sub(neighbor.getPosition());
float distanceSqr = toAgent.len2();
if(distanceSqr == 0) return true;
float maxAcceleration = getActualLimiter().getMaxLinearAcceleration();
// Calculate the strength of repulsion through inverse square law decay
float strength = getDecayCoefficient() / distanceSqr;
if(strength > maxAcceleration) strength = maxAcceleration;
// Add the acceleration
// Optimized code for linear.mulAdd(toAgent.nor(), strength);
linear.mulAdd(toAgent, strength / (float)Math.sqrt(distanceSqr));
return true;
}
/** Returns the coefficient of decay for the inverse square law force. */
public float getDecayCoefficient(){
return decayCoefficient;
}
/**
* Sets the coefficient of decay for the inverse square law force. It controls how fast the separation strength decays with
* distance.
* @param decayCoefficient the coefficient of decay to set
*/
public Separation setDecayCoefficient(float decayCoefficient){
this.decayCoefficient = decayCoefficient;
return this;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Separation setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Separation setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration.
* @return this behavior for chaining.
*/
@Override
public Separation setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
}

View File

@@ -0,0 +1,259 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.*;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
import mindustry.ai.ai.utils.Timepiece;
/**
* {@code Wander} behavior is designed to produce a steering acceleration that will give the impression of a random walk through
* the agent's environment. You'll often find it a useful ingredient when creating an agent's behavior.
* <p>
* There is a circle in front of the owner (where front is determined by its current facing direction) on which the target is
* constrained. Each time the behavior is run, we move the target around the circle a little, by a random amount. Now there are 2
* ways to implement wander behavior:
* <ul>
* <li>The owner seeks the target, using the {@link Seek} behavior, and performs a {@link LookWhereYouAreGoing} behavior to
* correct its orientation.</li>
* <li>The owner tries to face the target in each frame, using the {@link Face} behavior to align to the target, and applies full
* linear acceleration in the direction of its current orientation.</li>
* </ul>
* In either case, the orientation of the owner is retained between calls (so smoothing the changes in orientation). The angles
* that the edges of the circle subtend to the owner determine how fast it will turn. If the target is on one of these extreme
* points, it will turn quickly. The target will twitch and jitter around the edge of the circle, but the owner's orientation will
* change smoothly.
* <p>
* This implementation uses the second approach. However, if you manually align owner's orientation to its linear velocity on each
* time step, {@link Face} behavior should not be used (which is the default case). On the other hand, if the owner has
* independent facing you should explicitly call {@link #setFaceEnabled(boolean) setFaceEnabled(true)} before using Wander
* behavior.
* <p>
* Note that this behavior internally calls the {@link Timepiece#getTime() GdxAI.getTimepiece().getTime()} method to get the
* current AI time and make the {@link #wanderRate} FPS independent. This means that
* <ul>
* <li>if you forget to {@link Timepiece#update(float) update the timepiece} the wander orientation won't change.</li>
* <li>ideally the timepiece should be always updated before this steering behavior runs.</li>
* </ul>
* <p>
* This steering behavior can be used to produce a whole range of random motion, from very smooth undulating turns to wild
* Strictly Ballroom type whirls and pirouettes depending on the size of the circle, its distance from the agent, and the amount
* of random displacement each frame.
* @author davebaol
*/
public class Wander extends Face{
/** The forward offset of the wander circle */
protected float wanderOffset;
/** The radius of the wander circle */
protected float wanderRadius;
/** The rate, expressed in radian per second, at which the wander orientation can change */
protected float wanderRate;
/** The last time the orientation of the wander target has been updated */
protected float lastTime;
/** The current orientation of the wander target */
protected float wanderOrientation;
/**
* The flag indicating whether to use {@link Face} behavior or not. This should be set to {@code true} when independent facing
* is used.
*/
protected boolean faceEnabled;
private Vec2 internalTargetPosition;
private Vec2 wanderCenter;
/**
* Creates a {@code Wander} behavior for the specified owner.
* @param owner the owner of this behavior.
*/
public Wander(Steerable owner){
super(owner);
this.internalTargetPosition = newVector(owner);
this.wanderCenter = newVector(owner);
}
@Override
protected SteeringAcceleration calculateRealSteering(SteeringAcceleration steering){
// Update the wander orientation
float now = Timepiece.time;
if(lastTime > 0){
float delta = now - lastTime;
wanderOrientation += Mathf.randomTriangular(wanderRate * delta);
}
lastTime = now;
// Calculate the combined target orientation
float targetOrientation = wanderOrientation + owner.getOrientation();
// Calculate the center of the wander circle
wanderCenter.set(owner.getPosition()).mulAdd(owner.angleToVector(steering.linear, owner.getOrientation()), wanderOffset);
// Calculate the target location
// Notice that we're using steering.linear as temporary vector
internalTargetPosition.set(wanderCenter).mulAdd(owner.angleToVector(steering.linear, targetOrientation), wanderRadius);
float maxLinearAcceleration = getActualLimiter().getMaxLinearAcceleration();
if(faceEnabled){
// Delegate to face
face(steering, internalTargetPosition);
// Set the linear acceleration to be at full
// acceleration in the direction of the orientation
owner.angleToVector(steering.linear, owner.getOrientation()).scl(maxLinearAcceleration);
}else{
// Seek the internal target position
steering.linear.set(internalTargetPosition).sub(owner.getPosition()).nor().scl(maxLinearAcceleration);
// No angular acceleration
steering.angular = 0;
}
return steering;
}
/** Returns the forward offset of the wander circle. */
public float getWanderOffset(){
return wanderOffset;
}
/**
* Sets the forward offset of the wander circle.
* @return this behavior for chaining.
*/
public Wander setWanderOffset(float wanderOffset){
this.wanderOffset = wanderOffset;
return this;
}
/** Returns the radius of the wander circle. */
public float getWanderRadius(){
return wanderRadius;
}
/**
* Sets the radius of the wander circle.
* @return this behavior for chaining.
*/
public Wander setWanderRadius(float wanderRadius){
this.wanderRadius = wanderRadius;
return this;
}
/** Returns the rate, expressed in radian per second, at which the wander orientation can change. */
public float getWanderRate(){
return wanderRate;
}
/**
* Sets the rate, expressed in radian per second, at which the wander orientation can change.
* @return this behavior for chaining.
*/
public Wander setWanderRate(float wanderRate){
this.wanderRate = wanderRate;
return this;
}
/** Returns the current orientation of the wander target. */
public float getWanderOrientation(){
return wanderOrientation;
}
/**
* Sets the current orientation of the wander target.
* @return this behavior for chaining.
*/
public Wander setWanderOrientation(float wanderOrientation){
this.wanderOrientation = wanderOrientation;
return this;
}
/** Returns the flag indicating whether to use {@link Face} behavior or not. */
public boolean isFaceEnabled(){
return faceEnabled;
}
/**
* Sets the flag indicating whether to use {@link Face} behavior or not. This should be set to {@code true} when independent
* facing is used.
* @return this behavior for chaining.
*/
public Wander setFaceEnabled(boolean faceEnabled){
this.faceEnabled = faceEnabled;
return this;
}
/** Returns the current position of the wander target. This method is useful for debug purpose. */
public Vec2 getInternalTargetPosition(){
return internalTargetPosition;
}
/** Returns the current center of the wander circle. This method is useful for debug purpose. */
public Vec2 getWanderCenter(){
return wanderCenter;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
@Override
public Wander setOwner(Steerable owner){
this.owner = owner;
return this;
}
@Override
public Wander setEnabled(boolean enabled){
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration;
* additionally, if the flag {@code faceEnabled} is true, it must take care of the maximum angular speed and acceleration.
* @return this behavior for chaining.
*/
@Override
public Wander setLimiter(Limiter limiter){
this.limiter = limiter;
return this;
}
/**
* Sets the target to align to. Notice that this method is inherited from {@link ReachOrientation}, but is completely useless
* for {@code Wander} because owner's orientation is determined by the internal target, which is moving on the wander circle.
* @return this behavior for chaining.
*/
@Override
public Wander setTarget(Location target){
this.target = target;
return this;
}
@Override
public Wander setAlignTolerance(float alignTolerance){
this.alignTolerance = alignTolerance;
return this;
}
@Override
public Wander setDecelerationRadius(float decelerationRadius){
this.decelerationRadius = decelerationRadius;
return this;
}
@Override
public Wander setTimeToTarget(float timeToTarget){
this.timeToTarget = timeToTarget;
return this;
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.ai.ai.steer.limiters;
/**
* An {@code AngularAccelerationLimiter} provides the maximum magnitude of angular acceleration. All other methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class AngularAccelerationLimiter extends NullLimiter{
private float maxAngularAcceleration;
/**
* Creates an {@code AngularAccelerationLimiter}.
* @param maxAngularAcceleration the maximum angular acceleration
*/
public AngularAccelerationLimiter(float maxAngularAcceleration){
this.maxAngularAcceleration = maxAngularAcceleration;
}
/** Returns the maximum angular acceleration. */
@Override
public float getMaxAngularAcceleration(){
return maxAngularAcceleration;
}
/** Sets the maximum angular acceleration. */
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration){
this.maxAngularAcceleration = maxAngularAcceleration;
}
}

View File

@@ -0,0 +1,47 @@
package mindustry.ai.ai.steer.limiters;
/**
* An {@code AngularLimiter} provides the maximum magnitudes of angular speed and angular acceleration. Linear methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class AngularLimiter extends NullLimiter{
private float maxAngularAcceleration;
private float maxAngularSpeed;
/**
* Creates an {@code AngularLimiter}.
* @param maxAngularAcceleration the maximum angular acceleration
* @param maxAngularSpeed the maximum angular speed
*/
public AngularLimiter(float maxAngularAcceleration, float maxAngularSpeed){
this.maxAngularAcceleration = maxAngularAcceleration;
this.maxAngularSpeed = maxAngularSpeed;
}
/** Returns the maximum angular speed. */
@Override
public float getMaxAngularSpeed(){
return maxAngularSpeed;
}
/** Sets the maximum angular speed. */
@Override
public void setMaxAngularSpeed(float maxAngularSpeed){
this.maxAngularSpeed = maxAngularSpeed;
}
/** Returns the maximum angular acceleration. */
@Override
public float getMaxAngularAcceleration(){
return maxAngularAcceleration;
}
/** Sets the maximum angular acceleration. */
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration){
this.maxAngularAcceleration = maxAngularAcceleration;
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.ai.ai.steer.limiters;
/**
* An {@code AngularSpeedLimiter} provides the maximum magnitudes of angular speed. All other methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class AngularSpeedLimiter extends NullLimiter{
private float maxAngularSpeed;
/**
* Creates an {@code AngularSpeedLimiter}.
* @param maxAngularSpeed the maximum angular speed
*/
public AngularSpeedLimiter(float maxAngularSpeed){
this.maxAngularSpeed = maxAngularSpeed;
}
/** Returns the maximum angular speed. */
@Override
public float getMaxAngularSpeed(){
return maxAngularSpeed;
}
/** Sets the maximum angular speed. */
@Override
public void setMaxAngularSpeed(float maxAngularSpeed){
this.maxAngularSpeed = maxAngularSpeed;
}
}

View File

@@ -0,0 +1,80 @@
package mindustry.ai.ai.steer.limiters;
import mindustry.ai.ai.steer.*;
/**
* A {@code FullLimiter} provides the maximum magnitudes of speed and acceleration for both linear and angular components.
* @author davebaol
*/
public class FullLimiter implements Limiter{
private float maxLinearAcceleration;
private float maxLinearSpeed;
private float maxAngularAcceleration;
private float maxAngularSpeed;
private float zeroLinearSpeedThreshold;
/**
* Creates a {@code FullLimiter}.
* @param maxLinearAcceleration the maximum linear acceleration
* @param maxLinearSpeed the maximum linear speed
* @param maxAngularAcceleration the maximum angular acceleration
* @param maxAngularSpeed the maximum angular speed
*/
public FullLimiter(float maxLinearAcceleration, float maxLinearSpeed, float maxAngularAcceleration, float maxAngularSpeed){
this.maxLinearAcceleration = maxLinearAcceleration;
this.maxLinearSpeed = maxLinearSpeed;
this.maxAngularAcceleration = maxAngularAcceleration;
this.maxAngularSpeed = maxAngularSpeed;
}
@Override
public float getMaxLinearSpeed(){
return maxLinearSpeed;
}
@Override
public void setMaxLinearSpeed(float maxLinearSpeed){
this.maxLinearSpeed = maxLinearSpeed;
}
@Override
public float getMaxLinearAcceleration(){
return maxLinearAcceleration;
}
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration){
this.maxLinearAcceleration = maxLinearAcceleration;
}
@Override
public float getMaxAngularSpeed(){
return maxAngularSpeed;
}
@Override
public void setMaxAngularSpeed(float maxAngularSpeed){
this.maxAngularSpeed = maxAngularSpeed;
}
@Override
public float getMaxAngularAcceleration(){
return maxAngularAcceleration;
}
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration){
this.maxAngularAcceleration = maxAngularAcceleration;
}
@Override
public float getZeroLinearSpeedThreshold(){
return zeroLinearSpeedThreshold;
}
@Override
public void setZeroLinearSpeedThreshold(float zeroLinearSpeedThreshold){
this.zeroLinearSpeedThreshold = zeroLinearSpeedThreshold;
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.ai.ai.steer.limiters;
/**
* A {@code LinearAccelerationLimiter} provides the maximum magnitude of linear acceleration. All other methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class LinearAccelerationLimiter extends NullLimiter{
private float maxLinearAcceleration;
/**
* Creates a {@code LinearAccelerationLimiter}.
* @param maxLinearAcceleration the maximum linear acceleration
*/
public LinearAccelerationLimiter(float maxLinearAcceleration){
this.maxLinearAcceleration = maxLinearAcceleration;
}
/** Returns the maximum linear acceleration. */
@Override
public float getMaxLinearAcceleration(){
return maxLinearAcceleration;
}
/** Sets the maximum linear acceleration. */
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration){
this.maxLinearAcceleration = maxLinearAcceleration;
}
}

View File

@@ -0,0 +1,47 @@
package mindustry.ai.ai.steer.limiters;
/**
* A {@code LinearLimiter} provides the maximum magnitudes of linear speed and linear acceleration. Angular methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class LinearLimiter extends NullLimiter{
private float maxLinearAcceleration;
private float maxLinearSpeed;
/**
* Creates a {@code LinearLimiter}.
* @param maxLinearAcceleration the maximum linear acceleration
* @param maxLinearSpeed the maximum linear speed
*/
public LinearLimiter(float maxLinearAcceleration, float maxLinearSpeed){
this.maxLinearAcceleration = maxLinearAcceleration;
this.maxLinearSpeed = maxLinearSpeed;
}
/** Returns the maximum linear speed. */
@Override
public float getMaxLinearSpeed(){
return maxLinearSpeed;
}
/** Sets the maximum linear speed. */
@Override
public void setMaxLinearSpeed(float maxLinearSpeed){
this.maxLinearSpeed = maxLinearSpeed;
}
/** Returns the maximum linear acceleration. */
@Override
public float getMaxLinearAcceleration(){
return maxLinearAcceleration;
}
/** Sets the maximum linear acceleration. */
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration){
this.maxLinearAcceleration = maxLinearAcceleration;
}
}

View File

@@ -0,0 +1,32 @@
package mindustry.ai.ai.steer.limiters;
/**
* A {@code LinearSpeedLimiter} provides the maximum magnitudes of linear speed. All other methods throw an
* {@link UnsupportedOperationException}.
* @author davebaol
*/
public class LinearSpeedLimiter extends NullLimiter{
private float maxLinearSpeed;
/**
* Creates a {@code LinearSpeedLimiter}.
* @param maxLinearSpeed the maximum linear speed
*/
public LinearSpeedLimiter(float maxLinearSpeed){
this.maxLinearSpeed = maxLinearSpeed;
}
/** Returns the maximum linear speed. */
@Override
public float getMaxLinearSpeed(){
return maxLinearSpeed;
}
/** Sets the maximum linear speed. */
@Override
public void setMaxLinearSpeed(float maxLinearSpeed){
this.maxLinearSpeed = maxLinearSpeed;
}
}

View File

@@ -0,0 +1,129 @@
package mindustry.ai.ai.steer.limiters;
import mindustry.ai.ai.steer.*;
/**
* A {@code NullLimiter} always throws {@link UnsupportedOperationException}. Typically it's used as the base class of partial or
* immutable limiters.
* @author davebaol
*/
public class NullLimiter implements Limiter{
/**
* An immutable limiter whose getters return {@link Float#POSITIVE_INFINITY} and setters throw
* {@link UnsupportedOperationException}.
*/
public static final NullLimiter NEUTRAL_LIMITER = new NullLimiter(){
@Override
public float getMaxLinearSpeed(){
return Float.POSITIVE_INFINITY;
}
@Override
public float getMaxLinearAcceleration(){
return Float.POSITIVE_INFINITY;
}
@Override
public float getMaxAngularSpeed(){
return Float.POSITIVE_INFINITY;
}
@Override
public float getMaxAngularAcceleration(){
return Float.POSITIVE_INFINITY;
}
};
/** Creates a {@code NullLimiter}. */
public NullLimiter(){
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public float getMaxLinearSpeed(){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public void setMaxLinearSpeed(float maxLinearSpeed){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public float getMaxLinearAcceleration(){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public float getMaxAngularSpeed(){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public void setMaxAngularSpeed(float maxAngularSpeed){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public float getMaxAngularAcceleration(){
throw new UnsupportedOperationException();
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration){
throw new UnsupportedOperationException();
}
@Override
public float getZeroLinearSpeedThreshold(){
return 0.001f;
}
/**
* Guaranteed to throw UnsupportedOperationException.
* @throws UnsupportedOperationException always
*/
@Override
public void setZeroLinearSpeedThreshold(float zeroLinearSpeedThreshold){
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,141 @@
package mindustry.ai.ai.steer.proximities;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.Timepiece;
/**
* {@code FieldOfViewProximity} emulates the peripheral vision of the owner as if it had eyes. Any agents contained in the
* specified list that are within the field of view of the owner are considered owner's neighbors. The field of view is determined
* by a radius and an angle in degrees.
* <p>
* Note that this implementation checks the AI time of the current frame through the {@link mindustry.ai.ai.utils.Timepiece#getTime()
* GdxAI.getTimepiece().getTime()} method in order to calculate neighbors only once per frame (assuming delta time is always
* greater than 0, if time has changed the frame has changed too). This means that
* <ul>
* <li>if you forget to {@link mindustry.ai.ai.utils.Timepiece#update(float) update the timepiece} on each frame the proximity instance will be
* calculated only the very first time, which is not what you want of course.</li>
* <li>ideally the timepiece should be updated before the proximity is updated by the {@link #findNeighbors(ProximityCallback)}
* method.</li>
* </ul>
* @author davebaol
*/
public class FieldOfViewProximity extends ProximityBase{
/** The radius of this proximity. */
protected float radius;
/** The angle in radians of this proximity. */
protected float angle;
private float coneThreshold;
private float lastTime;
private Vec2 ownerOrientation;
private Vec2 toAgent;
/**
* Creates a {@code FieldOfViewProximity} for the specified owner, agents and cone area defined by the given radius and angle
* in radians.
* @param owner the owner of this proximity
* @param agents the agents
* @param radius the radius of the cone area
* @param angle the angle in radians of the cone area
*/
public FieldOfViewProximity(Steerable owner, Iterable<? extends Steerable> agents, float radius, float angle){
super(owner, agents);
this.radius = radius;
setAngle(angle);
this.lastTime = 0;
this.ownerOrientation = owner.getPosition().cpy().setZero();
this.toAgent = owner.getPosition().cpy().setZero();
}
/** Returns the radius of this proximity. */
public float getRadius(){
return radius;
}
/** Sets the radius of this proximity. */
public void setRadius(float radius){
this.radius = radius;
}
/** Returns the angle of this proximity in radians. */
public float getAngle(){
return angle;
}
/** Sets the angle of this proximity in radians. */
public void setAngle(float angle){
this.angle = angle;
this.coneThreshold = (float)Math.cos(angle * 0.5f);
}
@Override
public int findNeighbors(ProximityCallback callback){
int neighborCount = 0;
// If the frame is new then avoid repeating calculations
// when this proximity is used by multiple group behaviors.
float currentTime = Timepiece.time;
if(this.lastTime != currentTime){
// Save the current time
this.lastTime = currentTime;
Vec2 ownerPosition = owner.getPosition();
// Transform owner orientation to a Vector
owner.angleToVector(ownerOrientation, owner.getOrientation());
// Scan the agents searching for neighbors
for(Steerable currentAgent : agents){
// Make sure the agent being examined isn't the owner
if(currentAgent != owner){
toAgent.set(currentAgent.getPosition()).sub(ownerPosition);
// The bounding radius of the current agent is taken into account
// by adding it to the radius proximity
float range = radius + currentAgent.getBoundingRadius();
float toAgentLen2 = toAgent.len2();
// Make sure the current agent is within the range.
// Notice we're working in distance-squared space to avoid square root.
if(toAgentLen2 < range * range){
// If the current agent is within the field of view of the owner,
// report it to the callback and tag it for further consideration.
if(ownerOrientation.dot(toAgent) > coneThreshold){
if(callback.reportNeighbor(currentAgent)){
currentAgent.setTagged(true);
neighborCount++;
continue;
}
}
}
}
// Clear the tag
currentAgent.setTagged(false);
}
}else{
// Scan the agents searching for tagged neighbors
for(Steerable currentAgent : agents){
// Make sure the agent being examined isn't the owner and that
// it's tagged.
if(currentAgent != owner && currentAgent.isTagged()){
if(callback.reportNeighbor(currentAgent)){
neighborCount++;
}
}
}
}
return neighborCount;
}
}

View File

@@ -0,0 +1,36 @@
package mindustry.ai.ai.steer.proximities;
import mindustry.ai.ai.steer.*;
/**
* {@code InfiniteProximity} is likely the simplest type of Proximity one can imagine. All the agents contained in the specified
* list are considered neighbors of the owner, excluded the owner itself (if it is part of the list).
* @author davebaol
*/
public class InfiniteProximity extends ProximityBase{
/**
* Creates a {@code InfiniteProximity} for the specified owner and list of agents.
* @param owner the owner of this proximity
* @param agents the list of agents
*/
public InfiniteProximity(Steerable owner, Iterable<? extends Steerable> agents){
super(owner, agents);
}
@Override
public int findNeighbors(ProximityCallback callback){
int neighborCount = 0;
for(Steerable currentAgent : agents){
// Make sure the agent being examined isn't the owner
if(currentAgent != owner){
if(callback.reportNeighbor(currentAgent)){
neighborCount++;
}
}
}
return neighborCount;
}
}

View File

@@ -0,0 +1,52 @@
package mindustry.ai.ai.steer.proximities;
import mindustry.ai.ai.steer.*;
/**
* {@code ProximityBase} is the base class for any concrete proximity based on an iterable collection of agents.
* @author davebaol
*/
public abstract class ProximityBase implements Proximity{
/** The owner of this proximity. */
protected Steerable owner;
/**
* The collection of the agents handled by this proximity.
* <p>
* Note that, being this field of type {@code Iterable}, you can either use java or libgdx collections. See
* https://github.com/libgdx/gdx-ai/issues/65
*/
protected Iterable<? extends Steerable> agents;
/**
* Creates a {@code ProximityBase} for the specified owner and list of agents.
* @param owner the owner of this proximity
* @param agents the list of agents
*/
public ProximityBase(Steerable owner, Iterable<? extends Steerable> agents){
this.owner = owner;
this.agents = agents;
}
@Override
public Steerable getOwner(){
return owner;
}
@Override
public void setOwner(Steerable owner){
this.owner = owner;
}
/** Returns the the agents that represent potential neighbors. */
public Iterable<? extends Steerable> getAgents(){
return agents;
}
/** Sets the agents that represent potential neighbors. */
public void setAgents(Iterable<Steerable> agents){
this.agents = agents;
}
}

View File

@@ -0,0 +1,104 @@
package mindustry.ai.ai.steer.proximities;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.Timepiece;
/**
* A {@code RadiusProximity} elaborates any agents contained in the specified list that are within the radius of the owner.
* <p>
* Note that this implementation checks the AI time of the current frame through the {@link mindustry.ai.ai.utils.Timepiece#getTime()
* GdxAI.getTimepiece().getTime()} method in order to calculate neighbors only once per frame (assuming delta time is always
* greater than 0, if time has changed the frame has changed too). This means that
* <ul>
* <li>if you forget to {@link mindustry.ai.ai.utils.Timepiece#update(float) update the timepiece} on each frame the proximity instance will be
* calculated only the very first time, which is not what you want of course.</li>
* <li>ideally the timepiece should be updated before the proximity is updated by the {@link #findNeighbors(ProximityCallback)}
* method.</li>
* </ul>
* @author davebaol
*/
public class RadiusProximity extends ProximityBase{
/** The radius of this proximity. */
protected float radius;
private float lastTime;
/**
* Creates a {@code RadiusProximity} for the specified owner, agents and radius.
* @param owner the owner of this proximity
* @param agents the agents
* @param radius the radius of the cone area
*/
public RadiusProximity(Steerable owner, Iterable<? extends Steerable> agents, float radius){
super(owner, agents);
this.radius = radius;
this.lastTime = 0;
}
/** Returns the radius of this proximity. */
public float getRadius(){
return radius;
}
/** Sets the radius of this proximity. */
public void setRadius(float radius){
this.radius = radius;
}
@Override
public int findNeighbors(ProximityCallback callback){
int neighborCount = 0;
// If the frame is new then avoid repeating calculations
// when this proximity is used by multiple group behaviors.
float currentTime = Timepiece.time;
if(this.lastTime != currentTime){
// Save the current time
this.lastTime = currentTime;
Vec2 ownerPosition = owner.getPosition();
// Scan the agents searching for neighbors
for(Steerable currentAgent : agents){
// Make sure the agent being examined isn't the owner
if(currentAgent != owner){
float squareDistance = ownerPosition.dst2(currentAgent.getPosition());
// The bounding radius of the current agent is taken into account
// by adding it to the range
float range = radius + currentAgent.getBoundingRadius();
// If the current agent is within the range, report it to the callback
// and tag it for further consideration.
if(squareDistance < range * range){
if(callback.reportNeighbor(currentAgent)){
currentAgent.setTagged(true);
neighborCount++;
continue;
}
}
}
// Clear the tag
currentAgent.setTagged(false);
}
}else{
// Scan the agents searching for tagged neighbors
for(Steerable currentAgent : agents){
// Make sure the agent being examined isn't the owner and that
// it's tagged.
if(currentAgent != owner && currentAgent.isTagged()){
if(callback.reportNeighbor(currentAgent)){
neighborCount++;
}
}
}
}
return neighborCount;
}
}

View File

@@ -0,0 +1,64 @@
package mindustry.ai.ai.steer.utils;
import arc.math.geom.*;
import mindustry.ai.ai.steer.utils.Path.*;
/**
* The {@code Path} for an agent having path following behavior. A path can be shared by multiple path following behaviors because
* its status is maintained in a {@link PathParam} local to each behavior.
* <p>
* The most common type of path is made up of straight line segments, which usually gives reasonably good results while keeping
* the math simple. However, some driving games use splines to get smoother curved paths, which makes the math more complex.
* @param <P> Type of path parameter implementing the {@link PathParam} interface
* @author davebaol
*/
public interface Path<P extends PathParam>{
/** Returns a new instance of the path parameter. */
P createParam();
/** Returns {@code true} if this path is open; {@code false} otherwise. */
boolean isOpen();
/** Returns the length of this path. */
float getLength();
/** Returns the first point of this path. */
Vec2 getStartPoint();
/** Returns the last point of this path. */
Vec2 getEndPoint();
/**
* Maps the given position to the nearest point along the path using the path parameter to ensure coherence and returns the
* distance of that nearest point from the start of the path.
* @param position a location in game space
* @param param the path parameter
* @return the distance of the nearest point along the path from the start of the path itself.
*/
float calculateDistance(Vec2 position, P param);
/**
* Calculates the target position on the path based on its distance from the start and the path parameter.
* @param out the target position to calculate
* @param param the path parameter
* @param targetDistance the distance of the target position from the start of the path
*/
void calculateTargetPosition(Vec2 out, P param, float targetDistance);
/**
* A path parameter used by path following behaviors to keep the path status.
* @author davebaol
*/
interface PathParam{
/** Returns the distance from the start of the path */
float getDistance();
/**
* Sets the distance from the start of the path
* @param distance the distance to set
*/
void setDistance(float distance);
}
}

View File

@@ -0,0 +1,11 @@
package mindustry.ai.ai.steer.utils;
import mindustry.ai.ai.utils.*;
/**
* A {@code RayConfiguration} is a collection of rays typically used for collision avoidance.
* @author davebaol
*/
public interface RayConfiguration{
Ray[] updateRays();
}

View File

@@ -0,0 +1,265 @@
package mindustry.ai.ai.steer.utils.paths;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.ai.ai.steer.utils.Path;
import mindustry.ai.ai.steer.utils.paths.LinePath.*;
/**
* A {@code LinePath} is a path for path following behaviors that is made up of a series of waypoints. Each waypoint is connected
* to the successor with a {@link Segment}.
* @author davebaol
* @author Daniel Holderbaum
*/
public class LinePath implements Path<LinePathParam>{
private Array<Segment> segments;
private boolean isOpen;
private float pathLength;
private Vec2 nearestPointOnCurrentSegment;
private Vec2 nearestPointOnPath;
private Vec2 tmpB;
private Vec2 tmpC;
/**
* Creates a closed {@code LinePath} for the specified {@code waypoints}.
* @param waypoints the points making up the path
* @throws IllegalArgumentException if {@code waypoints} is {@code null} or has less than two (2) waypoints.
*/
public LinePath(Array waypoints){
this(waypoints, false);
}
/**
* Creates a {@code LinePath} for the specified {@code waypoints}.
* @param waypoints the points making up the path
* @param isOpen a flag indicating whether the path is open or not
* @throws IllegalArgumentException if {@code waypoints} is {@code null} or has less than two (2) waypoints.
*/
public LinePath(Array<Vec2> waypoints, boolean isOpen){
this.isOpen = isOpen;
createPath(waypoints);
nearestPointOnCurrentSegment = waypoints.first().cpy();
nearestPointOnPath = waypoints.first().cpy();
tmpB = waypoints.first().cpy();
tmpC = waypoints.first().cpy();
}
@Override
public boolean isOpen(){
return isOpen;
}
@Override
public float getLength(){
return pathLength;
}
@Override
public Vec2 getStartPoint(){
return segments.first().begin;
}
@Override
public Vec2 getEndPoint(){
return segments.peek().end;
}
/**
* Returns the square distance of the nearest point on line segment {@code a-b}, from point {@code c}. Also, the {@code out}
* vector is assigned to the nearest point.
* @param out the output vector that contains the nearest point on return
* @param a the start point of the line segment
* @param b the end point of the line segment
* @param c the point to calculate the distance from
*/
public float calculatePointSegmentSquareDistance(Vec2 out, Vec2 a, Vec2 b, Vec2 c){
out.set(a);
tmpB.set(b);
tmpC.set(c);
Vec2 ab = tmpB.sub(a);
float abLen2 = ab.len2();
if(abLen2 != 0){
float t = (tmpC.sub(a)).dot(ab) / abLen2;
out.mulAdd(ab, Mathf.clamp(t, 0, 1));
}
return out.dst2(c);
}
@Override
public LinePathParam createParam(){
return new LinePathParam();
}
// We pass the last parameter value to the path in order to calculate the current
// parameter value. This is essential to avoid nasty problems when lines are close together.
// We should limit the algorithm to only considering areas of the path close to the previous
// parameter value. The character is unlikely to have moved far, after all.
// This technique, assuming the new value is close to the old one, is called coherence, and it is a
// feature of many geometric algorithms.
// TODO: Currently coherence is not implemented.
@Override
public float calculateDistance(Vec2 agentCurrPos, LinePathParam parameter){
// Find the nearest segment
float smallestDistance2 = Float.POSITIVE_INFINITY;
Segment nearestSegment = null;
for(int i = 0; i < segments.size; i++){
Segment segment = segments.get(i);
float distance2 = calculatePointSegmentSquareDistance(nearestPointOnCurrentSegment, segment.begin, segment.end,
agentCurrPos);
// first point
if(distance2 < smallestDistance2){
nearestPointOnPath.set(nearestPointOnCurrentSegment);
smallestDistance2 = distance2;
nearestSegment = segment;
parameter.segmentIndex = i;
}
}
// Distance from path start
float lengthOnPath = nearestSegment.cumulativeLength - nearestPointOnPath.dst(nearestSegment.end);
parameter.setDistance(lengthOnPath);
return lengthOnPath;
}
@Override
public void calculateTargetPosition(Vec2 out, LinePathParam param, float targetDistance){
if(isOpen){
// Open path support
if(targetDistance < 0){
// Clamp target distance to the min
targetDistance = 0;
}else if(targetDistance > pathLength){
// Clamp target distance to the max
targetDistance = pathLength;
}
}else{
// Closed path support
if(targetDistance < 0){
// Backwards
targetDistance = pathLength + (targetDistance % pathLength);
}else if(targetDistance > pathLength){
// Forward
targetDistance = targetDistance % pathLength;
}
}
// Walk through lines to see on which line we are
Segment desiredSegment = null;
for(int i = 0; i < segments.size; i++){
Segment segment = segments.get(i);
if(segment.cumulativeLength >= targetDistance){
desiredSegment = segment;
break;
}
}
// begin-------targetPos-------end
float distance = desiredSegment.cumulativeLength - targetDistance;
out.set(desiredSegment.begin).sub(desiredSegment.end).scl(distance / desiredSegment.length).add(desiredSegment.end);
}
/**
* Sets up this {@link Path} using the given way points.
* @param waypoints The way points of this path.
* @throws IllegalArgumentException if {@code waypoints} is {@code null} or empty.
*/
public void createPath(Array<Vec2> waypoints){
if(waypoints == null || waypoints.size < 2)
throw new IllegalArgumentException("waypoints cannot be null and must contain at least two (2) waypoints");
segments = new Array<>(waypoints.size);
pathLength = 0;
Vec2 curr = waypoints.first();
Vec2 prev = null;
for(int i = 1; i <= waypoints.size; i++){
prev = curr;
if(i < waypoints.size)
curr = waypoints.get(i);
else if(isOpen)
break; // keep the path open
else
curr = waypoints.first(); // close the path
Segment segment = new Segment(prev, curr);
pathLength += segment.length;
segment.cumulativeLength = pathLength;
segments.add(segment);
}
}
public Array<Segment> getSegments(){
return segments;
}
/**
* A {@code LinePathParam} contains the status of a {@link LinePath}.
* @author davebaol
*/
public static class LinePathParam implements Path.PathParam{
int segmentIndex;
float distance;
@Override
public float getDistance(){
return distance;
}
@Override
public void setDistance(float distance){
this.distance = distance;
}
/** Returns the index of the current segment along the path */
public int getSegmentIndex(){
return segmentIndex;
}
}
/**
* A {@code Segment} connects two consecutive waypoints of a {@link LinePath}.
* @author davebaol
*/
public static class Segment{
Vec2 begin;
Vec2 end;
float length;
float cumulativeLength;
/**
* Creates a {@code Segment} for the 2 given points.
*/
Segment(Vec2 begin, Vec2 end){
this.begin = begin;
this.end = end;
this.length = begin.dst(end);
}
/** Returns the start point of this segment. */
public Vec2 getBegin(){
return begin;
}
/** Returns the end point of this segment. */
public Vec2 getEnd(){
return end;
}
/** Returns the length of this segment. */
public float getLength(){
return length;
}
/** Returns the cumulative length from the first waypoint of the {@link LinePath} this segment belongs to. */
public float getCumulativeLength(){
return cumulativeLength;
}
}
}

View File

@@ -0,0 +1,88 @@
package mindustry.ai.ai.steer.utils.rays;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.Ray;
/**
* A {@code CentralRayWithWhiskersConfiguration} uses a long central ray and two shorter whiskers.
* <p>
* A central ray with short whiskers is often the best initial configuration to try but can make it impossible for the character
* to move down tight passages. Also, it is still susceptible to the <a
* href="../behaviors/RaycastObstacleAvoidance.html#cornerTrap">corner trap</a>, far less than the parallel configuration though.
* @author davebaol
*/
public class CentralRayWithWhiskersConfiguration extends RayConfigurationBase{
private float rayLength;
private float whiskerLength;
private float whiskerAngle;
/**
* Creates a {@code CentralRayWithWhiskersConfiguration} for the given owner where the central ray has the specified length and
* the two whiskers have the specified length and angle.
* @param owner the owner of this configuration
* @param rayLength the length of the central ray
* @param whiskerLength the length of the two whiskers (usually shorter than the central ray)
* @param whiskerAngle the angle in radians of the whiskers from the central ray
*/
public CentralRayWithWhiskersConfiguration(Steerable owner, float rayLength, float whiskerLength, float whiskerAngle){
super(owner, 3);
this.rayLength = rayLength;
this.whiskerLength = whiskerLength;
this.whiskerAngle = whiskerAngle;
}
@Override
public Ray[] updateRays(){
Vec2 ownerPosition = owner.getPosition();
Vec2 ownerVelocity = owner.getLinearVelocity();
float velocityAngle = owner.vectorToAngle(ownerVelocity);
// Update central ray
rays[0].start.set(ownerPosition);
rays[0].end.set(ownerVelocity).nor().scl(rayLength).add(ownerPosition);
// Update left ray
rays[1].start.set(ownerPosition);
owner.angleToVector(rays[1].end, velocityAngle - whiskerAngle).scl(whiskerLength).add(ownerPosition);
// Update right ray
rays[2].start.set(ownerPosition);
owner.angleToVector(rays[2].end, velocityAngle + whiskerAngle).scl(whiskerLength).add(ownerPosition);
return rays;
}
/** Returns the length of the central ray. */
public float getRayLength(){
return rayLength;
}
/** Sets the length of the central ray. */
public void setRayLength(float rayLength){
this.rayLength = rayLength;
}
/** Returns the length of the two whiskers. */
public float getWhiskerLength(){
return whiskerLength;
}
/** Sets the length of the two whiskers. */
public void setWhiskerLength(float whiskerLength){
this.whiskerLength = whiskerLength;
}
/** Returns the angle in radians of the whiskers from the central ray. */
public float getWhiskerAngle(){
return whiskerAngle;
}
/** Sets the angle in radians of the whiskers from the central ray. */
public void setWhiskerAngle(float whiskerAngle){
this.whiskerAngle = whiskerAngle;
}
}

View File

@@ -0,0 +1,73 @@
package mindustry.ai.ai.steer.utils.rays;
import arc.math.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* A {@code ParallelSideRayConfiguration} uses two rays parallel to the direction of motion. The rays have the same length and
* opposite side offset.
* <p>
* The parallel configuration works well in areas where corners are highly obtuse but is very susceptible to the <a
* href="../behaviors/RaycastObstacleAvoidance.html">corner trap</a>.
* @author davebaol
*/
public class ParallelSideRayConfiguration extends RayConfigurationBase{
private static final float HALF_PI = Mathf.PI * 0.5f;
private float length;
private float sideOffset;
/**
* Creates a {@code ParallelSideRayConfiguration} for the given owner where the two rays have the specified length and side
* offset.
* @param owner the owner of this ray configuration
* @param length the length of the rays.
* @param sideOffset the side offset of the rays.
*/
public ParallelSideRayConfiguration(Steerable owner, float length, float sideOffset){
super(owner, 2);
this.length = length;
this.sideOffset = sideOffset;
}
@Override
public Ray[] updateRays(){
float velocityAngle = owner.vectorToAngle(owner.getLinearVelocity());
// Update ray 0
owner.angleToVector(rays[0].start, velocityAngle - HALF_PI).scl(sideOffset).add(owner.getPosition());
rays[0].end.set(owner.getLinearVelocity()).nor().scl(length); // later we'll add rays[0].start;
// Update ray 1
owner.angleToVector(rays[1].start, velocityAngle + HALF_PI).scl(sideOffset).add(owner.getPosition());
rays[1].end.set(rays[0].end).add(rays[1].start);
// add start position to ray 0
rays[0].end.add(rays[0].start);
return rays;
}
/** Returns the length of the rays. */
public float getLength(){
return length;
}
/** Sets the length of the rays. */
public void setLength(float length){
this.length = length;
}
/** Returns the side offset of the rays. */
public float getSideOffset(){
return sideOffset;
}
/** Sets the side offset of the rays. */
public void setSideOffset(float sideOffset){
this.sideOffset = sideOffset;
}
}

View File

@@ -0,0 +1,48 @@
package mindustry.ai.ai.steer.utils.rays;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.steer.utils.*;
import mindustry.ai.ai.utils.*;
/**
* {@code RayConfigurationBase} is the base class for concrete ray configurations having a fixed number of rays.
* @author davebaol
*/
public abstract class RayConfigurationBase implements RayConfiguration{
protected Steerable owner;
protected Ray[] rays;
/**
* Creates a {@code RayConfigurationBase} for the given owner and the specified number of rays.
* @param owner the owner of this configuration
* @param numRays the number of rays used by this configuration
*/
public RayConfigurationBase(Steerable owner, int numRays){
this.owner = owner;
this.rays = new Ray[numRays];
for(int i = 0; i < numRays; i++)
this.rays[i] = new Ray(owner.getPosition().cpy().setZero(), owner.getPosition().cpy().setZero());
}
/** Returns the owner of this configuration. */
public Steerable getOwner(){
return owner;
}
/** Sets the owner of this configuration. */
public void setOwner(Steerable owner){
this.owner = owner;
}
/** Returns the rays of this configuration. */
public Ray[] getRays(){
return rays;
}
/** Sets the rays of this configuration. */
public void setRays(Ray[] rays){
this.rays = rays;
}
}

View File

@@ -0,0 +1,44 @@
package mindustry.ai.ai.steer.utils.rays;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* As the name suggests, a {@code SingleRayConfiguration} uses just one ray cast.
* <p>
* This configuration is useful in concave environments but grazes convex obstacles. It is not susceptible to the <a
* href="../behaviors/RaycastObstacleAvoidance.html#cornerTrap">corner trap</a>, though.
* @author davebaol
*/
public class SingleRayConfiguration extends RayConfigurationBase{
private float length;
/**
* Creates a {@code SingleRayConfiguration} for the given owner where the ray has the specified length.
* @param owner the owner of this configuration
* @param length the length of the ray
*/
public SingleRayConfiguration(Steerable owner, float length){
super(owner, 1);
this.length = length;
}
@Override
public Ray[] updateRays(){
rays[0].start.set(owner.getPosition());
rays[0].end.set(owner.getLinearVelocity()).nor().scl(length).add(rays[0].start);
return rays;
}
/** Returns the length of the ray. */
public float getLength(){
return length;
}
/** Sets the length of the ray. */
public void setLength(float length){
this.length = length;
}
}

View File

@@ -0,0 +1,48 @@
package mindustry.ai.ai.utils;
import arc.math.geom.*;
/**
* A {@code Collision} is made up of a collision point and the normal at that point of collision.
* @author davebaol
*/
public class Collision{
/** The collision point. */
public Vec2 point;
/** The normal of this collision. */
public Vec2 normal;
/**
* Creates a {@code Collision} with the given {@code point} and {@code normal}.
* @param point the point where this collision occurred
* @param normal the normal of this collision
*/
public Collision(Vec2 point, Vec2 normal){
this.point = point;
this.normal = normal;
}
/**
* Sets this collision from the given collision.
* @param collision The collision
* @return this collision for chaining.
*/
public Collision set(Collision collision){
this.point.set(collision.point);
this.normal.set(collision.normal);
return this;
}
/**
* Sets this collision from the given point and normal.
* @param point the collision point
* @param normal the normal of this collision
* @return this collision for chaining.
*/
public Collision set(Vec2 point, Vec2 normal){
this.point.set(point);
this.normal.set(normal);
return this;
}
}

View File

@@ -0,0 +1,49 @@
package mindustry.ai.ai.utils;
import arc.math.geom.*;
/**
* The {@code Location} interface represents any game object having a position and an orientation.
* @author davebaol
*/
public interface Location{
/** Returns the vector indicating the position of this location. */
Vec2 getPosition();
/**
* Returns the float value indicating the orientation of this location. The orientation is the angle in radians representing
* the direction that this location is facing.
*/
float getOrientation();
/**
* Sets the orientation of this location, i.e. the angle in radians representing the direction that this location is facing.
* @param orientation the orientation in radians
*/
void setOrientation(float orientation);
/**
* Returns the angle in radians pointing along the specified vector.
* @param vector the vector
*/
float vectorToAngle(Vec2 vector);
/**
* Returns the unit vector in the direction of the specified angle expressed in radians.
* @param outVector the output vector.
* @param angle the angle in radians.
* @return the output vector for chaining.
*/
Vec2 angleToVector(Vec2 outVector, float angle);
/**
* Creates a new location.
* <p>
* This method is used internally to instantiate locations of the correct type parameter {@code T}. This technique keeps the API
* simple and makes the API easier to use with the GWVec2 backend because avoids the use of reflection.
* @return the newly created location.
*/
Location newLocation();
}

View File

@@ -0,0 +1,48 @@
package mindustry.ai.ai.utils;
import arc.math.geom.*;
/**
* A {@code Ray} is made up of a starting point and an ending point.
* @author davebaol
*/
public class Ray{
/** The starting point of this ray. */
public Vec2 start;
/** The ending point of this ray. */
public Vec2 end;
/**
* Creates a {@code Ray} with the given {@code start} and {@code end} points.
* @param start the starting point of this ray
* @param end the starting point of this ray
*/
public Ray(Vec2 start, Vec2 end){
this.start = start;
this.end = end;
}
/**
* Sets this ray from the given ray.
* @param ray The ray
* @return this ray for chaining.
*/
public Ray set(Ray ray){
start.set(ray.start);
end.set(ray.end);
return this;
}
/**
* Sets this Ray from the given start and end points.
* @param start the starting point of this ray
* @param end the starting point of this ray
* @return this ray for chaining.
*/
public Ray set(Vec2 start, Vec2 end){
this.start.set(start);
this.end.set(end);
return this;
}
}

View File

@@ -0,0 +1,25 @@
package mindustry.ai.ai.utils;
/**
* A {@code RaycastCollisionDetector} finds the closest intersection between a ray and any object in the game world.
* @author davebaol
*/
public interface RaycastCollisionDetector{
/**
* Casts the given ray to test if it collides with any objects in the game world.
* @param ray the ray to cast.
* @return {@code true} in case of collision; {@code false} otherwise.
*/
boolean collides(Ray ray);
/**
* Find the closest collision between the given input ray and the objects in the game world. In case of collision,
* {@code outputCollision} will contain the collision point and the normal vector of the obstacle at the point of collision.
* @param outputCollision the output collision.
* @param inputRay the ray to cast.
* @return {@code true} in case of collision; {@code false} otherwise.
*/
boolean findCollision(Collision outputCollision, Ray inputRay);
}

View File

@@ -0,0 +1,13 @@
package mindustry.ai.ai.utils;
/** All units are in seconds. */
public class Timepiece{
public static float time;
public static float deltaTime;
public void update(float deltaTime){
Timepiece.deltaTime = deltaTime;
Timepiece.time = time + deltaTime;
}
}

View File

@@ -19,7 +19,7 @@ public class FormationAI extends AIController{
if(control != null){
unit.controlWeapons(control.isRotate(), control.isShooting());
// unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed));
// unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed));
if(control.isShooting()){
unit.aimLook(control.aimX(), control.aimY());
}else{