From 0400e2dda4cca9178a26eff38b3a73e1ccea71b9 Mon Sep 17 00:00:00 2001 From: iam Date: Tue, 27 Jun 2023 17:40:57 -0400 Subject: [PATCH] hollow-cube/projectile-physics-improvements * Add better protectiles * Cleanup * better physics * Add filter * Update EntityCollision.java * Negate filter check --- .../server/collision/BlockCollision.java | 64 ++++--- .../server/collision/BoundingBox.java | 51 ++++- .../server/collision/CollisionUtils.java | 60 +++++- .../server/collision/EntityCollision.java | 68 +++++++ .../server/collision/PhysicsResult.java | 30 ++- .../minestom/server/collision/RayUtils.java | 2 +- .../minestom/server/collision/ShapeImpl.java | 7 +- .../server/collision/SweepResult.java | 13 +- .../net/minestom/server/entity/Entity.java | 40 +++- .../server/entity/PlayerProjectile.java | 179 ++++++++++++++++++ .../EntityBlockPhysicsIntegrationTest.java | 4 +- 11 files changed, 468 insertions(+), 50 deletions(-) create mode 100644 src/main/java/net/minestom/server/collision/EntityCollision.java create mode 100644 src/main/java/net/minestom/server/entity/PlayerProjectile.java diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 07f868296..3eae94f3d 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -24,10 +24,11 @@ final class BlockCollision { static PhysicsResult handlePhysics(@NotNull BoundingBox boundingBox, @NotNull Vec velocity, @NotNull Pos entityPosition, @NotNull Block.Getter getter, - @Nullable PhysicsResult lastPhysicsResult) { + @Nullable PhysicsResult lastPhysicsResult, + boolean singleCollision) { if (velocity.isZero()) { // TODO should return a constant - return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, velocity, null, Block.AIR); + return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, velocity, new Point[3], new Shape[3], false, SweepResult.NO_COLLISION); } // Fast-exit using cache final PhysicsResult cachedResult = cachedPhysics(velocity, entityPosition, getter, lastPhysicsResult); @@ -35,7 +36,7 @@ final class BlockCollision { return cachedResult; } // Expensive AABB computation - return stepPhysics(boundingBox, velocity, entityPosition, getter); + return stepPhysics(boundingBox, velocity, entityPosition, getter, singleCollision); } static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) { @@ -67,30 +68,35 @@ final class BlockCollision { private static PhysicsResult cachedPhysics(Vec velocity, Pos entityPosition, Block.Getter getter, PhysicsResult lastPhysicsResult) { - if (lastPhysicsResult != null) { + if (lastPhysicsResult != null && lastPhysicsResult.collisionShapes()[1] instanceof ShapeImpl shape) { + Block collisionBlockY = shape.block(); + + // Fast exit if entity hasn't moved if (lastPhysicsResult.collisionY() && velocity.y() == lastPhysicsResult.originalDelta().y() - && lastPhysicsResult.collidedBlockY() != null - && getter.getBlock(lastPhysicsResult.collidedBlockY(), Block.Getter.Condition.TYPE) == lastPhysicsResult.blockTypeY() + // Check block below to fast exit gravity + && getter.getBlock(lastPhysicsResult.collisionPoints()[1].sub(0, Vec.EPSILON, 0), Block.Getter.Condition.TYPE) == collisionBlockY && velocity.x() == 0 && velocity.z() == 0 && entityPosition.samePoint(lastPhysicsResult.newPosition()) - && lastPhysicsResult.blockTypeY() != Block.AIR) { + && collisionBlockY != Block.AIR) { return lastPhysicsResult; } } return null; } - private static PhysicsResult stepPhysics(@NotNull BoundingBox boundingBox, @NotNull Vec velocity, @NotNull Pos entityPosition, - @NotNull Block.Getter getter) { + @NotNull Block.Getter getter, boolean singleCollision) { // Allocate once and update values - SweepResult finalResult = new SweepResult(1 - Vec.EPSILON, 0, 0, 0, null); + SweepResult finalResult = new SweepResult(1 - Vec.EPSILON, 0, 0, 0, null, null); boolean foundCollisionX = false, foundCollisionY = false, foundCollisionZ = false; - Point collisionYBlock = null; - Block blockYType = Block.AIR; + + Point[] collidedPoints = new Point[3]; + Shape[] collisionShapes = new Shape[3]; + + boolean hasCollided = false; // Query faces to get the points needed for collision final Vec[] allFaces = calculateFaces(velocity, boundingBox); @@ -100,36 +106,48 @@ final class BlockCollision { // Looping until there are no collisions will allow the entity to move in axis other than the collision axis after a collision. while (result.collisionX() || result.collisionY() || result.collisionZ()) { // Reset final result - finalResult.res = 1 - Vec.EPSILON; finalResult.normalX = 0; finalResult.normalY = 0; finalResult.normalZ = 0; - if (result.collisionX()) foundCollisionX = true; - if (result.collisionZ()) foundCollisionZ = true; - if (result.collisionY()) { + if (result.collisionX()) { + foundCollisionX = true; + collisionShapes[0] = finalResult.collidedShape; + collidedPoints[0] = finalResult.collidedPosition; + hasCollided = true; + if (singleCollision) break; + } else if (result.collisionZ()) { + foundCollisionZ = true; + collisionShapes[2] = finalResult.collidedShape; + collidedPoints[2] = finalResult.collidedPosition; + hasCollided = true; + if (singleCollision) break; + } else if (result.collisionY()) { foundCollisionY = true; - // If we are only moving in the y-axis - if (!result.collisionX() && !result.collisionZ() && velocity.x() == 0 && velocity.z() == 0) { - collisionYBlock = result.collidedBlockY(); - blockYType = result.blockTypeY(); - } + collisionShapes[1] = finalResult.collidedShape; + collidedPoints[1] = finalResult.collidedPosition; + hasCollided = true; + if (singleCollision) break; } + // If all axis have had collisions, break if (foundCollisionX && foundCollisionY && foundCollisionZ) break; // If the entity isn't moving, break if (result.newVelocity().isZero()) break; + finalResult.res = 1 - Vec.EPSILON; result = computePhysics(boundingBox, result.newVelocity(), result.newPosition(), getter, allFaces, finalResult); } + finalResult.res = result.res().res; + final double newDeltaX = foundCollisionX ? 0 : velocity.x(); final double newDeltaY = foundCollisionY ? 0 : velocity.y(); final double newDeltaZ = foundCollisionZ ? 0 : velocity.z(); return new PhysicsResult(result.newPosition(), new Vec(newDeltaX, newDeltaY, newDeltaZ), newDeltaY == 0 && velocity.y() < 0, - foundCollisionX, foundCollisionY, foundCollisionZ, velocity, collisionYBlock, blockYType); + foundCollisionX, foundCollisionY, foundCollisionZ, velocity, collidedPoints, collisionShapes, hasCollided, finalResult); } private static PhysicsResult computePhysics(@NotNull BoundingBox boundingBox, @@ -165,7 +183,7 @@ final class BlockCollision { return new PhysicsResult(finalPos, new Vec(remainingX, remainingY, remainingZ), collisionY, collisionX, collisionY, collisionZ, - Vec.ZERO, finalResult.collidedShapePosition, finalResult.blockType); + Vec.ZERO, null, null, false, finalResult); } private static void slowPhysics(@NotNull BoundingBox boundingBox, diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index 38e35ec22..d7a70c40c 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -9,6 +9,8 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Iterator; + /** * See https://wiki.vg/Entity_metadata#Mobs_2 */ @@ -51,9 +53,8 @@ public final class BoundingBox implements Shape { @ApiStatus.Experimental public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, this, shapePos, finalResult) ) { - finalResult.collidedShapePosition = shapePos; + finalResult.collidedPosition = rayStart.add(rayDirection.mul(finalResult.res)); finalResult.collidedShape = this; - finalResult.blockType = null; return true; } @@ -160,6 +161,52 @@ public final class BoundingBox implements Shape { return relativeEnd().z(); } + public Iterator getBlocks(Point point) { + return new PointIterator(this, point); + } + + static class PointIterator implements Iterator { + int x = 0; + int y = 0; + int z = 0; + + private final int minX, minY, minZ, maxX, maxY, maxZ; + + public PointIterator(BoundingBox boundingBox, Point p) { + minX = (int) Math.floor(boundingBox.minX() + p.x()); + minY = (int) Math.floor(boundingBox.minY() + p.y()); + minZ = (int) Math.floor(boundingBox.minZ() + p.z()); + maxX = (int) Math.floor(boundingBox.maxX() + p.x()); + maxY = (int) Math.floor(boundingBox.maxY() + p.y()); + maxZ = (int) Math.floor(boundingBox.maxZ() + p.z()); + x = minX; + y = minY; + z = minZ; + } + + @Override + public boolean hasNext() { + return x <= maxX && y <= maxY && z <= maxZ; + } + + @Override + public Point next() { + var res = new Vec(x, y, z); + + x++; + if (x > maxX) { + x = minX; + y++; + if (y > maxY) { + y = minY; + z++; + } + } + + return res; + } + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index dacc00c2b..e68e0b8e5 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -14,6 +14,8 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.function.Function; + @ApiStatus.Internal @ApiStatus.Experimental public final class CollisionUtils { @@ -25,6 +27,55 @@ public final class CollisionUtils { * All bounding boxes inside the full blocks are checked for collisions with the entity. * * @param entity the entity to move + * @param entityVelocity the velocity of the entity + * @param lastPhysicsResult the last physics result, can be null + * @param singleCollision if the entity should only collide with one block + * @return the result of physics simulation + */ + public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, + @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { + final Instance instance = entity.getInstance(); + assert instance != null; + return handlePhysics(instance, entity.getChunk(), + entity.getBoundingBox(), + entity.getPosition(), entityVelocity, + lastPhysicsResult, singleCollision); + } + + /** + * + * @param entity the entity to move + * @param entityVelocity the velocity of the entity + * @return the closest entity we collide with + */ + public static PhysicsResult checkEntityCollisions(@NotNull Entity entity, @NotNull Vec entityVelocity) { + final Instance instance = entity.getInstance(); + assert instance != null; + return checkEntityCollisions(instance, entity.getBoundingBox(), entity.getPosition(), entityVelocity, 3, e -> true, null); + } + + /** + * + * @param velocity the velocity of the entity + * @param extendRadius the largest entity bounding box we can collide with + * Measured from bottom center to top corner + * This is used to extend the search radius for entities we collide with + * For players this is (0.3^2 + 0.3^2 + 1.8^2) ^ (1/3) ~= 1.51 + * @return the closest entity we collide with + */ + public static PhysicsResult checkEntityCollisions(@NotNull Instance instance, BoundingBox boundingBox, Point pos, @NotNull Vec velocity, double extendRadius, Function entityFilter, PhysicsResult blockResult) { + return EntityCollision.checkCollision(instance, boundingBox, pos, velocity, extendRadius, entityFilter, blockResult); + } + + /** + * Moves an entity with physics applied (ie checking against blocks) + *

+ * 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 + * @param entityVelocity the velocity of the entity + * @param lastPhysicsResult the last physics result, can be null * @return the result of physics simulation */ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, @@ -34,7 +85,7 @@ public final class CollisionUtils { return handlePhysics(instance, entity.getChunk(), entity.getBoundingBox(), entity.getPosition(), entityVelocity, - lastPhysicsResult); + lastPhysicsResult, false); } /** @@ -49,11 +100,11 @@ public final class CollisionUtils { public static PhysicsResult handlePhysics(@NotNull Instance instance, @Nullable Chunk chunk, @NotNull BoundingBox boundingBox, @NotNull Pos position, @NotNull Vec velocity, - @Nullable PhysicsResult lastPhysicsResult) { + @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { final Block.Getter getter = new ChunkCache(instance, chunk != null ? chunk : instance.getChunkAt(position), Block.STONE); return BlockCollision.handlePhysics(boundingBox, velocity, position, - getter, lastPhysicsResult); + getter, lastPhysicsResult, singleCollision); } /** @@ -73,7 +124,8 @@ public final class CollisionUtils { final PhysicsResult result = handlePhysics(instance, chunk, BoundingBox.ZERO, Pos.fromPoint(start), Vec.fromPoint(end.sub(start)), - null); + null, false); + return shape.intersectBox(end.sub(result.newPosition()).sub(Vec.EPSILON), BoundingBox.ZERO); } diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java new file mode 100644 index 000000000..7391e1dc0 --- /dev/null +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -0,0 +1,68 @@ +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.Instance; +import java.util.function.Function; + +final class EntityCollision { + public static PhysicsResult checkCollision(Instance instance, BoundingBox boundingBox, Point point, Vec entityVelocity, double extendRadius, Function entityFilter, PhysicsResult res) { + double minimumRes = res != null ? res.res().res : Double.MAX_VALUE; + + if (instance == null) return null; + SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, null); + + double closestDistance = minimumRes; + Entity closestEntity = null; + + var maxDistance = Math.pow(boundingBox.height() * boundingBox.height() + boundingBox.depth()/2 * boundingBox.depth()/2 + boundingBox.width()/2 * boundingBox.width()/2, 1/3.0); + double projectileDistance = entityVelocity.length(); + + for (Entity e : instance.getNearbyEntities(point, extendRadius + maxDistance + projectileDistance)) { + if (!entityFilter.apply(e)) continue; + if (!e.hasCollision()) continue; + + // Overlapping with entity, math can't be done we return the entity + if (e.getBoundingBox().intersectBox(e.getPosition().sub(point), boundingBox)) { + var p = Pos.fromPoint(point); + + return new PhysicsResult(p, + Vec.ZERO, + false, + true, + true, + true, + entityVelocity, + new Pos[] {p, p, p}, + new Shape[] {e, e, e}, + true, + sweepResult); + } + + // Check collisions with entity + e.getBoundingBox().intersectBoxSwept(point, entityVelocity, e.getPosition(), boundingBox, sweepResult); + + if (sweepResult.res < closestDistance && sweepResult.res < 1) { + closestDistance = sweepResult.res; + closestEntity = e; + } + } + + Pos[] collisionPoints = new Pos[3]; + + return new PhysicsResult(Pos.fromPoint(point).add(entityVelocity.mul(closestDistance)), + Vec.ZERO, + sweepResult.normalY == -1, + sweepResult.normalX != 0, + sweepResult.normalY != 0, + sweepResult.normalZ != 0, + entityVelocity, + collisionPoints, + new Shape[] {closestEntity, closestEntity, closestEntity}, + sweepResult.normalX != 0 || sweepResult.normalZ != 0 || sweepResult.normalY != 0, + sweepResult + ); + } +} diff --git a/src/main/java/net/minestom/server/collision/PhysicsResult.java b/src/main/java/net/minestom/server/collision/PhysicsResult.java index 7a3e90c69..06e02dbd8 100644 --- a/src/main/java/net/minestom/server/collision/PhysicsResult.java +++ b/src/main/java/net/minestom/server/collision/PhysicsResult.java @@ -3,11 +3,33 @@ 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; +import org.jetbrains.annotations.NotNull; @ApiStatus.Experimental -public record PhysicsResult(Pos newPosition, Vec newVelocity, boolean isOnGround, - boolean collisionX, boolean collisionY, boolean collisionZ, - Vec originalDelta, Point collidedBlockY, Block blockTypeY) { +/** + * The result of a physics simulation. + * @param newPosition the new position of the entity + * @param newVelocity the new velocity of the entity + * @param isOnGround if the entity is on the ground + * @param collisionX if the entity collided on the X axis + * @param collisionY if the entity collided on the Y axis + * @param collisionZ if the entity collided on the Z axis + * @param originalDelta the velocity delta of the entity + * @param collisionPoints the points where the entity collided + * @param collisionShapes the shapes the entity collided with + */ +public record PhysicsResult( + Pos newPosition, + Vec newVelocity, + boolean isOnGround, + boolean collisionX, + boolean collisionY, + boolean collisionZ, + Vec originalDelta, + @NotNull Point[] collisionPoints, + @NotNull Shape[] collisionShapes, + boolean hasCollision, + SweepResult res +) { } diff --git a/src/main/java/net/minestom/server/collision/RayUtils.java b/src/main/java/net/minestom/server/collision/RayUtils.java index d05fbf098..3ea648c36 100644 --- a/src/main/java/net/minestom/server/collision/RayUtils.java +++ b/src/main/java/net/minestom/server/collision/RayUtils.java @@ -174,6 +174,6 @@ final class RayUtils { } public static boolean BoundingBoxRayIntersectionCheck(Vec start, Vec direction, BoundingBox boundingBox, Pos position) { - return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position, new SweepResult(Double.MAX_VALUE, 0, 0, 0, null)); + return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position, new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, null)); } } diff --git a/src/main/java/net/minestom/server/collision/ShapeImpl.java b/src/main/java/net/minestom/server/collision/ShapeImpl.java index 3a3f691e2..8153266bf 100644 --- a/src/main/java/net/minestom/server/collision/ShapeImpl.java +++ b/src/main/java/net/minestom/server/collision/ShapeImpl.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -final class ShapeImpl implements Shape { +public final class ShapeImpl implements Shape { private static final Pattern PATTERN = Pattern.compile("\\d.\\d{1,3}", Pattern.MULTILINE); private final BoundingBox[] collisionBoundingBoxes; private final Point relativeStart, relativeEnd; @@ -165,16 +165,15 @@ final class ShapeImpl implements Shape { for (BoundingBox blockSection : collisionBoundingBoxes) { // Update final result if the temp result collision is sooner than the current final result if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos, finalResult)) { - finalResult.collidedShapePosition = shapePos; + finalResult.collidedPosition = rayStart.add(rayDirection.mul(finalResult.res)); finalResult.collidedShape = this; - finalResult.blockType = block(); hitBlock = true; } } return hitBlock; } - private Block block() { + public Block block() { Block block = this.block; if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId()); return block; diff --git a/src/main/java/net/minestom/server/collision/SweepResult.java b/src/main/java/net/minestom/server/collision/SweepResult.java index d74341bdb..56060c55c 100644 --- a/src/main/java/net/minestom/server/collision/SweepResult.java +++ b/src/main/java/net/minestom/server/collision/SweepResult.java @@ -1,13 +1,15 @@ package net.minestom.server.collision; import net.minestom.server.coordinate.Point; -import net.minestom.server.instance.block.Block; +import net.minestom.server.coordinate.Pos; + +public final class SweepResult { + public static SweepResult NO_COLLISION = new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, Pos.ZERO); -final class SweepResult { double res; double normalX, normalY, normalZ; - Point collidedShapePosition; - Block blockType; + Point collidedPosition; + Point collidedPos; Shape collidedShape; /** @@ -18,11 +20,12 @@ final class SweepResult { * @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) { + public SweepResult(double res, double normalX, double normalY, double normalZ, Shape collidedShape, Point collidedPos) { this.res = res; this.normalX = normalX; this.normalY = normalY; this.normalZ = normalZ; this.collidedShape = collidedShape; + this.collidedPos = collidedPos; } } diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index c72bfe9b2..759753ac7 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -9,9 +9,7 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; 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.collision.*; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -30,6 +28,7 @@ import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.LazyPacket; @@ -85,7 +84,7 @@ import java.util.function.UnaryOperator; * To create your own entity you probably want to extends {@link LivingEntity} or {@link EntityCreature} instead. */ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler, Taggable, - PermissionHandler, HoverEventSource, Sound.Emitter { + PermissionHandler, HoverEventSource, Sound.Emitter, Shape { private static final int VELOCITY_UPDATE_INTERVAL = 1; @@ -111,6 +110,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected Vec velocity = Vec.ZERO; // Movement in block per second protected boolean lastVelocityWasZero = true; protected boolean hasPhysics = true; + protected boolean hasCollision = true; /** * The amount of drag applied on the Y axle. @@ -627,9 +627,10 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } // Update velocity - if (hasVelocity || !newVelocity.isZero()) { + if (!noGravity && (hasVelocity || !newVelocity.isZero())) { updateVelocity(wasOnGround, flying, positionBeforeMove, newVelocity); } + // Verify if velocity packet has to be sent if (this.ticks % VELOCITY_UPDATE_INTERVAL == 0) { if (!isPlayer && (hasVelocity || !lastVelocityWasZero)) { @@ -1729,6 +1730,35 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return nearby.orElse(null); } + @Override + public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) { + return false; + } + + @Override + public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) { + return boundingBox.intersectBox(positionRelative, boundingBox); + } + + @Override + public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { + return boundingBox.intersectBoxSwept(rayStart, rayDirection, shapePos, moving, finalResult); + } + + @Override + public @NotNull Point relativeStart() { + return boundingBox.relativeStart(); + } + + @Override + public @NotNull Point relativeEnd() { + return boundingBox.relativeEnd(); + } + + public boolean hasCollision() { + return hasCollision; + } + public enum Pose { STANDING, FALL_FLYING, diff --git a/src/main/java/net/minestom/server/entity/PlayerProjectile.java b/src/main/java/net/minestom/server/entity/PlayerProjectile.java new file mode 100644 index 000000000..d23b87b22 --- /dev/null +++ b/src/main/java/net/minestom/server/entity/PlayerProjectile.java @@ -0,0 +1,179 @@ +package net.minestom.server.entity; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.collision.PhysicsResult; +import net.minestom.server.collision.ShapeImpl; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.metadata.ProjectileMeta; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.event.entity.EntityShootEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; + +public class PlayerProjectile extends LivingEntity { + private final Entity shooter; + private long cooldown = 0; + + public PlayerProjectile(Entity shooter, EntityType type) { + super(type); + this.shooter = shooter; + this.hasCollision = false; + setup(); + } + + private void setup() { + super.hasPhysics = false; + if (getEntityMeta() instanceof ProjectileMeta) { + ((ProjectileMeta) getEntityMeta()).setShooter(this.shooter); + } + } + + public void shoot(Point from, double power, double spread) { + var to = from.add(shooter.getPosition().direction()); + shoot(from, to, power, spread); + } + + @Override + public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { + var res = super.setInstance(instance, spawnPosition); + + Pos insideBlock = checkInsideBlock(instance); + // Check if we're inside of a block + if (insideBlock != null) { + var e = new ProjectileCollideWithBlockEvent(this, Pos.fromPoint(spawnPosition), instance.getBlock(spawnPosition)); + MinecraftServer.getGlobalEventHandler().call(e); + } + + return res; + } + + public void shoot(@NotNull Point from, @NotNull Point to, double power, double spread) { + var instance = shooter.getInstance(); + if (instance == null) return; + + float yaw = -shooter.getPosition().yaw(); + float pitch = -shooter.getPosition().pitch(); + + double pitchDiff = pitch - 45; + if (pitchDiff == 0) pitchDiff = 0.0001; + double pitchAdjust = pitchDiff * 0.002145329238474369D; + + double dx = to.x() - from.x(); + double dy = to.y() - from.y() + pitchAdjust; + double dz = to.z() - from.z(); + if (!hasNoGravity()) { + final double xzLength = Math.sqrt(dx * dx + dz * dz); + dy += xzLength * 0.20000000298023224D; + } + + final double length = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= length; + dy /= length; + dz /= length; + Random random = ThreadLocalRandom.current(); + spread *= 0.007499999832361937D; + dx += random.nextGaussian() * spread; + dy += random.nextGaussian() * spread; + dz += random.nextGaussian() * spread; + + final EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, from, power, spread); + EventDispatcher.call(shootEvent); + if (shootEvent.isCancelled()) { + remove(); + return; + } + + final double mul = 20 * power; + Vec v = new Vec(dx * mul, dy * mul * 0.9, dz * mul); + + this.setInstance(instance, new Pos(from.x(), from.y(), from.z(), yaw, pitch)).whenComplete((result, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + } else { + this.setVelocity(v); + } + }); + + cooldown = System.currentTimeMillis(); + } + + private Pos checkInsideBlock(@NotNull Instance instance) { + var iterator = this.getBoundingBox().getBlocks(this.getPosition()); + + while (iterator.hasNext()) { + var block = iterator.next(); + Block b = instance.getBlock(block); + var hit = b.registry().collisionShape().intersectBox(this.getPosition().sub(block), this.getBoundingBox()); + if (hit) return Pos.fromPoint(block); + } + + return null; + } + + @Override + public void refreshPosition(@NotNull Pos newPosition) { + } + + @Override + public void tick(long time) { + final Pos posBefore = getPosition(); + super.tick(time); + final Pos posNow = getPosition(); + + Vec diff = Vec.fromPoint(posNow.sub(posBefore)); + PhysicsResult result = CollisionUtils.handlePhysics( + instance, this.getChunk(), + this.getBoundingBox(), + posBefore, diff, + null, true + ); + + if (cooldown + 500 < System.currentTimeMillis()) { + float yaw = (float) Math.toDegrees(Math.atan2(diff.x(), diff.z())); + float pitch = (float) Math.toDegrees(Math.atan2(diff.y(), Math.sqrt(diff.x() * diff.x() + diff.z() * diff.z()))); + super.refreshPosition(new Pos(posNow.x(), posNow.y(), posNow.z(), yaw, pitch)); + cooldown = System.currentTimeMillis(); + } + + PhysicsResult collided = CollisionUtils.checkEntityCollisions(instance, this.getBoundingBox(), posBefore, diff, 3, (e) -> e != this, result); + if (collided != null && collided.collisionShapes()[0] != shooter) { + if (collided.collisionShapes()[0] instanceof Entity entity) { + var e = new ProjectileCollideWithEntityEvent(this, collided.newPosition(), entity); + MinecraftServer.getGlobalEventHandler().call(e); + return; + } + } + + if (result.hasCollision()) { + Block hitBlock = null; + Point hitPoint = null; + if (result.collisionShapes()[0] instanceof ShapeImpl block) { + hitBlock = block.block(); + hitPoint = result.collisionPoints()[0]; + } + if (result.collisionShapes()[1] instanceof ShapeImpl block) { + hitBlock = block.block(); + hitPoint = result.collisionPoints()[1]; + } + if (result.collisionShapes()[2] instanceof ShapeImpl block) { + hitBlock = block.block(); + hitPoint = result.collisionPoints()[2]; + } + + if (hitBlock == null) return; + + var e = new ProjectileCollideWithBlockEvent(this, Pos.fromPoint(hitPoint), hitBlock); + MinecraftServer.getGlobalEventHandler().call(e); + } + } +} diff --git a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java index 78e663263..55dc5ecb2 100644 --- a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java +++ b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java @@ -410,12 +410,12 @@ public class EntityBlockPhysicsIntegrationTest { BoundingBox bb = new Entity(EntityType.ZOMBIE).getBoundingBox(); - SweepResult sweepResultFinal = new SweepResult(1, 0, 0, 0, null); + SweepResult sweepResultFinal = new SweepResult(1, 0, 0, 0, null, null); bb.intersectBoxSwept(z1, movement, z2, bb, sweepResultFinal); bb.intersectBoxSwept(z1, movement, z3, bb, sweepResultFinal); - assertEquals(new Pos(11, 0, 0), sweepResultFinal.collidedShapePosition); + assertEqualsPoint(new Pos(10.4, 0.52, 0), sweepResultFinal.collidedPosition); assertEquals(sweepResultFinal.collidedShape, bb); }