From cb6920b8be98006f0465fddf08d55ed9df4034fb Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 2 May 2020 21:34:37 -0400 Subject: [PATCH] Commander unit component --- .../DistanceAssignmentStrategy.java | 51 ++++++++++++ .../mindustry/ai/formations/Formation.java | 25 ++++-- core/src/mindustry/ai/types/FormationAI.java | 58 ++++--------- core/src/mindustry/ai/types/GroundAI.java | 2 +- core/src/mindustry/content/UnitTypes.java | 2 +- .../mindustry/entities/def/CommanderComp.java | 83 +++++++++++++++++++ core/src/mindustry/entities/def/UnitComp.java | 4 +- core/src/mindustry/entities/def/VelComp.java | 4 + .../entities/units/UnitController.java | 2 +- core/src/mindustry/input/DesktopInput.java | 32 +++++-- core/src/mindustry/input/InputHandler.java | 1 + core/src/mindustry/type/UnitType.java | 2 +- 12 files changed, 207 insertions(+), 59 deletions(-) create mode 100644 core/src/mindustry/ai/formations/DistanceAssignmentStrategy.java create mode 100644 core/src/mindustry/entities/def/CommanderComp.java diff --git a/core/src/mindustry/ai/formations/DistanceAssignmentStrategy.java b/core/src/mindustry/ai/formations/DistanceAssignmentStrategy.java new file mode 100644 index 0000000000..3a68efc915 --- /dev/null +++ b/core/src/mindustry/ai/formations/DistanceAssignmentStrategy.java @@ -0,0 +1,51 @@ +package mindustry.ai.formations; + +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; + +public class DistanceAssignmentStrategy implements SlotAssignmentStrategy{ + private final Vec3 vec = new Vec3(); + private final FormationPattern form; + + public DistanceAssignmentStrategy(FormationPattern form){ + this.form = form; + } + + @Override + public void updateSlotAssignments(Array assignments){ + IntArray slots = IntArray.range(0, assignments.size); + + for(SlotAssignment slot : assignments){ + int mindex = 0; + float mcost = Float.MAX_VALUE; + + for(int i = 0; i < slots.size; i++){ + float cost = cost(slot.member, slots.get(i)); + if(cost < mcost){ + mcost = cost; + mindex = i; + } + } + + slot.slotNumber = slots.get(mindex); + slots.removeIndex(mindex); + + } + } + + @Override + public int calculateNumberOfSlots(Array assignments){ + return assignments.size; + } + + @Override + public void removeSlotAssignment(Array assignments, int index){ + assignments.remove(index); + } + + float cost(FormationMember member, int slot){ + form.calculateSlotLocation(vec, slot); + return Mathf.dst2(member.formationPos().x, member.formationPos().y, vec.x, vec.y); + } +} diff --git a/core/src/mindustry/ai/formations/Formation.java b/core/src/mindustry/ai/formations/Formation.java index 53b7a1853a..8ef03e509b 100644 --- a/core/src/mindustry/ai/formations/Formation.java +++ b/core/src/mindustry/ai/formations/Formation.java @@ -19,7 +19,7 @@ import arc.struct.*; public class Formation{ /** A list of slots assignments. */ - Array slotAssignments; + public Array slotAssignments; /** The anchor point of this formation. */ public Vec3 anchor; @@ -81,6 +81,8 @@ public class Formation{ /** Updates the assignment of members to slots */ public void updateSlotAssignments(){ + pattern.slots = slotAssignments.size; + // Apply the strategy to update slot assignments slotAssignmentStrategy.updateSlotAssignments(slotAssignments); @@ -114,6 +116,21 @@ public class Formation{ return false; } + /** Much more efficient than adding a single member. + * @return number of members added. */ + public int addMembers(Iterable members){ + int added = 0; + for(FormationMember member : members){ + if(pattern.supportsSlots(slotAssignments.size + 1)){ + slotAssignments.add(new SlotAssignment(member, slotAssignments.size)); + added ++; + } + } + + updateSlotAssignments(); + return added; + } + /** * Adds a new member to the first available slot and updates slot assignments if the number of member is supported by the * current pattern. @@ -121,13 +138,11 @@ public class Formation{ * @return {@code false} if no more slots are available; {@code true} otherwise. */ public boolean addMember(FormationMember member){ - // Find out how many slots we have occupied - int occupiedSlots = slotAssignments.size; // Check if the pattern supports one more slot - if(pattern.supportsSlots(occupiedSlots + 1)){ + if(pattern.supportsSlots(slotAssignments.size + 1)){ // Add a new slot assignment - slotAssignments.add(new SlotAssignment(member, occupiedSlots)); + slotAssignments.add(new SlotAssignment(member, slotAssignments.size)); // Update the slot assignments and return success updateSlotAssignments(); diff --git a/core/src/mindustry/ai/types/FormationAI.java b/core/src/mindustry/ai/types/FormationAI.java index 9898ff6fa5..3ee744da56 100644 --- a/core/src/mindustry/ai/types/FormationAI.java +++ b/core/src/mindustry/ai/types/FormationAI.java @@ -1,68 +1,46 @@ 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 implements FormationMember{ - public @Nullable Unitc leader; + public Unitc leader; - private transient Vec3 target = new Vec3(); + private Vec3 target = new Vec3(); + private Formation formation; - public FormationAI(@Nullable Unitc leader){ + public FormationAI(Unitc leader, Formation formation){ this.leader = leader; + this.formation = formation; } - 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); + target.set(unit.x(), unit.y(), 0); } @Override public void update(){ - if(leader != null){ + unit.controlWeapons(leader.isRotate(), leader.isShooting()); + // unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed)); + if(leader.isShooting()){ + unit.aimLook(leader.aimX(), leader.aimY()); + }else{ - unit.controlWeapons(leader.isRotate(), leader.isShooting()); - // unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed)); - if(leader.isShooting()){ - unit.aimLook(leader.aimX(), leader.aimY()); - }else{ - - unit.lookAt(leader.rotation()); - if(!unit.vel().isZero(0.001f)){ - // 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)); } + + unit.moveAt(vec.set(target).sub(unit).limit2(unit.type().speed)); } @Override - public boolean isFollowing(Playerc player){ - return leader == player.unit(); + public boolean isBeingControlled(Unitc player){ + return leader == player; } @Override diff --git a/core/src/mindustry/ai/types/GroundAI.java b/core/src/mindustry/ai/types/GroundAI.java index 6fb8addb03..f3bb0c6a21 100644 --- a/core/src/mindustry/ai/types/GroundAI.java +++ b/core/src/mindustry/ai/types/GroundAI.java @@ -19,7 +19,7 @@ public class GroundAI extends AIController{ target = null; //TODO this is hacky, cleanup - if(unit instanceof Legsc){ + if(unit instanceof Legsc && unit.moving()){ unit.lookAt(((Legsc)unit).baseRotation()); } } diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index f4c949fbfb..7e1126fb30 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -21,7 +21,7 @@ public class UnitTypes implements ContentList{ public static @EntityDef({Unitc.class, Builderc.class}) UnitType phantom, spirit; //water - public static @EntityDef({Unitc.class, WaterMovec.class}) UnitType vanguard; + public static @EntityDef({Unitc.class, WaterMovec.class, Commanderc.class}) UnitType vanguard; @Override public void load(){ diff --git a/core/src/mindustry/entities/def/CommanderComp.java b/core/src/mindustry/entities/def/CommanderComp.java new file mode 100644 index 0000000000..33390d490f --- /dev/null +++ b/core/src/mindustry/entities/def/CommanderComp.java @@ -0,0 +1,83 @@ +package mindustry.entities.def; + +import arc.struct.*; +import arc.util.ArcAnnotate.*; +import arc.util.*; +import mindustry.ai.formations.*; +import mindustry.ai.types.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.units.*; +import mindustry.gen.*; + +/** A unit that can command other units. */ +@Component +abstract class CommanderComp implements Unitc{ + private static final Array members = new Array<>(); + + @Import float x, y, rotation; + + transient @Nullable Formation formation; + transient Array controlling = new Array<>(); + + @Override + public void update(){ + if(formation != null){ + formation.anchor.set(x, y, rotation); + formation.updateSlots(); + } + } + + @Override + public void remove(){ + clearCommand(); + } + + @Override + public void killed(){ + clearCommand(); + } + + //make sure to reset command state when the controller is switched + @Override + public void controller(UnitController unitController){ + clearCommand(); + } + + void command(Formation formation, Array units){ + clearCommand(); + + controlling.addAll(units); + for(Unitc unit : units){ + unit.controller(new FormationAI(this, formation)); + } + this.formation = formation; + + members.clear(); + for(Unitc u : units){ + members.add((FormationAI)u.controller()); + } + + Log.info(members); + Log.info(members.size); + + + //TODO doesn't handle units that don't fit a formation + formation.addMembers(members); + } + + boolean isCommanding(){ + return formation != null; + } + + void clearCommand(){ + //reset controlled units + for(Unitc unit : controlling){ + if(unit.controller().isBeingControlled(this)){ + unit.controller(unit.type().createController()); + } + } + + controlling.clear(); + formation = null; + } +} diff --git a/core/src/mindustry/entities/def/UnitComp.java b/core/src/mindustry/entities/def/UnitComp.java index e046aec590..6378aca4cf 100644 --- a/core/src/mindustry/entities/def/UnitComp.java +++ b/core/src/mindustry/entities/def/UnitComp.java @@ -59,8 +59,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I } @Override - public void controller(UnitController controller){ - this.controller = controller; + public void controller(UnitController next){ + this.controller = next; if(controller.unit() != this) controller.unit(this); } diff --git a/core/src/mindustry/entities/def/VelComp.java b/core/src/mindustry/entities/def/VelComp.java index a704ad0398..475fb8b04e 100644 --- a/core/src/mindustry/entities/def/VelComp.java +++ b/core/src/mindustry/entities/def/VelComp.java @@ -20,6 +20,10 @@ abstract class VelComp implements Posc{ vel.scl(1f - drag * Time.delta()); } + boolean moving(){ + return !vel.isZero(0.001f); + } + void move(float cx, float cy){ x += cx; y += cy; diff --git a/core/src/mindustry/entities/units/UnitController.java b/core/src/mindustry/entities/units/UnitController.java index 5109ee74b0..5fb8ac9136 100644 --- a/core/src/mindustry/entities/units/UnitController.java +++ b/core/src/mindustry/entities/units/UnitController.java @@ -15,7 +15,7 @@ public interface UnitController{ } - default boolean isFollowing(Playerc player){ + default boolean isBeingControlled(Unitc player){ return false; } } diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 14266bd308..8b8a972f5b 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -14,7 +14,8 @@ import arc.scene.ui.layout.*; import arc.util.ArcAnnotate.*; import arc.util.*; import mindustry.*; -import mindustry.ai.types.*; +import mindustry.ai.formations.*; +import mindustry.ai.formations.patterns.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.units.*; @@ -182,13 +183,28 @@ public class DesktopInput extends InputHandler{ } //TODO this is for debugging, remove later - if(Core.input.keyTap(KeyCode.q) && !player.dead()){ - Fx.commandSend.at(player); - Units.nearby(player.team(), player.x(), player.y(), 200f, u -> { - if(u.isAI()){ - u.controller(new FormationAI(player.unit())); - } - }); + if(Core.input.keyTap(KeyCode.g) && !player.dead() && player.unit() instanceof Commanderc){ + Commanderc commander = (Commanderc)player.unit(); + + if(commander.isCommanding()){ + commander.clearCommand(); + }else{ + + FormationPattern pattern = new SquareFormation(); + Formation formation = new Formation(new Vec3(player.x(), player.y(), player.unit().rotation()), pattern); + formation.slotAssignmentStrategy = new DistanceAssignmentStrategy(pattern); + + units.clear(); + + Fx.commandSend.at(player); + Units.nearby(player.team(), player.x(), player.y(), 200f, u -> { + if(u.isAI()){ + units.add(u); + } + }); + + commander.command(formation, units); + } } } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index a34471f956..bdec1fa481 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -44,6 +44,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ final static int maxLength = 100; final static Vec2 stackTrns = new Vec2(); final static Rect r1 = new Rect(), r2 = new Rect(); + final static Array units = new Array<>(); /** Distance on the back from where items originate. */ final static float backTrns = 3f; diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index e57fcac481..2e5e48f575 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -115,7 +115,7 @@ public class UnitType extends UnlockableContent{ //region drawing public void draw(Unitc unit){ - if(unit.controller().isFollowing(player)){ + if(unit.controller().isBeingControlled(player.unit())){ drawControl(unit); }