Implémenter Vos Propres AITasks

Avant de commencer à implémenter notre propre AITask nous devons nous assurer que notre espace de travail est configuré correctement. Merci de suivre les instructions dans Utiliser MCP dans les Plugins.

Voici une liste d’imports que nous aloons utiliser lors de l’implémentation de l”AITask.

import java.util.Comparator;
import java.util.Optional;
import java.util.function.Predicate;

import org.spongepowered.api.GameRegistry;
import org.spongepowered.api.entity.Entity;
import org.spongepowered.api.entity.ai.task.AITask;
import org.spongepowered.api.entity.ai.task.AITaskType;
import org.spongepowered.api.entity.ai.task.AbstractAITask;
import org.spongepowered.api.entity.living.Agent;

import com.flowpowered.math.vector.Vector3d;
import com.google.common.base.Preconditions;

import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.pathfinding.PathNavigate;

Pour simplifier l’implémentation, nous utilisons les constantes suivantes. Elles sont optionnelles et peuvent être facilement remplacées par des champs dynamiques définis via un constructeur.

private static final double MOVEMENT_SPEED = 1;
private static final double APPROACH_DISTANCE_SQUARED = 2 * 2;
private static final double MAX_DISTANCE_SQUARED = 20 * 20;
private static final float EXECUTION_CHANCE = 0.2F;
private static final int MUTEX_FLAG_MOVE = 1; // Minecraft bit flag

Si vous souhaitez implémenter votre propre AITask, alors vous devez hériter de AbstractAITask avec votre tâche. L’exemple suivant montre un exemple d’implémentation d’une telle AITask personnalisée. Également, nous devons générer un nouveau AITaskType pour notre AITask. Afin d’éviter de propager la logique partout, nous créons simplement un champs static et une méthode register pour ça dans la classe elle-même, comme montré par l’exemple de code suivant :

public class CreepyCompanionAITask extends AbstractAITask<Agent> {

    private static AITaskType TYPE;

    public static void register(final Object plugin, final GameRegistry gameRegistry) {
        TYPE = gameRegistry
                .registerAITaskType(plugin, "creepy_companion", "CreepyCompanion", CreepyCompanionAITask.class);
    }

    [...]

}

Bien sûr, nous avons toujours besoin d’appeler cette méthode depuis notre classe principale, mais cela peut être facilement fait de cette manière :

@Listener
public void onInitialize(final GameInitializationEvent event) {
    CreepyCompanionAITask.register(this, game.getRegistry());
}

Après cela, nous pouvons finalement commencer à implémenter l’AITask. Pour cela, nous devons implémenter un total de sept méthodes et un constructeur :

  • CreepyCompanionAITask(...)

  • boolean canRunConcurrentWith(AITask<Agent>)

  • boolean canBeInterrupted()

  • boolean shouldUpdate()

  • void start()

  • boolean continueUpdating()

  • void update()

  • void reset()

L’image suivante décrit à peu près l’ordre d’exécution des méthodes :

The method execution order of the ai task

Nous avons besoin du constructeur pour définir tous les paramètres que nous voulons que notre AITask ait et configurer quelques options de base pour cela. Dans ce cas, il définit le AITaskType et configure le mutex de bits, ainsi qu’un Predicate entityFilter que nous utiliserons plus tard.

private final Predicate<Entity> entityFilter;

private Optional<Entity> optTarget;

public CreepyCompanionAITask(final Predicate<Entity> entityFilter) {
    super(TYPE);
    this.entityFilter = Preconditions.checkNotNull(entityFilter);
    ((EntityAIBase) (Object) this).setMutexBits(MUTEX_FLAG_MOVE);
}

Malheureusement, nous devons recourir à des conversions sales vers les classes de Minecraft. C’est également la raison pour laquelle nous devons mettre en place notre espace de travail avec les mappings MCP.

Après cela, nous continuons aveec la première série de méthodes : canRunConcurrentWith et canBeInterrupted. Elles sont très simples à implémenter. Pour la première nous alons dépendre de la logique de Minecraft par défaut, mais cela requiert encore une fois des casts sales. Pour la deuxième, nous devons juste considérer si il peut être interrompu ou doit être complété d’abord.

@Override
public boolean canRunConcurrentWith(final AITask<Agent> other) {
    return (((EntityAIBase) (Object) this).getMutexBits() & ((EntityAIBase) other).getMutexBits()) == 0;
}

@Override
public boolean canBeInterrupted() {
    return true;
}

Avec cela, nous avons tout ce dont nous avons besoin pour mettre de la logique personnalisée dans notre AITask. À savoir, la logique de quand commencer et de quoi faire dans ce cas. Nous commençons avec la vérification de si la tâche doit être exécutée. C’est également fait en deux étapes :

  • Vérifier une chance d’exécution aléatoire

  • Chercher une cible appropriée

L’exécution actuelle fait alors simplement la chose désirée avec la cible que nous avons trouvé.

@Override
public boolean shouldUpdate() {
    final Agent owner = getOwner().get();
    if (owner.getRandom().nextFloat() > EXECUTION_CHANCE) {
        return false;
    }

    final Vector3d position = getPositionOf(owner);
    this.optTarget = owner.getWorld()
            .getEntities().stream()
            .filter(this.entityFilter)
            .filter(e -> getPositionOf(e).distanceSquared(position) < MAX_DISTANCE_SQUARED)
            .min(Comparator.comparingDouble(e -> getPositionOf(e).distanceSquared(position)));
    return this.optTarget.isPresent();
}

@Override
public void start() {
    getNavigator().tryMoveToEntityLiving((net.minecraft.entity.Entity) this.optTarget.get(), MOVEMENT_SPEED);
}

Le Random du propriétaire est généralemeent une bonne source d’aléa dans ces tâches, mais n’importe quel Random pourrait également fonctionner. La seule raison pour laquellee l”Entity a un random est pour éviter d’avoir un Random dans chaque classe qui interagit avec l’entité.

La recherche de la cible est assez simple. Tout d’abord, nous devons récupérer toutes les entités du monde dans lequel l’entité est actuellement, et ensuite nous filtrons cette liste en utilisant le Predicate entityFilter que nous avons défini dans le constructeur. Après ça, nous supprimons toutes les entités qui sont trop loin. C’est 20 blocs, mais vous pouvez augmenter cette distance. Gardez à l’esprit qu’en élargissant la distance, vous augmentez également le temps de calcul nécessaire pour la recherche de chemin.

Après avoir trouvé notre cible, nous avons juste à dire à l’entité de se déplacer vers elle. La vitesse est un facteur très important pour ça, si la vitesse est trop rapide, alors l’entité pourrait accidentellement rater la cible et finir par courir en avant et en arrière pour toujours (donc soyez sûr d’avoir calculé les critères d’acceptation correctement). Si la vitesse est trop lente, alors l’entité bougera à peine (ce qui semblera étrange et rendra des tâches comme « suivre X » ridicules).

Le code ci-dessus contient quelques appels de méthodes qui ont été ajoutées afin d’améliorer la lisibilité du code; le bloc de code suivant montre quelques méthodes utiles :

private Vector3d getPositionOf(final Entity entity) {
    return entity.getLocation().getPosition();
}

private PathNavigate getNavigator() {
    return ((EntityLiving) (getOwner().get())).getNavigator();
}

La première méthode appelle simplement quelques méthodes en une seule fois, l’autre récupère le PathNavigate ou le contrôleur de mouvement depuis l’entité, ce qui requiert de cast vers les classes de Minecraft.

La dernière partie que nous devons implémenter est la vérification de sie la tâche doit continuer de tourner et ce qui doit être fait pour ça.

@Override
public boolean continueUpdating() {
    if (getNavigator().noPath()) {
        return false;
    }
    if (!this.optTarget.isPresent()) {
        return false;
    }
    final Entity target = this.optTarget.get();
    return getPositionOf(target).distanceSquared(getPositionOf(getOwner().get())) > APPROACH_DISTANCE_SQUARED;
}

@Override
public void update() {
}

@Override
public void reset() {
    getNavigator().clearPath();
    this.optTarget = Optional.empty();
}

Pour notre AITask c’est très simple. Tout d’abord, nous devons vérifier si l’entité a perdu son chemin et puis si l’entité est assez proche. Si nous retournons false, alors Minecraft va invoquer la méthode reset() ensuite. Là nous devons effectuer un peu de nettoyage. En particulier, la référence vers l’entité doit être nettoyé, sinon nous risquons d’avoir une fuite de mémoire. Si nous retournonss true, alors Minecraft va invoquer update(), ce qui ne fait rien dans notre cas car le contrôleur de mouvement fonctionne indépendamment de l’IA et l’entité va continuer de marcher vers la cible.

C’est tout ce qu’il faut pour écrire des AITasks personnalisées. Maintenant nous pouvons l’utiliser à l’intérieur d’une Entity, et le chapitre « Ajout d’AITasks Supplémentaires » sur la page IA d’Entité explique comment le faire.

Mais pourquoi avons nous appelé cette tâche CreepyCompanionAITask ? Il y a une solution simple pour ça. Lors de la vérification de si l”AITask doit être exécutée, on ne vérifie pas si l’entité est déjà assez proche, donc elle bougera un petit peu vers la cible à chaque fois. La prochaine étape dans l’horreur serait pobablement que l’entité fasses seulement ceci si la cible ne regarde pas. Imaginez un creeper s’approchant lentement dans votre dos.