Use predicate for entity targeting (#1099)

* Use predicate for entity targeting

* Change range variable in deprecated constructor from double to float

* Use distance squared
This commit is contained in:
Johnnywoof 2022-06-14 13:07:42 -04:00 committed by GitHub
parent 3821d204cf
commit bab2c22a51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 103 additions and 83 deletions

View File

@ -4,111 +4,69 @@ import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.ai.TargetSelector;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Comparator;
import java.util.function.Predicate;
/**
* Target the closest targetable entity (based on the class array)
* Target the closest targetable entity (based on the target predicate)
*/
public class ClosestEntityTarget extends TargetSelector {
private final float range;
private final Class<? extends LivingEntity>[] entitiesTarget;
private final double range;
private final Predicate<Entity> targetPredicate;
/**
* @param entityCreature the entity (self)
* @param range the maximum range the entity can target others within
* @param entitiesTarget the entities to target by class
* @deprecated Use {@link #ClosestEntityTarget(EntityCreature, double, Predicate)}
*/
@SafeVarargs
@Deprecated
public ClosestEntityTarget(@NotNull EntityCreature entityCreature, float range,
@NotNull Class<? extends LivingEntity>... entitiesTarget) {
this(entityCreature, range, ent -> {
Class<? extends Entity> clazz = ent.getClass();
for (Class<? extends LivingEntity> targetClass : entitiesTarget) {
if (targetClass.isAssignableFrom(clazz)) {
return true;
}
}
return false;
});
}
/**
* @param entityCreature the entity (self)
* @param range the maximum range the entity can target others within
* @param targetPredicate the predicate used to check if the other entity can be targeted
*/
public ClosestEntityTarget(@NotNull EntityCreature entityCreature, double range,
@NotNull Predicate<Entity> targetPredicate) {
super(entityCreature);
this.range = range;
this.entitiesTarget = entitiesTarget;
this.targetPredicate = targetPredicate;
}
@Override
public Entity findTarget() {
final Instance instance = getEntityCreature().getInstance();
final Chunk currentChunk = instance.getChunkAt(entityCreature.getPosition());
if (currentChunk == null) {
Instance instance = entityCreature.getInstance();
if (instance == null) {
return null;
}
final List<Chunk> chunks = getNeighbours(instance, currentChunk.getChunkX(), currentChunk.getChunkZ());
return instance.getNearbyEntities(entityCreature.getPosition(), range).stream()
// Don't target our self and make sure entity is valid
.filter(ent -> !entityCreature.equals(ent) && !ent.isRemoved())
.filter(targetPredicate)
.min(Comparator.comparingDouble(e -> e.getDistanceSquared(entityCreature)))
.orElse(null);
Entity entity = null;
double distance = Double.MAX_VALUE;
for (Chunk chunk : chunks) {
final Set<Entity> entities = instance.getChunkEntities(chunk);
for (Entity ent : entities) {
// Only target living entities
if (!(ent instanceof LivingEntity)) {
continue;
}
// Don't target itself
if (ent.equals(entityCreature)) {
continue;
}
if (ent.isRemoved()) {
// Entity not valid
return null;
}
// Check if the entity type can be targeted
final Class<? extends Entity> clazz = ent.getClass();
boolean correct = false;
for (Class<? extends LivingEntity> targetClass : entitiesTarget) {
if (targetClass.isAssignableFrom(clazz)) {
correct = true;
break;
}
}
if (!correct) {
continue;
}
// Check distance
final double d = entityCreature.getDistance(ent);
if ((entity == null || d < distance) && d < range) {
entity = ent;
distance = d;
continue;
}
}
}
return entity;
}
private List<Chunk> getNeighbours(Instance instance, int chunkX, int chunkZ) {
List<Chunk> chunks = new ArrayList<>();
// Constants used to loop through the neighbors
final int[] posX = {1, 0, -1};
final int[] posZ = {1, 0, -1};
for (int x : posX) {
for (int z : posZ) {
final int targetX = chunkX + x;
final int targetZ = chunkZ + z;
final Chunk chunk = instance.getChunk(targetX, targetZ);
if (ChunkUtils.isLoaded(chunk)) {
// Chunk is loaded, add it
chunks.add(chunk);
}
}
}
return chunks;
}
}

View File

@ -0,0 +1,62 @@
package net.minestom.server.entity.ai;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@EnvTest
public class ClosestEntityTargetTest {
@Test
public void validFindTarget(Env env) {
var instance = env.createFlatInstance();
var self = new EntityCreature(EntityType.ZOMBIE);
self.setInstance(instance, new Pos(0, 42, 0)).join();
var spider = new EntityCreature(EntityType.SPIDER);
spider.setInstance(instance, new Pos(-3, 42, -3)).join();
var secondSpider = new EntityCreature(EntityType.SPIDER);
secondSpider.setInstance(instance, new Pos(-4, 42, -4)).join();
var skeleton = new EntityCreature(EntityType.SKELETON);
skeleton.setInstance(instance, new Pos(5, 42, 5)).join();
var zombie = new EntityCreature(EntityType.ZOMBIE);
zombie.setInstance(instance, new Pos(10, 42, -10)).join();
assertEquals(5, instance.getEntities().size(), "Not all entities are in the instance");
assertNull(
new ClosestEntityTarget(self, 1, e -> true).findTarget(),
"Entity targets it self"
);
assertEquals(spider,
new ClosestEntityTarget(self, 20, e -> e.getEntityType() == EntityType.SPIDER).findTarget(),
"The closest spider was not selected"
);
assertNull(
new ClosestEntityTarget(self, 2, e -> e.getEntityType() == EntityType.SPIDER).findTarget(),
"Range distance is not being considered"
);
zombie.remove();
assertNull(
new ClosestEntityTarget(self, 20, e -> e.getEntityType() == EntityType.ZOMBIE).findTarget(),
"Removed entities are included in target selection"
);
}
}