From 8df0d37107815b5a3f29832223ab035a9792609a Mon Sep 17 00:00:00 2001 From: Konstantin Shandurenko Date: Thu, 31 Mar 2022 21:28:02 +0300 Subject: [PATCH] Entity's line of sight methods improvements (#842) --- .../server/collision/BoundingBox.java | 3 + .../server/collision/CollisionUtils.java | 48 +++++++++- .../minestom/server/collision/RayUtils.java | 3 +- .../net/minestom/server/entity/Entity.java | 42 ++++++--- .../EntityLineOfSightIntegrationTest.java | 92 ++++++++++++++++--- 5 files changed, 156 insertions(+), 32 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index ea42524c4..d8e55c6d3 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -11,6 +11,9 @@ import org.jetbrains.annotations.NotNull; * See https://wiki.vg/Entity_metadata#Mobs_2 */ public final class BoundingBox implements Shape { + + final static BoundingBox ZERO = new BoundingBox(0, 0, 0); + private final double width, height, depth; private final Point offset; private Point relativeEnd; diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index 5e5b2cb73..df8e73411 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; +import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.block.Block; @@ -30,14 +31,53 @@ public final class CollisionUtils { */ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, @Nullable PhysicsResult lastPhysicsResult) { - final BoundingBox boundingBox = entity.getBoundingBox(); - final Pos currentPosition = entity.getPosition(); - final Block.Getter getter = new ChunkCache(entity.getInstance(), entity.getChunk(), Block.STONE); + assert entity.getInstance() != null; + return handlePhysics(entity.getInstance(), entity.getChunk(), + entity.getBoundingBox(), + entity.getPosition(), entityVelocity, + lastPhysicsResult); + } + + /** + * Moves bounding box with physics applied (ie checking against blocks) + *

+ * Works by getting all the full blocks that a bounding box could interact with. + * All bounding boxes inside the full blocks are checked for collisions with the given bounding box. + * + * @param boundingBox the bounding box to move + * @return the result of physics simulation + */ + public static PhysicsResult handlePhysics(@NotNull Instance instance, @Nullable Chunk chunk, + @NotNull BoundingBox boundingBox, + @NotNull Pos position, @NotNull Vec velocity, + @Nullable PhysicsResult lastPhysicsResult) { + final Block.Getter getter = new ChunkCache(instance, chunk != null ? chunk : instance.getChunkAt(position), Block.STONE); return BlockCollision.handlePhysics(boundingBox, - entityVelocity, currentPosition, + velocity, position, getter, lastPhysicsResult); } + /** + * Checks whether shape is reachable by the given line of sight + * (ie there are no blocks colliding with it). + * + * @param instance the instance. + * @param chunk optional chunk reference for speedup purposes. + * @param start start of the line of sight. + * @param end end of the line of sight. + * @param shape shape to check. + * @return true is shape is reachable by the given line of sight; false otherwise. + */ + public static boolean isLineOfSightReachingShape(@NotNull Instance instance, @Nullable Chunk chunk, + @NotNull Point start, @NotNull Point end, + @NotNull Shape shape) { + final PhysicsResult result = handlePhysics(instance, chunk, + BoundingBox.ZERO, + Pos.fromPoint(start), Vec.fromPoint(end.sub(start)), + null); + return shape.intersectBox(end.sub(result.newPosition()), BoundingBox.ZERO); + } + public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity) { return handlePhysics(entity, entityVelocity, null); } diff --git a/src/main/java/net/minestom/server/collision/RayUtils.java b/src/main/java/net/minestom/server/collision/RayUtils.java index 4ceba2af0..11cc00921 100644 --- a/src/main/java/net/minestom/server/collision/RayUtils.java +++ b/src/main/java/net/minestom/server/collision/RayUtils.java @@ -345,7 +345,6 @@ final class RayUtils { } public static boolean BoundingBoxRayIntersectionCheck(Vec start, Vec direction, BoundingBox boundingBox, Pos position) { - // TODO: BoundingBox.ZERO? - return BoundingBoxIntersectionCheck(new BoundingBox(0, 0, 0), start, direction, boundingBox, position); + return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position); } } diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 302e8c074..ff6960e8f 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -1610,27 +1610,41 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } /** - * 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. + * Raycasts current entity's eye position to target eye position. * * @param entity the entity to be checked. - * @return if the current entity has line of sight to the given one. + * @param exactView if set to TRUE, checks whether target is IN the line of sight of the current one; + * otherwise checks if the current entity can rotate so that target will be in its line of sight. + * @return true if the ray reaches the target bounding box before hitting a block. */ - public boolean hasLineOfSight(Entity entity) { + public boolean hasLineOfSight(Entity entity, boolean exactView) { Instance instance = getInstance(); if (instance == null) { return false; } - final Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z()); - return entity.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), entity.getPosition()); + final Pos start = position.withY(position.y() + getEyeHeight()); + final Pos end = entity.position.withY(entity.position.y() + entity.getEyeHeight()); + final Vec direction = exactView ? position.direction() : end.sub(start).asVec().normalize(); + if (!entity.boundingBox.boundingBoxRayIntersectionCheck(start.asVec(), direction, entity.getPosition())) { + return false; + } + return CollisionUtils.isLineOfSightReachingShape(instance, currentChunk, start, end, entity.boundingBox); + } + + /** + * @see Entity#hasLineOfSight(Entity, boolean) + * @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) { + return hasLineOfSight(entity, false); } /** * 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 range max length of the line of sight of the current entity to be checked. * @param predicate optional predicate * @return resulting entity whether there're any, null otherwise. */ @@ -1640,12 +1654,16 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return null; } - final Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z()); + final Pos start = position.withY(position.y() + getEyeHeight()); + final Vec startAsVec = start.asVec(); + final Predicate finalPredicate = e -> e != this + && e.boundingBox.boundingBoxRayIntersectionCheck(startAsVec, position.direction(), e.getPosition()) + && predicate.test(e) + && CollisionUtils.isLineOfSightReachingShape(instance, currentChunk, start, + e.position.withY(e.position.y() + e.getEyeHeight()), e.boundingBox); Optional nearby = instance.getNearbyEntities(position, range).stream() - .filter(e -> e != this - && e.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), e.getPosition()) - && predicate.test(e)) + .filter(finalPredicate) .min(Comparator.comparingDouble(e -> e.getDistance(this.position))); return nearby.orElse(null); diff --git a/src/test/java/net/minestom/server/entity/EntityLineOfSightIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityLineOfSightIntegrationTest.java index 53a0dbba0..9f291fe86 100644 --- a/src/test/java/net/minestom/server/entity/EntityLineOfSightIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityLineOfSightIntegrationTest.java @@ -3,10 +3,10 @@ package net.minestom.server.entity; import net.minestom.server.api.Env; import net.minestom.server.api.EnvTest; import net.minestom.server.coordinate.Pos; +import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; @EnvTest public class EntityLineOfSightIntegrationTest { @@ -21,8 +21,17 @@ public class EntityLineOfSightIntegrationTest { var entity2 = new Entity(EntityTypes.ZOMBIE); entity2.setInstance(instance, new Pos(10, 42, 0)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertEquals(res, entity2); + assertEquals(entity2, entity.getLineOfSightEntity(20, (e) -> true)); + assertTrue(entity.hasLineOfSight(entity2, true)); + + for (int z = -1; z <= 1; ++z) { + for (int y = 40; y <= 44; ++y) { + instance.setBlock(5, y, z, Block.STONE); + } + } + + assertNull(entity.getLineOfSightEntity(20, (e) -> true)); + assertFalse(entity.hasLineOfSight(entity2, true)); } @Test @@ -36,8 +45,17 @@ public class EntityLineOfSightIntegrationTest { var entity2 = new Entity(EntityTypes.ZOMBIE); entity2.setInstance(instance, new Pos(-10, 42, 0)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertNull(res); + assertNull(entity.getLineOfSightEntity(20, (e) -> true)); + assertFalse(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); + + for (int z = -1; z <= 1; ++z) { + for (int y = 40; y <= 44; ++y) { + instance.setBlock(-5, y, z, Block.STONE); + } + } + + assertFalse(entity.hasLineOfSight(entity2, false)); } @Test @@ -51,8 +69,17 @@ public class EntityLineOfSightIntegrationTest { var entity2 = new Entity(EntityTypes.ZOMBIE); entity2.setInstance(instance, new Pos(10, 42, 0.31)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertNull(res); + assertNull(entity.getLineOfSightEntity(20, (e) -> true)); + assertFalse(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); + + for (int z = -1; z <= 1; ++z) { + for (int y = 40; y <= 44; ++y) { + instance.setBlock(5, y, z, Block.STONE); + } + } + + assertFalse(entity.hasLineOfSight(entity2, false)); } @Test @@ -66,8 +93,19 @@ public class EntityLineOfSightIntegrationTest { var entity2 = new Entity(EntityTypes.ZOMBIE); entity2.setInstance(instance, new Pos(10, 42, 0.3)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertEquals(res, entity2); + assertEquals(entity2, entity.getLineOfSightEntity(20, (e) -> true)); + assertTrue(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); + + for (int z = -1; z <= 1; ++z) { + for (int y = 40; y <= 44; ++y) { + instance.setBlock(5, y, z, Block.STONE); + } + } + + assertNull(entity.getLineOfSightEntity(20, (e) -> true)); + assertFalse(entity.hasLineOfSight(entity2, true)); + assertFalse(entity.hasLineOfSight(entity2, false)); } @Test @@ -84,8 +122,11 @@ public class EntityLineOfSightIntegrationTest { var entity3 = new Entity(EntityTypes.ZOMBIE); entity3.setInstance(instance, new Pos(5, 42, 0)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertEquals(res, entity3); + assertEquals(entity3, entity.getLineOfSightEntity(20, (e) -> true)); + assertTrue(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); + assertTrue(entity.hasLineOfSight(entity3, true)); + assertTrue(entity.hasLineOfSight(entity3, false)); } @Test @@ -99,7 +140,30 @@ public class EntityLineOfSightIntegrationTest { var entity2 = new Entity(EntityTypes.ZOMBIE); entity2.setInstance(instance, new Pos(10, 42, 10)).join(); - Entity res = entity.getLineOfSightEntity(20, (e) -> true); - assertNull(res); + assertNull(entity.getLineOfSightEntity(20, (e) -> true)); + assertFalse(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); + } + @Test + public void entityPhysicsCheckLineOfSightLargeBoundingBox(Env env) { + var instance = env.createFlatInstance(); + + var entity = new Entity(EntityTypes.ZOMBIE); + entity.setInstance(instance, new Pos(0, 42, 0)).join(); + entity.setView(-90, 0); + + var entity2 = new Entity(EntityTypes.ZOMBIE); + entity2.setInstance(instance, new Pos(6, 42, 0)).join(); + entity2.setBoundingBox(4.0, 2.0, 4.0); + + for (int z = -1; z <= 1; ++z) { + for (int y = 40; y <= 44; ++y) { + instance.setBlock(5, y, z, Block.STONE); + } + } + + assertEquals(entity2, entity.getLineOfSightEntity(20, (e) -> true)); + assertTrue(entity.hasLineOfSight(entity2, true)); + assertTrue(entity.hasLineOfSight(entity2, false)); } }