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.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.move.MoveEvent;
|
||||
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 lombok.NonNull;
|
||||
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 PathExecutor<N> executor;
|
||||
@@ -22,55 +24,88 @@ public abstract class FollowObjectAI<N extends NPC, T extends Locationable> impl
|
||||
|
||||
private MovementPath<N> path = null;
|
||||
|
||||
private boolean sees = false;
|
||||
|
||||
protected FollowObjectAI(@NonNull PathFinder finder, @NonNull N npc, @NonNull T target) {
|
||||
this.finder = finder;
|
||||
this.npc = npc;
|
||||
this.target = target;
|
||||
this.executor = new PathExecutor<>(npc);
|
||||
|
||||
setTarget(target);
|
||||
|
||||
npc.addEventListener(MoveEvent.TYPE, this::onMove);
|
||||
}
|
||||
|
||||
public void recomputePath() {
|
||||
path = null;
|
||||
}
|
||||
private void onMove(MoveEvent event) {
|
||||
if (npc.getStrategy() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
public void recomputePath(@NonNull MoveEvent event) {
|
||||
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
|
||||
if (movable == target || (path != null && path.contains(movable))) {
|
||||
path = null;
|
||||
if (movable == target) {
|
||||
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
|
||||
public void nextActivity(ObjectLayer layer, float dt) {
|
||||
if (!npc.isMoving()) {
|
||||
var distance = npc.manhattanDistance(target);
|
||||
|
||||
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);
|
||||
}
|
||||
if (npc.isMoving()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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