-
Notifications
You must be signed in to change notification settings - Fork 0
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 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.
public enum CortexSpiderGoal implements Goal {
NONE,
WANDER,
INVESTIGATE,
HUNT_TARGET;
@Override
public boolean isNone() {
return this == NONE;
}
}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.
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));
}
}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.
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 togglesCrawlController.setWallCrawling(agent, ...)accordingly. - It computes a direct velocity toward that waypoint via
NavigationQueries.computeWallCrawlVelocityand sets it withagent.setDeltaMovement(...), bypassing vanilla'sMoveControl/PathNavigationentirely. - It advances to the next waypoint once close enough, and reports
FAILED_STUCK/FAILED_NO_PATHthe same wayMoveToDestinationActiondoes 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.
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.
}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.
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 theEntityTypeand 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 ofregisterEntity/createalongside 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, registerSpiderRenderer::newfor the new entity type (see step 5 above for why this needs no new assets). -
lang/en_us.jsonand amodels/item/cortex_spider_spawn_egg.json, the same two plain-text/JSON resource entries the other two examples each have.
- 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.INSTANCEin place ofAStarPathfinder.INSTANCEwherever climbing actually needs to happen, here, that's scoped toHUNT_TARGET's chase action alone. - The execution layer is where the real work is: vanilla's
PathNavigation/MoveControlcan't reliably follow a route across walls and ceilings, soCrawlToDestinationActiondrives velocity directly from the computed path, andtravel(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 toCrawlStateand override only the terrain/hazard rules that differ for your creature. - Even branches that don't climb can still need crawler-aware validation:
INVESTIGATEstays on the ground pathfinder but swaps in a crawler-awarestandableFactoryso a wall/ceiling sighting isn't rejected as a candidate before pathing to it is even attempted.
- Wall Crawling, full reference for every piece used above.
- Ranged Example Walkthrough (Skeleton) and Full Example Walkthrough — the other two bundled examples this one's GOAP/tree layer mirrors.
-
Built-in Actions, where
CrawlToDestinationActionsits alongsideMoveToDestinationActionand the rest. -
Navigation and Pathfinding,
MovementCapability, incremental search, and howCrawlTraversalEvaluatorrelates to the plainAStarPathfinder.