Entity's line of sight methods improvements (#842)

This commit is contained in:
Konstantin Shandurenko 2022-03-31 21:28:02 +03:00 committed by GitHub
parent eebdb4a7a3
commit 8df0d37107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 32 deletions

View File

@ -11,6 +11,9 @@ import org.jetbrains.annotations.NotNull;
* See https://wiki.vg/Entity_metadata#Mobs_2 * See https://wiki.vg/Entity_metadata#Mobs_2
*/ */
public final class BoundingBox implements Shape { public final class BoundingBox implements Shape {
final static BoundingBox ZERO = new BoundingBox(0, 0, 0);
private final double width, height, depth; private final double width, height, depth;
private final Point offset; private final Point offset;
private Point relativeEnd; private Point relativeEnd;

View File

@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.WorldBorder;
import net.minestom.server.instance.block.Block; 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, public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity,
@Nullable PhysicsResult lastPhysicsResult) { @Nullable PhysicsResult lastPhysicsResult) {
final BoundingBox boundingBox = entity.getBoundingBox(); assert entity.getInstance() != null;
final Pos currentPosition = entity.getPosition(); return handlePhysics(entity.getInstance(), entity.getChunk(),
final Block.Getter getter = new ChunkCache(entity.getInstance(), entity.getChunk(), Block.STONE); entity.getBoundingBox(),
entity.getPosition(), entityVelocity,
lastPhysicsResult);
}
/**
* Moves bounding box with physics applied (ie checking against blocks)
* <p>
* 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, return BlockCollision.handlePhysics(boundingBox,
entityVelocity, currentPosition, velocity, position,
getter, lastPhysicsResult); 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) { public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity) {
return handlePhysics(entity, entityVelocity, null); return handlePhysics(entity, entityVelocity, null);
} }

View File

@ -345,7 +345,6 @@ final class RayUtils {
} }
public static boolean BoundingBoxRayIntersectionCheck(Vec start, Vec direction, BoundingBox boundingBox, Pos position) { public static boolean BoundingBoxRayIntersectionCheck(Vec start, Vec direction, BoundingBox boundingBox, Pos position) {
// TODO: BoundingBox.ZERO? return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position);
return BoundingBoxIntersectionCheck(new BoundingBox(0, 0, 0), start, direction, boundingBox, position);
} }
} }

View File

@ -1610,21 +1610,35 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
} }
/** /**
* Checks whether the current entity has line of sight to the given one. * Raycasts current entity's eye position to target eye position.
* 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. * @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(); Instance instance = getInstance();
if (instance == null) { if (instance == null) {
return false; return false;
} }
final Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z()); final Pos start = position.withY(position.y() + getEyeHeight());
return entity.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), entity.getPosition()); 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);
} }
/** /**
@ -1640,12 +1654,16 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
return null; 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<Entity> 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<Entity> nearby = instance.getNearbyEntities(position, range).stream() Optional<Entity> nearby = instance.getNearbyEntities(position, range).stream()
.filter(e -> e != this .filter(finalPredicate)
&& e.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), e.getPosition())
&& predicate.test(e))
.min(Comparator.comparingDouble(e -> e.getDistance(this.position))); .min(Comparator.comparingDouble(e -> e.getDistance(this.position)));
return nearby.orElse(null); return nearby.orElse(null);

View File

@ -3,10 +3,10 @@ package net.minestom.server.entity;
import net.minestom.server.api.Env; import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest; import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.block.Block;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNull;
@EnvTest @EnvTest
public class EntityLineOfSightIntegrationTest { public class EntityLineOfSightIntegrationTest {
@ -21,8 +21,17 @@ public class EntityLineOfSightIntegrationTest {
var entity2 = new Entity(EntityTypes.ZOMBIE); var entity2 = new Entity(EntityTypes.ZOMBIE);
entity2.setInstance(instance, new Pos(10, 42, 0)).join(); entity2.setInstance(instance, new Pos(10, 42, 0)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertEquals(entity2, entity.getLineOfSightEntity(20, (e) -> true));
assertEquals(res, entity2); 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 @Test
@ -36,8 +45,17 @@ public class EntityLineOfSightIntegrationTest {
var entity2 = new Entity(EntityTypes.ZOMBIE); var entity2 = new Entity(EntityTypes.ZOMBIE);
entity2.setInstance(instance, new Pos(-10, 42, 0)).join(); entity2.setInstance(instance, new Pos(-10, 42, 0)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertNull(entity.getLineOfSightEntity(20, (e) -> true));
assertNull(res); 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 @Test
@ -51,8 +69,17 @@ public class EntityLineOfSightIntegrationTest {
var entity2 = new Entity(EntityTypes.ZOMBIE); var entity2 = new Entity(EntityTypes.ZOMBIE);
entity2.setInstance(instance, new Pos(10, 42, 0.31)).join(); entity2.setInstance(instance, new Pos(10, 42, 0.31)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertNull(entity.getLineOfSightEntity(20, (e) -> true));
assertNull(res); 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 @Test
@ -66,8 +93,19 @@ public class EntityLineOfSightIntegrationTest {
var entity2 = new Entity(EntityTypes.ZOMBIE); var entity2 = new Entity(EntityTypes.ZOMBIE);
entity2.setInstance(instance, new Pos(10, 42, 0.3)).join(); entity2.setInstance(instance, new Pos(10, 42, 0.3)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertEquals(entity2, entity.getLineOfSightEntity(20, (e) -> true));
assertEquals(res, entity2); 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 @Test
@ -84,8 +122,11 @@ public class EntityLineOfSightIntegrationTest {
var entity3 = new Entity(EntityTypes.ZOMBIE); var entity3 = new Entity(EntityTypes.ZOMBIE);
entity3.setInstance(instance, new Pos(5, 42, 0)).join(); entity3.setInstance(instance, new Pos(5, 42, 0)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertEquals(entity3, entity.getLineOfSightEntity(20, (e) -> true));
assertEquals(res, entity3); assertTrue(entity.hasLineOfSight(entity2, true));
assertTrue(entity.hasLineOfSight(entity2, false));
assertTrue(entity.hasLineOfSight(entity3, true));
assertTrue(entity.hasLineOfSight(entity3, false));
} }
@Test @Test
@ -99,7 +140,30 @@ public class EntityLineOfSightIntegrationTest {
var entity2 = new Entity(EntityTypes.ZOMBIE); var entity2 = new Entity(EntityTypes.ZOMBIE);
entity2.setInstance(instance, new Pos(10, 42, 10)).join(); entity2.setInstance(instance, new Pos(10, 42, 10)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true); assertNull(entity.getLineOfSightEntity(20, (e) -> true));
assertNull(res); 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));
} }
} }