From f2f592209a668cec2d6b805a04128410fe51686c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A8=D0=B0=D0=BD=D0=B4=D1=83=D1=80=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9A=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=20=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=87?= Date: Sun, 22 Aug 2021 16:56:34 +0300 Subject: [PATCH] Entity#getLineOfSightEntity and BoundingBox#intersect(Point, Point) --- .../server/collision/BoundingBox.java | 93 ++++++++++++++ .../net/minestom/server/entity/Entity.java | 119 ++++++++++++++++++ .../minestom/server/entity/LivingEntity.java | 41 +----- .../server/utils/block/BlockIterator.java | 5 +- 4 files changed, 216 insertions(+), 42 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index 03d3ca42d..7087ee595 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -102,16 +102,109 @@ public class BoundingBox { return intersectWithBlock(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ()); } + /** + * Used to know if the bounding box intersects (contains) a point. + * + * @param x x-coord of a point + * @param y y-coord of a point + * @param z z-coord of a point + * @return true if the bounding box intersects (contains) with the point, false otherwise + */ public boolean intersect(double x, double y, double z) { return (x >= getMinX() && x <= getMaxX()) && (y >= getMinY() && y <= getMaxY()) && (z >= getMinZ() && z <= getMaxZ()); } + /** + * Used to know if the bounding box intersects (contains) a point. + * + * @param point the point to check + * @return true if the bounding box intersects (contains) with the point, false otherwise + */ public boolean intersect(@NotNull Point point) { return intersect(point.x(), point.y(), point.z()); } + /** + * Used to know if the bounding box intersects a line segment. + * + * @param x1 x-coord of first line segment point + * @param y1 y-coord of first line segment point + * @param z1 z-coord of first line segment point + * @param x2 x-coord of second line segment point + * @param y2 y-coord of second line segment point + * @param z2 z-coord of second line segment point + * @return true if the bounding box intersects with the line segment, false otherwise. + */ + public boolean intersect(double x1, double y1, double z1, double x2, double y2, double z2) { + double x3 = getMinX(); + double x4 = getMaxX(); + double y3 = getMinY(); + double y4 = getMaxY(); + double z3 = getMinZ(); + double z4 = getMaxZ(); + if (x1 > x3 && x1 < x4 && y1 > y3 && y1 < y4 && z1 > z3 && z1 < z4) { + return true; + } + if (x1 < x3 && x2 < x3 || x1 > x4 && x2 > x4 || + y1 < y3 && y2 < y3 || y1 > y4 && y2 > y4 || + z1 < z3 && z2 < z3 || z1 > z4 && z2 > z4) { + return false; + } + return isInsideBoxWithAxis(1, getSegmentIntersection(x1 - x3, x2 - x3, x1, y1, z1, x2, y2, z2)) || + isInsideBoxWithAxis(1, getSegmentIntersection(x1 - x4, x2 - x4, x1, y1, z1, x2, y2, z2)) || + isInsideBoxWithAxis(2, getSegmentIntersection(y1 - y3, y2 - y3, x1, y1, z1, x2, y2, z2)) || + isInsideBoxWithAxis(2, getSegmentIntersection(y1 - y4, y2 - y4, x1, y1, z1, x2, y2, z2)) || + isInsideBoxWithAxis(3, getSegmentIntersection(z1 - z3, z2 - z3, x1, y1, z1, x2, y2, z2)) || + isInsideBoxWithAxis(3, getSegmentIntersection(z1 - z4, z2 - z4, x1, y1, z1, x2, y2, z2)); + } + + /** + * Used to know if the bounding box intersects a line segment. + * + * @param start first line segment point + * @param end second line segment point + * @return true if the bounding box intersects with the line segment, false otherwise. + */ + public boolean intersect(@NotNull Point start, @NotNull Point end) { + return intersect( + Math.min(start.x(), end.x()), + Math.min(start.y(), end.y()), + Math.min(start.z(), end.z()), + Math.max(start.x(), end.x()), + Math.max(start.y(), end.y()), + Math.max(start.z(), end.z()) + ); + } + + private @Nullable Vec getSegmentIntersection(double dst1, double dst2, double x1, double y1, double z1, double x2, double y2, double z2) { + if (dst1 == dst2 || dst1 * dst2 >= 0D) { + return null; + } + double delta = dst1 / (dst1 - dst2); + return new Vec( + x1 + (x2 - x1) * delta, + y1 + (y2 - y1) * delta, + z1 + (z2 - z1) * delta + ); + } + + private boolean isInsideBoxWithAxis(int axis, @Nullable Vec intersection) { + if (intersection == null) { + return false; + } + double x1 = getMinX(); + double x2 = getMaxX(); + double y1 = getMinY(); + double y2 = getMaxY(); + double z1 = getMinZ(); + double z2 = getMaxZ(); + return axis == 1 && intersection.z() > z1 && intersection.z() < z2 && intersection.y() > y1 && intersection.y() < y2 || + axis == 2 && intersection.z() > z1 && intersection.z() < z2 && intersection.x() > x1 && intersection.x() < x2 || + axis == 3 && intersection.x() > x1 && intersection.x() < x2 && intersection.y() > y1 && intersection.y() < y2; + } + /** * Creates a new {@link BoundingBox} linked to the same {@link Entity} with expanded size. * diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 88eab1008..81236b35e 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -35,6 +35,7 @@ import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.thread.ThreadProvider; import net.minestom.server.utils.async.AsyncUtils; +import net.minestom.server.utils.block.BlockIterator; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.player.PlayerUtils; @@ -52,7 +53,9 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; /** * Could be a player, a monster, or an object. @@ -1452,6 +1455,122 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler } } + public Collection getNearbyEntities(double range) { + Instance instance = getInstance(); + if (instance == null) { + return Collections.emptySet(); + } + int minX = ChunkUtils.getChunkCoordinate(position.x() - range); + int maxX = ChunkUtils.getChunkCoordinate(position.x() + range); + int minZ = ChunkUtils.getChunkCoordinate(position.z() - range); + int maxZ = ChunkUtils.getChunkCoordinate(position.z() + range); + List result = new ArrayList<>(); + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + Chunk chunk = instance.getChunk(x, z); + if (chunk != null) { + result.addAll(instance.getChunkEntities(chunk)); + } + } + } + return result; + } + + /** + * Gets the line of sight of the entity. + * + * @param maxDistance The max distance to scan + * @return A list of {@link Point poiints} in this entities line of sight + */ + public List getLineOfSight(int maxDistance) { + List blocks = new ArrayList<>(); + Iterator it = new BlockIterator(this, maxDistance); + while (it.hasNext()) { + final Point position = it.next(); + if (!getInstance().getBlock(position).isAir()) blocks.add(position); + } + return blocks; + } + + /** + * Checks whether the current entity has line of sight to the given one. + * If so, it doesn't mean that the given entity is IN line of sight of the current, + * but the current one can rotate so that it will be true. + * + * @param entity the entity to be checked. + * @return if the current entity has line of sight to the given one. + */ + public boolean hasLineOfSight(Entity entity) { + final var start = getPosition().asVec().add(0D, getEyeHeight(), 0D); + final var end = entity.getPosition().asVec().add(0D, getEyeHeight(), 0D); + final var direction = end.sub(start); + final int maxDistance = (int) Math.ceil(direction.length()); + + Iterator it = new BlockIterator(start, direction.normalize(), 0D, maxDistance); + while (it.hasNext()) { + Block block = getInstance().getBlock(it.next()); + if (!block.isAir() && !block.isLiquid()) { + return false; + } + } + return true; + } + + /** + * Gets first entity on the line of sight of the current one that matches the given predicate. + * + * @param range max length of the line of sight of the current entity to be checked. + * @param predicate optional predicate, defaults to "accept any". + * @return resulting entity whether there're any, null otherwise. + */ + public @Nullable Entity getLineOfSightEntity(double range, @Nullable Predicate predicate) { + var instance = getInstance(); + if (instance == null) { + return null; + } + + var start = new Vec(position.x(), position.y() + getEyeHeight(), position.z()); + var end = start.add(position.direction().mul(range)); + + var finalPredicate = Objects.requireNonNullElseGet(predicate, () -> e -> true); + + var nearby = getNearbyEntities(range).stream() + .filter(e -> e != this && e.boundingBox.intersect(start, end) && finalPredicate.test(e)) + .collect(Collectors.toList()); + if (nearby.isEmpty()) { + return null; + } + + var direction = end.sub(start); + int maxDistance = (int) Math.ceil(direction.length()); + double maxVisibleDistanceSquared = direction.lengthSquared(); + + Iterator iterator = new BlockIterator(start, direction.normalize(), 0D, maxDistance); + while (iterator.hasNext()) { + var blockPos = iterator.next(); + Block block = instance.getBlock(blockPos); + if (!block.isAir() && !block.isLiquid()) { + maxVisibleDistanceSquared = blockPos.distanceSquared(position); + break; + } + } + + Entity result = null; + double minDistanceSquared = 0D; + for (var entity : nearby) { + double distanceSquared = entity.getDistanceSquared(this); + if (result == null || minDistanceSquared > distanceSquared) { + result = entity; + minDistanceSquared = distanceSquared; + } + } + if (minDistanceSquared < maxVisibleDistanceSquared) { + return result; + } else { + return null; + } + } + public enum Pose { STANDING, FALL_FLYING, diff --git a/src/main/java/net/minestom/server/entity/LivingEntity.java b/src/main/java/net/minestom/server/entity/LivingEntity.java index f07eba0b4..fde282732 100644 --- a/src/main/java/net/minestom/server/entity/LivingEntity.java +++ b/src/main/java/net/minestom/server/entity/LivingEntity.java @@ -37,6 +37,7 @@ import java.time.Duration; import java.time.temporal.TemporalUnit; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; public class LivingEntity extends Entity implements EquipmentHandler { @@ -711,46 +712,6 @@ public class LivingEntity extends Entity implements EquipmentHandler { return team; } - /** - * Gets the line of sight of the entity. - * - * @param maxDistance The max distance to scan - * @return A list of {@link Point poiints} in this entities line of sight - */ - public List getLineOfSight(int maxDistance) { - List blocks = new ArrayList<>(); - Iterator it = new BlockIterator(this, maxDistance); - while (it.hasNext()) { - final Point position = it.next(); - if (!getInstance().getBlock(position).isAir()) blocks.add(position); - } - return blocks; - } - - /** - * Checks whether the current entity has line of sight to the given one. - * If so, it doesn't mean that the given entity is IN line of sight of the current, - * but the current one can rotate so that it will be true. - * - * @param entity the entity to be checked. - * @return if the current entity has line of sight to the given one. - */ - public boolean hasLineOfSight(Entity entity) { - final var start = getPosition().asVec().add(0D, getEyeHeight(), 0D); - final var end = entity.getPosition().asVec().add(0D, getEyeHeight(), 0D); - final var direction = end.sub(start); - final int maxDistance = (int) Math.ceil(direction.length()); - - Iterator it = new BlockIterator(start, direction.normalize(), 0D, maxDistance); - while (it.hasNext()) { - Block block = getInstance().getBlock(it.next()); - if (!block.isAir() && !block.isLiquid()) { - return false; - } - } - return true; - } - /** * Gets the target (not-air) block position of the entity. * diff --git a/src/main/java/net/minestom/server/utils/block/BlockIterator.java b/src/main/java/net/minestom/server/utils/block/BlockIterator.java index c6ce2d79e..830cae804 100644 --- a/src/main/java/net/minestom/server/utils/block/BlockIterator.java +++ b/src/main/java/net/minestom/server/utils/block/BlockIterator.java @@ -1,5 +1,6 @@ package net.minestom.server.utils.block; +import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.coordinate.Point; @@ -268,7 +269,7 @@ public class BlockIterator implements Iterator { * unloaded chunks. A value of 0 indicates no limit */ - public BlockIterator(@NotNull LivingEntity entity, int maxDistance) { + public BlockIterator(@NotNull Entity entity, int maxDistance) { this(entity.getPosition(), entity.getEyeHeight(), maxDistance); } @@ -280,7 +281,7 @@ public class BlockIterator implements Iterator { * @param entity Information from the entity is used to set up the trace */ - public BlockIterator(@NotNull LivingEntity entity) { + public BlockIterator(@NotNull Entity entity) { this(entity, 0); }