From d4245e2e65b4e147ca4df6cf984107ec330c0b25 Mon Sep 17 00:00:00 2001 From: Retera Date: Sun, 24 Jan 2021 14:37:12 -0500 Subject: [PATCH] Change pathfinding processor to have a per player per frame cycle limit --- .../viewer5/handlers/w3x/SplatModel.java | 22 +- .../handlers/w3x/environment/Terrain.java | 15 +- .../handlers/w3x/simulation/CSimulation.java | 25 +- .../handlers/w3x/simulation/CUnit.java | 2 - .../handlers/w3x/simulation/CUnitType.java | 14 +- .../simulation/behaviors/CBehaviorMove.java | 182 ++++--- .../w3x/simulation/data/CUnitData.java | 6 +- .../pathing/CPathfindingProcessor.java | 484 ++++++++++-------- 8 files changed, 463 insertions(+), 287 deletions(-) diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java index ffa3c76..2c79b02 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java @@ -20,7 +20,7 @@ import com.etheller.warsmash.viewer5.ViewerTextureRenderable; * move around the unit selection circles without memory allocations? For now I * plan to simply port the RivSoft stuff, and come back later. */ -public class SplatModel { +public class SplatModel implements Comparable { private static final int MAX_VERTICES = 65000; private static final float NO_ABS_HEIGHT = -257f; private final ViewerTextureRenderable texture; @@ -514,4 +514,24 @@ public class SplatModel { this.absoluteHeights.size() * 4, RenderMathUtils.wrap(this.absoluteHeights)); } } + + @Override + public int compareTo(final SplatModel other) { + if (this.locations.isEmpty()) { + if (other.locations.isEmpty()) { + return 0; + } + else { + return 1; + } + } + else { + if (other.locations.isEmpty()) { + return -1; + } + else { + return Float.compare(this.locations.get(0)[4], other.locations.get(0)[4]); + } + } + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java index 232ab5a..fbd765c 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java @@ -7,6 +7,7 @@ import java.nio.Buffer; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -120,6 +121,7 @@ public class Terrain { public final DataTable uberSplatTable; private final Map uberSplatModels; + private final List uberSplatModelsList; private int shadowMap; public final Map splats = new HashMap<>(); public final Map> shadows = new HashMap<>(); @@ -414,6 +416,7 @@ public class Terrain { this.centerOffset = w3eFile.getCenterOffset(); this.uberSplatModels = new LinkedHashMap<>(); + this.uberSplatModelsList = new ArrayList<>(); this.mapBounds = w3iFile.getCameraBoundsComplements(); this.shaderMapBounds = new float[] { (this.mapBounds[0] * 128.0f) + this.centerOffset[0], (this.mapBounds[2] * 128.0f) + this.centerOffset[1], @@ -1040,7 +1043,7 @@ public class Terrain { gl.glUniform1f(shader.getUniformLocation("u_lightTextureHeight"), terrainLightsTexture.getHeight()); // Render the cliffs - for (final SplatModel splat : this.uberSplatModels.values()) { + for (final SplatModel splat : this.uberSplatModelsList) { if (splat.isNoDepthTest() == onTopLayer) { splat.render(gl, shader); } @@ -1432,16 +1435,18 @@ public class Terrain { (Texture) this.viewer.load(path, PathSolver.DEFAULT, null), splat.locations, this.centerOffset, splat.unitMapping.isEmpty() ? null : splat.unitMapping, false, false); splatModel.color[3] = splat.opacity; - this.uberSplatModels.put(path, splatModel); + this.addSplatBatchModel(path, splatModel); } } public void removeSplatBatchModel(final String path) { - this.uberSplatModels.remove(path); + this.uberSplatModelsList.remove(this.uberSplatModels.remove(path)); } public void addSplatBatchModel(final String path, final SplatModel model) { this.uberSplatModels.put(path, model); + this.uberSplatModelsList.add(model); + Collections.sort(this.uberSplatModelsList); } public SplatMover addUberSplat(final String path, final float x, final float y, final float z, final float scale, @@ -1450,7 +1455,7 @@ public class Terrain { if (splatModel == null) { splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(path, PathSolver.DEFAULT, null), new ArrayList<>(), this.centerOffset, new ArrayList<>(), unshaded, noDepthTest); - this.uberSplatModels.put(path, splatModel); + this.addSplatBatchModel(path, splatModel); } return splatModel.add(x - scale, y - scale, x + scale, y + scale, z, this.centerOffset); } @@ -1462,7 +1467,7 @@ public class Terrain { splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(texture, PathSolver.DEFAULT, null), new ArrayList<>(), this.centerOffset, new ArrayList<>(), false, false); splatModel.color[3] = opacity; - this.uberSplatModels.put(texture, splatModel); + this.addSplatBatchModel(texture, splatModel); } return splatModel.add(x, y, x2, y2, zDepthUpward, this.centerOffset); } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java index e9aec31..5bb923f 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java @@ -19,6 +19,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; @@ -48,7 +49,7 @@ public class CSimulation { private int gameTurnTick = 0; private final PathingGrid pathingGrid; private final CWorldCollision worldCollision; - private final CPathfindingProcessor pathfindingProcessor; + private final CPathfindingProcessor[] pathfindingProcessors; private final CGameplayConstants gameplayConstants; private final Random seededRandom; private float currentGameDayTimeElapsed; @@ -75,7 +76,10 @@ public class CSimulation { this.newProjectiles = new ArrayList<>(); this.handleIdAllocator = new HandleIdAllocator(); this.worldCollision = new CWorldCollision(entireMapBounds, this.gameplayConstants.getMaxCollisionRadius()); - this.pathfindingProcessor = new CPathfindingProcessor(pathingGrid, this.worldCollision); + this.pathfindingProcessors = new CPathfindingProcessor[WarsmashConstants.MAX_PLAYERS]; + for (int i = 0; i < this.pathfindingProcessors.length; i++) { + this.pathfindingProcessors[i] = new CPathfindingProcessor(pathingGrid, this.worldCollision); + } this.seededRandom = seededRandom; this.players = new ArrayList<>(); for (int i = 0; i < (WarsmashConstants.MAX_PLAYERS - 4); i++) { @@ -175,13 +179,19 @@ public class CSimulation { return this.pathingGrid; } - public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + public void findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, - final boolean allowSmoothing) { - return this.pathfindingProcessor.findNaiveSlowPath(ignoreIntersectionsWithThisUnit, + final boolean allowSmoothing, final CBehaviorMove queueItem) { + final int playerIndex = queueItem.getUnit().getPlayerIndex(); + this.pathfindingProcessors[playerIndex].findNaiveSlowPath(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, startX, startY, goal, movementType, collisionSize, - allowSmoothing); + allowSmoothing, queueItem); + } + + public void removeFromPathfindingQueue(final CBehaviorMove behaviorMove) { + final int playerIndex = behaviorMove.getUnit().getPlayerIndex(); + this.pathfindingProcessors[playerIndex].removeFromPathfindingQueue(behaviorMove); } public void update() { @@ -207,6 +217,9 @@ public class CSimulation { } this.projectiles.addAll(this.newProjectiles); this.newProjectiles.clear(); + for (final CPathfindingProcessor pathfindingProcessor : this.pathfindingProcessors) { + pathfindingProcessor.update(this); + } this.gameTurnTick++; this.currentGameDayTimeElapsed = (this.currentGameDayTimeElapsed + WarsmashConstants.SIMULATION_STEP_TIME) % this.gameplayConstants.getGameDayLength(); diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java index e16e779..4598942 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java @@ -317,8 +317,6 @@ public class CUnit extends CWidget { this.currentBehavior.begin(game); } if (this.currentBehavior.getHighlightOrderId() != lastBehaviorHighlightOrderId) { - System.out.println("order ID change detected from " - + this.currentBehavior.getHighlightOrderId() + " to " + lastBehaviorHighlightOrderId); this.stateNotifier.ordersChanged(); } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java index 5015faa..9130586 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java @@ -55,6 +55,8 @@ public class CUnitType { private final int buildTime; private final EnumSet preventedPathingTypes; private final EnumSet requiredPathingTypes; + private final float propWindow; + private final float turnRate; public CUnitType(final String name, final int life, final int manaInitial, final int manaMaximum, final int speed, final int defense, final String abilityList, final boolean isBldg, final MovementType movementType, @@ -66,7 +68,7 @@ public class CUnitType { final List unitsTrained, final List researchesAvailable, final CUnitRace unitRace, final int goldCost, final int lumberCost, final int foodUsed, final int foodMade, final int buildTime, final EnumSet preventedPathingTypes, - final EnumSet requiredPathingTypes) { + final EnumSet requiredPathingTypes, final float propWindow, final float turnRate) { this.name = name; this.life = life; this.manaInitial = manaInitial; @@ -101,6 +103,8 @@ public class CUnitType { this.buildTime = buildTime; this.preventedPathingTypes = preventedPathingTypes; this.requiredPathingTypes = requiredPathingTypes; + this.propWindow = propWindow; + this.turnRate = turnRate; } public String getName() { @@ -238,4 +242,12 @@ public class CUnitType { public EnumSet getRequiredPathingTypes() { return this.requiredPathingTypes; } + + public float getPropWindow() { + return this.propWindow; + } + + public float getTurnRate() { + return this.turnRate; + } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java index 383c693..3255241 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java @@ -21,6 +21,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CPathfindingProcessor; public class CBehaviorMove implements CBehavior { + private static boolean ALWAYS_INTERRUPT_MOVE = false; private static final Rectangle tempRect = new Rectangle(); private final CUnit unit; @@ -41,6 +42,9 @@ public class CBehaviorMove implements CBehavior { private CRangedBehavior rangedBehavior; private boolean firstUpdate = true; private boolean disableCollision = false; + private boolean pathfindingActive = false; + private boolean firstPathfindJob = false; + private boolean pathfindingFailedGiveUp; public CBehaviorMove reset(final int highlightOrderId, final AbilityTarget target) { target.visit(this.targetVisitingResetter.reset(highlightOrderId)); @@ -69,6 +73,7 @@ public class CBehaviorMove implements CBehavior { this.searchCycles = 0; this.followUnit = null; this.firstUpdate = true; + this.pathfindingFailedGiveUp = false; } private void internalResetMove(final int highlightOrderId, final CUnit followUnit) { @@ -82,6 +87,7 @@ public class CBehaviorMove implements CBehavior { this.searchCycles = 0; this.followUnit = followUnit; this.firstUpdate = true; + this.pathfindingFailedGiveUp = false; } @Override @@ -100,6 +106,9 @@ public class CBehaviorMove implements CBehavior { this.unit.setPointAndCheckUnstuck(this.unit.getX(), this.unit.getY(), simulation); this.firstUpdate = false; } + if (this.pathfindingFailedGiveUp) { + return this.unit.pollNextOrderBehavior(simulation); + } final float prevX = this.unit.getX(); final float prevY = this.unit.getY(); @@ -116,72 +125,31 @@ public class CBehaviorMove implements CBehavior { final float startFloatingX = prevX; final float startFloatingY = prevY; if (this.path == null) { - if (this.followUnit != null) { - this.target.x = this.followUnit.getX(); - this.target.y = this.followUnit.getY(); - } - this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, - this.target, movementType, collisionSize, true); - System.out.println("init path " + this.path); - // check for smoothing - if (!this.path.isEmpty()) { - float lastX = startFloatingX; - float lastY = startFloatingY; - float smoothingGroupStartX = startFloatingX; - float smoothingGroupStartY = startFloatingY; - final Point2D.Float firstPathElement = this.path.get(0); - double totalPathDistance = firstPathElement.distance(lastX, lastY); - lastX = firstPathElement.x; - lastY = firstPathElement.y; - int smoothingStartIndex = -1; - for (int i = 0; i < (this.path.size() - 1); i++) { - final Point2D.Float nextPossiblePathElement = this.path.get(i + 1); - totalPathDistance += nextPossiblePathElement.distance(lastX, lastY); - if ((totalPathDistance < (1.15 - * nextPossiblePathElement.distance(smoothingGroupStartX, smoothingGroupStartY))) - && pathingGrid.isPathable((smoothingGroupStartX + nextPossiblePathElement.x) / 2, - (smoothingGroupStartY + nextPossiblePathElement.y) / 2, movementType)) { - if (smoothingStartIndex == -1) { - smoothingStartIndex = i; - } - } - else { - if (smoothingStartIndex != -1) { - for (int j = i - 1; j >= smoothingStartIndex; j--) { - this.path.remove(j); - } - i = smoothingStartIndex; - } - smoothingStartIndex = -1; - final Point2D.Float smoothGroupNext = this.path.get(i); - smoothingGroupStartX = smoothGroupNext.x; - smoothingGroupStartY = smoothGroupNext.y; - totalPathDistance = nextPossiblePathElement.distance(smoothGroupNext); - } - lastX = nextPossiblePathElement.x; - lastY = nextPossiblePathElement.y; - } - if (smoothingStartIndex != -1) { - for (int j = smoothingStartIndex; j < (this.path.size() - 1); j++) { - final Point2D.Float removed = this.path.remove(j); - } + if (!this.pathfindingActive) { + if (this.followUnit != null) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); } + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, this.target, + movementType, collisionSize, true, this); + this.pathfindingActive = true; + this.firstPathfindJob = true; } } else if ((this.followUnit != null) && (this.path.size() > 1) && (this.target.distance(this.followUnit.getX(), this.followUnit.getY()) > (0.1 * this.target.distance(this.unit.getX(), this.unit.getY())))) { this.target.x = this.followUnit.getX(); this.target.y = this.followUnit.getY(); - this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, - this.target, movementType, collisionSize, this.searchCycles < 4); - System.out.println("new path (for target) " + this.path); - if (this.path.isEmpty()) { - return this.unit.pollNextOrderBehavior(simulation); + if (this.pathfindingActive) { + simulation.removeFromPathfindingQueue(this); } + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, this.target, + movementType, collisionSize, this.searchCycles < 4, this); + this.pathfindingActive = true; } float currentTargetX; float currentTargetY; - if (this.path.isEmpty()) { + if ((this.path == null) || this.path.isEmpty()) { if (this.followUnit != null) { currentTargetX = this.followUnit.getX(); currentTargetY = this.followUnit.getY(); @@ -212,8 +180,8 @@ public class CBehaviorMove implements CBehavior { } float facing = this.unit.getFacing(); float delta = goalAngle - facing; - final float propulsionWindow = simulation.getUnitData().getPropulsionWindow(this.unit.getTypeId()); - final float turnRate = simulation.getUnitData().getTurnRate(this.unit.getTypeId()); + final float propulsionWindow = this.unit.getUnitType().getPropWindow(); + final float turnRate = this.unit.getUnitType().getTurnRate(); final int speed = this.unit.getSpeed(); if (delta < -180) { @@ -235,7 +203,7 @@ public class CBehaviorMove implements CBehavior { facing += angleToAdd; this.unit.setFacing(facing); } - if (absDelta < propulsionWindow) { + if ((this.path != null) && !this.pathfindingActive && (absDelta < propulsionWindow)) { final float speedTick = speed * WarsmashConstants.SIMULATION_STEP_TIME; double continueDistance = speedTick; do { @@ -262,9 +230,9 @@ public class CBehaviorMove implements CBehavior { tempRect.set(this.unit.getCollisionRectangle()); tempRect.setCenter(nextX, nextY); if ((movementType == null) || (pathingGrid.isPathable(nextX, nextY, movementType, collisionSize)// ((int) - // collisionSize - // / 16) - // * 16 + // collisionSize + // / 16) + // * 16 && !worldCollision.intersectsAnythingOtherThan(tempRect, this.unit, movementType))) { this.unit.setPoint(nextX, nextY, worldCollision); if (done) { @@ -341,12 +309,12 @@ public class CBehaviorMove implements CBehavior { this.target.x = this.followUnit.getX(); this.target.y = this.followUnit.getY(); } - this.path = simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, - this.target, movementType, collisionSize, this.searchCycles < 4); - this.searchCycles++; - System.out.println("new path " + this.path); - if (this.path.isEmpty() || (this.searchCycles > 5)) { - return this.unit.pollNextOrderBehavior(simulation); + if (!this.pathfindingActive) { + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, + this.target, movementType, collisionSize, this.searchCycles < 4, this); + this.pathfindingActive = true; + this.searchCycles++; + return this; } } this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.WALK, SequenceUtils.EMPTY, 1.0f, @@ -408,6 +376,86 @@ public class CBehaviorMove implements CBehavior { @Override public void end(final CSimulation game) { + if (ALWAYS_INTERRUPT_MOVE) { + game.removeFromPathfindingQueue(this); + this.pathfindingActive = false; + } + } + public CUnit getUnit() { + return this.unit; + } + + public void pathFound(final List waypoints, final CSimulation simulation) { + this.pathfindingActive = false; + + final float prevX = this.unit.getX(); + final float prevY = this.unit.getY(); + + MovementType movementType = this.unit.getUnitType().getMovementType(); + if (movementType == null) { + movementType = MovementType.DISABLED; + } + else if ((movementType == MovementType.FOOT) && this.disableCollision) { + movementType = MovementType.FOOT_NO_COLLISION; + } + final PathingGrid pathingGrid = simulation.getPathingGrid(); + final CWorldCollision worldCollision = simulation.getWorldCollision(); + final float collisionSize = this.unit.getUnitType().getCollisionSize(); + final float startFloatingX = prevX; + final float startFloatingY = prevY; + + this.path = waypoints; + if (this.firstPathfindJob) { + this.firstPathfindJob = false; + System.out.println("init path " + this.path); + // check for smoothing + if (!this.path.isEmpty()) { + float lastX = startFloatingX; + float lastY = startFloatingY; + float smoothingGroupStartX = startFloatingX; + float smoothingGroupStartY = startFloatingY; + final Point2D.Float firstPathElement = this.path.get(0); + double totalPathDistance = firstPathElement.distance(lastX, lastY); + lastX = firstPathElement.x; + lastY = firstPathElement.y; + int smoothingStartIndex = -1; + for (int i = 0; i < (this.path.size() - 1); i++) { + final Point2D.Float nextPossiblePathElement = this.path.get(i + 1); + totalPathDistance += nextPossiblePathElement.distance(lastX, lastY); + if ((totalPathDistance < (1.15 + * nextPossiblePathElement.distance(smoothingGroupStartX, smoothingGroupStartY))) + && pathingGrid.isPathable((smoothingGroupStartX + nextPossiblePathElement.x) / 2, + (smoothingGroupStartY + nextPossiblePathElement.y) / 2, movementType)) { + if (smoothingStartIndex == -1) { + smoothingStartIndex = i; + } + } + else { + if (smoothingStartIndex != -1) { + for (int j = i - 1; j >= smoothingStartIndex; j--) { + this.path.remove(j); + } + i = smoothingStartIndex; + } + smoothingStartIndex = -1; + final Point2D.Float smoothGroupNext = this.path.get(i); + smoothingGroupStartX = smoothGroupNext.x; + smoothingGroupStartY = smoothGroupNext.y; + totalPathDistance = nextPossiblePathElement.distance(smoothGroupNext); + } + lastX = nextPossiblePathElement.x; + lastY = nextPossiblePathElement.y; + } + if (smoothingStartIndex != -1) { + for (int j = smoothingStartIndex; j < (this.path.size() - 1); j++) { + final Point2D.Float removed = this.path.remove(j); + } + } + } + } + else if (this.path.isEmpty() || (this.searchCycles > 6)) { + this.pathfindingFailedGiveUp = true; + } } } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java index 7b4ce48..13e5bc7 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java @@ -238,6 +238,9 @@ public class CUnitData { final float moveHeight = unitType.getFieldAsFloat(MOVE_HEIGHT, 0); final String movetp = unitType.getFieldAsString(MOVE_TYPE, 0); final float collisionSize = unitType.getFieldAsFloat(COLLISION_SIZE, 0); + final float propWindow = unitType.getFieldAsFloat(PROPULSION_WINDOW, 0); + final float turnRate = unitType.getFieldAsFloat(TURN_RATE, 0); + final boolean isBldg = unitType.getFieldAsBoolean(IS_BLDG, 0); final PathingGrid.MovementType movementType = PathingGrid.getMovementType(movetp); final String unitName = unitType.getFieldAsString(NAME, 0); @@ -425,7 +428,8 @@ public class CUnitData { isBldg, movementType, moveHeight, collisionSize, classifications, attacks, armorType, raise, decay, defenseType, impactZ, buildingPathingPixelMap, deathTime, targetedAs, acquisitionRange, minimumAttackRange, structuresBuilt, unitsTrained, researchesAvailable, unitRace, goldCost, - lumberCost, foodUsed, foodMade, buildTime, preventedPathingTypes, requiredPathingTypes); + lumberCost, foodUsed, foodMade, buildTime, preventedPathingTypes, requiredPathingTypes, propWindow, + turnRate); this.unitIdToUnitType.put(typeId, unitTypeInstance); } return unitTypeInstance; diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java index 0af31d0..da77227 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java @@ -4,19 +4,24 @@ import java.awt.geom.Point2D; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; import java.util.LinkedList; -import java.util.List; import java.util.PriorityQueue; import com.badlogic.gdx.math.Rectangle; import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWorldCollision; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; public class CPathfindingProcessor { private static final Rectangle tempRect = new Rectangle(); private final PathingGrid pathingGrid; private final CWorldCollision worldCollision; + private final LinkedList moveQueue = new LinkedList<>(); + // things with modified state per current job: private final Node[][] nodes; private final Node[][] cornerNodes; private final Node[] goalSet = new Node[4]; @@ -52,217 +57,29 @@ public class CPathfindingProcessor { * * @param start * @param goal + * @param playerIndex + * @param queueItem * @return */ - public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, final float startX, - final float startY, final Point2D.Float goal, final PathingGrid.MovementType movementType, - final float collisionSize, final boolean allowSmoothing) { - return findNaiveSlowPath(ignoreIntersectionsWithThisUnit, null, startX, startY, goal, movementType, - collisionSize, allowSmoothing); - } - - public List findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + public void findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, - final boolean allowSmoothing) { - final float goalX = goal.x; - final float goalY = goal.y; - float weightForHittingWalls = 1E9f; - if (!this.pathingGrid.isPathable(goalX, goalY, movementType, collisionSize) || !isPathableDynamically(goalX, - goalY, ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, movementType)) { - weightForHittingWalls = 5E2f; - } - System.out.println("beginning findNaiveSlowPath for " + startX + "," + startY + "," + goalX + "," + goalY); - if ((startX == goalX) && (startY == goalY)) { - return Collections.emptyList(); - } - tempRect.set(0, 0, collisionSize * 2, collisionSize * 2); - Node[][] searchGraph; - GridMapping gridMapping; - if (isCollisionSizeBetterSuitedForCorners(collisionSize)) { - searchGraph = this.cornerNodes; - gridMapping = GridMapping.CORNERS; - System.out.println("using corners"); - } - else { - searchGraph = this.nodes; - gridMapping = GridMapping.CELLS; - System.out.println("using cells"); - } - final int goalCellY = gridMapping.getY(this.pathingGrid, goalY); - final int goalCellX = gridMapping.getX(this.pathingGrid, goalX); - final Node mostLikelyGoal = searchGraph[goalCellY][goalCellX]; - final double bestGoalDistance = mostLikelyGoal.point.distance(goalX, goalY); - Arrays.fill(this.goalSet, null); - this.goals = 0; - for (int i = goalCellX - 1; i <= (goalCellX + 1); i++) { - for (int j = goalCellY - 1; j <= (goalCellY + 1); j++) { - final Node possibleGoal = searchGraph[j][i]; - if (possibleGoal.point.distance(goalX, goalY) <= bestGoalDistance) { - this.goalSet[this.goals++] = possibleGoal; - } - } - } - final int startGridY = gridMapping.getY(this.pathingGrid, startY); - final int startGridX = gridMapping.getX(this.pathingGrid, startX); - for (int i = 0; i < searchGraph.length; i++) { - for (int j = 0; j < searchGraph[i].length; j++) { - final Node node = searchGraph[i][j]; - node.g = Float.POSITIVE_INFINITY; - node.f = Float.POSITIVE_INFINITY; - node.cameFrom = null; - node.cameFromDirection = null; - } - } - final PriorityQueue openSet = new PriorityQueue<>(new Comparator() { - @Override - public int compare(final Node a, final Node b) { - return Double.compare(f(a), f(b)); - } - }); + final boolean allowSmoothing, final CBehaviorMove queueItem) { + this.moveQueue.offer(new PathfindingJob(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + startX, startY, goal, movementType, collisionSize, allowSmoothing, queueItem)); + } - final Node start = searchGraph[startGridY][startGridX]; - int startGridMinX; - int startGridMinY; - int startGridMaxX; - int startGridMaxY; - if (startX > start.point.x) { - startGridMinX = startGridX; - startGridMaxX = startGridX + 1; - } - else if (startX < start.point.x) { - startGridMinX = startGridX - 1; - startGridMaxX = startGridX; - } - else { - startGridMinX = startGridX; - startGridMaxX = startGridX; - } - if (startY > start.point.y) { - startGridMinY = startGridY; - startGridMaxY = startGridY + 1; - } - else if (startY < start.point.y) { - startGridMinY = startGridY - 1; - startGridMaxY = startGridY; - } - else { - startGridMinY = startGridY; - startGridMaxY = startGridY; - } - for (int cellX = startGridMinX; cellX <= startGridMaxX; cellX++) { - for (int cellY = startGridMinY; cellY <= startGridMaxY; cellY++) { - if ((cellX >= 0) && (cellX < this.pathingGrid.getWidth()) && (cellY >= 0) - && (cellY < this.pathingGrid.getHeight())) { - final Node possibleNode = searchGraph[cellY][cellX]; - final float x = possibleNode.point.x; - final float y = possibleNode.point.y; - if (pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, startX, - startY, movementType, collisionSize, x, y)) { - - final double tentativeScore = possibleNode.point.distance(startX, startY); - possibleNode.g = tentativeScore; - possibleNode.f = tentativeScore + h(possibleNode); - openSet.add(possibleNode); - - } - else { - final double tentativeScore = weightForHittingWalls; - possibleNode.g = tentativeScore; - possibleNode.f = tentativeScore + h(possibleNode); - openSet.add(possibleNode); - - } - } + public void removeFromPathfindingQueue(final CBehaviorMove behaviorMove) { + // TODO because of silly java things, this remove is O(N) for now, + // we could do some refactors to make it O(1) but do we care? + final Iterator iterator = this.moveQueue.iterator(); + while (iterator.hasNext()) { + final PathfindingJob job = iterator.next(); + if (job.queueItem == behaviorMove) { + iterator.remove(); } } - int searchIterations = 0; - while (!openSet.isEmpty() && searchIterations < 150000) { - Node current = openSet.poll(); - if (isGoal(current)) { - final LinkedList totalPath = new LinkedList<>(); - Direction lastCameFromDirection = null; - - if ((current.cameFrom != null) - && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, - current.point.x, current.point.y, movementType, collisionSize, goalX, goalY) - && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, - current.cameFrom.point.x, current.cameFrom.point.y, movementType, collisionSize, - current.point.x, current.point.y) - && pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, - current.cameFrom.point.x, current.cameFrom.point.y, movementType, collisionSize, goalX, - goalY) - && allowSmoothing) { - // do some basic smoothing to walk straight to the goal if it is not obstructed, - // skipping the last grid location - totalPath.addFirst(goal); - current = current.cameFrom; - } - else { - totalPath.addFirst(goal); - totalPath.addFirst(current.point); - } - lastCameFromDirection = current.cameFromDirection; - Node lastNode = null; - while (current.cameFrom != null) { - lastNode = current; - current = current.cameFrom; - if ((lastCameFromDirection == null) || (current.cameFromDirection != lastCameFromDirection) - || (current.cameFromDirection == null)) { - if ((current.cameFromDirection != null) || (lastNode == null) - || !pathableBetween(ignoreIntersectionsWithThisUnit, - ignoreIntersectionsWithThisSecondUnit, startX, startY, movementType, - collisionSize, current.point.x, current.point.y) - || !pathableBetween(ignoreIntersectionsWithThisUnit, - ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, - movementType, collisionSize, lastNode.point.x, lastNode.point.y) - || !pathableBetween(ignoreIntersectionsWithThisUnit, - ignoreIntersectionsWithThisSecondUnit, startX, startY, movementType, - collisionSize, lastNode.point.x, lastNode.point.y) - || !allowSmoothing) { - // Add the point if it's not the first one, or if we can only complete - // the journey by specifically walking to the first one - totalPath.addFirst(current.point); - lastCameFromDirection = current.cameFromDirection; - } - } - } - return totalPath; - } - - for (final Direction direction : Direction.VALUES) { - final float x = current.point.x + (direction.xOffset * 32); - final float y = current.point.y + (direction.yOffset * 32); - if (this.pathingGrid.contains(x, y)) { - double turnCost; - if ((current.cameFromDirection != null) && (direction != current.cameFromDirection)) { - turnCost = 0.25; - } - else { - turnCost = 0; - } - double tentativeScore = current.g + ((direction.length + turnCost) * 32); - if (!pathableBetween(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, - current.point.x, current.point.y, movementType, collisionSize, x, y)) { - tentativeScore += (direction.length) * weightForHittingWalls; - } - final Node neighbor = searchGraph[gridMapping.getY(this.pathingGrid, y)][gridMapping - .getX(this.pathingGrid, x)]; - if (tentativeScore < neighbor.g) { - neighbor.cameFrom = current; - neighbor.cameFromDirection = direction; - neighbor.g = tentativeScore; - neighbor.f = tentativeScore + h(neighbor); - if (!openSet.contains(neighbor)) { - openSet.add(neighbor); - } - } - } - } - searchIterations++; - } - return Collections.emptyList(); } private boolean pathableBetween(final CUnit ignoreIntersectionsWithThisUnit, @@ -384,4 +201,263 @@ public class CPathfindingProcessor { }; } + + public void update(final CSimulation simulation) { + int workIterations = 0; + JobsLoop: while (!this.moveQueue.isEmpty()) { + final PathfindingJob job = this.moveQueue.peek(); + if (!job.jobStarted) { + job.jobStarted = true; + System.out.println("starting job with smoothing=" + job.allowSmoothing); + workIterations += 5; // setup of job predicted cost + job.goalX = job.goal.x; + job.goalY = job.goal.y; + job.weightForHittingWalls = 1E9f; + if (!this.pathingGrid.isPathable(job.goalX, job.goalY, job.movementType, job.collisionSize) + || !isPathableDynamically(job.goalX, job.goalY, job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.movementType)) { + job.weightForHittingWalls = 5E2f; + } + System.out.println("beginning findNaiveSlowPath for " + job.startX + "," + job.startY + "," + job.goalX + + "," + job.goalY); + if ((job.startX == job.goalX) && (job.startY == job.goalY)) { + job.queueItem.pathFound(Collections.emptyList(), simulation); + this.moveQueue.poll(); + continue JobsLoop; + } + tempRect.set(0, 0, job.collisionSize * 2, job.collisionSize * 2); + if (isCollisionSizeBetterSuitedForCorners(job.collisionSize)) { + job.searchGraph = this.cornerNodes; + job.gridMapping = GridMapping.CORNERS; + System.out.println("using corners"); + } + else { + job.searchGraph = this.nodes; + job.gridMapping = GridMapping.CELLS; + System.out.println("using cells"); + } + final int goalCellY = job.gridMapping.getY(this.pathingGrid, job.goalY); + final int goalCellX = job.gridMapping.getX(this.pathingGrid, job.goalX); + final Node mostLikelyGoal = job.searchGraph[goalCellY][goalCellX]; + final double bestGoalDistance = mostLikelyGoal.point.distance(job.goalX, job.goalY); + Arrays.fill(this.goalSet, null); + this.goals = 0; + for (int i = goalCellX - 1; i <= (goalCellX + 1); i++) { + for (int j = goalCellY - 1; j <= (goalCellY + 1); j++) { + final Node possibleGoal = job.searchGraph[j][i]; + if (possibleGoal.point.distance(job.goalX, job.goalY) <= bestGoalDistance) { + this.goalSet[this.goals++] = possibleGoal; + } + } + } + final int startGridY = job.gridMapping.getY(this.pathingGrid, job.startY); + final int startGridX = job.gridMapping.getX(this.pathingGrid, job.startX); + for (int i = 0; i < job.searchGraph.length; i++) { + for (int j = 0; j < job.searchGraph[i].length; j++) { + final Node node = job.searchGraph[i][j]; + node.g = Float.POSITIVE_INFINITY; + node.f = Float.POSITIVE_INFINITY; + node.cameFrom = null; + node.cameFromDirection = null; + workIterations++; + } + } + job.openSet = new PriorityQueue<>(new Comparator() { + @Override + public int compare(final Node a, final Node b) { + return Double.compare(f(a), f(b)); + } + }); + + job.start = job.searchGraph[startGridY][startGridX]; + if (job.startX > job.start.point.x) { + job.startGridMinX = startGridX; + job.startGridMaxX = startGridX + 1; + } + else if (job.startX < job.start.point.x) { + job.startGridMinX = startGridX - 1; + job.startGridMaxX = startGridX; + } + else { + job.startGridMinX = startGridX; + job.startGridMaxX = startGridX; + } + if (job.startY > job.start.point.y) { + job.startGridMinY = startGridY; + job.startGridMaxY = startGridY + 1; + } + else if (job.startY < job.start.point.y) { + job.startGridMinY = startGridY - 1; + job.startGridMaxY = startGridY; + } + else { + job.startGridMinY = startGridY; + job.startGridMaxY = startGridY; + } + for (int cellX = job.startGridMinX; cellX <= job.startGridMaxX; cellX++) { + for (int cellY = job.startGridMinY; cellY <= job.startGridMaxY; cellY++) { + if ((cellX >= 0) && (cellX < this.pathingGrid.getWidth()) && (cellY >= 0) + && (cellY < this.pathingGrid.getHeight())) { + final Node possibleNode = job.searchGraph[cellY][cellX]; + final float x = possibleNode.point.x; + final float y = possibleNode.point.y; + if (pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, job.movementType, + job.collisionSize, x, y)) { + + final double tentativeScore = possibleNode.point.distance(job.startX, job.startY); + possibleNode.g = tentativeScore; + possibleNode.f = tentativeScore + h(possibleNode); + job.openSet.add(possibleNode); + + } + else { + final double tentativeScore = job.weightForHittingWalls; + possibleNode.g = tentativeScore; + possibleNode.f = tentativeScore + h(possibleNode); + job.openSet.add(possibleNode); + + } + } + } + } + } + + while (!job.openSet.isEmpty()) { + Node current = job.openSet.poll(); + if (isGoal(current)) { + final LinkedList totalPath = new LinkedList<>(); + Direction lastCameFromDirection = null; + + if ((current.cameFrom != null) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, job.goalX, job.goalY) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.cameFrom.point.x, + current.cameFrom.point.y, job.movementType, job.collisionSize, current.point.x, + current.point.y) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.cameFrom.point.x, + current.cameFrom.point.y, job.movementType, job.collisionSize, job.goalX, job.goalY) + && job.allowSmoothing) { + // do some basic smoothing to walk straight to the goal if it is not obstructed, + // skipping the last grid location + totalPath.addFirst(job.goal); + current = current.cameFrom; + } + else { + totalPath.addFirst(job.goal); + totalPath.addFirst(current.point); + } + lastCameFromDirection = current.cameFromDirection; + Node lastNode = null; + while (current.cameFrom != null) { + lastNode = current; + current = current.cameFrom; + if ((lastCameFromDirection == null) || (current.cameFromDirection != lastCameFromDirection) + || (current.cameFromDirection == null)) { + if ((current.cameFromDirection != null) || (lastNode == null) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, + job.movementType, job.collisionSize, current.point.x, current.point.y) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, lastNode.point.x, lastNode.point.y) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, + job.movementType, job.collisionSize, lastNode.point.x, lastNode.point.y) + || !job.allowSmoothing) { + // Add the point if it's not the first one, or if we can only complete + // the journey by specifically walking to the first one + totalPath.addFirst(current.point); + lastCameFromDirection = current.cameFromDirection; + } + } + } + job.queueItem.pathFound(totalPath, simulation); + this.moveQueue.poll(); + continue JobsLoop; + } + + for (final Direction direction : Direction.VALUES) { + final float x = current.point.x + (direction.xOffset * 32); + final float y = current.point.y + (direction.yOffset * 32); + if (this.pathingGrid.contains(x, y)) { + double turnCost; + if ((current.cameFromDirection != null) && (direction != current.cameFromDirection)) { + turnCost = 0.25; + } + else { + turnCost = 0; + } + double tentativeScore = current.g + ((direction.length + turnCost) * 32); + if (!pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, x, y)) { + tentativeScore += (direction.length) * job.weightForHittingWalls; + } + final Node neighbor = job.searchGraph[job.gridMapping.getY(this.pathingGrid, y)][job.gridMapping + .getX(this.pathingGrid, x)]; + if (tentativeScore < neighbor.g) { + neighbor.cameFrom = current; + neighbor.cameFromDirection = direction; + neighbor.g = tentativeScore; + neighbor.f = tentativeScore + h(neighbor); + if (!job.openSet.contains(neighbor)) { + job.openSet.add(neighbor); + } + } + } + } + workIterations++; + if (workIterations >= 15000) { + // breaking jobs loop will implicitly exit without calling pathFound() below + break JobsLoop; + } + } + job.queueItem.pathFound(Collections.emptyList(), simulation); + this.moveQueue.poll(); + } + } + + public static final class PathfindingJob { + private final CUnit ignoreIntersectionsWithThisUnit; + private final CUnit ignoreIntersectionsWithThisSecondUnit; + private final float startX; + private final float startY; + private final Point2D.Float goal; + private final MovementType movementType; + private final float collisionSize; + private final boolean allowSmoothing; + private final CBehaviorMove queueItem; + private boolean jobStarted; + public float goalY; + public float goalX; + public float weightForHittingWalls; + Node[][] searchGraph; + GridMapping gridMapping; + PriorityQueue openSet; + Node start; + int startGridMinX; + int startGridMinY; + int startGridMaxX; + int startGridMaxY; + + public PathfindingJob(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing, final CBehaviorMove queueItem) { + this.ignoreIntersectionsWithThisUnit = ignoreIntersectionsWithThisUnit; + this.ignoreIntersectionsWithThisSecondUnit = ignoreIntersectionsWithThisSecondUnit; + this.startX = startX; + this.startY = startY; + this.goal = goal; + this.movementType = movementType; + this.collisionSize = collisionSize; + this.allowSmoothing = allowSmoothing; + this.queueItem = queueItem; + this.jobStarted = false; + } + } }