Implement FollowEntityAI strategy | Discard other passage abilities than BLOCK and ALLOW

This commit is contained in:
2021-03-08 19:38:10 +01:00
parent 8b09f16827
commit bbae364908
6 changed files with 69 additions and 43 deletions

View File

@@ -8,27 +8,42 @@ import com.bartlomiejpluta.base.api.util.pathfinding.PathFinder;
import org.joml.Vector2i; import org.joml.Vector2i;
public class FollowEntityAI implements AI { public class FollowEntityAI implements AI {
private static final float recalculateInterval = 0.5f;
private final NPC npc; private final NPC npc;
private final Entity target; private final Entity target;
private final PathFinder pathFinder = new AstarPathFinder(); private final PathFinder pathFinder = new AstarPathFinder(100);
public FollowEntityAI(NPC npc, Entity target) { private final int range;
private float accumulator = 0.0f;
public FollowEntityAI(NPC npc, Entity target, int range) {
this.npc = npc; this.npc = npc;
this.range = range;
this.target = target; this.target = target;
} }
@Override @Override
public void nextActivity(ObjectLayer layer, float dt) { public void nextActivity(ObjectLayer layer, float dt) {
if (!npc.isMoving() && npc.manhattanDistance(target) > 1) { var distance = npc.manhattanDistance(target);
var path = pathFinder.findPath(layer, npc.getCoordinates(), target.getCoordinates(), 30);
if (!npc.isMoving() && distance > 1 && distance < range && accumulator >= recalculateInterval) {
var path = pathFinder.findPath(layer, npc.getCoordinates(), target.getCoordinates());
if (!path.isEmpty()) { if (!path.isEmpty()) {
accumulator = recalculateInterval;
var node = new Vector2i(path.getLast()).sub(npc.getCoordinates()); var node = new Vector2i(path.getLast()).sub(npc.getCoordinates());
var direction = Direction.ofVector(node); var direction = Direction.ofVector(node);
var movement = npc.prepareMovement(direction); var movement = npc.prepareMovement(direction);
layer.pushMovement(movement); layer.pushMovement(movement);
} else {
accumulator = 0.0f;
} }
} }
accumulator += dt;
} }
} }

View File

@@ -82,7 +82,7 @@ public class FollowPathAI implements AI {
public boolean perform(NPC npc, ObjectLayer layer, float dt) { public boolean perform(NPC npc, ObjectLayer layer, float dt) {
Movement movement = npc.prepareMovement(direction); Movement movement = npc.prepareMovement(direction);
if (ignore || layer.isMovementPossible(movement)) { if (ignore || layer.isTileReachable(movement.getTo())) {
layer.pushMovement(movement); layer.pushMovement(movement);
return true; return true;
} }

View File

@@ -4,6 +4,7 @@ import com.bartlomiejpluta.base.api.game.entity.Entity;
import com.bartlomiejpluta.base.api.game.entity.Movement; import com.bartlomiejpluta.base.api.game.entity.Movement;
import com.bartlomiejpluta.base.api.game.map.layer.base.Layer; import com.bartlomiejpluta.base.api.game.map.layer.base.Layer;
import com.bartlomiejpluta.base.api.game.rule.Rule; import com.bartlomiejpluta.base.api.game.rule.Rule;
import org.joml.Vector2i;
import java.util.List; import java.util.List;
@@ -26,7 +27,7 @@ public interface ObjectLayer extends Layer {
PassageAbility[][] getPassageMap(); PassageAbility[][] getPassageMap();
boolean isMovementPossible(Movement movement); boolean isTileReachable(Vector2i tileCoordinates);
void pushMovement(Movement movement); void pushMovement(Movement movement);
} }

View File

@@ -4,6 +4,7 @@ import com.bartlomiejpluta.base.api.game.map.layer.object.ObjectLayer;
import com.bartlomiejpluta.base.api.game.map.layer.object.PassageAbility; import com.bartlomiejpluta.base.api.game.map.layer.object.PassageAbility;
import org.joml.Vector2i; import org.joml.Vector2i;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Objects; import java.util.Objects;
import java.util.PriorityQueue; import java.util.PriorityQueue;
@@ -30,8 +31,14 @@ public class AstarPathFinder implements PathFinder {
new Vector2i(0, 1) new Vector2i(0, 1)
}; };
private final int maxNodes;
public AstarPathFinder(int maxNodes) {
this.maxNodes = maxNodes;
}
@Override @Override
public LinkedList<Vector2i> findPath(ObjectLayer layer, Vector2i start, Vector2i end, int range) { public LinkedList<Vector2i> findPath(ObjectLayer layer, Vector2i start, Vector2i end) {
var columns = layer.getMap().getColumns(); var columns = layer.getMap().getColumns();
var rows = layer.getMap().getRows(); var rows = layer.getMap().getRows();
@@ -47,12 +54,20 @@ public class AstarPathFinder implements PathFinder {
// We are starting with one open node (the start one) end empty closed lists // We are starting with one open node (the start one) end empty closed lists
var open = new PriorityQueue<Node>(); var open = new PriorityQueue<Node>();
var closed = new LinkedList<Node>(); var closed = new HashSet<Node>();
open.add(startNode); open.add(startNode);
// As long as there are at least one open node // As long as there are at least one open node
while (!open.isEmpty()) { while (!open.isEmpty()) {
// A safety valve which ideally should be used only and only
// if the target is not reachable (the path does not exist at all).
// It determines the maximum algorithm depth
// It's not the part of model A* algorithm.
if (closed.size() > maxNodes) {
return new LinkedList<>();
}
// We are retrieving the node with the **smallest** f score // We are retrieving the node with the **smallest** f score
// (That's the way the Astar.compare() comparator works) // (That's the way the Astar.compare() comparator works)
// And the same time we are removing the node from open list // And the same time we are removing the node from open list
@@ -81,9 +96,9 @@ public class AstarPathFinder implements PathFinder {
// We are limiting the algorithm to given range // We are limiting the algorithm to given range
// If current neighbour distance to start node exceeds given range parameter // If current neighbour distance to start node exceeds given range parameter
// we are getting rid of this neighbour // we are getting rid of this neighbour
if (manhattanDistance(startNode.position, position) > range) { //if (manhattanDistance(startNode.position, position) > range) {
continue; // continue;
} //}
// Define new neighbour // Define new neighbour
var neighbour = new Node(position); var neighbour = new Node(position);
@@ -97,7 +112,18 @@ public class AstarPathFinder implements PathFinder {
} }
// Get rid of nodes that are not reachable (blocked or something is staying on there) // Get rid of nodes that are not reachable (blocked or something is staying on there)
var reachable = layer.getPassageMap()[position.y][position.x] == PassageAbility.ALLOW; //
// ASSUME, that the end tile **always is** reachable, even if actually it is not.
// That means, if you want to have empty list if target actually is not reachable,
// you need to implement that condition by yourself.
// The reason for that is the fact, that when ObjectLayer is checking if
// the current tile is reachable via isTileReachable() method.
// If the target position is actually an entity which is blocking (does not allow other entities pass
// through it), the method rejects the end tile as reachable (because de facto it is not reachable since
// it is blocking) and eventually the path is assumed as not existing.
// It may not be consistent with a A* model implementation, however it is required to adapt
// the algorithm for the BASE project purpose.
var reachable = layer.isTileReachable(position) || position.equals(end);
if (!reachable) { if (!reachable) {
continue; continue;
} }
@@ -158,12 +184,12 @@ public class AstarPathFinder implements PathFinder {
for (Vector2i node : nodes) { for (Vector2i node : nodes) {
if (node.equals(column, row)) { if (node.equals(column, row)) {
System.out.print("#"); System.out.print(" # ");
continue tiles; continue tiles;
} }
} }
System.out.print(layer.getPassageMap()[row][column] == PassageAbility.ALLOW ? " " : "."); System.out.print(layer.getPassageMap()[row][column] == PassageAbility.ALLOW ? " " : " . ");
} }
System.out.println("|"); System.out.println("|");

View File

@@ -6,5 +6,5 @@ import org.joml.Vector2i;
import java.util.LinkedList; import java.util.LinkedList;
public interface PathFinder { public interface PathFinder {
LinkedList<Vector2i> findPath(ObjectLayer layer, Vector2i start, Vector2i end, int range); LinkedList<Vector2i> findPath(ObjectLayer layer, Vector2i start, Vector2i end);
} }

View File

@@ -2,7 +2,6 @@ package com.bartlomiejpluta.base.engine.world.map.layer.object;
import com.bartlomiejpluta.base.api.game.ai.NPC; import com.bartlomiejpluta.base.api.game.ai.NPC;
import com.bartlomiejpluta.base.api.game.camera.Camera; import com.bartlomiejpluta.base.api.game.camera.Camera;
import com.bartlomiejpluta.base.api.game.entity.Direction;
import com.bartlomiejpluta.base.api.game.entity.Entity; import com.bartlomiejpluta.base.api.game.entity.Entity;
import com.bartlomiejpluta.base.api.game.entity.Movement; import com.bartlomiejpluta.base.api.game.entity.Movement;
import com.bartlomiejpluta.base.api.game.map.layer.object.ObjectLayer; import com.bartlomiejpluta.base.api.game.map.layer.object.ObjectLayer;
@@ -14,6 +13,7 @@ import com.bartlomiejpluta.base.api.internal.render.ShaderManager;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import org.joml.Vector2f; import org.joml.Vector2f;
import org.joml.Vector2i;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
@@ -86,51 +86,35 @@ public class DefaultObjectLayer implements ObjectLayer {
} }
@Override @Override
public boolean isMovementPossible(Movement movement) { public boolean isTileReachable(Vector2i tileCoordinates) {
var target = movement.getTo();
// Is trying to go beyond the map // Is trying to go beyond the map
if (target.x < 0 || target.y < 0 || target.x >= columns || target.y >= rows) { if (tileCoordinates.x < 0 || tileCoordinates.y < 0 || tileCoordinates.x >= columns || tileCoordinates.y >= rows) {
return false; return false;
} }
var source = movement.getFrom(); if (passageMap[tileCoordinates.y][tileCoordinates.x] != PassageAbility.ALLOW) {
var direction = movement.getDirection(); return false;
}
var isTargetReachable = switch (passageMap[target.y][target.x]) {
case UP_ONLY -> direction != Direction.DOWN;
case DOWN_ONLY -> direction != Direction.UP;
case LEFT_ONLY -> direction != Direction.RIGHT;
case RIGHT_ONLY -> direction != Direction.LEFT;
case BLOCK -> false;
case ALLOW -> true;
};
var canMoveFromCurrentTile = switch (passageMap[source.y][source.x]) {
case UP_ONLY -> direction == Direction.UP;
case DOWN_ONLY -> direction == Direction.DOWN;
case LEFT_ONLY -> direction == Direction.LEFT;
case RIGHT_ONLY -> direction == Direction.RIGHT;
default -> true;
};
for (var entity : entities) { for (var entity : entities) {
if (entity.isBlocking()) { if (entity.isBlocking()) {
// The tile is occupied by other entity // The tile is occupied by other entity
if (entity.getCoordinates().equals(target)) { if (entity.getCoordinates().equals(tileCoordinates)) {
return false; return false;
} }
// The tile is empty, however another entity is moving on it // The tile is empty, however another entity is moving on it
var otherMovement = entity.getMovement(); var otherMovement = entity.getMovement();
if (otherMovement != null && otherMovement.getTo().equals(target)) { if (otherMovement != null && otherMovement.getTo().equals(tileCoordinates)) {
return false; return false;
} }
} }
} }
return isTargetReachable && canMoveFromCurrentTile;
return true;
} }
@Override @Override
@@ -143,7 +127,7 @@ public class DefaultObjectLayer implements ObjectLayer {
for (var iterator = movements.iterator(); iterator.hasNext(); ) { for (var iterator = movements.iterator(); iterator.hasNext(); ) {
var movement = iterator.next(); var movement = iterator.next();
if (isMovementPossible(movement)) { if (isTileReachable(movement.getTo())) {
movement.perform(); movement.perform();
} }