Create utility class to test the line of sight between Character and target point
This commit is contained in:
@@ -2,7 +2,7 @@ package com.bartlomiejpluta.base.lib.ai;
|
|||||||
|
|
||||||
import com.bartlomiejpluta.base.api.ai.AI;
|
import com.bartlomiejpluta.base.api.ai.AI;
|
||||||
import com.bartlomiejpluta.base.api.ai.NPC;
|
import com.bartlomiejpluta.base.api.ai.NPC;
|
||||||
import com.bartlomiejpluta.base.api.location.Locationable;
|
import com.bartlomiejpluta.base.api.entity.Entity;
|
||||||
import com.bartlomiejpluta.base.api.map.layer.object.ObjectLayer;
|
import com.bartlomiejpluta.base.api.map.layer.object.ObjectLayer;
|
||||||
import com.bartlomiejpluta.base.api.move.MoveEvent;
|
import com.bartlomiejpluta.base.api.move.MoveEvent;
|
||||||
import com.bartlomiejpluta.base.util.path.MovementPath;
|
import com.bartlomiejpluta.base.util.path.MovementPath;
|
||||||
@@ -10,8 +10,10 @@ import com.bartlomiejpluta.base.util.path.PathExecutor;
|
|||||||
import com.bartlomiejpluta.base.util.pathfinder.PathFinder;
|
import com.bartlomiejpluta.base.util.pathfinder.PathFinder;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
public abstract class FollowObjectAI<N extends NPC, T extends Locationable> implements AI {
|
@Slf4j
|
||||||
|
public abstract class FollowObjectAI<N extends NPC, T extends Entity> implements AI {
|
||||||
|
|
||||||
private final PathFinder finder;
|
private final PathFinder finder;
|
||||||
private final PathExecutor<N> executor;
|
private final PathExecutor<N> executor;
|
||||||
@@ -22,55 +24,88 @@ public abstract class FollowObjectAI<N extends NPC, T extends Locationable> impl
|
|||||||
|
|
||||||
private MovementPath<N> path = null;
|
private MovementPath<N> path = null;
|
||||||
|
|
||||||
|
private boolean sees = false;
|
||||||
|
|
||||||
protected FollowObjectAI(@NonNull PathFinder finder, @NonNull N npc, @NonNull T target) {
|
protected FollowObjectAI(@NonNull PathFinder finder, @NonNull N npc, @NonNull T target) {
|
||||||
this.finder = finder;
|
this.finder = finder;
|
||||||
this.npc = npc;
|
this.npc = npc;
|
||||||
this.target = target;
|
|
||||||
this.executor = new PathExecutor<>(npc);
|
this.executor = new PathExecutor<>(npc);
|
||||||
|
|
||||||
|
setTarget(target);
|
||||||
|
|
||||||
|
npc.addEventListener(MoveEvent.TYPE, this::onMove);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recomputePath() {
|
private void onMove(MoveEvent event) {
|
||||||
path = null;
|
if (npc.getStrategy() != this) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public void recomputePath(@NonNull MoveEvent event) {
|
|
||||||
var movable = event.getMovable();
|
var movable = event.getMovable();
|
||||||
|
|
||||||
// Refresh only when target has been displaced
|
if (movable == npc) {
|
||||||
|
this.sees = sees(npc, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate path only when target has been displaced
|
||||||
// or another object is blocking current path
|
// or another object is blocking current path
|
||||||
if (movable == target || (path != null && path.contains(movable))) {
|
if (movable == target) {
|
||||||
path = null;
|
this.path = null;
|
||||||
|
this.sees = sees(npc, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate path when another object is blocking current path
|
||||||
|
if (path != null && path.contains(movable)) {
|
||||||
|
this.path = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void nextActivity(ObjectLayer layer, float dt) {
|
public void nextActivity(ObjectLayer layer, float dt) {
|
||||||
if (!npc.isMoving()) {
|
if (npc.isMoving()) {
|
||||||
var distance = npc.manhattanDistance(target);
|
return;
|
||||||
|
|
||||||
if (distance == 1) {
|
|
||||||
npc.setFaceDirection(npc.getDirectionTowards(target));
|
|
||||||
interact(npc, target, layer, dt);
|
|
||||||
} else if (sees(npc, target, layer, distance)) {
|
|
||||||
follow(npc, target, layer, dt);
|
|
||||||
|
|
||||||
if (path == null || path.isEmpty()) {
|
|
||||||
path = finder.findPath(layer, npc, target.getCoordinates());
|
|
||||||
executor.setPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
executor.execute(layer, dt);
|
|
||||||
} else {
|
|
||||||
idle(npc, target, layer, dt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var distance = npc.manhattanDistance(target);
|
||||||
|
|
||||||
|
if (distance == 1) {
|
||||||
|
npc.setFaceDirection(npc.getDirectionTowards(target));
|
||||||
|
interact(npc, target, layer, dt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sees) {
|
||||||
|
follow(npc, target, layer, dt);
|
||||||
|
|
||||||
|
// Calculate path
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
path = finder.findPath(layer, npc, target.getCoordinates());
|
||||||
|
executor.setPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(layer, dt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
idle(npc, target, layer, dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract boolean sees(N npc, T target, ObjectLayer layer, int distance);
|
|
||||||
|
|
||||||
protected abstract void interact(N npc, T target, ObjectLayer layer, float dt);
|
protected abstract boolean sees(N npc, T target);
|
||||||
|
|
||||||
protected abstract void follow(N npc, T target, ObjectLayer layer, float dt);
|
protected void interact(N npc, T target, ObjectLayer layer, float dt) {
|
||||||
|
|
||||||
protected abstract void idle(N npc, T target, ObjectLayer layer, float dt);
|
}
|
||||||
|
|
||||||
|
protected void follow(N npc, T target, ObjectLayer layer, float dt) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void idle(N npc, T target, ObjectLayer layer, float dt) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.bartlomiejpluta.base.util.math;
|
||||||
|
|
||||||
|
import org.joml.Vector2i;
|
||||||
|
import org.joml.Vector2ic;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class BresenhamLine {
|
||||||
|
public static List<Vector2i> bresenhamLine(Vector2ic start, Vector2ic end) {
|
||||||
|
return bresenhamLine(start.x(), start.y(), end.x(), end.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Vector2i> bresenhamLine(int startX, int startY, int endX, int endY) {
|
||||||
|
var list = new LinkedList<Vector2i>();
|
||||||
|
|
||||||
|
bresenhamLine(startX, startY, endX, endY, v -> {
|
||||||
|
list.add(v);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T bresenhamLine(Vector2ic start, Vector2ic end, Function<Vector2i, T> consumer) {
|
||||||
|
return bresenhamLine(start.x(), start.y(), end.x(), end.y(), consumer, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T bresenhamLine(int startX, int startY, int endX, int endY, Function<Vector2i, T> consumer) {
|
||||||
|
return bresenhamLine(startX, startY, endX, endY, consumer, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T bresenhamLine(Vector2ic start, Vector2ic end, Function<Vector2i, T> consumer, T defaultValue) {
|
||||||
|
return bresenhamLine(start.x(), start.y(), end.x(), end.y(), consumer, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T bresenhamLine(int startX, int startY, int endX, int endY, Function<Vector2i, T> consumer, T defaultValue) {
|
||||||
|
int dx = Math.abs(endX - startX), dy = Math.abs(endY - startY);
|
||||||
|
int sx = startX < endX ? 1 : -1, sy = startY < endY ? 1 : -1;
|
||||||
|
int err = dx - dy;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var result = consumer.apply(new Vector2i(startX, startY));
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startX == endX && startY == endY) break;
|
||||||
|
|
||||||
|
int e2 = err << 1;
|
||||||
|
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
startX += sx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
startY += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.bartlomiejpluta.base.util.visibility;
|
||||||
|
|
||||||
|
|
||||||
|
import com.bartlomiejpluta.base.api.character.Character;
|
||||||
|
import org.joml.Vector2ic;
|
||||||
|
|
||||||
|
import static com.bartlomiejpluta.base.util.math.BresenhamLine.bresenhamLine;
|
||||||
|
|
||||||
|
public class VisibilityChecker {
|
||||||
|
final int[][] DIRS = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}}; // UP, LEFT, DOWN, RIGHT
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean isInCone(Character observer, Vector2ic target, float angle, int maxDistance) {
|
||||||
|
var origin = observer.getCoordinates();
|
||||||
|
var direction = observer.getFaceDirection().vector;
|
||||||
|
return isInCone(origin.x(), origin.y(), direction.x(), direction.y(), target.x(), target.y(), angle, maxDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a point (x, y) lies within a vision cone defined by an origin (x0, y0),
|
||||||
|
* a direction vector (dx, dy), a cone angle, and a maximum distance.
|
||||||
|
*
|
||||||
|
* @param x0 The x-coordinate of the origin point.
|
||||||
|
* @param y0 The y-coordinate of the origin point.
|
||||||
|
* @param dx The x-component of the direction vector (should be normalized or non-zero).
|
||||||
|
* @param dy The y-component of the direction vector (should be normalized or non-zero).
|
||||||
|
* @param x The x-coordinate of the point to check.
|
||||||
|
* @param y The y-coordinate of the point to check.
|
||||||
|
* @param angle The full angle of the cone in radians.
|
||||||
|
* @param maxDistance The maximum distance from the origin to consider (points beyond are outside the cone).
|
||||||
|
* @return true if the point is within the cone and range, false otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean isInCone(int x0, int y0, int dx, int dy, int x, int y, float angle, int maxDistance) {
|
||||||
|
int vx = x - x0;
|
||||||
|
int vy = y - y0;
|
||||||
|
int lenSq = vx * vx + vy * vy;
|
||||||
|
if (lenSq == 0) return true; // point is exactly at origin
|
||||||
|
if (maxDistance > 0 && lenSq > maxDistance * maxDistance) return false; // outside max distance
|
||||||
|
|
||||||
|
// Compute dot product between direction and vector to point
|
||||||
|
int dot = dx * vx + dy * vy;
|
||||||
|
|
||||||
|
// Precompute cosine of half the cone angle
|
||||||
|
float halfAngle = angle * 0.5f;
|
||||||
|
float cosHalfAngle = (float) Math.cos(halfAngle);
|
||||||
|
|
||||||
|
// Normalize dot by length of vector to point (len)
|
||||||
|
float len = (float) Math.sqrt(lenSq);
|
||||||
|
float cosTheta = dot / len;
|
||||||
|
|
||||||
|
// Check if the point is within the cone
|
||||||
|
return cosTheta >= cosHalfAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canSee(Character observer, Character target, float angle, int range, boolean direction) {
|
||||||
|
var layer = observer.getLayer();
|
||||||
|
|
||||||
|
// Observer and target are on different layers
|
||||||
|
if (layer != target.getLayer()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target is out of observer's visibility area
|
||||||
|
if (observer.chebyshevDistance(target) > range) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observer does not look at the target's direction
|
||||||
|
var targetCoords = target.getCoordinates();
|
||||||
|
if (direction && !isInCone(observer, targetCoords, angle, range)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var observerCoords = observer.getCoordinates();
|
||||||
|
|
||||||
|
// Checking line of sight
|
||||||
|
return bresenhamLine(observer.getCoordinates(), target.getCoordinates(), v -> {
|
||||||
|
// Don't check observer coords
|
||||||
|
if (observerCoords.equals(v)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target is visible
|
||||||
|
if (targetCoords.equals(v)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obstacle on the sight line
|
||||||
|
if (!layer.isTileReachable(v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user