Войти или зарегистрироваться
  • Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.

[Туториал] Кастомный интеллект мобов

Dymeth

Активный участник
Пользователь
Баллы
98
Имя в Minecraft
Dymeth
На самом деле, это не полноценный туториал, однако существенная подсазка о том, как организовать кастомный ИИ мобам. Возможно кто-то однажды решится написать полноценную статью на основе этих материалов.

Вариантов реализации кастомного интеллекта у мобов довольно много, но у каждого есть свои преимущества и недостатки. Выбирайте, что подойдёт конкретно вам.

Если нужно заменить мобу интеллект на интеллект другого ванильного моба - это элементарно можно сделать при помощи плагина LibsDisguises. Например, можно заставить свинью ходить за игроками и атаковать их (задать свинье интеллект зомби). При необходимости на моба можно даже повесить скин игрока, тем самым создав NPC.

Само собой, у плагина есть API.
Плагин: https://spigotmc.org/resources/81
Документация: https://spigotmc.org/wiki/lib-s-disguises

* Бесплатная версия LibsDisguises не включает в себя часть функций (смотрите на странице плагина, какие именно). Если нужны они - используйте платную версию: https://spigotmc.org/resources/32453

* Пробовал вместо LibsDisguises использовать iDusguise, но работает он существенно хуже, имеет меньше функционала и не поддерживает новые версии

Если же нужна полностью собственная логика работы интеллекта, то в Paper есть API для этого. Нужно понимать, что чем более новую версию вы используете - тем больше будет возможностей и тем меньше рутинного кода вы будете писать.
Всё перечислять не буду, упомяну лишь наиболее интересные, на мой взгляд, возможности.

На версии 1.12 или даже старше появились EntityPathfindEvent и SentientNPC.setTarget(LivingEntity)

В 1.13 появилась возможность управлять передвижением моба при помощи Mob.getPathfinder(), а также с комфортом проводить трассировку при помощи LivingEntity.rayTraceBlocks(), LivingEntity.getTargetBlockInfo() и LivingEntity.getTargetEntityInfo()

В 1.15 появились Server.getMobGoals(), который позволят полноценно добавлять, удалять и получать блоки интеллекта.
Примеры возможно даже не тестировал, но для понимания принципа работы этого должно быть достаточно.
Копипаст блока из ядра ванильного сервера, который заставит детёныша моба ходить за своим родителем до взросления.
PHP:
import com.destroystokyo.paper.entity.ai.Goal;
import com.destroystokyo.paper.entity.ai.GoalKey;
import com.destroystokyo.paper.entity.ai.GoalType;
import lombok.NonNull;
import org.bukkit.entity.Animals;

import java.util.EnumSet;

public final class GoalFollowParentAnimal implements Goal<Animals> {
    private static final GoalKey<Animals> KEY = AiHelper.key(GoalFollowParentAnimal.class, Animals.class);

    private final Animals mob;
    private Animals parent;
    private final double movingSpeed;
    private int cooldownTicks;

    public GoalFollowParentAnimal(Animals mob, double movingSpeed) {
        this.mob = mob;
        this.movingSpeed = movingSpeed;
    }

    @Override
    public boolean shouldActivate() {
        if (this.mob.isAdult()) return false;

        Animals parent = null;
        double minDistance = Double.MAX_VALUE;
        for (Animals animal : AiHelper.getNearbyEntitiesByType(
                this.mob.getWorld(),
                Animals.class,
                this.mob.getBoundingBox().expand(8.0D, 4.0D, 8.0D),
                null)
        ) {
            if (!animal.isAdult()) continue;
            double distance = this.mob.getLocation().distanceSquared(animal.getLocation());
            if (distance > minDistance) continue;
            minDistance = distance;
            parent = animal;
        }
        if (parent == null) return false;
        if (minDistance < 9.0D) return false;
        this.parent = parent;
        return true;
    }

    @Override
    public boolean shouldStayActive() {
        if (this.mob.isAdult()) return false;
        if (this.parent.isDead()) return false;
        double distanceSquired = this.mob.getLocation().distanceSquared(this.parent.getLocation());
        return !(distanceSquired < 9.0D) && !(distanceSquired > 256.0D);
    }

    @Override
    public void start() {
        this.cooldownTicks = 0;
    }

    @Override
    public void stop() {
        this.parent = null;
    }

    @Override
    public void tick() {
        if (--this.cooldownTicks > 0) return;
        this.cooldownTicks = 10;
        this.mob.getPathfinder().moveTo(this.parent, this.movingSpeed);
    }

    @Override
    @NonNull
    public GoalKey<Animals> getKey() {
        return KEY;
    }

    @Override
    @NonNull
    public EnumSet<GoalType> getTypes() {
        return EnumSet.of(GoalType.MOVE, GoalType.LOOK);
    }
}
Блок, который заставит мобов ходить за предметов в руке у других мобов.
PHP:
import com.destroystokyo.paper.entity.ai.Goal;
import com.destroystokyo.paper.entity.ai.GoalType;
import lombok.NonNull;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
import org.*********ventory.EntityEquipment;
import org.*********ventory.ItemStack;

import java.util.EnumSet;

public abstract class GoalFollowItemsInHand<M extends Mob, T extends LivingEntity> implements Goal<M> { // Mob, Target
    private final M mob;
    private final Class<T> targetClass;

    private final double entitySearchRadius;
    private final double walkingSpeed;
    private final double stopWalkingDistanceRadiusSquired;
    private final long cooldownTicksAfterStopping;

    private T closestEntity;
    private long cooldownTicks;

    protected GoalFollowItemsInHand(M mob, Class<T> targetClass,
                                    double entitySearchRadius,
                                    double walkingSpeed,
                                    double stopWalkingDistanceRadius,
                                    long cooldownTicksAfterStopping
    ) {
        this.mob = mob;
        this.targetClass = targetClass;

        this.entitySearchRadius = entitySearchRadius;
        this.walkingSpeed = walkingSpeed;
        this.stopWalkingDistanceRadiusSquired = Math.pow(stopWalkingDistanceRadius, 2);
        this.cooldownTicksAfterStopping = cooldownTicksAfterStopping;
    }

    @Override
    public boolean shouldActivate() {
        if (this.cooldownTicks-- > 0) return false;

        this.closestEntity = AiHelper.getClosestEntity(this.mob.getLocation(), this.entitySearchRadius, this.targetClass, this::isValidTarget);
        if (this.closestEntity == null) return false;

        EntityEquipment inv = this.closestEntity.getEquipment();
        if (inv == null) {
            throw new IllegalStateException("Сущности " + EntityTypeUtils.getTypeName(this.closestEntity) + " не имеют экипировки");
        }
        return this.isRequiredItem(inv.getItemInMainHand()) || this.isRequiredItem(inv.getItemInOffHand());
    }

    @Override
    public void stop() {
        this.mob.getPathfinder().stopPathfinding();
        this.mob.setTarget(null);
        this.cooldownTicks = this.cooldownTicksAfterStopping;
    }

    @Override
    public void tick() {
        this.mob.setTarget(this.closestEntity);
        if (this.mob.getLocation().distanceSquared(this.closestEntity.getLocation()) < this.stopWalkingDistanceRadiusSquired) {
            this.mob.getPathfinder().stopPathfinding();
        } else {
            this.mob.getPathfinder().moveTo(this.closestEntity, this.walkingSpeed);
        }
    }

    @Override
    @NonNull
    public EnumSet<GoalType> getTypes() {
        return EnumSet.of(GoalType.MOVE, GoalType.LOOK);
    }

    protected abstract boolean isValidTarget(@NonNull T target);

    protected abstract boolean isRequiredItem(@NonNull ItemStack stack);
}
Заставляет деревенщин ходить за изумрудами в руках игрока. Работает благодаря классу-родителю GoalFollowItemsInHand из примера вышею
PHP:
import com.destroystokyo.paper.entity.ai.GoalKey;
import lombok.NonNull;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.*********ventory.ItemStack;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public final class GoalVillagerFollowPlayerEmeralds extends GoalFollowItemsInHand<Villager, Player> {

    private static final Set<Material> REQUIRED_MATERIALS = new HashSet<>(Arrays.asList(Material.EMERALD, Material.EMERALD_BLOCK));

    static {
        REQUIRED_MATERIALS.addAll(Tag.EMERALD_ORES.getValues());
    }

    private static final GoalKey<Villager> KEY = AiHelper.key(GoalVillagerFollowPlayerEmeralds.class, Villager.class);

    public GoalVillagerFollowPlayerEmeralds(@NonNull Villager villager) {
        super(villager, Player.class,
                10,
                1.0,
                2,
                SchedulerHelper.toTicks(5, TimeUnit.SECONDS));
    }

    @Override
    @NonNull
    public GoalKey<Villager> getKey() {
        return KEY;
    }

    @Override
    public boolean isValidTarget(@NonNull Player target) {
        return !target.isDead() && target.getGameMode() != GameMode.SPECTATOR && target.isValid();
    }

    @Override
    protected boolean isRequiredItem(@NonNull ItemStack stack) {
        return REQUIRED_MATERIALS.contains(stack.getType());
    }
}

В 1.16 добавили удобный метод Mob.lookAt()

В 1.15 добавили методы LivingEntity.swingMainHand() и LivingEntity.swingOffHand()

В 1.19 появился и Mob.rayTraceEntitites() на замену старому LivingEntity.getTargetEntityInfo(), также для удобства добавили LivingEntity.swingHand()

Хотя полностью все задачи Paper до сих пор не покрывает. Лишь позволяет менять блоки интеллекта, но вот переопределить значения НМС-полей и логику НМС-методов по-прежнему нельзя.

Другие способы в сообщении ниже
 
Последнее редактирование:
В качестве альтернативы предыдущему варианту некоторые люди используют апи плагина Citizens. В том числе и потому, что можно ещё и модельки игрока спаунить. Я раньше тоже использовал этот вариант, но пришёл к выводу, что связка Paper API + LibsDisguises всё же удобней и функциональней.
Но, возможно, кто-то всё же решит использовать и этот вариант...
Плагин: https://spigotmc.org/resources/13811
Документация: https://wiki.citizensnpcs.co/API
Джавадоки: https://jd.citizensnpcs.co/

* Плагин можно спокойно скачать бесплатно с официального источника, если хорошо прочитаете описание на SpigotMC

Ещё один вариант задавать интеллект - как раз при помощи апишки плагина MythicMobs, там вроде довольно удобная система, хотя сам я с их апишкой не работал. Но не знаю, можно ли управлять интеллектом мобов, заспауненных не через сам MythicMobs. Поэтому, возможно, его придётся использовать и для создания сущностей.
Плагин: https://spigotmc.org/resources/5702
Документация: https://git.mythiccraft.io/mythiccraft/MythicMobs/-/wikis/API

Неоднократно слышал, что в BKCommonLib тоже есть какое-то апи для работы с интеллектом мобов. Сам не пользовался, но вам рекомендую ознакомиться.
Плагин: https://spigotmc.org/resources/39590
Документация: https://github.com/bergerhealer/BKCommonLib/blob/master/README.md
Джавадоки: https://ci.mg-dev.eu/javadocs/BKCommonLib

Если всё это не устроит, то только уже в этом случае стоит прибегать к НМС. Наследуется класс нмс-сущности, создаётся своя собственная реализация с переопределёнными методами. Для работы с NMS рекомендую использовать paperweight userdev, поскольку он позволяет использовать офциальные маппинги от Mojang.

Что такое маппинги: https://rubukkit.org/threads/165824
Как пользоваться papwerweight: https://rubukkit.org/threads/179669

С официальными маппингами спаун НМС-сущности будет выглядеть так:
PHP:
@NonNull
public static Drowned spawnCustomDrowned(@NonNull Location location) {
    return spawnNmsEntity(
            Drowned.class, // Баккит класс
            NmsCustomDrowned::new, // Наш нмс класс
            location, // Локация
            spider -> { // Действия, выполняемые после создания сущности, но перед её отправкой на клиент
                spider.setBaby();
                spider.setInvisible(true);
            });
}

private static <BukkitType extends org.bukkit.entity.Entity> BukkitType spawnNmsEntity(
        @NonNull Class<BukkitType> bukkitClass,
        @NonNull Function<ServerLevel, ? extends net.minecraft.world.entity.Mob> nmsConstructor,
        @NonNull Location location,
        @Nullable Consumer<BukkitType> onSpawn
) {
    try {
        ServerLevel nmsWorld = ((org.bukkit.craftbukkit.v1_20_R3.CraftWorld) location.getWorld()).getHandle(); // Вместо v1_20_R3 подставьте нужную версию
        net.minecraft.world.entity.Mob nmsEntity = nmsConstructor.apply(nmsWorld);
        nmsEntity.moveTo(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
        if (onSpawn != null) onSpawn.accept(bukkitClass.cast(nmsEntity.getBukkitEntity()));
        nmsWorld.addFreshEntity(nmsEntity, CreatureSpawnEvent.SpawnReason.CUSTOM);
        return bukkitClass.cast(nmsEntity.getBukkitEntity());
    } catch (Throwable t) {
        throw new RuntimeException("Unable to spawn NMS-entity", t);
    }
}
PHP:
public class NmsCustomDrowned extends net.minecraft.world.entity.monster.Drowned {

    public NmsCustomDrowned(Level world) {
        super(EntityType.DROWNED, world);
    }

   // Тут будет ещё куча методов из НМС
}
Это корректно работало на 1.19.4. Но на новых версиях вполне может сломаться, потому что это NMS.

Если вдруг сломается, то заходите в код ядра, и смотрите, как происходит спаун сущностей через баккит апи. Достаточно будет скопировать в метод спауна все необходимые вызовы
 
Последнее редактирование:

Поделиться этой страницей

Назад
Верх