Merge pull request #151 from RinesThaix/ai

New Entity AI
This commit is contained in:
TheMode 2021-03-02 13:35:54 +01:00 committed by GitHub
commit 1477a9bd41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 275 additions and 128 deletions

View File

@ -3,6 +3,7 @@ package net.minestom.server.entity;
import com.extollit.gaming.ai.path.HydrazinePathFinder;
import net.minestom.server.attribute.Attributes;
import net.minestom.server.entity.ai.EntityAI;
import net.minestom.server.entity.ai.EntityAIGroup;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.TargetSelector;
import net.minestom.server.entity.pathfinding.NavigableEntity;
@ -14,17 +15,13 @@ import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.*;
public class EntityCreature extends LivingEntity implements NavigableEntity, EntityAI {
private int removalAnimationDelay = 1000;
protected final List<GoalSelector> goalSelectors = new ArrayList<>();
protected final List<TargetSelector> targetSelectors = new ArrayList<>();
private GoalSelector currentGoalSelector;
private final Set<EntityAIGroup> aiGroups = new HashSet<>();
private final Navigator navigator = new Navigator(this);
@ -108,27 +105,9 @@ public class EntityCreature extends LivingEntity implements NavigableEntity, Ent
this.removalAnimationDelay = removalAnimationDelay;
}
@NotNull
@Override
public List<GoalSelector> getGoalSelectors() {
return goalSelectors;
}
@NotNull
@Override
public List<TargetSelector> getTargetSelectors() {
return targetSelectors;
}
@Nullable
@Override
public GoalSelector getCurrentGoalSelector() {
return currentGoalSelector;
}
@Override
public void setCurrentGoalSelector(GoalSelector currentGoalSelector) {
this.currentGoalSelector = currentGoalSelector;
public Collection<EntityAIGroup> getAIGroups() {
return this.aiGroups;
}
/**

View File

@ -1,102 +1,51 @@
package net.minestom.server.entity.ai;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
/**
* Represents an entity which can contain
* {@link GoalSelector goal selectors} and {@link TargetSelector target selectors}.
* <p>
* Both types of selectors are being stored in {@link EntityAIGroup AI groups}.
* For every group there could be only a single {@link GoalSelector goal selector} running at a time,
* but multiple groups are independent of each other, so each of them can have own goal selector running.
*/
public interface EntityAI {
/**
* Gets the goal selectors of this entity.
* Gets the AI groups of this entity.
*
* @return a modifiable list containing the entity goal selectors
* @return a modifiable collection of AI groups of this entity.
*/
@NotNull
List<GoalSelector> getGoalSelectors();
Collection<EntityAIGroup> getAIGroups();
/**
* Gets the target selectors of this entity.
* Adds new AI group to this entity.
*
* @return a modifiable list containing the entity target selectors
* @param group a group to be added.
*/
@NotNull
List<TargetSelector> getTargetSelectors();
/**
* Gets the current entity goal selector.
*
* @return the current entity goal selector, null if not any
*/
@Nullable
GoalSelector getCurrentGoalSelector();
/**
* Changes the entity current goal selector.
* <p>
* Mostly unsafe since the current goal selector should normally
* be chosen during the entity tick method.
*
* @param goalSelector the new entity goal selector, null to disable it
*/
void setCurrentGoalSelector(@Nullable GoalSelector goalSelector);
/**
* Performs an AI tick, it includes finding a new {@link GoalSelector}
* or tick the current one,
*
* @param time the tick time in milliseconds
*/
default void aiTick(long time) {
GoalSelector currentGoalSelector = getCurrentGoalSelector();
// true if the goal selector changed this tick
boolean newGoalSelector = false;
if (currentGoalSelector == null) {
// No goal selector, get a new one
currentGoalSelector = findGoal();
newGoalSelector = currentGoalSelector != null;
} else {
final boolean stop = currentGoalSelector.shouldEnd();
if (stop) {
// The current goal selector stopped, find a new one
currentGoalSelector.end();
currentGoalSelector = findGoal();
newGoalSelector = currentGoalSelector != null;
}
}
// Start the new goal selector
if (newGoalSelector) {
setCurrentGoalSelector(currentGoalSelector);
currentGoalSelector.start();
}
// Execute tick for the current goal selector
if (currentGoalSelector != null) {
currentGoalSelector.tick(time);
}
default void addAIGroup(EntityAIGroup group) {
getAIGroups().add(group);
}
/**
* Finds a new {@link GoalSelector} for the entity.
* <p>
* Uses {@link GoalSelector#shouldStart()} and return the goal selector if true.
* Adds new AI group to this entity, consisting of the given
* {@link GoalSelector goal selectors} and {@link TargetSelector target selectors}.
* Their order is also a priority: the lower element index is, the higher priority is.
*
* @return the goal selector found, null if not any
* @param goalSelectors goal selectors of the group.
* @param targetSelectors target selectors of the group.
*/
private GoalSelector findGoal() {
for (GoalSelector goalSelector : getGoalSelectors()) {
final boolean start = goalSelector.shouldStart();
if (start) {
return goalSelector;
}
}
return null;
default void addAIGroup(List<GoalSelector> goalSelectors, List<TargetSelector> targetSelectors) {
EntityAIGroup group = new EntityAIGroup();
group.getGoalSelectors().addAll(goalSelectors);
group.getTargetSelectors().addAll(targetSelectors);
addAIGroup(group);
}
default void aiTick(long time) {
getAIGroups().forEach(group -> group.tick(time));
}
}

View File

@ -0,0 +1,150 @@
package net.minestom.server.entity.ai;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.UnaryOperator;
/**
* Represents a group of entity's AI.
* It may contains {@link GoalSelector goal selectors} and {@link TargetSelector target selectors}.
* All AI groups of a single entity are independent of each other.
*/
public class EntityAIGroup {
private GoalSelector currentGoalSelector;
private final List<GoalSelector> goalSelectors = new GoalSelectorsArrayList();
private final List<TargetSelector> targetSelectors = new ArrayList<>();
/**
* Gets the goal selectors of this group.
*
* @return a modifiable list containing this group goal selectors
*/
@NotNull
public List<GoalSelector> getGoalSelectors() {
return this.goalSelectors;
}
/**
* Gets the target selectors of this group.
*
* @return a modifiable list containing this group target selectors
*/
@NotNull
public List<TargetSelector> getTargetSelectors() {
return this.targetSelectors;
}
/**
* Gets the current goal selector of this group.
*
* @return the current goal selector of this group, null if not any
*/
@Nullable
public GoalSelector getCurrentGoalSelector() {
return this.currentGoalSelector;
}
/**
* Changes the current goal selector of this group.
* <p>
* Mostly unsafe since the current goal selector should normally
* be chosen during the group tick method.
*
* @param goalSelector the new goal selector of this group, null to disable it
*/
public void setCurrentGoalSelector(@Nullable GoalSelector goalSelector) {
Check.argCondition(
goalSelector != null && goalSelector.getAIGroup() != this,
"Tried to set goal selector attached to another AI group!"
);
this.currentGoalSelector = goalSelector;
}
public void tick(long time) {
GoalSelector currentGoalSelector = getCurrentGoalSelector();
if (currentGoalSelector != null && currentGoalSelector.shouldEnd()) {
currentGoalSelector.end();
currentGoalSelector = null;
setCurrentGoalSelector(null);
}
for (GoalSelector selector : getGoalSelectors()) {
if (selector == currentGoalSelector) {
break;
}
if (selector.shouldStart()) {
if (currentGoalSelector != null) {
currentGoalSelector.end();
}
currentGoalSelector = selector;
setCurrentGoalSelector(currentGoalSelector);
currentGoalSelector.start();
break;
}
}
if (currentGoalSelector != null) {
currentGoalSelector.tick(time);
}
}
/**
* The purpose of this list is to guarantee that every {@link GoalSelector} added to that group
* has a reference to it for some internal interactions. We don't provide developers with
* methods like `addGoalSelector` or `removeGoalSelector`: instead we provide them with direct
* access to list of goal selectors, so that they could use operations such as `clear`, `set`, `removeIf`, etc.
*/
private class GoalSelectorsArrayList extends ArrayList<GoalSelector> {
private GoalSelectorsArrayList() {
}
@Override
public GoalSelector set(int index, GoalSelector element) {
element.setAIGroup(EntityAIGroup.this);
return super.set(index, element);
}
@Override
public boolean add(GoalSelector element) {
element.setAIGroup(EntityAIGroup.this);
return super.add(element);
}
@Override
public void add(int index, GoalSelector element) {
element.setAIGroup(EntityAIGroup.this);
super.add(index, element);
}
@Override
public boolean addAll(Collection<? extends GoalSelector> c) {
c.forEach(goalSelector -> goalSelector.setAIGroup(EntityAIGroup.this));
return super.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends GoalSelector> c) {
c.forEach(goalSelector -> goalSelector.setAIGroup(EntityAIGroup.this));
return super.addAll(index, c);
}
@Override
public void replaceAll(UnaryOperator<GoalSelector> operator) {
super.replaceAll(goalSelector -> {
goalSelector = operator.apply(goalSelector);
goalSelector.setAIGroup(EntityAIGroup.this);
return goalSelector;
});
}
}
}

View File

@ -0,0 +1,40 @@
package net.minestom.server.entity.ai;
public class EntityAIGroupBuilder {
private final EntityAIGroup group = new EntityAIGroup();
/**
* Adds {@link GoalSelector} to the list of goal selectors of the building {@link EntityAIGroup}.
* Addition order is also a priority: priority the higher the earlier selector was added.
*
* @param goalSelector goal selector to be added.
* @return this builder.
*/
public EntityAIGroupBuilder addGoalSelector(GoalSelector goalSelector) {
this.group.getGoalSelectors().add(goalSelector);
return this;
}
/**
* Adds {@link TargetSelector} to the list of target selectors of the building {@link EntityAIGroup}.
* Addition order is also a priority: priority the higher the earlier selector was added.
*
* @param targetSelector target selector to be added.
* @return this builder.
*/
public EntityAIGroupBuilder addTargetSelector(TargetSelector targetSelector) {
this.group.getTargetSelectors().add(targetSelector);
return this;
}
/**
* Creates new {@link EntityAIGroup}.
*
* @return new {@link EntityAIGroup}.
*/
public EntityAIGroup build() {
return this.group;
}
}

View File

@ -5,8 +5,11 @@ import net.minestom.server.entity.EntityCreature;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.WeakReference;
public abstract class GoalSelector {
private WeakReference<EntityAIGroup> aiGroupWeakReference;
protected EntityCreature entityCreature;
public GoalSelector(@NotNull EntityCreature entityCreature) {
@ -51,7 +54,11 @@ public abstract class GoalSelector {
*/
@Nullable
public Entity findTarget() {
for (TargetSelector targetSelector : entityCreature.getTargetSelectors()) {
EntityAIGroup aiGroup = getAIGroup();
if (aiGroup == null) {
return null;
}
for (TargetSelector targetSelector : aiGroup.getTargetSelectors()) {
final Entity entity = targetSelector.findTarget();
if (entity != null) {
return entity;
@ -74,12 +81,22 @@ public abstract class GoalSelector {
* Changes the entity affected by the goal selector.
* <p>
* WARNING: this does not add the goal selector to {@code entityCreature},
* this only change the internal entity field. Be sure to remove the goal from
* the previous entity and add it to the new one using {@link EntityCreature#getGoalSelectors()}.
* this only change the internal entity AI group's field. Be sure to remove the goal from
* the previous entity AI group and add it to the new one using {@link EntityAIGroup#getGoalSelectors()}.
*
* @param entityCreature the new affected entity
*/
public void setEntityCreature(@NotNull EntityCreature entityCreature) {
this.entityCreature = entityCreature;
}
void setAIGroup(@NotNull EntityAIGroup group) {
this.aiGroupWeakReference = new WeakReference<>(group);
}
@Nullable
protected EntityAIGroup getAIGroup() {
return this.aiGroupWeakReference.get();
}
}

View File

@ -1,41 +1,48 @@
package demo.entity;
import com.google.common.collect.ImmutableList;
import net.minestom.server.attribute.Attributes;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.ai.EntityAIGroupBuilder;
import net.minestom.server.entity.ai.goal.DoNothingGoal;
import net.minestom.server.entity.ai.goal.MeleeAttackGoal;
import net.minestom.server.entity.ai.goal.RandomStrollGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.ai.target.LastEntityDamagerTarget;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.entity.type.animal.EntityChicken;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.time.TimeUnit;
public class ChickenCreature extends EntityChicken {
public ChickenCreature(Position defaultPosition) {
super(defaultPosition);
//goalSelectors.add(new DoNothingGoal(this, 500, 0.1f));
//goalSelectors.add(new MeleeAttackGoal(this, 500, TimeUnit.MILLISECOND));
goalSelectors.add(new RandomStrollGoal(this, 2));
/*goalSelectors.add(new EatBlockGoal(this,
new HashMap<>() {
{
put(Block.GRASS.getBlockId(), Block.AIR.getBlockId());
}
},
new HashMap<>() {
{
put(Block.GRASS_BLOCK.getBlockId(), Block.DIRT.getBlockId());
}
},
100))
;
//goalSelectors.add(new FollowTargetGoal(this));*/
//targetSelectors.add(new LastEntityDamagerTarget(this, 15));
//targetSelectors.add(new ClosestEntityTarget(this, 15, LivingEntity.class));
addAIGroup(
ImmutableList.of(
// new DoNothingGoal(this, 500, 0.1f),
// new MeleeAttackGoal(this, 500, 2, TimeUnit.MILLISECOND),
new RandomStrollGoal(this, 2)
),
ImmutableList.of(
// new LastEntityDamagerTarget(this, 15),
// new ClosestEntityTarget(this, 15, LivingEntity.class)
)
);
// Another way to register previously added EntityAIGroup, using specialized builder:
// addAIGroup(
// new EntityAIGroupBuilder()
// .addGoalSelector(new DoNothingGoal(this, 500, .1F))
// .addGoalSelector(new MeleeAttackGoal(this, 500, 2, TimeUnit.MILLISECOND))
// .addGoalSelector(new RandomStrollGoal(this, 2))
// .addTargetSelector(new LastEntityDamagerTarget(this, 15))
// .addTargetSelector(new ClosestEntityTarget(this, 15, LivingEntity.class))
// .build()
// );
getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.1f);

View File

@ -1,5 +1,6 @@
package demo.entity;
import net.minestom.server.entity.ai.EntityAIGroupBuilder;
import net.minestom.server.entity.ai.goal.RandomLookAroundGoal;
import net.minestom.server.entity.type.monster.EntityZombie;
import net.minestom.server.utils.Position;
@ -8,6 +9,10 @@ public class ZombieCreature extends EntityZombie {
public ZombieCreature(Position spawnPosition) {
super(spawnPosition);
goalSelectors.add(new RandomLookAroundGoal(this, 20));
addAIGroup(
new EntityAIGroupBuilder()
.addGoalSelector(new RandomLookAroundGoal(this, 20))
.build()
);
}
}