Merge pull request #356 from Kebab11noel/fix/update-collision

Collision update
This commit is contained in:
TheMode 2021-07-07 01:25:54 +02:00 committed by GitHub
commit b00131c525
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 173 additions and 179 deletions

View File

@ -2,8 +2,8 @@ package net.minestom.server.collision;
import net.minestom.server.entity.Entity;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.coordinate.Point;
import net.minestom.server.utils.coordinate.Vec;
import org.jetbrains.annotations.NotNull;
/**
@ -199,92 +199,92 @@ public class BoundingBox {
}
/**
* Gets an array of {@link Vector} representing the points at the bottom of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getBottomFace() {
return new Vector[]{
new Vector(getMinX(), getMinY(), getMinZ()),
new Vector(getMaxX(), getMinY(), getMinZ()),
new Vector(getMaxX(), getMinY(), getMaxZ()),
new Vector(getMinX(), getMinY(), getMaxZ()),
public Vec[] getBottomFace() {
return new Vec[]{
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ()),
};
}
/**
* Gets an array of {@link Vector} representing the points at the top of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getTopFace() {
return new Vector[]{
new Vector(getMinX(), getMaxY(), getMinZ()),
new Vector(getMaxX(), getMaxY(), getMinZ()),
new Vector(getMaxX(), getMaxY(), getMaxZ()),
new Vector(getMinX(), getMaxY(), getMaxZ()),
public Vec[] getTopFace() {
return new Vec[]{
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
};
}
/**
* Gets an array of {@link Vector} representing the points on the left face of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getLeftFace() {
return new Vector[]{
new Vector(getMinX(), getMinY(), getMinZ()),
new Vector(getMinX(), getMaxY(), getMinZ()),
new Vector(getMinX(), getMaxY(), getMaxZ()),
new Vector(getMinX(), getMinY(), getMaxZ()),
public Vec[] getLeftFace() {
return new Vec[]{
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ()),
};
}
/**
* Gets an array of {@link Vector} representing the points on the right face of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getRightFace() {
return new Vector[]{
new Vector(getMaxX(), getMinY(), getMinZ()),
new Vector(getMaxX(), getMaxY(), getMinZ()),
new Vector(getMaxX(), getMaxY(), getMaxZ()),
new Vector(getMaxX(), getMinY(), getMaxZ()),
public Vec[] getRightFace() {
return new Vec[]{
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
};
}
/**
* Gets an array of {@link Vector} representing the points at the front of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getFrontFace() {
return new Vector[]{
new Vector(getMinX(), getMinY(), getMinZ()),
new Vector(getMaxX(), getMinY(), getMinZ()),
new Vector(getMaxX(), getMaxY(), getMinZ()),
new Vector(getMinX(), getMaxY(), getMinZ()),
public Vec[] getFrontFace() {
return new Vec[]{
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
};
}
/**
* Gets an array of {@link Vector} representing the points at the back of the {@link BoundingBox}.
* 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}
*/
@NotNull
public Vector[] getBackFace() {
return new Vector[]{
new Vector(getMinX(), getMinY(), getMaxZ()),
new Vector(getMaxX(), getMinY(), getMaxZ()),
new Vector(getMaxX(), getMaxY(), getMaxZ()),
new Vector(getMinX(), getMaxY(), getMaxZ()),
public Vec[] getBackFace() {
return new Vec[]{
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
};
}

View File

@ -5,67 +5,46 @@ 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.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.coordinate.Point;
import net.minestom.server.utils.coordinate.Pos;
import net.minestom.server.utils.coordinate.Vec;
import org.jetbrains.annotations.NotNull;
public class CollisionUtils {
private static final Vector Y_AXIS = new Vector(0, 1, 0);
private static final Vector X_AXIS = new Vector(1, 0, 0);
private static final Vector Z_AXIS = new Vector(0, 0, 1);
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);
/**
* Moves an entity with physics applied (ie checking against blocks)
*
* @param entity the entity to move
* @param deltaPosition
* @param positionOut the Position object in which the new position will be saved
* @param velocityOut the Vector object in which the new velocity will be saved
* @return whether this entity is on the ground
* @return the result of physics simulation
*/
public static boolean handlePhysics(@NotNull Entity entity,
@NotNull Vector deltaPosition,
@NotNull Position positionOut,
@NotNull Vector velocityOut) {
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 Position currentPosition = entity.getPosition();
final Pos currentPosition = entity.getPosition();
final BoundingBox boundingBox = entity.getBoundingBox();
Vector intermediaryPosition = new Vector();
boolean yCollision = stepAxis(instance, originChunk, currentPosition.toVector(), Y_AXIS, deltaPosition.getY(),
intermediaryPosition,
deltaPosition.getY() > 0 ? boundingBox.getTopFace() : boundingBox.getBottomFace());
final StepResult yCollision = stepAxis(instance, originChunk, currentPosition.asVec(), Y_AXIS, deltaPosition.y(),
deltaPosition.y() > 0 ? boundingBox.getTopFace() : boundingBox.getBottomFace());
boolean xCollision = stepAxis(instance, originChunk, intermediaryPosition, X_AXIS, deltaPosition.getX(),
intermediaryPosition,
deltaPosition.getX() < 0 ? boundingBox.getLeftFace() : boundingBox.getRightFace());
final StepResult xCollision = stepAxis(instance, originChunk, yCollision.newPosition, X_AXIS, deltaPosition.x(),
deltaPosition.x() < 0 ? boundingBox.getLeftFace() : boundingBox.getRightFace());
boolean zCollision = stepAxis(instance, originChunk, intermediaryPosition, Z_AXIS, deltaPosition.getZ(),
intermediaryPosition,
deltaPosition.getZ() > 0 ? boundingBox.getBackFace() : boundingBox.getFrontFace());
final StepResult zCollision = stepAxis(instance, originChunk, xCollision.newPosition, Z_AXIS, deltaPosition.z(),
deltaPosition.z() > 0 ? boundingBox.getBackFace() : boundingBox.getFrontFace());
positionOut.setX(intermediaryPosition.getX());
positionOut.setY(intermediaryPosition.getY());
positionOut.setZ(intermediaryPosition.getZ());
velocityOut.copy(deltaPosition);
if (xCollision) {
velocityOut.setX(0f);
}
if (yCollision) {
velocityOut.setY(0f);
}
if (zCollision) {
velocityOut.setZ(0f);
}
return yCollision && deltaPosition.getY() < 0;
return new PhysicsResult(currentPosition.withCoord(zCollision.newPosition),
deltaPosition.with(((x, y, z) -> new Vec(
xCollision.foundCollision ? 0 : x,
yCollision.foundCollision ? 0 : y,
zCollision.foundCollision ? 0 : z
))), yCollision.foundCollision && deltaPosition.y() < 0);
}
/**
@ -76,111 +55,82 @@ public class CollisionUtils {
* @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 positionOut the vector in which to store the new position
* @param corners the corners to check against
* @return true if a collision has been found
* @return result of the step
*/
private static boolean stepAxis(Instance instance,
Chunk originChunk,
Vector startPosition, Vector axis,
double stepAmount, Vector positionOut,
Vector... corners) {
positionOut.copy(startPosition);
private static StepResult stepAxis(Instance instance, Chunk originChunk, Vec startPosition, Vec axis, double stepAmount, Vec... corners) {
if (corners.length == 0)
return false; // avoid degeneracy in following computations
// perform copies to allow in place modifications
// prevents making a lot of new objects. Well at least it reduces the count
BlockPosition[] cornerPositions = new BlockPosition[corners.length];
Vector[] cornersCopy = new Vector[corners.length];
for (int i = 0; i < corners.length; i++) {
cornersCopy[i] = corners[i].clone();
cornerPositions[i] = new BlockPosition(corners[i]);
}
return new StepResult(startPosition, false); // avoid degeneracy in following computations
final Vec[] originalCorners = corners.clone();
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++) {
if (!stepOnce(instance, originChunk, axis, sign, cornersCopy, cornerPositions)) {
collisionFound = true;
}
if (collisionFound) {
break;
}
final OneStepResult oneStepResult = stepOnce(instance, originChunk, axis, sign, corners);
corners = oneStepResult.newCorners;
if (collisionFound = oneStepResult.foundCollision) break;
}
// add remainingLength
if (!collisionFound) {
Vector direction = new Vector();
direction.copy(axis);
collisionFound = !stepOnce(instance, originChunk, direction, remainingLength, cornersCopy, cornerPositions);
final OneStepResult oneStepResult = stepOnce(instance, originChunk, axis, remainingLength, corners);
corners = oneStepResult.newCorners;
collisionFound = oneStepResult.foundCollision;
}
// find the corner which moved the least
double smallestDisplacement = Double.POSITIVE_INFINITY;
for (int i = 0; i < corners.length; i++) {
final double displacement = corners[i].distance(cornersCopy[i]);
final double displacement = originalCorners[i].distance(corners[i]);
if (displacement < smallestDisplacement) {
smallestDisplacement = displacement;
}
}
positionOut.copy(startPosition);
positionOut.add(smallestDisplacement * axis.getX() * sign, smallestDisplacement * axis.getY() * sign, smallestDisplacement * axis.getZ() * sign);
return collisionFound;
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 amount
* @param cornersCopy the corners of the bounding box to consider (mutable)
* @param cornerPositions the corners, converted to BlockPosition (mutable)
* @return false if this method encountered a collision
* @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 the result of one step
*/
private static boolean stepOnce(Instance instance,
Chunk originChunk,
Vector axis, double amount, Vector[] cornersCopy, BlockPosition[] cornerPositions) {
private static OneStepResult stepOnce(Instance instance, Chunk originChunk, Vec axis, double amount, Vec[] corners) {
final double sign = Math.signum(amount);
for (int cornerIndex = 0; cornerIndex < cornersCopy.length; cornerIndex++) {
Vector corner = cornersCopy[cornerIndex];
BlockPosition blockPos = cornerPositions[cornerIndex];
corner.add(axis.getX() * amount, axis.getY() * amount, axis.getZ() * amount);
blockPos.setX((int) Math.floor(corner.getX()));
blockPos.setY((int) Math.floor(corner.getY()));
blockPos.setZ((int) Math.floor(corner.getZ()));
Vec[] newCorners = new Vec[corners.length];
for (int cornerIndex = 0; cornerIndex < corners.length; cornerIndex++) {
final Vec originalCorner = corners[cornerIndex];
final Vec corner = originalCorner.add(axis.mul(amount));
Chunk chunk = ChunkUtils.retrieve(instance, originChunk, blockPos);
Chunk chunk = ChunkUtils.retrieve(instance, originChunk, corner);
if (!ChunkUtils.isLoaded(chunk)) {
// Collision at chunk border
return false;
return new OneStepResult(corners, true);
}
final Block block = chunk.getBlock(blockPos);
final Block block = chunk.getBlock(corner);
// TODO: block collision boxes
// TODO: for the moment, always consider a full block
if (block.isSolid()) {
corner.subtract(axis.getX() * amount, axis.getY() * amount, axis.getZ() * amount);
newCorners[cornerIndex] = originalCorner.with(((x, y, z) -> new Vec(
Math.abs(axis.x()) > 10e-16 ? originalCorner.blockX() - axis.x() * sign : x,
Math.abs(axis.y()) > 10e-16 ? originalCorner.blockY() - axis.y() * sign : y,
Math.abs(axis.z()) > 10e-16 ? originalCorner.blockZ() - axis.z() * sign : z
)));
if (Math.abs(axis.getX()) > 10e-16) {
corner.setX(blockPos.getX() - axis.getX() * sign);
}
if (Math.abs(axis.getY()) > 10e-16) {
corner.setY(blockPos.getY() - axis.getY() * sign);
}
if (Math.abs(axis.getZ()) > 10e-16) {
corner.setZ(blockPos.getZ() - axis.getZ() * sign);
}
return false;
return new OneStepResult(newCorners, true);
}
newCorners[cornerIndex] = corner;
}
return true;
return new OneStepResult(newCorners, false);
}
/**
@ -213,4 +163,47 @@ public class CollisionUtils {
throw new IllegalStateException("Something weird happened...");
}
public static class PhysicsResult {
private final Pos newPosition;
private final Vec newVelocity;
private final boolean isOnGround;
public PhysicsResult(Pos newPosition, Vec newVelocity, boolean isOnGround) {
this.newPosition = newPosition;
this.newVelocity = newVelocity;
this.isOnGround = isOnGround;
}
public Pos newPosition() {
return newPosition;
}
public Vec newVelocity() {
return newVelocity;
}
public boolean isOnGround() {
return isOnGround;
}
}
private static class StepResult {
private final Vec newPosition;
private final boolean foundCollision;
public StepResult(Vec newPosition, boolean foundCollision) {
this.newPosition = newPosition;
this.foundCollision = foundCollision;
}
}
private static class OneStepResult {
private final Vec[] newCorners;
private final boolean foundCollision;
public OneStepResult(Vec[] newCorners, boolean foundCollision) {
this.newCorners = newCorners;
this.foundCollision = foundCollision;
}
}
}

View File

@ -38,7 +38,6 @@ import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
@ -508,26 +507,30 @@ public class Entity implements Viewable, Tickable, EventHandler<EntityEvent>, Da
if (applyVelocity) {
final float tps = MinecraftServer.TICK_PER_SECOND;
final double newX = position.x() + velocity.x() / tps;
final double newY = position.y() + velocity.y() / tps;
final double newZ = position.z() + velocity.z() / tps;
Position newPosition = new Position(newX, newY, newZ);
Vector newVelocityOut = new Vector();
final Pos newPosition;
final Vec newVelocity;
// Gravity force
final double gravityY = hasNoGravity() ? 0 : gravityAcceleration;
final Vector deltaPos = new Vector(
getVelocity().getX() / tps,
getVelocity().getY() / tps - gravityY,
getVelocity().getZ() / tps
final Vec deltaPos = new Vec(
getVelocity().x() / tps,
getVelocity().y() / tps - gravityY,
getVelocity().z() / tps
);
if (this.hasPhysics) {
this.onGround = CollisionUtils.handlePhysics(this, deltaPos, newPosition, newVelocityOut);
final CollisionUtils.PhysicsResult physicsResult = CollisionUtils.handlePhysics(this, deltaPos);
this.onGround = physicsResult.isOnGround();
newPosition = physicsResult.newPosition();
newVelocity = physicsResult.newVelocity();
} else {
newVelocityOut = deltaPos;
newVelocity = deltaPos;
newPosition = new Pos(
position.x() + velocity.x() / tps,
position.y() + velocity.y() / tps,
position.z() + velocity.z() / tps
);
}
// World border collision
@ -545,27 +548,23 @@ public class Entity implements Viewable, Tickable, EventHandler<EntityEvent>, Da
}
// Update velocity
if (hasVelocity() || !newVelocityOut.isZero()) {
this.velocity.copy(newVelocityOut);
this.velocity.multiply(tps);
if (hasVelocity() || !newVelocity.equals(Vec.ZERO)) {
velocity = newVelocity.mul(tps); // Convert from blocks/tick to blocks/sec
final Block block = finalChunk.getBlock(position);
final double drag = block.registry().friction();
if (onGround) {
// Stop player velocity
if (isNettyClient) {
this.velocity.zero();
velocity = Vec.ZERO;
}
}
this.velocity.setX(velocity.getX() * drag);
this.velocity.setZ(velocity.getZ() * drag);
if (!hasNoGravity())
this.velocity.setY(velocity.getY() * (1 - gravityDragPerTick));
if (velocity.equals(new Vector())) {
this.velocity.zero();
}
velocity = velocity.with((x, y, z) -> new Vec(
x * drag,
!hasNoGravity() ? y * (1 - gravityDragPerTick) : y,
z * drag
)).with(Vec.Operator.EPSILON);
}
// Synchronization and packets...

View File

@ -9,8 +9,6 @@ import net.minestom.server.entity.LivingEntity;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.coordinate.Point;
import net.minestom.server.utils.coordinate.Pos;
@ -75,16 +73,11 @@ public class Navigator {
// Update 'position' view
PositionUtils.lookAlong(position, dx, direction.y(), dz);
Position newPosition = new Position();
Vector newVelocityOut = new Vector();
// Prevent ghosting
CollisionUtils.handlePhysics(entity,
new Vector(speedX, speedY, speedZ),
newPosition, newVelocityOut);
final CollisionUtils.PhysicsResult physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ));
// Will move the entity during Entity#tick
position.copyCoordinates(newPosition);
entity.teleport(physicsResult.newPosition());
}
public void jump(float height) {

View File

@ -392,6 +392,15 @@ public final class Vec implements Point {
@FunctionalInterface
public interface Operator {
/**
* Checks each axis' value, if it's below {@code 1E-6} then it gets replaced with {@code 0}
*/
Operator EPSILON = (x, y, z) -> new Vec(
Math.abs(x) < 1E-6 ? 0 : x,
Math.abs(y) < 1E-6 ? 0 : y,
Math.abs(z) < 1E-6 ? 0 : z
);
@NotNull Vec apply(double x, double y, double z);
}