mirror of
https://github.com/Anuken/Mindustry.git
synced 2025-02-06 08:57:45 +07:00
WIP grouped unit arrival
This commit is contained in:
parent
276245bf3c
commit
2aab745603
@ -397,6 +397,30 @@ public class ControlPathfinder{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return 0 if nothing was hit, otherwise the packed coordinates. This is an internal function and will likely be moved - do not use!*/
|
||||
public static int raycastFast(int team, PathCost type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while(x >= 0 && y >= 0 && x < ww && y < wh){
|
||||
if(solid(team, type, x + y * wwidth, true)) return Point2.pack(x, y);
|
||||
if(x == x2 && y == y2) return 0;
|
||||
|
||||
//no diagonals
|
||||
if(2 * err + dy > dx - 2 * err){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}else{
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static boolean cast(int team, PathCost cost, int from, int to){
|
||||
return raycast(team, cost, from % wwidth, from / wwidth, to % wwidth, to / wwidth);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public class UnitCommand extends MappableContent{
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||
return (char)Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
443
core/src/mindustry/ai/UnitGroup.java
Normal file
443
core/src/mindustry/ai/UnitGroup.java
Normal file
@ -0,0 +1,443 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.Pathfinder.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class UnitGroup{
|
||||
public Seq<Unit> units = new Seq<>();
|
||||
public float[] positions;
|
||||
public volatile boolean valid;
|
||||
|
||||
public void calculateFormation(Vec2 dest, int collisionLayer){
|
||||
|
||||
float cx = 0f, cy = 0f;
|
||||
for(Unit unit : units){
|
||||
cx += unit.x;
|
||||
cy += unit.y;
|
||||
}
|
||||
cx /= units.size;
|
||||
cy /= units.size;
|
||||
positions = new float[units.size * 2];
|
||||
|
||||
//all positions are relative to the center
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
Unit unit = units.get(i);
|
||||
positions[i * 2] = unit.x - cx;
|
||||
positions[i * 2 + 1] = unit.y - cy;
|
||||
unit.command().groupIndex = i;
|
||||
}
|
||||
|
||||
//run on new thread to prevent stutter
|
||||
Vars.mainExecutor.submit(() -> {
|
||||
//unused space between circles that needs to be reached for compression to end
|
||||
float maxSpaceUsage = 0.7f;
|
||||
boolean compress = true;
|
||||
|
||||
int compressionIterations = 0;
|
||||
int physicsIterations = 0;
|
||||
int totalIterations = 0;
|
||||
int maxPhysicsIterations = Math.min(1 + (int)(Math.pow(units.size, 0.65) / 10), 6);
|
||||
|
||||
//yep, new allocations, because this is a new thread.
|
||||
IntQuadTree tree = new IntQuadTree(new Rect(0f, 0f, Vars.world.unitWidth(), Vars.world.unitHeight()),
|
||||
(index, hitbox) -> hitbox.setCentered(positions[index * 2], positions[index * 2 + 1], units.get(index).hitSize));
|
||||
IntSeq tmpseq = new IntSeq();
|
||||
Vec2 v1 = new Vec2();
|
||||
Vec2 v2 = new Vec2();
|
||||
|
||||
//this algorithm basically squeezes all the circle colliders together, then proceeds to simulate physics to push them apart across several iterations.
|
||||
//it's rather slow, but shouldn't be too much of an issue when run in a different thread
|
||||
while(totalIterations++ < 40 && physicsIterations < maxPhysicsIterations){
|
||||
float spaceUsed = 0f;
|
||||
|
||||
if(compress){
|
||||
compressionIterations ++;
|
||||
|
||||
float maxDst = 1f, totalArea = 0f;
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
v1.set(positions[a * 2], positions[a * 2 + 1]).lerp(v2.set(0f, 0f), 0.3f);
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
|
||||
float rad = units.get(a).hitSize/2f;
|
||||
|
||||
maxDst = Math.max(maxDst, v1.dst(0f, 0f) + rad);
|
||||
totalArea += Mathf.PI * rad * rad;
|
||||
}
|
||||
|
||||
//total area of bounding circle
|
||||
float boundingArea = Mathf.PI * maxDst * maxDst;
|
||||
spaceUsed = totalArea / boundingArea;
|
||||
|
||||
//ex: 60% (0.6) of the total area is used, this will not be enough to satisfy a maxSpaceUsage of 70% (0.7)
|
||||
compress = spaceUsed <= maxSpaceUsage && compressionIterations < 20;
|
||||
}
|
||||
|
||||
//uncompress units
|
||||
if(!compress || spaceUsed > 0.5f){
|
||||
physicsIterations++;
|
||||
|
||||
tree.clear();
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
tree.insert(a);
|
||||
}
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
Unit unit = units.get(a);
|
||||
float x = positions[a * 2], y = positions[a * 2 + 1], radius = unit.hitSize/2f;
|
||||
|
||||
tmpseq.clear();
|
||||
tree.intersect(x - radius, y - radius, radius * 2f, radius * 2f, tmpseq);
|
||||
for(int res = 0; res < tmpseq.size; res ++){
|
||||
int b = tmpseq.items[res];
|
||||
|
||||
//simulate collision physics
|
||||
if(a != b){
|
||||
float ox = positions[b * 2], oy = positions[b * 2 + 1];
|
||||
Unit other = units.get(b);
|
||||
|
||||
float rs = (radius + other.hitSize/2f) * 1.2f;
|
||||
float dst = Mathf.dst(x, y, ox, oy);
|
||||
|
||||
if(dst < rs){
|
||||
v2.set(x - ox, y - oy).setLength(rs - dst);
|
||||
float mass1 = unit.hitSize, mass2 = other.hitSize;
|
||||
float ms = mass1 + mass2;
|
||||
float m1 = mass2 / ms, m2 = mass1 / ms;
|
||||
float scl = 1f;
|
||||
|
||||
positions[a * 2] += v2.x * m1 * scl;
|
||||
positions[a * 2 + 1] += v2.y * m1 * scl;
|
||||
|
||||
positions[b * 2] -= v2.x * m2 * scl;
|
||||
positions[b * 2 + 1] -= v2.y * m2 * scl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//raycast from the destination to the offset to make sure it's reachable
|
||||
if(collisionLayer != PhysicsProcess.layerFlying){
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
//coordinates in world space
|
||||
float
|
||||
x = positions[a * 2] + dest.x,
|
||||
y = positions[a * 2 + 1] + dest.y;
|
||||
|
||||
Unit unit = units.get(a);
|
||||
|
||||
PathCost cost = unit.type.pathCost;
|
||||
int res = ControlPathfinder.raycastFast(unit.team.id, cost, World.toTile(dest.x), World.toTile(dest.y), World.toTile(x), World.toTile(y));
|
||||
|
||||
//collision found, make th destination the point right before the collision
|
||||
if(res != 0){
|
||||
v1.set(Point2.x(res) * Vars.tilesize - dest.x, Point2.y(res) * Vars.tilesize - dest.y);
|
||||
v1.setLength(Math.max(v1.len() - Vars.tilesize - 4f, 0));
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
}
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> Fx.debugLine.at(unit.x, unit.y, 0f, Color.green, new Vec2[]{new Vec2(dest.x, dest.y), new Vec2(x, y)}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valid = true;
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> {
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
float x = positions[i * 2], y = positions[i * 2 + 1];
|
||||
|
||||
Fx.placeBlock.at(x + dest.x, y + dest.y, 1f, Color.green);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class IntQuadTree{
|
||||
protected final Rect tmp = new Rect();
|
||||
protected static final int maxObjectsPerNode = 5;
|
||||
|
||||
public IntQuadTreeProvider prov;
|
||||
public Rect bounds;
|
||||
public IntSeq objects = new IntSeq(false, 10);
|
||||
public IntQuadTree botLeft, botRight, topLeft, topRight;
|
||||
public boolean leaf = true;
|
||||
public int totalObjects;
|
||||
|
||||
public IntQuadTree(Rect bounds, IntQuadTreeProvider prov){
|
||||
this.bounds = bounds;
|
||||
this.prov = prov;
|
||||
}
|
||||
|
||||
protected void split(){
|
||||
if(!leaf) return;
|
||||
|
||||
float subW = bounds.width / 2;
|
||||
float subH = bounds.height / 2;
|
||||
|
||||
if(botLeft == null){
|
||||
botLeft = newChild(new Rect(bounds.x, bounds.y, subW, subH));
|
||||
botRight = newChild(new Rect(bounds.x + subW, bounds.y, subW, subH));
|
||||
topLeft = newChild(new Rect(bounds.x, bounds.y + subH, subW, subH));
|
||||
topRight = newChild(new Rect(bounds.x + subW, bounds.y + subH, subW, subH));
|
||||
}
|
||||
leaf = false;
|
||||
|
||||
// Transfer objects to children if they fit entirely in one
|
||||
for(int i = 0; i < objects.size; i ++){
|
||||
int obj = objects.items[i];
|
||||
hitbox(obj);
|
||||
IntQuadTree child = getFittingChild(tmp);
|
||||
if(child != null){
|
||||
child.insert(obj);
|
||||
objects.removeIndex(i);
|
||||
i --;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void unsplit(){
|
||||
if(leaf) return;
|
||||
objects.addAll(botLeft.objects);
|
||||
objects.addAll(botRight.objects);
|
||||
objects.addAll(topLeft.objects);
|
||||
objects.addAll(topRight.objects);
|
||||
botLeft.clear();
|
||||
botRight.clear();
|
||||
topLeft.clear();
|
||||
topRight.clear();
|
||||
leaf = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an object into this node or its child nodes. This will split a leaf node if it exceeds the object limit.
|
||||
*/
|
||||
public void insert(int obj){
|
||||
hitbox(obj);
|
||||
if(!bounds.overlaps(tmp)){
|
||||
// New object not in quad tree, ignoring
|
||||
// throw an exception?
|
||||
return;
|
||||
}
|
||||
|
||||
totalObjects ++;
|
||||
|
||||
if(leaf && objects.size + 1 > maxObjectsPerNode) split();
|
||||
|
||||
if(leaf){
|
||||
// Leaf, so no need to add to children, just add to root
|
||||
objects.add(obj);
|
||||
}else{
|
||||
hitbox(obj);
|
||||
// Add to relevant child, or root if can't fit completely in a child
|
||||
IntQuadTree child = getFittingChild(tmp);
|
||||
if(child != null){
|
||||
child.insert(obj);
|
||||
}else{
|
||||
objects.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an object from this node or its child nodes.
|
||||
*/
|
||||
public boolean remove(int obj){
|
||||
boolean result;
|
||||
if(leaf){
|
||||
// Leaf, no children, remove from root
|
||||
result = objects.removeValue(obj);
|
||||
}else{
|
||||
// Remove from relevant child
|
||||
hitbox(obj);
|
||||
IntQuadTree child = getFittingChild(tmp);
|
||||
|
||||
if(child != null){
|
||||
result = child.remove(obj);
|
||||
}else{
|
||||
// Or root if object doesn't fit in a child
|
||||
result = objects.removeValue(obj);
|
||||
}
|
||||
|
||||
if(totalObjects <= maxObjectsPerNode) unsplit();
|
||||
}
|
||||
if(result){
|
||||
totalObjects --;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Removes all objects. */
|
||||
public void clear(){
|
||||
objects.clear();
|
||||
totalObjects = 0;
|
||||
if(!leaf){
|
||||
topLeft.clear();
|
||||
topRight.clear();
|
||||
botLeft.clear();
|
||||
botRight.clear();
|
||||
}
|
||||
leaf = true;
|
||||
}
|
||||
|
||||
protected IntQuadTree getFittingChild(Rect boundingBox){
|
||||
float verticalMidpoint = bounds.x + (bounds.width / 2);
|
||||
float horizontalMidpoint = bounds.y + (bounds.height / 2);
|
||||
|
||||
// Object can completely fit within the top quadrants
|
||||
boolean topQuadrant = boundingBox.y > horizontalMidpoint;
|
||||
// Object can completely fit within the bottom quadrants
|
||||
boolean bottomQuadrant = boundingBox.y < horizontalMidpoint && (boundingBox.y + boundingBox.height) < horizontalMidpoint;
|
||||
|
||||
// Object can completely fit within the left quadrants
|
||||
if(boundingBox.x < verticalMidpoint && boundingBox.x + boundingBox.width < verticalMidpoint){
|
||||
if(topQuadrant){
|
||||
return topLeft;
|
||||
}else if(bottomQuadrant){
|
||||
return botLeft;
|
||||
}
|
||||
}else if(boundingBox.x > verticalMidpoint){ // Object can completely fit within the right quadrants
|
||||
if(topQuadrant){
|
||||
return topRight;
|
||||
}else if(bottomQuadrant){
|
||||
return botRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Else, object needs to be in parent cause it can't fit completely in a quadrant
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes objects that may intersect the given rectangle.
|
||||
* <p>
|
||||
* This will never result in false positives.
|
||||
*/
|
||||
public void intersect(float x, float y, float width, float height, Intc out){
|
||||
if(!leaf){
|
||||
if(topLeft.bounds.overlaps(x, y, width, height)) topLeft.intersect(x, y, width, height, out);
|
||||
if(topRight.bounds.overlaps(x, y, width, height)) topRight.intersect(x, y, width, height, out);
|
||||
if(botLeft.bounds.overlaps(x, y, width, height)) botLeft.intersect(x, y, width, height, out);
|
||||
if(botRight.bounds.overlaps(x, y, width, height)) botRight.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
IntSeq objects = this.objects;
|
||||
|
||||
for(int i = 0; i < objects.size; i++){
|
||||
int item = objects.items[i];
|
||||
hitbox(item);
|
||||
if(tmp.overlaps(x, y, width, height)){
|
||||
out.get(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether an object overlaps this rectangle.
|
||||
* This will never result in false positives.
|
||||
*/
|
||||
public boolean any(float x, float y, float width, float height){
|
||||
if(!leaf){
|
||||
if(topLeft.bounds.overlaps(x, y, width, height) && topLeft.any(x, y, width, height)) return true;
|
||||
if(topRight.bounds.overlaps(x, y, width, height) && topRight.any(x, y, width, height)) return true;
|
||||
if(botLeft.bounds.overlaps(x, y, width, height) && botLeft.any(x, y, width, height)) return true;
|
||||
if(botRight.bounds.overlaps(x, y, width, height) && botRight.any(x, y, width, height))return true;
|
||||
}
|
||||
|
||||
IntSeq objects = this.objects;
|
||||
|
||||
for(int i = 0; i < objects.size; i++){
|
||||
int item = objects.items[i];
|
||||
hitbox(item);
|
||||
if(tmp.overlaps(x, y, width, height)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes objects that may intersect the given rectangle.
|
||||
* <p>
|
||||
* This will never result in false positives.
|
||||
*/
|
||||
public void intersect(Rect rect, Intc out){
|
||||
intersect(rect.x, rect.y, rect.width, rect.height, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the out parameter with any objects that may intersect the given rectangle.
|
||||
* <p>
|
||||
* This will result in false positives, but never a false negative.
|
||||
*/
|
||||
public void intersect(Rect toCheck, IntSeq out){
|
||||
intersect(toCheck.x, toCheck.y, toCheck.width, toCheck.height, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the out parameter with any objects that may intersect the given rectangle.
|
||||
*/
|
||||
public void intersect(float x, float y, float width, float height, IntSeq out){
|
||||
if(!leaf){
|
||||
if(topLeft.bounds.overlaps(x, y, width, height)) topLeft.intersect(x, y, width, height, out);
|
||||
if(topRight.bounds.overlaps(x, y, width, height)) topRight.intersect(x, y, width, height, out);
|
||||
if(botLeft.bounds.overlaps(x, y, width, height)) botLeft.intersect(x, y, width, height, out);
|
||||
if(botRight.bounds.overlaps(x, y, width, height)) botRight.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
IntSeq objects = this.objects;
|
||||
|
||||
for(int i = 0; i < objects.size; i++){
|
||||
int item = objects.items[i];
|
||||
hitbox(item);
|
||||
if(tmp.overlaps(x, y, width, height)){
|
||||
out.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds all quadtree objects to the specified Seq. */
|
||||
public void getObjects(IntSeq out){
|
||||
out.addAll(objects);
|
||||
|
||||
if(!leaf){
|
||||
topLeft.getObjects(out);
|
||||
topRight.getObjects(out);
|
||||
botLeft.getObjects(out);
|
||||
botRight.getObjects(out);
|
||||
}
|
||||
}
|
||||
|
||||
protected IntQuadTree newChild(Rect rect){
|
||||
return new IntQuadTree(rect, prov);
|
||||
}
|
||||
|
||||
protected void hitbox(int t){
|
||||
prov.hitbox(t, tmp);
|
||||
}
|
||||
|
||||
/**Represents an object in a QuadTree.*/
|
||||
public interface IntQuadTreeProvider{
|
||||
/**Fills the out parameter with this element's rough bounding box. This should never be smaller than the actual object, but may be larger.*/
|
||||
void hitbox(int object, Rect out);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
@ -18,21 +17,21 @@ import static mindustry.Vars.*;
|
||||
|
||||
public class CommandAI extends AIController{
|
||||
protected static final int maxCommandQueueSize = 50;
|
||||
protected static final float localInterval = 40f;
|
||||
protected static final Vec2 vecOut = new Vec2(), flockVec = new Vec2(), separation = new Vec2(), cohesion = new Vec2(), massCenter = new Vec2();
|
||||
protected static final Vec2 vecOut = new Vec2(), vecMovePos = new Vec2();
|
||||
protected static final boolean[] noFound = {false};
|
||||
|
||||
public Seq<Position> commandQueue = new Seq<>(5);
|
||||
public @Nullable Vec2 targetPos;
|
||||
public @Nullable Teamc attackTarget;
|
||||
/** Group of units that were all commanded to reach the same point.. */
|
||||
public @Nullable UnitGroup group;
|
||||
public int groupIndex = 0;
|
||||
/** All encountered unreachable buildings of this AI. Why a sequence? Because contains() is very rarely called on it. */
|
||||
public IntSeq unreachableBuildings = new IntSeq(8);
|
||||
|
||||
protected boolean stopAtTarget, stopWhenInRange;
|
||||
protected Vec2 lastTargetPos;
|
||||
protected int pathId = -1;
|
||||
protected Seq<Unit> local = new Seq<>(false);
|
||||
protected boolean flocked;
|
||||
|
||||
/** Stance, usually related to firing mode. */
|
||||
public UnitStance stance = UnitStance.shoot;
|
||||
@ -176,24 +175,6 @@ public class CommandAI extends AIController{
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(targetPos != null){
|
||||
if(timer.get(timerTarget3, localInterval) || !flocked){
|
||||
if(!flocked){
|
||||
//make sure updates are staggered randomly
|
||||
timer.reset(timerTarget3, Mathf.random(localInterval));
|
||||
}
|
||||
|
||||
local.clear();
|
||||
//TODO experiment with 2/3/4
|
||||
float size = unit.hitSize * 3f;
|
||||
unit.team.data().tree().intersect(unit.x - size / 2f, unit.y - size/2f, size, size, local);
|
||||
local.remove(unit);
|
||||
flocked = true;
|
||||
}
|
||||
}else{
|
||||
flocked = false;
|
||||
}
|
||||
|
||||
if(attackTarget != null){
|
||||
if(targetPos == null){
|
||||
targetPos = new Vec2();
|
||||
@ -210,8 +191,13 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
if(targetPos != null){
|
||||
boolean move = true;
|
||||
boolean move = true, isFinalPoint = commandQueue.size == 0;
|
||||
vecOut.set(targetPos);
|
||||
vecMovePos.set(targetPos);
|
||||
|
||||
if(group != null && group.valid && groupIndex < group.units.size){
|
||||
vecMovePos.add(group.positions[groupIndex * 2], group.positions[groupIndex * 2 + 1]);
|
||||
}
|
||||
|
||||
//TODO: should the unit stop when it finds a target?
|
||||
if(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f)){
|
||||
@ -219,10 +205,12 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
if(unit.isGrounded() && stance != UnitStance.ram){
|
||||
move = Vars.controlPath.getPathPosition(unit, pathId, targetPos, vecOut, noFound);
|
||||
move = Vars.controlPath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound);
|
||||
//we've reached the final point if the returned coordinate is equal to the supplied input
|
||||
isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f);
|
||||
|
||||
//if the path is invalid, stop trying and record the end as unreachable
|
||||
if(unit.team.isAI() && (noFound[0] || unit.isPathImpassable(World.toTile(targetPos.x), World.toTile(targetPos.y)) )){
|
||||
if(unit.team.isAI() && (noFound[0] || unit.isPathImpassable(World.toTile(vecMovePos.x), World.toTile(vecMovePos.y)) )){
|
||||
if(attackTarget instanceof Building build){
|
||||
unreachableBuildings.addUnique(build.pos());
|
||||
}
|
||||
@ -230,6 +218,8 @@ public class CommandAI extends AIController{
|
||||
finishPath();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
vecOut.set(vecMovePos);
|
||||
}
|
||||
|
||||
float engageRange = unit.type.range - 10f;
|
||||
@ -239,8 +229,6 @@ public class CommandAI extends AIController{
|
||||
target = attackTarget;
|
||||
circleAttack(80f);
|
||||
}else{
|
||||
boolean isFinalPoint = targetPos.epsilonEquals(vecOut, 4.1f) && commandQueue.size == 0;
|
||||
|
||||
moveTo(vecOut,
|
||||
attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram ? engageRange :
|
||||
unit.isGrounded() ? 0f :
|
||||
@ -255,31 +243,17 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
if(unit.isFlying()){
|
||||
unit.lookAt(targetPos);
|
||||
unit.lookAt(vecMovePos);
|
||||
}else{
|
||||
faceTarget();
|
||||
}
|
||||
|
||||
if(attackTarget == null){
|
||||
if(unit.within(targetPos, Math.max(5f, unit.hitSize / 2f))){
|
||||
finishPath();
|
||||
}else if(local.size > 1){
|
||||
int count = 0;
|
||||
for(var near : local){
|
||||
//has arrived - no current command, but last one is equal
|
||||
if(near.isCommandable() && !near.command().hasCommand() && targetPos.epsilonEquals(near.command().lastTargetPos, 0.001f)){
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
|
||||
//others have arrived at destination, so this one will too
|
||||
if(count >= Math.max(3, local.size / 2)){
|
||||
finishPath();
|
||||
}
|
||||
}
|
||||
//reached destination, end pathfinding
|
||||
if(attackTarget == null && unit.within(vecMovePos, Math.max(5f, unit.hitSize / 2f))){
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(stopWhenInRange && targetPos != null && unit.within(targetPos, engageRange * 0.9f)){
|
||||
if(stopWhenInRange && targetPos != null && unit.within(vecMovePos, engageRange * 0.9f)){
|
||||
finishPath();
|
||||
stopWhenInRange = false;
|
||||
}
|
||||
@ -292,6 +266,7 @@ public class CommandAI extends AIController{
|
||||
void finishPath(){
|
||||
Vec2 prev = targetPos;
|
||||
targetPos = null;
|
||||
|
||||
if(commandQueue.size > 0){
|
||||
var next = commandQueue.remove(0);
|
||||
if(next instanceof Teamc target){
|
||||
@ -303,6 +278,10 @@ public class CommandAI extends AIController{
|
||||
if(prev != null && stance == UnitStance.patrol){
|
||||
commandQueue.add(prev.cpy());
|
||||
}
|
||||
}else{
|
||||
if(group != null){
|
||||
group = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,11 @@ import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class PhysicsProcess implements AsyncProcess{
|
||||
private static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
public static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
|
||||
private PhysicsWorld physics;
|
||||
private Seq<PhysicRef> refs = new Seq<>(false);
|
||||
@ -58,9 +58,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
//save last position
|
||||
PhysicRef ref = entity.physref;
|
||||
|
||||
ref.body.layer =
|
||||
entity.type.allowLegStep && entity.type.legPhysicsLayer ? layerLegs :
|
||||
entity.isGrounded() ? layerGround : layerFlying;
|
||||
ref.body.layer = entity.collisionLayer();
|
||||
ref.x = entity.x;
|
||||
ref.y = entity.y;
|
||||
ref.body.local = local || entity.isLocal();
|
||||
|
@ -2558,5 +2558,23 @@ public class Fx{
|
||||
|
||||
stroke(data.region.height * scl);
|
||||
line(data.region, data.a.x + ox, data.a.y + oy, data.b.x + ox, data.b.y + oy, false);
|
||||
}).layer(Layer.groundUnit + 5f);
|
||||
}).layer(Layer.groundUnit + 5f),
|
||||
|
||||
debugLine = new Effect(90f, 1000000000000f, e -> {
|
||||
if(!(e.data instanceof Vec2[] vec)) return;
|
||||
|
||||
Draw.color(e.color);
|
||||
Lines.stroke(1f);
|
||||
|
||||
if(vec.length == 2){
|
||||
Lines.line(vec[0].x, vec[0].y, vec[1].x, vec[1].y);
|
||||
}else{
|
||||
Lines.beginLine();
|
||||
for(Vec2 v : vec)
|
||||
Lines.linePoint(v.x, v.y);
|
||||
Lines.endLine();
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
});
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
@ -390,6 +391,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
controller(controller);
|
||||
}
|
||||
|
||||
/** @return the collision layer to use for unit physics. Returning anything outside of PhysicsProcess contents will crash the game. */
|
||||
public int collisionLayer(){
|
||||
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
|
||||
}
|
||||
|
||||
/** @return pathfinder path type for calculating costs */
|
||||
public int pathType(){
|
||||
return Pathfinder.costGround;
|
||||
|
@ -17,6 +17,7 @@ import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
@ -46,6 +47,9 @@ import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
//not sure where else to put this - maps unique commands based on position to a list of units that will be turned into a unit group
|
||||
static ObjectMap<Vec2, Seq<Unit>> queuedCommands = new ObjectMap<>();
|
||||
|
||||
/** Used for dropping items. */
|
||||
final static float playerSelectRange = mobile ? 17f : 11f;
|
||||
final static IntSeq removed = new IntSeq();
|
||||
@ -227,7 +231,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget, boolean queueCommand){
|
||||
public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget, boolean queueCommand, boolean finalBatch){
|
||||
if(player == null || unitIds == null) return;
|
||||
|
||||
//why did I ever think this was a good idea
|
||||
@ -240,6 +244,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
Teamc teamTarget = buildTarget == null ? unitTarget : buildTarget;
|
||||
Vec2 targetAsVec = new Vec2().set(teamTarget != null ? teamTarget : posTarget);
|
||||
Seq<Unit> toAdd = queuedCommands.get(targetAsVec, Seq::new);
|
||||
boolean anyCommandedTarget = false;
|
||||
|
||||
for(int id : unitIds){
|
||||
@ -276,6 +282,37 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
if(!headless && player != Vars.player){
|
||||
control.input.selectedUnits.remove(unit);
|
||||
}
|
||||
|
||||
toAdd.add(unit);
|
||||
}
|
||||
}
|
||||
|
||||
//in the "final batch" of commands, assign formations based on EVERYTHING that was commanded.
|
||||
if(finalBatch){
|
||||
//each physics layer has its own group
|
||||
UnitGroup[] groups = new UnitGroup[PhysicsProcess.layers];
|
||||
var units = queuedCommands.remove(targetAsVec);
|
||||
|
||||
for(Unit unit : units){
|
||||
if(unit.controller() instanceof CommandAI ai){
|
||||
//only assign a group when this is not a queued command
|
||||
if(ai.commandQueue.size == 0 && unitIds.length > 1){
|
||||
int layer = unit.collisionLayer();
|
||||
if(groups[layer] == null){
|
||||
groups[layer] = new UnitGroup();
|
||||
}
|
||||
|
||||
groups[layer].units.add(unit);
|
||||
ai.group = groups[layer];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < groups.length; i ++){
|
||||
var group = groups[i];
|
||||
if(group != null && group.units.size > 0){
|
||||
group.calculateFormation(targetAsVec, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -934,10 +971,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
if(ids.length > maxChunkSize){
|
||||
for(int i = 0; i < ids.length; i += maxChunkSize){
|
||||
int[] data = Arrays.copyOfRange(ids, i, Math.min(i + maxChunkSize, ids.length));
|
||||
Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue);
|
||||
Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, i + maxChunkSize >= ids.length);
|
||||
}
|
||||
}else{
|
||||
Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue);
|
||||
Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ public class PointDefenseWeapon extends Weapon{
|
||||
rotate = true;
|
||||
useAmmo = false;
|
||||
useAttackRange = false;
|
||||
targetInterval = 10f;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
Reference in New Issue
Block a user