Add NpcMovementTask which randomly moves bounded NPCs around the map, fix Npc#equals, bug fixes for Pathfinding and CollisionMatrix.

This commit is contained in:
Major-
2015-03-03 01:24:34 +00:00
parent 05e0afa83a
commit 2d5d484c18
9 changed files with 253 additions and 94 deletions
+14
View File
@@ -30,6 +30,7 @@ import org.apollo.game.model.event.EventListener;
import org.apollo.game.model.event.EventListenerChainSet;
import org.apollo.game.scheduling.ScheduledTask;
import org.apollo.game.scheduling.Scheduler;
import org.apollo.game.scheduling.impl.NpcMovementTask;
import org.apollo.io.EquipmentDefinitionParser;
import org.apollo.util.MobRepository;
import org.apollo.util.NameUtil;
@@ -97,6 +98,11 @@ public final class World {
*/
private final EventListenerChainSet events = new EventListenerChainSet();
/**
* The ScheduledTask that moves Npcs.
*/
private NpcMovementTask npcMovement;
/**
* The {@link MobRepository} of {@link Npc}s.
*/
@@ -242,8 +248,12 @@ public final class World {
placeEntities(objects);
logger.fine("Loaded " + objects.length + " static objects.");
npcMovement = new NpcMovementTask(); // Must be exactly here because of ordering issues.
scheduler.schedule(npcMovement);
manager.start();
pluginManager = manager; // TODO move!!
}
/**
@@ -285,6 +295,10 @@ public final class World {
if (success) {
Sector sector = sectors.fromPosition(npc.getPosition());
sector.addEntity(npc);
if (npc.hasBoundaries()) {
npcMovement.addNpc(npc);
}
} else {
logger.warning("Failed to register npc, repository capacity reached: [count=" + npcRepository.size() + "]");
}
+9 -4
View File
@@ -14,6 +14,7 @@ import org.apollo.game.model.area.collision.CollisionMatrix;
import org.apollo.game.model.entity.Entity;
import org.apollo.game.model.entity.Entity.EntityType;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
@@ -175,14 +176,13 @@ public final class Sector {
Set<Entity> local = entities.get(old);
if (local == null || !local.remove(entity)) {
throw new IllegalArgumentException("Entity belongs in this sector but does not exist.");
throw new IllegalArgumentException("Entity belongs in this sector (" + this + ") but does not exist.");
}
local = entities.computeIfAbsent(position, key -> new HashSet<>(DEFAULT_SET_SIZE));
local.add(entity);
notifyListeners(entity, SectorOperation.MOVE);
}
/**
@@ -225,9 +225,14 @@ public final class Sector {
*/
public boolean traversable(Position position, EntityType entity, Direction direction) {
CollisionMatrix matrix = matrices[position.getHeight()];
int x = position.getLocalX(), y = position.getLocalY();
int x = position.getX(), y = position.getY();
return matrix.traversable(x, y, entity, direction);
return !matrix.untraversable(x % SECTOR_SIZE, y % SECTOR_SIZE, entity, direction);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("coordinates", coordinates).toString();
}
/**
@@ -166,16 +166,16 @@ public final class CollisionMatrix {
}
/**
* Returns whether or not an Entity of the specified {@link EntityType type} can traverse the tile at the specified
* coordinate pair.
* Returns whether or not an Entity of the specified {@link EntityType type} cannot traverse the tile at the
* specified coordinate pair.
*
* @param x The x coordinate.
* @param y The y coordinate.
* @param entity The {@link EntityType}.
* @param direction The {@link Direction} the Entity is approaching from.
* @return {@code true} if the tile at the specified coordinate pair is traversable, {@code false} if not.
* @return {@code true} if the tile at the specified coordinate pair is not traversable, {@code false} if not.
*/
public boolean traversable(int x, int y, EntityType entity, Direction direction) {
public boolean untraversable(int x, int y, EntityType entity, Direction direction) {
CollisionFlag[] flags = CollisionFlag.forType(entity);
int north = 0, east = 1, south = 2, west = 3;
+28 -28
View File
@@ -1,6 +1,5 @@
package org.apollo.game.model.entity;
import java.util.Arrays;
import java.util.Optional;
import org.apollo.game.model.Position;
@@ -20,9 +19,9 @@ import com.google.common.base.Preconditions;
public final class Npc extends Mob {
/**
* The positions representing the bounds (i.e. walking limits) of this Npc.
* The Positions representing the boundaries (i.e. walking limits) of this Npc.
*/
private Position[] boundary;
private Optional<Position[]> boundaries;
/**
* Creates a new Npc with the specified id and {@link Position}.
@@ -31,18 +30,20 @@ public final class Npc extends Mob {
* @param position The position.
*/
public Npc(int id, Position position) {
this(position, NpcDefinition.lookup(id));
this(position, NpcDefinition.lookup(id), null);
}
/**
* Creates a new Npc with the specified {@link NpcDefinition} and {@link Position}.
*
* @param position The position.
* @param definition The definition.
* @param position The Position.
* @param definition The NpcDefinition.
* @param boundaries The boundary Positions.
*/
public Npc(Position position, NpcDefinition definition) {
public Npc(Position position, NpcDefinition definition, Position[] boundaries) {
super(position, definition);
this.boundaries = Optional.ofNullable(boundaries);
init();
}
@@ -50,19 +51,19 @@ public final class Npc extends Mob {
public boolean equals(Object obj) {
if (obj instanceof Npc) {
Npc other = (Npc) obj;
return position.equals(other.position) && Arrays.equals(boundary, other.boundary) && getId() == other.getId();
return index == other.index && getId() == other.getId();
}
return false;
}
/**
* Gets the boundary of this Npc.
* Gets the boundaries of this Npc.
*
* @return The boundary.
* @return The boundaries.
*/
public Position[] getBoundary() {
return boundary.clone();
public Optional<Position[]> getBoundaries() {
return boundaries.isPresent() ? Optional.of(boundaries.get().clone()) : Optional.empty();
}
@Override
@@ -79,30 +80,29 @@ public final class Npc extends Mob {
return definition.get().getId();
}
/**
* Returns whether or not this Npc has boundaries.
*
* @return {@code true} if this Npc has boundaries, {@code false} if not.
*/
public boolean hasBoundaries() {
return boundaries.isPresent();
}
@Override
public int hashCode() {
final int prime = 31;
int result = prime * position.hashCode() + Arrays.hashCode(boundary);
return prime * result + getId();
return prime * index + getId();
}
/**
* Indicates whether or not this Npc is bound to a specific set of coordinates.
* Sets the boundaries of this Npc.
*
* @return {@code true} if the Npc is bound, otherwise {@code false}.
* @param boundaries The boundaries.
*/
public boolean isBound() {
return boundary == null;
}
/**
* Sets the boundary of this Npc.
*
* @param boundary The boundary.
*/
public void setBoundary(Position[] boundary) {
Preconditions.checkArgument(boundary.length == 2, "Boundary count must be 2.");
this.boundary = boundary.clone();
public void setBoundaries(Position[] boundaries) {
Preconditions.checkArgument(boundaries.length == 2, "Boundary count must be 2.");
this.boundaries = Optional.of(boundaries.clone());
}
@Override
@@ -23,7 +23,7 @@ import org.apollo.game.model.Position;
*
* @author Major
*/
final class AStarPathfindingAlgorithm extends PathfindingAlgorithm {
public final class AStarPathfindingAlgorithm extends PathfindingAlgorithm {
/**
* The heuristic.
@@ -1,7 +1,7 @@
package org.apollo.game.model.entity.path;
import java.util.Deque;
import java.util.Set;
import java.util.Optional;
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
@@ -9,7 +9,8 @@ import org.apollo.game.model.World;
import org.apollo.game.model.area.Sector;
import org.apollo.game.model.area.SectorRepository;
import org.apollo.game.model.entity.Entity.EntityType;
import org.apollo.game.model.entity.GameObject;
import com.google.common.base.Preconditions;
/**
* An algorithm used to find a path between two {@link Position}s.
@@ -19,46 +20,48 @@ import org.apollo.game.model.entity.GameObject;
abstract class PathfindingAlgorithm {
/**
* The repository of sectors.
* The repository of Sectors.
*/
private static final SectorRepository repository = World.getWorld().getSectorRepository();
private static final SectorRepository REPOSITORY = World.getWorld().getSectorRepository();
/**
* Finds a valid path from the origin {@link Position} to the target one.
*
* @param origin The origin position.
* @param target The target position.
* @return The {@link Deque} containing the positions to go through.
* @param origin The origin Position.
* @param target The target Position.
* @return The {@link Deque} containing the Positions to go through.
*/
public abstract Deque<Position> find(Position origin, Position target);
/**
* Returns whether or not the tile at the specified position is walkable. FIXME do this properly w/tile collision
* data!
*
* @param position The {@link Position}.
* @return {@code true} if the tile is walkable, otherwise {@code false}.
* Returns whether or not a {@link Position} walking one step in any of the specified {@link Direction}s would lead
* to is traversable.
*
* @param current The current Position.
* @param directions The Directions that should be checked.
* @return {@code true} if any of the Directions lead to a traversable tile, otherwise {@code false}.
*/
protected boolean traversable(Position position) {
Sector sector = repository.get(position.getSectorCoordinates());
Set<GameObject> objects = sector.getEntities(position, EntityType.GAME_OBJECT);
return objects.stream().anyMatch(object -> object.getDefinition().isSolid());
protected boolean traversable(Position current, Direction... directions) {
return traversable(current, Optional.empty(), directions);
}
/**
* Returns whether or not the {@link Position}s walking one step in a specified {@link Direction} would lead to is
* traversable.
* Returns whether or not a {@link Position} walking one step in any of the specified {@link Direction}s would lead
* to is traversable.
*
* @param position The starting position.
* @param directions The directions that should be checked.
* @return {@code true} if any of the directions lead to a traversable tile, otherwise {@code false}.
* @param current The current Position.
* @param boundaries The {@link Optional} containing the Position boundaries.
* @param directions The Directions that should be checked.
* @return {@code true} if any of the Directions lead to a traversable tile, otherwise {@code false}.
*/
protected boolean traversable(Position position, Direction... directions) {
int height = position.getHeight();
protected boolean traversable(Position current, Optional<Position[]> boundaries, Direction... directions) {
Preconditions.checkArgument(directions != null && directions.length > 0, "Directions array cannot be null.");
int height = current.getHeight();
Position[] positions = boundaries.isPresent() ? boundaries.get() : new Position[0];
for (Direction direction : directions) {
int x = position.getX(), y = position.getY();
int x = current.getX(), y = current.getY();
int value = direction.toInteger();
if (value >= Direction.NORTH_WEST.toInteger() && value <= Direction.NORTH_EAST.toInteger()) {
@@ -73,7 +76,9 @@ abstract class PathfindingAlgorithm {
x--;
}
if (traversable(new Position(x, y, height))) {
Position next = new Position(x, y, height);
Sector sector = REPOSITORY.get(next.getSectorCoordinates());
if (sector.traversable(next, EntityType.NPC, direction) && (positions.length == 0 || inside(next, positions))) {
return true;
}
}
@@ -81,4 +86,18 @@ abstract class PathfindingAlgorithm {
return false;
}
/**
* Returns whether or not the specified {@link Position} is inside the specified {@code boundary}.
*
* @param position The Position.
* @param boundary The boundary Positions.
* @return {@code true} if the specified Position is inside the boundary, {@code false} if not.
*/
private boolean inside(Position position, Position[] boundary) {
int x = position.getX(), y = position.getY();
Position min = boundary[0], max = boundary[1];
return x >= min.getX() && y >= min.getY() && x <= max.getX() && y <= max.getY();
}
}
@@ -2,6 +2,7 @@ package org.apollo.game.model.entity.path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
@@ -12,7 +13,12 @@ import org.apollo.game.model.Position;
*
* @author Major
*/
final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
public final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
/**
* The Optional containing the boundary Positions.
*/
private Optional<Position[]> boundaries = Optional.empty();
@Override
public Deque<Position> find(Position origin, Position target) {
@@ -22,6 +28,19 @@ final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
return addHorizontal(origin, target, positions);
}
/**
* Finds a valid path from the origin {@link Position} to the target one.
*
* @param origin The origin Position.
* @param target The target Position.
* @param boundaries The boundary Positions, which are marking as untraversable.
* @return The {@link Deque} containing the Positions to go through.
*/
public Deque<Position> find(Position origin, Position target, Position[] boundaries) {
this.boundaries = Optional.of(boundaries);
return find(origin, target);
}
/**
* Adds the necessary and possible horizontal {@link Position}s to the existing {@link Deque}.
* <p>
@@ -33,33 +52,33 @@ final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
* </ul>
*
* @param current The current position.
* @param start The current position.
* @param target The target position.
* @param positions The deque of positions.
* @return The deque of positions containing the path.
*/
private Deque<Position> addHorizontal(Position current, Position target, Deque<Position> positions) {
int x = current.getX(), y = current.getY(), height = current.getHeight();
int dx = x - target.getX();
private Deque<Position> addHorizontal(Position start, Position target, Deque<Position> positions) {
int x = start.getX(), y = start.getY(), height = start.getHeight();
int dx = x - target.getX(), dy = y - target.getY();
if (dx > 0) {
Position west = new Position(x - 1, y, height);
Position current = start;
while (traversable(west) && dx-- > 0) {
west = new Position(--x, y, height);
positions.addLast(west);
while (traversable(current, boundaries, Direction.WEST) && dx-- > 0) {
current = new Position(--x, y, height);
positions.addLast(current);
}
} else if (dx < 0) {
Position east = new Position(x + 1, y, height);
Position current = start;
while (traversable(east) && dx++ < 0) {
east = new Position(++x, y, height);
positions.addLast(east);
while (traversable(current, boundaries, Direction.EAST) && dx++ < 0) {
current = new Position(++x, y, height);
positions.addLast(current);
}
}
Position last = new Position(x, y, height);
if (!current.equals(last) && traversable(last, Direction.NORTH, Direction.SOUTH)) {
if (!start.equals(last) && dy != 0 && traversable(last, boundaries, (dy > 0) ? Direction.SOUTH : Direction.NORTH)) {
return addVertical(last, target, positions);
}
@@ -77,33 +96,33 @@ final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
* </ul>
*
* @param current The current position.
* @param start The current position.
* @param target The target position.
* @param positions The deque of positions.
* @return The deque of positions containing the path.
*/
private Deque<Position> addVertical(Position current, Position target, Deque<Position> positions) {
int x = current.getX(), y = current.getY(), height = current.getHeight();
int dy = y - target.getY();
private Deque<Position> addVertical(Position start, Position target, Deque<Position> positions) {
int x = start.getX(), y = start.getY(), height = start.getHeight();
int dy = y - target.getY(), dx = x - target.getX();
if (dy > 0) {
Position south = new Position(x, y - 1, height);
Position current = start;
while (traversable(south) && dy-- > 0) {
south = new Position(x, --y, height);
positions.addLast(south);
while (traversable(current, boundaries, Direction.SOUTH) && dy-- > 0) {
current = new Position(x, --y, height);
positions.addLast(current);
}
} else if (dy < 0) {
Position north = new Position(x, y + 1, height);
Position current = start;
while (traversable(north) && dy++ < 0) {
north = new Position(x, ++y, height);
positions.addLast(north);
while (traversable(current, boundaries, Direction.NORTH) && dy++ < 0) {
current = new Position(x, ++y, height);
positions.addLast(current);
}
}
Position last = new Position(x, y, height);
if (!last.equals(target) && traversable(last, Direction.EAST, Direction.WEST)) {
if (!last.equals(target) && dx != 0 && traversable(last, boundaries, (dx > 0) ? Direction.WEST : Direction.EAST)) {
return addHorizontal(last, target, positions);
}
@@ -0,0 +1,101 @@
package org.apollo.game.scheduling.impl;
import java.util.Comparator;
import java.util.Deque;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import org.apollo.game.model.Position;
import org.apollo.game.model.entity.Npc;
import org.apollo.game.model.entity.WalkingQueue;
import org.apollo.game.model.entity.path.SimplePathfindingAlgorithm;
import org.apollo.game.scheduling.ScheduledTask;
import com.google.common.base.Preconditions;
/**
* A {@link ScheduledTask} that causes {@link Npc}s to randomly walk around in their boundary.
*
* @author Major
*/
public final class NpcMovementTask extends ScheduledTask {
/**
* The delay between executions of this task, in pulses.
*/
private static final int DELAY = 5;
/**
* The random number generator used to calculate how many Npcs should be moved per execution.
*/
private static final Random RANDOM = new Random();
/**
* The comparator used to sort the Npcs in the PriorityQueue.
*/
private static final Comparator<Npc> RANDOM_COMPARATOR = (first, second) -> RANDOM.nextInt(2) - 1;
/**
* The PathfindingAlgorithm used by this Task.
*/
private final SimplePathfindingAlgorithm algorithm = new SimplePathfindingAlgorithm();
/**
* The Queue of Npcs.
*/
private final Queue<Npc> npcs = new PriorityQueue<>(RANDOM_COMPARATOR);
/**
* Creates the NpcMovementTask.
*/
public NpcMovementTask() {
super(DELAY, false);
}
/**
* Adds the {@link Npc} to this {@link ScheduledTask}.
*
* @param npc The Npc to add.
*/
public void addNpc(Npc npc) {
Preconditions.checkArgument(npc.hasBoundaries(), "Cannot add an npc with no boundaries to the NpcMovementTask.");
npcs.offer(npc);
System.out.println("Adding npc to movement task: " + npc.getId());
}
@Override
public void execute() {
int count = RANDOM.nextInt(npcs.size() / 50 + 5);
for (int iterations = 0; iterations < count; iterations++) {
Npc npc = npcs.poll();
if (npc == null) {
break;
}
Position[] boundary = npc.getBoundaries().get();
Position current = npc.getPosition();
Position min = boundary[0], max = boundary[1];
int currentX = current.getX(), currentY = current.getY();
boolean negativeX = RANDOM.nextBoolean(), negativeY = RANDOM.nextBoolean();
int x = RANDOM.nextInt(negativeX ? (currentX - min.getX()) : (max.getX() - currentX));
int y = RANDOM.nextInt(negativeY ? (currentY - min.getY()) : (max.getY() - currentY));
int dx = negativeX ? -x : x;
int dy = negativeY ? -y : y;
Position next = new Position(currentX + dx, currentY + dy);
Deque<Position> positions = algorithm.find(current, next, boundary);
WalkingQueue queue = npc.getWalkingQueue();
Position first = positions.pollFirst();
if (first != null && queue.addFirstStep(first)) {
positions.forEach(npc.getWalkingQueue()::addStep);
}
npcs.offer(npc);
}
}
}