mirror of
https://github.com/Minestom/Minestom.git
synced 2025-02-23 15:51:37 +01:00
Block Collision Physics (#730)
This commit is contained in:
parent
9bca6ee0e3
commit
6891a530f5
261
src/main/java/net/minestom/server/collision/BlockCollision.java
Normal file
261
src/main/java/net/minestom/server/collision/BlockCollision.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
360
src/main/java/net/minestom/server/collision/RayUtils.java
Normal file
360
src/main/java/net/minestom/server/collision/RayUtils.java
Normal 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);
|
||||
}
|
||||
}
|
44
src/main/java/net/minestom/server/collision/Shape.java
Normal file
44
src/main/java/net/minestom/server/collision/Shape.java
Normal 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();
|
||||
}
|
114
src/main/java/net/minestom/server/collision/ShapeImpl.java
Normal file
114
src/main/java/net/minestom/server/collision/ShapeImpl.java
Normal 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;
|
||||
}
|
||||
}
|
28
src/main/java/net/minestom/server/collision/SweepResult.java
Normal file
28
src/main/java/net/minestom/server/collision/SweepResult.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user