diff --git a/src/main/java/net/minestom/server/collision/Aerodynamics.java b/src/main/java/net/minestom/server/collision/Aerodynamics.java new file mode 100644 index 000000000..954b8cdbc --- /dev/null +++ b/src/main/java/net/minestom/server/collision/Aerodynamics.java @@ -0,0 +1,46 @@ +package net.minestom.server.collision; + +import it.unimi.dsi.fastutil.doubles.DoubleUnaryOperator; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * Represents the aerodynamic properties of an entity + * + * @param gravity the entity's downward acceleration per tick + * @param horizontalAirResistance the horizontal drag coefficient; the entity's current horizontal + * velocity is multiplied by this every tick + * @param verticalAirResistance the vertical drag coefficient; the entity's current vertical + * * velocity is multiplied by this every tick + */ +public record Aerodynamics(double gravity, double horizontalAirResistance, double verticalAirResistance) { + @Contract(pure = true) + public @NotNull Aerodynamics withGravity(double gravity) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withHorizontalAirResistance(double horizontalAirResistance) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withHorizontalAirResistance(@NotNull DoubleUnaryOperator operator) { + return withHorizontalAirResistance(operator.apply(horizontalAirResistance)); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withVerticalAirResistance(double verticalAirResistance) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withVerticalAirResistance(@NotNull DoubleUnaryOperator operator) { + return withVerticalAirResistance(operator.apply(verticalAirResistance)); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withAirResistance(double horizontal, double vertical) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } +} diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 3eae94f3d..3d3595421 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -28,7 +28,8 @@ final class BlockCollision { boolean singleCollision) { if (velocity.isZero()) { // TODO should return a constant - return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, velocity, new Point[3], new Shape[3], false, SweepResult.NO_COLLISION); + 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); diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index e68e0b8e5..00533def4 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -102,9 +102,26 @@ public final class CollisionUtils { @NotNull Pos position, @NotNull Vec velocity, @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { final Block.Getter getter = new ChunkCache(instance, chunk != null ? chunk : instance.getChunkAt(position), Block.STONE); + return handlePhysics(getter, boundingBox, position, velocity, lastPhysicsResult, singleCollision); + } + + /** + * Moves bounding box with physics applied (ie checking against blocks) + *
+ * Works by getting all the full blocks that a bounding box could interact with. + * All bounding boxes inside the full blocks are checked for collisions with the given bounding box. + * + * @param blockGetter the block getter to check collisions against, ensure block access is synchronized + * @return the result of physics simulation + */ + @ApiStatus.Internal + public static PhysicsResult handlePhysics(@NotNull Block.Getter blockGetter, + @NotNull BoundingBox boundingBox, + @NotNull Pos position, @NotNull Vec velocity, + @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { return BlockCollision.handlePhysics(boundingBox, velocity, position, - getter, lastPhysicsResult, singleCollision); + blockGetter, lastPhysicsResult, singleCollision); } /** @@ -140,14 +157,13 @@ public final class CollisionUtils { /** * Applies world border collision. * - * @param instance the instance where the world border is + * @param worldBorder the world border * @param currentPosition the current position * @param newPosition the future target position * @return the position with the world border collision applied (can be {@code newPosition} if not changed) */ - public static @NotNull Pos applyWorldBorder(@NotNull Instance instance, + public static @NotNull Pos applyWorldBorder(@NotNull WorldBorder worldBorder, @NotNull Pos currentPosition, @NotNull Pos newPosition) { - final WorldBorder worldBorder = instance.getWorldBorder(); final WorldBorder.CollisionAxis collisionAxis = worldBorder.getCollisionAxis(newPosition); return switch (collisionAxis) { case NONE -> @@ -168,4 +184,17 @@ public final class CollisionUtils { public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) { return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry); } + + /** + * Simulate the entity's collision physics as if the world had no blocks + * + * @param entityPosition the position of the entity + * @param entityVelocity the velocity of the entity + * @return the result of physics simulation + */ + public static PhysicsResult blocklessCollision(@NotNull Pos entityPosition, @NotNull Vec entityVelocity) { + return new PhysicsResult(entityPosition.add(entityVelocity), entityVelocity, false, + false, false, false, entityVelocity, new Point[3], + new Shape[3], false, SweepResult.NO_COLLISION); + } } diff --git a/src/main/java/net/minestom/server/collision/PhysicsUtils.java b/src/main/java/net/minestom/server/collision/PhysicsUtils.java new file mode 100644 index 000000000..c428cdccc --- /dev/null +++ b/src/main/java/net/minestom/server/collision/PhysicsUtils.java @@ -0,0 +1,65 @@ +package net.minestom.server.collision; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class PhysicsUtils { + /** + * Simulate the entity's movement physics + *
+ * This is done by first attempting to move the entity forward with the
+ * current velocity passed in. Then adjusting the velocity by applying
+ * air resistance and friction.
+ *
+ * @param entityPosition the current entity position
+ * @param entityVelocityPerTick the current entity velocity in blocks/tick
+ * @param entityBoundingBox the current entity bounding box
+ * @param worldBorder the world border to test bounds against
+ * @param blockGetter the block getter to test block collisions against
+ * @param aerodynamics the current entity aerodynamics
+ * @param entityNoGravity whether the entity has gravity
+ * @param entityHasPhysics whether the entity has physics
+ * @param entityOnGround whether the entity is on the ground
+ * @param entityFlying whether the entity is flying
+ * @param previousPhysicsResult the physics result from the previous simulation or null
+ * @return a {@link PhysicsResult} containing the resulting physics state of this simulation
+ */
+ public static @NotNull PhysicsResult simulateMovement(@NotNull Pos entityPosition, @NotNull Vec entityVelocityPerTick, @NotNull BoundingBox entityBoundingBox,
+ @NotNull WorldBorder worldBorder, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics, boolean entityNoGravity,
+ boolean entityHasPhysics, boolean entityOnGround, boolean entityFlying, @Nullable PhysicsResult previousPhysicsResult) {
+ final PhysicsResult physicsResult = entityHasPhysics ?
+ CollisionUtils.handlePhysics(blockGetter, entityBoundingBox, entityPosition, entityVelocityPerTick, previousPhysicsResult, false) :
+ CollisionUtils.blocklessCollision(entityPosition, entityVelocityPerTick);
+
+ Pos newPosition = physicsResult.newPosition();
+ Vec newVelocity = physicsResult.newVelocity();
+
+ Pos positionWithinBorder = CollisionUtils.applyWorldBorder(worldBorder, entityPosition, newPosition);
+ newVelocity = updateVelocity(entityPosition, newVelocity, blockGetter, aerodynamics, !positionWithinBorder.samePoint(entityPosition), entityFlying, entityOnGround, entityNoGravity);
+ return new PhysicsResult(positionWithinBorder, newVelocity, physicsResult.isOnGround(), physicsResult.collisionX(), physicsResult.collisionY(), physicsResult.collisionZ(),
+ physicsResult.originalDelta(), physicsResult.collisionPoints(), physicsResult.collisionShapes(), physicsResult.hasCollision(), physicsResult.res());
+ }
+
+ private static @NotNull Vec updateVelocity(@NotNull Pos entityPosition, @NotNull Vec currentVelocity, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics,
+ boolean positionChanged, boolean entityFlying, boolean entityOnGround, boolean entityNoGravity) {
+ if (!positionChanged) {
+ if (entityOnGround || entityFlying) return Vec.ZERO;
+ return new Vec(0, entityNoGravity ? 0 : -aerodynamics.gravity() * aerodynamics.verticalAirResistance(), 0);
+ }
+
+ double drag = entityOnGround ? blockGetter.getBlock(entityPosition.sub(0, 0.5000001, 0)).registry().friction() * aerodynamics.horizontalAirResistance() :
+ aerodynamics.horizontalAirResistance();
+ double gravity = entityFlying ? 0 : aerodynamics.gravity();
+ double gravityDrag = entityFlying ? 0.6 : aerodynamics.verticalAirResistance();
+
+ double x = currentVelocity.x() * drag, z = currentVelocity.z() * drag;
+ double y = !entityNoGravity ? ((currentVelocity.y() - gravity) * gravityDrag) : currentVelocity.y();
+ return new Vec(Math.abs(x) < Vec.EPSILON ? 0 : x, Math.abs(y) < Vec.EPSILON ? 0 : y, Math.abs(z) < Vec.EPSILON ? 0 : z);
+ }
+
+ private PhysicsUtils() {}
+}
diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java
index bcc7c4abc..a315431bb 100644
--- a/src/main/java/net/minestom/server/entity/Entity.java
+++ b/src/main/java/net/minestom/server/entity/Entity.java
@@ -79,13 +79,17 @@ import java.util.function.UnaryOperator;
*/
public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler
- * Unit: 1/tick
- */
- protected double gravityDragPerTick;
- /**
- * Acceleration on the Y axle due to gravity
- *
- * Unit: blocks/tick
- */
- protected double gravityAcceleration;
+ private Aerodynamics aerodynamics;
protected int gravityTickCount; // Number of tick where gravity tick was applied
private final int id;
@@ -192,8 +185,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
Entity.ENTITY_BY_ID.put(id, this);
Entity.ENTITY_BY_UUID.put(uuid, this);
- this.gravityAcceleration = entityType.registry().acceleration();
- this.gravityDragPerTick = entityType.registry().drag();
+ EntitySpawnType type = entityType.registry().spawnType();
+ this.aerodynamics = new Aerodynamics(entityType.registry().acceleration(),
+ type == EntitySpawnType.LIVING || type == EntitySpawnType.PLAYER ? 0.91 : 0.98, 1 - entityType.registry().drag());
final ServerProcess process = MinecraftServer.process();
if (process != null) {
@@ -529,7 +523,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
this.entityType = entityType;
this.metadata = new Metadata(this);
this.entityMeta = EntityTypeImpl.createMeta(entityType, this, this.metadata);
-
+ EntitySpawnType type = entityType.registry().spawnType();
+ this.aerodynamics = aerodynamics.withAirResistance(type == EntitySpawnType.LIVING ||
+ type == EntitySpawnType.PLAYER ? 0.91 : 0.98, 1 - entityType.registry().drag());
Set