Block Collision Physics (#730)

This commit is contained in:
iam 2022-03-09 13:08:42 -05:00 committed by GitHub
parent 9bca6ee0e3
commit 6891a530f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1833 additions and 608 deletions

View File

@ -0,0 +1,261 @@
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.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
final class BlockCollision {
// Minimum move amount, minimum final velocity
private static final double MIN_DELTA = 0.001;
/**
* Moves an entity with physics applied (ie checking against blocks)
* <p>
* Works by getting all the full blocks that an entity could interact with.
* All bounding boxes inside the full blocks are checked for collisions with the entity.
*
* @param entity the entity to move
* @return the result of physics simulation
*/
static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity,
@Nullable PhysicsResult lastPhysicsResult) {
final BoundingBox.Faces faces = entity.getBoundingBox().faces();
Vec remainingMove = entityVelocity;
// Allocate once and update values
final SweepResult finalResult = new SweepResult(1, 0, 0, 0, null);
boolean foundCollisionX = false, foundCollisionY = false, foundCollisionZ = false;
Point collisionYBlock = null;
Block blockYType = Block.AIR;
// Check cache to see if the entity is standing on a block without moving.
// If the entity isn't moving and the block below hasn't changed, return
if (lastPhysicsResult != null && entity.getInstance() != null) {
if (lastPhysicsResult.collisionY()
&& Math.signum(remainingMove.y()) == Math.signum(lastPhysicsResult.originalDelta().y())
&& lastPhysicsResult.collidedBlockY() != null
&& entity.getInstance().getChunk(lastPhysicsResult.collidedBlockY().chunkX(), lastPhysicsResult.collidedBlockY().chunkZ()) != null
&& entity.getInstance().getBlock(lastPhysicsResult.collidedBlockY(), Block.Getter.Condition.TYPE) == lastPhysicsResult.blockTypeY()
&& remainingMove.x() == 0 && remainingMove.z() == 0
&& entity.getPosition().samePoint(lastPhysicsResult.newPosition())
&& lastPhysicsResult.blockTypeY() != Block.AIR) {
remainingMove = remainingMove.withY(0);
foundCollisionY = true;
collisionYBlock = lastPhysicsResult.collidedBlockY();
blockYType = lastPhysicsResult.blockTypeY();
}
}
// If we're moving less than the MIN_DELTA value, set the velocity in that axis to 0.
// This prevents tiny moves from wasting cpu time
final double deltaX = Math.abs(remainingMove.x()) < MIN_DELTA ? 0 : remainingMove.x();
final double deltaY = Math.abs(remainingMove.y()) < MIN_DELTA ? 0 : remainingMove.y();
final double deltaZ = Math.abs(remainingMove.z()) < MIN_DELTA ? 0 : remainingMove.z();
remainingMove = new Vec(deltaX, deltaY, deltaZ);
if (remainingMove.isZero())
if (lastPhysicsResult != null)
return new PhysicsResult(entity.getPosition(), Vec.ZERO, lastPhysicsResult.isOnGround(),
lastPhysicsResult.collisionX(), lastPhysicsResult.collisionY(), lastPhysicsResult.collisionZ(),
entityVelocity, lastPhysicsResult.collidedBlockY(), lastPhysicsResult.blockTypeY());
else
return new PhysicsResult(entity.getPosition(), Vec.ZERO, false, false, false, false, entityVelocity, null, Block.AIR);
// Query faces to get the points needed for collision
Vec queryVec = new Vec(Math.signum(remainingMove.x()), Math.signum(remainingMove.y()), Math.signum(remainingMove.z()));
List<Vec> allFaces = faces.query().get(queryVec);
PhysicsResult res = handlePhysics(entity, remainingMove, entity.getPosition(), allFaces, finalResult);
// Loop until no collisions are found.
// When collisions are found, the collision axis is set to 0
// Looping until there are no collisions will allow the entity to move in axis other than the collision axis after a collision.
while (res.collisionX() || res.collisionY() || res.collisionZ()) {
// Reset final result
finalResult.res = 1;
finalResult.normalX = 0;
finalResult.normalY = 0;
finalResult.normalZ = 0;
if (res.collisionX()) foundCollisionX = true;
if (res.collisionZ()) foundCollisionZ = true;
if (res.collisionY()) {
foundCollisionY = true;
// If we are only moving in the y-axis
if (!res.collisionX() && !res.collisionZ() && entityVelocity.x() == 0 && entityVelocity.z() == 0) {
collisionYBlock = res.collidedBlockY();
blockYType = res.blockTypeY();
}
}
// If all axis have had collisions, break
if (foundCollisionX && foundCollisionY && foundCollisionZ) break;
// If the entity isn't moving, break
if (res.newVelocity().isZero()) break;
queryVec = new Vec(Math.signum(remainingMove.x()), Math.signum(remainingMove.y()), Math.signum(remainingMove.z()));
allFaces = faces.query().get(queryVec);
res = handlePhysics(entity, res.newVelocity(), res.newPosition(), allFaces, finalResult);
}
final double newDeltaX = foundCollisionX ? 0 : entityVelocity.x();
final double newDeltaY = foundCollisionY ? 0 : entityVelocity.y();
final double newDeltaZ = foundCollisionZ ? 0 : entityVelocity.z();
return new PhysicsResult(res.newPosition(), new Vec(newDeltaX, newDeltaY, newDeltaZ),
newDeltaY == 0 && entityVelocity.y() < 0,
foundCollisionX, foundCollisionY, foundCollisionZ, entityVelocity, collisionYBlock, blockYType);
}
/**
* Does a physics step until a boundary is found
*
* @param entity the entity to move
* @param deltaPosition the movement vector
* @param entityPosition the position of the entity
* @param allFaces point list to use for collision checking
* @param finalResult place to store final result of collision
* @return result of physics calculation
*/
private static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec deltaPosition, Pos entityPosition,
@NotNull List<Vec> allFaces, @NotNull SweepResult finalResult) {
final Instance instance = entity.getInstance();
final Chunk originChunk = entity.getChunk();
final BoundingBox boundingBox = entity.getBoundingBox();
double remainingX = deltaPosition.x();
double remainingY = deltaPosition.y();
double remainingZ = deltaPosition.z();
// If the movement is small we don't need to run the expensive ray casting.
if (deltaPosition.length() < 1) {
// Go through all points to check. See if the point after the move will be in a new block
// If the point after is in a new block that new block needs to be checked, otherwise only check the current block
for (Vec point : allFaces) {
Vec pointBefore = point.add(entityPosition);
Vec pointAfter = point.add(entityPosition).add(deltaPosition);
if (pointBefore.blockX() != pointAfter.blockX()) {
checkBoundingBox(pointAfter.blockX(), pointBefore.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
if (pointBefore.blockY() != pointAfter.blockY()) {
checkBoundingBox(pointAfter.blockX(), pointAfter.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
}
if (pointBefore.blockZ() != pointAfter.blockZ()) {
checkBoundingBox(pointAfter.blockX(), pointBefore.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
}
}
if (pointBefore.blockY() != pointAfter.blockY()) {
checkBoundingBox(pointBefore.blockX(), pointAfter.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
if (pointBefore.blockZ() != pointAfter.blockZ()) {
checkBoundingBox(pointBefore.blockX(), pointAfter.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
}
}
if (pointBefore.blockZ() != pointAfter.blockZ()) {
checkBoundingBox(pointBefore.blockX(), pointBefore.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
}
checkBoundingBox(pointBefore.blockX(), pointBefore.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
if (pointBefore.blockX() != pointAfter.blockX()
&& pointBefore.blockY() != pointAfter.blockY()
&& pointBefore.blockZ() != pointAfter.blockZ())
checkBoundingBox(pointAfter.blockX(), pointAfter.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult);
}
} else {
// When large moves are done we need to ray-cast to find all blocks that could intersect with the movement
for (Vec point : allFaces) {
RayUtils.RaycastCollision(deltaPosition, point.add(entityPosition), instance, originChunk, boundingBox, entityPosition, finalResult);
}
}
double finalX = entityPosition.x() + finalResult.res * remainingX;
double finalY = entityPosition.y() + finalResult.res * remainingY;
double finalZ = entityPosition.z() + finalResult.res * remainingZ;
boolean collisionX = false, collisionY = false, collisionZ = false;
// Remaining delta
remainingX -= finalResult.res * remainingX;
remainingY -= finalResult.res * remainingY;
remainingZ -= finalResult.res * remainingZ;
if (finalResult.normalX != 0) {
collisionX = true;
remainingX = 0;
}
if (finalResult.normalY != 0) {
collisionY = true;
remainingY = 0;
}
if (finalResult.normalZ != 0) {
collisionZ = true;
remainingZ = 0;
}
remainingX = Math.abs(remainingX) < MIN_DELTA ? 0 : remainingX;
remainingY = Math.abs(remainingY) < MIN_DELTA ? 0 : remainingY;
remainingZ = Math.abs(remainingZ) < MIN_DELTA ? 0 : remainingZ;
finalX = Math.abs(finalX - entityPosition.x()) < MIN_DELTA ? entityPosition.x() : finalX;
finalY = Math.abs(finalY - entityPosition.y()) < MIN_DELTA ? entityPosition.y() : finalY;
finalZ = Math.abs(finalZ - entityPosition.z()) < MIN_DELTA ? entityPosition.z() : finalZ;
return new PhysicsResult(new Pos(finalX, finalY, finalZ),
new Vec(remainingX, remainingY, remainingZ), collisionY,
collisionX, collisionY, collisionZ,
Vec.ZERO, finalResult.collidedShapePosition, finalResult.blockType);
}
/**
* Check if a moving entity will collide with a block. Updates finalResult
*
* @param blockX block x position
* @param blockY block y position
* @param blockZ block z position
* @param entityVelocity entity movement vector
* @param entityPosition entity position
* @param boundingBox entity bounding box
* @param instance entity instance
* @param originChunk entity chunk
* @param finalResult place to store final result of collision
* @return true if entity finds collision, other false
*/
static boolean checkBoundingBox(int blockX, int blockY, int blockZ,
Vec entityVelocity, Pos entityPosition, BoundingBox boundingBox,
Instance instance, Chunk originChunk, SweepResult finalResult) {
final Chunk c = ChunkUtils.retrieve(instance, originChunk, blockX, blockZ);
// Don't step if chunk isn't loaded yet
final Block checkBlock;
if (ChunkUtils.isLoaded(c)) {
checkBlock = c.getBlock(blockX, blockY, blockZ, Block.Getter.Condition.TYPE);
} else {
checkBlock = Block.STONE; // Generic full block
}
boolean hitBlock = false;
final Pos blockPos = new Pos(blockX, blockY, blockZ);
if (checkBlock.isSolid()) {
hitBlock = checkBlock.registry().collisionShape().intersectBoxSwept(entityPosition, entityVelocity, blockPos, boundingBox, finalResult);
}
return hitBlock;
}
}

View File

@ -4,83 +4,66 @@ 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 org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* See https://wiki.vg/Entity_metadata#Mobs_2
*/
public class BoundingBox {
public final class BoundingBox implements Shape {
private final double width, height, depth;
private final Point offset;
private Faces faces;
private final Entity entity;
private final double x, y, z;
private final CachedFace bottomFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())
));
private final CachedFace topFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
private final CachedFace leftFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())
));
private final CachedFace rightFace = new CachedFace(() -> List.of(
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())
));
private final CachedFace frontFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())
));
private final CachedFace backFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
/**
* Creates a {@link BoundingBox} linked to an {@link Entity} and with a specific size.
*
* @param entity the linked entity
* @param x the width size
* @param y the height size
* @param z the depth size
*/
public BoundingBox(@NotNull Entity entity, double x, double y, double z) {
this.entity = entity;
this.x = x;
this.y = y;
this.z = z;
BoundingBox(double width, double height, double depth, Point offset) {
this.width = width;
this.height = height;
this.depth = depth;
this.offset = offset;
}
/**
* Used to know if two {@link BoundingBox} intersect with each other.
*
* @param boundingBox the {@link BoundingBox} to check
* @return true if the two {@link BoundingBox} intersect with each other, false otherwise
*/
public boolean intersect(@NotNull BoundingBox boundingBox) {
return (getMinX() <= boundingBox.getMaxX() && getMaxX() >= boundingBox.getMinX()) &&
(getMinY() <= boundingBox.getMaxY() && getMaxY() >= boundingBox.getMinY()) &&
(getMinZ() <= boundingBox.getMaxZ() && getMaxZ() >= boundingBox.getMinZ());
public BoundingBox(double width, double height, double depth) {
this(width, height, depth, new Vec(-width / 2, 0, -depth / 2));
}
@Override
@ApiStatus.Experimental
public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) {
return (minX() + positionRelative.x() <= boundingBox.maxX() && maxX() + positionRelative.x() >= boundingBox.minX()) &&
(minY() + positionRelative.y() <= boundingBox.maxY() && maxY() + positionRelative.y() >= boundingBox.minY()) &&
(minZ() + positionRelative.z() <= boundingBox.maxZ() && maxZ() + positionRelative.z() >= boundingBox.minZ());
}
@Override
@ApiStatus.Experimental
public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) {
final boolean isHit = RayUtils.BoundingBoxIntersectionCheck(
moving, rayStart, rayDirection,
this,
shapePos
);
if (!isHit) return false;
SweepResult tempResult = new SweepResult(1, 0, 0, 0, null);
// Longer check to get result of collision
RayUtils.SweptAABB(moving, rayStart, rayDirection, this, shapePos, tempResult);
// Update final result if the temp result collision is sooner than the current final result
if (tempResult.res < finalResult.res) {
finalResult.res = tempResult.res;
finalResult.normalX = tempResult.normalX;
finalResult.normalY = tempResult.normalY;
finalResult.normalZ = tempResult.normalZ;
finalResult.collidedShapePosition = shapePos;
finalResult.collidedShape = this;
finalResult.blockType = null;
}
return true;
}
/**
@ -89,142 +72,36 @@ public class BoundingBox {
* @param entity the entity to check the bounding box
* @return true if this bounding box intersects with the entity, false otherwise
*/
public boolean intersect(@NotNull Entity entity) {
return intersect(entity.getBoundingBox());
@ApiStatus.Experimental
public boolean intersectEntity(@NotNull Point src, @NotNull Entity entity) {
return intersectBox(src.sub(entity.getPosition()), entity.getBoundingBox());
}
/**
* Used to know if the bounding box intersects at a block position.
*
* @param blockX the block X
* @param blockY the block Y
* @param blockZ the block Z
* @return true if the bounding box intersects with the position, false otherwise
*/
public boolean intersectWithBlock(int blockX, int blockY, int blockZ) {
final double offsetX = 1;
final double maxX = (double) blockX + offsetX;
final boolean checkX = getMinX() < maxX && getMaxX() > (double) blockX;
if (!checkX) return false;
final double maxY = (double) blockY + 0.99999;
final boolean checkY = getMinY() < maxY && getMaxY() > (double) blockY;
if (!checkY) return false;
final double offsetZ = 1;
final double maxZ = (double) blockZ + offsetZ;
// Z check
return getMinZ() < maxZ && getMaxZ() > (double) blockZ;
@ApiStatus.Experimental
public boolean boundingBoxRayIntersectionCheck(Vec start, Vec direction, Pos position) {
return RayUtils.BoundingBoxRayIntersectionCheck(start, direction, this, position);
}
/**
* Used to know if the bounding box intersects at a point.
*
* @param blockPosition the position to check
* @return true if the bounding box intersects with the position, false otherwise
*/
public boolean intersectWithBlock(@NotNull Point blockPosition) {
return intersectWithBlock(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ());
@Override
public @NotNull Point relativeStart() {
return offset;
}
/**
* 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());
@Override
public @NotNull Point relativeEnd() {
return offset.add(width, height, depth);
}
/**
* 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) {
// originally from http://www.3dkingdoms.com/weekly/weekly.php?a=3
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(Axis.X, getSegmentIntersection(x1 - x3, x2 - x3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.X, getSegmentIntersection(x1 - x4, x2 - x4, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Y, getSegmentIntersection(y1 - y3, y2 - y3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Y, getSegmentIntersection(y1 - y4, y2 - y4, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Z, getSegmentIntersection(z1 - z3, z2 - z3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Z, 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;
final double delta = dst1 / (dst1 - dst2);
return new Vec(
x1 + (x2 - x1) * delta,
y1 + (y2 - y1) * delta,
z1 + (z2 - z1) * delta
);
}
private boolean isInsideBoxWithAxis(Axis 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 == Axis.X && intersection.z() > z1 && intersection.z() < z2 && intersection.y() > y1 && intersection.y() < y2 ||
axis == Axis.Y && intersection.z() > z1 && intersection.z() < z2 && intersection.x() > x1 && intersection.x() < x2 ||
axis == Axis.Z && intersection.x() > x1 && intersection.x() < x2 && intersection.y() > y1 && intersection.y() < y2;
@Override
public String toString() {
String result = "BoundingBox";
result += "\n";
result += "[" + minX() + " : " + maxX() + "]";
result += "\n";
result += "[" + minY() + " : " + maxY() + "]";
result += "\n";
result += "[" + minZ() + " : " + maxZ() + "]";
return result;
}
/**
@ -236,7 +113,7 @@ public class BoundingBox {
* @return a new {@link BoundingBox} expanded
*/
public @NotNull BoundingBox expand(double x, double y, double z) {
return new BoundingBox(entity, this.x + x, this.y + y, this.z + z);
return new BoundingBox(this.width + x, this.height + y, this.depth + z);
}
/**
@ -248,187 +125,160 @@ public class BoundingBox {
* @return a new bounding box contracted
*/
public @NotNull BoundingBox contract(double x, double y, double z) {
return new BoundingBox(entity, this.x - x, this.y - y, this.z - z);
return new BoundingBox(this.width - x, this.height - y, this.depth - z);
}
/**
* Gets the width of the {@link BoundingBox}.
*
* @return the width
*/
public double getWidth() {
return x;
public double width() {
return width;
}
/**
* Gets the height of the {@link BoundingBox}.
*
* @return the height
*/
public double getHeight() {
return y;
public double height() {
return height;
}
/**
* Gets the depth of the {@link BoundingBox}.
*
* @return the depth
*/
public double getDepth() {
return z;
public double depth() {
return depth;
}
/**
* Gets the min X based on {@link #getWidth()} and the {@link Entity} position.
*
* @return the min X
*/
public double getMinX() {
return entity.getPosition().x() - (x / 2);
}
/**
* Gets the max X based on {@link #getWidth()} and the {@link Entity} position.
*
* @return the max X
*/
public double getMaxX() {
return entity.getPosition().x() + (x / 2);
}
/**
* Gets the min Y based on the {@link Entity} position.
*
* @return the min Y
*/
public double getMinY() {
return entity.getPosition().y();
}
/**
* Gets the max Y based on {@link #getHeight()} and the {@link Entity} position.
*
* @return the max Y
*/
public double getMaxY() {
return entity.getPosition().y() + y;
}
/**
* Gets the min Z based on {@link #getDepth()} and the {@link Entity} position.
*
* @return the min Z
*/
public double getMinZ() {
return entity.getPosition().z() - (z / 2);
}
/**
* Gets the max Z based on {@link #getDepth()} and the {@link Entity} position.
*
* @return the max Z
*/
public double getMaxZ() {
return entity.getPosition().z() + (z / 2);
}
/**
* Gets an array of {@link Vec} representing the points at the bottom of the {@link BoundingBox}.
*
* @return the points at the bottom of the {@link BoundingBox}
*/
public @NotNull List<Vec> getBottomFace() {
return bottomFace.get();
}
/**
* Gets an array of {@link Vec} representing the points at the top of the {@link BoundingBox}.
*
* @return the points at the top of the {@link BoundingBox}
*/
public @NotNull List<Vec> getTopFace() {
return topFace.get();
}
/**
* Gets an array of {@link Vec} representing the points on the left face of the {@link BoundingBox}.
*
* @return the points on the left face of the {@link BoundingBox}
*/
public @NotNull List<Vec> getLeftFace() {
return leftFace.get();
}
/**
* Gets an array of {@link Vec} representing the points on the right face of the {@link BoundingBox}.
*
* @return the points on the right face of the {@link BoundingBox}
*/
public @NotNull List<Vec> getRightFace() {
return rightFace.get();
}
/**
* Gets an array of {@link Vec} representing the points at the front of the {@link BoundingBox}.
*
* @return the points at the front of the {@link BoundingBox}
*/
public @NotNull List<Vec> getFrontFace() {
return frontFace.get();
}
/**
* Gets an array of {@link Vec} representing the points at the back of the {@link BoundingBox}.
*
* @return the points at the back of the {@link BoundingBox}
*/
public @NotNull List<Vec> getBackFace() {
return backFace.get();
}
@Override
public String toString() {
String result = "BoundingBox";
result += "\n";
result += "[" + getMinX() + " : " + getMaxX() + "]";
result += "\n";
result += "[" + getMinY() + " : " + getMaxY() + "]";
result += "\n";
result += "[" + getMinZ() + " : " + getMaxZ() + "]";
return result;
}
private enum Axis {
X, Y, Z
}
private final class CachedFace {
private final AtomicReference<@Nullable PositionedPoints> reference = new AtomicReference<>(null);
private final Supplier<@NotNull List<Vec>> faceProducer;
private CachedFace(Supplier<@NotNull List<Vec>> faceProducer) {
this.faceProducer = faceProducer;
@NotNull Faces faces() {
Faces faces = this.faces;
if (faces == null) {
this.faces = faces = retrieveFaces();
}
return faces;
}
@NotNull List<Vec> get() {
//noinspection ConstantConditions
return reference.updateAndGet(value -> {
Pos entityPosition = entity.getPosition();
if (value == null || !value.lastPosition.samePoint(entityPosition)) {
return new PositionedPoints(entityPosition, faceProducer.get());
}
return value;
}).points;
public double minX() {
return relativeStart().x();
}
public double maxX() {
return relativeEnd().x();
}
public double minY() {
return relativeStart().y();
}
public double maxY() {
return relativeEnd().y();
}
public double minZ() {
return relativeStart().z();
}
public double maxZ() {
return relativeEnd().z();
}
record Faces(Map<Vec, List<Vec>> query) {
public Faces {
query = Map.copyOf(query);
}
}
private static final class PositionedPoints {
private final @NotNull Pos lastPosition;
private final @NotNull List<Vec> points;
private List<Vec> buildSet(Set<Vec> a) {
return a.stream().toList();
}
private PositionedPoints(@NotNull Pos lastPosition, @NotNull List<Vec> points) {
this.lastPosition = lastPosition;
this.points = points;
}
private List<Vec> buildSet(Set<Vec> a, Set<Vec> b) {
Set<Vec> allFaces = new HashSet<>();
Stream.of(a, b).forEach(allFaces::addAll);
return allFaces.stream().toList();
}
private List<Vec> buildSet(Set<Vec> a, Set<Vec> b, Set<Vec> c) {
Set<Vec> allFaces = new HashSet<>();
Stream.of(a, b, c).forEach(allFaces::addAll);
return allFaces.stream().toList();
}
private Faces retrieveFaces() {
double minX = minX();
double maxX = maxX();
double minY = minY();
double maxY = maxY();
double minZ = minZ();
double maxZ = maxZ();
// Calculate steppings for each axis
// Start at minimum, increase by step size until we reach maximum
// This is done to catch all blocks that are part of that axis
// Since this stops before max point is reached, we add the max point after
final List<Double> stepsX = IntStream.rangeClosed(0, (int) ((maxX - minX))).mapToDouble(x -> x + minX).boxed().collect(Collectors.toCollection(ArrayList<Double>::new));
final List<Double> stepsY = IntStream.rangeClosed(0, (int) ((maxY - minY))).mapToDouble(x -> x + minY).boxed().collect(Collectors.toCollection(ArrayList<Double>::new));
final List<Double> stepsZ = IntStream.rangeClosed(0, (int) ((maxZ - minZ))).mapToDouble(x -> x + minZ).boxed().collect(Collectors.toCollection(ArrayList<Double>::new));
stepsX.add(maxX);
stepsY.add(maxY);
stepsZ.add(maxZ);
final Set<Vec> bottom = new HashSet<>();
final Set<Vec> top = new HashSet<>();
final Set<Vec> left = new HashSet<>();
final Set<Vec> right = new HashSet<>();
final Set<Vec> front = new HashSet<>();
final Set<Vec> back = new HashSet<>();
CartesianProduct.product(stepsX, stepsY).forEach(cross -> {
double i = (double) ((List<?>) cross).get(0);
double j = (double) ((List<?>) cross).get(1);
front.add(new Vec(i, j, minZ));
back.add(new Vec(i, j, maxZ));
});
CartesianProduct.product(stepsY, stepsZ).forEach(cross -> {
double j = (double) ((List<?>) cross).get(0);
double k = (double) ((List<?>) cross).get(1);
left.add(new Vec(minX, j, k));
right.add(new Vec(maxX, j, k));
});
CartesianProduct.product(stepsX, stepsZ).forEach(cross -> {
double i = (double) ((List<?>) cross).get(0);
double k = (double) ((List<?>) cross).get(1);
bottom.add(new Vec(i, minY, k));
top.add(new Vec(i, maxY, k));
});
// X -1 left | 1 right
// Y -1 bottom | 1 top
// Z -1 front | 1 back
var query = new HashMap<Vec, List<Vec>>();
query.put(new Vec(0, 0, 0), List.of());
query.put(new Vec(-1, 0, 0), buildSet(left));
query.put(new Vec(1, 0, 0), buildSet(right));
query.put(new Vec(0, -1, 0), buildSet(bottom));
query.put(new Vec(0, 1, 0), buildSet(top));
query.put(new Vec(0, 0, -1), buildSet(front));
query.put(new Vec(0, 0, 1), buildSet(back));
query.put(new Vec(0, -1, -1), buildSet(bottom, front));
query.put(new Vec(0, -1, 1), buildSet(bottom, back));
query.put(new Vec(0, 1, -1), buildSet(top, front));
query.put(new Vec(0, 1, 1), buildSet(top, back));
query.put(new Vec(-1, -1, 0), buildSet(left, bottom));
query.put(new Vec(-1, 1, 0), buildSet(left, top));
query.put(new Vec(1, -1, 0), buildSet(right, bottom));
query.put(new Vec(1, 1, 0), buildSet(right, top));
query.put(new Vec(-1, 0, -1), buildSet(left, front));
query.put(new Vec(-1, 0, 1), buildSet(left, back));
query.put(new Vec(1, 0, -1), buildSet(right, front));
query.put(new Vec(1, 0, 1), buildSet(right, back));
query.put(new Vec(1, 1, 1), buildSet(right, top, back));
query.put(new Vec(1, 1, -1), buildSet(right, top, front));
query.put(new Vec(1, -1, 1), buildSet(right, bottom, back));
query.put(new Vec(1, -1, -1), buildSet(right, bottom, front));
query.put(new Vec(-1, 1, 1), buildSet(left, top, back));
query.put(new Vec(-1, 1, -1), buildSet(left, top, front));
query.put(new Vec(-1, -1, 1), buildSet(left, bottom, back));
query.put(new Vec(-1, -1, -1), buildSet(left, bottom, front));
return new Faces(query);
}
}

View File

@ -0,0 +1,30 @@
package net.minestom.server.collision;
import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Optional.of;
import static java.util.stream.Collectors.toList;
// https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists#Java
final class CartesianProduct {
public static List<?> product(List<?>... a) {
if (a.length >= 2) {
List<?> product = a[0];
for (int i = 1; i < a.length; i++) {
product = product(product, a[i]);
}
return product;
}
return emptyList();
}
private static <A, B> List<?> product(List<A> a, List<B> b) {
return of(a.stream()
.map(e1 -> of(b.stream().map(e2 -> asList(e1, e2)).collect(toList())).orElse(emptyList()))
.flatMap(List::stream)
.collect(toList())).orElse(emptyList());
}
}

View File

@ -7,132 +7,34 @@ 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;
import net.minestom.server.item.Material;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Supplier;
public class CollisionUtils {
private static final Vec Y_AXIS = new Vec(0, 1, 0);
private static final Vec X_AXIS = new Vec(1, 0, 0);
private static final Vec Z_AXIS = new Vec(0, 0, 1);
@ApiStatus.Internal
@ApiStatus.Experimental
public final class CollisionUtils {
/**
* Moves an entity with physics applied (ie checking against blocks)
* <p>
* Works by getting all the full blocks that an entity could interact with.
* All bounding boxes inside the full blocks are checked for collisions with the entity.
*
* @param entity the entity to move
* @return the result of physics simulation
*/
public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec deltaPosition) {
// TODO handle collisions with nearby entities (should it be done here?)
final Instance instance = entity.getInstance();
final Chunk originChunk = entity.getChunk();
final Pos currentPosition = entity.getPosition();
final BoundingBox boundingBox = entity.getBoundingBox();
Vec stepVec = currentPosition.asVec();
boolean xCheck = false, yCheck = false, zCheck = false;
if (deltaPosition.y() != 0) {
final StepResult yCollision = stepAxis(instance, originChunk, stepVec, Y_AXIS, deltaPosition.y(),
deltaPosition.y() > 0 ? boundingBox.getTopFace() : boundingBox.getBottomFace());
yCheck = yCollision.foundCollision;
stepVec = yCollision.newPosition;
}
if (deltaPosition.x() != 0) {
final StepResult xCollision = stepAxis(instance, originChunk, stepVec, X_AXIS, deltaPosition.x(),
deltaPosition.x() < 0 ? boundingBox.getLeftFace() : boundingBox.getRightFace());
xCheck = xCollision.foundCollision;
stepVec = xCollision.newPosition;
}
if (deltaPosition.z() != 0) {
final StepResult zCollision = stepAxis(instance, originChunk, stepVec, Z_AXIS, deltaPosition.z(),
deltaPosition.z() > 0 ? boundingBox.getBackFace() : boundingBox.getFrontFace());
zCheck = zCollision.foundCollision;
stepVec = zCollision.newPosition;
}
return new PhysicsResult(currentPosition.samePoint(stepVec) ? currentPosition : currentPosition.withCoord(stepVec),
new Vec(xCheck ? 0 : deltaPosition.x(),
yCheck ? 0 : deltaPosition.y(),
zCheck ? 0 : deltaPosition.z()),
yCheck && deltaPosition.y() < 0);
public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity,
@Nullable PhysicsResult lastPhysicsResult) {
return BlockCollision.handlePhysics(entity, entityVelocity, lastPhysicsResult);
}
/**
* Steps on a single axis. Checks against collisions for each point of 'corners'. This method assumes that startPosition is valid.
* Immediately return false if corners is of length 0.
*
* @param instance instance to check blocks from
* @param startPosition starting position for stepping, can be intermediary position from last step
* @param axis step direction. Works best if unit vector and aligned to an axis
* @param stepAmount how much to step in the direction (in blocks)
* @param corners the corners to check against
* @return result of the step
*/
private static StepResult stepAxis(Instance instance, Chunk originChunk, Vec startPosition, Vec axis, double stepAmount, List<Vec> corners) {
final Vec[] mutableCorners = corners.toArray(Vec[]::new);
final double sign = Math.signum(stepAmount);
final int blockLength = (int) stepAmount;
final double remainingLength = stepAmount - blockLength;
// used to determine if 'remainingLength' should be used
boolean collisionFound = false;
for (int i = 0; i < Math.abs(blockLength); i++) {
collisionFound = stepOnce(instance, originChunk, axis, sign, mutableCorners);
if (collisionFound) break;
}
// add remainingLength
if (!collisionFound) {
collisionFound = stepOnce(instance, originChunk, axis, remainingLength, mutableCorners);
}
// find the corner which moved the least
double smallestDisplacement = Double.POSITIVE_INFINITY;
for (int i = 0; i < corners.size(); i++) {
final double displacement = corners.get(i).distance(mutableCorners[i]);
if (displacement < smallestDisplacement) {
smallestDisplacement = displacement;
}
}
return new StepResult(startPosition.add(new Vec(smallestDisplacement).mul(axis).mul(sign)), collisionFound);
}
/**
* Steps once (by a length of 1 block) on the given axis.
*
* @param instance instance to get blocks from
* @param axis the axis to move along
* @param corners the corners of the bounding box to consider
* @return true if found collision
*/
private static boolean stepOnce(Instance instance, Chunk originChunk, Vec axis, double amount, Vec[] corners) {
final double sign = Math.signum(amount);
for (int cornerIndex = 0; cornerIndex < corners.length; cornerIndex++) {
final Vec originalCorner = corners[cornerIndex];
final Vec newCorner = originalCorner.add(axis.mul(amount));
final Chunk chunk = ChunkUtils.retrieve(instance, originChunk, newCorner);
if (!ChunkUtils.isLoaded(chunk)) {
// Collision at chunk border
return true;
}
final Block block = chunk.getBlock(newCorner, Block.Getter.Condition.TYPE);
// TODO: block collision boxes
// TODO: for the moment, always consider a full block
if (block != null && block.isSolid()) {
corners[cornerIndex] = new Vec(
Math.abs(axis.x()) > 10e-16 ? newCorner.blockX() - axis.x() * sign : originalCorner.x(),
Math.abs(axis.y()) > 10e-16 ? newCorner.blockY() - axis.y() * sign : originalCorner.y(),
Math.abs(axis.z()) > 10e-16 ? newCorner.blockZ() - axis.z() * sign : originalCorner.z());
return true;
}
corners[cornerIndex] = newCorner;
}
return false;
public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity) {
return handlePhysics(entity, entityVelocity, null);
}
/**
@ -149,26 +51,21 @@ public class CollisionUtils {
final WorldBorder.CollisionAxis collisionAxis = worldBorder.getCollisionAxis(newPosition);
return switch (collisionAxis) {
case NONE ->
// Apply velocity + gravity
// Apply velocity + gravity
newPosition;
case BOTH ->
// Apply Y velocity/gravity
// Apply Y velocity/gravity
new Pos(currentPosition.x(), newPosition.y(), currentPosition.z());
case X ->
// Apply Y/Z velocity/gravity
// Apply Y/Z velocity/gravity
new Pos(currentPosition.x(), newPosition.y(), newPosition.z());
case Z ->
// Apply X/Y velocity/gravity
// Apply X/Y velocity/gravity
new Pos(newPosition.x(), newPosition.y(), currentPosition.z());
};
}
public record PhysicsResult(Pos newPosition,
Vec newVelocity,
boolean isOnGround) {
}
private record StepResult(Vec newPosition,
boolean foundCollision) {
public static Shape parseBlockShape(String str, Supplier<Material> block) {
return ShapeImpl.parseBlockFromRegistry(str, block);
}
}

View File

@ -0,0 +1,13 @@
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.instance.block.Block;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Experimental
public record PhysicsResult(Pos newPosition, Vec newVelocity, boolean isOnGround,
boolean collisionX, boolean collisionY, boolean collisionZ,
Vec originalDelta, Point collidedBlockY, Block blockTypeY) {
}

View File

@ -0,0 +1,360 @@
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.instance.Chunk;
import net.minestom.server.instance.Instance;
class RayUtils {
/**
* @param rayDirection Ray vector
* @param rayStart Ray start point
* @param instance entity instance
* @param originChunk entity chunk
* @param boundingBox entity bounding box
* @param entityCentre position of entity
* @param finalResult place to store final result of collision
*/
public static void RaycastCollision(Vec rayDirection, Point rayStart, Instance instance, Chunk originChunk, BoundingBox boundingBox, Pos entityCentre, SweepResult finalResult) {
// This works by finding all the x, y and z grid line intersections and calculating the value of the point at that intersection
// Finding all the intersections will give us all the full blocks that are traversed by the ray
if (rayDirection.x() != 0) {
// Which direction we're stepping the block boundary in
double xStep = rayDirection.x() < 0 ? -1 : 1;
// If we are going in the positive direction, the block that we stepped over is the one we want
int xFix = rayDirection.x() > 0 ? 1 : 0;
// Total number of axis block boundaries that will be passed
int xStepCount = (int) Math.ceil((rayDirection.x()) / xStep) + xFix;
int xStepsCompleted = xFix;
while (xStepsCompleted <= xStepCount) {
// Get the axis value
int xi = (int) (xStepsCompleted * xStep + rayStart.blockX());
double factor = (xi - rayStart.x()) / rayDirection.x();
if (Math.abs(rayDirection.x() * finalResult.res) - Math.abs(rayStart.x() - (xi)) < -2) break;
// Solve for y and z
int yi = (int) Math.floor(rayDirection.y() * factor + rayStart.y());
// If the y distance is much greater than the collision point that is currently being used, break
if (Math.abs(rayDirection.y() * finalResult.res) - Math.abs(rayStart.y() - (yi)) < -2) break;
int zi = (int) Math.floor(rayDirection.z() * factor + rayStart.z());
if (Math.abs(rayDirection.z() * finalResult.res) - Math.abs(rayStart.z() - (zi)) < -2) break;
xi -= xFix;
xStepsCompleted++;
// Check for collisions with the found block
// If a collision was found, break
if (BlockCollision.checkBoundingBox(xi, yi, zi, rayDirection, entityCentre, boundingBox, instance, originChunk, finalResult))
break;
}
}
if (rayDirection.z() != 0) {
double zStep = rayDirection.z() < 0 ? -1 : 1;
int zFix = rayDirection.z() > 0 ? 1 : 0;
int zStepsCompleted = zFix;
int zStepCount = (int) Math.ceil((rayDirection.z()) / zStep) + zFix;
while (zStepsCompleted <= zStepCount) {
int zi = (int) (zStepsCompleted * zStep + rayStart.blockZ());
double factor = (zi - rayStart.z()) / rayDirection.z();
if (Math.abs(rayDirection.z() * finalResult.res) - Math.abs(rayStart.z() - (zi)) < -2) break;
int xi = (int) Math.floor(rayDirection.x() * factor + rayStart.x());
if (Math.abs(rayDirection.x() * finalResult.res) - Math.abs(rayStart.x() - (xi)) < -2) break;
int yi = (int) Math.floor(rayDirection.y() * factor + rayStart.y());
if (Math.abs(rayDirection.y() * finalResult.res) - Math.abs(rayStart.y() - (yi)) < -2) break;
zi -= zFix;
zStepsCompleted++;
if (BlockCollision.checkBoundingBox(xi, yi, zi, rayDirection, entityCentre, boundingBox, instance, originChunk, finalResult))
break;
}
}
if (rayDirection.y() != 0) {
int yFix = rayDirection.y() > 0 ? 1 : 0;
double yStep = rayDirection.y() < 0 ? -1 : 1;
int yStepsCompleted = yFix;
int yStepCount = (int) Math.ceil((rayDirection.y()) / yStep) + yFix;
while (yStepsCompleted <= yStepCount) {
int yi = (int) (yStepsCompleted * yStep + rayStart.blockY());
double factor = (yi - rayStart.y()) / rayDirection.y();
if (Math.abs(rayDirection.y() * finalResult.res) - Math.abs(rayStart.y() - (yi)) < -2) break;
int xi = (int) Math.floor(rayDirection.x() * factor + rayStart.x());
if (Math.abs(rayDirection.x() * finalResult.res) - Math.abs(rayStart.x() - (xi)) < -2) break;
int zi = (int) Math.floor(rayDirection.z() * factor + rayStart.z());
if (Math.abs(rayDirection.z() * finalResult.res) - Math.abs(rayStart.z() - (zi)) < -2) break;
yi -= yFix;
yStepsCompleted++;
if (BlockCollision.checkBoundingBox(xi, yi, zi, rayDirection, entityCentre, boundingBox, instance, originChunk, finalResult))
break;
}
}
}
/**
* Check if a bounding box intersects a ray
*
* @param rayStart Ray start position
* @param rayDirection Ray to check
* @param collidableStatic Bounding box
* @return true if an intersection between the ray and the bounding box was found
*/
public static boolean BoundingBoxIntersectionCheck(BoundingBox moving, Point rayStart, Point rayDirection, BoundingBox collidableStatic, Point staticCollidableOffset) {
Point bbCentre = new Pos(moving.minX() + moving.width() / 2, moving.minY() + moving.height() / 2, moving.minZ() + moving.depth() / 2);
Point rayCentre = rayStart.add(bbCentre);
// Translate bounding box
Vec bbOffMin = new Vec(collidableStatic.minX() - rayCentre.x() + staticCollidableOffset.x() - moving.width() / 2, collidableStatic.minY() - rayCentre.y() + staticCollidableOffset.y() - moving.height() / 2, collidableStatic.minZ() - rayCentre.z() + staticCollidableOffset.z() - moving.depth() / 2);
Vec bbOffMax = new Vec(collidableStatic.maxX() - rayCentre.x() + staticCollidableOffset.x() + moving.width() / 2, collidableStatic.maxY() - rayCentre.y() + staticCollidableOffset.y() + moving.height() / 2, collidableStatic.maxZ() - rayCentre.z() + staticCollidableOffset.z() + moving.depth() / 2);
// This check is done in 2d. it can be visualised as a rectangle (the face we are checking), and a point.
// If the point is within the rectangle, we know the vector intersects the face.
double signumRayX = Math.signum(rayDirection.x());
double signumRayY = Math.signum(rayDirection.y());
double signumRayZ = Math.signum(rayDirection.z());
// Intersect X
if (rayDirection.x() != 0) {
// Left side of bounding box
{
double xFac = bbOffMin.x() / rayDirection.x();
double yix = rayDirection.y() * xFac + rayCentre.y();
double zix = rayDirection.z() * xFac + rayCentre.z();
// Check if ray passes through y/z plane
if (rayDirection.x() > 0
&& ((yix - rayCentre.y()) * signumRayY) >= 0
&& ((zix - rayCentre.z()) * signumRayZ) >= 0
&& yix >= collidableStatic.minY() + staticCollidableOffset.y() - moving.height() / 2
&& yix <= collidableStatic.maxY() + staticCollidableOffset.y() + moving.height() / 2
&& zix >= collidableStatic.minZ() + staticCollidableOffset.z() - moving.depth() / 2
&& zix <= collidableStatic.maxZ() + staticCollidableOffset.z() + moving.depth() / 2) {
return true;
}
}
// Right side of bounding box
{
double xFac = bbOffMax.x() / rayDirection.x();
double yix = rayDirection.y() * xFac + rayCentre.y();
double zix = rayDirection.z() * xFac + rayCentre.z();
if (rayDirection.x() < 0
&& ((yix - rayCentre.y()) * signumRayY) >= 0
&& ((zix - rayCentre.z()) * signumRayZ) >= 0
&& yix >= collidableStatic.minY() + staticCollidableOffset.y() - moving.height() / 2
&& yix <= collidableStatic.maxY() + staticCollidableOffset.y() + moving.height() / 2
&& zix >= collidableStatic.minZ() + staticCollidableOffset.z() - moving.depth() / 2
&& zix <= collidableStatic.maxZ() + staticCollidableOffset.z() + moving.depth() / 2) {
return true;
}
}
}
// Intersect Z
if (rayDirection.z() != 0) {
{
double zFac = bbOffMin.z() / rayDirection.z();
double xiz = rayDirection.x() * zFac + rayCentre.x();
double yiz = rayDirection.y() * zFac + rayCentre.y();
if (rayDirection.z() > 0
&& ((yiz - rayCentre.y()) * signumRayY) >= 0
&& ((xiz - rayCentre.x()) * signumRayX) >= 0
&& xiz >= collidableStatic.minX() + staticCollidableOffset.x() - moving.width() / 2
&& xiz <= collidableStatic.maxX() + staticCollidableOffset.x() + moving.width() / 2
&& yiz >= collidableStatic.minY() + staticCollidableOffset.y() - moving.height() / 2
&& yiz <= collidableStatic.maxY() + staticCollidableOffset.y() + moving.height() / 2) {
return true;
}
}
{
double zFac = bbOffMax.z() / rayDirection.z();
double xiz = rayDirection.x() * zFac + rayCentre.x();
double yiz = rayDirection.y() * zFac + rayCentre.y();
if (rayDirection.z() < 0
&& ((yiz - rayCentre.y()) * signumRayY) >= 0
&& ((xiz - rayCentre.x()) * signumRayX) >= 0
&& xiz >= collidableStatic.minX() + staticCollidableOffset.x() - moving.width() / 2
&& xiz <= collidableStatic.maxX() + staticCollidableOffset.x() + moving.width() / 2
&& yiz >= collidableStatic.minY() + staticCollidableOffset.y() - moving.height() / 2
&& yiz <= collidableStatic.maxY() + staticCollidableOffset.y() + moving.height() / 2) {
return true;
}
}
}
// Intersect Y
if (rayDirection.y() != 0) {
{
double yFac = bbOffMin.y() / rayDirection.y();
double xiy = rayDirection.x() * yFac + rayCentre.x();
double ziy = rayDirection.z() * yFac + rayCentre.z();
if (rayDirection.y() > 0
&& ((ziy - rayCentre.z()) * signumRayZ) >= 0
&& ((xiy - rayCentre.x()) * signumRayX) >= 0
&& xiy >= collidableStatic.minX() + staticCollidableOffset.x() - moving.width() / 2
&& xiy <= collidableStatic.maxX() + staticCollidableOffset.x() + moving.width() / 2
&& ziy >= collidableStatic.minZ() + staticCollidableOffset.z() - moving.depth() / 2
&& ziy <= collidableStatic.maxZ() + staticCollidableOffset.z() + moving.depth() / 2) {
return true;
}
}
{
double yFac = bbOffMax.y() / rayDirection.y();
double xiy = rayDirection.x() * yFac + rayCentre.x();
double ziy = rayDirection.z() * yFac + rayCentre.z();
if (rayDirection.y() < 0
&& ((ziy - rayCentre.z()) * signumRayZ) >= 0
&& ((xiy - rayCentre.x()) * signumRayX) >= 0
&& xiy >= collidableStatic.minX() + staticCollidableOffset.x() - moving.width() / 2
&& xiy <= collidableStatic.maxX() + staticCollidableOffset.x() + moving.width() / 2
&& ziy >= collidableStatic.minZ() + staticCollidableOffset.z() - moving.depth() / 2
&& ziy <= collidableStatic.maxZ() + staticCollidableOffset.z() + moving.depth() / 2) {
return true;
}
}
}
return false;
}
// Extended from 2d implementation found here https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/
public static void SweptAABB(BoundingBox collidableMoving, Point rayStart, Point rayDirection, BoundingBox collidableStatic, Point staticCollidableOffset, SweepResult writeTo) {
double normalx, normaly, normalz;
double xInvEntry, yInvEntry, zInvEntry;
double xInvExit, yInvExit, zInvExit;
// find the distance between the objects on the near and far sides for x, y, z
if (rayDirection.x() > 0.0f) {
xInvEntry = (staticCollidableOffset.x() + collidableStatic.minX()) - (rayStart.x() + collidableMoving.maxX());
xInvExit = (staticCollidableOffset.x() + collidableStatic.maxX()) - (rayStart.x() + collidableMoving.minX());
} else {
xInvEntry = (staticCollidableOffset.x() + collidableStatic.maxX()) - (rayStart.x() + collidableMoving.minX());
xInvExit = (staticCollidableOffset.x() + collidableStatic.minX()) - (rayStart.x() + collidableMoving.maxX());
}
if (rayDirection.y() > 0.0f) {
yInvEntry = (staticCollidableOffset.y() + collidableStatic.minY()) - (rayStart.y() + collidableMoving.maxY());
yInvExit = (staticCollidableOffset.y() + collidableStatic.maxY()) - (rayStart.y() + collidableMoving.minY());
} else {
yInvEntry = (staticCollidableOffset.y() + collidableStatic.maxY()) - (rayStart.y() + collidableMoving.minY());
yInvExit = (staticCollidableOffset.y() + collidableStatic.minY()) - (rayStart.y() + collidableMoving.maxY());
}
if (rayDirection.z() > 0.0f) {
zInvEntry = (staticCollidableOffset.z() + collidableStatic.minZ()) - (rayStart.z() + collidableMoving.maxZ());
zInvExit = (staticCollidableOffset.z() + collidableStatic.maxZ()) - (rayStart.z() + collidableMoving.minZ());
} else {
zInvEntry = (staticCollidableOffset.z() + collidableStatic.maxZ()) - (rayStart.z() + collidableMoving.minZ());
zInvExit = (staticCollidableOffset.z() + collidableStatic.minZ()) - (rayStart.z() + collidableMoving.maxZ());
}
// find time of collision and time of leaving for each axis (if statement is to prevent divide by zero)
double xEntry, yEntry, zEntry;
double xExit, yExit, zExit;
if (rayDirection.x() == 0.0f) {
xEntry = -Double.MAX_VALUE;
xExit = Double.MAX_VALUE;
} else {
xEntry = xInvEntry / rayDirection.x();
xExit = xInvExit / rayDirection.x();
}
if (rayDirection.y() == 0.0f) {
yEntry = -Double.MAX_VALUE;
yExit = Double.MAX_VALUE;
} else {
yEntry = yInvEntry / rayDirection.y();
yExit = yInvExit / rayDirection.y();
}
if (rayDirection.z() == 0.0f) {
zEntry = -Double.MAX_VALUE;
zExit = Double.MAX_VALUE;
} else {
zEntry = zInvEntry / rayDirection.z();
zExit = zInvExit / rayDirection.z();
}
// find the earliest/latest times of collision
double entryTime = Math.max(Math.max(xEntry, yEntry), zEntry);
double exitTime = Math.min(Math.max(xExit, yExit), zExit);
if (entryTime > exitTime || xEntry > 1.0f || yEntry > 1.0f || zEntry > 1.0f || (xEntry < 0.0f && yEntry < 0.0f && zEntry < 0.0f)) {
writeTo.res = 1;
writeTo.normalX = 0;
writeTo.normalY = 0;
writeTo.normalZ = 0;
return;
}
// calculate normal of collided surface
if (xEntry > yEntry && xEntry > zEntry) {
if (xInvEntry < 0.0f) {
normalx = 1.0f;
normaly = 0.0f;
normalz = 0.0f;
} else {
normalx = -1.0f;
normaly = 0.0f;
normalz = 0.0f;
}
} else if (yEntry > zEntry) {
if (yInvEntry < 0.0f) {
normalx = 0.0f;
normaly = 1.0f;
normalz = 0.0f;
} else {
normalx = 0.0f;
normaly = -1.0f;
normalz = 0.0f;
}
} else {
if (zInvEntry < 0.0f) {
normalx = 0.0f;
normaly = 0.0f;
normalz = 1.0f;
} else {
normalx = 0.0f;
normaly = 0.0f;
normalz = -1.0f;
}
}
writeTo.res = entryTime * 0.99999;
writeTo.normalX = normalx;
writeTo.normalY = normaly;
writeTo.normalZ = normalz;
}
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);
}
}

View File

@ -0,0 +1,44 @@
package net.minestom.server.collision;
import net.minestom.server.coordinate.Point;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ApiStatus.Experimental
public interface Shape {
/**
* Checks if two bounding boxes intersect.
*
* @param positionRelative Relative position of bounding box to check with
* @param boundingBox Bounding box to check for intersections with
* @return is an intersection found
*/
boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox);
/**
* Checks if a moving bounding box will hit this shape.
*
* @param rayStart Position of the moving shape
* @param rayDirection Movement vector
* @param shapePos Position of this shape
* @param moving Bounding Box of moving shape
* @param finalResult Stores final SweepResult
* @return is an intersection found
*/
boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection,
@NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult);
/**
* Relative Start
*
* @return Start of shape
*/
@NotNull Point relativeStart();
/**
* Relative End
*
* @return End of shape
*/
@NotNull Point relativeEnd();
}

View File

@ -0,0 +1,114 @@
package net.minestom.server.collision;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class ShapeImpl implements Shape {
private final List<BoundingBox> blockSections;
private final Supplier<Material> block;
ShapeImpl(List<BoundingBox> boundingBoxes, Supplier<Material> block) {
this.blockSections = boundingBoxes;
this.block = block;
}
static ShapeImpl parseBlockFromRegistry(String str, Supplier<Material> block) {
final String regex = "\\d.\\d{1,3}";
final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
final Matcher matcher = pattern.matcher(str);
ArrayList<Double> vals = new ArrayList<>();
while (matcher.find()) {
double newVal = Double.parseDouble(matcher.group());
vals.add(newVal);
}
List<BoundingBox> boundingBoxes = new ArrayList<>();
final int count = vals.size() / 6;
for (int i = 0; i < count; ++i) {
final double boundXSize = vals.get(3 + 6 * i) - vals.get(0 + 6 * i);
final double boundYSize = vals.get(4 + 6 * i) - vals.get(1 + 6 * i);
final double boundZSize = vals.get(5 + 6 * i) - vals.get(2 + 6 * i);
final double minX, minY, minZ;
minX = vals.get(0 + 6 * i);
minY = vals.get(1 + 6 * i);
minZ = vals.get(2 + 6 * i);
final BoundingBox bb = new BoundingBox(boundXSize, boundYSize, boundZSize, new Vec(minX, minY, minZ));
assert bb.minX() == minX;
assert bb.minY() == minY;
assert bb.minZ() == minZ;
boundingBoxes.add(bb);
}
return new ShapeImpl(boundingBoxes, block);
}
@Override
public @NotNull Point relativeStart() {
double minX = 1, minY = 1, minZ = 1;
for (BoundingBox blockSection : blockSections) {
if (blockSection.minX() < minX) minX = blockSection.minX();
if (blockSection.minY() < minY) minY = blockSection.minY();
if (blockSection.minZ() < minZ) minZ = blockSection.minZ();
}
return new Vec(minX, minY, minZ);
}
@Override
public @NotNull Point relativeEnd() {
double maxX = 1, maxY = 1, maxZ = 1;
for (BoundingBox blockSection : blockSections) {
if (blockSection.maxX() < maxX) maxX = blockSection.maxX();
if (blockSection.maxY() < maxY) maxY = blockSection.maxY();
if (blockSection.maxZ() < maxZ) maxZ = blockSection.maxZ();
}
return new Vec(maxX, maxY, maxZ);
}
@Override
public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) {
return blockSections.stream().anyMatch(section -> boundingBox.intersectBox(position, section));
}
@Override
public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) {
List<BoundingBox> collidables = blockSections.stream().filter(blockSection -> {
// Fast check to see if a collision happens
// Uses minkowski sum
return RayUtils.BoundingBoxIntersectionCheck(
moving, rayStart, rayDirection,
blockSection,
shapePos
);
}).toList();
boolean hitBlock = false;
SweepResult tempResult = new SweepResult(1, 0, 0, 0, null);
for (BoundingBox bb : collidables) {
// Longer check to get result of collision
RayUtils.SweptAABB(moving, rayStart, rayDirection, bb, shapePos, tempResult);
// Update final result if the temp result collision is sooner than the current final result
if (tempResult.res < finalResult.res) {
finalResult.res = tempResult.res;
finalResult.normalX = tempResult.normalX;
finalResult.normalY = tempResult.normalY;
finalResult.normalZ = tempResult.normalZ;
finalResult.collidedShapePosition = shapePos;
finalResult.collidedShape = this;
finalResult.blockType = block.get().block();
}
hitBlock = true;
}
return hitBlock;
}
}

View File

@ -0,0 +1,28 @@
package net.minestom.server.collision;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
final class SweepResult {
double res;
double normalX, normalY, normalZ;
Point collidedShapePosition;
Block blockType;
Shape collidedShape;
/**
* Store the result of a movement operation
*
* @param res Percentage of move completed
* @param normalX -1 if intersected on left, 1 if intersected on right
* @param normalY -1 if intersected on bottom, 1 if intersected on top
* @param normalZ -1 if intersected on front, 1 if intersected on back
*/
public SweepResult(double res, double normalX, double normalY, double normalZ, Shape collidedShape) {
this.res = res;
this.normalX = normalX;
this.normalY = normalY;
this.normalZ = normalZ;
this.collidedShape = collidedShape;
}
}

View File

@ -11,6 +11,7 @@ import net.minestom.server.Tickable;
import net.minestom.server.Viewable;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.collision.PhysicsResult;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
@ -98,6 +99,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
protected boolean onGround;
private BoundingBox boundingBox;
private PhysicsResult lastPhysicsResult = null;
protected Entity vehicle;
@ -181,7 +183,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
this.previousPosition = Pos.ZERO;
this.lastSyncedPosition = Pos.ZERO;
setBoundingBox(entityType.width(), entityType.height(), entityType.width());
setBoundingBox(entityType.registry().boundingBox());
this.entityMeta = EntityTypeImpl.createMeta(entityType, this, this.metadata);
@ -483,7 +485,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
* Works by changing the internal entity type field and by calling {@link #removeViewer(Player)}
* followed by {@link #addViewer(Player)} to all current viewers.
* <p>
* Be aware that this only change the visual of the entity, the {@link net.minestom.server.collision.BoundingBox}
* Be aware that this only change the visual of the entity, the {@link BoundingBox}
* will not be modified.
*
* @param entityType the new entity type
@ -566,7 +568,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
final Pos newPosition;
final Vec newVelocity;
if (this.hasPhysics) {
final var physicsResult = CollisionUtils.handlePhysics(this, deltaPos);
final var physicsResult = CollisionUtils.handlePhysics(this, deltaPos, lastPhysicsResult);
this.lastPhysicsResult = physicsResult;
this.onGround = physicsResult.isOnGround();
newPosition = physicsResult.newPosition();
newVelocity = physicsResult.newVelocity();
@ -624,12 +627,14 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
private void touchTick() {
// TODO do not call every tick (it is pretty expensive)
final int minX = (int) Math.floor(boundingBox.getMinX());
final int maxX = (int) Math.ceil(boundingBox.getMaxX());
final int minY = (int) Math.floor(boundingBox.getMinY());
final int maxY = (int) Math.ceil(boundingBox.getMaxY());
final int minZ = (int) Math.floor(boundingBox.getMinZ());
final int maxZ = (int) Math.ceil(boundingBox.getMaxZ());
final Pos position = this.position;
final int minX = (int) Math.floor(boundingBox.minX() + position.x());
final int maxX = (int) Math.ceil(boundingBox.maxX() + position.x());
final int minY = (int) Math.floor(boundingBox.minY() + position.y());
final int maxY = (int) Math.ceil(boundingBox.maxY() + position.y());
final int minZ = (int) Math.floor(boundingBox.minZ() + position.z());
final int maxZ = (int) Math.ceil(boundingBox.maxZ() + position.z());
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
@ -641,8 +646,10 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
continue;
final BlockHandler handler = block.handler();
if (handler != null) {
// checks that we are actually in the block, and not just here because of a rounding error
if (boundingBox.intersectWithBlock(x, y, z)) {
final double triggerDelta = 0.01;
// Move a small amount towards the entity. If the entity is within 0.01 blocks of the block, touch will trigger
Point blockPos = new Pos(x * (1 - triggerDelta), y * (1 - triggerDelta), z * (1 - triggerDelta));
if (block.registry().collisionShape().intersectBox(position.sub(blockPos), boundingBox)) {
// TODO: replace with check with custom block bounding box
handler.onTouch(new BlockHandler.Touch(block, instance, new Vec(x, y, z), this));
}
@ -750,12 +757,12 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
* <p>
* WARNING: this does not change the entity hit-box which is client-side.
*
* @param x the bounding box X size
* @param y the bounding box Y size
* @param z the bounding box Z size
* @param width the bounding box X size
* @param height the bounding box Y size
* @param depth the bounding box Z size
*/
public void setBoundingBox(double x, double y, double z) {
this.boundingBox = new BoundingBox(this, x, y, z);
public void setBoundingBox(double width, double height, double depth) {
this.boundingBox = new BoundingBox(width, height, depth);
}
/**
@ -1352,12 +1359,12 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
/**
* Gets the entity eye height.
* <p>
* Default to {@link BoundingBox#getHeight()}x0.85
* Default to {@link BoundingBox#height()}x0.85
*
* @return the entity eye height
*/
public double getEyeHeight() {
return boundingBox.getHeight() * 0.85;
return boundingBox.height() * 0.85;
}
/**
@ -1583,12 +1590,11 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
}
}
/**
* 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
* @return A list of {@link Point points} in this entities line of sight
*/
public List<Point> getLineOfSight(int maxDistance) {
Instance instance = getInstance();
@ -1619,19 +1625,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
return false;
}
final Vec start = getPosition().asVec().add(0D, getEyeHeight(), 0D);
final Vec end = entity.getPosition().asVec().add(0D, getEyeHeight(), 0D);
final Vec direction = end.sub(start);
final int maxDistance = (int) Math.ceil(direction.length());
var it = new BlockIterator(start, direction.normalize(), 0D, maxDistance);
while (it.hasNext()) {
Block block = instance.getBlock(it.next());
if (!block.isAir() && !block.isLiquid()) {
return false;
}
}
return true;
final Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z());
return entity.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), entity.getPosition());
}
/**
@ -1647,43 +1642,15 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
return null;
}
Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z());
Vec end = start.add(position.direction().mul(range));
final Vec start = new Vec(position.x(), position.y() + getEyeHeight(), position.z());
List<Entity> nearby = instance.getNearbyEntities(position, range).stream()
.filter(e -> e != this && e.boundingBox.intersect(start, end) && predicate.test(e)).toList();
if (nearby.isEmpty()) {
return null;
}
Optional<Entity> nearby = instance.getNearbyEntities(position, range).stream()
.filter(e -> e != this
&& e.boundingBox.boundingBoxRayIntersectionCheck(start, position.direction(), e.getPosition())
&& predicate.test(e))
.min(Comparator.comparingDouble(e -> e.getDistance(this.position)));
Vec direction = end.sub(start);
int maxDistance = (int) Math.ceil(direction.length());
double maxVisibleDistanceSquared = direction.lengthSquared();
var iterator = new BlockIterator(start, direction.normalize(), 0D, maxDistance);
while (iterator.hasNext()) {
Point 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 (Entity entity : nearby) {
double distanceSquared = entity.getDistanceSquared(this);
if (result == null || minDistanceSquared > distanceSquared) {
result = entity;
minDistanceSquared = distanceSquared;
}
}
if (minDistanceSquared < maxVisibleDistanceSquared) {
return result;
} else {
return null;
}
return nearby.orElse(null);
}
public enum Pose {

View File

@ -172,9 +172,8 @@ public class EntityProjectile extends Entity {
if (getAliveTicks() < 3) {
continue;
}
final Pos finalPos = pos;
Optional<Entity> victimOptional = entities.stream()
.filter(entity -> entity.getBoundingBox().intersect(finalPos))
.filter(entity -> getBoundingBox().intersectEntity(getPosition(), entity))
.findAny();
if (victimOptional.isPresent()) {
LivingEntity victim = (LivingEntity) victimOptional.get();

View File

@ -203,12 +203,11 @@ public class LivingEntity extends Entity implements EquipmentHandler {
// Items picking
if (canPickupItem() && itemPickupCooldown.isReady(time)) {
itemPickupCooldown.refreshLastUpdate(time);
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(),
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.width(),
EntityTracker.Target.ITEMS, itemEntity -> {
if (this instanceof Player player && !itemEntity.isViewer(player)) return;
if (!itemEntity.isPickable()) return;
final BoundingBox itemBoundingBox = itemEntity.getBoundingBox();
if (expandedBoundingBox.intersect(itemBoundingBox)) {
if (expandedBoundingBox.intersectEntity(position, itemEntity)) {
PickupItemEvent pickupItemEvent = new PickupItemEvent(this, itemEntity);
EventDispatcher.callCancellable(pickupItemEvent, () -> {
final ItemStack item = itemEntity.getItemStack();

View File

@ -20,7 +20,6 @@ import net.minestom.server.adventure.AdventurePacketConvertor;
import net.minestom.server.adventure.Localizable;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.command.CommandManager;
import net.minestom.server.command.CommandSender;
import net.minestom.server.coordinate.Point;
@ -326,10 +325,9 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Experience orb pickup
if (experiencePickupCooldown.isReady(time)) {
experiencePickupCooldown.refreshLastUpdate(time);
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(),
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.width(),
EntityTracker.Target.EXPERIENCE_ORBS, experienceOrb -> {
final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox();
if (expandedBoundingBox.intersect(itemBoundingBox)) {
if (expandedBoundingBox.intersectEntity(position, experienceOrb)) {
PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(this, experienceOrb);
EventDispatcher.callCancellable(pickupExperienceEvent, () -> {
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player

View File

@ -24,11 +24,11 @@ public class AgeableMobMeta extends PathfinderMobMeta {
this.consumeEntity((entity) -> {
BoundingBox bb = entity.getBoundingBox();
if (value) {
double width = bb.getWidth() / 2;
entity.setBoundingBox(width, bb.getHeight() / 2, width);
double width = bb.width() / 2;
entity.setBoundingBox(width, bb.height() / 2, width);
} else {
double width = bb.getWidth() * 2;
entity.setBoundingBox(width, bb.getHeight() * 2, width);
double width = bb.width() * 2;
entity.setBoundingBox(width, bb.height() * 2, width);
}
});
super.metadata.setIndex(OFFSET, Metadata.Boolean(value));

View File

@ -24,11 +24,11 @@ public class PiglinMeta extends BasePiglinMeta {
this.consumeEntity((entity) -> {
BoundingBox bb = entity.getBoundingBox();
if (value) {
double width = bb.getWidth() / 2;
entity.setBoundingBox(width, bb.getHeight() / 2, width);
double width = bb.width() / 2;
entity.setBoundingBox(width, bb.height() / 2, width);
} else {
double width = bb.getWidth() * 2;
entity.setBoundingBox(width, bb.getHeight() * 2, width);
double width = bb.width() * 2;
entity.setBoundingBox(width, bb.height() * 2, width);
}
});
super.metadata.setIndex(OFFSET, Metadata.Boolean(value));

View File

@ -24,11 +24,11 @@ public class ZoglinMeta extends MonsterMeta {
this.consumeEntity((entity) -> {
BoundingBox bb = entity.getBoundingBox();
if (value) {
double width = bb.getWidth() / 2;
entity.setBoundingBox(width, bb.getHeight() / 2, width);
double width = bb.width() / 2;
entity.setBoundingBox(width, bb.height() / 2, width);
} else {
double width = bb.getWidth() * 2;
entity.setBoundingBox(width, bb.getHeight() * 2, width);
double width = bb.width() * 2;
entity.setBoundingBox(width, bb.height() * 2, width);
}
});
super.metadata.setIndex(OFFSET, Metadata.Boolean(value));

View File

@ -25,11 +25,11 @@ public class ZombieMeta extends MonsterMeta {
this.consumeEntity((entity) -> {
BoundingBox bb = entity.getBoundingBox();
if (value) {
double width = bb.getWidth() / 2;
entity.setBoundingBox(width, bb.getHeight() / 2, width);
double width = bb.width() / 2;
entity.setBoundingBox(width, bb.height() / 2, width);
} else {
double width = bb.getWidth() * 2;
entity.setBoundingBox(width, bb.getHeight() * 2, width);
double width = bb.width() * 2;
entity.setBoundingBox(width, bb.height() * 2, width);
}
});
super.metadata.setIndex(OFFSET, Metadata.Boolean(value));

View File

@ -1,6 +1,5 @@
package net.minestom.server.entity.metadata.other;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Metadata;
import net.minestom.server.entity.metadata.MobMeta;

View File

@ -1,6 +1,5 @@
package net.minestom.server.entity.metadata.water.fish;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Metadata;
import org.jetbrains.annotations.NotNull;

View File

@ -206,12 +206,12 @@ public final class PFPathingEntity implements IPathingEntity {
@Override
public float width() {
return (float) entity.getBoundingBox().getWidth();
return (float) entity.getBoundingBox().width();
}
@Override
public float height() {
return (float) entity.getBoundingBox().getHeight();
return (float) entity.getBoundingBox().height();
}
private float getAttributeValue(@NotNull Attribute attribute) {

View File

@ -26,7 +26,7 @@ import net.minestom.server.network.packet.server.play.BlockChangePacket;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.validate.Check;
import java.util.Set;
import java.util.Collection;
public class BlockPlacementListener {
private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager();
@ -109,26 +109,24 @@ public class BlockPlacementListener {
}
final Block placedBlock = useMaterial.block();
final Set<Entity> entities = instance.getChunkEntities(chunk);
final Collection<Entity> entities = instance.getNearbyEntities(placementPosition, 5);
// Check if the player is trying to place a block in an entity
boolean intersect = player.getBoundingBox().intersectWithBlock(placementPosition);
if (!intersect && placedBlock.isSolid()) {
// TODO push entities too close to the position
for (Entity entity : entities) {
// 'player' has already been checked
if (entity == player ||
entity.getEntityType() == EntityType.ITEM)
continue;
// Marker Armor Stands should not prevent block placement
if (entity.getEntityMeta() instanceof ArmorStandMeta armorStandMeta) {
if (armorStandMeta.isMarker()) continue;
}
intersect = entity.getBoundingBox().intersectWithBlock(placementPosition);
if (intersect)
break;
}
}
if (intersect) {
boolean intersectPlayer = placedBlock.registry().collisionShape().intersectBox(player.getPosition().sub(placementPosition), player.getBoundingBox());
boolean hasIntersect = intersectPlayer || entities
.stream()
.filter(entity -> entity.getEntityType() != EntityType.ITEM)
.filter(entity -> {
// Marker Armor Stands should not prevent block placement
if (entity.getEntityMeta() instanceof ArmorStandMeta armorStandMeta) {
return !armorStandMeta.isMarker();
}
return true;
})
.anyMatch(entity -> placedBlock.registry().collisionShape().intersectBox(entity.getPosition().sub(placementPosition), entity.getBoundingBox()));
if (hasIntersect) {
refresh(player, chunk);
return;
}

View File

@ -3,6 +3,9 @@ package net.minestom.server.registry;
import com.google.gson.ToNumberPolicy;
import com.google.gson.stream.JsonReader;
import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.collision.Shape;
import net.minestom.server.entity.EntitySpawnType;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.instance.block.Block;
@ -164,6 +167,7 @@ public final class Registry {
private final String blockEntity;
private final int blockEntityId;
private final Supplier<Material> materialSupplier;
private final Shape shape;
private final Properties custom;
private BlockEntry(String namespace, Properties main, Properties custom) {
@ -194,6 +198,10 @@ public final class Registry {
final String materialNamespace = main.getString("correspondingItem", null);
this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null;
}
{
final String string = main.getString("collisionShape");
this.shape = CollisionUtils.parseBlockShape(string, this.materialSupplier);
}
}
public @NotNull NamespaceID namespace() {
@ -260,6 +268,10 @@ public final class Registry {
return materialSupplier.get();
}
public Shape collisionShape() {
return shape;
}
@Override
public Properties custom() {
return custom;
@ -289,7 +301,6 @@ public final class Registry {
final String blockNamespace = main.getString("correspondingBlock", null);
this.blockSupplier = blockNamespace != null ? () -> Block.fromNamespaceId(blockNamespace) : () -> null;
}
{
final Properties armorProperties = main.section("armorProperties");
if (armorProperties != null) {
@ -353,6 +364,7 @@ public final class Registry {
double width, double height,
double drag, double acceleration,
EntitySpawnType spawnType,
BoundingBox boundingBox,
Properties custom) implements Entry {
public EntityEntry(String namespace, Properties main, Properties custom) {
this(NamespaceID.from(namespace),
@ -363,7 +375,12 @@ public final class Registry {
main.getDouble("drag", 0.02),
main.getDouble("acceleration", 0.08),
EntitySpawnType.valueOf(main.getString("packetType").toUpperCase(Locale.ROOT)),
custom);
new BoundingBox(
main.getDouble("width"),
main.getDouble("height"),
main.getDouble("width")),
custom
);
}
}

View File

@ -0,0 +1,536 @@
package net.minestom.server.collision;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
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.entity.EntityType;
import net.minestom.server.entity.metadata.other.SlimeMeta;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class EntityBlockPhysicsIntegrationTest {
private static final Point PRECISION = new Pos(0.01, 0.01, 0.01);
private static boolean checkPoints(Point expected, Point actual) {
Point diff = expected.sub(actual);
return (PRECISION.x() > Math.abs(diff.x()))
&& (PRECISION.y() > Math.abs(diff.y()))
&& (PRECISION.z() > Math.abs(diff.z()));
}
private static void assertEqualsPoint(Point expected, Point actual) {
assertEquals(expected.x(), actual.x(), PRECISION.x());
assertEquals(expected.y(), actual.y(), PRECISION.y());
assertEquals(expected.z(), actual.z(), PRECISION.z());
}
@Test
public void entityPhysicsCheckCollision(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 43, 1, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 10));
assertEqualsPoint(new Pos(0, 42, 0.7), res.newPosition());
}
@Test
public void entityPhysicsCheckSlab(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 42, 0, Block.STONE_SLAB);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 44, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -10, 0));
assertEqualsPoint(new Pos(0, 42.5, 0), res.newPosition());
}
@Test
public void entityPhysicsCheckDiagonal(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(1, 43, 1, Block.STONE);
instance.setBlock(1, 43, 2, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(10, 0, 10));
boolean isFirst = checkPoints(new Pos(10, 42, 0.7), res.newPosition());
boolean isSecond = checkPoints(new Pos(0.7, 42, 10), res.newPosition());
// First and second are both valid, it depends on the implementation
// If x collision is checked first then isFirst will be true
// If z collision is checked first then isSecond will be true
assertTrue(isFirst || isSecond);
}
@Test
public void entityPhysicsCheckDirectSlide(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(1, 43, 1, Block.STONE);
instance.setBlock(1, 43, 2, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.69, 42, 0.69)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(10, 0, 11));
assertEqualsPoint(new Pos(0.7, 42, 11.69), res.newPosition());
}
@Test
public void entityPhysicsCheckCorner(Env env) {
var instance = env.createFlatInstance();
for (int i = -2; i <= 2; ++i)
for (int j = -2; j <= 2; ++j)
instance.loadChunk(i, j).join();
var entity = new Entity(EntityType.ZOMBIE);
instance.setBlock(5, 43, -5, Block.STONE);
entity.setInstance(instance, new Pos(-0.3, 42, -0.3)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(10, 0, -10));
assertEqualsPoint(new Pos(4.7, 42, -10.3), res.newPosition());
}
@Test
public void entityPhysicsCheckEnclosedHit(Env env) {
var instance = env.createFlatInstance();
for (int i = -2; i <= 2; ++i)
for (int j = -2; j <= 2; ++j)
instance.loadChunk(i, j).join();
instance.setBlock(8, 42, 8, Block.STONE);
var entity = new Entity(EntityType.SLIME);
SlimeMeta meta = (SlimeMeta) entity.getEntityMeta();
meta.setSize(20);
entity.setInstance(instance, new Pos(5, 50, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -20, 0));
assertEqualsPoint(new Pos(5, 43, 5), res.newPosition());
}
@Test
public void entityPhysicsCheckEnclosedHitSubBlock(Env env) {
var instance = env.createFlatInstance();
for (int i = -2; i <= 2; ++i)
for (int j = -2; j <= 2; ++j)
instance.loadChunk(i, j).join();
instance.setBlock(8, 42, 8, Block.LANTERN);
var entity = new Entity(EntityType.SLIME);
SlimeMeta meta = (SlimeMeta) entity.getEntityMeta();
meta.setSize(20);
entity.setInstance(instance, new Pos(5, 42.8, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -0.4, 0));
assertEqualsPoint(new Pos(5, 42.56, 5), res.newPosition());
}
@Test
public void entityPhysicsCheckEnclosedMiss(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(11, 43, 11, Block.STONE);
var entity = new Entity(EntityType.SLIME);
SlimeMeta meta = (SlimeMeta) entity.getEntityMeta();
meta.setSize(5);
entity.setInstance(instance, new Pos(5, 44, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -2, 0));
assertEqualsPoint(new Pos(5, 42, 5), res.newPosition());
}
@Test
public void entityPhysicsCheckEntityHit(Env env) {
Point z1 = new Pos(0, 0, 0);
Point z2 = new Pos(15, 0, 0);
Point z3 = new Pos(11, 0, 0);
Point movement = new Pos(20, 1, 0);
BoundingBox bb = new Entity(EntityType.ZOMBIE).getBoundingBox();
SweepResult sweepResultFinal = new SweepResult(1, 0, 0, 0, null);
bb.intersectBoxSwept(z1, movement, z2, bb, sweepResultFinal);
bb.intersectBoxSwept(z1, movement, z3, bb, sweepResultFinal);
assertEquals(new Pos(11, 0, 0), sweepResultFinal.collidedShapePosition);
assertEquals(sweepResultFinal.collidedShape, bb);
}
@Test
public void entityPhysicsCheckEdgeClip(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(1, 43, 1, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0.7)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(10, 0, 0));
assertEqualsPoint(new Pos(0.7, 42, 0.7), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockNorth(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR.withProperties(Map.of("facing", "north", "open", "true"));
instance.setBlock(0, 42, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.5, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 0.4));
assertEqualsPoint(new Pos(0.5, 42.5, 0.512), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockSouth(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR.withProperties(Map.of("facing", "south", "open", "true"));
instance.setBlock(0, 42, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.5, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -0.4));
assertEqualsPoint(new Pos(0.5, 42.5, 0.487), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockWest(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR.withProperties(Map.of("facing", "west", "open", "true"));
instance.setBlock(0, 42, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.5, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.6, 0, 0));
assertEqualsPoint(new Pos(0.512, 42.5, 0.5), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockEast(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR.withProperties(Map.of("facing", "east", "open", "true"));
instance.setBlock(0, 42, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.5, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(-0.6, 0, 0));
assertEqualsPoint(new Pos(0.487, 42.5, 0.5), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockUp(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR.withProperties(Map.of("half", "top"));
instance.setBlock(0, 44, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.7, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0.4, 0));
assertEqualsPoint(new Pos(0.5, 42.862, 0.5), res.newPosition());
}
@Test
public void entityPhysicsCheckDoorSubBlockDown(Env env) {
var instance = env.createFlatInstance();
Block b = Block.ACACIA_TRAPDOOR;
instance.setBlock(0, 42, 0, b);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42.2, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -0.4, 0));
assertEqualsPoint(new Pos(0.5, 42.187, 0.5), res.newPosition());
}
@Test
public void entityPhysicsCheckTouchTick(Env env) {
var instance = env.createFlatInstance();
Set<Point> positions = new HashSet<>();
var handler = new BlockHandler() {
@Override
public void onTouch(@NotNull Touch touch) {
assertTrue(positions.add(touch.getBlockPosition()));
}
@Override
public @NotNull NamespaceID getNamespaceId() {
return NamespaceID.from("minestom:test");
}
};
instance.setBlock(0, 42, 0, Block.STONE.withHandler(handler));
instance.setBlock(0, 42, 1, Block.STONE.withHandler(handler));
instance.setBlock(0, 43, 1, Block.STONE.withHandler(handler));
instance.setBlock(0, 43, -1, Block.STONE.withHandler(handler));
instance.setBlock(1, 42, 1, Block.STONE.withHandler(handler));
instance.setBlock(1, 42, 0, Block.STONE.withHandler(handler));
instance.setBlock(0, 42, 10, Block.STONE.withHandler(handler));
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0.7)).join();
entity.tick(0);
assertEquals(positions, Set.of(
new Vec(0, 42, 0),
new Vec(0, 42, 1),
new Vec(0, 43, 1)));
assertEquals(instance, entity.getInstance());
}
@Test
public void entityPhysicsCheckOnGround(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 40, 0, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 50, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -20, 0));
assertTrue(res.isOnGround());
}
@Test
public void entityPhysicsCheckStairTop(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 42, 0, Block.ACACIA_STAIRS);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.4, 42.5, 0.9)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -1.2));
assertEqualsPoint(new Pos(0.4, 42.5, 0.8), res.newPosition());
}
@Test
public void entityPhysicsCheckStairTopSmall(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 42, 0, Block.ACACIA_STAIRS);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.4, 42.5, 0.9)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -0.2));
assertEqualsPoint(new Pos(0.4, 42.5, 0.8), res.newPosition());
}
@Test
public void entityPhysicsCheckNotOnGround(Env env) {
var instance = env.createFlatInstance();
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 50, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, -1, 0));
assertFalse(res.isOnGround());
}
@Test
public void entityPhysicsCheckNotOnGroundHitUp(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 60, 0, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 50, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 20, 0));
assertFalse(res.isOnGround());
}
@Test
public void entityPhysicsCheckSlide(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(1, 43, 1, Block.STONE);
instance.setBlock(1, 43, 2, Block.STONE);
instance.setBlock(1, 43, 3, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(11, 0, 10));
assertEqualsPoint(new Pos(11, 42, 0.7), res.newPosition());
}
@Test
public void entityPhysicsSmallMoveCollide(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(1, 43, 1, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.6, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.3, 0, 0));
assertEqualsPoint(new Pos(0.7, 42, 0), res.newPosition());
}
@Test
public void entityPhysicsCheckNoCollision(Env env) {
var instance = env.createFlatInstance();
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 10));
assertEqualsPoint(new Pos(0, 42, 10), res.newPosition());
}
@Test
public void entityPhysicsCheckBlockMiss(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 43, 2, Block.STONE);
instance.setBlock(2, 43, 0, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(10, 0, 10));
assertEqualsPoint(new Pos(10, 42, 10), res.newPosition());
}
@Test
public void entityPhysicsCheckBlockDirections(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 43, 1, Block.STONE);
instance.setBlock(1, 43, 0, Block.STONE);
instance.setBlock(0, 43, -1, Block.STONE);
instance.setBlock(-1, 43, 0, Block.STONE);
instance.setBlock(0, 41, 0, Block.STONE);
instance.setBlock(0, 44, 0, Block.STONE);
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(0.5, 42, 0.5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult px = CollisionUtils.handlePhysics(entity, new Vec(10, 0, 0));
PhysicsResult py = CollisionUtils.handlePhysics(entity, new Vec(0, 10, 0));
PhysicsResult pz = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 10));
PhysicsResult nx = CollisionUtils.handlePhysics(entity, new Vec(-10, 0, 0));
PhysicsResult ny = CollisionUtils.handlePhysics(entity, new Vec(0, -10, 0));
PhysicsResult nz = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -10));
assertEqualsPoint(new Pos(0.7, 42, 0.5), px.newPosition());
assertEqualsPoint(new Pos(0.5, 42.04, 0.5), py.newPosition());
assertEqualsPoint(new Pos(0.5, 42, 0.7), pz.newPosition());
assertEqualsPoint(new Pos(0.3, 42, 0.5), nx.newPosition());
assertEqualsPoint(new Pos(0.5, 42, 0.5), ny.newPosition());
assertEqualsPoint(new Pos(0.5, 42, 0.3), nz.newPosition());
}
@Test
public void entityPhysicsCheckLargeVelocityMiss(Env env) {
var instance = env.createFlatInstance();
var entity = new Entity(EntityType.ZOMBIE);
final int distance = 20;
for (int x = 0; x < distance; ++x) instance.loadChunk(x, 0).join();
entity.setInstance(instance, new Pos(5, 42, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec((distance - 1) * 16, 0, 0));
assertEqualsPoint(new Pos((distance - 1) * 16 + 5, 42, 5), res.newPosition());
}
@Test
public void entityPhysicsCheckLargeVelocityHit(Env env) {
var instance = env.createFlatInstance();
var entity = new Entity(EntityType.ZOMBIE);
final int distance = 20;
for (int x = 0; x < distance; ++x) instance.loadChunk(x, 0).join();
instance.setBlock(distance * 8, 43, 5, Block.STONE);
entity.setInstance(instance, new Pos(5, 42, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec((distance - 1) * 16, 0, 0));
assertEqualsPoint(new Pos(distance * 8 - 0.3, 42, 5), res.newPosition());
}
@Test
public void entityPhysicsCheckNoMove(Env env) {
var instance = env.createFlatInstance();
var entity = new Entity(EntityType.ZOMBIE);
entity.setInstance(instance, new Pos(5, 42, 5)).join();
assertEquals(instance, entity.getInstance());
PhysicsResult res = CollisionUtils.handlePhysics(entity, Vec.ZERO);
assertEqualsPoint(new Pos(5, 42, 5), res.newPosition());
}
}

View File

@ -0,0 +1,105 @@
package net.minestom.server.entity;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Pos;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@EnvTest
public class EntityLineOfSightIntegrationTest {
@Test
public void entityPhysicsCheckLineOfSight(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(10, 42, 0)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true);
assertEquals(res, entity2);
}
@Test
public void entityPhysicsCheckLineOfSightBehind(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(-10, 42, 0)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true);
assertNull(res);
}
@Test
public void entityPhysicsCheckLineOfSightNearMiss(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(10, 42, 0.31)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true);
assertNull(res);
}
@Test
public void entityPhysicsCheckLineOfSightNearHit(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(10, 42, 0.3)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true);
assertEquals(res, entity2);
}
@Test
public void entityPhysicsCheckLineOfSightCorrectOrder(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(10, 42, 0)).join();
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);
}
@Test
public void entityPhysicsCheckLineOfSightBigMiss(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(10, 42, 10)).join();
Entity res = entity.getLineOfSightEntity(20, (e) -> true);
assertNull(res);
}
}

View File

@ -1,5 +1,7 @@
package net.minestom.server.instance;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.Tag;
import org.jglrxavpok.hephaistos.nbt.NBT;
@ -74,4 +76,13 @@ public class BlockTest {
assertThrows(Exception.class, () -> block.properties().put("facing", "north"));
assertThrows(Exception.class, () -> block.withProperty("facing", "north").properties().put("facing", "south"));
}
@Test
public void testShape() {
Point start = Block.LANTERN.registry().collisionShape().relativeStart();
Point end = Block.LANTERN.registry().collisionShape().relativeEnd();
assertEquals(start, new Vec(0.312, 0, 0.312));
assertEquals(end, new Vec(0.625, 0.437, 0.625));
}
}