Implement FollowEntityAI strategy | Discard other passage abilities than BLOCK and ALLOW
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user