1. Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
    Скрыть объявление
Скрыть объявление
В преддверии глобального обновления, мы проводим исследования, которые помогут нам сделать опыт пользования форумом ещё удобнее. Помогите нам, примите участие!

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

Тема в разделе "Разработка плагинов для новичков", создана пользователем Dymeth, 10 янв 2024.

  1. Автор темы
    Dymeth

    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<AnimalsKEY AiHelper.key(GoalFollowParentAnimal.class, Animals.class);

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

        public 
    GoalFollowParentAnimal(Animals mobdouble 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.0D4.0D8.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.parentthis.movingSpeed);
        }

        @
    Override
        
    @NonNull
        
    public GoalKey<AnimalsgetKey() {
            return 
    KEY;
        }

        @
    Override
        
    @NonNull
        
    public EnumSet<GoalTypegetTypes() {
            return 
    EnumSet.of(GoalType.MOVEGoalType.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<extends Mobextends LivingEntity> implements Goal<M> { // Mob, Target
        
    private final M mob;
        private final Class<
    TtargetClass;

        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<TtargetClass,
                                        
    double entitySearchRadius,
                                        
    double walkingSpeed,
                                        
    double stopWalkingDistanceRadius,
                                        
    long cooldownTicksAfterStopping
        
    ) {
            
    this.mob mob;
            
    this.targetClass targetClass;

            
    this.entitySearchRadius entitySearchRadius;
            
    this.walkingSpeed walkingSpeed;
            
    this.stopWalkingDistanceRadiusSquired Math.pow(stopWalkingDistanceRadius2);
            
    this.cooldownTicksAfterStopping cooldownTicksAfterStopping;
        }

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

            
    this.closestEntity AiHelper.getClosestEntity(this.mob.getLocation(), this.entitySearchRadiusthis.targetClassthis::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.closestEntitythis.walkingSpeed);
            }
        }

        @
    Override
        
    @NonNull
        
    public EnumSet<GoalTypegetTypes() {
            return 
    EnumSet.of(GoalType.MOVEGoalType.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<VillagerPlayer> {

        private static final 
    Set<MaterialREQUIRED_MATERIALS = new HashSet<>(Arrays.asList(Material.EMERALDMaterial.EMERALD_BLOCK));

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

        private static final 
    GoalKey<VillagerKEY AiHelper.key(GoalVillagerFollowPlayerEmeralds.class, Villager.class);

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

        @
    Override
        
    @NonNull
        
    public GoalKey<VillagergetKey() {
            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 до сих пор не покрывает. Лишь позволяет менять блоки интеллекта, но вот переопределить значения НМС-полей и логику НМС-методов по-прежнему нельзя.

    Другие способы в сообщении ниже
     
    Последнее редактирование: 12 янв 2024
  2. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    В качестве альтернативы предыдущему варианту некоторые люди используют апи плагина 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.EntityBukkitType spawnNmsEntity(
            @
    NonNull Class<BukkitTypebukkitClass,
            @
    NonNull Function<ServerLevel, ? extends net.minecraft.world.entity.MobnmsConstructor,
            @
    NonNull Location location,
            @
    Nullable Consumer<BukkitTypeonSpawn
    ) {
        try {
            
    ServerLevel nmsWorld = ((org.bukkit.craftbukkit.v1_20_R3.CraftWorldlocation.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 != nullonSpawn.accept(bukkitClass.cast(nmsEntity.getBukkitEntity()));
            
    nmsWorld.addFreshEntity(nmsEntityCreatureSpawnEvent.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.DROWNEDworld);
        }

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

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

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