Skip to content

Wall Climbing Example (Spider)

AzureDoom edited this page Aug 29, 2026 · 1 revision

Wall-Climbing Example (Spider)

com.azure.azurecortex.example.spider.CortexSpiderEntity is the third bundled example, alongside the melee zombie (Full Example Walkthrough) and the ranged skeleton (Ranged Example Walkthrough (Skeleton)). It shows the same GOAP/behavior-tree shape as those two, combined with the wall/ceiling-crawling movement model from Wall Crawling, and, unlike the other two examples, it introduces one new piece of framework code: CrawlToDestinationAction, in com.azure.azurecortex.action.movement, alongside the example itself.

The interesting design point

The goal enum, planner, and most of the behavior tree for CortexSpiderEntity are structurally identical to the skeleton's. That's deliberate: wall-crawling is a navigation-layer capability, not a decision-layer one. Nothing about GOAP or the behavior tree needs to know or care that this creature can climb, the only things that change are which Pathfinder a movement action is given and how that action actually applies velocity to the entity.

1. The goal-type enum

public enum CortexSpiderGoal implements Goal {
    NONE,
    WANDER,
    INVESTIGATE,
    HUNT_TARGET;

    @Override
    public boolean isNone() {
        return this == NONE;
    }
}

2. The goal planner

Identical in shape to CortexSkeletonGoalPlanner, hunt a live target, otherwise investigate a recent sighting, otherwise wander, with the usual GoalFailureCooldowns/PlanFeedback handling. See GOAP Planning for what each piece does; there is nothing spider-specific in this class at all.

3. The behavior tree

public final class CortexSpiderTree {

    public static BehaviorNode<CortexSpiderEntity, CortexSpiderGoal> create() {
        var idle = new IdleAction<CortexSpiderEntity, CortexSpiderGoal>();
        var wander = new WanderAction<CortexSpiderEntity, CortexSpiderGoal>(1.0D, 10.0D, 100);

        var investigate = new InvestigateLastSeenTargetAction<CortexSpiderEntity, CortexSpiderGoal>(
            AStarPathfinder.INSTANCE, 1.0D, 2, 60, 100, 60, 0.02D, 2.0D, 8.0D,
            SpiderTraversalPredicates::standableForCrawler);

        var hunt = getHunt();

        return PrioritySelector.of(
            new ActionNode<>(idle, 0),
            new Condition<>((agent, bb, cd) -> bb.get(CommonBlackboardKeys.ACTIVE_GOAL_TYPE)
                == CortexSpiderGoal.WANDER, new ActionNode<>(wander, 5)),
            new Condition<>((agent, bb, cd) -> bb.get(CommonBlackboardKeys.ACTIVE_GOAL_TYPE)
                == CortexSpiderGoal.INVESTIGATE, new ActionNode<>(investigate, 8)),
            hunt
        );
    }

    private static HuntTargetNode<CortexSpiderEntity, CortexSpiderGoal> getHunt() {
        var melee = new AttackProfile<CortexSpiderEntity, CortexSpiderGoal>(
            "melee",
            new TimedAttackAction<>("spider_melee", 4, 2.0D, "spider_melee_cooldown", 15),
            "spider_melee_cooldown", 0.0D, 2.0D, 20);

        // The climbing branch: CrawlToDestinationAction + CrawlTraversalEvaluator, not the ordinary
        // MoveToDestinationAction + AStarPathfinder pairing used everywhere else in the bundled examples.
        var chase = new CrawlToDestinationAction<CortexSpiderEntity, CortexSpiderGoal>(
            CrawlTraversalEvaluator.INSTANCE, 1.1D, 1, 10, 60);

        return new HuntTargetNode<>(CortexSpiderGoal.HUNT_TARGET, chase, List.of(melee));
    }
}

Why WANDER/INVESTIGATE stay on the ordinary pathfinder

WanderAction and InvestigateLastSeenTargetAction both ultimately hand off to the agent's vanilla PathNavigation, the former drives it directly, the latter (like MoveToDestinationAction) computes a full route with whatever Pathfinder it's given but then hands only the final waypoint to agent.getNavigation().moveTo(...), letting vanilla's own ground-only node evaluator re-solve getting there. That's fine for an ordinary destination, but not something that reliably reaches a point stuck to a wall or ceiling. Rather than fight that inside two actions that are shared with every other example, HUNT_TARGET's chase action is where this example actually solves the problem, see below. (If you want investigate/wander to climb too, the same CrawlToDestinationAction class works for any action slot that reads CommonBlackboardKeys.DESTINATION.)

Note also that InvestigateLastSeenTargetAction validates its extrapolated search point against a pluggable standableFactory (a Function<Level, Predicate<BlockPos>>) rather than a hardcoded predicate, it defaults to TargetPrediction::standableIn (the ground-mob check) when omitted, exactly as it did before this parameter existed, so the zombie and skeleton examples pass none and are unaffected. This example passes SpiderTraversalPredicates::standableForCrawler instead, so a sighting last seen on a wall or ceiling isn't wrongly rejected as a candidate, even though this branch still routes through the ordinary ground pathfinder. That pairing is intentional, not an oversight: standableForCrawler only ever widens which points are accepted (every ground-standable point is still accepted, since "solid neighbor below" is one of the six directions it checks), so it can't make a previously-reachable point unreachable, it just stops rejecting wall/ceiling points outright before the pathfinder even gets a chance to fail on them naturally.

4. CrawlToDestinationAction: the new action that actually climbs

This is the one genuinely new piece of framework code this example adds, in com.azure.azurecortex.action.movement.CrawlToDestinationAction. Unlike MoveToDestinationAction, it walks the entire CrawlTraversalEvaluator path one waypoint at a time instead of handing only the last point to vanilla navigation:

  • Each tick, it checks whether the current waypoint is a climb/tunnel/shaft node (via CollisionQueries.isSafeClimbNode, CrawlTraversalEvaluator.tunnelCanStandAt, CrawlTraversalEvaluator.verticalShaftCanCrawlAt) and toggles CrawlController.setWallCrawling(agent, ...) accordingly.
  • It computes a direct velocity toward that waypoint via NavigationQueries.computeWallCrawlVelocity and sets it with agent.setDeltaMovement(...), bypassing vanilla's MoveControl/PathNavigation entirely.
  • It advances to the next waypoint once close enough, and reports FAILED_STUCK/FAILED_NO_PATH the same way MoveToDestinationAction does when progress stalls.

See the class's own Javadoc (reproduced in source) for the full reasoning. It's written generically, nothing about it is spider-specific, so it's usable for any CrawlCapability-implementing entity's chase/approach action.

5. The entity

CortexSpiderEntity extends vanilla Spider (not Monster), deliberately, for two reasons: it inherits Spider's correct 1.4×0.9 bounding box and tuned attributes, and, more importantly, both platform client mods (AzureCortexClientMod for Fabric, AzureCortexNeoForgeClientMod for NeoForge) register vanilla's own SpiderRenderer for it, a renderer that's generic over T extends Spider, so the example renders with vanilla's spider model and texture with no new art assets needed. registerGoals() still strips out every one of vanilla Spider's own AI goals in favor of the AzureCortex runtime, exactly as the zombie/skeleton examples do to their own vanilla base classes.

public class CortexSpiderEntity extends Spider implements CrawlCapability, MovementCapability {

    // ... SynchedEntityData accessors for isCrawling / crawl-forward / crawl-up / crawl-dist ...

    private final CrawlState crawlState = new CrawlState(this, IS_CRAWLING, /* ... */);
    private final CortexRuntime<CortexSpiderEntity, CortexSpiderGoal> runtime;
    private final CortexSpiderGoalPlanner goalPlanner = new CortexSpiderGoalPlanner();

    public CortexSpiderEntity(EntityType<? extends Spider> entityType, Level level) {
        super(entityType, level);
        var validity = VanillaTargetPredicates.players().or(VanillaTargetPredicates.ironGolems());
        var targetSensor = new TargetSensor<CortexSpiderEntity>(
            TargetSensor.nearestMatching(20.0D, validity), 10, TargetSensor.lineOfSight());
        this.runtime = new CortexRuntime<>(this, targetSensor, CortexSpiderTree.create());
        this.runtime.addPeriodicHook("spider_sync_chase_destination", 5, (agent, blackboard) -> {
            var goalType = blackboard.get(CommonBlackboardKeys.ACTIVE_GOAL_TYPE);
            var target = blackboard.get(CommonBlackboardKeys.TARGET);
            if (goalType == CortexSpiderGoal.HUNT_TARGET && target != null) {
                blackboard.set(CommonBlackboardKeys.DESTINATION, target.blockPosition());
            }
        });
    }

    // CrawlCapability: every method delegates straight through to `crawlState`.

    // MovementCapability: cobwebs are passable-solid; lava is a hazard fluid.

    @Override
    protected void registerGoals() {
        this.goalSelector.addGoal(0, new FloatGoal(this));
        this.goalSelector.addGoal(1, new LookAtPlayerGoal(this, Player.class, 8.0F));
        this.goalSelector.addGoal(2, new RandomLookAroundGoal(this));
    }

    @Override
    public void travel(Vec3 travelVector) {
        if (CrawlController.isWallCrawling(this)) {
            move(MoverType.SELF, getDeltaMovement());
            setDeltaMovement(getDeltaMovement().scale(0.6D));
        } else {
            super.travel(travelVector);
        }
    }

    @Override
    public void tick() {
        super.tick();
        crawlState.tick();
        CrawlController.updateWallCrawlingPhysics(this);
        if (!this.level().isClientSide() && this.isAlive() && !this.isNoAi()) {
            tickGoalPlanner();
            runtime.tick();
            CrawlController.updateCrawlOrientation(this, getDeltaMovement());
        }
    }

    // tickGoalPlanner(), identical shape to the zombie/skeleton examples.
}

The one piece that's genuinely new: travel(Vec3)

CrawlController.updateWallCrawlingPhysics(mob) (called every tick above) suppresses gravity while crawling, but does not by itself change how the entity's velocity gets applied to its position, that's still vanilla LivingEntity.travel(), which is built around ordinary ground gravity and friction. The travel(Vec3) override above is what makes the velocity CrawlToDestinationAction sets each tick actually move the entity along a wall or ceiling: while CrawlController.isWallCrawling(this) is true, it skips vanilla's gravity-based travel entirely and applies getDeltaMovement() directly via move(MoverType.SELF, ...), the same general pattern vanilla itself uses for flying mobs (FlyingMob#travel), applied here to crawling instead of flight. The 0.6 friction-scale factor is a reasonable starting point, not a tuned constant; adjust it (and CrawlToDestinationAction's speed parameter) to taste once you can see it in-game.

Registration

Wiring a new example entity into the mod touches a few more files beyond the four above, all following the exact pattern the zombie/skeleton examples already use:

  • ExampleRegistry, registers the EntityType and spawn egg. Because a spider's footprint is wider than it is tall (unlike the zombie/skeleton, which share a hardcoded 0.6-wide profile), this adds a small width-aware overload of registerEntity/create alongside the existing height-only ones, rather than changing their signatures.
  • AzureCortexFabric / AzureCortexNeoForge, register the entity's attributes, spawn placement (ground-only, same as the other two, the crawling behavior is a pursuit-time navigation detail, not a spawn-rule concern), and spawn-egg creative-tab entry.
  • AzureCortexClientMod / AzureCortexNeoForgeClientMod, register SpiderRenderer::new for the new entity type (see step 5 above for why this needs no new assets).
  • lang/en_us.json and a models/item/cortex_spider_spawn_egg.json, the same two plain-text/JSON resource entries the other two examples each have.

What to take away

  • The decision layer (goal enum, planner, most of the tree) for a wall-crawling creature isn't meaningfully different from an ordinary ground-walking one.
  • The pathfinding layer needs CrawlTraversalEvaluator.INSTANCE in place of AStarPathfinder.INSTANCE wherever climbing actually needs to happen, here, that's scoped to HUNT_TARGET's chase action alone.
  • The execution layer is where the real work is: vanilla's PathNavigation/MoveControl can't reliably follow a route across walls and ceilings, so CrawlToDestinationAction drives velocity directly from the computed path, and travel(Vec3) is overridden so that velocity actually moves the entity instead of being overwritten by vanilla's gravity-driven movement.
  • The capability layer (CrawlCapability, MovementCapability) is mechanical: delegate to CrawlState and override only the terrain/hazard rules that differ for your creature.
  • Even branches that don't climb can still need crawler-aware validation: INVESTIGATE stays on the ground pathfinder but swaps in a crawler-aware standableFactory so a wall/ceiling sighting isn't rejected as a candidate before pathing to it is even attempted.

Next

Clone this wiki locally