From 8675a90e9c5d949c2b8e9ea8682c3919773733d6 Mon Sep 17 00:00:00 2001 From: Felix Cravic Date: Sun, 29 Nov 2020 23:14:21 +0100 Subject: [PATCH] Added the NavigableEntity interface to use the pathfinder (instead of hardcoding it in EntityCreature) --- .../server/entity/EntityCreature.java | 198 ++++------------ .../entity/pathfinding/NavigableEntity.java | 219 ++++++++++++++++++ .../entity/pathfinding/PFPathingEntity.java | 27 ++- .../server/utils/position/PositionUtils.java | 17 ++ 4 files changed, 296 insertions(+), 165 deletions(-) create mode 100644 src/main/java/net/minestom/server/entity/pathfinding/NavigableEntity.java create mode 100644 src/main/java/net/minestom/server/utils/position/PositionUtils.java diff --git a/src/main/java/net/minestom/server/entity/EntityCreature.java b/src/main/java/net/minestom/server/entity/EntityCreature.java index 567ddb0e2..d2b9d6f7f 100644 --- a/src/main/java/net/minestom/server/entity/EntityCreature.java +++ b/src/main/java/net/minestom/server/entity/EntityCreature.java @@ -1,28 +1,22 @@ package net.minestom.server.entity; import com.extollit.gaming.ai.path.HydrazinePathFinder; -import com.extollit.gaming.ai.path.SchedulingPriority; import com.extollit.gaming.ai.path.model.IPath; import net.minestom.server.attribute.Attributes; -import net.minestom.server.collision.CollisionUtils; import net.minestom.server.entity.ai.GoalSelector; import net.minestom.server.entity.ai.TargetSelector; +import net.minestom.server.entity.pathfinding.NavigableEntity; import net.minestom.server.entity.pathfinding.PFPathingEntity; import net.minestom.server.event.entity.EntityAttackEvent; import net.minestom.server.event.item.ArmorEquipEvent; -import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; -import net.minestom.server.instance.WorldBorder; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.server.play.EntityEquipmentPacket; import net.minestom.server.network.packet.server.play.EntityMovementPacket; import net.minestom.server.network.packet.server.play.SpawnLivingEntityPacket; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.utils.Position; -import net.minestom.server.utils.Vector; -import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.time.TimeUnit; -import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,7 +25,10 @@ import java.util.List; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; -public abstract class EntityCreature extends LivingEntity { +public abstract class EntityCreature extends LivingEntity implements NavigableEntity { + + // TODO all pathfinding requests should be process in another thread + private final ReentrantLock pathLock = new ReentrantLock(); private final PFPathingEntity pathingEntity = new PFPathingEntity(this); private HydrazinePathFinder pathFinder; @@ -53,9 +50,6 @@ public abstract class EntityCreature extends LivingEntity { private ItemStack leggings; private ItemStack boots; - // TODO all pathfinding requests should be process in another thread - private final ReentrantLock pathLock = new ReentrantLock(); - public EntityCreature(@NotNull EntityType entityType, @NotNull Position spawnPosition) { super(entityType, spawnPosition); @@ -129,27 +123,7 @@ public abstract class EntityCreature extends LivingEntity { // Path finding - { - if (pathPosition != null) { - this.pathLock.lock(); - this.path = pathFinder.updatePathFor(pathingEntity); - - if (path != null) { - final float speed = getAttributeValue(Attributes.MOVEMENT_SPEED); - final Position targetPosition = pathingEntity.getTargetPosition(); - if (targetPosition != null) { - moveTowards(targetPosition, speed); - } - } else { - if (pathPosition != null) { - this.pathPosition = null; - this.pathFinder.reset(); - } - } - - this.pathLock.unlock(); - } - } + pathFindingTick(getAttributeValue(Attributes.MOVEMENT_SPEED)); super.update(time); } @@ -339,151 +313,59 @@ public abstract class EntityCreature extends LivingEntity { attack(target, false); } - public void jump(float height) { - // FIXME magic value - final Vector velocity = new Vector(0, height * 2.5f, 0); - setVelocity(velocity); - } - - /** - * Retrieves the path to {@code position} and ask the entity to follow the path. - *

- * Can be set to null to reset the pathfinder. - *

- * The position is cloned, if you want the entity to continually follow this position object - * you need to call this when you want the path to update. - * - * @param position the position to find the path to, null to reset the pathfinder - * @return true if a path has been found - */ - public boolean setPathTo(@Nullable Position position) { - if (position != null && getPathPosition() != null && position.isSimilar(getPathPosition())) { - // Tried to set path to the same target position - return false; - } - - if (pathFinder == null) { - // Unexpected error - return false; - } - + @Override + public void pathFindingTick(float speed) { this.pathLock.lock(); - - this.pathFinder.reset(); - if (position == null) { - this.pathLock.unlock(); - return false; - } - - // Can't path outside of the world border - final WorldBorder worldBorder = instance.getWorldBorder(); - if (!worldBorder.isInside(position)) { - this.pathLock.unlock(); - return false; - } - - // Can't path in an unloaded chunk - final Chunk chunk = instance.getChunkAt(position); - if (!ChunkUtils.isLoaded(chunk)) { - this.pathLock.unlock(); - return false; - } - - final Position targetPosition = position.copy(); - - this.path = pathFinder.initiatePathTo(targetPosition.getX(), targetPosition.getY(), targetPosition.getZ()); + NavigableEntity.super.pathFindingTick(speed); this.pathLock.unlock(); - final boolean success = path != null; - this.pathPosition = success ? targetPosition : null; - - return success; } - /** - * Gets the target pathfinder position. - * - * @return the target pathfinder position, null if there is no one - */ + @Override + public boolean setPathTo(@Nullable Position position) { + this.pathLock.lock(); + final boolean result = NavigableEntity.super.setPathTo(position); + this.pathLock.unlock(); + return result; + } + @Nullable + @Override public Position getPathPosition() { return pathPosition; } - /** - * Changes the pathfinding priority for this entity. - * - * @param schedulingPriority the new scheduling priority - * @see Scheduling Priority - */ - public void setPathfindingPriority(@NotNull SchedulingPriority schedulingPriority) { - this.pathFinder.schedulingPriority(schedulingPriority); + @Override + public void setPathPosition(Position pathPosition) { + this.pathPosition = pathPosition; } - /** - * Used to move the entity toward {@code direction} in the X and Z axis - * Gravity is still applied but the entity will not attempt to jump - * Also update the yaw/pitch of the entity to look along 'direction' - * - * @param direction the targeted position - * @param speed define how far the entity will move - */ - public void moveTowards(@NotNull Position direction, float speed) { - Check.notNull(direction, "The direction cannot be null"); - - final float currentX = position.getX(); - final float currentY = position.getY(); - final float currentZ = position.getZ(); - - final float targetX = direction.getX(); - final float targetY = direction.getY(); - final float targetZ = direction.getZ(); - - final float dx = targetX - currentX; - final float dy = targetY - currentY; - final float dz = targetZ - currentZ; - - // the purpose of these few lines is to slow down entities when they reach their destination - final float distSquared = dx * dx + dy * dy + dz * dz; - if (speed > distSquared) { - speed = distSquared; - } - - final float radians = (float) Math.atan2(dz, dx); - final float speedX = (float) (Math.cos(radians) * speed); - final float speedY = dy * speed; - final float speedZ = (float) (Math.sin(radians) * speed); - - lookAlong(dx, direction.getY(), dz); - - Position newPosition = new Position(); - Vector newVelocityOut = new Vector(); - - // Prevent ghosting - this.onGround = CollisionUtils.handlePhysics(this, new Vector(speedX, speedY, speedZ), newPosition, newVelocityOut); - - // Will move the entity during Entity#tick - getPosition().copyCoordinates(newPosition); + @Nullable + @Override + public IPath getPath() { + return path; + } + + @Override + public void setPath(IPath path) { + this.path = path; } - /** - * Gets the pathing entity. - *

- * Used by the pathfinder. - * - * @return the pathing entity - */ @NotNull + @Override public PFPathingEntity getPathingEntity() { return pathingEntity; } - private void lookAlong(float dx, float dy, float dz) { - final float horizontalAngle = (float) Math.atan2(dz, dx); - final float yaw = (float) (horizontalAngle * (180.0 / Math.PI)) - 90; - final float pitch = (float) Math.atan2(dy, Math.max(Math.abs(dx), Math.abs(dz))); + @Nullable + @Override + public HydrazinePathFinder getPathFinder() { + return pathFinder; + } - getPosition().setYaw(yaw); - getPosition().setPitch(pitch); + @NotNull + @Override + public Entity getNavigableEntity() { + return this; } private ItemStack getEquipmentItem(@NotNull ItemStack itemStack, @NotNull ArmorEquipEvent.ArmorSlot armorSlot) { diff --git a/src/main/java/net/minestom/server/entity/pathfinding/NavigableEntity.java b/src/main/java/net/minestom/server/entity/pathfinding/NavigableEntity.java new file mode 100644 index 000000000..06a31dc0b --- /dev/null +++ b/src/main/java/net/minestom/server/entity/pathfinding/NavigableEntity.java @@ -0,0 +1,219 @@ +package net.minestom.server.entity.pathfinding; + +import com.extollit.gaming.ai.path.HydrazinePathFinder; +import com.extollit.gaming.ai.path.SchedulingPriority; +import com.extollit.gaming.ai.path.model.IPath; +import net.minestom.server.collision.CollisionUtils; +import net.minestom.server.entity.Entity; +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.position.PositionUtils; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents an entity which can use the pathfinder. + */ +public interface NavigableEntity { + + /** + * Used to move the entity toward {@code direction} in the X and Z axis + * Gravity is still applied but the entity will not attempt to jump + * Also update the yaw/pitch of the entity to look along 'direction' + * + * @param direction the targeted position + * @param speed define how far the entity will move + */ + default void moveTowards(@NotNull Position direction, float speed) { + Check.notNull(direction, "The direction cannot be null"); + + final Position position = getNavigableEntity().getPosition(); + + final float currentX = position.getX(); + final float currentY = position.getY(); + final float currentZ = position.getZ(); + + final float targetX = direction.getX(); + final float targetY = direction.getY(); + final float targetZ = direction.getZ(); + + final float dx = targetX - currentX; + final float dy = targetY - currentY; + final float dz = targetZ - currentZ; + + // the purpose of these few lines is to slow down entities when they reach their destination + final float distSquared = dx * dx + dy * dy + dz * dz; + if (speed > distSquared) { + speed = distSquared; + } + + final float radians = (float) Math.atan2(dz, dx); + final float speedX = (float) (Math.cos(radians) * speed); + final float speedY = dy * speed; + final float speedZ = (float) (Math.sin(radians) * speed); + + // Update 'position' view + PositionUtils.lookAlong(position, dx, direction.getY(), dz); + + Position newPosition = new Position(); + Vector newVelocityOut = new Vector(); + + // Prevent ghosting + CollisionUtils.handlePhysics(getNavigableEntity(), + new Vector(speedX, speedY, speedZ), + newPosition, newVelocityOut); + + // Will move the entity during Entity#tick + position.copyCoordinates(newPosition); + } + + default void jump(float height) { + // FIXME magic value + final Vector velocity = new Vector(0, height * 2.5f, 0); + getNavigableEntity().setVelocity(velocity); + } + + /** + * Retrieves the path to {@code position} and ask the entity to follow the path. + *

+ * Can be set to null to reset the pathfinder. + *

+ * The position is cloned, if you want the entity to continually follow this position object + * you need to call this when you want the path to update. + * + * @param position the position to find the path to, null to reset the pathfinder + * @return true if a path has been found + */ + default boolean setPathTo(@Nullable Position position) { + if (position != null && getPathPosition() != null && position.isSimilar(getPathPosition())) { + // Tried to set path to the same target position + return false; + } + + final Instance instance = getNavigableEntity().getInstance(); + final HydrazinePathFinder pathFinder = getPathFinder(); + + if (pathFinder == null) { + // Unexpected error + return false; + } + + pathFinder.reset(); + if (position == null) { + return false; + } + + // Can't path outside of the world border + final WorldBorder worldBorder = instance.getWorldBorder(); + if (!worldBorder.isInside(position)) { + return false; + } + + // Can't path in an unloaded chunk + final Chunk chunk = instance.getChunkAt(position); + if (!ChunkUtils.isLoaded(chunk)) { + return false; + } + + final Position targetPosition = position.copy(); + + final IPath path = pathFinder.initiatePathTo(targetPosition.getX(), targetPosition.getY(), targetPosition.getZ()); + setPath(path); + + final boolean success = path != null; + setPathPosition(success ? targetPosition : null); + + return success; + } + + default void pathFindingTick(float speed) { + final Position pathPosition = getPathPosition(); + if (pathPosition != null) { + final HydrazinePathFinder pathFinder = getPathFinder(); + + IPath path = pathFinder.updatePathFor(getPathingEntity()); + setPath(path); + + if (path != null) { + final Position targetPosition = getPathingEntity().getTargetPosition(); + if (targetPosition != null) { + moveTowards(targetPosition, speed); + } + } else { + if (pathPosition != null) { + setPathPosition(null); + pathFinder.reset(); + } + } + } + } + + /** + * Changes the pathfinding priority for this entity. + * + * @param schedulingPriority the new scheduling priority + * @see Scheduling Priority + */ + default void setPathfindingPriority(@NotNull SchedulingPriority schedulingPriority) { + final HydrazinePathFinder pathFinder = getPathFinder(); + if (pathFinder != null) { + pathFinder.schedulingPriority(schedulingPriority); + } + } + + /** + * Gets the target pathfinder position. + * + * @return the target pathfinder position, null if there is no one + */ + @Nullable + Position getPathPosition(); + + /** + * Changes the position this element is trying to reach. + * + * @param path the new current path position + * @deprecated Please use {@link #setPathTo(Position)} + */ + @Deprecated + void setPathPosition(@Nullable Position path); + + @Nullable + IPath getPath(); + + void setPath(@Nullable IPath path); + + /** + * Gets the pathing entity. + *

+ * Used by the pathfinder. + * + * @return the pathing entity + */ + @NotNull + PFPathingEntity getPathingEntity(); + + /** + * Gets the assigned pathfinder. + *

+ * Can be null if the navigable element hasn't been assigned to an {@link Instance} yet. + * + * @return the current pathfinder, null if none + */ + @Nullable + HydrazinePathFinder getPathFinder(); + + /** + * Gets the entity concerned by this navigable implementation. + * + * @return the navigable entity + */ + @NotNull + Entity getNavigableEntity(); + +} diff --git a/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java b/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java index 4ffa8b9e2..f76b981ee 100644 --- a/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java +++ b/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java @@ -4,13 +4,17 @@ import com.extollit.gaming.ai.path.model.Gravitation; import com.extollit.gaming.ai.path.model.IPathingEntity; import com.extollit.gaming.ai.path.model.Passibility; import com.extollit.linalg.immutable.Vec3d; +import net.minestom.server.attribute.Attribute; import net.minestom.server.attribute.Attributes; -import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; import net.minestom.server.utils.Position; +import org.jetbrains.annotations.NotNull; public class PFPathingEntity implements IPathingEntity { - private final EntityCreature entity; + private final NavigableEntity navigableEntity; + private final Entity entity; private float searchRange; private Position targetPosition; @@ -26,9 +30,11 @@ public class PFPathingEntity implements IPathingEntity { private boolean avoidsDoorways; private boolean opensDoors; - public PFPathingEntity(EntityCreature entity) { - this.entity = entity; - this.searchRange = entity.getAttributeValue(Attributes.FOLLOW_RANGE); + public PFPathingEntity(NavigableEntity navigableEntity) { + this.navigableEntity = navigableEntity; + this.entity = navigableEntity.getNavigableEntity(); + + this.searchRange = getAttributeValue(Attributes.FOLLOW_RANGE); } public Position getTargetPosition() { @@ -131,7 +137,7 @@ public class PFPathingEntity implements IPathingEntity { return new Capabilities() { @Override public float speed() { - return entity.getAttributeValue(Attributes.MOVEMENT_SPEED); + return getAttributeValue(Attributes.MOVEMENT_SPEED); } @Override @@ -190,7 +196,7 @@ public class PFPathingEntity implements IPathingEntity { final float entityY = entity.getPosition().getY(); if (entityY < y) { - entity.jump(1); + navigableEntity.jump(1); } } @@ -209,4 +215,11 @@ public class PFPathingEntity implements IPathingEntity { public float height() { return entity.getBoundingBox().getHeight(); } + + private float getAttributeValue(@NotNull Attribute attribute) { + if (entity instanceof LivingEntity) { + return ((LivingEntity) entity).getAttributeValue(attribute); + } + return 0f; + } } diff --git a/src/main/java/net/minestom/server/utils/position/PositionUtils.java b/src/main/java/net/minestom/server/utils/position/PositionUtils.java new file mode 100644 index 000000000..8a421348c --- /dev/null +++ b/src/main/java/net/minestom/server/utils/position/PositionUtils.java @@ -0,0 +1,17 @@ +package net.minestom.server.utils.position; + +import net.minestom.server.utils.Position; +import org.jetbrains.annotations.NotNull; + +public final class PositionUtils { + + public static void lookAlong(@NotNull Position position, float dx, float dy, float dz) { + final float horizontalAngle = (float) Math.atan2(dz, dx); + final float yaw = (float) (horizontalAngle * (180.0 / Math.PI)) - 90; + final float pitch = (float) Math.atan2(dy, Math.max(Math.abs(dx), Math.abs(dz))); + + position.setYaw(yaw); + position.setPitch(pitch); + } + +}