mirror of
https://github.com/Minestom/Minestom.git
synced 2025-02-16 04:11:39 +01:00
feat: add aerodynamics record and the capability to set custom horizontal air resistance (#2053)
* feat: add aerodynamics record and the ability to set horizontal drag * feat: entity physics simulation overhaul * fix: made physics utils private, renamed to match other utils * chore: separate concept of chunks and tps from PhysicsUtils, remove bad PhysicsResult constants * chore: remove synchronization from PhysicsUtils, SYNCHRONIZE_ONLY_ENTITIES collection > set * chore: remove extra vec allocations * chore: improved flyingVelocity test * chore: add all entities with client side prediction to SYNCRHONIZE_ONLY_ENTITIES, refactor velocity --------- Co-authored-by: iam <iam4722202468@users.noreply.github.com>
This commit is contained in:
parent
d04e9e3e71
commit
f034296f28
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
* <p>
|
||||
* 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() {}
|
||||
}
|
@ -79,13 +79,17 @@ import java.util.function.UnaryOperator;
|
||||
*/
|
||||
public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler<EntityEvent>, Taggable,
|
||||
PermissionHandler, HoverEventSource<ShowEntity>, Sound.Emitter, Shape {
|
||||
|
||||
private static final int VELOCITY_UPDATE_INTERVAL = 1;
|
||||
|
||||
private static final Int2ObjectSyncMap<Entity> ENTITY_BY_ID = Int2ObjectSyncMap.hashmap();
|
||||
private static final Map<UUID, Entity> ENTITY_BY_UUID = new ConcurrentHashMap<>();
|
||||
private static final AtomicInteger LAST_ENTITY_ID = new AtomicInteger();
|
||||
|
||||
// Certain entities should only have their position packets sent during synchronization
|
||||
private static final Set<EntityType> SYNCHRONIZE_ONLY_ENTITIES = Set.of(EntityType.ITEM, EntityType.FALLING_BLOCK,
|
||||
EntityType.ARROW, EntityType.SPECTRAL_ARROW, EntityType.TRIDENT, EntityType.LLAMA_SPIT, EntityType.WIND_CHARGE,
|
||||
EntityType.FISHING_BOBBER, EntityType.SNOWBALL, EntityType.EGG, EntityType.ENDER_PEARL, EntityType.POTION,
|
||||
EntityType.EYE_OF_ENDER, EntityType.DRAGON_FIREBALL, EntityType.FIREBALL, EntityType.SMALL_FIREBALL,
|
||||
EntityType.TNT);
|
||||
|
||||
private final CachedPacket destroyPacketCache = new CachedPacket(() -> new DestroyEntitiesPacket(getEntityId()));
|
||||
|
||||
protected Instance instance;
|
||||
@ -96,7 +100,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
protected boolean onGround;
|
||||
|
||||
protected BoundingBox boundingBox;
|
||||
private PhysicsResult lastPhysicsResult = null;
|
||||
private PhysicsResult previousPhysicsResult = null;
|
||||
|
||||
protected Entity vehicle;
|
||||
|
||||
@ -106,18 +110,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
protected boolean hasPhysics = true;
|
||||
protected boolean hasCollision = true;
|
||||
|
||||
/**
|
||||
* The amount of drag applied on the Y axle.
|
||||
* <p>
|
||||
* Unit: 1/tick
|
||||
*/
|
||||
protected double gravityDragPerTick;
|
||||
/**
|
||||
* Acceleration on the Y axle due to gravity
|
||||
* <p>
|
||||
* 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<Player> viewers = new HashSet<>(getViewers());
|
||||
getViewers().forEach(this::updateOldViewer);
|
||||
viewers.forEach(this::updateNewViewer);
|
||||
@ -559,8 +555,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
|
||||
// Entity tick
|
||||
{
|
||||
// Cache the number of "gravity tick"
|
||||
velocityTick();
|
||||
// handle position and velocity updates
|
||||
movementTick();
|
||||
|
||||
// handle block contacts
|
||||
touchTick();
|
||||
@ -580,117 +576,28 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
}
|
||||
}
|
||||
|
||||
private void velocityTick() {
|
||||
@ApiStatus.Internal
|
||||
protected void movementTick() {
|
||||
this.gravityTickCount = onGround ? 0 : gravityTickCount + 1;
|
||||
if (vehicle != null) return;
|
||||
|
||||
final boolean noGravity = hasNoGravity();
|
||||
final boolean hasVelocity = hasVelocity();
|
||||
if (!hasVelocity && noGravity) {
|
||||
return;
|
||||
boolean entityIsPlayer = this instanceof Player;
|
||||
boolean entityFlying = entityIsPlayer && ((Player) this).isFlying();
|
||||
PhysicsResult physicsResult = PhysicsUtils.simulateMovement(position, velocity.div(ServerFlag.SERVER_TICKS_PER_SECOND), boundingBox,
|
||||
instance.getWorldBorder(), instance, aerodynamics, hasNoGravity(), hasPhysics, onGround, entityFlying, previousPhysicsResult);
|
||||
this.previousPhysicsResult = physicsResult;
|
||||
|
||||
Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, physicsResult.newPosition());
|
||||
if (!ChunkUtils.isLoaded(finalChunk)) return;
|
||||
|
||||
velocity = physicsResult.newVelocity().mul(ServerFlag.SERVER_TICKS_PER_SECOND);
|
||||
onGround = physicsResult.isOnGround();
|
||||
boolean shouldSendVelocity = !entityIsPlayer && hasVelocity();
|
||||
|
||||
if (!PlayerUtils.isSocketClient(this)) {
|
||||
refreshPosition(physicsResult.newPosition(), true, !SYNCHRONIZE_ONLY_ENTITIES.contains(entityType));
|
||||
if (shouldSendVelocity) sendPacketToViewers(getVelocityPacket());
|
||||
}
|
||||
final float tps = MinecraftServer.TICK_PER_SECOND;
|
||||
final Pos positionBeforeMove = getPosition();
|
||||
final Vec currentVelocity = getVelocity();
|
||||
final boolean wasOnGround = this.onGround;
|
||||
final Vec deltaPos = currentVelocity.div(tps);
|
||||
|
||||
final Pos newPosition;
|
||||
final Vec newVelocity;
|
||||
if (this.hasPhysics) {
|
||||
final var physicsResult = CollisionUtils.handlePhysics(this, deltaPos, lastPhysicsResult);
|
||||
this.lastPhysicsResult = physicsResult;
|
||||
if (!PlayerUtils.isSocketClient(this))
|
||||
this.onGround = physicsResult.isOnGround();
|
||||
|
||||
newPosition = physicsResult.newPosition();
|
||||
newVelocity = physicsResult.newVelocity();
|
||||
} else {
|
||||
newVelocity = deltaPos;
|
||||
newPosition = position.add(currentVelocity.div(20));
|
||||
}
|
||||
|
||||
// World border collision
|
||||
final Pos finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
|
||||
final boolean positionChanged = !finalVelocityPosition.samePoint(position);
|
||||
final boolean isPlayer = this instanceof Player;
|
||||
final boolean flying = isPlayer && ((Player) this).isFlying();
|
||||
if (!positionChanged) {
|
||||
if (flying) {
|
||||
this.velocity = Vec.ZERO;
|
||||
return;
|
||||
} else if (hasVelocity || newVelocity.isZero()) {
|
||||
this.velocity = noGravity ? Vec.ZERO : new Vec(
|
||||
0,
|
||||
-gravityAcceleration * tps * (1 - gravityDragPerTick),
|
||||
0
|
||||
);
|
||||
if (this.ticks % VELOCITY_UPDATE_INTERVAL == 0) {
|
||||
if (!isPlayer && !this.lastVelocityWasZero) {
|
||||
sendPacketToViewers(getVelocityPacket());
|
||||
this.lastVelocityWasZero = !hasVelocity;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition);
|
||||
if (!ChunkUtils.isLoaded(finalChunk)) {
|
||||
// Entity shouldn't be updated when moving in an unloaded chunk
|
||||
return;
|
||||
}
|
||||
|
||||
if (positionChanged) {
|
||||
if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) {
|
||||
// TODO find other exceptions
|
||||
this.previousPosition = this.position;
|
||||
this.position = finalVelocityPosition;
|
||||
refreshCoordinate(finalVelocityPosition);
|
||||
} else {
|
||||
if (!PlayerUtils.isSocketClient(this))
|
||||
refreshPosition(finalVelocityPosition, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Update velocity
|
||||
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)) {
|
||||
sendPacketToViewers(getVelocityPacket());
|
||||
this.lastVelocityWasZero = !hasVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateVelocity(boolean wasOnGround, boolean flying, Pos positionBeforeMove, Vec newVelocity) {
|
||||
EntitySpawnType type = entityType.registry().spawnType();
|
||||
final double airDrag = type == EntitySpawnType.LIVING || type == EntitySpawnType.PLAYER ? 0.91 : 0.98;
|
||||
final double drag;
|
||||
if (wasOnGround) {
|
||||
final Chunk chunk = ChunkUtils.retrieve(instance, currentChunk, position);
|
||||
synchronized (chunk) {
|
||||
drag = chunk.getBlock(positionBeforeMove.sub(0, 0.5000001, 0)).registry().friction() * airDrag;
|
||||
}
|
||||
} else drag = airDrag;
|
||||
|
||||
double gravity = flying ? 0 : gravityAcceleration;
|
||||
double gravityDrag = flying ? 0.6 : (1 - gravityDragPerTick);
|
||||
|
||||
this.velocity = newVelocity
|
||||
// Apply gravity and drag
|
||||
.apply((x, y, z) -> new Vec(
|
||||
x * drag,
|
||||
!hasNoGravity() ? (y - gravity) * gravityDrag : y,
|
||||
z * drag
|
||||
))
|
||||
// Convert from block/tick to block/sec
|
||||
.mul(MinecraftServer.TICK_PER_SECOND)
|
||||
// Prevent infinitely decreasing velocity
|
||||
.apply(Vec.Operator.EPSILON);
|
||||
}
|
||||
|
||||
private void touchTick() {
|
||||
@ -972,21 +879,21 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the gravity drag per tick.
|
||||
* Gets the aerodynamics; how the entity behaves in the air.
|
||||
*
|
||||
* @return the gravity drag per tick in block
|
||||
* @return the aerodynamic properties this entity is using
|
||||
*/
|
||||
public double getGravityDragPerTick() {
|
||||
return gravityDragPerTick;
|
||||
public @NotNull Aerodynamics getAerodynamics() {
|
||||
return aerodynamics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the gravity acceleration.
|
||||
* Sets the aerodynamics; how the entity behaves in the air.
|
||||
*
|
||||
* @return the gravity acceleration in block
|
||||
* @param aerodynamics the new aerodynamic properties
|
||||
*/
|
||||
public double getGravityAcceleration() {
|
||||
return gravityAcceleration;
|
||||
public void setAerodynamics(@NotNull Aerodynamics aerodynamics) {
|
||||
this.aerodynamics = aerodynamics;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -998,18 +905,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
return gravityTickCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the gravity of the entity.
|
||||
*
|
||||
* @param gravityDragPerTick the gravity drag per tick in block
|
||||
* @param gravityAcceleration the gravity acceleration in block
|
||||
* @see <a href="https://minecraft.wiki/w/Entity#Motion_of_entities">Entities motion</a>
|
||||
*/
|
||||
public void setGravity(double gravityDragPerTick, double gravityAcceleration) {
|
||||
this.gravityDragPerTick = gravityDragPerTick;
|
||||
this.gravityAcceleration = gravityAcceleration;
|
||||
}
|
||||
|
||||
public double getDistance(@NotNull Point point) {
|
||||
return getPosition().distance(point);
|
||||
}
|
||||
@ -1357,14 +1252,14 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
* @param newPosition the new position
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView) {
|
||||
public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView, boolean sendPackets) {
|
||||
final var previousPosition = this.position;
|
||||
final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition;
|
||||
if (position.equals(lastSyncedPosition)) return;
|
||||
this.position = position;
|
||||
this.previousPosition = previousPosition;
|
||||
if (!position.samePoint(previousPosition)) refreshCoordinate(position);
|
||||
if (nextSynchronizationTick <= ticks + 1) {
|
||||
if (nextSynchronizationTick <= ticks + 1 || !sendPackets) {
|
||||
// The entity will be synchronized at the end of its tick
|
||||
// not returning here will duplicate position packets
|
||||
return;
|
||||
@ -1399,6 +1294,11 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
|
||||
this.lastSyncedPosition = position;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView) {
|
||||
refreshPosition(newPosition, ignoreView, true);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void refreshPosition(@NotNull final Pos newPosition) {
|
||||
refreshPosition(newPosition, false);
|
||||
|
@ -29,6 +29,7 @@ import java.util.stream.Stream;
|
||||
public class EntityProjectile extends Entity {
|
||||
|
||||
private final Entity shooter;
|
||||
private boolean wasStuck;
|
||||
|
||||
public EntityProjectile(@Nullable Entity shooter, @NotNull EntityType entityType) {
|
||||
super(entityType);
|
||||
@ -99,12 +100,12 @@ public class EntityProjectile extends Entity {
|
||||
this.velocity = Vec.ZERO;
|
||||
sendPacketToViewersAndSelf(getVelocityPacket());
|
||||
setNoGravity(true);
|
||||
wasStuck = true;
|
||||
} else {
|
||||
if (!super.onGround) {
|
||||
return;
|
||||
}
|
||||
if (!wasStuck) return;
|
||||
wasStuck = false;
|
||||
setNoGravity(super.onGround);
|
||||
super.onGround = false;
|
||||
setNoGravity(false);
|
||||
EventDispatcher.call(new ProjectileUncollideEvent(this));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.minestom.server.entity;
|
||||
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.testing.Env;
|
||||
import net.minestom.testing.EnvTest;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
@ -109,7 +110,7 @@ public class EntityVelocityIntegrationTest {
|
||||
var player = env.createPlayer(instance, new Pos(0, 42, 0));
|
||||
env.tick();
|
||||
|
||||
final double epsilon = 0.00001;
|
||||
final double epsilon = 0.000001;
|
||||
|
||||
assertEquals(player.getVelocity().y(), -1.568, epsilon);
|
||||
double previousVelocity = player.getVelocity().y();
|
||||
@ -118,7 +119,7 @@ public class EntityVelocityIntegrationTest {
|
||||
env.tick();
|
||||
|
||||
// Every tick, the y velocity is multiplied by 0.6, and after 27 ticks it should be 0
|
||||
for (int i = 0; i < 27; i++) {
|
||||
for (int i = 0; i < 22; i++) {
|
||||
assertEquals(player.getVelocity().y(), previousVelocity * 0.6, epsilon);
|
||||
previousVelocity = player.getVelocity().y();
|
||||
env.tick();
|
||||
@ -172,6 +173,8 @@ public class EntityVelocityIntegrationTest {
|
||||
viewerConnection.connect(instance, new Pos(1, 40, 1)).join();
|
||||
var entity = new Entity(EntityType.ZOMBIE);
|
||||
entity.setInstance(instance, new Pos(0,40,0)).join();
|
||||
instance.setBlock(new Vec(0, 39, 0), Block.STONE);
|
||||
env.tick(); // Tick because the entity is in the air, they'll send velocity from gravity
|
||||
|
||||
AtomicInteger i = new AtomicInteger();
|
||||
BooleanSupplier tickLoopCondition = () -> i.getAndIncrement() < Math.max(VELOCITY_UPDATE_INTERVAL, 1);
|
||||
|
Loading…
Reference in New Issue
Block a user