末影人的AI

接下来到了末影人的AI。因为末影人的行为较简单,所以用Goal系统已经足够。因为这一节属于末影人部分,所以本节的内容以分析末影人特有的AI为主,其余的通用AI以后找机会再讲吧~

下面来看registerGoals方法:

@Override
protected void registerGoals() {
//  FloatGoal旧称SwimGoal,游泳的AI。
    goalSelector.addGoal(0, new FloatGoal(this));
    goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this));
    goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false));
    goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F));
    goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
    goalSelector.addGoal(8, new RandomLookAroundGoal(this));
    goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this));
    goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this));
    targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt));
    targetSelector.addGoal(2, new HurtByTargetGoal(this));
    targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, true, false));
    targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false));
}

注意以下一部分实例化了静态内部类:

@Override
protected void registerGoals() {
    goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this));
    goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this));
    goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this));
    targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt));
}

我们先看EndermanFreezeWhenLookedAt这个类。

static class EndermanFreezeWhenLookedAt extends Goal {
    private final EnderMan enderman;
    @Nullable
    private LivingEntity target;

    public EndermanFreezeWhenLookedAt(EnderMan enderman) {
        this.enderman = enderman;
        setFlags(EnumSet.of(Goal.Flag.JUMP, Goal.Flag.MOVE));
    }

    @Override
    public boolean canUse() {
        target = enderman.getTarget();
        if (!(target instanceof Player)) {
            return false;
        } else {
            double disSqr = target.distanceToSqr(enderman);
            return !(disSqr > 256.0D) && enderman.isLookingAtMe((Player) target);
        }
    }

    @Override
    public void start() {
        enderman.getNavigation().stop();
    }

    @Override
    public void tick() {
        enderman.getLookControl().setLookAt(target.getX(), target.getEyeY(), target.getZ());
    }

//  P.S. canContinueToUse省略了,意味着返回值与canUse相同
}

提示:构造方法内的setFlags(EnumSet<Goal.Flag>)很容易遗漏,在设计生物的AI时需特别注意是否需要添加Flag,以防在禁用或启用Flag时(例如在被拴绳牵引时Flag.MOVE会被禁用)生物出现意料之外的问题

这个类比较简单,目的就是为了让末影人可以在攻击目标在不远处(16格内),且攻击目标正看向自己时看向其攻击目标。另外,这个AI的实时性不高,不需要每tick更新。

接下来是两个与方块操作相关的AI,先给出完整的代码。

static class EndermanLeaveBlockGoal extends Goal {
    private final EnderMan enderman;

    public EndermanLeaveBlockGoal(EnderMan enderman) {
        this.enderman = enderman;
    }

    @Override
    public boolean canUse() {
        if (enderman.getCarriedBlock() == null) {
            return false;
        } else if (!ForgeEventFactory.getMobGriefingEvent(enderman.level(), enderman)) {
            return false;
        } else {
        //  因为这是个每两刻更新的AI,所以必须调用reducedTickDelay(或adjustedTickDelay)方法,下同
        //  执行的概率是1/2000每刻
            return enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0;
        }
    }

    @Override
    public void tick() {
        RandomSource random = enderman.getRandom();
        Level level = enderman.level();

        int x = Mth.floor(enderman.getX() - 1.0D + random.nextDouble() * 2.0D);
        int y = Mth.floor(enderman.getY() + random.nextDouble() * 2.0D);
        int z = Mth.floor(enderman.getZ() - 1.0D + random.nextDouble() * 2.0D);

        BlockPos pos = new BlockPos(x, y, z);
        BlockState currentState = level.getBlockState(pos);
        BlockPos belowPos = pos.below();
        BlockState belowState = level.getBlockState(belowPos);
        BlockState carriedState = enderman.getCarriedBlock();

        if (carriedState != null) {
        //  根据周围方块状态更新自己的方块状态
            carriedState = Block.updateFromNeighbourShapes(carriedState, enderman.level(), pos);
            if (canPlaceBlock(level, pos, carriedState, currentState, belowState, belowPos) && !ForgeEventFactory.onBlockPlace(enderman, BlockSnapshot.create(level.dimension(), level, belowPos), Direction.UP)) {
            //  放置方块
                level.setBlock(pos, carriedState, 3);
                level.gameEvent(GameEvent.BLOCK_PLACE, pos, GameEvent.Context.of(this.enderman, carriedState));
                enderman.setCarriedBlock(null);
            }
        }
    }

    private boolean canPlaceBlock(Level level, BlockPos pos, BlockState carriedBlock, BlockState currentState, BlockState belowState, BlockPos belowPos) {
        return currentState.isAir() // 当前方块状态是空气
                && !belowState.isAir() // 下方方块状态不是空气
                && !belowState.is(Blocks.BEDROCK)  // 下方方块不是基岩
                && !belowState.is(Tags.Blocks.ENDERMAN_PLACE_ON_BLACKLIST) // 下方方块无ENDERMAN_PLACE_ON_BLACKLIST标签
                && belowState.isCollisionShapeFullBlock(level, belowPos) // 下方方块是完整的
                && carriedBlock.canSurvive(level, pos) // 末影人手上拿着的方块可以放在pos处
                && level.getEntities(enderman, AABB.unitCubeFromLowerCorner(Vec3.atLowerCornerOf(pos))).isEmpty(); // 放置方块的位置没有实体
    }
}

static class EndermanTakeBlockGoal extends Goal {
    private final EnderMan enderman;

    public EndermanTakeBlockGoal(EnderMan enderman) {
        this.enderman = enderman;
    }

    @Override
    public boolean canUse() {
        if (enderman.getCarriedBlock() != null) {
            return false;
        } else if (!ForgeEventFactory.getMobGriefingEvent(enderman.level(), enderman)) {
            return false;
        } else {
        //  执行的概率是1/20每刻
            return enderman.getRandom().nextInt(reducedTickDelay(20)) == 0;
        }
    }

    @Override
    public void tick() {
        RandomSource random = enderman.getRandom();
        Level level = enderman.level();
        int x = Mth.floor(enderman.getX() - 2.0D + random.nextDouble() * 4.0D);
        int y = Mth.floor(enderman.getY() + random.nextDouble() * 3.0D);
        int z = Mth.floor(enderman.getZ() - 2.0D + random.nextDouble() * 4.0D);

        BlockPos placeBlockPos = new BlockPos(x, y, z);
        BlockState state = level.getBlockState(placeBlockPos);
        Vec3 myPos = new Vec3(enderman.getBlockX() + 0.5D, y + 0.5D, enderman.getBlockZ() + 0.5D);
        Vec3 placePos = new Vec3(x + 0.5D, y + 0.5D, z + 0.5D);

    //  HitResult很重要,马上会讲
        BlockHitResult res = level.clip(new ClipContext(myPos, placePos, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, enderman));
        boolean notBlocked = res.getBlockPos().equals(placeBlockPos);

        if (state.is(BlockTags.ENDERMAN_HOLDABLE) && notBlocked) {
        //  搬起方块
            level.removeBlock(placeBlockPos, false);
            level.gameEvent(GameEvent.BLOCK_DESTROY, placeBlockPos, GameEvent.Context.of(enderman, state));
            enderman.setCarriedBlock(state.getBlock().defaultBlockState());
        }
    }
}

可以发现,ForgeEventFactory.getMobGriefingEvent(enderman.level(), enderman)总是在canUse中被调用。这首先是因为GameRule只是标签,在操作前一定要确认GameRule允许你的行为,其次也因为Forge提供了EntityMobGriefingEvent这一事件。如果Forge有相关的事件(例如上面有EntityMobGriefingEvent和EntityPlaceEvent),不要忘记直接或间接post它们。

EndermanLeaveBlockGoal相对容易理解一些。每两刻如果AI可用,首先会生成一个随机方块坐标,然后检查这个方块坐标是否可以放下手中的方块。如果可以,那么canContinueToUse就会返回false,这个AI就会stop,否则这个AI就会继续运行并寻找方块。

EndermanTakeBlockGoal也用了类似的机制,但是该AI被“触发”的概率更大(因为搬起方块更难找到合适的,有可搬运方块的坐标)。

提一下EndermanTakeBlockGoal用到的HitResult(旧称RayTraceResult),不论是用于描述指向的方块的BlockHitResult,还是用于描述指向的实体的EntityHitResult,使用频率都很高。在这个AI中的作用,则是判断末影人坐标到尝试搬起方块的坐标间是否有障碍。
下表对比了BlockHitResultEntityHitResult的一些区别:

类别 BlockHitResult EntityHitResult
主要作用 描述指向的方块 描述指向的实体
用途举例 方块的放置(如在地上放置草方块) 与实体的交互(如用骨头喂狼)
一般的获取方式 Level类的clip实例方法 ProjectileUtil类中的一系列静态方法

最后是代码最长的AI,EndermanLookForPlayerGoal。Wiki上这样写:“玩家在64格距离内注视末影人的头部达到5游戏刻(0.25秒)也会激怒它们。”,这是怎么实现的呢?

static class EndermanLookForPlayerGoal extends NearestAttackableTargetGoal<Player> {
    private final EnderMan enderman;
    @Nullable
    private Player pendingTarget;
    private int aggroTime;
    private int teleportTime;
    private final TargetingConditions startAggroTargetConditions;
    private final TargetingConditions continueAggroTargetConditions = TargetingConditions.forCombat().ignoreLineOfSight();
    private final Predicate<LivingEntity> isAngerInducing;

    public EndermanLookForPlayerGoal(EnderMan enderman, @Nullable Predicate<LivingEntity> targetConditions) {
        super(enderman, Player.class, 10, false, false, targetConditions);
        this.enderman = enderman;
        this.isAngerInducing = entity -> (enderman.isLookingAtMe((Player) entity) || enderman.isAngryAt(entity)) && !enderman.hasIndirectPassenger(entity);
        // TargetingConditions里forCombat和forNonCombat的区别在于forCombat会检查是否是和平模式以及target是否为队友,而forNonCombat不会
        this.startAggroTargetConditions = TargetingConditions.forCombat().range(getFollowDistance()).selector(isAngerInducing);
    }

    @Override
    public boolean canUse() {
    //  pendingTarget,指待定的攻击目标
        pendingTarget = enderman.level().getNearestPlayer(startAggroTargetConditions, enderman);
        return pendingTarget != null;
    }

    @Override
    public void start() {
        aggroTime = adjustedTickDelay(5);
        teleportTime = 0;
        enderman.setBeingStaredAt();
    }

    @Override
    public void stop() {
        pendingTarget = null;
        super.stop();
    }

    @Override
    public boolean canContinueToUse() {
        if (pendingTarget != null) {
            if (!isAngerInducing.test(pendingTarget)) {
                return false;
            } else {
            //  lookAt的后两个float参数分别表示最大的yRot,最大的xRot
                enderman.lookAt(pendingTarget, 10.0F, 10.0F);
                return true;
            }
        } else {
            if (target != null) {
                if (enderman.hasIndirectPassenger(target)) {
                    return false;
                }
                if (continueAggroTargetConditions.test(enderman, target)) {
                    return true;
                }
            }
            return super.canContinueToUse();
        }
    }

    @Override
    public void tick() {
        if (enderman.getTarget() == null) {
            super.setTarget(null);
        }
        if (pendingTarget != null) {
            if (--aggroTime <= 0) {
          //    达到5刻,切换目标
                target = pendingTarget;
                pendingTarget = null;
                super.start();
            }
        } else {
        //  注视末影人后末影人的行为
            if (target != null && !enderman.isPassenger()) {
                if (enderman.isLookingAtMe((Player) target)) {
                    if (target.distanceToSqr(enderman) < 16.0D) {
                        enderman.teleport();
                    }
                    teleportTime = 0;
                } else if (target.distanceToSqr(enderman) > 256.0D && teleportTime++ >= adjustedTickDelay(30) && enderman.teleportTowards(target)) {
                    teleportTime = 0;
                }
            }
            super.tick();
        }
    }
}

EndermanLookForPlayerGoal先用pendingTarget临时记录了待定的攻击目标,等到注视5游戏刻后,再将其设置为真正的攻击目标。注意canUse被重写了,也就是说只要附近有玩家,这个AI就会“start”。

本节的内容就到此为止了,下一节将会简单讲一下末影人的模型与渲染~

results matching ""

    No results matching ""

    results matching ""

      No results matching ""