Change pathfinding processor to have a per player per frame cycle limit

This commit is contained in:
Retera 2021-01-24 14:37:12 -05:00
parent cacdf7f266
commit d4245e2e65
8 changed files with 463 additions and 287 deletions

View File

@ -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<SplatModel> {
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]);
}
}
}
}

View File

@ -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<String, SplatModel> uberSplatModels;
private final List<SplatModel> uberSplatModelsList;
private int shadowMap;
public final Map<String, Splat> splats = new HashMap<>();
public final Map<String, List<float[]>> 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);
}

View File

@ -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<Point2D.Float> 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();

View File

@ -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();
}
}

View File

@ -55,6 +55,8 @@ public class CUnitType {
private final int buildTime;
private final EnumSet<CBuildingPathingType> preventedPathingTypes;
private final EnumSet<CBuildingPathingType> 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<War3ID> unitsTrained, final List<War3ID> researchesAvailable, final CUnitRace unitRace,
final int goldCost, final int lumberCost, final int foodUsed, final int foodMade, final int buildTime,
final EnumSet<CBuildingPathingType> preventedPathingTypes,
final EnumSet<CBuildingPathingType> requiredPathingTypes) {
final EnumSet<CBuildingPathingType> 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<CBuildingPathingType> getRequiredPathingTypes() {
return this.requiredPathingTypes;
}
public float getPropWindow() {
return this.propWindow;
}
public float getTurnRate() {
return this.turnRate;
}
}

View File

@ -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<Point2D.Float> 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;
}
}
}

View File

@ -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;

View File

@ -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<PathfindingJob> 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<Point2D.Float> 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<Point2D.Float> 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<Node> openSet = new PriorityQueue<>(new Comparator<Node>() {
@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<PathfindingJob> 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<Point2D.Float> 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<Node>() {
@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<Point2D.Float> 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<Node> 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;
}
}
}