Cleanup / Functioning formation

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
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.
*/
default float getZeroLinearSpeedThreshold(){
return 0.001f;
}
/** Returns the maximum linear speed. */
default float getMaxLinearSpeed(){
return Float.MAX_VALUE;
}
/** Returns the maximum linear acceleration. */
default float getMaxLinearAcceleration(){
return Float.MAX_VALUE;
}
/** Returns the maximum angular speed. */
default float getMaxAngularSpeed(){
return Float.MAX_VALUE;
}
/** Returns the maximum angular acceleration. */
default float getMaxAngularAcceleration(){
return Float.MAX_VALUE;
}
}

View File

@@ -1,77 +0,0 @@
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#report(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#report(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 report(Steerable neighbor);
}
}

View File

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

View File

@@ -1,86 +0,0 @@
package mindustry.ai.ai.steer;
import arc.math.geom.*;
/**
* 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 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){
}
}

View File

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

View File

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

View File

@@ -1,55 +0,0 @@
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 report(Steerable neighbor){
// Accumulate neighbor velocity
averageVelocity.add(neighbor.getLinearVelocity());
return true;
}
}

View File

@@ -1,88 +0,0 @@
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. */
public 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.
*/
public float arrivalTolerance;
/** The radius for beginning to slow down */
public float decelerationRadius;
/** The time over which to achieve target speed */
public 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;
}
}

View File

@@ -1,153 +0,0 @@
package mindustry.ai.ai.steer.behaviors;
import arc.struct.*;
import mindustry.ai.ai.steer.*;
/**
* 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 = new Array<>();
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;
}
//
// Nested classes
//
public static class BehaviorAndWeight{
protected SteeringBehavior behavior;
protected float weight;
public BehaviorAndWeight(SteeringBehavior behavior, float weight){
this.behavior = behavior;
this.weight = weight;
}
public SteeringBehavior getBehavior(){
return behavior;
}
public void setBehavior(SteeringBehavior behavior){
this.behavior = behavior;
}
public float getWeight(){
return weight;
}
public void setWeight(float weight){
this.weight = weight;
}
}
}

View File

@@ -1,56 +0,0 @@
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 report(Steerable neighbor){
// Accumulate neighbor position
centerOfMass.add(neighbor.getPosition());
return true;
}
}

View File

@@ -1,119 +0,0 @@
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 report(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;
}
}

View File

@@ -1,38 +0,0 @@
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();
}
}

View File

@@ -1,49 +0,0 @@
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);
}
}

View File

@@ -1,43 +0,0 @@
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;
}
}

View File

@@ -1,94 +0,0 @@
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. */
public FlowField flowField;
/** The time in the future to predict the owner's position. Set it to 0 for non-predictive flow field following. */
public 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;
}
/**
* A {@code FlowField} defines a mapping from a location in space to a flow vector. Typically flow fields are implemented as a
* multidimensional array representing a grid of cells. In each cell of the grid lives a flow vector.
* @author davebaol
*/
public interface FlowField{
/**
* Returns the flow vector for the specified position in space.
* @param position the position to map
*/
Vec2 lookup(Vec2 position);
}
}

View File

@@ -1,124 +0,0 @@
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.*;
/**
* {@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 */
public 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. */
public float pathOffset;
/** The current position on the path */
public P pathParam;
/** The flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. It defaults to {@code true}. */
public boolean arriveEnabled;
/** The time in the future to predict the owner's position. Set it to 0 for non-predictive path following. */
public 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 current position of the internal target. This method is useful for debug purpose. */
public Vec2 getInternalTargetPosition(){
return internalTargetPosition;
}
}

View File

@@ -1,131 +0,0 @@
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. */
public Proximity proximity;
/** The distance from the boundary of the obstacle behind which to hide. */
public 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 report(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;
}
/**
* 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);
}
}

View File

@@ -1,89 +0,0 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
/**
* {@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{
public Steerable agentA;
public Steerable agentB;
public 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);
}
@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;
}
}

View File

@@ -1,362 +0,0 @@
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;
}
private static class JumpTarget extends SteerableAdapter{
Vec2 position;
Vec2 linearVelocity;
public JumpTarget(Steerable other){
this.position = null;
this.linearVelocity = other.getPosition().cpy().setZero();
}
@Override
public Vec2 getPosition(){
return position;
}
@Override
public Vec2 getLinearVelocity(){
return linearVelocity;
}
}
/**
* A {@code JumpDescriptor} contains jump information like the take-off and the landing position.
* @author davebaol
*/
public static class JumpDescriptor{
/** The position of the takeoff pad */
public Vec2 takeoffPosition;
/** The position of the landing pad */
public Vec2 landingPosition;
/** The change in position from takeoff to landing. This is calculated from the other values. */
public Vec2 delta;
/**
* Creates a {@code JumpDescriptor} with the given takeoff and landing positions.
* @param takeoffPosition the position of the takeoff pad
* @param landingPosition the position of the landing pad
*/
public JumpDescriptor(Vec2 takeoffPosition, Vec2 landingPosition){
this.takeoffPosition = takeoffPosition;
this.landingPosition = landingPosition;
this.delta = landingPosition.cpy();
set(takeoffPosition, landingPosition);
}
/**
* Sets this {@code JumpDescriptor} from the given takeoff and landing positions.
* @param takeoffPosition the position of the takeoff pad
* @param landingPosition the position of the landing pad
*/
public void set(Vec2 takeoffPosition, Vec2 landingPosition){
this.takeoffPosition.set(takeoffPosition);
this.landingPosition.set(landingPosition);
this.delta.set(landingPosition).sub(takeoffPosition);
}
}
/**
* A {@code GravityComponentHandler} is aware of the axis along which the gravity acts.
* @author davebaol
*/
public interface GravityComponentHandler{
/**
* Returns the component of the given vector along which the gravity operates.
* <p>
* Assuming a 3D coordinate system where the gravity is acting along the y-axis, this method will be implemented as follows:
*
* <pre>
* public float getComponent (Vector3 vector) {
* return vector.y;
* }
* </pre>
* <p>
* Of course, the equivalent 2D implementation will use Vector2 instead of Vector3.
* @param vector the vector
* @return the value of the component affected by gravity.
*/
float getComponent(Vec2 vector);
/**
* Sets the component of the given vector along which the gravity operates.
* <p>
* Assuming a 3D coordinate system where the gravity is acting along the y-axis, this method will be implemented as follows:
*
* <pre>
* public void setComponent (Vector3 vector, float value) {
* vector.y = value;
* }
* </pre>
* <p>
* Of course, the equivalent 2D implementation will use Vector2 instead of Vector3.
* @param vector the vector
* @param value the value of the component affected by gravity
*/
void setComponent(Vec2 vector, float value);
}
/**
* The {@code JumpCallback} allows you to know whether a jump is achievable and when to jump.
* @author davebaol
*/
public interface JumpCallback{
/**
* Reports whether the jump is achievable or not.
* <p>
* A jump is not achievable when the character's maximum linear velocity is not enough, in which case the jump behavior
* won't produce any acceleration; you might want to use pathfinding to plan a new path.
* <p>
* If the jump is achievable the run up phase will start immediately and the character will try to match the target velocity
* toward the takeoff point. This is the right moment to start the run up animation, if needed.
* @param achievable whether the jump is achievable or not.
*/
void reportAchievability(boolean achievable);
/**
* This method is called to notify that both the position and velocity of the character are good enough to jump.
* @param maxVerticalVelocity the velocity to set along the vertical axis to achieve the jump
* @param time the duration of the jump
*/
void takeoff(float maxVerticalVelocity, float time);
}
}

View File

@@ -1,42 +0,0 @@
package mindustry.ai.ai.steer.behaviors;
import mindustry.ai.ai.steer.*;
/**
* 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);
}
}

View File

@@ -1,58 +0,0 @@
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 */
public Steerable target;
/** The time over which to achieve target speed */
public 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;
}
}

View File

@@ -1,107 +0,0 @@
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. */
public 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.
*/
public Array<SteeringBehavior> behaviors = new Array<>();
/** The index of the behavior whose acceleration has been returned by the last evaluation of this priority steering. */
public 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();
}
}

View File

@@ -1,88 +0,0 @@
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 */
public Steerable target;
/** The maximum prediction time */
public 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;
}
}

View File

@@ -1,147 +0,0 @@
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 */
public RayConfiguration rayConfiguration;
/** The collision detector */
public RaycastCollisionDetector raycastCollisionDetector;
/** The minimum distance to a wall, i.e. how far to avoid collision. */
public 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;
}
}

View File

@@ -1,96 +0,0 @@
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. */
public Location target;
/** The tolerance for aligning to the target without letting small errors keep the owner swinging. */
public float alignTolerance;
/** The radius for beginning to slow down */
public float decelerationRadius;
/** The time over which to achieve target rotation speed */
public 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;
}
}

View File

@@ -1,45 +0,0 @@
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 */
public 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;
}
}

View File

@@ -1,66 +0,0 @@
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.
*/
public 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 report(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 = decayCoefficient / 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;
}
}

View File

@@ -1,114 +0,0 @@
package mindustry.ai.ai.steer.behaviors;
import arc.math.*;
import arc.math.geom.*;
import mindustry.ai.ai.steer.*;
import mindustry.ai.ai.utils.*;
/**
* {@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 */
public float wanderOffset;
/** The radius of the wander circle */
public float wanderRadius;
/** The rate, expressed in radian per second, at which the wander orientation can change */
public float wanderRate;
/** The last time the orientation of the wander target has been updated */
public float lastTime;
/** The current orientation of the wander target */
public 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.
*/
public 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;
}
}

View File

@@ -1,141 +0,0 @@
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.report(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.report(currentAgent)){
neighborCount++;
}
}
}
}
return neighborCount;
}
}

View File

@@ -1,36 +0,0 @@
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.report(currentAgent)){
neighborCount++;
}
}
}
return neighborCount;
}
}

View File

@@ -1,34 +0,0 @@
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. */
public 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;
}
}

View File

@@ -1,103 +0,0 @@
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.report(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.report(currentAgent)){
neighborCount++;
}
}
}
}
return neighborCount;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
package mindustry.ai.ai.utils;
import arc.math.*;
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
*/
default float vectorToAngle(Vec2 vector){
return Mathf.atan2(-vector.x, vector.y);
}
/**
* 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.
*/
default Vec2 angleToVector(Vec2 outVector, float angle){
return outVector.set(-Mathf.sin(angle), Mathf.cos(angle));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
package mindustry.ai.ai.utils;
import arc.math.geom.*;
public class VecLocation implements Location{
float orientation;
Vec2 position = new Vec2();
@Override
public Vec2 getPosition(){
return position;
}
@Override
public float getOrientation(){
return orientation;
}
@Override
public void setOrientation(float orientation){
this.orientation = orientation;
}
}

View File

@@ -1,4 +1,4 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import arc.struct.*;

View File

@@ -1,10 +1,8 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
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
@@ -24,54 +22,51 @@ public class Formation{
Array<SlotAssignment> slotAssignments;
/** The anchor point of this formation. */
protected Location anchor;
public Vec3 anchor;
/** The formation pattern */
protected FormationPattern pattern;
public FormationPattern pattern;
/** The strategy used to assign a member to his slot */
protected SlotAssignmentStrategy slotAssignmentStrategy;
public SlotAssignmentStrategy slotAssignmentStrategy;
/** The formation motion moderator */
protected FormationMotionModerator motionModerator;
public FormationMotionModerator motionModerator;
private final Vec2 positionOffset;
private final Mat orientationMatrix = new Mat();
/** The location representing the drift offset for the currently filled slots. */
private final Location driftOffset;
private final Vec3 driftOffset;
/**
* Creates a {@code Formation} for the specified {@code pattern} using a {@link FreeSlotAssignmentStrategy} and no motion
* moderator.
* @param anchor the anchor point of this formation, usually a {@link Steerable}. Cannot be {@code null}.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Location anchor, FormationPattern pattern){
public Formation(Vec3 anchor, FormationPattern pattern){
this(anchor, pattern, new FreeSlotAssignmentStrategy(), null);
}
/**
* Creates a {@code Formation} for the specified {@code pattern} and {@code slotAssignmentStrategy} using no motion moderator.
* @param anchor the anchor point of this formation, usually a {@link Steerable}. Cannot be {@code null}.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Location anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy){
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy){
this(anchor, pattern, slotAssignmentStrategy, null);
}
/**
* Creates a {@code Formation} for the specified {@code pattern}, {@code slotAssignmentStrategy} and {@code moderator}.
* @param anchor the anchor point of this formation, usually a {@link Steerable}. Cannot be {@code null}.
* @param anchor the anchor point of this formation, Cannot be {@code null}.
* @param pattern the pattern of this formation
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
* @param motionModerator the motion moderator. Can be {@code null} if moderation is not needed
* @throws IllegalArgumentException if the anchor point is {@code null}
*/
public Formation(Location anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy,
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy,
FormationMotionModerator motionModerator){
if(anchor == null) throw new IllegalArgumentException("The anchor point cannot be null");
this.anchor = anchor;
@@ -80,64 +75,8 @@ public class Formation{
this.motionModerator = motionModerator;
this.slotAssignments = new Array<>();
this.driftOffset = new VecLocation();
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;
this.driftOffset = new Vec3();
this.positionOffset = new Vec2(anchor.x, anchor.y).cpy();
}
/** Updates the assignment of members to slots */
@@ -146,7 +85,7 @@ public class Formation{
slotAssignmentStrategy.updateSlotAssignments(slotAssignments);
// Set the newly calculated number of slots
pattern.setNumberOfSlots(slotAssignmentStrategy.calculateNumberOfSlots(slotAssignments));
pattern.slots = slotAssignmentStrategy.calculateNumberOfSlots(slotAssignments);
// Update the drift offset if a motion moderator is set
if(motionModerator != null) motionModerator.calculateDriftOffset(driftOffset, slotAssignments, pattern);
@@ -164,7 +103,7 @@ public class Formation{
// Check if the pattern supports one more slot
if(pattern.supportsSlots(occupiedSlots)){
setPattern(pattern);
this.pattern = pattern;
// Update the slot assignments and return success
updateSlotAssignments();
@@ -236,38 +175,33 @@ public class Formation{
/** 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();
positionOffset.set(anchor);
float orientationOffset = anchor.z;
if(motionModerator != null){
positionOffset.sub(driftOffset.getPosition());
orientationOffset -= driftOffset.getOrientation();
positionOffset.sub(driftOffset);
orientationOffset -= driftOffset.z;
}
// Get the orientation of the anchor point as a matrix
orientationMatrix.idt().rotateRad(anchor.getOrientation());
orientationMatrix.idt().rotate(anchor.z);
// Go through each member in turn
for(int i = 0; i < slotAssignments.size; i++){
SlotAssignment slotAssignment = slotAssignments.get(i);
// Retrieve the location reference of the formation member to calculate the new value
Location relativeLoc = slotAssignment.member.getTargetLocation();
Vec3 relativeLoc = slotAssignment.member.formationPos();
float z = relativeLoc.z;
// Ask for the location of the slot relative to the anchor point
pattern.calculateSlotLocation(relativeLoc, slotAssignment.slotNumber);
Vec2 relativeLocPosition = relativeLoc.getPosition();
// Transform it by the anchor point's position and orientation
//relativeLocPosition.mul(orientationMatrix).add(anchor.position);
relativeLocPosition.mul(orientationMatrix);
relativeLoc.mul(orientationMatrix);
// Add the anchor and drift components
relativeLocPosition.add(positionOffset);
relativeLoc.setOrientation(relativeLoc.getOrientation() + orientationOffset);
relativeLoc.add(positionOffset.x, positionOffset.y, 0);
relativeLoc.z = z + orientationOffset;
}
// Possibly reset the anchor point if a moderator is set

View File

@@ -1,6 +1,6 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import mindustry.ai.ai.utils.*;
import arc.math.geom.*;
/**
* Game characters coordinated by a {@link Formation} must implement this interface. Any {@code FormationMember} has a target
@@ -9,7 +9,6 @@ import mindustry.ai.ai.utils.*;
* @author davebaol
*/
public interface FormationMember{
/** Returns the target location of this formation member. */
Location getTargetLocation();
Vec3 formationPos();
}

View File

@@ -1,8 +1,7 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
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
@@ -11,14 +10,13 @@ import mindustry.ai.ai.utils.*;
* @author davebaol
*/
public abstract class FormationMotionModerator{
private Location tempLocation;
private Vec3 tempLocation;
/**
* Update the anchor point to moderate formation motion. This method is called at each frame.
* @param anchor the anchor point
*/
public abstract void updateAnchorPoint(Location anchor);
public abstract void updateAnchorPoint(Vec3 anchor);
/**
* Calculates the drift offset when members are in the given set of slots for the specified pattern.
@@ -27,31 +25,26 @@ public abstract class FormationMotionModerator{
* @param pattern the pattern
* @return the given location for chaining.
*/
public Location calculateDriftOffset(Location centerOfMass, Array<SlotAssignment> slotAssignments,
FormationPattern pattern){
public Vec3 calculateDriftOffset(Vec3 centerOfMass, Array<SlotAssignment> slotAssignments, FormationPattern pattern){
// Clear the center of mass
centerOfMass.getPosition().setZero();
centerOfMass.x = centerOfMass.y = 0;
float centerOfMassOrientation = 0;
// Make sure tempLocation is instantiated
if(tempLocation == null) tempLocation = new VecLocation();
Vec2 centerOfMassPos = centerOfMass.getPosition();
Vec2 tempLocationPos = tempLocation.getPosition();
if(tempLocation == null) tempLocation = new Vec3();
// Go through each assignment and add its contribution to the center
float numberOfAssignments = slotAssignments.size;
for(int i = 0; i < numberOfAssignments; i++){
pattern.calculateSlotLocation(tempLocation, slotAssignments.get(i).slotNumber);
centerOfMassPos.add(tempLocationPos);
centerOfMassOrientation += tempLocation.getOrientation();
centerOfMass.add(tempLocation);
centerOfMassOrientation += tempLocation.z;
}
// Divide through to get the drift offset.
centerOfMassPos.scl(1f / numberOfAssignments);
centerOfMass.scl(1f / numberOfAssignments);
centerOfMassOrientation /= numberOfAssignments;
centerOfMass.setOrientation(centerOfMassOrientation);
centerOfMass.z = centerOfMassOrientation;
return centerOfMass;
}

View File

@@ -1,6 +1,6 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import mindustry.ai.ai.utils.*;
import arc.math.geom.*;
/**
* The {@code FormationPattern} interface represents the shape of a formation and generates the slot offsets, relative to its
@@ -10,21 +10,18 @@ import mindustry.ai.ai.utils.*;
* {@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);
public abstract class FormationPattern{
public int slots;
/** Returns the location of the given slot index. */
Location calculateSlotLocation(Location outLocation, int slotNumber);
public abstract Vec3 calculateSlotLocation(Vec3 out, int slot);
/**
* Returns true if the pattern can support the given number of slots
* @param slotCount the number of slots
* @return {@code true} if this pattern can support the given number of slots; {@code false} othervwise.
*/
boolean supportsSlots(int slotCount);
public boolean supportsSlots(int slotCount){
return true;
}
}

View File

@@ -1,4 +1,4 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import arc.struct.*;

View File

@@ -1,4 +1,4 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
/**

View File

@@ -1,4 +1,4 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import arc.struct.*;

View File

@@ -1,4 +1,4 @@
package mindustry.ai.ai.fma;
package mindustry.ai.formations;
import arc.struct.*;

View File

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

View File

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

View File

@@ -1,36 +1,72 @@
package mindustry.ai.types;
import arc.*;
import arc.math.geom.*;
import arc.util.ArcAnnotate.*;
import mindustry.ai.formations.*;
import mindustry.ai.formations.patterns.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
public class FormationAI extends AIController{
public @Nullable Unitc control;
public class FormationAI extends AIController implements FormationMember{
public @Nullable Unitc leader;
public FormationAI(@Nullable Unitc control){
this.control = control;
private transient Vec3 target = new Vec3();
public FormationAI(@Nullable Unitc leader){
this.leader = leader;
}
public FormationAI(){
static Formation formation;
static Vec2 vec = new Vec2();
@Override
public void init(){
if(formation == null){
Vec3 vec = new Vec3();
formation = new Formation(vec, new SquareFormation());
Core.app.addListener(new ApplicationListener(){
@Override
public void update(){
formation.updateSlots();
vec.set(leader.x(), leader.y(), leader.rotation());
}
});
}
formation.addMember(this);
}
@Override
public void update(){
if(control != null){
if(leader != null){
unit.controlWeapons(control.isRotate(), control.isShooting());
unit.controlWeapons(leader.isRotate(), leader.isShooting());
// unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed));
if(control.isShooting()){
unit.aimLook(control.aimX(), control.aimY());
if(leader.isShooting()){
unit.aimLook(leader.aimX(), leader.aimY());
}else{
unit.lookAt(unit.vel().angle());
unit.lookAt(leader.rotation());
if(!unit.vel().isZero(0.001f)){
// unit.lookAt(unit.vel().angle());
}
}
unit.moveAt(vec.set(target).sub(unit).limit2(unit.type().speed));
}
}
@Override
public boolean isFollowing(Playerc player){
return control == player.unit();
return leader == player.unit();
}
@Override
public Vec3 formationPos(){
return target;
}
}

View File

@@ -19,6 +19,7 @@ import static mindustry.Vars.*;
@Component
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc{
@Import float x, y, rotation, elevation, maxHealth;
private UnitController controller;

View File

@@ -44,9 +44,14 @@ public class AIController implements UnitController{
}
}
protected void init(){
}
@Override
public void unit(Unitc unit){
this.unit = unit;
init();
}
@Override

View File

@@ -277,6 +277,8 @@ public class UnitType extends UnlockableContent{
}
public void drawLegs(Legsc unit){
Draw.reset();
Draw.mixcol(Color.white, unit.hitTime());
float ft = Mathf.sin(unit.walkTime(), 6f, 2f + unit.hitSize() / 15f);