Cleanup, pathfind demo, and PathfinderImpl improvements

This commit is contained in:
KrystilizeNevaDies 2022-03-12 17:32:01 +10:00
parent 6b57058a6b
commit 48cf157370
9 changed files with 276 additions and 65 deletions

View File

@ -52,7 +52,7 @@ public class Main {
commandManager.register(new AutoViewCommand());
commandManager.register(new SaveCommand());
commandManager.register(new GamemodeCommand());
commandManager.register(new PathfindCommand());
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED)));

View File

@ -31,6 +31,7 @@ import net.minestom.server.item.metadata.BundleMeta;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.world.DimensionType;
@ -121,11 +122,13 @@ public class PlayerInit {
});
static {
DimensionType fullbright = DimensionType.builder(NamespaceID.from("minestom:fullbright")).ambientLight(2.0F).build();
MinecraftServer.getDimensionTypeManager().addDimension(fullbright);
InstanceManager instanceManager = MinecraftServer.getInstanceManager();
ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo();
NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator();
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD);
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(fullbright);
instanceContainer.setChunkGenerator(chunkGeneratorDemo);
if (false) {

View File

@ -0,0 +1,49 @@
package net.minestom.demo.commands;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.utils.entity.EntityFinder;
import java.util.List;
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
public class PathfindCommand extends Command {
public PathfindCommand() {
super("pathfind");
addSyntax(
this::usageE2E,
Entity("from"),
Entity("to")
);
}
private void usageE2E(CommandSender sender, CommandContext context) {
EntityFinder from = context.get("from");
EntityFinder to = context.get("to");
List<Entity> fromList = from.find(sender);
Entity toEntity = to.findFirstEntity(sender);
if (toEntity == null) {
sender.sendMessage("No entity found");
return;
}
Pos destination = toEntity.getPosition();
destination = destination.add(toEntity.getBoundingBox().relativeStart());
for (Entity fromEntity : fromList) {
if (fromEntity instanceof EntityCreature creature) {
sender.sendMessage("Pathfinding from " + fromEntity + " to " + toEntity);
creature.getNavigator().setPathTo(destination);
}
}
}
}

View File

@ -18,6 +18,7 @@ public class SummonCommand extends Command {
private final ArgumentEntityType entity;
private final Argument<RelativeVec> pos;
private final Argument<EntityClass> entityClass;
private final Argument<Integer> count;
public SummonCommand() {
super("summon");
@ -32,14 +33,17 @@ public class SummonCommand extends Command {
entityClass = ArgumentType.Enum("class", EntityClass.class)
.setFormat(ArgumentEnum.Format.LOWER_CASED)
.setDefaultValue(EntityClass.CREATURE);
addSyntax(this::execute, entity, pos, entityClass);
setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon <type> <x> <y> <z> <class>"));
count = ArgumentType.Integer("count").setDefaultValue(1);
addSyntax(this::execute, entity, pos, entityClass, count);
setDefaultExecutor((sender, context) -> sender.sendMessage("Usage: /summon <type> <x> <y> <z> <class> <count>"));
}
private void execute(@NotNull CommandSender commandSender, @NotNull CommandContext commandContext) {
final Entity entity = commandContext.get(entityClass).instantiate(commandContext.get(this.entity));
//noinspection ConstantConditions - One couldn't possibly execute a command without being in an instance
entity.setInstance(((Player) commandSender).getInstance(), commandContext.get(pos).fromSender(commandSender));
for (int i = 0; i < commandContext.get(count); i++) {
final Entity entity = commandContext.get(entityClass).instantiate(commandContext.get(this.entity));
//noinspection ConstantConditions - One couldn't possibly execute a command without being in an instance
entity.setInstance(((Player) commandSender).getInstance(), commandContext.get(pos).fromSender(commandSender));
}
}
@SuppressWarnings("unused")
@ -54,6 +58,10 @@ public class SummonCommand extends Command {
}
public Entity instantiate(EntityType type) {
if (type == EntityType.BEE && this == CREATURE) {
// Flying creature for development
return new FlyingEntity(type);
}
return factory.newInstance(type);
}
}
@ -61,4 +69,11 @@ public class SummonCommand extends Command {
interface EntityFactory {
Entity newInstance(EntityType type);
}
private static final class FlyingEntity extends EntityCreature {
public FlyingEntity(@NotNull EntityType entityType) {
super(entityType);
setGravity(0.0, 0.0);
}
}
}

View File

@ -7,13 +7,14 @@ import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.position.PositionUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// TODO all pathfinding requests could be processed in another thread
/**
* Necessary object for all {@link NavigableEntity}.
*/
@ -23,7 +24,7 @@ public final class Navigator {
public Navigator(@NotNull Entity entity) {
this.entity = entity;
this.pathfinder = new PathfinderImpl(entity);
this.pathfinder = new PathfinderImpl(entity, null, null);
}
/**
@ -36,21 +37,34 @@ public final class Navigator {
*/
public void moveTowards(@NotNull Point direction, double speed) {
final Pos position = entity.getPosition();
final double dx = direction.x() - position.x();
final double dy = direction.y() - position.y();
final double dz = direction.z() - position.z();
// Find the direction
double dx = direction.x() - position.x();
double dy = direction.y() - position.y();
double dz = direction.z() - position.z();
// the purpose of these few lines is to slow down entities when they reach their destination
final double distSquared = dx * dx + dy * dy + dz * dz;
double distSquared = dx * dx + dy * dy + dz * dz;
if (speed > distSquared) {
speed = distSquared;
}
final double radians = Math.atan2(dz, dx);
final double speedX = Math.cos(radians) * speed;
final double speedY = dy * speed;
final double speedZ = Math.sin(radians) * speed;
final float yaw = PositionUtils.getLookYaw(dx, dz);
final float pitch = PositionUtils.getLookPitch(dx, dy, dz);
// Prevent ghosting
// Find the movement speed
double radians = Math.atan2(dz, dx);
double speedX = Math.cos(radians) * speed;
double speedY = dy * speed;
double speedZ = Math.sin(radians) * speed;
// Now calculate the new yaw/pitch
float oldYaw = position.yaw();
float oldPitch = position.pitch();
float newYaw = PositionUtils.getLookYaw(dx, dz);
float newPitch = PositionUtils.getLookPitch(dx, dy, dz);
// Average the pitch and yaw to avoid jittering
float yaw = PositionUtils.averageYaw(PositionUtils.averageYaw(oldYaw, newYaw), oldYaw);
float pitch = PositionUtils.averagePitch(PositionUtils.averagePitch(oldPitch, newPitch), oldPitch);
// Prevent ghosting, and refresh position
final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ));
this.entity.refreshPosition(physicsResult.newPosition().withView(yaw, pitch));
}
@ -71,6 +85,7 @@ public final class Navigator {
final Point next = this.pathfinder.nextPoint(entity.getPosition());
if (next != null) {
moveTowards(next, getAttributeValue(Attribute.MOVEMENT_SPEED));
PathfindUtils.debugParticle(next, Particle.WHITE_ASH);
final double entityY = entity.getPosition().y();
if (entityY < next.y()) {
jump(1);
@ -84,14 +99,13 @@ public final class Navigator {
* @return the target pathfinder position, null if there is no one
*/
public @Nullable Point getPathPosition() {
return null; // TODO
//return pathfinder.pathPosition;
return this.pathfinder.nextPoint(entity.getPosition());
}
private float getAttributeValue(@NotNull Attribute attribute) {
if (entity instanceof LivingEntity) {
return ((LivingEntity) entity).getAttributeValue(attribute);
}
return 0f;
return attribute.defaultValue();
}
}

View File

@ -3,15 +3,26 @@ package net.minestom.server.entity.pathfinding;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull;
final class PathfindUtils {
public static boolean isBlocked(@NotNull Point point, @NotNull BoundingBox box,
@NotNull Block.Getter getter, double entityPadding) {
@NotNull Block.Getter getter) {
Point relStart = box.relativeStart();
Point relEnd = box.relativeEnd();
relStart = relStart.mul(2, 0, 2).sub(entityPadding, 0, entityPadding);
relEnd = relEnd.mul(2, 0, 2).add(entityPadding, 0, entityPadding);
// Double for some reason (Necessary, I don't know why)
relStart = relStart.mul(2, 0, 2);
relEnd = relEnd.mul(2, 0, 2);
// Add a little padding so pathfinding is not against the very edge of the block
relStart = relStart.add(-0.1, 0, -0.1);
relEnd = relEnd.add(0.1, 0, 0.1);
// TODO: Use BlockIterator instead
return getter.getBlock(point.add(relStart.x(), relStart.y(), relStart.z())).isSolid() ||
getter.getBlock(point.add(relStart.x(), relStart.y(), relEnd.z())).isSolid() ||
getter.getBlock(point.add(relStart.x(), relEnd.y(), relStart.z())).isSolid() ||
@ -21,4 +32,11 @@ final class PathfindUtils {
getter.getBlock(point.add(relEnd.x(), relEnd.y(), relStart.z())).isSolid() ||
getter.getBlock(point.add(relEnd.x(), relEnd.y(), relEnd.z())).isSolid();
}
public static void debugParticle(@NotNull Point point, @NotNull Particle particle) {
// TODO: Remove this debug
PacketUtils.broadcastPacket(ParticleCreator.createParticlePacket(
particle, point.x(), point.y(), point.z(),
0, 0, 0, 1));
}
}

View File

@ -1,13 +1,37 @@
package net.minestom.server.entity.pathfinding;
import net.minestom.server.coordinate.Point;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* A pathfinder is used to maintain a dynamic path to a target.
* This dynamic path may change target, or be cancelled with {@link Pathfinder#updatePath(Point)}.
* It may be forcibly generated with {@link Pathfinder#forcePath(Point)}.
* And you may query the path with {@link Pathfinder#nextPoint(Point)}.
* <br><br>
* Implementations of this interface may be threaded, as long as the interface methods are thread-safe.
*/
public interface Pathfinder {
Point nextPoint(Point currentPoint);
/**
* This query will return the next point to reach the target, from the current position.
* @param currentPoint the current position
* @return the next point to reach the target, or null if a path does not currently exist
*/
@Nullable Point nextPoint(@NotNull Point currentPoint);
void updatePath(Point target);
/**
* This method will update the path to the given target, starting a new path if none exists.
* @param target the new target, or null to cancel the path
*/
void updatePath(@Nullable Point target);
List<Point> forcePath(Point target);
/**
* This method will force the path to the given target.
* @param target the new target
* @return the list of points to reach the target
*/
@Nullable List<@NotNull Point> forcePath(Point target);
}

View File

@ -1,29 +1,49 @@
package net.minestom.server.entity.pathfinding;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.block.BlockIterator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* A simplified, synchronized A* pathfinder.
* This pathfinder is focussed on stability, not performance.
* <p>
* If you would like to limit the entity to a
* </p>
*/
final class PathfinderImpl implements Pathfinder {
// The delta is used as a failsafe to avoid incorrect pathfinding, generally 0.01 is enough
private static final double DELTA = 0.01;
private final Entity entity;
private final @NotNull Entity entity;
private final @NotNull BlockedPredicate blocked;
private final @NotNull CostFunction cost;
volatile Point pathPosition;
volatile List<Point> path;
PathfinderImpl(Entity entity) {
/**
* Creates an instance of this pathfinder.
* @param entity the entity to find a path for
* @param blocked the predicate to check if a block is blocked, {@link BlockedPredicate#BLOCK_SOLID_BLOCKS} if null
* @param cost the cost function to calculate the cost of a block, {@link CostFunction#BLOCK_SPEED_FACTOR} if null
*/
PathfinderImpl(@NotNull Entity entity, @Nullable BlockedPredicate blocked, @Nullable CostFunction cost) {
this.entity = entity;
this.blocked = blocked == null ? BlockedPredicate.BLOCK_SOLID_BLOCKS : blocked;
this.cost = cost == null ? CostFunction.BLOCK_SPEED_FACTOR : cost;
}
@Override
public Point nextPoint(Point currentPoint) {
public Point nextPoint(@NotNull Point currentPoint) {
var path = this.path;
if (path == null || path.isEmpty()) {
return null;
@ -46,25 +66,28 @@ final class PathfinderImpl implements Pathfinder {
}
@Override
public void updatePath(Point target) {
public void updatePath(@Nullable Point target) {
var start = entity.getPosition();
var result = findPath(start, target, 1);
this.path = result.stream().toList();
this.path = findPath(start, target);
this.pathPosition = target;
}
@Override
public List<Point> forcePath(Point target) {
public @Nullable List<Point> forcePath(Point target) {
updatePath(target);
return path;
}
@Nullable Queue<Point> findPath(Point start, Point goal, double step) {
@Nullable List<Point> findPath(Point start, Point goal) {
// The step is half of the lowest dimension of the entity's bounding box
// This is used so that the entity will never be able to skip over any point in the path
BoundingBox box = entity.getBoundingBox();
double step = Math.min(box.width(), Math.min(box.height(), box.depth())) / 2;
// The distance cost is the distance between the current point and the goal, plus the cost of the current point
Comparator<Point> distanceCost = Comparator.comparingDouble(p ->
p.distance(start) +
p.distance(goal) +
getCost(p, p)
);
p.distance(start) + p.distance(goal) + cost.getCost(entity, p, p));
// The queue of nodes to be evaluated next
Queue<Point> next = new PriorityQueue<>(distanceCost);
Set<Point> nextSet = new HashSet<>();
@ -81,9 +104,7 @@ final class PathfinderImpl implements Pathfinder {
nextSet.remove(current);
// TODO: Remove this debug
PacketUtils.broadcastPacket(ParticleCreator.createParticlePacket(
Particle.FLAME, current.x(), current.y(), current.z(),
0, 0, 0, 1));
PathfindUtils.debugParticle(current, Particle.SMOKE);
// Return if the current node is the goal
if (current.distance(goal) - DELTA <= step) {
@ -98,7 +119,7 @@ final class PathfinderImpl implements Pathfinder {
}
// If the neighbor is not walkable, skip it
if (isBlocked(neighbor)) {
if (blocked.test(entity, current, neighbor)) {
continue;
}
@ -115,18 +136,21 @@ final class PathfinderImpl implements Pathfinder {
return null;
}
private static Queue<Point> reconstructPath(Map<Point, Point> cameFrom, Point current) {
private static List<Point> reconstructPath(Map<Point, Point> cameFrom, Point current) {
Deque<Point> path = new ArrayDeque<>();
path.add(current);
while (cameFrom.containsKey(current)) {
current = cameFrom.get(current);
path.addFirst(current);
}
return path;
for (Point point : path) {
PathfindUtils.debugParticle(point, Particle.FLAME);
}
return List.copyOf(path);
}
private static Point[] neighbors(Point point, double step) {
return new Point[]{
return new Point[] {
// Direct neighbors
point.add(step, 0, 0),
point.add(-step, 0, 0),
@ -157,22 +181,67 @@ final class PathfinderImpl implements Pathfinder {
};
}
public double getCost(Point from, Point to) {
// TODO: Implement line intersection algorithm to determine the cost
// The current algorithm is flawed and may tell the navigator to move through very
// specific corners that are not actually possible
Instance instance = entity.getInstance();
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
Block block = instance.getBlock(to);
if (block.isSolid()) {
return Double.POSITIVE_INFINITY;
public interface BlockedPredicate {
/**
* A predicate used as default that blocks movement if any of the blocks are solid.
*/
BlockedPredicate BLOCK_SOLID_BLOCKS = (entity, from, to) -> {
Instance instance = entity.getInstance();
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
return PathfindUtils.isBlocked(to, entity.getBoundingBox(), instance);
};
/**
* Returns true if the given entity cannot move between the two points, false otherwise.
* @param entity The entity to check.
* @param from The starting point.
* @param to The ending point.
* @return True if the entity cannot move between the two points, false otherwise.
*/
boolean test(@NotNull Entity entity, @NotNull Point from, @NotNull Point to);
/**
* Combines this predicate with another one.
* @param other The other predicate.
* @return A new predicate that returns true if either this or the other predicate returns true.
*/
default @NotNull BlockedPredicate combine(@NotNull BlockedPredicate other) {
return (entity, from, to) -> test(entity, from, to) || other.test(entity, from, to);
}
return block.registry().speedFactor();
}
public boolean isBlocked(Point point) {
Instance instance = entity.getInstance();
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
return PathfindUtils.isBlocked(point, entity.getBoundingBox(), instance, 0.1);
public interface CostFunction {
/**
* A cost function used as default that returns the block speed factor
*/
CostFunction BLOCK_SPEED_FACTOR = (entity, from, to) -> {
// TODO: Implement line intersection algorithm to determine the cost
// The current algorithm is flawed and may tell the navigator to move through very
// specific corners that are not actually possible
Instance instance = entity.getInstance();
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
Block block = instance.getBlock(to);
if (block.isSolid()) {
return Double.POSITIVE_INFINITY;
}
return block.registry().speedFactor();
};
/**
* Returns the cost of moving from one point to another.
* @param from The starting point.
* @param to The ending point.
* @return The cost of moving from one point to another.
*/
double getCost(@NotNull Entity entity, @NotNull Point from, @NotNull Point to);
/**
* Combines this cost function with another cost function.
* @param other The other cost function.
* @return A new cost function that combines this cost function with the other cost function.
*/
default @NotNull CostFunction combine(@NotNull CostFunction other) {
return (entity, from, to) -> getCost(entity, from, to) + other.getCost(entity, from, to);
}
}
}

View File

@ -24,4 +24,23 @@ public final class PositionUtils {
final double radians = -Math.atan2(dy, Math.max(Math.abs(dx), Math.abs(dz)));
return (float) Math.toDegrees(radians);
}
private static float averageWrapped(float valueA, float valueB, float wrap) {
float diff = valueB - valueA;
while (diff > wrap / 2) {
diff -= wrap;
}
while (diff < -wrap / 2) {
diff += wrap;
}
return valueA + diff / 2;
}
public static float averagePitch(float pitchA, float pitchB) {
return averageWrapped(pitchA, pitchB, 180);
}
public static float averageYaw(float yawA, float yawB) {
return averageWrapped(yawA, yawB, 360);
}
}