Added steering AI classes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
278
core/src/mindustry/ai/ai/fma/Formation.java
Normal file
278
core/src/mindustry/ai/ai/fma/Formation.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
core/src/mindustry/ai/ai/fma/FormationMember.java
Normal file
15
core/src/mindustry/ai/ai/fma/FormationMember.java
Normal 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();
|
||||
}
|
||||
59
core/src/mindustry/ai/ai/fma/FormationMotionModerator.java
Normal file
59
core/src/mindustry/ai/ai/fma/FormationMotionModerator.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
30
core/src/mindustry/ai/ai/fma/FormationPattern.java
Normal file
30
core/src/mindustry/ai/ai/fma/FormationPattern.java
Normal 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);
|
||||
}
|
||||
33
core/src/mindustry/ai/ai/fma/FreeSlotAssignmentStrategy.java
Normal file
33
core/src/mindustry/ai/ai/fma/FreeSlotAssignmentStrategy.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
29
core/src/mindustry/ai/ai/fma/SlotAssignment.java
Normal file
29
core/src/mindustry/ai/ai/fma/SlotAssignment.java
Normal 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;
|
||||
}
|
||||
}
|
||||
20
core/src/mindustry/ai/ai/fma/SlotAssignmentStrategy.java
Normal file
20
core/src/mindustry/ai/ai/fma/SlotAssignmentStrategy.java
Normal 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);
|
||||
|
||||
}
|
||||
166
core/src/mindustry/ai/ai/fma/SoftRoleSlotAssignmentStrategy.java
Normal file
166
core/src/mindustry/ai/ai/fma/SoftRoleSlotAssignmentStrategy.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
39
core/src/mindustry/ai/ai/steer/GroupBehavior.java
Normal file
39
core/src/mindustry/ai/ai/steer/GroupBehavior.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
44
core/src/mindustry/ai/ai/steer/Limiter.java
Normal file
44
core/src/mindustry/ai/ai/steer/Limiter.java
Normal 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);
|
||||
}
|
||||
77
core/src/mindustry/ai/ai/steer/Proximity.java
Normal file
77
core/src/mindustry/ai/ai/steer/Proximity.java
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
33
core/src/mindustry/ai/ai/steer/Steerable.java
Normal file
33
core/src/mindustry/ai/ai/steer/Steerable.java
Normal 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);
|
||||
|
||||
}
|
||||
111
core/src/mindustry/ai/ai/steer/SteerableAdapter.java
Normal file
111
core/src/mindustry/ai/ai/steer/SteerableAdapter.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
95
core/src/mindustry/ai/ai/steer/SteeringAcceleration.java
Normal file
95
core/src/mindustry/ai/ai/steer/SteeringAcceleration.java
Normal 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());
|
||||
}
|
||||
}
|
||||
138
core/src/mindustry/ai/ai/steer/SteeringBehavior.java
Normal file
138
core/src/mindustry/ai/ai/steer/SteeringBehavior.java
Normal 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();
|
||||
}
|
||||
}
|
||||
83
core/src/mindustry/ai/ai/steer/behaviors/Alignment.java
Normal file
83
core/src/mindustry/ai/ai/steer/behaviors/Alignment.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
180
core/src/mindustry/ai/ai/steer/behaviors/Arrive.java
Normal file
180
core/src/mindustry/ai/ai/steer/behaviors/Arrive.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
181
core/src/mindustry/ai/ai/steer/behaviors/BlendedSteering.java
Normal file
181
core/src/mindustry/ai/ai/steer/behaviors/BlendedSteering.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
82
core/src/mindustry/ai/ai/steer/behaviors/Cohesion.java
Normal file
82
core/src/mindustry/ai/ai/steer/behaviors/Cohesion.java
Normal 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;
|
||||
}
|
||||
}
|
||||
146
core/src/mindustry/ai/ai/steer/behaviors/CollisionAvoidance.java
Normal file
146
core/src/mindustry/ai/ai/steer/behaviors/CollisionAvoidance.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
70
core/src/mindustry/ai/ai/steer/behaviors/Evade.java
Normal file
70
core/src/mindustry/ai/ai/steer/behaviors/Evade.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
101
core/src/mindustry/ai/ai/steer/behaviors/Face.java
Normal file
101
core/src/mindustry/ai/ai/steer/behaviors/Face.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
75
core/src/mindustry/ai/ai/steer/behaviors/Flee.java
Normal file
75
core/src/mindustry/ai/ai/steer/behaviors/Flee.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
153
core/src/mindustry/ai/ai/steer/behaviors/FollowFlowField.java
Normal file
153
core/src/mindustry/ai/ai/steer/behaviors/FollowFlowField.java
Normal 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);
|
||||
}
|
||||
}
|
||||
247
core/src/mindustry/ai/ai/steer/behaviors/FollowPath.java
Normal file
247
core/src/mindustry/ai/ai/steer/behaviors/FollowPath.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
209
core/src/mindustry/ai/ai/steer/behaviors/Hide.java
Normal file
209
core/src/mindustry/ai/ai/steer/behaviors/Hide.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
182
core/src/mindustry/ai/ai/steer/behaviors/Interpose.java
Normal file
182
core/src/mindustry/ai/ai/steer/behaviors/Interpose.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
411
core/src/mindustry/ai/ai/steer/behaviors/Jump.java
Normal file
411
core/src/mindustry/ai/ai/steer/behaviors/Jump.java
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
117
core/src/mindustry/ai/ai/steer/behaviors/MatchVelocity.java
Normal file
117
core/src/mindustry/ai/ai/steer/behaviors/MatchVelocity.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
163
core/src/mindustry/ai/ai/steer/behaviors/PrioritySteering.java
Normal file
163
core/src/mindustry/ai/ai/steer/behaviors/PrioritySteering.java
Normal 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;
|
||||
}
|
||||
}
|
||||
144
core/src/mindustry/ai/ai/steer/behaviors/Pursue.java
Normal file
144
core/src/mindustry/ai/ai/steer/behaviors/Pursue.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
183
core/src/mindustry/ai/ai/steer/behaviors/ReachOrientation.java
Normal file
183
core/src/mindustry/ai/ai/steer/behaviors/ReachOrientation.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
87
core/src/mindustry/ai/ai/steer/behaviors/Seek.java
Normal file
87
core/src/mindustry/ai/ai/steer/behaviors/Seek.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
108
core/src/mindustry/ai/ai/steer/behaviors/Separation.java
Normal file
108
core/src/mindustry/ai/ai/steer/behaviors/Separation.java
Normal 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;
|
||||
}
|
||||
}
|
||||
259
core/src/mindustry/ai/ai/steer/behaviors/Wander.java
Normal file
259
core/src/mindustry/ai/ai/steer/behaviors/Wander.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
47
core/src/mindustry/ai/ai/steer/limiters/AngularLimiter.java
Normal file
47
core/src/mindustry/ai/ai/steer/limiters/AngularLimiter.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
80
core/src/mindustry/ai/ai/steer/limiters/FullLimiter.java
Normal file
80
core/src/mindustry/ai/ai/steer/limiters/FullLimiter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
47
core/src/mindustry/ai/ai/steer/limiters/LinearLimiter.java
Normal file
47
core/src/mindustry/ai/ai/steer/limiters/LinearLimiter.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
129
core/src/mindustry/ai/ai/steer/limiters/NullLimiter.java
Normal file
129
core/src/mindustry/ai/ai/steer/limiters/NullLimiter.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
104
core/src/mindustry/ai/ai/steer/proximities/RadiusProximity.java
Normal file
104
core/src/mindustry/ai/ai/steer/proximities/RadiusProximity.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
64
core/src/mindustry/ai/ai/steer/utils/Path.java
Normal file
64
core/src/mindustry/ai/ai/steer/utils/Path.java
Normal 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);
|
||||
}
|
||||
}
|
||||
11
core/src/mindustry/ai/ai/steer/utils/RayConfiguration.java
Normal file
11
core/src/mindustry/ai/ai/steer/utils/RayConfiguration.java
Normal 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();
|
||||
}
|
||||
265
core/src/mindustry/ai/ai/steer/utils/paths/LinePath.java
Normal file
265
core/src/mindustry/ai/ai/steer/utils/paths/LinePath.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
48
core/src/mindustry/ai/ai/utils/Collision.java
Normal file
48
core/src/mindustry/ai/ai/utils/Collision.java
Normal 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;
|
||||
}
|
||||
}
|
||||
49
core/src/mindustry/ai/ai/utils/Location.java
Normal file
49
core/src/mindustry/ai/ai/utils/Location.java
Normal 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();
|
||||
}
|
||||
48
core/src/mindustry/ai/ai/utils/Ray.java
Normal file
48
core/src/mindustry/ai/ai/utils/Ray.java
Normal 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;
|
||||
}
|
||||
}
|
||||
25
core/src/mindustry/ai/ai/utils/RaycastCollisionDetector.java
Normal file
25
core/src/mindustry/ai/ai/utils/RaycastCollisionDetector.java
Normal 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);
|
||||
}
|
||||
13
core/src/mindustry/ai/ai/utils/Timepiece.java
Normal file
13
core/src/mindustry/ai/ai/utils/Timepiece.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user