mirror of
https://github.com/Minestom/Minestom.git
synced 2024-10-31 15:59:35 +01:00
feat: raycasting api
This commit is contained in:
parent
63f02929ed
commit
640532ea1c
@ -0,0 +1,64 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* The result of a {@link Ray#cast(Block.Getter)}
|
||||
*/
|
||||
public interface BlockCastResult extends CastResult {
|
||||
/**
|
||||
* Whether this {@link BlockCastResult} has a block collision or not
|
||||
*
|
||||
* @return true if there is a block collision, otherwise false
|
||||
*/
|
||||
boolean hasBlockCollision();
|
||||
|
||||
/**
|
||||
* Returns the first block collision from the proceeding {@link Ray#cast}
|
||||
*
|
||||
* @return the first block collision
|
||||
* @throws NoSuchElementException when the block cast result has no {@link BlockRayCollision}
|
||||
*/
|
||||
@NotNull BlockRayCollision firstBlockCollision();
|
||||
|
||||
/**
|
||||
* Returns the last block collision from the proceeding {@link Ray#cast}
|
||||
*
|
||||
* @return the last block collision
|
||||
* @throws NoSuchElementException when the block cast result has no {@link BlockRayCollision}
|
||||
*/
|
||||
@NotNull BlockRayCollision lastBlockCollision();
|
||||
|
||||
/**
|
||||
* Returns an ordered immutable {@link BlockRayCollision} list from the proceeding {@link Ray#cast}.
|
||||
*
|
||||
* @return the list of block collisions
|
||||
*/
|
||||
@NotNull List<BlockRayCollision> blockCollisions();
|
||||
|
||||
record BlockRayCollision(@NotNull Point entry, @NotNull Point exit, @Nullable Vec entrySurfaceNormal,
|
||||
@Nullable Vec exitSurfaceNormal, @NotNull Block block) implements CastResult.RayCollision {
|
||||
@Override
|
||||
public @NotNull Vec entrySurfaceNormal() {
|
||||
if (entrySurfaceNormal == null) {
|
||||
throw new NoSuchElementException("Cast ray with Ray.Configuration.computeSurfaceNormals = true");
|
||||
}
|
||||
return entrySurfaceNormal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Vec exitSurfaceNormal() {
|
||||
if (exitSurfaceNormal == null) {
|
||||
throw new NoSuchElementException("Cast ray with Ray.Configuration.computeSurfaceNormals = true");
|
||||
}
|
||||
return exitSurfaceNormal;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
record BlockCastResultImpl(@NotNull List<BlockRayCollision> blockCollisions) implements BlockCastResult
|
||||
{
|
||||
BlockCastResultImpl {
|
||||
blockCollisions = Collections.unmodifiableList(blockCollisions);
|
||||
}
|
||||
|
||||
public boolean hasCollision() {
|
||||
return !blockCollisions.isEmpty();
|
||||
}
|
||||
|
||||
public @NotNull CastResult.RayCollision firstCollision() {
|
||||
return blockCollisions.getFirst();
|
||||
}
|
||||
|
||||
public @NotNull CastResult.RayCollision lastCollision() {
|
||||
return blockCollisions.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBlockCollision() {
|
||||
return !blockCollisions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockRayCollision firstBlockCollision() {
|
||||
return blockCollisions.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockRayCollision lastBlockCollision() {
|
||||
return blockCollisions.getLast();
|
||||
}
|
||||
}
|
75
src/main/java/net/minestom/server/collision/CastResult.java
Normal file
75
src/main/java/net/minestom/server/collision/CastResult.java
Normal file
@ -0,0 +1,75 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* The result of any {@link Ray#cast}
|
||||
*/
|
||||
public interface CastResult {
|
||||
/**
|
||||
* Returns this {@link CastResult} has a collision or not.
|
||||
*
|
||||
* @return true if there is any collision, otherwise false
|
||||
*/
|
||||
boolean hasCollision();
|
||||
|
||||
/**
|
||||
* Returns the first collision from the proceeding {@link Ray#cast}.
|
||||
*
|
||||
* @return the first collision
|
||||
* @throws NoSuchElementException when the cast result has no {@link RayCollision}
|
||||
*/
|
||||
@NotNull RayCollision firstCollision();
|
||||
|
||||
/**
|
||||
* Returns the last collision from the proceeding {@link Ray#cast}.
|
||||
*
|
||||
* @return the last collision
|
||||
* @throws NoSuchElementException when the cast result has no {@link RayCollision}
|
||||
*/
|
||||
@NotNull RayCollision lastCollision();
|
||||
|
||||
/**
|
||||
* Represents a pair of intersection entry & exit points
|
||||
* along with their surface normals.
|
||||
*/
|
||||
sealed interface RayCollision permits EntityCastResult.EntityRayCollision, BlockCastResult.BlockRayCollision {
|
||||
/**
|
||||
* The {@link Point} where the ray entered the bounding box.
|
||||
*
|
||||
* @return the entry point
|
||||
*/
|
||||
@NotNull Point entry();
|
||||
|
||||
/**
|
||||
* The {@link Point} where the ray exited the bounding box.
|
||||
*
|
||||
* @return the exit point
|
||||
*/
|
||||
@NotNull Point exit();
|
||||
|
||||
/**
|
||||
* The surface normal corresponding to the face of the bounding box that
|
||||
* the ray entered
|
||||
*
|
||||
* @throws NoSuchElementException when the {@link Ray} was not configured with
|
||||
* {@link Ray.Configuration#computeSurfaceNormals()}
|
||||
* @return the entry surface normal
|
||||
*/
|
||||
@NotNull Vec entrySurfaceNormal();
|
||||
|
||||
/**
|
||||
* The surface normal corresponding to the face of the bounding box that
|
||||
* the ray exited
|
||||
*
|
||||
* @throws NoSuchElementException when the {@link Ray} was not configured with
|
||||
* {@link Ray.Configuration#computeSurfaceNormals()}
|
||||
* @return the exit surface normal
|
||||
*/
|
||||
@NotNull Vec exitSurfaceNormal();
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The result of a {@link Ray#cast(Block.Getter, Collection)}
|
||||
*/
|
||||
public interface EntityBlockCastResult extends BlockCastResult, EntityCastResult {
|
||||
/**
|
||||
* Generates a list of entities that intersected the ray
|
||||
* before the collisionThreshold-th block was intersected.
|
||||
*
|
||||
* @param collisionThreshold the amount of blocks the ray could pass through
|
||||
* before entities are no longer included
|
||||
* @return the ordered list of entities before the
|
||||
* block collisionThreshold is reached
|
||||
*/
|
||||
@NotNull List<EntityRayCollision> findEntitiesBeforeBlockCollision(int collisionThreshold);
|
||||
|
||||
/**
|
||||
* Generate a list of entities that intersected the ray
|
||||
* before the ray intersected a block.
|
||||
*
|
||||
* @see EntityBlockCastResult#findEntitiesBeforeBlockCollision(int)
|
||||
*/
|
||||
@NotNull List<EntityRayCollision> findEntitiesBeforeBlockCollision();
|
||||
|
||||
/**
|
||||
* Generates a list of blocks that intersected the ray
|
||||
* before the collisionThreshold-th entity was intersected.
|
||||
*
|
||||
* @param collisionThreshold the amount of entities the ray could pass through
|
||||
* before blocks are no longer included
|
||||
* @return the ordered list of blocks before the
|
||||
* entity collisionThreshold is reached
|
||||
*/
|
||||
@NotNull List<BlockRayCollision> findBlocksBeforeEntityCollision(int collisionThreshold);
|
||||
|
||||
/**
|
||||
* Generate a list of blocks that intersected the ray
|
||||
* before the ray intersected an entity.
|
||||
*
|
||||
* @see EntityBlockCastResult#findBlocksBeforeEntityCollision(int)
|
||||
*/
|
||||
@NotNull List<BlockRayCollision> findBlocksBeforeEntityCollision();
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
record EntityBlockRaycastResultImpl(@NotNull List<EntityRayCollision> entityCollisions, @NotNull List<BlockRayCollision> blockCollisions,
|
||||
@NotNull List<RayCollision> collisions) implements EntityBlockCastResult {
|
||||
EntityBlockRaycastResultImpl {
|
||||
blockCollisions = Collections.unmodifiableList(blockCollisions);
|
||||
entityCollisions = Collections.unmodifiableList(entityCollisions);
|
||||
collisions = Collections.unmodifiableList(collisions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<EntityCastResult.EntityRayCollision> findEntitiesBeforeBlockCollision(int collisionThreshold) {
|
||||
return RaycastUtils.findEntitiesBeforeEntityCollision(collisions, collisionThreshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<EntityCastResult.EntityRayCollision> findEntitiesBeforeBlockCollision() {
|
||||
return findEntitiesBeforeBlockCollision(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockCastResult.BlockRayCollision> findBlocksBeforeEntityCollision(int collisionThreshold) {
|
||||
return RaycastUtils.findBlocksBeforeEntityCollision(collisions, collisionThreshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockCastResult.BlockRayCollision> findBlocksBeforeEntityCollision() {
|
||||
return findBlocksBeforeEntityCollision(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCollision() {
|
||||
return !collisions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CastResult.RayCollision firstCollision() {
|
||||
return collisions.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CastResult.RayCollision lastCollision() {
|
||||
return collisions.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasEntityCollision() {
|
||||
return !entityCollisions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EntityCastResult.EntityRayCollision firstEntityCollision() {
|
||||
return entityCollisions.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EntityCastResult.EntityRayCollision lastEntityCollision() {
|
||||
return entityCollisions.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBlockCollision() {
|
||||
return !blockCollisions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockRayCollision firstBlockCollision() {
|
||||
return blockCollisions.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockRayCollision lastBlockCollision() {
|
||||
return blockCollisions.getLast();
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* The result of a {@link Ray#cast(Collection)}
|
||||
*/
|
||||
public interface EntityCastResult extends CastResult {
|
||||
/**
|
||||
* Whether this {@link EntityCastResult} has an entity collision or not
|
||||
*
|
||||
* @return true if there is an entity collision, otherwise false
|
||||
*/
|
||||
boolean hasEntityCollision();
|
||||
|
||||
/**
|
||||
* Returns the first entity collision from the proceeding {@link Ray#cast}
|
||||
*
|
||||
* @return the first entity collision
|
||||
* @throws NoSuchElementException when the entity cast result has no {@link EntityRayCollision}
|
||||
*/
|
||||
@NotNull EntityCastResult.EntityRayCollision firstEntityCollision();
|
||||
|
||||
/**
|
||||
* Returns the last entity collision from the proceeding {@link Ray#cast}
|
||||
*
|
||||
* @return the last entity collision
|
||||
* @throws NoSuchElementException when the entity cast result has no {@link EntityRayCollision}
|
||||
*/
|
||||
@NotNull EntityCastResult.EntityRayCollision lastEntityCollision();
|
||||
|
||||
/**
|
||||
* Returns an ordered immutable {@link EntityRayCollision} list from the proceeding {@link Ray#cast}.
|
||||
*
|
||||
* @return the list of entity collisions
|
||||
*/
|
||||
@NotNull List<EntityRayCollision> entityCollisions();
|
||||
|
||||
/**
|
||||
* @param entity the intersected entity
|
||||
* @see CastResult.RayCollision
|
||||
*/
|
||||
record EntityRayCollision(@NotNull Point entry, @NotNull Point exit, @Nullable Vec entrySurfaceNormal,
|
||||
@Nullable Vec exitSurfaceNormal, @NotNull Entity entity) implements RayCollision {
|
||||
@Override
|
||||
public @NotNull Vec entrySurfaceNormal() {
|
||||
if (entrySurfaceNormal == null) {
|
||||
throw new NoSuchElementException("Cast ray with Ray.Configuration.computeSurfaceNormals = true");
|
||||
}
|
||||
return entrySurfaceNormal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Vec exitSurfaceNormal() {
|
||||
if (exitSurfaceNormal == null) {
|
||||
throw new NoSuchElementException("Cast ray with Ray.Configuration.computeSurfaceNormals = true");
|
||||
}
|
||||
return exitSurfaceNormal;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
record EntityCastResultImpl(@NotNull List<EntityRayCollision> entityCollisions) implements EntityCastResult
|
||||
{
|
||||
EntityCastResultImpl {
|
||||
entityCollisions = Collections.unmodifiableList(entityCollisions);
|
||||
}
|
||||
|
||||
public boolean hasCollision() {
|
||||
return !entityCollisions.isEmpty();
|
||||
}
|
||||
|
||||
public @NotNull CastResult.RayCollision firstCollision() {
|
||||
return entityCollisions.getFirst();
|
||||
}
|
||||
|
||||
public @NotNull CastResult.RayCollision lastCollision() {
|
||||
return entityCollisions.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasEntityCollision() {
|
||||
return !entityCollisions.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EntityRayCollision firstEntityCollision() {
|
||||
return entityCollisions.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EntityRayCollision lastEntityCollision() {
|
||||
return entityCollisions.getLast();
|
||||
}
|
||||
}
|
194
src/main/java/net/minestom/server/collision/Ray.java
Normal file
194
src/main/java/net/minestom/server/collision/Ray.java
Normal file
@ -0,0 +1,194 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
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.block.Block;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Represents an immutable ray which can be cast to find intersections
|
||||
* through blocks and entities.
|
||||
*
|
||||
* @param origin the starting point of this ray
|
||||
* @param direction the direction of this ray
|
||||
* @param distance the distance this ray travels
|
||||
* @param configuration the additional configuration properties for this ray
|
||||
*/
|
||||
public record Ray(@NotNull Point origin, @NotNull Vec direction, double distance, @NotNull Configuration configuration) {
|
||||
/**
|
||||
* @throws IllegalStateException if distance is less than 0
|
||||
*/
|
||||
public Ray {
|
||||
if (origin instanceof Pos) origin = Vec.fromPoint(origin);
|
||||
if (!direction.isNormalized()) {
|
||||
direction = direction.normalize();
|
||||
}
|
||||
Check.stateCondition(distance < 0, "Distance cannot be less than 0");
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
private Ray(@NotNull Point origin, @NotNull Vec direction, double distance, @NotNull UnaryOperator<Configuration.Builder> unaryOperator) {
|
||||
this(origin, direction, distance, unaryOperator.apply(Configuration.builder()).build());
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public Ray(@NotNull Point origin, @NotNull Vec direction, double distance, @NotNull Consumer<Configuration.Builder> consumer) {
|
||||
this(origin, direction, distance, builder -> {
|
||||
consumer.accept(builder);
|
||||
return builder;
|
||||
});
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public Ray(@NotNull Point origin, @NotNull Vec direction, double distance) {
|
||||
this(origin, direction, distance, Configuration.DEFAULT);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public @NotNull Ray withOrigin(@NotNull Point origin) {
|
||||
return new Ray(origin, direction, distance, configuration);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public @NotNull Ray withDirection(@NotNull Vec direction) {
|
||||
return new Ray(origin, direction, distance, configuration);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public @NotNull Ray withDistance(double distance) {
|
||||
return new Ray(origin, direction, distance, configuration);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public @NotNull Ray withConfiguration(@NotNull Configuration configuration) {
|
||||
return new Ray(origin, direction, distance, configuration);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public @NotNull Ray withConfiguration(@NotNull Consumer<Configuration.Builder> consumer) {
|
||||
Configuration.Builder builder = Configuration.builder();
|
||||
consumer.accept(builder);
|
||||
return new Ray(origin, direction, distance, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this ray against the target entity and block bounding boxes.
|
||||
* <p>
|
||||
* Note: entity and block collisions are determined independently,
|
||||
* intersecting blocks will never stop proceeding entities from being collected;
|
||||
* the resulting {@link EntityBlockCastResult} gives you tools to filter
|
||||
* blocks/entities with this logic.
|
||||
*
|
||||
* @param blockGetter the {@link Block.Getter} supplying blocks to cast the ray against
|
||||
* @param entities the entities to cast the ray against
|
||||
* @return an {@link EntityBlockCastResult} containing the intersections of this ray
|
||||
*/
|
||||
public @NotNull EntityBlockCastResult cast(@NotNull Block.Getter blockGetter, @NotNull Collection<Entity> entities) {
|
||||
return RaycastUtils.performEntityBlockCast(this, blockGetter, entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this ray against the target entity bounding boxes.
|
||||
*
|
||||
* @param entities the entities to cast the ray against
|
||||
* @return an {@link EntityCastResult} containing the entities intersected
|
||||
* by this ray
|
||||
*/
|
||||
public @NotNull EntityCastResult cast(@NotNull Collection<Entity> entities) {
|
||||
return RaycastUtils.performEntityCast(this, entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this ray against the target block bounding boxes.
|
||||
*
|
||||
* @param blockGetter the {@link Block.Getter} supplying blocks to cast the ray against
|
||||
* @return the {@link BlockCastResult} containing the blocks intersected by this ray
|
||||
*/
|
||||
public @NotNull BlockCastResult cast(@NotNull Block.Getter blockGetter) {
|
||||
return RaycastUtils.performBlockCast(this, blockGetter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represent the collision configuration for a given ray
|
||||
*
|
||||
* @param blockFilter the filter blocks must pass to be considered for collision
|
||||
* note: air is implicitly filtered
|
||||
* (defaults to allow any block)
|
||||
* @param blockCollisionLimit stop checking for blocks collisions beyond this limit
|
||||
* (defaults to {@link Integer#MAX_VALUE})
|
||||
* note: does not stop entity collision checks beyond this limit
|
||||
* - Useful for optimization
|
||||
* @param entityFilter the filter entities must pass to be considered for collision
|
||||
* (defaults to allow any entity)
|
||||
* @param entityBoundingBoxExpansion amount to expand the entity {@link BoundingBox} axis to
|
||||
* increase leniency in the ray cast
|
||||
* (defaults to no expansion)
|
||||
* @param computeSurfaceNormals true if surface normals should be computed
|
||||
* (defaults to false)
|
||||
*/
|
||||
public record Configuration(@NotNull Predicate<Block> blockFilter, int blockCollisionLimit, @NotNull Predicate<Entity> entityFilter,
|
||||
@NotNull Vec entityBoundingBoxExpansion, boolean computeSurfaceNormals) {
|
||||
public static final Configuration DEFAULT = Configuration.builder().build();
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if bounding box expansion components are less than 0
|
||||
*/
|
||||
public Configuration {
|
||||
Check.stateCondition(entityBoundingBoxExpansion.x() < 0, "Bounding box x expansion cannot be less than 0");
|
||||
Check.stateCondition(entityBoundingBoxExpansion.y() < 0, "Bounding box y expansion cannot be less than 0");
|
||||
Check.stateCondition(entityBoundingBoxExpansion.z() < 0, "Bounding box z expansion cannot be less than 0");
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Predicate<Block> blockFilter = block -> true;
|
||||
private int blockCollisionLimit = Integer.MAX_VALUE;
|
||||
private Predicate<Entity> entityFilter = entity -> true;
|
||||
private Vec entityBoundingBoxExpansion = Vec.ZERO;
|
||||
private boolean computeNormals = true;
|
||||
|
||||
private @NotNull Configuration build() {
|
||||
return new Configuration(blockFilter, blockCollisionLimit, entityFilter, entityBoundingBoxExpansion, computeNormals);
|
||||
}
|
||||
|
||||
public @NotNull Builder blockFilter(@NotNull Predicate<Block> blockFilter) {
|
||||
this.blockFilter = blockFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NotNull Builder blockCollisionLimit(int blockCollisionLimit) {
|
||||
this.blockCollisionLimit = blockCollisionLimit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NotNull Builder entityFilter(@NotNull Predicate<Entity> entityFilter) {
|
||||
this.entityFilter = entityFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NotNull Builder entityBoundingBoxExpansion(double x, double y, double z) {
|
||||
this.entityBoundingBoxExpansion = new Vec(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NotNull Builder computeSurfaceNormals(boolean computeNormals) {
|
||||
this.computeNormals = computeNormals;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Builder() { }
|
||||
}
|
||||
|
||||
private static @NotNull Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
}
|
363
src/main/java/net/minestom/server/collision/RaycastUtils.java
Normal file
363
src/main/java/net/minestom/server/collision/RaycastUtils.java
Normal file
@ -0,0 +1,363 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utils class for {@link Ray}.
|
||||
*/
|
||||
class RaycastUtils
|
||||
{
|
||||
/**
|
||||
* Raycasting section
|
||||
*/
|
||||
private static final double NORMAL_EPSILON = 0.000001;
|
||||
private static final Vec MAX_X_NORMAL = new Vec(1, 0, 0);
|
||||
private static final Vec MIN_X_NORMAL = new Vec(-1, 0, 0);
|
||||
private static final Vec MAX_Y_NORMAL = new Vec(0, 1, 0);
|
||||
private static final Vec MIN_Y_NORMAL = new Vec(0, -1, 0);
|
||||
private static final Vec MAX_Z_NORMAL = new Vec(0, 0, 1);
|
||||
private static final Vec MIN_Z_NORMAL = new Vec(0, 0, -1);
|
||||
|
||||
static @NotNull EntityBlockCastResult performEntityBlockCast(@NotNull Ray ray, @NotNull Block.Getter blockGetter, @NotNull Collection<Entity> entities) {
|
||||
List<BlockCastResult.BlockRayCollision> blockCollisions = performBlockCast(ray, blockGetter).blockCollisions();
|
||||
List<EntityCastResult.EntityRayCollision> entityCollisions = performEntityCast(ray, entities).entityCollisions();
|
||||
// Create an ordered list with all results
|
||||
ArrayList<CastResult.RayCollision> allRayCollisions = new ArrayList<>(blockCollisions.size() + entityCollisions.size());
|
||||
allRayCollisions.addAll(blockCollisions);
|
||||
allRayCollisions.addAll(entityCollisions);
|
||||
allRayCollisions.sort(Comparator.comparingDouble(a -> a.entry().distanceSquared(ray.origin())));
|
||||
return new EntityBlockRaycastResultImpl(entityCollisions, blockCollisions, allRayCollisions);
|
||||
}
|
||||
|
||||
static @NotNull BlockCastResult performBlockCast(@NotNull Ray ray, @NotNull Block.Getter blockGetter) {
|
||||
Vec reciprocal = new Vec(1 / ray.direction().x(), 1 / ray.direction().y(), 1 / ray.direction().z());
|
||||
return new BlockCastResultImpl(findIntersectingBlocks(ray, blockGetter, reciprocal));
|
||||
}
|
||||
|
||||
static @NotNull EntityCastResult performEntityCast(@NotNull Ray ray, @NotNull Collection<Entity> entities) {
|
||||
Vec reciprocal = new Vec(1 / ray.direction().x(), 1 / ray.direction().y(), 1 / ray.direction().z());
|
||||
return new EntityCastResultImpl(findIntersectingEntities(ray, entities, reciprocal));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ray the casting ray
|
||||
* @param blockGetter the getter containing the target blocks
|
||||
* @param reciprocal the pre-computed ray direction reciprocal (1/vec)
|
||||
* @return the intersecting block ray collision list
|
||||
*/
|
||||
private static @NotNull List<BlockCastResult.BlockRayCollision> findIntersectingBlocks(@NotNull Ray ray, @NotNull Block.Getter blockGetter,
|
||||
@NotNull Vec reciprocal) {
|
||||
// Grab every block coordinate that the ray passes through using
|
||||
// a 3d digital differential analyzer (line drawing) algorithm
|
||||
boolean infiniteX = Double.isInfinite(reciprocal.x()), infiniteY = Double.isInfinite(reciprocal.y()), infiniteZ = Double.isInfinite(reciprocal.z());
|
||||
|
||||
// Initialization phase
|
||||
Point origin = ray.origin();
|
||||
Vec direction = ray.direction();
|
||||
int blockX = origin.blockX();
|
||||
int blockY = origin.blockY();
|
||||
int blockZ = origin.blockZ();
|
||||
final int stepX = direction.x() > 0 ? 1 : -1;
|
||||
final int stepY = direction.y() > 0 ? 1 : -1;
|
||||
final int stepZ = direction.z() > 0 ? 1 : -1;
|
||||
final double deltaX = Math.abs(1 / direction.x());
|
||||
final double deltaY = Math.abs(1 / direction.y());
|
||||
final double deltaZ = Math.abs(1 / direction.z());
|
||||
double currentX = (direction.x() > 0 ? blockX + 1 - origin.x() : origin.x() - blockX) * deltaX;
|
||||
double currentY = (direction.y() > 0 ? blockY + 1 - origin.y() : origin.y() - blockY) * deltaY;
|
||||
double currentZ = (direction.z() > 0 ? blockZ + 1 - origin.z() : origin.z() - blockZ) * deltaZ;
|
||||
if (Double.isNaN(currentX)) currentX = Double.POSITIVE_INFINITY;
|
||||
if (Double.isNaN(currentY)) currentY = Double.POSITIVE_INFINITY;
|
||||
if (Double.isNaN(currentZ)) currentZ = Double.POSITIVE_INFINITY;
|
||||
|
||||
int collisionLimit = ray.configuration().blockCollisionLimit();
|
||||
final List<BlockCastResult.BlockRayCollision> collisions = new ArrayList<>(Math.min(collisionLimit, (int) ray.distance() + 1));
|
||||
// Execution phase
|
||||
// Test for block at the beginning of the ray
|
||||
int collisionCount = appendSuccessfulBlockCollisions(ray, blockGetter, blockX, blockY, blockZ, collisions, 0,
|
||||
reciprocal, infiniteX, infiniteY, infiniteZ);
|
||||
|
||||
while (collisionCount < collisionLimit && Math.min(currentX, Math.min(currentY, currentZ)) <= ray.distance()) {
|
||||
// Travel the minimum distance needed to progress to the next block coordinate
|
||||
if (currentX < currentZ && currentX < currentY) {
|
||||
currentX += deltaX;
|
||||
blockX += stepX;
|
||||
}
|
||||
else if (currentZ < currentY) {
|
||||
currentZ += deltaZ;
|
||||
blockZ += stepZ;
|
||||
}
|
||||
else {
|
||||
currentY += deltaY;
|
||||
blockY += stepY;
|
||||
}
|
||||
|
||||
collisionCount += appendSuccessfulBlockCollisions(ray, blockGetter, blockX, blockY, blockZ, collisions, collisionCount,
|
||||
reciprocal, infiniteX, infiniteY, infiniteZ);
|
||||
}
|
||||
|
||||
return collisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ray the casting ray
|
||||
* @param blockGetter the getter containing the target block
|
||||
* @param blockX the x coordinate of the block to check against
|
||||
* @param blockY the y coordinate of the block to check against
|
||||
* @param blockZ the z coordinate of the block to check against
|
||||
* @param collisions the array to append any successful block collisions to
|
||||
* @param collisionCount the current collision count; for exiting when a block has
|
||||
* multiple bounding boxes and exceeds the config limit
|
||||
* @param reciprocal the pre-computed ray direction reciprocal (1/vec)
|
||||
* @param infiniteX pass true if the reciprocal x component is infinity
|
||||
* @param infiniteY pass true if the reciprocal y component is infinity
|
||||
* @param infiniteZ pass true if the reciprocal z component is infinity
|
||||
* @return the amount of blocks added to the collisions array
|
||||
*/
|
||||
private static int appendSuccessfulBlockCollisions(@NotNull Ray ray, @NotNull Block.Getter blockGetter,
|
||||
int blockX, int blockY, int blockZ,
|
||||
@NotNull List<BlockCastResult.BlockRayCollision> collisions,
|
||||
int collisionCount,
|
||||
@NotNull Vec reciprocal,
|
||||
boolean infiniteX, boolean infiniteY, boolean infiniteZ) {
|
||||
Block block = blockGetter.getBlock(blockX, blockY, blockZ);
|
||||
if (block.isAir() || !ray.configuration().blockFilter().test(block)) return 0;
|
||||
BoundingBox[] boxes = ((ShapeImpl) block.registry().collisionShape()).getCollisionBoundingBoxes();
|
||||
if (boxes.length == 0) return 0;
|
||||
|
||||
final double distanceSquared = ray.distance() * ray.distance();
|
||||
if (boxes.length == 1) {
|
||||
final BoundingBox box = boxes[0];
|
||||
CollisionResult result = findBoundingBoxCollision(ray, box, blockX, blockY, blockZ, reciprocal, infiniteX, infiniteY, infiniteZ);
|
||||
if (result != null && result.entry().distanceSquared(ray.origin()) <= distanceSquared) {
|
||||
collisions.add(createBlockRayCollision(result, block, box, blockX, blockY, blockZ, ray.configuration()));
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Exit early if the collision limit if the block has multiple bounding boxes
|
||||
int collisionLimit = ray.configuration().blockCollisionLimit();
|
||||
List<BlockCastResult.BlockRayCollision> orderedCollisions = new ArrayList<>(boxes.length);
|
||||
for (BoundingBox box : boxes) {
|
||||
CollisionResult result = findBoundingBoxCollision(ray, box, blockX, blockY, blockZ, reciprocal, false, false, false);
|
||||
if (result != null && collisionCount < collisionLimit && result.entry().distanceSquared(ray.origin()) <= distanceSquared) {
|
||||
orderedCollisions.add(createBlockRayCollision(result, block, box, blockX, blockY, blockZ, ray.configuration()));
|
||||
collisionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Order multiple collisions by their distance to the ray origin
|
||||
orderedCollisions.sort(Comparator.comparingDouble(a -> a.entry().distanceSquared(ray.origin())));
|
||||
collisions.addAll(orderedCollisions);
|
||||
return orderedCollisions.size();
|
||||
}
|
||||
|
||||
private static @NotNull BlockCastResult.BlockRayCollision createBlockRayCollision(@NotNull CollisionResult collisionResult, @NotNull Block block,
|
||||
@NotNull BoundingBox box, double offsetX, double offsetY, double offsetZ,
|
||||
@NotNull Ray.Configuration configuration) {
|
||||
Vec entrySurfaceNormal = null;
|
||||
Vec exitSurfaceNormal = null;
|
||||
if (configuration.computeSurfaceNormals()) {
|
||||
entrySurfaceNormal = computeSurfaceNormal(collisionResult.entry(), box, offsetX, offsetY, offsetZ);
|
||||
exitSurfaceNormal = computeSurfaceNormal(collisionResult.exit(), box, offsetX, offsetY, offsetZ);
|
||||
}
|
||||
return new BlockCastResult.BlockRayCollision(collisionResult.entry(), collisionResult.exit(), entrySurfaceNormal, exitSurfaceNormal, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ray the casting ray
|
||||
* @param entities the entities to test intersections again
|
||||
* @param reciprocal the pre-computed ray direction reciprocal (1/vec)
|
||||
* @return the intersecting block ray collision list
|
||||
*/
|
||||
private static @NotNull List<EntityCastResult.EntityRayCollision> findIntersectingEntities(@NotNull Ray ray, @NotNull Collection<Entity> entities,
|
||||
@NotNull Vec reciprocal) {
|
||||
// Make the assumption that a ray is unlikely to intersect > 1 entity at once
|
||||
final List<EntityCastResult.EntityRayCollision> collisions = new ArrayList<>(1);
|
||||
final double maxDistanceSquared = ray.distance() * ray.distance();
|
||||
Vec boxExpansion = ray.configuration().entityBoundingBoxExpansion();
|
||||
boolean expandBoundingBox = !boxExpansion.isZero();
|
||||
boolean infiniteX = Double.isInfinite(reciprocal.x()), infiniteY = Double.isInfinite(reciprocal.y()), infiniteZ = Double.isInfinite(reciprocal.z());
|
||||
// Check entities
|
||||
for (Entity entity : entities) {
|
||||
if (!ray.configuration().entityFilter().test(entity)) continue;
|
||||
BoundingBox box = expandBoundingBox ? entity.getBoundingBox().expand(boxExpansion.x(), boxExpansion.y(), boxExpansion.z()) : entity.getBoundingBox();
|
||||
CollisionResult result = findBoundingBoxCollision(ray, box, entity.getPosition(), reciprocal, infiniteX, infiniteY, infiniteZ);
|
||||
if (result != null && result.entry().distanceSquared(ray.origin()) < maxDistanceSquared) {
|
||||
collisions.add(createEntityRayCollision(result, entity, box, ray.configuration()));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by intersection order
|
||||
if (collisions.size() > 1) {
|
||||
collisions.sort(Comparator.comparingDouble(a -> a.entry().distanceSquared(ray.origin())));
|
||||
}
|
||||
return collisions;
|
||||
}
|
||||
|
||||
private static @NotNull EntityCastResult.EntityRayCollision createEntityRayCollision(@NotNull CollisionResult collisionResult,
|
||||
@NotNull Entity entity, @NotNull BoundingBox box,
|
||||
@NotNull Ray.Configuration configuration) {
|
||||
Point offset = entity.getPosition();
|
||||
Vec entrySurfaceNormal = null;
|
||||
Vec exitSurfaceNormal = null;
|
||||
if (configuration.computeSurfaceNormals()) {
|
||||
entrySurfaceNormal = computeSurfaceNormal(collisionResult.entry(), box, offset.x(), offset.y(), offset.z());
|
||||
exitSurfaceNormal = computeSurfaceNormal(collisionResult.exit(), box, offset.x(), offset.y(), offset.z());
|
||||
}
|
||||
return new EntityCastResult.EntityRayCollision(collisionResult.entry(), collisionResult.exit(), entrySurfaceNormal, exitSurfaceNormal, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry/exit points of a ray on any given aabb bounding box.
|
||||
* Pre-calculating the reciprocal and infinities saves significant time.
|
||||
*
|
||||
* @param ray the casting ray
|
||||
* @param box the target bounding box
|
||||
* @param offsetX the x offset of the box to in global space
|
||||
* @param offsetY the y offset of the box to in global space
|
||||
* @param offsetZ the z offset of the box to in global space
|
||||
* @param reciprocal the pre-computed ray direction reciprocal (1/vec)
|
||||
* @param infiniteX pass true if the reciprocal x component is infinity
|
||||
* @param infiniteY pass true if the reciprocal y component is infinity
|
||||
* @param infiniteZ pass true if the reciprocal z component is infinity
|
||||
* @return a collision result if an intersection occurred, otherwise null
|
||||
*/
|
||||
private static @Nullable RaycastUtils.CollisionResult findBoundingBoxCollision(@NotNull Ray ray, @NotNull BoundingBox box,
|
||||
double offsetX, double offsetY, double offsetZ,
|
||||
@NotNull Vec reciprocal,
|
||||
boolean infiniteX, boolean infiniteY, boolean infiniteZ) {
|
||||
Point origin = ray.origin();
|
||||
Vec direction = ray.direction();
|
||||
// Determine the AABB ray intersections fast with the slab method
|
||||
// t[min/max] represents t in the parametric ray equation
|
||||
// ray.origin + ray.direction * t
|
||||
|
||||
final double tx1, tx2;
|
||||
if (infiniteX) {
|
||||
// Explicitly set these to infinity to avoid an edge case
|
||||
// where 0 is multiplied by infinity
|
||||
tx1 = Double.NEGATIVE_INFINITY;
|
||||
tx2 = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
else {
|
||||
tx1 = (box.minX() + offsetX - origin.x()) * reciprocal.x();
|
||||
tx2 = (box.minX() + box.width() + offsetX - origin.x()) * reciprocal.x();
|
||||
}
|
||||
|
||||
final double ty1, ty2;
|
||||
if (infiniteY) {
|
||||
ty1 = Double.NEGATIVE_INFINITY;
|
||||
ty2 = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
else {
|
||||
ty1 = (box.minY() + offsetY - origin.y()) * reciprocal.y();
|
||||
ty2 = (box.minY() + box.height() + offsetY - origin.y()) * reciprocal.y();
|
||||
}
|
||||
|
||||
final double tz1, tz2;
|
||||
if (infiniteZ) {
|
||||
tz1 = Double.NEGATIVE_INFINITY;
|
||||
tz2 = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
else {
|
||||
tz1 = (box.minZ() + offsetZ - origin.z()) * reciprocal.z();
|
||||
tz2 = (box.minZ() + box.depth() + offsetZ - origin.z()) * reciprocal.z();
|
||||
}
|
||||
|
||||
double tEntry = Math.min(tx1, tx2);
|
||||
double tExit = Math.max(tx1, tx2);
|
||||
|
||||
tEntry = Math.max(tEntry, Math.min(ty1, ty2));
|
||||
tExit = Math.min(tExit, Math.max(ty1, ty2));
|
||||
|
||||
tEntry = Math.max(tEntry, Math.min(tz1, tz2));
|
||||
tExit = Math.min(tExit, Math.max(tz1, tz2));
|
||||
|
||||
if (tEntry < 0 || tExit < tEntry) return null;
|
||||
return new CollisionResult(origin.add(direction.x() * tEntry, direction.y() * tEntry, direction.z() * tEntry),
|
||||
origin.add(direction.x() * tExit, direction.y() * tExit, direction.z() * tExit));
|
||||
}
|
||||
|
||||
private static @Nullable CollisionResult findBoundingBoxCollision(@NotNull Ray ray, @NotNull BoundingBox box,
|
||||
@NotNull Point offset,
|
||||
@NotNull Vec reciprocal,
|
||||
boolean infiniteX, boolean infiniteY, boolean infiniteZ) {
|
||||
return findBoundingBoxCollision(ray, box, offset.x(), offset.y(), offset.z(), reciprocal, infiniteX, infiniteY, infiniteZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the surface normal on a given collision.
|
||||
*
|
||||
* @return the normalized surface normal vec
|
||||
*/
|
||||
private static @NotNull Vec computeSurfaceNormal(@NotNull Point intersection, @NotNull BoundingBox box,
|
||||
double offsetX, double offsetY, double offsetZ) {
|
||||
if (Math.abs(intersection.x() - (box.maxX() + offsetX)) < NORMAL_EPSILON) return MAX_X_NORMAL;
|
||||
if (Math.abs(intersection.x() - (box.minX() + offsetX)) < NORMAL_EPSILON) return MIN_X_NORMAL;
|
||||
if (Math.abs(intersection.y() - (box.maxY() + offsetY)) < NORMAL_EPSILON) return MAX_Y_NORMAL;
|
||||
if (Math.abs(intersection.y() - (box.minY() + offsetY)) < NORMAL_EPSILON) return MIN_Y_NORMAL;
|
||||
if (Math.abs(intersection.z() - (box.maxZ() + offsetZ)) < NORMAL_EPSILON) return MAX_Z_NORMAL;
|
||||
else return MIN_Z_NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* For returning collision data from {@link RaycastUtils#findBoundingBoxCollision}
|
||||
* to construct {@link CastResult} objects.
|
||||
*/
|
||||
private record CollisionResult(@NotNull Point entry, @NotNull Point exit) {}
|
||||
|
||||
|
||||
/**
|
||||
* Ray helpers section
|
||||
*/
|
||||
|
||||
static @NotNull List<EntityCastResult.EntityRayCollision> findEntitiesBeforeEntityCollision(@NotNull List<CastResult.RayCollision> collisions, int collisionThreshold) {
|
||||
ArrayList<EntityCastResult.EntityRayCollision> foundCollisions = new ArrayList<>();
|
||||
int blockCollisionCount = 0;
|
||||
for (CastResult.RayCollision collision : collisions) {
|
||||
if (collision instanceof EntityCastResult.EntityRayCollision entityCollision) {
|
||||
foundCollisions.add(entityCollision);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Must be a block collision
|
||||
if (++blockCollisionCount == collisionThreshold) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return foundCollisions;
|
||||
}
|
||||
|
||||
static @NotNull List<BlockCastResult.BlockRayCollision> findBlocksBeforeEntityCollision(@NotNull List<CastResult.RayCollision> collisions, int collisionThreshold) {
|
||||
ArrayList<BlockCastResult.BlockRayCollision> foundCollisions = new ArrayList<>();
|
||||
int blockCollisionCount = 0;
|
||||
for (CastResult.RayCollision collision : collisions) {
|
||||
if (collision instanceof BlockCastResult.BlockRayCollision entityCollision) {
|
||||
foundCollisions.add(entityCollision);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Must be an entity collision
|
||||
if (++blockCollisionCount == collisionThreshold) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return foundCollisions;
|
||||
}
|
||||
|
||||
private RaycastUtils() {}
|
||||
}
|
@ -195,6 +195,10 @@ public final class ShapeImpl implements Shape {
|
||||
return block;
|
||||
}
|
||||
|
||||
BoundingBox @NotNull [] getCollisionBoundingBoxes() {
|
||||
return collisionBoundingBoxes;
|
||||
}
|
||||
|
||||
private static @NotNull List<Rectangle> computeOcclusionSet(BlockFace face, BoundingBox[] boundingBoxes) {
|
||||
List<Rectangle> rSet = new ArrayList<>();
|
||||
for (BoundingBox boundingBox : boundingBoxes) {
|
||||
|
117
src/test/java/net/minestom/server/collision/TestRay.java
Normal file
117
src/test/java/net/minestom/server/collision/TestRay.java
Normal file
@ -0,0 +1,117 @@
|
||||
package net.minestom.server.collision;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.testing.Env;
|
||||
import net.minestom.testing.EnvTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@EnvTest
|
||||
public class TestRay
|
||||
{
|
||||
@Test
|
||||
public void axisAlignedLines(Env env) {
|
||||
// Straight axis aligned line (i.e. Vec(1, 0, 0) require special handling
|
||||
// to ensure they process correctly
|
||||
|
||||
var instance = env.createFlatInstance();
|
||||
instance.loadChunk(-1, 0).join();
|
||||
instance.loadChunk(0, -1).join();
|
||||
|
||||
instance.setBlock(0, 52, 0, Block.DIRT);
|
||||
instance.setBlock(1, 52, 0, Block.DIRT);
|
||||
|
||||
Ray ray = new Ray(new Vec(-1, 52, 0), new Vec(1, 0, 0), 10);
|
||||
assertEquals(2, ray.cast(instance).blockCollisions().size());
|
||||
assertEquals(0, ray.withDirection(new Vec(-1, 0, 0)).cast(instance).blockCollisions().size());
|
||||
|
||||
ray = new Ray(new Vec(0, 52, 1), new Vec(0, 0, -1), 10);
|
||||
assertEquals(1, ray.cast(instance).blockCollisions().size());
|
||||
assertEquals(0, ray.withDirection(new Vec(0, 0, 1)).cast(instance).blockCollisions().size());
|
||||
|
||||
ray = new Ray(new Vec(0, 53, 0), new Vec(0, -1, 0), 10);
|
||||
assertEquals(1, ray.cast(instance).blockCollisions().size());
|
||||
assertEquals(0, ray.withDirection(new Vec(0, 1, 1)).cast(instance).blockCollisions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intersectionDistance(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
Ray ray = new Ray(new Vec(0, 42.01, 0), new Vec(0, -1, 0), 10);
|
||||
ray.cast(instance).blockCollisions().forEach(collision -> {
|
||||
assertTrue(collision.entry().distance(ray.origin()) <= 10);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockLimit(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
Ray ray = new Ray(new Vec(0, 42, 0), new Vec(0, -1, 1), 20, config -> {
|
||||
config.blockCollisionLimit(8);
|
||||
});
|
||||
assertEquals(8, ray.cast(instance).blockCollisions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entityBoundingBoxExpansion(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
Entity entity = new Entity(EntityType.ZOMBIE);
|
||||
entity.setInstance(instance, new Vec(0, 42, 0));
|
||||
|
||||
Ray ray = new Ray(new Vec(-11, 42, 0), new Vec(1, 0, 0), 10);
|
||||
assertFalse(ray.cast(List.of(entity)).hasEntityCollision());
|
||||
assertTrue(ray.withConfiguration(config -> config.entityBoundingBoxExpansion(2, 2, 2)).cast(List.of(entity)).hasEntityCollision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findBeforeCollision(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
Vec direction = new Vec(1, 0, 1).normalize();
|
||||
Point origin = new Vec(0, 50, 0);
|
||||
Ray ray = new Ray(origin.add(0, 0.05, 0), direction, 14);
|
||||
|
||||
// Make a diagonal line of entities and dirt
|
||||
instance.setBlock(origin.add(direction.mul(2)), Block.DIRT);
|
||||
|
||||
Entity entity1 = new Entity(EntityType.ZOMBIE);
|
||||
entity1.setInstance(instance, origin.add(direction.mul(4)));
|
||||
|
||||
instance.setBlock(origin.add(direction.mul(6)), Block.DIRT);
|
||||
|
||||
Entity entity2 = new Entity(EntityType.ZOMBIE);
|
||||
entity2.setInstance(instance, origin.add(direction.mul(8)));
|
||||
|
||||
instance.setBlock(origin.add(direction.mul(10)), Block.DIRT);
|
||||
|
||||
Entity entity3 = new Entity(EntityType.ZOMBIE);
|
||||
entity3.setInstance(instance, origin.add(direction.mul(12)));
|
||||
|
||||
assertEquals(0, ray.cast(instance, List.of(entity1, entity2, entity3)).findEntitiesBeforeBlockCollision().size());
|
||||
assertEquals(1, ray.cast(instance, List.of(entity1, entity2, entity3)).findEntitiesBeforeBlockCollision(2).size());
|
||||
assertEquals(2, ray.cast(instance, List.of(entity1, entity2, entity3)).findEntitiesBeforeBlockCollision(3).size());
|
||||
|
||||
assertEquals(1, ray.cast(instance, List.of(entity1, entity2, entity3)).findBlocksBeforeEntityCollision().size());
|
||||
assertEquals(2, ray.cast(instance, List.of(entity1, entity2, entity3)).findBlocksBeforeEntityCollision(2).size());
|
||||
assertEquals(3, ray.cast(instance, List.of(entity1, entity2, entity3)).findBlocksBeforeEntityCollision(3).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void castFromInsideBoundingBox(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
instance.loadChunk(new Vec(1, 10, 1)).join();
|
||||
|
||||
Ray ray = new Ray(new Vec(1, 10, 1), new Vec(1, 2, 1.4), 2);
|
||||
assertEquals(ray.cast(instance).firstBlockCollision().entry(), ray.origin());
|
||||
|
||||
Entity entity = new Entity(EntityType.ZOMBIE);
|
||||
entity.setInstance(instance, ray.origin());
|
||||
assertEquals(ray.cast(List.of(entity)).firstEntityCollision().entry(), ray.origin());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user