GitHub avatar

Fox's Blog

Minecraft Pathfinding Logic and Applications

How A*, block malice, and POI mechanics let you control, predict,

Introduction

I've spent hours watching sheep bump into walls.

Best investment of my life xD

The more you watch these mobs, the more you realize nothing about their movement is random. Every step is coded, predictable, and most importantly -- breakable. I ended up digging through Minecraft's source code to understand exactly how pathfinding works, and what I found is that you can literally mind-control mobs. Like, force them to go where YOU want, not where randomness decides.

This guide is everything I found while digging. The AI system, the A* algorithm, the hidden malice values, the exploits you can pull in survival. Grab your pickaxe.


How Mob AI Works (spoiler: it's kinda dumb)

Goals

Every mob has a list of goals. Things it CAN do, and how badly it WANTS to do them. Lower number = higher priority. Like a todo list from hell.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Ever seen a zombie ignore a turtle egg to chase you instead? That's why: ZombieAttackTurtleEggGoal has priority 4, while ZombieAttackGoal (the "eat your face" goal) is priority 2. Zombies prefer snacks with a pulse.

The goal we actually care about is WaterAvoidingRandomStrollGoal, priority 7. The "I got nothing better to do so I wander around" goal. This is where the fun begins.

Movement (or "how a random walk has a 1 in 60 chance per tick")

Every tick (every 0.05 seconds), the game calls canUse() to check if the mob can be bothered to move. 1 in 60 chance per tick. Horrifically inefficient design, and I love it.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

So to sum up: if you're riding the mob -> no, if the mob hasn't done anything in 5 seconds -> no, if RNG says no -> no. The game REALLY doesn't want mobs to move.

But when it does move, getPosition() takes over:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Those two numbers at the end? XZ radius and Y radius. In water, the mob searches wider (15 vs 10). If it can't find land, it falls back to super.getPosition() which accepts water. Result: mobs WANT to get out of water. That's why your animals swim like maniacs toward the shore.

Fun detail: there's literally a 0.1% chance the mob picks super.getPosition() over LandRandomPos. One in a thousand. Mojang I guess xD

LandRandomPos: the optimization that breaks everything

This is MY favorite step. The most beautiful technical mess that makes pathfinding exploitable.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. The name says it all. If the chosen position is inside a solid block, the game pushes it upward until it's in the air.

It's an optimization: instead of wasting time skipping underground positions, the game just shoves them to the surface. Smart? Yes. But it creates a MASSIVE bias: mobs prefer high ground.

Think about it. Lots of blocks underground, the game generates 10 random positions. The ones inside blocks get pushed up. Dense areas (under a hill) produce more valid positions than hollow areas. Result: the mob statistically goes toward the hill more often.

Trust me, we're about to break this wide open.

The selection: best block wins

10 positions, one winner, a score contest:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

The position with the highest score WINS. And if you know the scoring criteria, you can make YOUR position win. It's like rigging an election.


Mob Preferences (or "why your cow crossed the road")

Every mob has different tastes. And it changes everything.

Mob Loves
Animals (cows, sheep, pigs) Grass blocks, light
Monsters (zombies, skeletons) Darkness (hipsters)
Turtles Water > sand > light
Hoglins crimson_nylium; hate warped_fungus
Striders Lava and NOTHING ELSE
Silverfish Infestable blocks
Guardians Water + light (snobs)
Mooshrooms Mycelium + light
Bees Air. Yes, they prefer AIR.
// Animal: look down, if grass -> max score
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: literally the opposite
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Monsters are basically "if it's lit, negative score, I'm out." They throw a FIT at light levels xD

So you can -- literally -- guide animals with grass and light, and monsters with darkness. It's dumb and brilliant at the same time.


A* in Minecraft (the secret formula)

Minecraft uses A* (A-star) for pathfinding. But Mojang added their own twist:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = distance already traveled (1 per block, ~1.41 diagonal)
  • h(n) = straight-line distance to target
  • 1.5 = because Mojang likes things slightly busted

Normal A* uses f(n) = g(n) + h(n). MOJANG ADDED A 1.5 MULTIPLIER. Why? So the algorithm homes in on the destination faster and prunes fewer search branches. Result: the path is "good enough" but not always optimal. It's a drunk A*.

mermaid diagram

Key limitation: a mob can only pathfind 16 blocks (its follow range). If the destination is too far, it picks the closest reachable block. This means you can build a monolith out of range and the mob will path toward the closest block that gets it closer -- making its movement completely predictable.

The two exploits that break the game

1. Block updates force recalculation

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Every block update near the mob's path forces an A* recomputation with a 1-second cooldown. Put a 1-second clock next to a mob and it recalculates CONSTANTLY. It's like a GPS that resets every second.

And if you do this with 50 mobs? Lag city. RIP TPS.

2. Pathfinding Malice (block cost penalties)

Some blocks scare mobs. Literally. Every block has an associated cost defined by an enum:

Block / Condition Malice
Honey block +8 to walk through
Powder snow Impassable
Closed doors Impassable
Fire +16 through, +8 adjacent
Animals & Villagers Fire = -1 (HARD NO)
Cactus / Sweet berry Impassable; adjacent = +8
Water +8 through or adjacent
Magma +8 adjacent

Animals go even further:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE at -1.0F is literally "forbidden." An animal would rather jump into the void than walk through fire.

Exercise: the great path contest

A villager choosing between multiple paths:

  • Path A: 15 blocks but 6 border water (+8 each)
  • Path B: 18 blocks with 2 water blocks (+8) + 1 water-adjacent (+8)
  • Path C: 14 blocks straight... but fire -> IMPASSABLE for villagers
  • Path D: 16 blocks with 1 magma-adjacent (+8) + 1 honey-adjacent (+8)
  • Path E: 25 blocks with cacti everywhere (+8 everywhere) -> 90.82 total LOL

The winner is usually Path B: the detour pays off because water is EXPENSIVE.

A villager is basically a cost calculator with legs xD

Every mob picks different paths

A villager: "fire? NOPE BYE" A zombie: "fire? OK boomer walks through on fire"

You can literally build highways that villagers take and zombies won't -- or vice versa.


Villagers: the ultimate mess

Villagers are the most misunderstood thing in Minecraft. But once you've read the code, you realize they're just predictable machines with office hours.

Sensors and memories

9 sensors running every 20 ticks (1 second). Each scans a radius around the villager and stores the result in memory. The villager sees everything, remembers everything, and acts accordingly.

Activity packages

A villager's brain is divided into activity packages that activate based on the time:

Package Time The villager...
Core 24/7 Opens doors, swims (80% of the time), ACQUIRES POIs
Work 8am-3pm "Gotta work" -- walks to workstation
Meet 3pm-5pm "Happy hour!" -- goes to the bell, socializes
Rest 6pm-6am "Bed time" -- goes to bed
Idle 6am-8am, 5pm-6pm "Bored" -- wanders, breeds, jumps on beds
Panic Hurt / hostile "RUN" -- FLEES

Panic is the only package that can interrupt ALL others. Even if the villager is sleeping or working, if there's a zombie, PANIC MODE.

Acquire POI: the mechanic that enables wireless redstone

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI scans a 48-block radius for all valid points of interest. It keeps the 5 closest, checks if a path exists, and acquires the closest reachable one. Each POI has limited slots:

  • Workstations: 1 slot
  • Beds: 1 slot
  • Bells: 32 slots

The INSANE thing: the slot is reserved at ACQUISITION time, not on arrival. A villager can lock a composter from across the map without ever reaching it.

You see where this is going?

Wireless redstone. Yes, WIRELESS.

  1. Put a villager in a minecart with a path to a composter
  2. It acquires the composter (slot taken, nobody else can use it)
  3. The villager is too far to click it -- bonemeal stays
  4. MOVE this villager ANYWHERE in the world, it keeps the slot
  5. When you want to activate your thing, KILL the villager
  6. Slot releases, another villager acquires the composter, removes bonemeal
  7. BLOCK UPDATE -> any redstone circuit activated

You've created wireless redstone, transmittable across the entire world, with zero chunk loading needed on the path. Hook this to an ender pearl stasis chamber and teleport yourself from anywhere by killing a villager.

My favorite use? A bounty hunter minigame: multiple villagers with composters, the player has to kill THE RIGHT villager to activate the exit. Completely wtf mechanic xD

The Pathfinding Deadlock (or "the villager that freezes forever")

There's a bug between Acquire POI (which sees a path) and the actual navigation (which refuses to follow it). Happens when the block above a workstation isn't walkable. Result:

  • Core package: "I want to acquire the POI"
  • Navigation: "I can't walk there"
  • Result: the villager stays FROZEN, forever, fighting itself.

Literally frozen villagers, usable as decoration or props. An armor stand tank? Yes. A guard that doesn't move? Yes. Macabre? Maybe. Effective? Totally xD


Conclusion

Mob pathfinding in Minecraft isn't random. It's a deterministic, score-based system, predictable AND breakable.

Three things to remember:

  1. Solid blocks underneath = height bias -- fill or empty the subfloor to guide mobs
  2. Malice is different per mob -- create routes that some take and others don't
  3. POI slots reserved at distance -- free wireless redstone, teleportation, all of it

Minecraft's source code is a goldmine of under-exploited mechanics. I spent hours reading decompiled Java and honestly? Every line is a functional Easter Egg. Except these ones work in survival for wireless redstone with villagers. Best game confirmed xD

Logique de pathfinding Minecraft et ses applications

Comment l'algorithme A*, les malus de blocs et les POI permettent

Introduction

J'ai passé des heures à regarder des moutons se cogner dans des murs.

Et honnetement ? Meilleur investissement de ma vie xD

Parce que plus tu regardes ces mobs, plus tu réalises qu'ils n'ont rien d'aléatoire. Chaque mouvement est codé, prévisible, et surtout -- complètement cassable. J'ai fini par plonger dans le code source de Minecraft pour piger exactement comment le pathfinding marche, et ce que j'ai découvert c'est que tu peux littéralement mind-control les mobs. Genre, les forcer à aller où TU veux, pas là où le hasard décide.

Ce guide c'est tout ce que j'ai appris en fouillant. L'IA, l'algorithme A*, les malus cachés, les exploits que tu peux balancer en survie. Prépare ta pioche.


Comment fonctionne l'IA des mobs (spoiler : c'est débile)

Les Goals

Chaque mob a des goals. C'est une liste de trucs qu'il PEUT faire et à quel point il a ENVIE de les faire. Plus le nombre est petit, plus c'est prioritaire -- comme une todo list version chaos.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

T'as déjà vu un zombie ignorer un oeuf de tortue pour te courser dessus ? Voilà pourquoi : ZombieAttackTurtleEggGoal a priorité 4, alors que ZombieAttackGoal (le truc qui lui dit de te bouffer la tronche) est à priorité 2.

Oui, un zombie préfère te manger plutôt que de casser un oeuf. C'est beau l'amour xD

Le goal qui nous intéresse vraiment c'est WaterAvoidingRandomStrollGoal, priorité 7. Le "j'ai rien à foutre donc je marche au pif" goal. C'est là que commence le bordel.

Le mouvement (ou "comment un random march a 1 chance sur 60 de se produire")

Tous les ticks (toutes les 0.05 secondes), le jeu appelle canUse() pour voir si le mob daigne bouger. 1 chance sur 60 à chaque tick. Complètement débile comme design, et j'adore ça.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Donc pour résumer : si t'es monté sur le mob -> non, si le mob a rien fait depuis 5 secondes -> non, si le random dit non -> non. Le jeu veut VRAIMENT pas que le mob bouge.

Mais quand il bouge, getPosition() prend le relais :

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Regarde ces deux nombres à la fin : c'est le rayon XZ et le rayon Y. Dans l'eau, le mob cherche plus loin (15 au lieu de 10). Si il trouve pas de terre, il se rabat sur super.getPosition() qui accepte l'eau. Résultat : les mobs VEULENT sortir de l'eau. C'est pour ca que tes animaux nagent comme des malades vers le bord.

P'tit détail croustillant : y'a littéralement 0.1% de chance que le mob prenne super.getPosition() au lieu de LandRandomPos. Un pour mille. Mojang quoi xD

LandRandomPos : l'optimisation pourrie qui change tout

C'est MON étape préférée. La plus belle connerie technique qui rend le pathfinding exploitable.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. Le nom dit tout. Si la position choisie est dans un bloc solide, le jeu la pousse vers le haut jusqu'à ce qu'elle soit dans l'air.

C'est une optimisation : au lieu de perdre du temps à ignorer les positions sous terre, le jeu les remonte à la surface. Malin ? Oui. Mais ça crée un biais de dingue : les mobs préfèrent les hauteurs.

Imagine. T'as plein de blocs sous la surface, le jeu génère 10 positions aléatoires. Celles dans les blocs sont poussées vers le haut. Les zones denses (sous une colline) produisent plus de positions valides que les zones creuses. Résultat : le mob va statistiquement plus souvent vers la colline.

Fais-moi confiance, on va casser ça en 2 minutes.

La sélection : le concours du meilleur bloc

10 positions, un seul gagnant, un concours de score :

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

La position avec le meilleur score GAGNE. Et si on connaît les critères de score, on peut faire gagner la position qu'on veut. C'est comme truquer une election.


Les préférences des mobs (ou "pourquoi ta vache traverse la route")

Chaque mob a des goûts différents. Et ça change tout.

Mob Kiffe ça
Animaux (vaches, moutons, cochons) L'herbe et la lumière (hipsters)
Monstres (zombies, squelettes) Le noir (edgelords)
Tortues L'eau, sinon le sable, sinon la lumière
Hoglins crimson_nylium ; détestent warped_fungus
Striders Que la lave. RIEN d'autre.
Silverfish Les blocs infestables (logique)
Guardians Eau + lumière (les snobs)
Mooshrooms Mycelium + lumière (champignons)
Abeilles L'air. Oui, elles préfèrent L'AIR.
// Animal : regarde en bas, si c'est de l'herbe, score max
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monstre : exactement l'inverse
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Genre les monstres c'est littéralement "si c'est éclairé, score négatif, je vais ailleurs". Ils font LA GUEULE a la lumière xD

Donc tu peux -- littéralement -- guider tes animaux avec de l'herbe et de la lumière, et tes monstres avec du noir. C'est débile et génial à la fois.


L'algorithme A* dans Minecraft (la formule secrète)

Minecraft utilise l'algorithme A* (A-star) pour le pathfinding. Mais Mojang a mis sa patte :

f(n) = g(n) + 1.5 × h(n)
  • g(n) = le chemin déjà parcouru (1 par bloc, ~1.41 en diagonale)
  • h(n) = la distance à vol d'oiseau
  • 1.5 = parce que Mojang aime les trucs un peu pétés

Normalement A* utilise f(n) = g(n) + h(n). MOJANG A AJOUTÉ UN FACTEUR 1.5. Pourquoi ? Pour que l'algo aille plus vite vers la destination et coupe moins de branches de recherche. Résultat : le chemin trouvé est "bon" mais pas toujours le meilleur. C'est un A* un peu bourré.

mermaid diagram

Détail important : un mob ne peut pathfinder que sur 16 blocs (sa follow range). Si la destination est trop loin, il choisit le bloc le plus proche qu'il PEUT atteindre. Ça veut dire que tu peux créer un monolithe hors de portée, et le mob va pathfinder vers le bloc le plus proche qui le rapproche de ce monolithe -- making ses mouvements complètement prévisibles.

Les deux exploits qui cassent le jeu

1. Les block updates = recalculation forcée

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Chaque mise à jour de bloc près du chemin du mob force un recalcul d'A* avec un cooldown d'1 seconde. Tu mets une horloge à 1 seconde à côté d'un mob, et il re-calcule son chemin CONSTAMMENT. C'est l'équivalent de mettre un GPS qui se reset toutes les secondes.

Et si tu fais ça avec 50 mobs en même temps ? Lag city. RIP TPS.

2. Les malus de blocs (Pathfinding Malice)

Certains blocs font peur aux mobs. Littéralement. Chaque bloc a un coût associé, défini par une énumération :

Bloc / Condition Malus
Bloc de miel +8 à traverser
Poudreuse Infranchissable
Portes fermées Infranchissable
Feu +16 à traverser, +8 à longer
Animaux & Villageois Feu = -1 (NOPE)
Cactus / Sweet berry Infranchissable ; adjacent = +8
Eau +8 à traverser ou longer
Magma +8 à longer (ouille)

Les animaux sont encore plus extrêmes :

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE à -1.0F, c'est littéralement "interdit". Un animal préfère se jeter dans le vide plutôt que de traverser du feu. Fiou.

Exercice : le grand concours de chemins

Imagine un villageois qui doit choisir entre plusieurs chemins.

  • Chemin A : 15 blocs mais 6 blocs longent de l'eau (+8 chaque)
  • Chemin B : 18 blocs avec 2 blocs d'eau (+8) et 1 bloc adjacent d'eau (+8)
  • Chemin C : 14 blocs tout droits... mais avec du feu -> IMPASSABLE pour un villageois
  • Chemin D : 16 blocs avec 1 bloc adjacent magma (+8) + 1 bloc adjacent miel (+8)
  • Chemin E : 25 blocs mais des cactus partout (+8 partout) -> 90.82 de coût total LOL

Calcul mental :

  • Chemin A : 15 blocs + 6×8 pour l'eau = 15 + 48 = 63 ... mais y'a le 1.5×distance à ajouter. Faisons les vrais calculs.
  • Chemin B : plus long mais moins de malus. Le cost total = distance cumulée + malus.
  • Chemin D : le magma et le miel stack leurs malus.

Le gagnant c'est souvent le Chemin B : le détour est rentable parce que l'eau est CHÈRE.

Un villageois c'est essentiellement un calculateur de coûts avec des jambes xD

Chaque mob a ses goûts

Un villageois : "du feu ? NON MERCI BYE" Un zombie : "du feu ? OK boomer traverse en flambant"

T'as littéralement des routes que certains mobs prennent et d'autres non. Tu peux faire des autoroutes à villageois où les zombies se font cramer.


Les villageois : le bazar ultime

Ok, les villageois. C'est LE truc le moins compris de tout Minecraft. Mais une fois que t'as pogné le code, tu te rends compte que ce sont des machines prévisibles avec des horaires de bureau.

Senseurs et mémoires

9 senseurs, qui tournent toutes les 20 ticks (1 seconde). Chacun scrute un rayon autour du villageois et stocke le résultat en mémoire. Le villageois voit tout, se souvient de tout, et agit en fonction.

Genre : "est-ce que y'a un ennemi ? un item par terre ? un joueur avec qui parler ?" -- il checke TOUT.

Les packages (ses phases de la journée)

Le cerveau d'un villageois c'est des packages d'activité qui s'activent selon l'heure :

Package Horaire Le villageois...
Core H24 Ouvre des portes, nage (80% du temps), et ACQUIERT DES POI
Work 8h-15h "Je vais bosser" -- marche vers son poste
Meet 15h-17h "Apéro !" -- va à la cloche, papote
Rest 18h-6h "Faut dormir" -- va au lit
Idle 6h-8h, 17h-18h "Je glande" -- se balade, fait des bébés, saute sur les lits
Panic Blessure/hostile "AU SECOURS" -- FUITE

Le package Panic est le seul qui peut interrompre TOUS les autres. Même si le villageois est en train de dormir ou de bosser, si y'a un zombie, PANIQUE GÉNÉRALE.

Acquire POI : le truc qui permet la redstone sans fil

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI scanne dans un rayon de 48 blocs tous les POI (points d'interet). Il garde les 5 plus proches, vérifie qu'un chemin existe, et acquiert le premier accessible.

Chaque POI a un nombre limité de slots :

  • Postes de travail : 1 slot
  • Lits : 1 slot
  • Cloches : 32 slots

Le truc DINGUE : le slot est réservé au moment de l'acquisition, PAS à l'arrivée. Un villageois peut verrouiller un composter depuis l'autre bout de la map, sans jamais l'atteindre.

Tu captes la puissance ?

Redstone sans fil. Oui, SANS FIL.

  1. Tu mets un villageois dans un minecart avec un chemin vers un composter
  2. Il acquiert le composter (slot pris, plus personne peut l'utiliser)
  3. Le villageois est trop loin pour cliquer dessus -- la bone meal reste
  4. Tu BALADES ce villageois n'importe où dans le monde, il garde le slot
  5. Quand tu veux activer ton machin, tu T U E S le villageois
  6. Le slot se libère, un autre villageois acquiert le composter, retire la bone meal
  7. BLOCK UPDATE -> n'importe quel circuit redstone activé

T'as littéralement créé un signal redstone sans fil, transmissible dans tout le monde, avec zéro chunk load nécessaire sur le chemin. Tu peux brancher ça sur une ender pearl stasis chamber, te faire téléporter depuis n'importe où en tuant un villageois.

Mon utilisation préférée ? Un mini-jeu "bounty hunter" : tu mets plusieurs villageois avec des composters, le joueur doit tuer LE BON villageois pour activer la sortie. C'est complètement wtf comme mécanique xD

Le Pathfinding Deadlock (ou "le villageois qui freeze pour toujours")

Y'a un bug TROP bon entre Acquire POI (qui voit un chemin) et la navigation réelle (qui refuse de l'emprunter). Ça arrive quand le bloc au-dessus du poste de travail est pas marchable. Résultat :

  • Core package : "je veux acquérir le POI"
  • Navigation : "je peux pas marcher là"
  • Résultat : le villageois reste FIGÉ, pour toujours, à se battre avec lui-même.

Littéralement des villageois frozen en place, utilisables comme décoration ou comme "props" dans des builds. Un tank à armure stand ? Oui. Un garde qui bouge pas ? Oui. Macabre ? Ptet. Mais efficace xD


Conclusion

Le pathfinding des mobs Minecraft c'est pas du hasard. C'est un système déterministe, basé sur des scores, prévisible ET pétable.

Les trois trucs à retenir :

  1. Des blocs sous les pieds = biais de hauteur -- remplis ou vide le sous-sol pour guider les mobs
  2. Les malus sont différents pour chaque mob -- crée des routes que certains prennent et pas d'autres
  3. Les POI slots sont réservés à distance -- redstone sans fil gratuite, téléportation, tout ça

Le code source de Minecraft c'est une mine d'or de mécaniques sous-exploitées. J'ai passé des heures à lire du Java décompilé et franchement ? Chaque ligne est un Easter Egg fonctionnel. Sauf que ceux-là, tu t'en sers en survie pour faire de la redstone sans fil avec des villageois. Meilleur jeu confirmé.

xD

Minecraft寻路逻辑及其应用

A*算法、方块惩罚和POI机制如何让你控制、预测和利用生物移动 -- 从无线红石到优化农场。

引言

我花了几个钟头看绵羊撞墙。

人生最值的投资 xD

你看这些 mob 越久,就越能发现它们的移动根本不是随机的。每一步都是写死的、可预测的,最重要的是----可以拿来搞破坏。我去翻了 Minecraft 的源代码,搞懂了寻路到底怎么运作,然后发现你基本可以精神控制 mob。就像,逼它们去你想让它们去的地方,而不是随机决定去哪。

这篇指南就是我挖到的所有东西。AI 系统、A* 算法、隐藏的 malice 数值、还有你在生存模式能用的骚操作。拿起你的镐子。


Mob AI 是怎么工作的(剧透:挺蠢的)

Goals

每个 mob 都有一堆 goals。就是它能做的事,以及它想做这些事的欲望有多强。数字越小 = 优先级越高。就像一张来自地狱的待办清单。

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

见过僵尸无视海龟蛋追你吗?原因在这:ZombieAttackTurtleEggGoal 优先级是 4,而 ZombieAttackGoal("啃你脸" 的目标)是 2。僵尸更喜欢有心跳的零食。

我们真正关心的目标是 WaterAvoidingRandomStrollGoal,优先级 7。就是 "我没什么好做的就随便走走" 的目标。有趣的部分从这里开始。

移动(或者说 "随机行走每 tick 只有 1/60 的几率")

每个 tick(每 0.05 秒),游戏会调用 canUse() 检查 mob 能不能动。每 tick 只有 1/60 的几率。效率低到可怕的设计,但我超爱。

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

总结一下:如果你骑着 mob -> 不行,如果 mob 已经 5 秒没动过 -> 不行,如果 RNG 说不 -> 不行。游戏真的不想让 mob 移动。

但一旦它要动了,getPosition() 开始干活:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

最后那两个数字?XZ 半径和 Y 半径。在水里时,mob 搜索范围更大(15 比 10)。如果找不到陆地,就退回 super.getPosition(),这个接受水域。结果:mob 想离开水。 这就是为什么你的动物会疯了一样朝岸边游。

有趣细节:mob 有 0.1% 的概率选 super.getPosition() 而不是 LandRandomPos。千分之一。Mojang 我服了 xD

LandRandomPos:打破一切的优化

这是我最喜欢的一步。最美丽的屎山代码,让寻路变得可以被利用。

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid。名字说明一切。如果选中的位置在一个实体方块里面,游戏会把它往上推直到它出现在空气中。

这是一个优化:与其浪费时间跳过地下的位置,游戏直接把它们推到地表。聪明吗?是的。但这产生了一个巨大的偏差:mob 更喜欢高地。

想想看。地下有很多方块,游戏生成 10 个随机位置。卡在方块里面的会被推上去。密集区域(山丘下面)会产生比空旷区域更多有效位置。结果:mob 统计上会更频繁地走向山丘。

信我,我们马上要把它玩坏了。

选择:最好的方块赢

10 个位置,一个赢家,分数竞赛:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

分数最高的位置获胜。如果你知道评分标准,你可以让你的位置赢。就像操纵选举一样。


Mob 偏好(或者 "为什么你的牛过了马路")

每个 mob 口味不同。这改变了一切。

Mob 喜欢
动物(牛、羊、猪) 草方块、光照
怪物(僵尸、骷髅) 黑暗(装逼犯)
海龟 水 > 沙子 > 光照
疣猪兽 crimson_nylium;讨厌 warped_fungus
炽足兽 岩浆,其他什么都不行
蠹虫 可被 infest 的方块
守卫者 水 + 光照(装逼犯)
哞菇 菌丝 + 光照
蜜蜂 空气。对的,它们喜欢空气。
// 动物:向下看,如果是草 -> 最高分
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// 怪物:完全相反
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

怪物基本上就是 "有光?负分,我跑了。" 它们看到光照就爆炸 xD

所以你可以----真的----用草和光照引导动物,用黑暗引导怪物。又蠢又聪明。


Minecraft 里的 A*(秘密公式)

Minecraft 用 A*(A-star)做寻路。但 Mojang 加了点自己的料:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = 已经走的距离(每格 1,对角线约 1.41)
  • h(n) = 到目标的直线距离
  • 1.5 = 因为 Mojang 喜欢稍微搞坏一点东西

正常的 A* 是 f(n) = g(n) + h(n)。MOJANG 加了个 1.5 倍系数。为什么?这样算法能更快锁定目标,剪掉更少搜索分支。结果:路径"够好"但未必是最优的。就是个喝醉了的 A*。

mermaid diagram

关键限制:mob 只能寻路 16 格(它的 follow range)。如果目标太远,它会选最近的可到达方块。这意味着你可以造一个超出范围的巨塔,mob 会走向离目标最近的可到达方块----让它的移动完全可预测。

两个打破游戏的漏洞

1. 方块更新强制重新计算

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

mob 路径附近的每个方块更新都会强制 A* 重新计算,附带 1 秒冷却。在 mob 旁边放个 1 秒时钟,它会不停地重新计算。就像一个每秒重置的 GPS。

如果你对 50 个 mob 这么做?lag 城。RIP TPS。

2. 寻路 Malice(方块成本惩罚)

有些方块会吓到 mob。真的。每个方块都有一个由枚举定义的关联成本:

方块 / 条件 Malice
蜂蜜块 穿过 +8
细雪 不可通行
关着的门 不可通行
火 穿过 +16,相邻 +8
动物 & 村民 火 = -1(HARD NO)
仙人掌 / 甜浆果 不可通行;相邻 +8
水 穿过或相邻 +8
岩浆块 相邻 +8

动物更进一步:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE 的 -1.0F 字面意思就是"禁止"。动物宁愿跳进虚空也不愿走过火。

练习:伟大的寻路比赛

一个村民在多个路径中选择:

  • 路径 A:15 格但 6 格邻水(每个 +8)
  • 路径 B:18 格,2 格水(+8)+ 1 格邻水(+8)
  • 路径 C:14 格直达……但有火 -> 对村民来说不可通行
  • 路径 D:16 格,1 格邻岩浆块(+8)+ 1 格邻蜂蜜块(+8)
  • 路径 E:25 格,到处都是仙人掌(每个 +8)-> 总分 90.82 草

赢家通常是路径 B:绕路是值得的,因为水太贵了。

村民就是个长着腿的成本计算器 xD

每个 mob 选不同的路

村民:"火?不了拜拜" 僵尸:"火?好的大叔 直接穿火走过"

你真的可以造出村民走但僵尸不走的高速公路----反过来也行。


村民:终极屎山

村民是 Minecraft 里最被误解的东西。但一旦你读了代码,就会发现它们只是有固定上下班的可预测机器。

传感器和记忆

9 个传感器每 20 tick(1 秒)运行一次。每个扫描村民周围一定半径,把结果存到记忆里。村民看到一切,记住一切,然后做出相应的行为。

活动包

村民的大脑被分成不同的活动包,根据时间激活:

包 时间 村民在……
Core 24/7 开门、游泳(80% 时间)、获取 POI
Work 早上 8 点 - 下午 3 点 "要上班了"----走向工作站
Meet 下午 3 点 - 5 点 "欢乐时光!"----去钟那里社交
Rest 下午 6 点 - 早上 6 点 "该睡了"----去床上
Idle 早上 6 点 - 8 点,下午 5 点 - 6 点 "好无聊"----闲逛、繁殖、跳床
Panic 受伤 / 有敌对 "快跑"----逃跑

Panic 是唯一能打断所有其他活动的包。即使村民在睡觉或工作,如果有僵尸,恐慌模式启动。

Acquire POI:实现无线红石的机制

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI 扫描 48 格半径内所有有效兴趣点。它保留最近的 5 个,检查是否有路径可达,然后获取最近可到达的。每个 POI 有有限槽位:

  • 工作站:1 个槽位
  • 床:1 个槽位
  • 钟:32 个槽位

疯狂的地方在于:槽位在获取时就锁定了,不是到达时锁定。一个村民可以从地图另一端锁定一个堆肥桶,根本不需要走到它面前。

你知道这意味着什么吧?

无线红石。对的,无线。

  1. 把村民放进矿车,让它有一条通往堆肥桶的路径
  2. 它获取堆肥桶(槽位被占,没人能用)
  3. 村民太远点不到它----骨粉留在里面
  4. 把这个村民移动到世界的任何地方,它仍然持有槽位
  5. 当你想激活你的装置时,杀掉这个村民
  6. 槽位释放,另一个村民获取堆肥桶,取走骨粉
  7. 方块更新 -> 任何红石电路被激活

你就做出了无线红石,可以跨越整个世界传输,路径上不需要加载任何区块。把这个连接到末影珍珠静滞装置,你就可以通过杀一个村民从任何地方传送回来。

我最喜欢的用法?赏金猎人小游戏:多个村民各有一个堆肥桶,玩家必须杀掉正确的村民来激活出口。完全是 wtf 级别的机制 xD

寻路死锁(或者说 "永远冻结的村民")

Acquire POI(能看到路径)和实际导航(拒绝走那条路)之间存在一个 bug。当工作站上面的方块不可行走时会发生这种情况。结果:

  • Core 包:"我要获取 POI"
  • 导航:"我走不到那里"
  • 结果:村民永远冻住,一直在和自己打架。

字面意义上冻住的村民,可以当装饰或道具用。一个盔甲架坦克?可以。一个不动的守卫?可以。阴间吗?也许。有效吗?绝对 xD


结论

Minecraft 的 mob 寻路不是随机的。这是一个确定性的、基于分数的系统,可预测又可破坏。

要记住三件事:

  1. 实体方块底下 = 高度偏差----填充或清空底层地板来引导 mob
  2. Malice 因 mob 而异----创建一些 mob 会走而另一些不会的路径
  3. POI 槽位可在远处锁定----免费的无线红石、传送,全都有

Minecraft 的源代码是一座未被充分发掘的机制金矿。我花了几个小时读反编译的 Java,说实话?每一行都是一个能用的彩蛋。只不过这些在生存模式能用来做村民无线红石。史上最佳游戏确认 xD

Minecraftの経路探索ロジックとその応用

A*アルゴリズム、ブロックのペナルティ、POIメカニズムを使って、mobの動きをコントロール、予測、悪用する方法 --

導入

羊が壁にぶつかるのを何時間も見てたんだ。

人生で最高の投資だったわ xD

こういうモブたちを観察すればするほど、その動きに一切ランダム性がないって気づくんだよね。一歩一歩がコード化されてて、予測可能で、そして何より----破壊できる。結局Minecraftのソースコードを掘りまくって、パスファインディングがどう動いてるのか完全に理解したんだけど、要はモブを文字通りマインドコントロールできるってことだ。つまり、ランダムが決める場所じゃなくて、こっちが行かせたい場所に強制的に動かせるんだ。

このガイドは俺が掘り当てた全部だ。AIシステム、A*アルゴリズム、隠されたMalice値、サバイバルで使える悪用技。ピッケル持ってこい。


Mob AIの仕組み(ネタバレ:結構バカ)

Goals(目標)

どのモブにも目標のリストがある。やれることと、どれだけそれをやりたいかのリスト。数字が小さいほど優先度高い。地獄のTODOリストみたいなもんだ。

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

ゾンビがカメの卵を無視してこっちを追いかけてくるのを見たことあるだろ?その理由がこれ:ZombieAttackTurtleEggGoalは優先度4、一方ZombieAttackGoal(「顔を食う」目標)は優先度2。ゾンビは脈のあるスナックの方が好きなんだ。

俺たちが実際に気にするべき目標はWaterAvoidingRandomStrollGoal、優先度7。「他にやることないからぶらぶらする」目標だ。ここからが本番。

Movement(あるいは「乱歩が1ティックにつき1/60の確率で発動する仕組み」)

毎ティック(0.05秒ごと)、ゲームはcanUse()を呼び出してモブが動く気あるかチェックする。確率は1ティックにつき1/60。非効率極まりない設計で、それを愛してる。

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

まとめると:モブに乗ってる→ダメ、5秒間何もしてない→ダメ、RNGがノーって言った→ダメ。ゲームはマジでモブに動いてほしくないんだな。

でも動くときは、getPosition()が動き出す:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

最後の2つの数字?XZ半径とY半径だ。水中ではモブはより広く探す(15対10)。陸が見つからなければsuper.getPosition()にフォールバックして、水中を受け入れる。結果:モブは水から出たがる。 動物が必死に岸に向かって泳ぐ理由はこれだ。

面白い詳細:モブがLandRandomPosじゃなくてsuper.getPosition()を選ぶ確率が文字通り0.1%ある。千分の一。Mojangさんマジか xD

LandRandomPos:すべてを壊す最適化

これが俺の一番好きなステップだ。パスファインディングを悪用可能にする、最も美しい技術的カオス。

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid。名前がすべてを物語ってる。選んだ位置が固体ブロックの中だった場合、ゲームはそれを空中に出るまで上に押し上げる。

最適化なんだ:地下の位置をスキップする時間を無駄にしない代わりに、ゲームは単に地表に押し出す。賢い?確かに。でも、これで巨大なバイアスが生まれる:モブは高台を好む。

考えてみて。地下にブロックがたくさんある場合、ゲームは10個のランダムな位置を生成する。ブロック内にあるものは上に押し出される。密集した場所(丘の下とか)は空洞の場所より多くの有効な位置を生み出す。結果:モブは統計的に丘の方に行きやすい。

信じてくれ、これからこれをガッツリ破壊するからな。

選択:最高のブロックが勝つ

10個の位置、1つの勝者、スコアコンテスト:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

最高スコアの位置が勝つ。そしてスコアリング条件を知ってれば、こっちの位置を勝たせられる。選挙を不正操作するようなもんだ。


Mobの好み(あるいは「牛が道路を渡った理由」)

モブによって好みが違う。そしてそれがすべてを変える。

Mob 好きなもの
動物(牛、羊、豚) 草ブロック、明るさ
モンスター(ゾンビ、スケルトン) 暗闇(ヒップスター)
カメ 水 > 砂 > 明るさ
ホグリン crimson_nylium;warped_fungusは嫌い
ストライダー 溶岩とそれだけ
シルバーフィッシュ 寄生可能ブロック
ガーディアン 水 + 明るさ(スノッブ)
ムーシュルーム 菌糸 + 明るさ
ハチ 空気。そう、空気が好きなんだ。
// Animal: look down, if grass -> max score
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: literally the opposite
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

モンスターは要するに「明るいとこはマイナススコア、俺は出てくわ。」明るさでマジでキレるんだ xD

つまり----文字通り----草と明るさで動物を誘導して、暗闇でモンスターを誘導できる。バカみたいで天才的だ。


MinecraftのA*(秘密の計算式)

MinecraftはパスファインディングにA*(Aスター)を使ってる。でもMojangは独自のひねりを加えた:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = すでに移動した距離(1ブロックにつき1、斜めは約1.41)
  • h(n) = 目的地までの直線距離
  • 1.5 = Mojangがちょっと壊れたのが好きだから

通常のAはf(n) = g(n) + h(n)。MOJANGは1.5倍を追加した。 なぜ?アルゴリズムがより速く目的地に収束して、検索ブランチを減らすため。結果:パスは「十分良い」けど常に最適とは限らない。酔っぱらったAだ。

mermaid diagram

主な制限:モブは16ブロックしかパスファインディングできない(フォローレンジ)。目的地が遠すぎる場合、最も近い到達可能なブロックを選ぶ。つまり、範囲外にモノリスを建てれば、モブはそれに近づくための最も近いブロックに向かってパスを通す----動きを完全に予測可能にできる。

ゲームを壊す2つの悪用技

1. ブロック更新で再計算が強制される

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

モブのパス付近のブロック更新はすべて、1秒のクールダウンでA*の再計算を強制する。モブの隣に1秒クロックを置けば、常に再計算し続ける。毎秒リセットされるGPSみたいなもんだ。

そしてこれを50体のモブでやったら?ラグシティ。TPSさようなら。

2. パスファインディングMalice(ブロックコストペナルティ)

一部のブロックはモブを怖がらせる。文字通り。すべてのブロックにはenumで定義されたコストがある:

ブロック / 条件 Malice
ハニーブロック 通過に+8
粉雪 通行不可
閉じたドア 通行不可
炎 通過+16、隣接+8
動物 & 村人 炎 = -1(絶対ダメ)
サボテン / スイートベリー 通行不可;隣接+8
水 通過または隣接+8
マグマ 隣接+8

動物はさらに極端:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIREの-1.0Fは文字通り「禁止」。動物は炎の中を歩くくらいなら虚無に飛び込みたいんだ。

演習:壮大なパスコンテスト

村人が複数のパスから選ぶ場合:

  • パスA:15ブロックだけど水に隣接6回(各+8)
  • パスB:18ブロックで水ブロック2回(+8)+ 水隣接1回(+8)
  • パスC:14ブロック直線...でも炎 → 村人には通行不可
  • パスD:16ブロックでマグマ隣接1回(+8)+ ハニー隣接1回(+8)
  • パスE:25ブロックでサボテンだらけ(どこも+8)→ 合計90.82 LOL

勝者はたいていパスB:遠回りが報われる。なぜなら水が高いから。

村人は要するに脚のついたコスト計算機なんだ xD

モブによって選ぶパスが違う

村人:「炎?ノー感謝、バイバイ」 ゾンビ:「炎?OK爺ちゃん 炎の中を歩いてく」

村人が通ってゾンビは通らない----あるいはその逆----のハイウェイを文字通り作れる。


村人:究極のカオス

村人はMinecraftで最も誤解されてる存在だ。でも一度コードを読めば、ただの勤務時間のある予測可能な機械だってわかる。

センサーとメモリー

9個のセンサーが20ティック(1秒)ごとに動く。各センサーは村人の周りの半径をスキャンして、結果をメモリーに保存する。村人はすべてを見て、すべてを覚えて、それに応じて行動する。

アクティビティパッケージ

村人の脳は時間に基づいてアクティブになるアクティビティパッケージに分かれてる:

パッケージ 時間 村人の行動…
Core 24/7 ドアを開け、泳ぎ(80%の確率で)、POIを取得
Work 午前8時〜午後3時 「仕事だ」-- 仕事場に行く
Meet 午後3時〜午後5時 「ハッピーアワー!」-- 鐘のところに行き、交流
Rest 午後6時〜午前6時 「就寝時間」-- ベッドに行く
Idle 午前6時〜午前8時、午後5時〜午後6時 「暇だ」-- ぶらつき、繁殖、ベッドで飛び跳ねる
Panic ダメージ / 敵対 「逃げろ」-- 逃走

Panicだけが他すべてを中断できる唯一のパッケージだ。村人が寝てようが働いてようが、ゾンビがいたらパニックモード。

Acquire POI:ワイヤレスレッドストーンを可能にする仕組み

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POIは48ブロック半径内のすべての有効なPOIをスキャンする。最も近い5つを保持し、パスが存在するか確認して、到達可能な最も近いものを取得する。各POIには限られたスロットがある:

  • 仕事場:1スロット
  • ベッド:1スロット
  • 鐘:32スロット

ヤバいこと:スロットは到着時じゃなくて取得時に予約される。 村人はマップの反対側からコンポスターをロックできるのに、そこにたどり着く必要すらない。

どこへ向かってるかわかるだろ?

ワイヤレスレッドストーン。そう、ワイヤレスだ。

  1. 村人をコンポスターへのパスがある状態でトロッコに入れる
  2. 村人がコンポスターを取得する(スロット取得、他の誰も使えない)
  3. 村人は遠すぎてクリックできない -- 骨粉はそのまま
  4. この村人を世界中のどこにでも移動させろ、スロットは保持され続ける
  5. 何かを起動したいとき、村人を殺せ
  6. スロットが解放され、別の村人がコンポスターを取得し、骨粉を取り除く
  7. ブロック更新 → 任意のレッドストーン回路が起動

これでワイヤレスレッドストーンができた。世界中どこでも送信可能で、経路にチャンクロードも必要ない。これにエンダーパール静止ステーションを組み合わせれば、村人を殺すだけでどこからでもテレポートできる。

俺の一番好きな使い方?賞金稼ぎミニゲーム:複数の村人にコンポスターを持たせて、プレイヤーは正しい村人を殺して出口を起動しなきゃいけない。完全に意味不明な仕組みだ xD

パスファインディングデッドロック(あるいは「永遠にフリーズする村人」)

Acquire POI(パスが見える)と実際のナビゲーション(それに従うのを拒否する)の間にバグがある。仕事場の上のブロックが歩行可能じゃないときに発生する。結果:

  • Coreパッケージ:「POIを取得したい」
  • ナビゲーション:「そこに歩けない」
  • 結果:村人は永遠にフリーズして、自分自身と戦い続ける。

文字通り凍った村人、装飾や小道具として使える。防具立てタンク?あり。動かない警備員?あり。陰気?かも。効果的?めっちゃ xD


結論

Mobのパスファインディングはランダムじゃない。決定論的でスコアベースのシステムで、予測可能で破壊可能だ。

覚えておくべき3つのこと:

  1. 下の固体ブロック = 高さバイアス -- 地下を埋めるか空にしてモブを誘導
  2. Maliceはモブごとに違う -- あるモブは通って他は通らないルートを作れる
  3. POIスロットは遠距離で予約される -- 無料のワイヤレスレッドストーン、テレポート、全部使える

Minecraftのソースコードは使われていない仕組みの宝庫だ。俺は何時間もデコンパイルされたJavaを読んだけど、正直?どの行も機能するイースターエッグだ。ただしこれらはサバイバルで村人ワイヤレスレッドストーンに使える。最高のゲーム確認済み xD

Minecraft 경로찾기 로직과 응용

A* 알고리즘, 블록 패널티, POI 메커니즘으로 몹의 움직임을 제어, 예측, 활용하는 방법 -- 무선 레드스톤부터 최적화된 농장까지.

소개

양들이 벽에 머리 박는 거 몇 시간째 보고 있었음.

내 인생 최고의 투자 xD

몹들이 움직이는 거 계속 보다 보면 알게 됨 -- 이새끼들 랜덤이 아님. 한 땀 한 땀 코드로 짜여 있고, 예측 가능하고, 결정적으로 부술 수 있음. 결국 마인크래프트 소스코드를 까보게 됐는데, 알고 보니 몹들을 정신지배 할 수 있었음. 말 그대로 네가 원하는 곳으로 가게 조종 가능. 랜덤이 아니라.

이 가이드는 내가 뒤지면서 찾은 모든 내용임. AI 시스템, A* 알고리즘, 숨은 악의값(?), 서바이벌에서 써먹을 수 있는 익스플로잇까지. 곡괭이 챙겨라.


몹 AI 작동 원리 (스포: 좀 멍청함)

목표

모든 몹은 목표 리스트를 가지고 있음. 할 수 있는 일, 그리고 얼마나 하고 싶은지. 숫자가 낮을수록 우선순위 높음. 지옥에서 온 투두리스트 같음.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

좀비가 거북이 알 씹고 너한테 달려드는 거 본 적 있음? 그게 이유임. ZombieAttackTurtleEggGoal은 4순위인데 ZombieAttackGoal (면상 뜯기)는 2순위임. 좀비는 맥박 뛰는 간식을 더 좋아함.

우리가 진짜 관심있는 건 WaterAvoidingRandomStrollGoal, 7순위임. "딱히 할 거 없어서 그냥 돌아다닐래" 목표. 여기가 재미 시작되는 곳임.

이동 (또는 "랜덤 워크가 틱당 1/60 확률인 이유")

매 틱(0.05초)마다 게임이 canUse()를 호출해서 몹이 움직일 의향이 있는지 체크함. 확률 1/60. 끔찍하게 비효율적인 설계, 근데 사랑스러움.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

요약: 몹 타고 있음 -> 안 됨, 5초 동안 아무것도 안 함 -> 안 됨, RNG가 싫음 -> 안 됨. 게임이 몹한테 진짜 움직이기 싫어함.

근데 움직이기로 했을 때, getPosition()이 작동함:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

끝에 두 숫자? XZ 반경과 Y 반경임. 물에선 몹이 더 멀리 찾음(15 vs 10). 땅을 못 찾으면 물도 허용하는 super.getPosition()으로 폴백함. 결과: 몹들은 물 밖으로 나가고 싶어함. 그래서 동물들이 미친 듯이 해안가로 헤엄쳐 가는 거임.

재밌는 디테일: 몹이 LandRandomPos 대신 super.getPosition()을 선택할 확률이 말 그대로 0.1%임. 천분의 일. 모장 뭐함 xD

LandRandomPos: 모든 걸 망가뜨리는 최적화

이게 내가 제일 좋아하는 단계임. 가장 아름다운 기술적 쓰레기. 길찾기를 익스플로잇 가능하게 만듦.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. 이름이 다 말해줌. 고른 위치가 고체 블록 안에 있으면, 게임이 위로 밀어 올림.

최적화임: 지하 위치 스킵하는 데 시간 낭비하지 말고 그냥 표면으로 올리자. 똑똑함? ㅇㅇ. 근데 이게 엄청난 편향을 만듦: 몹들은 높은 지형을 선호함.

생각해봐. 지하에 블록이 많음, 게임이 10개 랜덤 위치 생성함. 블록 안에 있는 것들은 위로 밀려남. 밀집된 지역(언덕 아래)이 빈 공간보다 더 많은 유효 위치를 만듦. 결과: 몹이 통계적으로 언덕 쪽으로 더 자감.

믿어, 이거 곧 마개조할 거임.

선택: 최고 블록이 승리

10개 위치, 하나의 승자, 점수 대결:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

가장 높은 점수 받은 위치가 승리함. 그리고 점수 기준을 알면 네가 원하는 위치를 이기게 만들 수 있음. 선거 조작하는 느낌임.


몹 취향 (또는 "네 소가 길을 건넌 이유")

모든 몹은 취향이 다름. 그리고 이게 모든 걸 바꿈.

몹 좋아하는 거
동물 (소, 양, 돼지) 잔디 블록, 빛
몬스터 (좀비, 스켈레톤) 어둠 (히스터임)
거북이 물 > 모래 > 빛
호글린 crimson_nylium; warped_fungus 싫어함
스트라이더 용암, 그것만
은어 감염 가능 블록
가디언 물 + 빛 (스놉)
무시룸 균사체 + 빛
벌 공기. ㅇㅇ, 공기를 선호함.
// 동물: 아래 보기, 잔디면 최대 점수
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// 몬스터: 말 그대로 반대
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

몬스터는 "밝으면 점수 마이너스, 나갈게"임. 불들어오면 발작함 xD

그래서 잔디랑 빛으로 동물을 유도할 수 있고, 어둠으로 몬스터를 유도할 수 있음. 멍청하면서도 천재적임.


마인크래프트의 A* (비밀 공식)

마인크래프트는 길찾기에 A*(A-star)를 사용함. 근데 모장이 자기들만의 트위스트를 추가함:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = 이미 이동한 거리 (블록당 1, 대각선은 ~1.41)
  • h(n) = 목표까지 직선 거리
  • 1.5 = 모장이 약간 고장난 걸 좋아해서

일반 A는 f(n) = g(n) + h(n)임. 모장이 1.5 배수를 추가함. 왜? 알고리즘이 목적지에 더 빨리 도달하게 하고 검색 가지를 덜 자르려고. 결과: 경로가 "충분히 좋음" 항상 최적은 아님. 취한 A임.

mermaid diagram

핵심 제한: 몹은 최대 16블록까지만 길찾기 가능함 (추종 범위). 목적지가 너무 멀면, 도달 가능한 가장 가까운 블록을 선택함. 이 말은 범위 밖에 기념비를 세우면 몹이 더 가까워지는 가장 가까운 블록으로 이동하게 만들 수 있음 -- 완전 예측 가능해짐.

게임을 부수는 두 가지 익스플로잇

1. 블록 업데이트가 재계산을 강제함

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

몹 경로 근처의 모든 블록 업데이트가 1초 쿨다운으로 A* 재계산을 강제함. 몹 옆에 1초 클락을 두면 계속 재계산함. 1초마다 리셋되는 GPS 같음.

그리고 이걸 50마리 몹으로 하면? 렉 도시. RIP TPS.

2. 길찾기 Malice (블록 비용 패널티)

어떤 블록은 몹을 무서워하게 만듦. 말 그대로. 모든 블록은 열거형으로 정의된 연관 비용이 있음:

블록 / 조건 Malice
꿀 블록 통과 +8
가루 눈 통과 불가
닫힌 문 통과 불가
불 통과 +16, 인접 +8
동물 & 주민 불 = -1 (HARD NO)
선인장 / 달콤한 열매 통과 불가; 인접 +8
물 통과 또는 인접 +8
마그마 인접 +8

동물은 더 나아감:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE가 -1.0F라는 건 말 그대로 "금지"라는 뜻임. 동물은 불 속을 걷느니 차라리 공허로 뛰어들겠음.

연습문제: 위대한 경로 대회

여러 경로 사이에서 선택하는 주민:

  • 경로 A: 15블록, 근데 물 6개 인접 (+8씩)
  • 경로 B: 18블록, 물 2개 통과 (+8) + 물 1개 인접 (+8)
  • 경로 C: 14블록 직진... 근데 불 -> 주민 통과 불가
  • 경로 D: 16블록, 마그마 1개 인접 (+8) + 꿀 1개 인접 (+8)
  • 경로 E: 25블록, 선인장 도배 (+8 everywhere) -> 총 90.82 ㅋㅋㅋ

승자는 보통 경로 B임: 우회하는 게 이득인 이유는 물이 비싸서임.

주민은 다리 달린 계산기임 xD

몹마다 다른 경로를 선택함

주민: "불? 난 뒤질래 ㅂ2" 좀비: "불? ㅇㅋ 불타면서 걸음"

주민이 타고 좀비는 안 타는 길을 말 그대로 만들 수 있음 -- 또는 그 반대로.


주민: 궁극의 개판

주민은 마인크래프트에서 가장 오해받는 존재임. 근데 코드를 읽고 나면 그냥 업무 시간이 있는 예측 가능한 기계라는 걸 알게 됨.

센서와 기억

9개의 센서가 20틱(1초)마다 실행됨. 각각 주변 반경을 스캔해서 결과를 기억에 저장함. 주민은 모든 걸 보고, 모든 걸 기억하고, 그에 따라 행동함.

활동 패키지

주민의 뇌는 시간에 따라 활성화되는 활동 패키지로 나뉘어 있음:

패키지 시간 주민이 하는 일
코어 24/7 문 열기, 수영 (80% 확률), POI 획득
일 오전 8시-오후 3시 "일해야지" -- 작업대로 걸어감
회의 오후 3시-오후 5시 "해피아워!" -- 종으로 감, 사교 활동
휴식 오후 6시-오전 6시 "잘 시간" -- 침대로 감
한가함 오전 6시-8시, 오후 5시-6시 "심심함" -- 배회, 번식, 침대에서 폴짝
공황 피해 / 적 발견 "도망쳐" -- 도주

공황만이 다른 모든 패키지를 중단시킬 수 있음. 주민이 자거나 일하는 중이어도 좀비가 있으면 공황 모드임.

POI 획득: 무선 레드스톤을 가능하게 하는 매커니즘

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI는 48블록 반경 내 모든 유효 관심 지점을 스캔함. 가장 가까운 5개를 유지하고, 경로가 있는지 확인해서 도달 가능한 가장 가까운 지점을 획득함. 각 POI에는 제한된 슬롯이 있음:

  • 작업대: 1 슬롯
  • 침대: 1 슬롯
  • 종: 32 슬롯

미친 건: 슬롯이 도착할 때가 아니라 획득할 때 예약됨. 주민이 지도 반대편에 있는 퇴비통을 잠글 수 있음, 도달하지도 않고.

어디로 흘러가는지 보임?

무선 레드스톤. ㅇㅇ, 무선.

  1. 주민을 퇴비통 경로가 있는 마인카트에 태움
  2. 퇴비통 획득 (슬롯 차지, 아무도 못 씀)
  3. 주민이 너무 멀어서 클릭 못 함 -- 뼛가루 그대로
  4. 이 주민을 세계 어디로든 이동시켜도 슬롯 유지됨
  5. 작동시키고 싶을 때 주민 죽임
  6. 슬롯 해제, 다른 주민이 퇴비통 획득, 뼛가루 제거
  7. 블록 업데이트 -> 모든 레드스톤 회로 작동

전 세계에서 전송 가능한 무선 레드스톤을 만든 거임, 경로에 청크 로딩도 필요 없음. 이걸 엔더 진자 스테이시스 체임버에 연결하면 주민 하나 죽여서 어디서든 텔레포트 가능.

내가 제일 좋아하는 용도? 현상금 사냥꾼 미니게임: 여러 주민이 각자 퇴비통을 가지고 있고, 플레이어가 올바른 주민을 죽여서 출구를 활성화해야 함. 완전 xD 매커니즘임.

길찾기 데드락 (또는 "영원히 멈추는 주민")

Acquire POI(경로를 봄)와 실제 내비게이션(따라가길 거부함) 사이에 버그가 있음. 작업대 위 블록이 걸을 수 없을 때 발생함. 결과:

  • 코어 패키지: "POI를 획득하고 싶어"
  • 내비게이션: "거기 못 걸어가"
  • 결과: 주민이 영원히 얼어붙음, 자기 자신과 싸우면서.

말 그대로 얼어붙은 주민, 장식이나 소품으로 사용 가능. 갑옷 거치대 탱크? ㅇㅇ. 움직이지 않는 경비병? ㅇㅇ. 막장? 아마도. 효과적? 완전 xD


결론

마인크래프트 몹 길찾기는 랜덤이 아님. 결정론적이고 점수 기반 시스템이며, 예측 가능하고 부술 수 있음.

세 가지 기억할 점:

  1. 아래 고체 블록 = 높이 편향 -- 바닥을 채우거나 비워서 몹 유도
  2. Malice는 몹마다 다름 -- 어떤 몹은 타고 어떤 몹은 안 타는 경로 생성
  3. POI 슬롯은 원거리에서 예약됨 -- 공짜 무선 레드스톤, 텔레포테이션, 다 됨

마인크래프트의 소스코드는 덜 발굴된 매커니즘의 금광임. 디컴파일된 자바를 몇 시간 읽었는데 솔직히? 모든 줄이 기능성 이스터에그임. 근데 이건 서바이벌에서 주민으로 무선 레드스톤이 된다는 게 다름. 갓겜 맞음 xD

Minecraft Pathfinding Mantığı ve Uygulamaları

A* algoritması, blok cezaları ve POI mekanikleri ile mob

Giriş

Koyunların duvarlara toslayışını izleyerek saatler harcadım.

Hayatımın en iyi yatırımı xD

Bu mobları ne kadar izlersen, hareketlerinde rastgele hiçbir şey olmadığını o kadar fark ediyorsun. Her adım kodlanmış, tahmin edilebilir ve en önemlisi -- kırılabilir. Sonunda Minecraft'ın kaynak kodunu didik didik ederek pathfinding'in tam olarak nasıl çalıştığını anladım ve bulduğum şey şu: mobları resmen zihin kontrolü yapabilirsin. Yani, onları RASGELENİN değil, SENİN istediğin yere gitmeye zorlayabilirsin.

Bu rehber, kazarken bulduğum her şey. AI sistemi, A* algoritması, gizli malice değerleri, survival'da kullanabileceğin exploit'ler. Kazmanı kap.


Mob AI Nasıl Çalışır (spoiler: biraz gerizekalı)

Hedefler (Goals)

Her mob'un bir goal listesi vardır. YAPABİLECEĞİ şeyler ve ne kadar çok İSTEDİĞİ. Düşük sayı = yüksek öncelik. Cehennemden bir yapılacaklar listesi gibi.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Hiç bir zombie'nin kaplumbağa yumurtasını görmezden gelip seni kovaladığını gördün mü? İşte nedeni: ZombieAttackTurtleEggGoal'ın önceliği 4, ZombieAttackGoal (yani "suratını ye" hedefi) ise öncelik 2. Zombiler nabzı olan atıştırmalıkları tercih ediyor.

Asıl umursadığımız hedef WaterAvoidingRandomStrollGoal, öncelik 7. "Yapacak daha iyi bir şeyim yok o yüzden boş boş dolanayım" hedefi. İşte eğlence burada başlıyor.

Hareket (ya da "rastgele yürüyüşün tick başına 60'ta 1 şansı olması")

Her tick'te (her 0.05 saniyede), oyun hareket edip edemeyeceğini kontrol etmek için canUse()'u çağırır. Tick başına 60'ta 1 şans. Dehşet verici derecede verimsiz bir tasarım ve buna bayılıyorum.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Özet geçmek gerekirse: mob'a biniyorsan -> hayır, mob 5 saniyedir hiçbir şey yapmadıysa -> hayır, RNG hayır derse -> hayır. Oyun GERÇEKTEN mobların hareket etmesini istemiyor.

Ama hareket ettiğinde, getPosition() devreye giriyor:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Sondaki iki sayı? XZ yarıçapı ve Y yarıçapı. Suda mob daha geniş arar (15 vs 10). Kara bulamazsa suyu kabul eden super.getPosition()'a düşer. Sonuç: moblar sudan ÇIKMAK ister. İşte bu yüzden hayvanların kıyıya doğru manyak gibi yüzer.

Eğlenceli detay: mob'un LandRandomPos yerine super.getPosition()'ı seçme ihtimali %0.1. Binde bir. Mojang yani xD

LandRandomPos: her şeyi kıran optimizasyon

Bu BENİM en sevdiğim adım. Pathfinding'i exploit edilebilir yapan en güzel teknik karmaşa.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. İsmi her şeyi anlatıyor. Seçilen pozisyon katı bir bloğun içindeyse, oyun onu havaya çıkana kadar yukarı iter.

Bu bir optimizasyon: yer altı pozisyonlarını atlamakla zaman harcamak yerine, oyun onları direkt yüzeye fırlatır. Akıllıca mı? Evet. Ama DEV bir önyargı yaratıyor: moblar yüksek zemini tercih ediyor.

Düşünsene. Yer altında bir sürü blok var, oyun 10 rastgele pozisyon üretiyor. Blok içindekiler yukarı itiliyor. Yoğun alanlar (bir tepenin altı) içi boş alanlardan daha fazla geçerli pozisyon üretiyor. Sonuç: mob istatistiksel olarak tepeye doğru daha sık gidiyor.

Güven bana, bunu kırmak üzereyiz.

Seçim: en iyi blok kazanır

10 pozisyon, bir kazanan, bir puan yarışması:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

En yüksek puanı alan pozisyon KAZANIR. Ve puanlama kriterlerini biliyorsan, SENİN pozisyonunu kazandırabilirsin. Seçim hilesi yapmak gibi.


Mob Tercihleri (ya da "ineğin neden karşıdan karşıya geçtiği")

Her mob'un farklı zevkleri var. Ve bu her şeyi değiştiriyor.

Mob Neyi sever
Hayvanlar (inek, koyun, domuz) Çim blokları, ışık
Canavarlar (zombi, iskelet) Karanlık (hipsterlar)
Kaplumbağalar Su > kum > ışık
Hoglinler crimson_nylium; warped_fungus'tan nefret eder
Striderlar Lav ve BAŞKA HİÇBİR ŞEY
Gümüş balıkları Infestable bloklar
Guardian'lar Su + ışık (snoblar)
Mooshroom'lar Miselyum + ışık
Arılar Hava. Evet, HAVAYI tercih ediyorlar.
// Hayvan: aşağı bak, çim varsa -> max puan
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Canavar: kelimenin tam anlamıyla tersi
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Canavarlar temelde "ışıklıysa, negatif puan, ben kaçarım" gibi. Işık seviyesinde RESMEN sinir krizi geçiriyorlar xD

Yani hayvanları çim ve ışıkla, canavarları da karanlıkla -- kelimenin tam anlamıyla -- yönlendirebilirsin. Aynı anda hem aptalca hem de zekice.


Minecraft'ta A* (gizli formül)

Minecraft pathfinding için A* (A-star) kullanıyor. Ama Mojang kendi dokunuşunu eklemiş:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = şimdiye kadar gidilen mesafe (blok başına 1, çaprazda ~1.41)
  • h(n) = hedefe düz çizgi mesafesi
  • 1.5 = çünkü Mojang işleri biraz bozuk seviyor

Normal A* f(n) = g(n) + h(n) kullanır. MOJANG 1.5 ÇARPANI EKLEMİŞ. Neden mi? Algoritma hedefe daha hızlı kilitlensin ve daha az dal araştırsın diye. Sonuç: yol "yeterince iyi" ama her zaman optimal değil. Sarhoş bir A* yani.

mermaid diagram

Ana sınırlama: bir mob sadece 16 blok öteye pathfind yapabilir (follow range'i). Hedef çok uzaktaysa, ulaşabileceği en yakın bloğu seçer. Bu, menzil dışında bir anıt inşa edebileceğin ve mob'un kendisine en yakın bloka doğru yol alacağı anlamına gelir -- hareketini tamamen tahmin edilebilir kılar.

Oyunu kıran iki exploit

1. Blok güncellemeleri yeniden hesaplamayı zorlar

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Mob'un yoluna yakın her blok güncellemesi, 1 saniye bekleme süresiyle bir A* yeniden hesaplamasını zorlar. Bir mob'un yanına 1 saniyelik bir saat koy, SÜREKLİ yeniden hesaplasın. Her saniye sıfırlanan bir GPS gibi.

Ve bunu 50 mob'la yaparsan? Lag şehri. TPS'in canı cehenneme.

2. Pathfinding Malice (blok maliyet cezaları)

Bazı bloklar mobları korkutuyor. Cidden. Her bloğun bir enum tarafından tanımlanmış ilişkili bir maliyeti vardır:

Blok / Durum Malice
Bal bloğu İçinden geçmek +8
Toz kar Geçilemez
Kapalı kapılar Geçilemez
Ateş İçinden +16, yanından +8
Hayvanlar & Köylüler Ateş = -1 (KATI HAYIR)
Kaktüs / Tatlı meyve Geçilemez; yanı = +8
Su İçinden veya yanından +8
Magma Yanından +8

Hayvanlar daha da ileri gidiyor:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE, -1.0F'de kelimenin tam anlamıyla "yasak". Bir hayvan ateşin içinden geçmektense boşluğa atlamayı tercih eder.

Egzersiz: büyük yol yarışı

Birden fazla yol arasında seçim yapan bir köylü:

  • Yol A: 15 blok ama 6 blok su kenarı (her biri +8)
  • Yol B: 18 blok, 2 su bloğu (+8) + 1 su-kenarı (+8)
  • Yol C: 14 blok düz... ama ateş -> köylüler için GEÇİLEMEZ
  • Yol D: 16 blok, 1 magma-kenarı (+8) + 1 bal-kenarı (+8)
  • Yol E: 25 blok, her yerde kaktüs (her yerde +8) -> 90.82 toplam LOL

Kazanan genelde Yol B: detay yolu işe yarar çünkü su PAHALI.

Bir köylü temelde bacakları olan bir maliyet hesaplayıcısıdır xD

Her mob farklı yollar seçer

Bir köylü: "ateş mi? YOK HAYIR BEN KAÇTIM" Bir zombi: "ateş mi? tamam boomer yanarak içinden geçer"

Köylülerin gidip zombilerin gitmediği -- ya da tam tersi -- yollar inşa edebilirsin.


Köylüler: nihai karmaşa

Köylüler Minecraft'taki en yanlış anlaşılan şey. Ama kodu okuduğunda, sadece mesai saatleri olan tahmin edilebilir makineler olduklarını anlıyorsun.

Sensörler ve hafıza

Her 20 tick'te (1 saniye) çalışan 9 sensör. Her biri köylünün etrafındaki bir yarıçapı tarar ve sonucu hafızada saklar. Köylü her şeyi görür, her şeyi hatırlar ve ona göre hareket eder.

Aktivite paketleri

Köylünün beyni, saate göre etkinleşen aktivite paketlerine bölünmüştür:

Paket Zaman Köylü...
Core 7/24 Kapıları açar, yüzer (%80 ihtimalle), POI EDİNİR
Work 08:00-15:00 "Çalışmam gerek" -- iş istasyonuna yürür
Meet 15:00-17:00 "Mutlu saat!" -- zile gider, sosyalleşir
Rest 18:00-06:00 "Uyku vakti" -- yatağa gider
Idle 06:00-08:00, 17:00-18:00 "Sıkıldım" -- gezinir, ürer, yataklara zıplar
Panic Hasar / düşman "KAÇ" -- KAÇAR

Panic diğer TÜM paketleri kesebilen tek pakettir. Köylü uyusa veya çalışıyor olsa bile, bir zombi varsa PANİK MODU.

POI Edinme: kablosuz redstone'u mümkün kılan mekanik

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI, 48 blok yarıçapındaki tüm geçerli ilgi noktalarını tarar. En yakın 5 tanesini tutar, bir yol olup olmadığını kontrol eder ve ulaşılabilir en yakın olanı edinir. Her POI'nin sınırlı slotu vardır:

  • İş istasyonları: 1 slot
  • Yataklar: 1 slot
  • Ziller: 32 slot

ÇILGIN OLAN ŞEY: slot, VARILMA anında değil, EDİNME anında ayrılır. Bir köylü, haritanın öbür ucundan bir komposteri kilitleyebilir -- oraya asla ulaşmasa bile.

Nereye vardığını görüyor musun?

Kablosuz redstone. Evet, KABLOSUZ.

  1. Bir köylüyü bir kompostere giden yolu olan bir maden arabasına koy
  2. Komposteri edinir (slot dolu, kimse kullanamaz)
  3. Köylü tıklayamayacak kadar uzaktır -- gübre yerinde kalır
  4. Bu köylüyü DÜNYADA HERHANGİ BİR YERE taşı, slotu tutmaya devam eder
  5. Bir şeyi etkinleştirmek istediğinde, köylüyü ÖLDÜR
  6. Slot boşalır, başka bir köylü komposteri edinir, gübreyi alır
  7. BLOK GÜNCELLEMESİ -> herhangi bir redstone devresi etkinleşir

Tam dünya çapında iletilebilen, yol üzerinde sıfır chunk yüklemesi gerektiren kablosuz redstone yarattın. Bunu bir ender incisi stasis odasına bağla ve bir köylüyü öldürerek herhangi bir yerden ışınlan.

Benim favori kullanımım? Bir ödül avcısı minigame'i: her biri komposterli birden çok köylü, oyuncu çıkışı etkinleştirmek için DOĞRU köylüyü öldürmek zorunda. Tamamen wtf bir mekanik xD

Pathfinding Deadlock (ya da "sonsuza kadar donan köylü")

Acquire POI (bir yol gören) ile gerçek navigasyon (onu takip etmeyi reddeden) arasında bir hata var. İş istasyonunun üstündeki blok yürünebilir değilse olur. Sonuç:

  • Core paketi: "POI'yi edinmek istiyorum"
  • Navigasyon: "Oraya yürüyemem"
  • Sonuç: köylü DONAR, sonsuza kadar, kendi kendisiyle savaşır.

Cidden donmuş köylüler, dekorasyon veya aksesuar olarak kullanılabilir. Zırh standı tankı? Evet. Hareket etmeyen bir muhafız? Evet. Ürkütücü mü? Belki. Etkili mi? Kesinlikle xD


Sonuç

Minecraft'ta mob pathfinding'i rastgele değil. Belirleyici, puana dayalı bir sistem, tahmin edilebilir VE kırılabilir.

Unutulmaması gereken üç şey:

  1. Altındaki katı bloklar = yükseklik önyargısı -- mobları yönlendirmek için alt zemini doldur veya boşalt
  2. Malice mob'a göre değişir -- bazılarının gidip diğerlerinin gitmediği yollar oluştur
  3. POI slotları uzaktan ayrılır -- bedava kablosuz redstone, ışınlanma, hepsi

Minecraft'ın kaynak kodu, az kullanılmış mekaniklerle dolu bir altın madeni. Decompile edilmiş Java okumak için saatler harcadım ve dürüst olmak gerekirse? Her satır işlevsel bir Paskalya yumurtası. Ama bunlar survival'da köylülerle kablosuz redstone için çalışıyor. En iyi oyun onaylandı xD

Logica di pathfinding di Minecraft e le sue applicazioni

Come l'algoritmo A*, le penalità dei blocchi e i POI permettono di

Introduzione

Ho passato ore a guardare pecore sbattere contro i muri.

Miglior investimento della mia vita xD

Più guardi questi mob, più realizzi che nei loro movimenti non c'è niente di casuale. Ogni passo è programmato, prevedibile e, cosa più importante, sfruttabile. Alla fine mi sono messo a spulciare il codice sorgente di Minecraft per capire esattamente come funziona il pathfinding, e ho scoperto che puoi letteralmente controllare i mob con la mente. Tipo, forzarli ad andare dove VUOI tu, non dove decide il caso.

Questa guida è tutto quello che ho scoperto mentre scavavo. Il sistema AI, l'algoritmo A*, i valori nascosti di malus, gli exploit che puoi usare in survival. Prendi il tuo piccone.


Come Funziona la Mob AI (spoiler: è un po' tonta)

Goals

Ogni mob ha una lista di goal. Cose che PUÒ fare, e quanto tanto le VUOLE fare. Numero più basso = priorità più alta. Tipo una lista di cose da fare infernale.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Mai visto uno zombie ignorare un uovo di tartaruga per inseguire te invece? Ecco perché: ZombieAttackTurtleEggGoal ha priorità 4, mentre ZombieAttackGoal (il goal "mangiami la faccia") ha priorità 2. Gli zombie preferiscono spuntini col polso.

Il goal che ci interessa davvero è WaterAvoidingRandomStrollGoal, priorità 7. Il goal "non ho niente di meglio da fare quindi giro a caso". È qui che inizia il divertimento.

Movimento (ovvero "come un random walk ha 1 possibilità su 60 per tick")

Ogni tick (ogni 0.05 secondi), il gioco chiama canUse() per controllare se il mob ha voglia di muoversi. 1 possibilità su 60 per tick. Design orribilmente inefficiente, e lo adoro.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Quindi, per riassumere: se stai cavalcando il mob -> no, se il mob non ha fatto niente per 5 secondi -> no, se l'RNG dice no -> no. Il gioco NON VUOLE davvero che i mob si muovano.

Ma quando si muove, getPosition() prende il controllo:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Quei due numeri alla fine? Raggio XZ e raggio Y. In acqua, il mob cerca più lontano (15 vs 10). Se non trova terra, ripiega su super.getPosition() che accetta l'acqua. Risultato: i mob VOGLIONO uscire dall'acqua. Ecco perché i tuoi animali nuotano come pazzi verso la riva.

Dettaglio divertente: c'è letteralmente lo 0.1% di possibilità che il mob scelga super.getPosition() invece di LandRandomPos. Uno su mille. Mojang immagino xD

LandRandomPos: l'ottimizzazione che rompe tutto

Questo è il MIO passo preferito. Il più bel casino tecnico che rende il pathfinding sfruttabile.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. Il nome dice tutto. Se la posizione scelta è dentro un blocco solido, il gioco la spinge verso l'alto finché non è in aria.

È un'ottimizzazione: invece di perdere tempo a saltare posizioni sottoterra, il gioco le spinge direttamente in superficie. Intelligente? Sì. Ma crea un ENORME bias: i mob preferiscono le zone alte.

Pensaci. Tanti blocchi sottoterra, il gioco genera 10 posizioni casuali. Quelle dentro ai blocchi vengono spinte su. Le aree dense (sotto una collina) producono più posizioni valide delle aree vuote. Risultato: il mob va statisticamente più spesso verso la collina.

Fidati, stiamo per spaccare tutto quanto.

La selezione: vince il blocco migliore

10 posizioni, un vincitore, una gara a punti:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

La posizione col punteggio più alto VINCE. E se conosci i criteri di punteggio, puoi far vincere LA TUA posizione. È come truccare un'elezione.


Preferenze dei Mob (ovvero "perché la tua mucca ha attraversato la strada")

Ogni mob ha gusti diversi. E cambia tutto.

Mob Ama
Animali (mucche, pecore, maiali) Blocchi d'erba, luce
Mostri (zombie, scheletri) Buio (hipster)
Tartarughe Acqua > sabbia > luce
Hoglin crimson_nylium; odiano warped_fungus
Strider Lava e NULL'ALTRO
Silverfish Blocchi infestabili
Guardian Acqua + luce (snob)
Mooshroom Micelio + luce
Api Aria. Sì, preferiscono l'ARIA.
// Animale: guarda giù, se erba -> punteggio massimo
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Mostro: letteralmente l'opposto
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

I mostri sono tipo "se c'è luce, punteggio negativo, me ne vado." Si INCAZZANO con i livelli di luce xD

Quindi puoi -- letteralmente -- guidare gli animali con erba e luce, e i mostri con l'oscurità. È stupido e geniale allo stesso tempo.


A* in Minecraft (la formula segreta)

Minecraft usa A* (A-star) per il pathfinding. Ma Mojang ci ha messo il suo tocco:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = distanza già percorsa (1 per blocco, ~1.41 in diagonale)
  • h(n) = distanza in linea retta verso il target
  • 1.5 = perché a Mojang piacciono le cose leggermente rotte

L'A* normale usa f(n) = g(n) + h(n). MOJANG HA AGGIUNTO UN MOLTIPLICATORE 1.5. Perché? Così l'algoritmo punta dritto alla destinazione più velocemente e pota meno rami di ricerca. Risultato: il percorso è "abbastanza buono" ma non sempre ottimale. È un A* ubriaco.

mermaid diagram

Limitazione chiave: un mob può fare pathfinding solo per 16 blocchi (il suo follow range). Se la destinazione è troppo lontana, sceglie il blocco raggiungibile più vicino. Questo significa che puoi costruire un monolito fuori portata e il mob farà pathfinding verso il blocco più vicino che lo avvicina -- rendendo il suo movimento completamente prevedibile.

I due exploit che rompono il gioco

1. Gli aggiornamenti dei blocchi forzano il ricalcolo

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Ogni aggiornamento di blocco vicino al percorso del mob forza un ricalcolo A* con un cooldown di 1 secondo. Metti un clock da 1 secondo accanto a un mob e lui ricalcola COSTANTEMENTE. È come un GPS che si resetta ogni secondo.

E se lo fai con 50 mob? Lag city. RIP TPS.

2. Malus di Pathfinding (penalità sui blocchi)

Alcuni blocchi spaventano i mob. Letteralmente. Ogni blocco ha un costo associato definito da un enum:

Blocco / Condizione Malus
Blocco di miele +8 per attraversarlo
Neve polverosa Impraticabile
Porte chiuse Impraticabile
Fuoco +16 dentro, +8 adiacente
Animali & Villager Fuoco = -1 (NO ASSOLUTO)
Cactus / Bacche dolci Impraticabile; adiacente = +8
Acqua +8 dentro o adiacente
Magma +8 adiacente

Gli animali vanno anche oltre:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE a -1.0F è letteralmente "vietato." Un animale preferirebbe saltare nel vuoto piuttosto che camminare nel fuoco.

Esercizio: la grande gara di percorsi

Un villager che sceglie tra più percorsi:

  • Percorso A: 15 blocchi ma 6 confinanti con acqua (+8 ciascuno)
  • Percorso B: 18 blocchi con 2 blocchi d'acqua (+8) + 1 adiacente all'acqua (+8)
  • Percorso C: 14 blocchi dritti... ma fuoco -> IMPRATICABILE per i villager
  • Percorso D: 16 blocchi con 1 adiacente a magma (+8) + 1 adiacente a miele (+8)
  • Percorso E: 25 blocchi con cactus dappertutto (+8 ovunque) -> 90.82 totale LOL

Il vincitore è di solito il Percorso B: la deviazione ripaga perché l'acqua è COSTOSA.

Un villager è praticamente una calcolatrice di costi con le gambe xD

Ogni mob sceglie percorsi diversi

Un villager: "fuoco? NOPE CIAO" Uno zombie: "fuoco? OK boomer ci cammina dentro in fiamme"

Puoi letteralmente costruire autostrade che i villager prendono e gli zombie no -- o viceversa.


Villager: il caos finale

I villager sono la cosa più fraintesa di Minecraft. Ma dopo aver letto il codice, realizzi che sono solo macchine prevedibili con orari d'ufficio.

Sensori e memorie

9 sensori che girano ogni 20 tick (1 secondo). Ognuno scansiona un raggio attorno al villager e salva il risultato in memoria. Il villager vede tutto, ricorda tutto, e agisce di conseguenza.

Pacchetti di attività

Il cervello di un villager è diviso in pacchetti di attività che si attivano in base all'ora:

Pacchetto Orario Il villager...
Core 24/7 Apre porte, nuota (80% del tempo), ACQUISISCE POI
Lavoro 8:00-15:00 "Devo lavorare" -- va alla postazione
Incontro 15:00-17:00 "Happy hour!" -- va alla campana, socializza
Riposo 18:00-6:00 "Ora di dormire" -- va a letto
Ozioso 6:00-8:00, 17:00-18:00 "Mi annoio" -- vaga, si riproduce, salta sui letti
Panico Danno / ostile "CORRI" -- SCAPPA

Panico è l'unico pacchetto che può interrompere TUTTI gli altri. Anche se il villager sta dormendo o lavorando, se c'è uno zombie, MODALITÀ PANICO.

Acquisisci POI: la meccanica che abilita la redstone wireless

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquisisci POI scansiona un raggio di 48 blocchi per tutti i punti d'interesse validi. Tiene i 5 più vicini, controlla se esiste un percorso, e acquisisce il più vicino raggiungibile. Ogni POI ha slot limitati:

  • Postazioni di lavoro: 1 slot
  • Letti: 1 slot
  • Campane: 32 slot

La cosa FOLLE: lo slot viene riservato al momento dell'ACQUISIZIONE, non all'arrivo. Un villager può bloccarti un composter dall'altra parte della mappa senza mai raggiungerlo.

Vedi dove voglio arrivare?

Redstone wireless. Sì, WIRELESS.

  1. Metti un villager in un minecart con un percorso verso un composter
  2. Lui acquisisce il composter (slot occupato, nessun altro può usarlo)
  3. Il villager è troppo lontano per cliccarlo -- la farina d'ossa resta
  4. MUOVI questo villager DOVUNQUE nel mondo, lui mantiene lo slot
  5. Quando vuoi attivare la tua cosa, UCCIDI il villager
  6. Lo slot si libera, un altro villager acquisisce il composter, rimuove la farina d'ossa
  7. AGGIORNAMENTO BLOCCO -> qualsiasi circuito redstone attivato

Hai creato redstone wireless, trasmissibile attraverso l'intero mondo, senza bisogno di caricare chunk sul percorso. Collegala a una camera di stasi con ender pearl e teletrasportati da qualsiasi posto uccidendo un villager.

Il mio uso preferito? Un minigioco cacciatore di taglie: villager multipli con composter, il giocatore deve uccidere IL VILLAGER GIUSTO per attivare l'uscita. Meccanica completamente wtf xD

Lo Stallo del Pathfinding (ovvero "il villager che si congela per sempre")

C'è un bug tra Acquisisci POI (che vede un percorso) e la navigazione reale (che si rifiuta di seguirlo). Succede quando il blocco sopra una postazione di lavoro non è camminabile. Risultato:

  • Pacchetto Core: "Voglio acquisire il POI"
  • Navigazione: "Non ci posso camminare"
  • Risultato: il villager rimane CONGELATO, per sempre, in lotta con sé stesso.

Villager letteralmente congelati, usabili come decorazione o oggetti di scena. Un armor stand tank? Sì. Una guardia che non si muove? Sì. Macabro? Forse. Efficace? Totalmente xD


Conclusione

Il pathfinding dei mob in Minecraft non è casuale. È un sistema deterministico basato su punteggi, prevedibile E sfruttabile.

Tre cose da ricordare:

  1. Blocchi solidi sotto = bias di altezza -- riempi o svuota il sottosuolo per guidare i mob
  2. Il malus è diverso per ogni mob -- crea percorsi che alcuni prendono e altri no
  3. Slot POI riservati a distanza -- redstone wireless gratuita, teletrasporto, tutto quanto

Il codice sorgente di Minecraft è una miniera d'oro di meccaniche poco sfruttate. Ho passato ore a leggere Java decompilato e onestamente? Ogni riga è un Easter Egg funzionale. Tranne che queste funzionano in survival per la redstone wireless con i villager. Miglior gioco confermato xD

Minecraft Pathfinding-Logik und ihre Anwendungen

Wie A*, Block-Malus und POI-Mechaniken es dir ermöglichen,

Einleitung

Ich hab Stunden damit verbracht, Schafen beim Rennen gegen Wände zuzuschauen.

Beste Investition meines Lebens xD

Je mehr du dir diese Mobs anschaust, desto mehr merkst du: Nichts an ihrer Bewegung ist zufällig. Jeder Schritt ist programmiert, vorhersagbar, und vor allem -- ausnutzbar. Ich hab den Minecraft-Sourcecode durchgewühlt um genau zu verstehen, wie Pathfinding funktioniert, und was ich gefunden hab ist, dass du Mobs buchstäblich mit Gedanken kontrollieren kannst. So à la zwing sie dahin zu gehen, wo DU willst, nicht wo der Zufall entscheidet.

Dieser Guide ist alles, was ich beim Graben gefunden hab. Das KI-System, der A*-Algorithmus, die versteckten Malice-Werte, die Exploits die du im Überlebensmodus ziehen kannst. Hol deine Spitzhacke.


Wie Mob-KI funktioniert (Spoiler: sie ist irgendwie dumm)

Goals

Jeder Mob hat eine Liste von Goals. Dinge, die er TUN KANN, und wie sehr er sie TUN WILL. Niedrigere Zahl = höhere Priorität. Wie eine TODO-Liste aus der Hölle.

protected void registerGoals() {
    this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
    this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
    this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
    this.addBehaviourGoals();
}

Schon mal gesehen, wie ein Zombie ein Schildkröten-Ei ignoriert um dich zu jagen? Daher: ZombieAttackTurtleEggGoal hat Priorität 4, während ZombieAttackGoal (das "fress-dein-Gesicht"-Goal) Priorität 2 hat. Zombies bevorzugen Snacks mit Puls.

Das Goal, das uns eigentlich interessiert, ist WaterAvoidingRandomStrollGoal, Priorität 7. Das "ich hab nix Besseres zu tun, also lauf ich rum"-Goal. Hier fängt der Spaß an.

Bewegung (oder "wie ein Random Walk eine 1-zu-60-Chance pro Tick hat")

Jeden Tick (alle 0,05 Sekunden) ruft das Spiel canUse() auf, um zu checken ob der Mob überhaupt Bock hat, sich zu bewegen. 1 zu 60 Chance pro Tick. Unfassbar ineffizientes Design -- und ich liebe es.

public boolean canUse() {
    if (this.mob.hasControllingPassenger()) {
        return false;
    } else {
        if (!this.forceTrigger) {
            if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
                return false;
            }
            if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
                return false;
            }
        }
        Vec3 $$0 = this.getPosition();
        if ($$0 == null) {
            return false;
        } else {
            this.wantedX = $$0.x;
            this.wantedY = $$0.y;
            this.wantedZ = $$0.z;
            this.forceTrigger = false;
            return true;
        }
    }
}

Also zusammengefasst: wenn du auf dem Mob reitest -> nein, wenn der Mob 5 Sekunden nix getan hat -> nein, wenn RNG nein sagt -> nein. Das Spiel will REALLY nicht, dass Mobs sich bewegen.

Aber wenn er sich doch bewegt, übernimmt getPosition():

protected Vec3 getPosition() {
    if (this.mob.isInWater()) {
        Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
        return $$0 == null ? super.getPosition() : $$0;
    } else {
        return this.mob.getRandom().nextFloat() >= this.probability
            ? LandRandomPos.getPos(this.mob, 10, 7)
            : super.getPosition();
    }
}

Die zwei Zahlen am Ende? XZ-Radius und Y-Radius. Im Wasser sucht der Mob weiter (15 vs 10). Wenn er kein Land findet, fallbackt er auf super.getPosition() -- das Wasser akzeptiert. Ergebnis: Mobs WOLLEN aus dem Wasser raus. Deshalb schwimmen deine Tiere wie Verrückte Richtung Ufer.

Lustiges Detail: es gibt buchstäblich eine 0,1%-Chance, dass der Mob super.getPosition() statt LandRandomPos nimmt. Eins zu tausend. Mojang halt xD

LandRandomPos: die Optimierung, die alles zerstört

Das ist MEIN Lieblingsschritt. Das schönste technische Chaos, das Pathfinding ausnutzbar macht.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
    boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
    return RandomPos.generateRandomPos(() -> {
        BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
        BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
        return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
    }, $$3);
}

movePosUpOutOfSolid. Der Name sagt alles. Wenn die gewählte Position in einem soliden Block ist, schiebt das Spiel sie nach oben, bis sie in der Luft ist.

Das ist eine Optimierung: anstatt Zeit damit zu verschwenden, unterirdische Positionen zu überspringen, werden sie einfach an die Oberfläche geschoben. Klug? Ja. Aber es erzeugt einen MASSIVEN Bias: Mobs bevorzugen Höhenlagen.

Denk mal drüber nach. Viele Blöcke unter der Erde, das Spiel generiert 10 zufällige Positionen. Die in Blöcken werden nach oben geschoben. Dichte Gebiete (unter einem Hügel) erzeugen mehr gültige Positionen als hohle Gebiete. Ergebnis: der Mob läuft statistisch gesehen öfter zum Hügel.

Vertrau mir, wir werden das gleich komplett ausnutzen.

Die Auswahl: bester Block gewinnt

10 Positionen, ein Gewinner, ein Score-Contest:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
    double $$2 = Double.NEGATIVE_INFINITY;
    BlockPos $$3 = null;
    for(int $$4 = 0; $$4 < 10; ++$$4) {
        BlockPos $$5 = (BlockPos)$$0.get();
        if ($$5 != null) {
            double $$6 = $$1.applyAsDouble($$5);
            if ($$6 > $$2) {
                $$2 = $$6;
                $$3 = $$5;
            }
        }
    }
    return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

Die Position mit dem höchsten Score GEWINNT. Und wenn du die Bewertungskriterien kennst, kannst du DEINE Position gewinnen lassen. Das ist wie eine Wahl manipulieren.


Mob-Präferenzen (oder "warum deine Kuh über die Straße ging")

Jeder Mob hat andere Vorlieben. Und das ändert alles.

Mob Liebt
Tiere (Kühe, Schafe, Schweine) Grasblöcke, Licht
Monster (Zombies, Skelette) Dunkelheit (Hipster)
Schildkröten Wasser > Sand > Licht
Hoglin crimson_nylium; hasst warped_fungus
Lohen Lava und NICHTS ANDERES
Silberfischchen Infizierbare Blöcke
Wächter Wasser + Licht (Snobs)
Mooshrooms Myzel + Licht
Bienen Luft. Ja, die bevorzugen LUFT.
// Animal: runtergucken, wenn Gras -> max Score
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
    return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: buchstäblich das Gegenteil
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
    return -$$1.getPathfindingCostFromLightLevels($$0);
}

Monster sind praktisch "wenn's hell ist, negativ-Score, ich bin raus." Die kriegen einen KRAMPF bei Licht xD

Du kannst also -- buchstäblich -- Tiere mit Gras und Licht lenken, und Monster mit Dunkelheit. Es ist dumm und genial zugleich.


A* in Minecraft (die geheime Formel)

Minecraft benutzt A* (A-Star) fürs Pathfinding. Aber Mojang hat ihren eigenen Dreh eingebaut:

f(n) = g(n) + 1,5 × h(n)
  • g(n) = bereits zurückgelegte Distanz (1 pro Block, ~1,41 diagonal)
  • h(n) = Luftlinien-Distanz zum Ziel
  • 1,5 = weil Mojang gern Dinge leicht kaputt macht

Normales A* benutzt f(n) = g(n) + h(n). MOJANG HAT EINEN 1,5-MULTIPLIKATOR EINGEBAUT. Warum? Damit der Algorithmus schneller aufs Ziel zusteuert und weniger Such-Äste beschneidet. Ergebnis: der Pfad ist "gut genug" aber nicht immer optimal. Es ist ein betrunkenes A*.

mermaid diagram

Wichtige Einschränkung: ein Mob kann nur 16 Blöcke weit pathen (seine Follow Range). Wenn das Ziel zu weit weg ist, nimmt es den nächstgelegenen erreichbaren Block. Das heißt, du kannst einen Monolithen außerhalb der Reichweite bauen und der Mob patht zum nächstgelegenen Block, der ihn näher bringt -- was seine Bewegung komplett vorhersagbar macht.

Die zwei Exploits die das Spiel zerstören

1. Block-Updates erzwingen Neuberechnung

public boolean shouldRecomputePath(BlockPos $$0) {
    if (this.hasDelayedRecomputation) return false;
    if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
        Node $$1 = this.path.getEndNode();
        Vec3 $$2 = new Vec3(
            ((double)$$1.x + this.mob.getX()) / 2.0,
            ((double)$$1.y + this.mob.getY()) / 2.0,
            ((double)$$1.z + this.mob.getZ()) / 2.0
        );
        return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
    }
    return false;
}

Jedes Block-Update in der Nähe des Mob-Pfads erzwingt eine A*-Neuberechnung mit 1-Sekunden-Cooldown. Bau einen 1-Sekunden-Taktgeber neben einen Mob und er rechnet STÄNDIG neu. Das ist wie ein GPS das sich jede Sekunde zurücksetzt.

Und wenn du das mit 50 Mobs machst? Lag-City. RIP TPS.

2. Pathfinding Malice (Block-Kosten-Strafen)

Manche Blöcke machen Mobs Angst. Buchstäblich. Jeder Block hat zugehörige Kosten, definiert durch ein Enum:

Block / Bedingung Malice
Honigblock +8 zum Durchlaufen
Pulverschnee Unpassierbar
Geschlossene Türen Unpassierbar
Feuer +16 durch, +8 angrenzend
Tiere & Dorfbewohner Feuer = -1 (HARTES NEIN)
Kaktus / Süßbeeren Unpassierbar; angrenzend = +8
Wasser +8 durch oder angrenzend
Magma +8 angrenzend

Tiere gehen noch weiter:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
    super($$0, $$1);
    this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
    this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE bei -1.0F ist buchstäblich "verboten." Ein Tier würde lieber in die Leere springen als durch Feuer zu laufen.

Übung: der große Pfad-Wettbewerb

Ein Dorfbewohner wählt zwischen mehreren Pfaden:

  • Pfad A: 15 Blöcke aber 6 an Wasser angrenzend (+8 jeder)
  • Pfad B: 18 Blöcke mit 2 Wasserblöcken (+8) + 1 wasserangrenzend (+8)
  • Pfad C: 14 Blöcke geradeaus... aber Feuer -> UNPASSIERBAR für Dorfbewohner
  • Pfad D: 16 Blöcke mit 1 magma-angrenzend (+8) + 1 honig-angrenzend (+8)
  • Pfad E: 25 Blöcke mit Kakteen überall (+8 überall) -> 90,82 Gesamt LOL

Der Gewinner ist normalerweise Pfad B: der Umweg lohnt sich weil Wasser TEUER ist.

Ein Dorfbewohner ist quasi ein Taschenrechner mit Beinen xD

Jeder Mob wählt andere Pfade

Ein Dorfbewohner: "Feuer? NOPE TSCHÜSS" Ein Zombie: "Feuer? OK Boomer läuft brennend durch"

Du kannst buchstäblich Highways bauen, die Dorfbewohner nehmen und Zombies nicht -- oder umgekehrt.


Dorfbewohner: das ultimative Chaos

Dorfbewohner sind das am meisten missverstandene Ding in Minecraft. Aber wenn du den Code einmal gelesen hast, merkst du: sie sind nur vorhersagbare Maschinen mit Bürozeiten.

Sensoren und Erinnerungen

9 Sensoren laufen alle 20 Ticks (1 Sekunde). Jeder scannt einen Radius um den Dorfbewohner und speichert das Ergebnis im Gedächtnis. Der Dorfbewohner sieht alles, erinnert sich an alles, und handelt entsprechend.

Aktivitätspakete

Das Gehirn eines Dorfbewohners ist in Aktivitätspakete unterteilt, die je nach Uhrzeit aktiv werden:

Paket Zeit Der Dorfbewohner...
Core 24/7 Macht Türen auf, schwimmt (80% der Zeit), SAMMELT POIs
Work 8-15 Uhr "Muss arbeiten" -- läuft zur Arbeitsstation
Meet 15-17 Uhr "Happy Hour!" -- geht zur Glocke, sozialisiert
Rest 18-6 Uhr "Schlafenszeit" -- geht ins Bett
Idle 6-8 Uhr, 17-18 Uhr "Gelangweilt" -- läuft rum, vermehrt sich, springt auf Betten
Panic Verletzt / Feind "RENN" -- FLIEHT

Panic ist das einzige Paket, das ALLE anderen unterbrechen kann. Selbst wenn der Dorfbewohner schläft oder arbeitet -- wenn ein Zombie da ist, PANIK-MODUS.

POI akquirieren: der Mechanismus der drahtloses Redstone ermöglicht

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
    $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI scannt einen 48-Block-Radius nach allen gültigen Points of Interest. Es behält die 5 nächsten, checkt ob ein Pfad existiert, und akquiriert den nächstgelegenen erreichbaren. Jeder POI hat begrenzte Slots:

  • Arbeitsstationen: 1 Slot
  • Betten: 1 Slot
  • Glocken: 32 Slots

Das VERRÜCKTE: der Slot wird beim AKQUIRIEREN reserviert, nicht bei Ankunft. Ein Dorfbewohner kann einen Komposter von der anderen Seite der Karte blocken, ohne ihn jemals zu erreichen.

Du siehst, worauf das hinausläuft?

Drahtloses Redstone. Ja, DRAHTLOS.

  1. Setz einen Dorfbewohner in eine Lore mit Pfad zu einem Komposter
  2. Er akquiriert den Komposter (Slot belegt, niemand sonst kann ihn nutzen)
  3. Der Dorfbewohner ist zu weit weg um zu klicken -- Knochenmehl bleibt
  4. BEWEGE diesen Dorfbewohner IRGENDWOHIN in der Welt, er behält den Slot
  5. Wenn du dein Ding aktivieren willst, TÖTE den Dorfbewohner
  6. Slot wird frei, ein anderer Dorfbewohner akquiriert den Komposter, entfernt Knochenmehl
  7. BLOCK-UPDATE -> jeder Redstone-Kreis wird aktiviert

Du hast drahtloses Redstone erschaffen, übertragbar über die gesamte Welt, ohne dass ein Chunk auf dem Weg geladen werden muss. Koppel das mit einer Enderperlen-Stasis-Kammer und teleportiere dich von überall her, indem du einen Dorfbewohner tötest.

Meine Lieblingsanwendung? Ein Kopfgeldjäger-Minispiel: mehrere Dorfbewohner mit Kompostern, der Spieler muss DEN RICHTIGEN Dorfbewohner töten um den Ausgang zu aktivieren. Komplett wtf Mechanik xD

Der Pathfinding-Deadlock (oder "der Dorfbewohner der für immer einfriert")

Es gibt einen Bug zwischen Acquire POI (das einen Pfad sieht) und der eigentlichen Navigation (die sich weigert, ihn zu folgen). Passiert wenn der Block über einer Arbeitsstation nicht begehbar ist. Ergebnis:

  • Core-Paket: "Ich will den POI akquirieren"
  • Navigation: "Ich kann da nicht hingehen"
  • Ergebnis: der Dorfbewohner bleibt EINGEFROREN, für immer, im Kampf mit sich selbst.

Buchstäblich eingefrorene Dorfbewohner, nutzbar als Dekoration oder Requisiten. Ein Rüstungsständer-Panzer? Ja. Eine Wache die sich nicht bewegt? Ja. Makaber? Vielleicht. Effektiv? Absolut xD


Fazit

Mob-Pathfinding in Minecraft ist nicht zufällig. Es ist ein deterministisches, Score-basiertes System, vorhersagbar UND ausnutzbar.

Drei Dinge zum Merken:

  1. Solide Blöcke drunter = Höhen-Bias -- füll oder leere den Untergrund um Mobs zu lenken
  2. Malice ist unterschiedlich pro Mob -- erstelle Routen, die manche nehmen und andere nicht
  3. POI-Slots werden auf Distanz reserviert -- kostenloses drahtloses Redstone, Teleportation, alles dabei

Minecrafts Sourcecode ist eine Goldgrube an unterausgenutzten Mechaniken. Ich hab Stunden damit verbracht, dekompiliertes Java zu lesen und ehrlich? Jede Zeile ist ein funktionales Easter Egg. Nur dass diese im Überlebensmodus für drahtloses Redstone mit Dorfbewohnern funktionieren. Bestes Game bestätigt xD

Логика поиска пути в Minecraft и её применение

Как алгоритм A*, штрафы блоков и механика POI позволяют

Вступление

Я потратил часы, наблюдая за тем, как овцы врезаются в стены.

Лучшая инвестиция в моей жизни xD

Чем больше ты смотришь на этих мобов, тем больше понимаешь -- в их движении нет ничего случайного. Каждый шаг запрограммирован, предсказуем и, самое главное -- взламываем. Я залез в исходники Minecraft чтобы понять, как работает pathfinding, и обнаружил, что можно буквально контролировать разум мобов. Серьёзно, заставлять их идти туда, куда ТЫ хочешь, а не куда рандом решит.

Этот гайд -- всё, что я нашёл, пока копался. Система AI, алгоритм A*, скрытые значения "злобы", эксплойты для выживания. Хватай кирку.


Как работает Mob AI (спойлер: она туповата)

Цели

У каждого моба есть список целей. Что он МОЖЕТ делать и как сильно он этого ХОЧЕТ. Меньше число = выше приоритет. Как список задач из ада.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Видел, как зомби игнорирует черепашьи яйца, чтобы погнаться за тобой? Вот почему: ZombieAttackTurtleEggGoal имеет приоритет 4, а ZombieAttackGoal (цель "сожри твоё лицо") -- приоритет 2. Зомби предпочитают закуски с пульсом.

Цель, которая нас реально интересует -- WaterAvoidingRandomStrollGoal, приоритет 7. Цель "мне больше нечем заняться, так что пойду поброжу". Вот где начинается веселье.

Движение (или "как случайный шаг имеет шанс 1 к 60 за тик")

Каждый тик (каждые 0.05 секунд) игра вызывает canUse() чтобы проверить, хочет ли моб вообще напрягаться. Шанс 1 к 60 за тик. Ужасно неэффективный дизайн, и я его обожаю.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Короче: если ты сидишь на мобе -- нет, если моб ничего не делал 5 секунд -- нет, если RNG сказал нет -- нет. Игре РЕАЛЬНО не хочется, чтобы мобы двигались.

Но когда он всё-таки двигается, в дело вступает getPosition():

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Видишь те два числа в конце? Радиус XZ и радиус Y. В воде моб ищет шире (15 против 10). Если не может найти землю -- падает на super.getPosition(), который принимает воду. Результат: мобы ХОТЯТ выбраться из воды. Вот почему твои животные плавают как бешеные к берегу.

Забавный факт: есть буквально 0.1% шанс, что моб выберет super.getPosition() вместо LandRandomPos. Один на тысячу. Моянг, я полагаю xD

LandRandomPos: оптимизация, которая всё ломает

Это мой ЛЮБИМЫЙ шаг. Самый красивый технический бардак, который делает pathfinding эксплуатируемым.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. Название говорит само за себя. Если выбранная позиция внутри твёрдого блока, игра выталкивает её наверх, пока она не окажется в воздухе.

Это оптимизация: вместо того чтобы тратить время на пропуск подземных позиций, игра просто вышвыривает их на поверхность. Умно? Да. Но это создаёт ОГРОМНОЕ смещение: мобы предпочитают высоту.

Подумай об этом. Много блоков под землёй, игра генерирует 10 случайных позиций. Те, что внутри блоков, выталкиваются наверх. Плотные зоны (под холмом) дают больше валидных позиций, чем пустые. Результат: моб статистически чаще идёт к холму.

Поверь, мы скоро разнесём это в щепки.

Выбор: лучший блок побеждает

10 позиций, один победитель, соревнование по очкам:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

Позиция с наибольшим количеством очков ПОБЕЖДАЕТ. И если ты знаешь критерии оценки, ты можешь сделать так, чтобы ТВОЯ позиция победила. Это как подстроить выборы.


Предпочтения мобов (или "почему твоя корова перешла дорогу")

У каждого моба разные вкусы. И это меняет всё.

Моб Любит
Животные (коровы, овцы, свиньи) Траву, свет
Монстры (зомби, скелеты) Тьму (хипстеры)
Черепахи Вода > песок > свет
Хоглины crimson_nylium; ненавидят warped_fungus
Страйдеры Лаву и БОЛЬШЕ НИЧЕГО
Чешуйницы Заражаемые блоки
Стражи Вода + свет (снобы)
Муушрумы Мицелий + свет
Пчёлы Воздух. Да, они предпочитают ВОЗДУХ.
// Animal: look down, if grass -> max score
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: literally the opposite
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Монстры -- это буквально "если светло -- минусовой скор, я сваливаю". Они УСТРАИВАЮТ ИСТЕРИКУ от света xD

Так что ты можешь -- буквально -- вести животных травой и светом, а монстров -- тьмой. Это тупо и гениально одновременно.


A* в Minecraft (секретная формула)

Minecraft использует A* (A-star) для pathfinding. Но Mojang добавили свой твист:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = уже пройденное расстояние (1 за блок, ~1.41 по диагонали)
  • h(n) = расстояние по прямой до цели
  • 1.5 = потому что Mojang любят, когда всё слегка сломано

Обычный A* использует f(n) = g(n) + h(n). MOJANG ДОБАВИЛИ МНОЖИТЕЛЬ 1.5. Зачем? Чтобы алгоритм быстрее сходился к цели и обрезал меньше веток поиска. Результат: путь "достаточно хорош", но не всегда оптимален. Это пьяный A*.

mermaid diagram

Ключевое ограничение: моб может прокладывать путь только на 16 блоков (его follow range). Если цель слишком далеко, он выбирает ближайший достижимый блок. Это значит, что ты можешь построить монолит вне диапазона, и моб будет идти к ближайшему блоку, который приблизит его к цели -- делая его движение полностью предсказуемым.

Два эксплойта, которые ломают игру

1. Обновления блоков форсируют пересчёт

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Каждое обновление блока рядом с путём моба заставляет A* пересчитываться с кулдауном в 1 секунду. Поставь часовой механизм с периодом 1 секунда рядом с мобом -- и он будет пересчитывать ПОСТОЯННО. Это как GPS, который сбрасывается каждую секунду.

А если сделать это с 50 мобами? Лаговое гетто. RIP TPS.

2. Pathfinding Malice (штрафы за блоки)

Некоторые блоки пугают мобов. Буквально. У каждого блока есть ассоциированная стоимость, определённая enum'ом:

Блок / Состояние Злоба
Медовый блок +8 пройти сквозь
Рыхлый снег Непроходимо
Закрытые двери Непроходимо
Огонь +16 сквозь, +8 рядом
Животные и жители Огонь = -1 (ЖЁСТКИЙ НЕТ)
Кактус / Сладкие ягоды Непроходимо; рядом = +8
Вода +8 сквозь или рядом
Магма +8 рядом

Животные заходят ещё дальше:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE на -1.0F -- это буквально "запрещено". Животное скорее прыгнет в пустоту, чем пройдёт сквозь огонь.

Упражнение: великий конкурс путей

Житель выбирает между несколькими путями:

  • Путь A: 15 блоков, но 6 граничат с водой (+8 каждый)
  • Путь B: 18 блоков с 2 блоками воды (+8) + 1 блок рядом с водой (+8)
  • Путь C: 14 блоков напрямую... но огонь -> НЕПРОХОДИМО для жителей
  • Путь D: 16 блоков с 1 блоком рядом с магмой (+8) + 1 рядом с медом (+8)
  • Путь E: 25 блоков с кактусами везде (+8 везде) -> 90.82 всего LOL

Победитель -- обычно Путь B: обход окупается, потому что вода ДОРОГАЯ.

Житель -- это калькулятор стоимости на ножках xD

Каждый моб выбирает разные пути

Житель: "огонь? НЕТ ПОКА" Зомби: "огонь? Ок, бумер проходит сквозь огонь горящим"

Ты можешь буквально строить магистрали, по которым ходят жители, а зомби -- нет. И наоборот.


Жители: ultimate бардак

Жители -- самая недопонятая вещь в Minecraft. Но когда ты прочитал код, ты понимаешь, что это просто предсказуемые машины с рабочим графиком.

Сенсоры и память

9 сенсоров, работающих каждые 20 тиков (1 секунда). Каждый сканирует радиус вокруг жителя и сохраняет результат в память. Житель видит всё, помнит всё и действует соответственно.

Пакеты активности

Мозг жителя разделён на пакеты активности, которые активируются в зависимости от времени:

Пакет Время Житель...
Core 24/7 Открывает двери, плавает (80% времени), ПОЛУЧАЕТ POI
Work 8:00-15:00 "Надо работать" -- идёт к рабочему месту
Meet 15:00-17:00 "Счастливый час!" -- идёт к колоколу, тусит
Rest 18:00-6:00 "Спать пора" -- идёт в кровать
Idle 6:00-8:00, 17:00-18:00 "Скучно" -- бродит, размножается, прыгает по кроватям
Panic Урон / враждебность "БЕЖАТЬ" -- УБЕГАЕТ

Panic -- единственный пакет, который может прервать ВСЕ остальные. Даже если житель спит или работает, если есть зомби -- ПАНИКА.

Acquire POI: механика, которая дает беспроводной редстоун

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI сканирует радиус 48 блоков в поисках всех валидных точек интереса. Он хранит 5 ближайших, проверяет, существует ли путь, и получает ближайшую достижимую. У каждой POI есть ограниченное количество слотов:

  • Рабочие станции: 1 слот
  • Кровати: 1 слот
  • Колокола: 32 слота

БЕЗУМНАЯ вещь: слот резервируется в момент ПОЛУЧЕНИЯ, а не прибытия. Житель может заблокировать компостер с другого конца карты, даже не дойдя до него.

Понимаешь, к чему это?

Беспроводной редстоун. Да, БЕСПРОВОДНОЙ.

  1. Посади жителя в вагонетку с путём к компостеру
  2. Он получает компостер (слот занят, никто другой не может им пользоваться)
  3. Житель слишком далеко, чтобы кликнуть -- костная мука остаётся
  4. ПЕРЕМЕСТИ этого жителя КУДА УГОДНО в мире, он сохраняет слот
  5. Когда хочешь активировать свою штуку -- УБЕЙ жителя
  6. Слот освобождается, другой житель получает компостер, забирает костную муку
  7. ОБНОВЛЕНИЕ БЛОКА -> любая редстоун-схема активируется

Ты создал беспроводной редстоун, передаваемый через весь мир, без единой загруженной чанковой дороги. Подключи это к камере с эндер-жемчугом и телепортируйся откуда угодно, убив жителя.

Моё любимое применение? Мини-игра "охотник за головами": несколько жителей с компостерами, игрок должен убить ПРАВИЛЬНОГО жителя, чтобы активировать выход. Полная wtf механика xD

Взаимоблокировка pathfinding (или "житель, который застывает навсегда")

Есть баг между Acquire POI (который видит путь) и фактической навигацией (которая отказывается по нему идти). Происходит, когда блок над рабочей станцией непроходим. Результат:

  • Core-пакет: "Я хочу получить POI"
  • Навигация: "Я не могу туда пройти"
  • Результат: житель стоит ЗАМЕРШИЙ, навсегда, борясь сам с собой.

Буквально замороженные жители, используемые как декор или реквизит. Танк из бронестоек? Да. Охранник, который не двигается? Да. Жутковато? Возможно. Эффективно? Полностью xD


Заключение

Pathfinding мобов в Minecraft -- не случайность. Это детерминированная система на основе очков, предсказуемая И взламываемая.

Три вещи, которые нужно запомнить:

  1. Твёрдые блоки под ногами = смещение в высоту -- заполни или опустоши подпол, чтобы направлять мобов
  2. Злоба разная для каждого моба -- создавай маршруты, которые одни проходят, а другие нет
  3. Слоты POI резервируются на расстоянии -- бесплатный беспроводной редстоун, телепортация, всё это

Исходный код Minecraft -- золотая жила недоиспользованных механик. Я потратил часы на чтение декомпилированной Java и, честно? Каждая строка -- это функциональное пасхальное яйцо. Кроме того, эти работают в выживании для беспроводного редстоуна с жителями. Лучшая игра, подтверждено xD

Lógica de pathfinding de Minecraft y sus aplicaciones

Cómo el algoritmo A*, los malus de bloques y los POI permiten

Introducción

Pasé horas viendo ovejas chocarse contra paredes.

La mejor inversión de mi vida xD

Cuanto más miras a estos mobs, más te das cuenta de que nada en su movimiento es aleatorio. Cada paso está codificado, es predecible y, lo más importante, se puede romper. Terminé escarbando en el código fuente de Minecraft para entender exactamente cómo funciona el pathfinding, y lo que encontré es que literalmente puedes controlar mentalmente a los mobs. Como, forzarlos a ir a donde TÚ quieres, no donde la random decide.

Esta guía es todo lo que encontré mientras investigaba. El sistema de IA, el algoritmo A*, los valores ocultos de malicia, los exploits que puedes usar en survival. Agarra tu pico.


Cómo funciona la IA de los Mobs (spoiler: es medio tonta)

Goals

Cada mob tiene una lista de goals (objetivos). Cosas que PUEDE hacer y qué tanto QUIERE hacerlas. Número más bajo = mayor prioridad. Como una lista de tareas del infierno.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

¿Alguna vez viste a un zombie ignorar un huevo de tortuga para perseguirte a ti? Por eso: ZombieAttackTurtleEggGoal tiene prioridad 4, mientras que ZombieAttackGoal (el objetivo "cómete la cara") es prioridad 2. Los zombies prefieren bocadillos con pulso.

El goal que realmente nos importa es WaterAvoidingRandomStrollGoal, prioridad 7. El objetivo de "no tengo nada mejor que hacer, así que deambulo". Aquí empieza la diversión.

Movimiento (o "cómo un paseo aleatorio tiene 1 de 60 probabilidades por tick")

Cada tick (cada 0.05 segundos), el juego llama a canUse() para ver si el mob se molesta en moverse. 1 de 60 probabilidades por tick. Diseño horriblemente ineficiente, y me encanta.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

En resumen: si estás montando al mob -> no, si el mob no ha hecho nada en 5 segundos -> no, si RNG dice no -> no. El juego DE VERDAD no quiere que los mobs se muevan.

Pero cuando se mueve, getPosition() toma el control:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

¿Esos dos números al final? Radio XZ y radio Y. En el agua, el mob busca más lejos (15 vs 10). Si no encuentra tierra, usa super.getPosition() que acepta agua. Resultado: los mobs QUIEREN salir del agua. Por eso tus animales nadan como locos hacia la orilla.

Dato curioso: hay literalmente 0.1% de probabilidad de que el mob elija super.getPosition() en vez de LandRandomPos. Uno en mil. Mojang supongo xD

LandRandomPos: la optimización que rompe todo

Este es MI paso favorito. El desastre técnico más hermoso que hace que el pathfinding sea explotable.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. El nombre lo dice todo. Si la posición elegida está dentro de un bloque sólido, el juego la empuja hacia arriba hasta que esté en el aire.

Es una optimización: en lugar de perder tiempo saltándose posiciones subterráneas, el juego las empuja a la superficie. ¿Inteligente? Sí. Pero crea un SESGO MASIVO: los mobs prefieren terreno alto.

Piénsalo. Muchos bloques subterráneos, el juego genera 10 posiciones aleatorias. Las que están dentro de bloques se empujan hacia arriba. Las áreas densas (debajo de una colina) producen más posiciones válidas que las áreas huecas. Resultado: el mob estadísticamente va hacia la colina más seguido.

Confía en mí, estamos a punto de romper esto por completo.

La selección: el mejor bloque gana

10 posiciones, un ganador, un concurso de puntuación:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

La posición con la puntuación más alta GANA. Y si conoces los criterios de puntuación, puedes hacer que TU posición gane. Es como amañar una elección.


Preferencias de los Mobs (o "por qué tu vaca cruzó la carretera")

Cada mob tiene gustos diferentes. Y eso lo cambia todo.

Mob Le encanta
Animales (vacas, ovejas, cerdos) Bloques de pasto, luz
Monstruos (zombies, esqueletos) Oscuridad (hipsters)
Tortugas Agua > arena > luz
Hoglins crimson_nylium; odian warped_fungus
Striders Lava y NADA MÁS
Silverfish Bloques infestables
Guardianes Agua + luz (snobs)
Mooshrooms Micelio + luz
Abejas Aire. Sí, prefieren el AIRE.
// Animal: mira abajo, si es pasto -> puntuación máxima
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: literalmente lo opuesto
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Los monstruos son básicamente "si está iluminado, puntuación negativa, me voy". Les da un ATAQUE con los niveles de luz xD

Así que puedes -- literalmente -- guiar animales con pasto y luz, y monstruos con oscuridad. Es tonto y brillante al mismo tiempo.


A* en Minecraft (la fórmula secreta)

Minecraft usa A* (A-star) para el pathfinding. Pero Mojang le agregó su propio toque:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = distancia ya recorrida (1 por bloque, ~1.41 diagonal)
  • h(n) = distancia en línea recta al objetivo
  • 1.5 = porque a Mojang le gustan las cosas ligeramente rotas

El A* normal usa f(n) = g(n) + h(n). MOJANG LE AGREGÓ UN MULTIPLICADOR DE 1.5. ¿Por qué? Para que el algoritmo se dirija al destino más rápido y pode menos ramas de búsqueda. Resultado: el camino es "suficientemente bueno" pero no siempre óptimo. Es un A* borracho.

mermaid diagram

Limitación clave: un mob solo puede pathfindear 16 bloques (su follow range). Si el destino está muy lejos, elige el bloque alcanzable más cercano. Esto significa que puedes construir un monolito fuera de alcance y el mob pathfindeará hacia el bloque más cercano que lo acerque -- haciendo su movimiento completamente predecible.

Los dos exploits que rompen el juego

1. Las actualizaciones de bloque fuerzan recálculo

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Cada actualización de bloque cerca del camino del mob fuerza una recomputación de A* con un cooldown de 1 segundo. Pon un reloj de 1 segundo junto a un mob y recalcula CONSTANTEMENTE. Es como un GPS que se reinicia cada segundo.

Y si haces esto con 50 mobs? Ciudad lag. RIP TPS.

2. Malicia de Pathfinding (penalizaciones de costo de bloques)

Algunos bloques asustan a los mobs. Literalmente. Cada bloque tiene un costo asociado definido por un enum:

Bloque / Condición Malicia
Bloque de miel +8 al atravesar
Nieve polvo Imposible de atravesar
Puertas cerradas Imposible de atravesar
Fuego +16 al atravesar, +8 adyacente
Animales y Aldeanos Fuego = -1 (NO ROTUNDO)
Cactus / Bayas dulces Imposible; adyacente = +8
Agua +8 al atravesar o adyacente
Magma +8 adyacente

Los animales van aún más lejos:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE en -1.0F es literalmente "prohibido". Un animal preferiría saltar al vacío antes que caminar a través del fuego.

Ejercicio: el gran concurso de caminos

Un aldeano eligiendo entre múltiples caminos:

  • Camino A: 15 bloques pero 6 bordean agua (+8 cada uno)
  • Camino B: 18 bloques con 2 bloques de agua (+8) + 1 adyacente a agua (+8)
  • Camino C: 14 bloques directos... pero fuego -> IMPASABLE para aldeanos
  • Camino D: 16 bloques con 1 adyacente a magma (+8) + 1 adyacente a miel (+8)
  • Camino E: 25 bloques con cactus por todos lados (+8 en todas partes) -> 90.82 total LOL

El ganador es usualmente el Camino B: el desvío vale la pena porque el agua es CARA.

Un aldeano es básicamente una calculadora de costos con patas xD

Cada mob elige caminos diferentes

Un aldeano: "¿fuego? NOPE BYE" Un zombie: "¿fuego? OK boomer camina a través en llamas"

Puedes literalmente construir autopistas que los aldeanos usen y los zombies no -- o viceversa.


Aldeanos: el desastre definitivo

Los aldeanos son lo más incomprendido de Minecraft. Pero una vez que lees el código, te das cuenta de que son solo máquinas predecibles con horario de oficina.

Sensores y memorias

9 sensores ejecutándose cada 20 ticks (1 segundo). Cada uno escanea un radio alrededor del aldeano y almacena el resultado en la memoria. El aldeano lo ve todo, lo recuerda todo, y actúa en consecuencia.

Paquetes de actividad

El cerebro de un aldeano está dividido en paquetes de actividad que se activan según la hora:

Paquete Hora El aldeano...
Core 24/7 Abre puertas, nada (80% del tiempo), ADQUIERE POIs
Work 8am-3pm "A trabajar" -- camina a la estación de trabajo
Meet 3pm-5pm "¡Hora feliz!" -- va a la campana, socializa
Rest 6pm-6am "Hora de dormir" -- va a la cama
Idle 6am-8am, 5pm-6pm "Aburrido" -- deambula, se reproduce, salta en camas
Panic Herido / hostil "CORRE" -- HUYE

Panic es el único paquete que puede interrumpir a TODOS los demás. Incluso si el aldeano está durmiendo o trabajando, si hay un zombie, MODO PÁNICO.

Acquire POI: la mecánica que habilita la redstone inalámbrica

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI escanea un radio de 48 bloques buscando todos los puntos de interés válidos. Guarda los 5 más cercanos, verifica si existe un camino, y adquiere el más cercano al que se pueda llegar. Cada POI tiene espacios limitados:

  • Estaciones de trabajo: 1 espacio
  • Camas: 1 espacio
  • Campanas: 32 espacios

Lo INCREÍBLE: el espacio se reserva en el momento de ADQUISICIÓN, no al llegar. Un aldeano puede bloquear un composter desde el otro lado del mapa sin siquiera alcanzarlo.

¿Ves por dónde va esto?

Redstone inalámbrica. Sí, INALÁMBRICA.

  1. Pon a un aldeano en un minecart con un camino hacia un composter
  2. Lo adquiere (espacio ocupado, nadie más puede usarlo)
  3. El aldeano está demasiado lejos para hacer clic -- el bonemeal se queda
  4. MUEVE a este aldeano a CUALQUIER LUGAR del mundo, mantiene el espacio
  5. Cuando quieras activar tu cosa, MATA al aldeano
  6. El espacio se libera, otro aldeano adquiere el composter, saca el bonemeal
  7. BLOCK UPDATE -> cualquier circuito de redstone se activa

Has creado redstone inalámbrica, transmisible a través del mundo entero, sin necesidad de cargar chunks en el camino. Conecta esto a una cámara de estasis de ender pearl y teletranspórtate desde cualquier lugar matando a un aldeano.

¿Mi uso favorito? Un minijuego de cazarrecompensas: múltiples aldeanos con composters, el jugador tiene que matar AL ALDEANO CORRECTO para activar la salida. Mecánica completamente wtf xD

El Deadlock de Pathfinding (o "el aldeano que se congela para siempre")

Hay un bug entre Acquire POI (que ve un camino) y la navegación real (que se niega a seguirlo). Pasa cuando el bloque encima de una estación de trabajo no es caminable. Resultado:

  • Paquete Core: "Quiero adquirir el POI"
  • Navegación: "No puedo caminar ahí"
  • Resultado: el aldeano se queda CONGELADO, para siempre, peleándose consigo mismo.

Aldeanos literalmente congelados, utilizables como decoración o props. ¿Un soporte de armadura con tanque? Sí. ¿Un guardia que no se mueve? Sí. ¿Macabro? Quizás. ¿Efectivo? Totalmente xD


Conclusión

El pathfinding de los mobs en Minecraft no es aleatorio. Es un sistema determinista, basado en puntuaciones, predecible Y rompible.

Tres cosas para recordar:

  1. Bloques sólidos debajo = sesgo de altura -- llena o vacía el subsuelo para guiar mobs
  2. La malicia es diferente por cada mob -- crea rutas que unos tomen y otros no
  3. Los espacios de POI se reservan a distancia -- redstone inalámbrica gratis, teletransportación, todo eso

El código fuente de Minecraft es una mina de mecánicas poco explotadas. Pasé horas leyendo Java decompilado y ¿sabes qué? Cada línea es un Easter Egg funcional. Excepto que estos funcionan en survival para hacer redstone inalámbrica con aldeanos. Mejor juego confirmado xD

Lógica de pathfinding do Minecraft e suas aplicações

Como o algoritmo A*, as penalidades de blocos e os POI permitem

Introdução

Passei horas assistindo ovelhas batendo em paredes.

E sinceramente? Melhor investimento da minha vida xD

Porque quanto mais você observa esses mobs, mais percebe que não há nada de aleatório. Cada movimento é codificado, previsível e, acima de tudo -- completamente quebrável. Acabei mergulhando no código fonte do Minecraft para entender exatamente como o pathfinding funciona, e o que descobri é que você pode literalmente fazer mind-control nos mobs. Tipo, forçá-los a ir para onde VOCÊ quer, não para onde o acaso decide.

Este guia é tudo que aprendi fuçando. A IA, o algoritmo A*, as penalidades ocultas, os exploits que você pode usar no survival. Prepara sua picareta.


Como funciona a IA dos mobs (spoiler: é bizarra)

Os Goals

Cada mob tem goals. É uma lista de coisas que ele PODE fazer e o quanto ele QUER fazê-las. Quanto menor o número, mais prioritário -- como uma lista de tarefas versão caos.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Já viu um zumbi ignorar um ovo de tartaruga para correr atrás de você? É por isso: ZombieAttackTurtleEggGoal tem prioridade 4, enquanto ZombieAttackGoal (a parada que manda ele te morder a cara) está na prioridade 2.

Sim, um zumbi prefere te comer a quebrar um ovo. Que amor xD

O goal que realmente nos interessa é o WaterAvoidingRandomStrollGoal, prioridade 7. O goal "não tenho nada pra fazer então vou andar aleatoriamente". É aí que a bagunça começa.

O movimento (ou "como um random walk tem 1 chance em 60 de acontecer")

A cada tick (a cada 0.05 segundos), o jogo chama canUse() pra ver se o mob digna-se a se mover. 1 chance em 60 a cada tick. Um design completamente doido, e eu adoro isso.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Resumindo: se você está montado no mob -> não, se o mob não fez nada por 5 segundos -> não, se o random disse não -> não. O jogo REALMENTE não quer que o mob se mexa.

Mas quando ele se mexe, getPosition() entra em ação:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Repara nesses dois números no final: é o raio XZ e o raio Y. Na água, o mob procura mais longe (15 em vez de 10). Se não encontrar terra, ele recorre ao super.getPosition() que aceita água. Resultado: os mobs QUEREM sair da água. É por isso que seus animais nadam feito loucos em direção à borda.

Detalhe suculento: tem literalmente 0.1% de chance do mob pegar super.getPosition() em vez de LandRandomPos. Um em mil. Mojang né xD

LandRandomPos: a otimização porcaria que muda tudo

Essa é a MINHA etapa favorita. A maior cagada técnica que torna o pathfinding explorável.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. O nome diz tudo. Se a posição escolhida está dentro de um bloco sólido, o jogo empurra ela pra cima até que esteja no ar.

É uma otimização: em vez de perder tempo ignorando posições subterrâneas, o jogo as leva de volta à superfície. Esperto? Sim. Mas cria um viés absurdo: os mobs preferem altitudes mais altas.

Imagina. Você tem vários blocos abaixo da superfície, o jogo gera 10 posições aleatórias. As que estão em blocos são empurradas pra cima. Áreas densas (debaixo de um morro) produzem mais posições válidas que áreas vazias. Resultado: o mob vai estatisticamente mais vezes em direção ao morro.

Confia em mim, vamos quebrar isso em 2 minutos.

A seleção: o concurso do melhor bloco

10 posições, um só vencedor, um concurso de pontuação:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

A posição com a melhor pontuação GANHA. E se conhecemos os critérios de pontuação, podemos fazer a posição que queremos ganhar. É como fraudar uma eleição.


As preferências dos mobs (ou "por que sua vaca atravessa a rua")

Cada mob tem gostos diferentes. E isso muda tudo.

Mob Curti isso
Animais (vacas, ovelhas, porcos) Grama e luz (hipsters)
Monstros (zumbis, esqueletos) Escuridão (edgelords)
Tartarugas Água, senão areia, senão luz
Hoglins crimson_nylium; odeiam warped_fungus
Striders Só lava. NADA mais.
Silverfish Blocos infestáveis (lógico)
Guardians Água + luz (os snobs)
Mooshrooms Micélio + luz (cogumelos)
Abelhas Ar. Sim, elas preferem o AR.
// Animal: olha pra baixo, se for grama, pontuação máxima
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monstro: exatamente o oposto
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Tipo, os monstros é literalmente "se tá iluminado, pontuação negativa, vou pra outro lugar". Eles FAZEM CARA FEIA pra luz xD

Então você pode -- literalmente -- guiar seus animais com grama e luz, e seus monstros com escuridão. É bizarro e genial ao mesmo tempo.


O algoritmo A* no Minecraft (a fórmula secreta)

Minecraft usa o algoritmo A* (A-star) para pathfinding. Mas a Mojang colocou seu toque:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = o caminho já percorrido (1 por bloco, ~1.41 na diagonal)
  • h(n) = a distância em linha reta
  • 1.5 = porque a Mojang gosta de coisas meio quebradas

Normalmente A* usa f(n) = g(n) + h(n). A MOJANG ADICIONOU UM FATOR 1.5. Por quê? Pra fazer o algoritmo ir mais rápido ao destino e cortar menos ramos de busca. Resultado: o caminho encontrado é "bom" mas nem sempre o melhor. É um A* meio bêbado.

mermaid diagram

Detalhe importante: um mob só pode pathfindar até 16 blocos (sua follow range). Se o destino está muito longe, ele escolhe o bloco mais próximo que CONSEGUE alcançar. Isso significa que você pode criar um monólito fora de alcance, e o mob vai pathfindar até o bloco mais próximo que o aproxime desse monólito -- tornando seus movimentos completamente previsíveis.

Os dois exploits que quebram o jogo

1. Block updates = recálculo forçado

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Cada atualização de bloco perto do caminho do mob força um recálculo do A* com um cooldown de 1 segundo. Você coloca um clock de 1 segundo perto de um mob, e ele recalcula o caminho CONSTANTEMENTE. É o equivalente a colocar um GPS que reseta a cada segundo.

E se você fizer isso com 50 mobs ao mesmo tempo? Lag city. RIP TPS.

2. As penalidades de blocos (Pathfinding Malus)

Certos blocos assustam os mobs. Literalmente. Cada bloco tem um custo associado, definido por uma enumeração:

Bloco / Condição Penalidade
Bloco de mel +8 para atravessar
Neve fofa Intransponível
Portas fechadas Intransponível
Fogo +16 para atravessar, +8 para margear
Animais & Aldeões Fogo = -1 (NOPE)
Cactos / Sweet berry Intransponível; adjacente = +8
Água +8 para atravessar ou margear
Magma +8 para margear (dói)

Os animais são ainda mais extremos:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE em -1.0F é literalmente "proibido". Um animal prefere se jogar no vazio a atravessar fogo. Ufa.

Exercício: o grande concurso de caminhos

Imagina um aldeão que precisa escolher entre vários caminhos.

  • Caminho A: 15 blocos mas 6 blocos margeiam água (+8 cada)
  • Caminho B: 18 blocos com 2 blocos de água (+8) e 1 bloco adjacente de água (+8)
  • Caminho C: 14 blocos em linha reta... mas com fogo -> INTRANSPONÍVEL para um aldeão
  • Caminho D: 16 blocos com 1 bloco adjacente de magma (+8) + 1 bloco adjacente de mel (+8)
  • Caminho E: 25 blocos mas com cactos em tudo (+8 em tudo) -> 90.82 de custo total LOL

Cálculo mental:

  • Caminho A: 15 blocos + 6×8 pela água = 15 + 48 = 63 ... mas tem o 1.5×distância para somar. Vamos fazer os cálculos de verdade.
  • Caminho B: mais longo mas menos penalidades. O custo total = distância acumulada + penalidades.
  • Caminho D: magma e mel acumulam suas penalidades.

O vencedor geralmente é o Caminho B: o desvio vale a pena porque a água é CARA.

Um aldeão é essencialmente uma calculadora de custos com pernas xD

Cada mob tem seus gostos

Um aldeão: "fogo? NÃO OBRIGADO TCHAU" Um zumbi: "fogo? OK boomer atravessa em chamas"

Você tem literalmente rotas que alguns mobs pegam e outros não. Dá pra fazer autoestradas de aldeões onde zumbis se queimam.


Os aldeões: a bagunça suprema

Ok, os aldeões. É A parada menos compreendida de todo Minecraft. Mas quando você pega o código, percebe que são máquinas previsíveis com horário comercial.

Sensores e memórias

9 sensores, que rodam a cada 20 ticks (1 segundo). Cada um escaneia um raio em volta do aldeão e armazena o resultado na memória. O aldeão vê tudo, lembra de tudo, e age de acordo.

Tipo: "tem um inimigo? um item no chão? um jogador com quem falar?" -- ele checa TUDO.

Os packages (suas fases do dia)

O cérebro de um aldeão são packages de atividade que ativam conforme a hora:

Package Horário O aldeão...
Core 24h Abre portas, nada (80% do tempo), e ADQUIRE POI
Work 8h-15h "Vou trabalhar" -- anda até seu posto
Meet 15h-17h "Happy hour!" -- vai ao sino, papeia
Rest 18h-6h "Preciso dormir" -- vai pra cama
Idle 6h-8h, 17h-18h "Vagabundear" -- passeia, faz bebês, pula nas camas
Panic Ferimento/hostil "SOCORRO" -- FUGE

O package Panic é o único que pode interromper TODOS os outros. Mesmo se o aldeão estiver dormindo ou trabalhando, se tem um zumbi, PÂNICO GERAL.

Acquire POI: a parada que permite redstone sem fio

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI escaneia num raio de 48 blocos todos os POI (pontos de interesse). Ele guarda os 5 mais próximos, verifica se existe um caminho, e adquire o primeiro acessível.

Cada POI tem um número limitado de vagas:

  • Postos de trabalho: 1 vaga
  • Camas: 1 vaga
  • Sinos: 32 vagas

A parada LOUCA: a vaga é reservada no momento da aquisição, NÃO na chegada. Um aldeão pode travar um composter do outro lado do mapa, sem nunca alcançá-lo.

Saca o potencial?

Redstone sem fio. Sim, SEM FIO.

  1. Você coloca um aldeão num minecart com um caminho até um composter
  2. Ele adquire o composter (vaga ocupada, ninguém mais pode usar)
  3. O aldeão está longe demais para clicar nele -- a bone meal fica
  4. Você LEVA esse aldeão pra qualquer lugar do mundo, ele mantém a vaga
  5. Quando você quer ativar seu negócio, você M A T A o aldeão
  6. A vaga é liberada, outro aldeão adquire o composter, retira a bone meal
  7. BLOCK UPDATE -> qualquer circuito de redstone ativado

Você literalmente criou um sinal de redstone sem fio, transmissível em todo o mundo, com zero chunk load necessário no caminho. Dá pra conectar isso numa ender pearl stasis chamber, se teletransportar de qualquer lugar matando um aldeão.

Minha utilização favorita? Um minigame "bounty hunter": você coloca vários aldeões com composters, o jogador precisa matar O ALDEÃO CERTO para ativar a saída. É uma mecânica completamente wtf xD

O Pathfinding Deadlock (ou "o aldeão que congela pra sempre")

Tem um bug BOA demais entre o Acquire POI (que vê um caminho) e a navegação real (que se recusa a usá-lo). Acontece quando o bloco acima do posto de trabalho não é caminhável. Resultado:

  • Core package: "quero adquirir o POI"
  • Navegação: "não consigo andar aí"
  • Resultado: o aldeão fica CONGELADO, pra sempre, lutando consigo mesmo.

Literalmente aldeões frozen no lugar, utilizáveis como decoração ou como "props" em builds. Um tank de armadura parado? Sim. Um guarda que não se mexe? Sim. Macabro? Talvez. Mas eficaz xD


Conclusão

O pathfinding dos mobs do Minecraft não é aleatório. É um sistema determinístico, baseado em pontuações, previsível E quebrável.

As três coisas pra guardar:

  1. Blocos sob os pés = viés de altura -- preencha ou esvazie o subsolo para guiar os mobs
  2. As penalidades são diferentes para cada mob -- crie rotas que alguns pegam e outros não
  3. As vagas de POI são reservadas à distância -- redstone sem fio gratuita, teleporte, tudo isso

O código fonte do Minecraft é uma mina de ouro de mecânicas sub-exploradas. Passei horas lendo Java descompilado e sinceramente? Cada linha é um Easter Egg funcional. Só que esses, você usa no survival pra fazer redstone sem fio com aldeões. Melhor jogo confirmado.

xD

Logika Pathfinding Minecraft dan Aplikasinya

Bagaimana algoritma A*, malus blok, dan POI memungkinkan kita

Pendahuluan

Aku menghabiskan berjam-jam melihat domba menabrak tembok.

Dan jujur? Investasi terbaik dalam hidupku xD

Karena semakin kamu lihat mob-mob ini, semakin kamu sadar bahwa mereka tidaklah acak. Setiap gerakan dikodekan, bisa diprediksi, dan yang terpenting -- bisa dieksploitasi sepenuhnya. Aku akhirnya menyelami kode sumber Minecraft untuk benar-benar memahami cara kerja pathfinding, dan yang kutemukan adalah kamu bisa benar-benar mengontrol pikiran mob. Maksudku, memaksa mereka pergi ke mana KAMU mau, bukan ke tempat yang diputuskan secara acak.

Panduan ini adalah semua yang kupelajari saat menyelidikinya. AI, algoritma A*, malus tersembunyi, eksploitasi yang bisa kamu pakai di survival. Siapkan beliungmu.


Cara kerja AI mob (spoiler: kacau)

Goals

Setiap mob memiliki goals. Ini adalah daftar hal yang BISA dia lakukan dan seberapa besar dia INGIN melakukannya. Semakin kecil angkanya, semakin prioritas -- seperti daftar tugas versi chaos.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Pernah lihat zombie mengabaikan telur penyu untuk mengejarmu? Itu alasannya: ZombieAttackTurtleEggGoal punya prioritas 4, sedangkan ZombieAttackGoal (hal yang menyuruhnya memakan mukamu) ada di prioritas 2.

Ya, zombie lebih memilih memakanmu daripada memecahkan telur. Indahnya cinta xD

Goal yang benar-benar menarik perhatian kita adalah WaterAvoidingRandomStrollGoal, prioritas 7. Goal "nggak ada kerjaan jadi jalan-jalan random". Di sinilah kekacauan dimulai.

Pergerakan (atau "bagaimana random walk punya 1 dari 60 kesempatan untuk terjadi")

Setiap tick (setiap 0.05 detik), game memanggil canUse() untuk melihat apakah mob mau bergerak. 1 dari 60 kesempatan setiap tick. Benar-benar kacau desainnya, dan aku suka itu.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Jadi singkatnya: kalau kamu naik di atas mob -> tidak, kalau mob tidak melakukan apa-apa selama 5 detik -> tidak, kalau random bilang tidak -> tidak. Game ini BENAR-BENAR tidak ingin mob bergerak.

Tapi saat dia bergerak, getPosition() mengambil alih:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Lihat dua angka di akhir: itu radius XZ dan radius Y. Di air, mob mencari lebih jauh (15 bukan 10). Kalau tidak menemukan daratan, dia fallback ke super.getPosition() yang menerima air. Hasilnya: mob INGIN keluar dari air. Itulah kenapa hewan-hewanmu berenang seperti gila ke tepi.

Detail kecil yang menarik: ada 0.1% kemungkinan mob mengambil super.getPosition() daripada LandRandomPos. Satu banding seribu. Mojanglah xD

LandRandomPos: optimasi jelek yang mengubah segalanya

Ini adalah tahap FAVORITKU. Kebodohan teknis paling indah yang membuat pathfinding bisa dieksploitasi.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. Namanya sudah menjelaskan semuanya. Jika posisi yang dipilih ada di dalam blok solid, game mendorongnya ke atas sampai berada di udara.

Ini adalah optimasi: daripada membuang waktu mengabaikan posisi di bawah tanah, game mendorongnya ke permukaan. Pintar? Ya. Tapi ini menciptakan bias yang gila: mob lebih suka tempat tinggi.

Bayangkan. Kamu punya banyak blok di bawah permukaan, game menghasilkan 10 posisi acak. Yang ada di dalam blok didorong ke atas. Area padat (di bawah bukit) menghasilkan lebih banyak posisi valid daripada area kosong. Hasilnya: mob secara statistik lebih sering pergi ke bukit.

Percayalah, kita akan mengeksploitasi ini dalam 2 menit.

Seleksi: kontes blok terbaik

10 posisi, satu pemenang, kontes skor:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

Posisi dengan skor tertinggi MENANG. Dan kalau kita tahu kriteria skornya, kita bisa membuat posisi yang kita inginkan menang. Ini seperti mengatur kecurangan pemilu.


Preferensi mob (atau "kenapa sapimu menyeberang jalan")

Setiap mob punya selera berbeda. Dan itu mengubah segalanya.

Mob Suka ini
Hewan (sapi, domba, babi) Rumput dan cahaya (hipsters)
Monster (zombie, skeleton) Gelap (edgelords)
Penyu Air, kalau tidak pasir, kalau tidak cahaya
Hoglin crimson_nylium; benci warped_fungus
Strider Hanya lava. Bukan yang LAIN.
Silverfish Blok yang bisa diinfestasi (logis)
Guardian Air + cahaya (para snob)
Mooshroom Mycelium + cahaya (jamur)
Lebah Udara. Ya, mereka lebih suka UDARA.
// Hewan: lihat ke bawah, kalau rumput, skor maks
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: kebalikannya
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Monster tuh literally "kalau terang, skor negatif, aku pergi ke lain tempat". Mereka CEMBERUT sama cahaya xD

Jadi kamu bisa -- literally -- memandu hewanmu dengan rumput dan cahaya, dan monstermu dengan kegelapan. Ini konyol sekaligus brilian.


Algoritma A* di Minecraft (rumus rahasia)

Minecraft menggunakan algoritma A* (A-star) untuk pathfinding. Tapi Mojang menambahkan sentuhannya:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = jarak yang sudah ditempuh (1 per blok, ~1.41 secara diagonal)
  • h(n) = jarak garis lurus
  • 1.5 = karena Mojang suka hal-hal yang sedikit rusak

Normalnya A* menggunakan f(n) = g(n) + h(n). MOJANG MENAMBAHKAN FAKTOR 1.5. Kenapa? Agar algo lebih cepat menuju tujuan dan memangkas lebih sedikit cabang pencarian. Hasilnya: jalur yang ditemukan "cukup bagus" tapi tidak selalu yang terbaik. Ini A* yang sedikit mabuk.

mermaid diagram

Detail penting: mob hanya bisa pathfinding dalam 16 blok (follow range-nya). Jika tujuan terlalu jauh, dia memilih blok terdekat yang bisa DIA capai. Artinya kamu bisa membuat monolit di luar jangkauan, dan mob akan pathfinding menuju blok terdekat yang mendekatkannya ke monolit itu -- membuat pergerakannya sepenuhnya bisa diprediksi.

Dua eksploitasi yang merusak game

1. Block updates = recalculation paksa

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Setiap update blok di dekat jalur mob memaksa recalculation A* dengan cooldown 1 detik. Kamu pasang clock 1 detik di samping mob, dan dia menghitung ulang jalurnya TERUS-MENERUS. Ini setara dengan memasang GPS yang di-reset setiap detik.

Dan kalau kamu lakukan ini dengan 50 mob sekaligus? Lag city. RIP TPS.

2. Malus blok (Pathfinding Malice)

Blok tertentu menakut-nakuti mob. Secara harfiah. Setiap blok memiliki biaya terkait, yang didefinisikan oleh enum:

Blok / Kondisi Malus
Blok madu +8 untuk melewati
Bubuk salju Tidak bisa dilalui
Pintu tertutup Tidak bisa dilalui
Api +16 untuk melewati, +8 untuk menyusuri
Hewan & Villageois Api = -1 (NGGAK)
Kaktus / Sweet berry Tidak bisa dilalui; berdekatan = +8
Air +8 untuk melewati atau menyusuri
Magma +8 untuk menyusuri (aduh)

Hewan bahkan lebih ekstrem:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE di -1.0F, itu literally "terlarang". Seekor hewan lebih memilih menjatuhkan diri ke jurang daripada melewati api. Fiuh.

Latihan: kontes jalur besar

Bayangkan seorang villageois yang harus memilih antara beberapa jalur.

  • Jalur A: 15 blok tapi 6 blok menyusuri air (+8 setiap)
  • Jalur B: 18 blok dengan 2 blok air (+8) dan 1 blok berdekatan air (+8)
  • Jalur C: 14 blok lurus... tapi ada api -> TIDAK BISA DILALUI untuk villageois
  • Jalur D: 16 blok dengan 1 blok berdekatan magma (+8) + 1 blok berdekatan madu (+8)
  • Jalur E: 25 blok tapi kaktus di mana-mana (+8 di mana-mana) -> 90.82 total biaya LOL

Hitungan mental:

  • Jalur A : 15 blok + 6×8 untuk air = 15 + 48 = 63 ... tapi ada 1.5×jarak yang harus ditambahkan. Mari kita hitung yang bener.
  • Jalur B: lebih panjang tapi lebih sedikit malus. Total biaya = jarak kumulatif + malus.
  • Jalur D: magma dan madu menumpuk malusnya.

Pemenangnya biasanya Jalur B: memutar lebih menguntungkan karena air MAHAL.

Seorang villageois pada dasarnya adalah kalkulator biaya dengan kaki xD

Setiap mob punya seleranya

Seorang villageois: "api? NGGAK MAU DONG BYE" Zombie: "api? OK boomer lewat sambil terbakar"

Kamu literally punya rute yang diambil beberapa mob dan yang lain tidak. Kamu bisa membuat jalan tol villageois di mana zombie hangus terbakar.


Villageois: kekacauan tertinggi

Oke, villageois. Ini adalah hal yang PALING tidak dipahami di seluruh Minecraft. Tapi begitu kamu paham kodenya, kamu sadar bahwa mereka adalah mesin yang bisa diprediksi dengan jam kerja kantoran.

Sensor dan memori

9 sensor, berjalan setiap 20 tick (1 detik). Masing-masing mengamati radius di sekitar villageois dan menyimpan hasilnya di memori. Villageois melihat segalanya, mengingat segalanya, dan bertindak berdasarkan itu.

Kayak: "apakah ada musuh? item di tanah? pemain untuk diajak bicara?" -- dia ngecek SEMUANYA.

Package (fase hariannya)

Otak villageois adalah package aktivitas yang aktif tergantung waktu:

Package Jadwal Villageois...
Core 24 jam Membuka pintu, berenang (80% waktu), dan MEMPEROLEH POI
Work 8-15 "Aku mau kerja" -- berjalan ke tempat kerjanya
Meet 15-17 "Ngopi!" -- pergi ke bell, ngobrol
Rest 18-6 "Harus tidur" -- pergi ke tempat tidur
Idle 6-8, 17-18 "Santai" -- jalan-jalan, bikin bayi, lompat di tempat tidur
Panic Terluka/hostile "TOLONG" -- KABUR

Package Panic adalah satu-satunya yang bisa menginterupsi SEMUA yang lain. Bahkan jika villageois sedang tidur atau kerja, kalau ada zombie, PANIK TOTAL.

Acquire POI: hal yang memungkinkan redstone nirkabel

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI memindai dalam radius 48 blok semua POI (points of interest). Dia menyimpan 5 yang terdekat, memeriksa apakah jalur ada, dan memperoleh yang pertama bisa dijangkau.

Setiap POI memiliki jumlah slot terbatas:

  • Workstation: 1 slot
  • Tempat tidur: 1 slot
  • Bell: 32 slot

Hal yang GILA: slot dipesan saat akuisisi, BUKAN saat tiba. Seorang villageois bisa mengunci composter dari ujung map yang lain, tanpa pernah mencapainya.

Kamu paham potensinya?

Redstone nirkabel. Ya, NIRKABEL.

  1. Kamu taruh villageois di minecart dengan jalur menuju composter
  2. Dia memperoleh composter (slot terambil, tidak ada yang bisa menggunakannya)
  3. Villageois terlalu jauh untuk mengkliknya -- bone meal tetap ada
  4. Kamu BAWA villageois ini ke mana saja di dunia, dia tetap memegang slot
  5. Saat kamu ingin mengaktifkan barangmu, kamu B U N U H villageois itu
  6. Slot terbebas, villageois lain memperoleh composter, mengambil bone meal
  7. BLOCK UPDATE -> sirkuit redstone mana pun aktif

Kamu literally menciptakan sinyal redstone nirkabel, yang bisa ditransmisikan di seluruh dunia, tanpa perlu chunk load sama sekali di sepanjang jalur. Kamu bisa menghubungkannya ke ender pearl stasis chamber, membuatmu teleport dari mana saja dengan membunuh villageois.

Penggunaan favoritku? Mini-game "bounty hunter": kamu taruh beberapa villageois dengan composter, pemain harus membunuh villageois yang TEPAT untuk mengaktifkan pintu keluar. Benar-benar wtf sebagai mekanik xD

Pathfinding Deadlock (atau "villageois yang freeze selamanya")

Ada bug yang TERLALU bagus antara Acquire POI (yang melihat jalur) dan navigasi nyata (yang menolak melewatinya). Ini terjadi ketika blok di atas workstation tidak bisa diinjak. Hasilnya:

  • Core package: "aku ingin memperoleh POI"
  • Navigation: "aku tidak bisa berjalan di sana"
  • Hasil: villageois tetap BEKU, selamanya, bergumul dengan dirinya sendiri.

Villageois yang literally beku di tempat, bisa digunakan sebagai dekorasi atau "props" di build. Tank baju besi berdiri? Ya. Penjaga yang tidak bergerak? Ya. Mengerikan? Mungkin. Tapi efektif xD


Kesimpulan

Pathfinding mob Minecraft bukanlah kebetulan. Ini adalah sistem deterministik, berbasis skor, bisa diprediksi DAN bisa dieksploitasi.

Tiga hal yang perlu diingat:

  1. Blok di bawah kaki = bias ketinggian -- isi atau kosongkan bawah tanah untuk memandu mob
  2. Malus berbeda untuk setiap mob -- buat rute yang diambil beberapa mob dan tidak oleh yang lain
  3. Slot POI dipesan dari jarak jauh -- redstone nirkabel gratis, teleportasi, semuanya

Kode sumber Minecraft adalah tambang emas mekanik yang kurang dimanfaatkan. Aku menghabiskan berjam-jam membaca Java yang didekompilasi dan jujur? Setiap baris adalah Easter Egg fungsional. Hanya saja yang ini, kamu pakai di survival untuk bikin redstone nirkabel dengan villageois. Game terbaik confirmed.

xD

Minecraft पाथफ़ाइंडिंग तर्क और इसके अनुप्रयोग

कैसे A* एल्गोरिदम, ब्लॉक मैलस और POI मॉब्स की गति को नियंत्रित,

परिचय

मैंने घंटों भेड़ों को दीवारों से टकराते देखा है।

और सच कहूँ? ज़िंदगी का सबसे अच्छा निवेश xD

क्योंकि जितना तुम इन मॉब्स को देखोगे, उतना ही एहसास होगा कि इनमें कुछ भी रैंडम नहीं है। हर हरकत कोडेड है, पूर्वानुमेय है, और सबसे ज़रूरी -- पूरी तरह तोड़ने योग्य। मैंने Minecraft के सोर्स कोड में गोता लगाया ताकि समझ सकूँ कि पाथफ़ाइंडिंग असल में कैसे काम करता है, और मैंने पाया कि तुम सचमुच मॉब्स को माइंड-कंट्रोल कर सकते हो। मतलब, उन्हें वहाँ जाने के लिए मजबूर करना जहाँ तुम चाहो, न कि जहाँ संयोग तय करे।

यह गाइड वह सब कुछ है जो मैंने छानबीन करके सीखा। AI, A* एल्गोरिदम, छिपे हुए मैलस, वो एक्सप्लॉइट जो तुम सर्वाइवल में इस्तेमाल कर सकते हो। अपनी कुदाल तैयार रखो।


मॉब्स का AI कैसे काम करता है (स्पॉइलर: यह बेवकूफी भरा है)

Goals (लक्ष्य)

हर मॉब के goals होते हैं। यह उन चीज़ों की एक सूची है जो वह कर सकता है और वह उन्हें कितना करना चाहता है। संख्या जितनी छोटी, उतनी ज़्यादा प्राथमिकता -- बिल्कुल अराजकता वर्जन की टू-डू लिस्ट जैसा।

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

तुमने कभी ज़ॉम्बी को कछुए के अंडे को नज़रअंदाज़ करके तुम्हारे पीछे भागते देखा है? यही वजह है: ZombieAttackTurtleEggGoal की प्राथमिकता 4 है, जबकि ZombieAttackGoal (जो उसे तुम्हारा मुँह खाने को कहता है) की प्राथमिकता 2 है।

हाँ, एक ज़ॉम्बी तुम्हें खाना पसंद करेगा बजाय अंडा तोड़ने के। कितना प्यार है न xD

जो goal हमारे लिए सचमुच मायने रखता है वह है WaterAvoidingRandomStrollGoal, प्राथमिकता 7। "मेरे पास करने को कुछ नहीं तो बेतरतीब घूमता हूँ" वाला goal। यहीं से गड़बड़ शुरू होती है।

गति (या "एक रैंडम वॉक के होने की 1/60 संभावना कैसे है")

हर tick (हर 0.05 सेकंड) पर, गेम canUse() को कॉल करता है यह देखने के लिए कि मॉब हिलने की कृपा करेगा या नहीं। हर tick पर 1 मौका 60 में। डिज़ाइन पूरी तरह बेवकूफी भरा है, और मुझे यह पसंद है।

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

तो संक्षेप में: अगर तुम मॉब पर सवार हो -> नहीं, अगर मॉब ने 5 सेकंड से कुछ नहीं किया -> नहीं, अगर रैंडम ने ना कहा -> नहीं। गेम सचमुच नहीं चाहता कि मॉब हिले।

लेकिन जब वह हिलता है, getPosition() कमान संभालता है:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

अंत में उन दो नंबरों को देखो: यह XZ त्रिज्या और Y त्रिज्या है। पानी में, मॉब आगे तलाशता है (10 की बजाय 15)। अगर ज़मीन नहीं मिलती, तो वह super.getPosition() पर चला जाता है जो पानी को स्वीकार करता है। परिणाम: मॉब पानी से बाहर निकलना चाहते हैं। यही कारण है कि तुम्हारे जानवर पागलों की तरह किनारे की ओर तैरते हैं।

एक रसदार छोटी डिटेल: सचमुच 0.1% मौका है कि मॉब LandRandomPos के बजाय super.getPosition() ले। एक हजार में एक। Mojang है न xD

LandRandomPos: घटिया ऑप्टिमाइज़ेशन जो सब कुछ बदल देता है

यह मेरा सबसे पसंदीदा चरण है। सबसे खूबसूरत तकनीकी बेवकूफी जो पाथफ़ाइंडिंग को दोहन योग्य बनाती है।

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. नाम ही सब कह देता है। अगर चुनी गई पोज़ीशन किसी ठोस ब्लॉक के अंदर है, तो गेम उसे ऊपर धकेलता है जब तक वह हवा में न हो।

यह एक ऑप्टिमाइज़ेशन है: भूमिगत पोज़ीशनों को नज़रअंदाज़ करने में समय बर्बाद करने के बजाय, गेम उन्हें सतह पर ले आता है। चतुर? हाँ। लेकिन यह एक जबरदस्त बायस पैदा करता है: मॉब ऊँचाइयों को पसंद करते हैं।

सोचो। सतह के नीचे ढेर सारे ब्लॉक हैं, गेम 10 रैंडम पोज़ीशन जनरेट करता है। जो ब्लॉकों के अंदर हैं उन्हें ऊपर धकेल दिया जाता है। घने क्षेत्र (किसी पहाड़ी के नीचे) खोखले क्षेत्रों की तुलना में अधिक मान्य पोज़ीशन पैदा करते हैं। नतीजा: मॉब सांख्यिकीय रूप से अधिक बार पहाड़ी की ओर जाता है।

मुझ पर भरोसा करो, हम इसे 2 मिनट में तोड़ देंगे।

चयन: सबसे अच्छे ब्लॉक की प्रतियोगिता

10 पोज़ीशन, एक विजेता, एक स्कोर प्रतियोगिता:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

सबसे अच्छा स्कोर वाली पोज़ीशन जीतती है। और अगर हम स्कोर के मापदंड जानते हैं, तो हम अपनी मनचाही पोज़ीशन जितवा सकते हैं। यह चुनावों में धांधली करने जैसा है।


मॉब्स की पसंद (या "क्यों तुम्हारी गाय सड़क पार करती है")

हर मॉब की अलग पसंद होती है। और यह सब कुछ बदल देता है।

मॉब यह पसंद है
जानवर (गाय, भेड़, सूअर) घास और रोशनी (हिप्स्टर)
मॉन्स्टर (ज़ॉम्बी, स्केलेटन) अंधेरा (एजलॉर्ड)
कछुए पानी, नहीं तो रेत, नहीं तो रोशनी
हॉगलिन crimson_nylium; warped_fungus से नफरत
स्ट्राइडर सिर्फ लावा। और कुछ नहीं।
सिल्वरफ़िश संक्रमित होने योग्य ब्लॉक (लॉजिकल)
गार्डियन पानी + रोशनी (स्नॉब)
मशरूम माइसीलियम + रोशनी (मशरूम)
मधुमक्खियाँ हवा। हाँ, उन्हें हवा पसंद है।
// Animal: नीचे देखो, अगर घास है तो अधिकतम स्कोर
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: बिल्कुल उल्टा
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

जैसे मॉन्स्टर सचमुच में "अगर रोशनी है तो स्कोर नेगेटिव, मैं कहीं और जाऊँगा" होते हैं। वे रोशनी से मुँह फेर लेते हैं xD

तो तुम सचमुच -- अपने जानवरों को घास और रोशनी से, और अपने मॉन्स्टरों को अंधेरे से गाइड कर सकते हो। यह एक ही समय में बेवकूफी भरा और प्रतिभाशाली है।


Minecraft में A* एल्गोरिदम (गुप्त फॉर्मूला)

Minecraft पाथफ़ाइंडिंग के लिए A* (A-star) एल्गोरिदम का उपयोग करता है। लेकिन Mojang ने अपनी छाप छोड़ी है:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = अब तक तय किया गया पथ (1 प्रति ब्लॉक, विकर्ण में ~1.41)
  • h(n) = सीधी दूरी (जैसे उड़ता हुआ कौआ)
  • 1.5 = क्योंकि Mojang को थोड़ी टूटी-फूटी चीज़ें पसंद हैं

सामान्यतः A* f(n) = g(n) + h(n) का उपयोग करता है। MOJANG ने 1.5 का फैक्टर जोड़ा है। क्यों? ताकि एल्गोरिदम गंतव्य की ओर तेज़ी से जाए और कम सर्च ब्रांच काटे। परिणाम: मिला रास्ता "अच्छा" है लेकिन हमेशा सबसे अच्छा नहीं। यह थोड़ा नशे में A* है।

mermaid diagram

महत्वपूर्ण विवरण: एक मॉब केवल 16 ब्लॉक तक पाथफ़ाइंड कर सकता है (उसकी follow range)। अगर गंतव्य बहुत दूर है, तो वह सबसे नज़दीकी ब्लॉक चुनता है जिस तक वह पहुँच सकता है। इसका मतलब है कि तुम एक ऐसा मोनोलिथ बना सकते हो जो पहुँच से बाहर हो, और मॉब उसके सबसे नज़दीकी ब्लॉक तक पाथफ़ाइंड करेगा जो उसे मोनोलिथ के करीब ले जाए -- जिससे उसकी हरकतें पूरी तरह पूर्वानुमेय हो जाती हैं।

दो एक्सप्लॉइट जो गेम तोड़ देते हैं

1. Block updates = जबरन पुनर्गणना

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

मॉब के रास्ते के पास हर ब्लॉक अपडेट 1 सेकंड के कूलडाउन के साथ A* की पुनर्गणना को मजबूर करता है। तुम मॉब के पास 1 सेकंड की घड़ी रख दो, और वह लगातार अपने रास्ते की पुनर्गणना करेगा। यह वैसे ही है जैसे GPS हर सेकंड रीसेट हो जाए।

और अगर तुम यह एक साथ 50 मॉब्स पर करो? Lag city. TPS को विदाई।

2. ब्लॉक मैलस (Pathfinding Malice)

कुछ ब्लॉक मॉब्स को डराते हैं। सचमुच। हर ब्लॉक की एक जुड़ी हुई लागत होती है, जो एक एनुम द्वारा परिभाषित होती है:

ब्लॉक / शर्त मैलस
हनी ब्लॉक पार करने में +8
पाउडर स्नो अगम्य
बंद दरवाजे अगम्य
आग पार करने में +16, बगल में चलने में +8
जानवर और ग्रामीण आग = -1 (नहीं)
कैक्टस / स्वीट बेरी अगम्य; बगल में = +8
पानी पार करने या बगल में चलने में +8
मैग्मा बगल में चलने में +8 (आउच)

जानवर और भी ज़्यादा चरम हैं:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE को -1.0F, यह सचमुच "निषिद्ध" है। एक जानवर आग पार करने के बजाय खुद को खाई में फेंकना पसंद करेगा। वाह।

अभ्यास: रास्तों की महान प्रतियोगिता

एक ग्रामीण की कल्पना करो जिसे कई रास्तों में से चुनना है।

  • रास्ता A: 15 ब्लॉक लेकिन 6 ब्लॉक पानी के बगल में (+8 प्रत्येक)
  • रास्ता B: 18 ब्लॉक जिसमें 2 ब्लॉक पानी (+8) और 1 ब्लॉक पानी के बगल में (+8)
  • रास्ता C: 14 ब्लॉक सीधे... लेकिन आग के साथ -> ग्रामीण के लिए अगम्य
  • रास्ता D: 16 ब्लॉक जिसमें 1 मैग्मा (+8) + 1 हनी (+8)
  • रास्ता E: 25 ब्लॉक लेकिन हर जगह कैक्टस (+8 हर जगह) -> कुल 90.82 की लागत LOL

मानसिक गणना:

  • रास्ता A: 15 ब्लॉक + 6×8 पानी के लिए = 15 + 48 = 63 ... लेकिन 1.5×दूरी जोड़नी है। चलो असली गणना करते हैं।
  • रास्ता B: लंबा लेकिन कम मैलस। कुल लागत = संचित दूरी + मैलस।
  • रास्ता D: मैग्मा और हनी अपने मैलस स्टैक करते हैं।

विजेता अक्सर रास्ता B होता है: चक्कर लगाना फ़ायदेमंद है क्योंकि पानी महँगा है।

एक ग्रामीण मूलतः पैरों वाला लागत कैलकुलेटर है xD

हर मॉब की अपनी पसंद

एक ग्रामीण: "आग? नहीं धन्यवाद अलविदा" एक ज़ॉम्बी: "आग? ठीक है बूमर जलता हुआ पार करता है"

तुम्हारे पास सचमुच ऐसे रास्ते हैं जो कुछ मॉब लेते हैं और कुछ नहीं। तुम ग्रामीणों के लिए हाइवे बना सकते हो जहाँ ज़ॉम्बी जलकर खत्म हो जाएँ।


ग्रामीण: अंतिम गड़बड़ी

ठीक है, ग्रामीण। यह वह चीज़ है जो पूरे Minecraft में सबसे कम समझी जाती है। लेकिन एक बार जब तुमने कोड पकड़ लिया, तो तुम्हें एहसास होता है कि ये ऑफिस शेड्यूल वाली पूर्वानुमेय मशीनें हैं।

सेंसर और मेमोरी

9 सेंसर, जो हर 20 टिक (1 सेकंड) पर चलते हैं। प्रत्येक ग्रामीण के चारों ओर एक त्रिज्या में स्कैन करता है और परिणाम मेमोरी में स्टोर करता है। ग्रामीण सब कुछ देखता है, सब कुछ याद रखता है, और उसके अनुसार कार्य करता है।

जैसे: "क्या कोई दुश्मन है? ज़मीन पर कोई आइटम? बात करने के लिए कोई खिलाड़ी?" -- वह सब कुछ चेक करता है।

पैकेज (दिन के उसके चरण)

ग्रामीण का दिमाग एक्टिविटी पैकेज होते हैं जो समय के अनुसार सक्रिय होते हैं:

पैकेज समय ग्रामीण...
Core 24/7 दरवाजे खोलता है, तैरता है (80% समय), और POI अधिग्रहित करता है
Work 8am-3pm "काम पर जाऊँ" -- अपने स्टेशन की ओर चलता है
Meet 3pm-5pm "चाय-चर्चा!" -- घंटी के पास जाता है, गपशप करता है
Rest 6pm-6am "सोना है" -- बिस्तर पर जाता है
Idle 6am-8am, 5pm-6pm "समय बर्बाद कर रहा हूँ" -- घूमता है, बच्चे पैदा करता है, बिस्तरों पर कूदता है
Panic चोट/शत्रु "बचाओ" -- भागना

Panic पैकेज एकमात्र ऐसा है जो बाकी सभी को बीच में रोक सकता है। भले ही ग्रामीण सो रहा हो या काम कर रहा हो, अगर कोई ज़ॉम्बी है, तो सर्वत्र घबराहट।

Acquire POI: वह चीज़ जो वायरलेस रेडस्टोन संभव बनाती है

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI 48 ब्लॉक की त्रिज्या में सभी POI (रुचि के बिंदु) को स्कैन करता है। वह 5 सबसे नज़दीकी रखता है, जाँचता है कि कोई रास्ता मौजूद है, और पहले सुलभ को अधिग्रहित करता है।

हर POI के पास सीमित संख्या में स्लॉट होते हैं:

  • वर्कस्टेशन: 1 स्लॉट
  • बिस्तर: 1 स्लॉट
  • घंटियाँ: 32 स्लॉट

पागलपन वाली बात: स्लॉट अधिग्रहण के समय रिज़र्व होता है, पहुँचने पर नहीं। एक ग्रामीण मानचित्र के दूसरे छोर से कम्पोस्टर को लॉक कर सकता है, बिना कभी उस तक पहुँचे।

समझ में आ रही है पावर?

वायरलेस रेडस्टोन। हाँ, वायरलेस।

  1. एक ग्रामीण को कम्पोस्टर की ओर रास्ते वाले माइनकार्ट में रखो
  2. वह कम्पोस्टर अधिग्रहित करता है (स्लॉट ले लिया, कोई और उपयोग नहीं कर सकता)
  3. ग्रामीण क्लिक करने के लिए बहुत दूर है -- बोन मील बनी रहती है
  4. तुम इस ग्रामीण को दुनिया में कहीं भी ले जाओ, वह स्लॉट रखता है
  5. जब तुम अपनी चीज़ सक्रिय करना चाहो, तुम ग्रामीण को मार देते हो
  6. स्लॉट खाली होता है, दूसरा ग्रामीण कम्पोस्टर अधिग्रहित करता है, बोन मील निकालता है
  7. BLOCK UPDATE -> कोई भी रेडस्टोन सर्किट सक्रिय

तुमने सचमुच एक वायरलेस रेडस्टोन सिग्नल बनाया है, जो पूरी दुनिया में संचारणीय है, जिसके रास्ते में शून्य चंक लोड की आवश्यकता है। तुम इसे एंडर पर्ल स्टेसिस चैंबर से जोड़ सकते हो, एक ग्रामीण को मारकर कहीं से भी टेलीपोर्ट करवा सकते हो।

मेरा पसंदीदा उपयोग? एक मिनी-गेम "bounty hunter": तुम कई ग्रामीणों को कम्पोस्टर के साथ रखो, खिलाड़ी को आउटपुट सक्रिय करने के लिए सही ग्रामीण को मारना होगा। यह पूरी तरह wtf मैकेनिक है xD

Pathfinding Deadlock (या "वह ग्रामीण जो हमेशा के लिए फ़्रीज़ हो जाता है")

एक बहुत अच्छा बग है Acquire POI (जो रास्ता देखता है) और वास्तविक नेविगेशन (जो उसका उपयोग करने से मना करता है) के बीच। यह तब होता है जब वर्कस्टेशन के ऊपर का ब्लॉक चलने योग्य नहीं है। परिणाम:

  • Core package: "मैं POI अधिग्रहित करना चाहता हूँ"
  • Navigation: "मैं वहाँ चल नहीं सकता"
  • नतीजा: ग्रामीण स्थिर रहता है, हमेशा के लिए, खुद से लड़ता हुआ।

सचमुच जगह-जगह जमे हुए ग्रामीण, बिल्ड में सजावट या "प्रॉप्स" के रूप में उपयोग करने लायक। एक स्टैंडिंग आर्मर रैक? हाँ। एक गार्ड जो हिलता नहीं? हाँ। भयानक? शायद। लेकिन प्रभावी xD


निष्कर्ष

Minecraft मॉब्स की पाथफ़ाइंडिंग कोई संयोग नहीं है। यह एक नियतिवादी प्रणाली है, जो स्कोर पर आधारित, पूर्वानुमेय और तोड़ने योग्य है।

याद रखने वाली तीन बातें:

  1. पैरों के नीचे ब्लॉक = ऊँचाई बायस -- मॉब्स को गाइड करने के लिए तहखाना भरो या खाली करो
  2. मैलस हर मॉब के लिए अलग होते हैं -- ऐसे रास्ते बनाओ जो कुछ लें और कुछ न लें
  3. POI स्लॉट दूर से रिज़र्व होते हैं -- मुफ्त वायरलेस रेडस्टोन, टेलीपोर्टेशन, सब कुछ

Minecraft का सोर्स कोड कम उपयोग की गई मैकेनिकों की सोने की खान है। मैंने डीकंपाइल्ड Java पढ़ने में घंटों बिताए और सच कहूँ? हर लाइन एक फंक्शनल ईस्टर एग है। सिवाय इसके कि इनका उपयोग तुम सर्वाइवल में ग्रामीणों के साथ वायरलेस रेडस्टोन बनाने के लिए कर सकते हो। सबसे अच्छा गेम कन्फर्म।

xD

منطق تحديد المسار في ماينكرافت وتطبيقاته

كيف تتيح خوارزمية A* وعقوبات الكتل ونقاط الاهتمام (POI) التحكم

مقدمة

قضيت ساعات وأنا أشاهد الخرفان ترتطم بالجدران.

وبصراحة؟ أفضل استثمار في حياتي xD

لأنه كلما زادت مشاهدتك لهذه الموبات، أدركت أنها ليست عشوائية أبداً. كل حركة مبرمجة، متوقعة، والأهم -- قابلة للكسر تماماً. في النهاية غصت في الكود المصدري لماينكرافت لأفهم بالضبط كيف يعمل تحديد المسار، وما اكتشفته هو أنك تستطيع حرفياً التحكم بعقول الموبات. يعني، إجبارها على الذهاب حيث تريد أنت، وليس حيث يقرر العشوائي.

هذا الدليل هو كل ما تعلمته أثناء البحث. الذكاء الاصطناعي، خوارزمية A*، العقوبات المخفية، الاستغلالات التي يمكنك تطبيقها في البقاء. جهز معولك.


كيف يعمل ذكاء الموبات (المفسد: إنه سخيف)

الأهداف (Goals)

كل موب لديه أهداف. إنها قائمة بأشياء يمكنه فعلها ومدى رغبته في فعلها. كلما كان الرقم أصغر، زادت الأولوية -- مثل قائمة مهام بنسخة فوضوية.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

سبق ورأيت زومبي يتجاهل بيضة سلحفاة ليلاحقك؟ هذا هو السبب: ZombieAttackTurtleEggGoal له أولوية 4، بينما ZombieAttackGoal (الشيء الذي يخبره بأكل وجهك) له أولوية 2.

نعم، الزومبي يفضّل أكلک على كسر بيضة. هذا جميل xD

الهدف الذي يهمنا حقاً هو WaterAvoidingRandomStrollGoal، أولوية 7. هدف "ليس لدي ما أفعله لذا أمشي عشوائياً". هنا تبدأ الفوضى.

الحركة (أو "كيف أن random march لديه فرصة 1 من 60 للحدوث")

كل تيك (كل 0.05 ثانية)، يستدعي اللعبة canUse() لترى إذا كان الموب يتفضل بالتحرك. فرصة 1 من 60 في كل تيك. تصميم سخيف تماماً، وأنا أحبه.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

إذاً للتبسيط: إن كنت راكباً على الموب -> لا، إذا لم يفعل الموب شيئاً منذ 5 ثوانٍ -> لا، إذا قال العشوائي لا -> لا. اللعبة حقاً لا تريد أن يتحرك الموب.

لكن عندما يتحرك، getPosition() تتولى الأمر:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

انظر إلى هذين الرقمين في النهاية: هما نصف القطر XZ ونصف القطر Y. في الماء، يبحث الموب أبعد (15 بدلاً من 10). إذا لم يجد أرضاً، يتراجع إلى super.getPosition() التي تقبل الماء. النتيجة: الموبات تريد الخروج من الماء. لهذا تسبح حيواناتك كالمجانين نحو الحافة.

تفصيل صغير مثير: هناك حرفياً 0.1% فرصة أن يأخذ الموب super.getPosition() بدلاً من LandRandomPos. واحد في الألف. موجانغ يعني xD

LandRandomPos: التحسين التافه الذي يغير كل شيء

هذه مرحلتي المفضلة. أغبى خدعة تقنية تجعل تحديد المسار قابلاً للاستغلال.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. الاسم يقول كل شيء. إذا كان الموضع المختار داخل كتلة صلبة، تدفعه اللعبة للأعلى حتى يصبح في الهواء.

هذا تحسين: بدلاً من إضاعة الوقت بتجاهل المواضع تحت الأرض، ترفعها اللعبة إلى السطح. ذكي؟ نعم. لكنه يخلق انحيازاً جنونياً: الموبات تفضل المرتفعات.

تخيل. لديك الكثير من الكتل تحت السطح، تولد اللعبة 10 مواضع عشوائية. تلك الموجودة في الكتل تُدفع للأعلى. المناطق الكثيفة (تحت تل) تنتج مواضع صالحة أكثر من المناطق الفارغة. النتيجة: الموب سيذهب إحصائياً نحو التل أكثر.

ثق بي، سنكسر هذا في دقيقتين.

الاختيار: مسابقة أفضل كتلة

10 مواضع، فائز واحد، مسابقة نقاط:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

الموضع ذو أعلى نقاط يَفوز. وإذا عرفنا معايير النقاط، يمكننا جعل الموضع الذي نريده يفوز. إنه مثل تزوير انتخابات.


تفضيلات الموبات (أو "لماذا تعبر بقرتك الطريق")

كل موب لديه أذواق مختلفة. وهذا يغير كل شيء.

موب يحب هذا
الحيوانات (بقر، خرفان، خنازير) العشب والضوء (الهيبيون)
الوحوش (زومبي، هياكل عظمية) الظلام (محبو الظل)
السلاحف الماء، وإلا الرمل، وإلا الضوء
Hoglins crimson_nylium؛ يكرهون warped_fungus
Striders الحمم فقط. لا شيء غيرها.
Silverfish الكتل القابلة للإصابة (منطقي)
Guardians ماء + ضوء (المتكبرون)
Mooshrooms mycelium + ضوء (فطر)
النحل الهواء. نعم، يفضلون الهواء.
// حيوان: ينظر للأسفل، إذا كان عشباً، أعلى نقاط
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// وحش: العكس تماماً
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

الوحوش حرفياً "إذا كان مضاءً، نقاط سلبية، سأذهب لمكان آخر". إنهم يتجهمون من الضوء xD

لذا يمكنك -- حرفياً -- توجيه حيواناتك بالعشب والضوء، ووحوشك بالظلام. هذا سخيف ورائع في آن واحد.


خوارزمية A* في ماينكرافت (الصيغة السرية)

تستخدم ماينكرافت خوارزمية A* (A-star) لتحديد المسار. لكن موجانغ وضعت بصمتها:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = المسار المقطوع بالفعل (1 لكل كتلة، ~1.41 قطرياً)
  • h(n) = المسافة الجوية
  • 1.5 = لأن موجانغ تحب الأشياء المكسورة قليلاً

عادة A* تستخدم f(n) = g(n) + h(n). موجانغ أضافت عامل 1.5. لماذا؟ لتجعل الخوارزمية تذهب أسرع نحو الوجهة وتقطع فروع بحث أقل. النتيجة: المسار الموجود "جيد" لكنه ليس الأفضل دائماً. إنها A* ثملة بعض الشيء.

mermaid diagram

تفصيل مهم: الموب لا يمكنه تحديد مسار لأكثر من 16 كتلة (مدى متابعته). إذا كانت الوجهة بعيدة جداً، يختار أقرب كتلة يمكنه الوصول إليها. هذا يعني أنه يمكنك إنشاء نصب تذكاري خارج متناوله، وسيحدد الموب مساره نحو أقرب كتلة تقربه منه -- مما يجعل حركاته متوقعة تماماً.

الاستغلالان اللذان يكسّران اللعبة

1. تحديثات الكتلة = إعادة حساب إجبارية

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

كل تحديث كتلة قرب مسار الموب يجبر إعادة حساب A* مع تبريد لمدة ثانية واحدة. تضع ساعة بثانية واحدة بجانب موب، فيعيد حساب مساره باستمرار. هذا يعادل وضع GPS يُعاد ضبطه كل ثانية.

وإذا فعلت هذا مع 50 موباً في نفس الوقت؟ مدينة التأخير. وداعاً TPS.

2. عقوبات الكتل (Pathfinding Malice)

بعض الكتل تخيف الموبات. حرفياً. كل كتلة لها تكلفة مرتبطة، معرفة بتعداد:

كتلة / حالة عقوبة
كتلة العسل +8 لعبورها
الثلج المسحوق غير قابل للعبور
الأبواب المغلقة غير قابل للعبور
النار +16 لعبورها، +8 للمحاذاة
الحيوانات والقرويون نار = -1 (كلا)
صبار / توت حلو غير قابل للعبور؛ المجاور = +8
الماء +8 لعبوره أو محاذاته
السبج +8 للمحاذاة (أوتش)

الحيوانات أكثر تطرفاً:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE بقيمة -1.0F، هذا حرفياً "ممنوع". الحيوان يفضل رمي نفسه في الهاوية على عبور النار. فيوو.

تمرين: مسابقة المسارات الكبرى

تخيل قروياً يجب أن يختار بين عدة مسارات.

  • المسار A: 15 كتلة لكن 6 كتل تحاذي الماء (+8 لكل منها)
  • المسار B: 18 كتلة مع كتلتين من الماء (+8) وكتلة واحدة مجاورة للماء (+8)
  • المسار C: 14 كتلة بشكل مستقيم... لكن مع نار -> غير قابل للعبور للقروي
  • المسار D: 16 كتلة مع كتلة واحدة مجاورة للسبج (+8) + كتلة واحدة مجاورة للعسل (+8)
  • المسار E: 25 كتلة لكن مع صبار في كل مكان (+8 في كل مكان) -> 90.82 تكلفة إجمالية LOL

حساب ذهني:

  • المسار A: 15 كتلة + 6×8 للماء = 15 + 48 = 63 ... لكن يجب إضافة 1.5× المسافة. دعنا نجري الحسابات الحقيقية.
  • المسار B: أطول لكن عقوبات أقل. التكلفة الإجمالية = المسافة المقطوعة + العقوبات.
  • المسار D: السبج والعسل يكدسان عقوباتهما.

الفائز غالباً هو المسار B: الالتفاف مربح لأن الماء غالي الثمن.

القروي هو أساساً آلة حساب تكاليف بأرجل xD

كل موب له أذواقه

قروي: "نار؟ لا شكراً باي" زومبي: "نار؟ تمام يا كبير يعبرها وهو يحترق"

لديك حرفياً طرق يمر بها بعض الموبات وآخرون لا. يمكنك عمل طرق سريعة للقرويين حيث يحترق الزومبي.


القرويون: الفوضى العظمى

حسناً، القرويون. هذا أكثر شيء غير مفهوم في ماينكرافت. لكن بمجرد أن تفهم الكود، تدرك أنهم آلات متوقعة بساعات مكتبية.

المستشعرات والذكريات

9 مستشعرات، تعمل كل 20 تيك (ثانية واحدة). كل منها يمسح نصف قطر حول القروي ويخزن النتيجة في الذاكرة. القروي يرى كل شيء، يتذكر كل شيء، ويتصرف بناءً عليه.

مثل: "هل هناك عدو؟ شيء على الأرض؟ لاعب للتحدث معه؟" -- يتحقق من كل شيء.

الحزم (مراحل يومه)

دماغ القروي عبارة عن حزم نشاط تنشط حسب الوقت:

حزمة الوقت القروي...
Core 24 ساعة يفتح الأبواب، يسبح (80% من الوقت)، ويَكْتَسِب POIs
Work 8ص-3م "سأذهب للعمل" -- يمشي نحو محطة عمله
Meet 3م-5م "أبريه!" -- يذهب للجرس، يثرثر
Rest 6م-6ص "يجب النوم" -- يذهب للسرير
Idle 6ص-8ص، 5م-6م "أتسكع" -- يتجول، ينجب أطفالاً، يقفز على الأسرة
Panic إصابة/عداء "النجدة" -- هروب

حزمة Panic هي الوحيدة التي يمكنها مقاطعة كل الحزم الأخرى. حتى لو كان القروي نائماً أو يعمل، إذا كان هناك زومبي، ذعر عام.

Acquire POI: الشيء الذي يتيح الريدستون اللاسلكية

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI يمسح في نصف قطر 48 كتلة كل نقاط الاهتمام (POIs). يحتفظ بأقرب 5، ويتحقق من وجود مسار، ويكتسب أول مسار يمكن الوصول إليه.

كل POI له عدد محدود من الفتحات:

  • محطات العمل: فتحة واحدة
  • أسرة: فتحة واحدة
  • أجراس: 32 فتحة

الشيء المذهل: الفتحة تُحجز في وقت الاكتساب، وليس عند الوصول. يمكن لقروي أن يقفل كومبوستر من الطرف الآخر للخريطة، دون أن يصل إليه أبداً.

هل تستوعب القوة؟

ريدستون لاسلكية. نعم، لاسلكية.

  1. تضع قروياً في عربة منجم مع مسار نحو كومبوستر
  2. يكتسب الكومبوستر (فتحة محجوزة، لا يمكن لأحد استخدامها)
  3. القروي بعيد جداً ليضغط عليه -- تبقى الـ bone meal
  4. تَجُر هذا القروي في أي مكان في العالم، يحتفظ بالفتحة
  5. عندما تريد تفعيل جهازك، تَقْتُل القروي
  6. تتحرر الفتحة، يكتسب قروي آخر الكومبوستر، يزيل الـ bone meal
  7. BLOCK UPDATE -> أي دائرة ريدستون تُفعل

لقد أنشأت حرفياً إشارة ريدستون لاسلكية، قابلة للنقل في أي مكان في العالم، بدون حاجة لتحميل أي chunks في الطريق. يمكنك وصل هذا بحجرة stasis pearl، وتجعلك تنتقل من أي مكان بقتل قروي.

استخدامي المفضل؟ لعبة صغيرة "صائد جوائز": تضع عدة قرويين مع كومبوسترات، على اللاعب قتل القروي الصحيح لتفعيل المخرج. إنها آلية wtf تماماً xD

طريق مسدود لتحديد المسار (أو "القروي الذي يتجمد للأبد")

هناك خطأ رائع بين Acquire POI (الذي يرى مساراً) والملاحة الفعلية (التي ترفض استخدامه). يحدث عندما تكون الكتلة فوق محطة العمل غير قابلة للسير. النتيجة:

  • حزمة Core: "أريد اكتساب POI"
  • الملاحة: "لا أستطيع المشي هناك"
  • النتيجة: يبقى القروي مُجمّداً، للأبد، في صراع مع نفسه.

حرفياً قرويون متجمدون في مكانهم، يمكن استخدامهم كديكور أو كـ"دعائم" في المباني. دبابة واقف؟ نعم. حارس لا يتحرك؟ نعم. مروع؟ ربما. لكن فعال xD


الخاتمة

تحديد مسار الموبات في ماينكرافت ليس عشوائياً. إنه نظام حتمي، قائم على النقاط، متوقع وقابل للكسر.

الأشياء الثلاثة التي يجب تذكرها:

  1. كتل تحت الأقدام = انحياز ارتفاعي -- املأ أو أفرغ تحت الأرض لتوجيه الموبات
  2. العقوبات مختلفة لكل موب -- أنشئ طرقاً يسلكها البعض دون الآخرين
  3. فتحات POI تُحجز عن بعد -- ريدستون لاسلكية مجانية، انتقال فوري، كل ذلك

الكود المصدري لماينكرافت هو منجم ذهب من الآليات غير المستغلة. قضيت ساعات أقرأ Java مفككة وبصراحة؟ كل سطر هو بيضة عيد الفصح وظيفية. إلا أن هذه، تستخدمها في البقاء لعمل ريدستون لاسلكية بالقرويين. أفضل لعبة مؤكدة.

xD

Logic pathfinding Minecraft và các ứng dụng

Cách thuật toán A*, điểm phạt khối và POI cho phép điều khiển, dự đoán và khai thác

Giới thiệu

Tôi đã dành hàng giờ để nhìn mấy con cừu đâm đầu vào tường.

Thành thật mà nói? Đầu tư xứng đáng nhất đời tôi xD

Bởi vì càng nhìn mấy con mob này, bạn càng nhận ra chúng chẳng có gì ngẫu nhiên. Mọi chuyển động đều được mã hóa, có thể dự đoán, và quan trọng nhất -- hoàn toàn có thể phá vỡ. Cuối cùng tôi đã đào sâu vào mã nguồn Minecraft để hiểu chính xác pathfinding hoạt động thế nào, và thứ tôi khám phá ra là bạn có thể điều khiển tâm trí mob theo đúng nghĩa đen. Kiểu, ép chúng đi chỗ BẠN muốn, chứ không phải nơi ngẫu nhiên quyết định.

Hướng dẫn này là tất cả những gì tôi học được khi mày mò. AI, thuật toán A*, điểm phạt ẩn, những exploit bạn có thể xài trong survival. Chuẩn bị cuốc của bạn đi.


Cách AI của mob hoạt động (spoiler: nó khá ngu)

Các Goal

Mỗi mob có các goal. Đó là danh sách những thứ nó CÓ THỂ làm và mức độ nó MUỐN làm. Số càng nhỏ thì càng ưu tiên -- như một todo list phiên bản hỗn loạn.

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

Bạn đã bao giờ thấy zombie lơ một quả trứng rùa để rượt bạn chưa? Đây là lý do: ZombieAttackTurtleEggGoal có độ ưu tiên 4, trong khi ZombieAttackGoal (thứ bảo nó ăn mặt bạn) có độ ưu tiên 2.

Ừ, zombie thích ăn bạn hơn là đập trứng. Tình yêu đẹp đấy xD

Goal mà chúng ta thực sự quan tâm là WaterAvoidingRandomStrollGoal, độ ưu tiên 7. Cái goal "chả có gì để làm nên đi bộ lung tung". Đó là nơi mọi chuyện bắt đầu.

Di chuyển (hay "cách một random walk có 1/60 cơ hội xảy ra")

Mỗi tick (mỗi 0.05 giây), game gọi canUse() để xem mob có thèm động đậy không. 1 trên 60 cơ hội mỗi tick. Thiết kế ngu vãi, và tôi yêu nó.

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

Tóm lại: nếu bạn đang cưỡi trên mob -> không, nếu mob chưa làm gì 5 giây -> không, nếu random bảo không -> không. Game thực sự KHÔNG muốn mob động đậy.

Nhưng khi nó động đậy, getPosition() tiếp quản:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

Nhìn hai con số ở cuối: đó là bán kính XZ và bán kính Y. Trong nước, mob tìm xa hơn (15 thay vì 10). Nếu không tìm thấy đất, nó rơi xuống super.getPosition() – cái chấp nhận nước. Kết quả: mob MUỐN ra khỏi nước. Đó là lý do động vật của bạn bơi như điên về phía bờ.

Chi tiết ngon: có đúng 0.1% cơ hội mob lấy super.getPosition() thay vì LandRandomPos. Một phần nghìn. Mojang đó xD

LandRandomPos: cái tối ưu dở hơi làm thay đổi mọi thứ

Đây là bước TÔI yêu thích nhất. Thứ kỹ thuật ngu ngốc nhất khiến pathfinding có thể khai thác được.

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. Cái tên nói lên tất cả. Nếu vị trí được chọn nằm trong khối rắn, game đẩy nó lên trên cho đến khi ở trong không khí.

Đây là một tối ưu hóa: thay vì mất thời gian bỏ qua các vị trí dưới lòng đất, game đưa chúng lên mặt đất. Thông minh? Ừ. Nhưng nó tạo ra một thiên vị điên rồ: mob thích độ cao hơn.

Hãy tưởng tượng. Bạn có đầy khối dưới mặt đất, game tạo ra 10 vị trí ngẫu nhiên. Những vị trí trong khối bị đẩy lên trên. Vùng đặc (dưới đồi) tạo ra nhiều vị trí hợp lệ hơn vùng rỗng. Kết quả: mob về mặt thống kê sẽ đi về phía đồi nhiều hơn.

Tin tôi đi, chúng ta sẽ phá vỡ điều này trong 2 phút.

Chọn lọc: cuộc thi khối nào ngon nhất

10 vị trí, một người thắng, một cuộc thi điểm số:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

Vị trí có điểm cao nhất THẮNG. Và nếu ta biết tiêu chí chấm điểm, ta có thể làm cho vị trí mình muốn thắng. Như gian lận bầu cử vậy.


Sở thích của mob (hay "tại sao bò của bạn băng qua đường")

Mỗi mob có gu khác nhau. Và điều đó thay đổi mọi thứ.

Mob Thích
Động vật (bò, cừu, heo) Cỏ và ánh sáng (hipster)
Quái vật (zombie, skeleton) Bóng tối (edgelord)
Rùa Nước, nếu không thì cát, nếu không thì ánh sáng
Hoglin crimson_nylium; ghét warped_fungus
Strider Chỉ lava. KHÔNG gì khác.
Silverfish Khối có thể infest (hợp lý)
Guardian Nước + ánh sáng (kẻ hợm)
Mooshroom Mycelium + ánh sáng (nấm)
Ong Không khí. Ừ, chúng thích KHÔNG KHÍ.
// Animal: nhìn xuống, nếu là cỏ, điểm tối đa
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// Monster: chính xác thì ngược lại
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

Kiểu quái vật là "nếu chỗ sáng, điểm âm, tao đi chỗ khác". Chúng MẶT NẶC với ánh sáng xD

Vậy bạn có thể -- theo nghĩa đen -- dẫn động vật bằng cỏ và ánh sáng, và quái vật bằng bóng tối. Vừa ngu vừa tuyệt.


Thuật toán A* trong Minecraft (công thức bí mật)

Minecraft sử dụng thuật toán A* (A-star) cho pathfinding. Nhưng Mojang đã thêm dấu ấn riêng:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = đường đã đi (1 mỗi khối, ~1.41 đường chéo)
  • h(n) = khoảng cách đường chim bay
  • 1.5 = vì Mojang thích mấy thứ hơi lỗi

Bình thường A* dùng f(n) = g(n) + h(n). MOJANG ĐÃ THÊM HỆ SỐ 1.5. Tại sao? Để thuật toán đi nhanh hơn đến đích và cắt bớt nhánh tìm kiếm. Kết quả: đường tìm được là "tốt" nhưng không phải lúc nào cũng tốt nhất. Đây là A* hơi say.

mermaid diagram

Chi tiết quan trọng: một mob chỉ có thể pathfinding trong 16 khối (tầm follow range của nó). Nếu đích quá xa, nó chọn khối gần nhất mà nó CÓ THỂ tới. Điều này có nghĩa bạn có thể tạo một monolithe ngoài tầm với, và mob sẽ pathfinding đến khối gần nhất đưa nó đến gần monolithe đó -- khiến chuyển động của nó hoàn toàn có thể dự đoán.

Hai exploit phá game

1. Block updates = buộc tính toán lại

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

Mỗi lần cập nhật khối gần đường đi của mob buộc tính toán lại A* với cooldown 1 giây. Bạn đặt một đồng hồ 1 giây cạnh mob, và nó tính toán lại LIÊN TỤC. Như kiểu GPS bị reset mỗi giây.

Và nếu bạn làm vậy với 50 mob cùng lúc? Lag city. RIP TPS.

2. Điểm phạt khối (Pathfinding Malice)

Một số khối làm mob sợ. Theo nghĩa đen. Mỗi khối có một chi phí kèm theo, được định nghĩa bằng enum:

Khối / Điều kiện Phạt
Khối mật ong +8 khi đi qua
Bột tuyết Không thể đi qua
Cửa đóng Không thể đi qua
Lửa +16 khi đi qua, +8 khi đi cạnh
Động vật & Dân làng Lửa = -1 (NOPE)
Cactus / Sweet berry Không thể đi qua; kế bên = +8
Nước +8 khi đi qua hoặc đi cạnh
Magma +8 khi đi cạnh (đau)

Động vật còn cực đoan hơn:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE ở -1.0F, có nghĩa là "cấm" theo nghĩa đen. Một con vật thà nhảy xuống vực còn hơn đi qua lửa. Phù.

Bài tập: cuộc thi đường đi lớn

Hãy tưởng tượng một dân làng phải chọn giữa nhiều con đường.

  • Đường A: 15 khối nhưng 6 khối đi cạnh nước (+8 mỗi cái)
  • Đường B: 18 khối với 2 khối nước (+8) và 1 khối kế nước (+8)
  • Đường C: 14 khối thẳng tắp... nhưng có lửa -> KHÔNG THỂ ĐI đối với dân làng
  • Đường D: 16 khối với 1 khối kế magma (+8) + 1 khối kế mật ong (+8)
  • Đường E: 25 khối nhưng toàn cactus (+8 khắp nơi) -> 90.82 tổng chi phí LOL

Tính nhẩm:

  • Đường A: 15 khối + 6×8 cho nước = 15 + 48 = 63 ... nhưng còn thêm 1.5×khoảng cách. Tính thực tế nào.
  • Đường B: dài hơn nhưng ít phạt hơn. Tổng cost = khoảng cách tích lũy + phạt.
  • Đường D: magma và mật ong stack phạt.

Người thắng thường là Đường B: đường vòng có lợi vì nước ĐẮT.

Một dân làng thực chất là một máy tính chi phí có chân xD

Mỗi mob có gu riêng

Một dân làng: "lửa ư? KHÔNG CẢM ƠN BAI" Một zombie: "lửa ư? OK boomer đi xuyên qua rực lửa"

Bạn có đúng nghĩa đen những con đường mà mob này đi nhưng mob kia không. Bạn có thể làm đường cao tốc cho dân làng mà zombie bị cháy.


Dân làng: cái đống hỗn độn tối thượng

Ok, dân làng. Đây là thứ ÍT được hiểu nhất trong toàn bộ Minecraft. Nhưng một khi đã nắm được code, bạn nhận ra chúng là những cỗ máy có thể dự đoán với lịch làm việc văn phòng.

Cảm biến và bộ nhớ

9 cảm biến, chạy mỗi 20 tick (1 giây). Mỗi cái quét một bán kính quanh dân làng và lưu kết quả vào bộ nhớ. Dân làng thấy tất cả, nhớ tất cả, và hành động dựa trên đó.

Kiểu: "có kẻ thù không? có item trên đất không? có người chơi để nói chuyện không?" -- nó check MỌI THỨ.

Các package (giai đoạn trong ngày)

Não dân làng là các gói hoạt động kích hoạt theo giờ:

Package Giờ Dân làng...
Core 24/7 Mở cửa, bơi (80% thời gian), và THU NHẬN POI
Work 8h-15h "Tôi đi làm" -- đi đến bàn làm việc
Meet 15h-17h "Giờ gặp mặt!" -- đi đến chuông, tán gẫu
Rest 18h-6h "Phải ngủ" -- đi lên giường
Idle 6h-8h, 17h-18h "Tôi lười" -- đi dạo, đẻ con, nhảy lên giường
Panic Bị thương/thù địch "CỨU" -- CHẠY TRỐN

Package Panic là cái duy nhất có thể ngắt TẤT CẢ cái khác. Kể cả dân làng đang ngủ hay làm việc, nếu có zombie, HOẢNG LOẠN TOÀN DIỆN.

Acquire POI: thứ cho phép redstone không dây

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI quét trong bán kính 48 khối tất cả POI (điểm ưa thích). Nó giữ 5 cái gần nhất, kiểm tra đường có tồn tại không, và thu nhận cái đầu tiên có thể tới được.

Mỗi POI có số slot giới hạn:

  • Bàn làm việc: 1 slot
  • Giường: 1 slot
  • Chuông: 32 slot

Điều ĐIÊN RỒ: slot được giữ tại thời điểm thu nhận, KHÔNG PHẢI lúc tới nơi. Một dân làng có thể khóa một composter từ đầu bên kia bản đồ, mà không bao giờ tới được nó.

Bạn thấy sức mạnh chưa?

Redstone không dây. Đúng, KHÔNG DÂY.

  1. Bạn đặt dân làng vào minecart với đường dẫn tới composter
  2. Nó thu nhận composter (slot bị chiếm, không ai dùng được nữa)
  3. Dân làng ở quá xa không thể click -- bone meal vẫn còn
  4. Bạn DẮT dân làng này đi bất kỳ đâu trong thế giới, nó giữ slot
  5. Khi bạn muốn kích hoạt thứ của mình, bạn G I Ế T dân làng
  6. Slot được giải phóng, dân làng khác thu nhận composter, lấy bone meal
  7. BLOCK UPDATE -> bất kỳ mạch redstone nào cũng được kích hoạt

Bạn vừa tạo ra tín hiệu redstone không dây, có thể truyền khắp thế giới, với zero chunk load cần thiết trên đường đi. Bạn có thể kết nối nó với ender pearl stasis chamber, tự dịch chuyển từ bất kỳ đâu bằng cách giết một dân làng.

Cách dùng yêu thích của tôi? Một mini-game "bounty hunter": bạn đặt nhiều dân làng với composter, người chơi phải giết ĐÚNG dân làng để kích hoạt lối ra. Cơ chế wtf hoàn toàn xD

Pathfinding Deadlock (hay "dân làng đóng băng vĩnh viễn")

Có một bug QUÁ ngon giữa Acquire POI (thấy đường) và navigation thực tế (từ chối đi đường đó). Xảy ra khi khối phía trên bàn làm việc không thể đi được. Kết quả:

  • Core package: "tao muốn thu nhận POI"
  • Navigation: "tao không thể đi ở đó"
  • Kết quả: dân làng ĐỨNG YÊN, mãi mãi, tự đấu tranh với chính mình.

Dân làng đóng băng tại chỗ, có thể dùng làm trang trí hoặc "đạo cụ" trong build. Một cái tủ đựng giáp? Ừ. Một lính canh không động đậy? Ừ. Rùng rợn? Có thể. Nhưng hiệu quả xD


Kết luận

Pathfinding của mob Minecraft không phải ngẫu nhiên. Đó là một hệ thống tất định, dựa trên điểm số, có thể dự đoán VÀ phá vỡ được.

Ba điều cần nhớ:

  1. Khối dưới chân = thiên vị độ cao -- lấp đầy hoặc làm rỗng tầng hầm để dẫn mob
  2. Điểm phạt khác nhau cho mỗi mob -- tạo đường mà mob này đi nhưng mob kia không
  3. Slot POI được giữ từ xa -- redstone không dây miễn phí, dịch chuyển, đủ thứ

Mã nguồn Minecraft là một mỏ vàng các cơ chế bị khai thác ít. Tôi đã dành hàng giờ đọc Java đã dịch ngược và thành thật? Mỗi dòng code là một Easter Egg có chức năng. Chỉ khác là mấy cái này, bạn xài được trong survival để làm redstone không dây với dân làng. Game hay nhất confirmed.

xD

ตรรกะการหาเส้นทางของ Minecraft และการประยุกต์ใช้

อัลกอริทึม A*, ค่าปรับของบล็อก และ POI ช่วยให้คุณควบคุม, ทำนาย และใช้ประโยชน์

บทนำ

ผมใช้เวลาหลายชั่วโมงดูแกะเดินชนกำแพง

และพูดตรงๆเหรอ? การลงทุนที่ดีที่สุดในชีวิตเลย xD

เพราะยิ่งคุณดูม็อบพวกนี้มากเท่าไหร่ คุณก็ยิ่งรู้ว่าพวกมันไม่ได้สุ่มเลย ทุกการเคลื่อนไหวถูกเขียนโค้ดไว้ คาดเดาได้ และที่สำคัญ -- แก้ทางได้หมด ผมเลยดำดิ่งเข้าไปในซอร์สโค้ดของ Minecraft เพื่อทำความเข้าใจว่าการหาเส้นทางทำงานยังไง และสิ่งที่ค้นพบคือคุณสามารถควบคุมจิตใจม็อบได้จริงๆ แบบ บังคับให้มันไปที่ที่คุณอยากให้ไป ไม่ใช่ที่ที่ความสุ่มเลือก

ไกด์นี้คือทุกสิ่งที่ผมเรียนรู้จากการขุดคุ้ย ระบบ AI, อัลกอริทึม A*, ค่าปรับที่ซ่อนอยู่, เอ็กซ์พลอยต์ที่คุณใช้ในเซิร์ฟวิฟอลได้ เตรียมเสียมให้พร้อม


ระบบ AI ของม็อบทำงานยังไง (สปอยเลอร์: มันโคตรกาก)

Goals (เป้าหมาย)

ม็อบทุกตัวมี goals มันคือลิสต์สิ่งที่มันสามารถทำได้และอยากจะทำมากแค่ไหน ยิ่งเลขน้อยยิ่งสำคัญ -- เหมือนทูดู list เวอร์ชันโกลาหล

protected void registerGoals() {
   this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0, 3));
   this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
   this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
   this.addBehaviourGoals();
}

เคยเห็นซอมบี้ไม่สนไข่เต่าเพื่อวิ่งไล่คุณไหม? นี่คือสาเหตุ: ZombieAttackTurtleEggGoal มีลำดับความสำคัญ 4, ขณะที่ ZombieAttackGoal (สิ่งที่บอกให้มันกัดหัวคุณ) มีลำดับความสำคัญ 2

ใช่, ซอมบี้ชอบกินคุณมากกว่าไข่เต่า น่ารักชะมัด xD

Goal ที่เราสนใจจริงๆ คือ WaterAvoidingRandomStrollGoal, ลำดับความสำคัญ 7 goal "ฉันไม่มีอะไรทำเลยเดินสุ่มๆ" นี่คือจุดเริ่มต้นของความวุ่นวาย

การเคลื่อนที่ (หรือ "random walk มีโอกาส 1 ใน 60 ที่จะเกิดขึ้น")

ทุก ๆ ticks (ทุก 0.05 วินาที), เกมจะเรียก canUse() เพื่อดูว่าม็อบยอมขยับไหม โอกาส 1 ใน 60 ในแต่ละ tick ออกแบบมาได้กากมาก และผมชอบนะ

public boolean canUse() {
   if (this.mob.hasControllingPassenger()) {
      return false;
   } else {
      if (!this.forceTrigger) {
         if (this.checkNoActionTime && this.mob.getNoActionTime() >= 100) {
            return false;
         }
         if (this.mob.getRandom().nextInt(reducedTickDelay(this.interval)) != 0) {
            return false;
         }
      }
      Vec3 $$0 = this.getPosition();
      if ($$0 == null) {
         return false;
      } else {
         this.wantedX = $$0.x;
         this.wantedY = $$0.y;
         this.wantedZ = $$0.z;
         this.forceTrigger = false;
         return true;
      }
   }
}

สรุปคือ: ถ้าคุณขี่ม็อบอยู่ -> ไม่, ถ้าม็อบไม่ได้ทำอะไรเกิน 5 วินาที -> ไม่, ถ้าสุ่มได้ไม่ -> ไม่ เกมไม่อยากให้ม็อบขยับจริงๆ

แต่เมื่อมันขยับ getPosition() จะทำงานต่อ:

protected Vec3 getPosition() {
   if (this.mob.isInWater()) {
      Vec3 $$0 = LandRandomPos.getPos(this.mob, 15, 7);
      return $$0 == null ? super.getPosition() : $$0;
   } else {
      return this.mob.getRandom().nextFloat() >= this.probability
         ? LandRandomPos.getPos(this.mob, 10, 7)
         : super.getPosition();
   }
}

ดูตัวเลขสองตัวท้ายสิ: มันคือรัศมี XZ และรัศมี Y ในน้ำ ม็อบจะมองหาไกลขึ้น (15 แทน 10) ถ้าหาที่ดินไม่เจอมันจะใช้ super.getPosition() ที่ยอมให้น้ำได้ ผลลัพธ์: ม็อบอยากขึ้นจากน้ำ นี่คือสาเหตุที่สัตว์เลี้ยงของคุณว่ายน้ำอย่างบ้าคลั่งเข้าหาฝั่ง

รายละเอียดเล็กๆ น้อยๆ: มีโอกาสแค่ 0.1% ที่ม็อบจะใช้ super.getPosition() แทน LandRandomPos หนึ่งในพัน โมแจงอะไรอย่างงี้ xD

LandRandomPos: optimization ห่วยๆ ที่เปลี่ยนทุกอย่าง

นี่คือขั้นตอนที่ผมชอบที่สุด โค้ดกากๆ ที่ทำให้ pathfinding ใช้ประโยชน์ได้

public static Vec3 getPos(PathfinderMob $$0, int $$1, int $$2, ToDoubleFunction<BlockPos> $$3) {
   boolean $$4 = GoalUtils.mobRestricted($$0, $$1);
   return RandomPos.generateRandomPos(() -> {
      BlockPos $$4xx = RandomPos.generateRandomDirection($$0.getRandom(), $$1, $$2);
      BlockPos $$5 = generateRandomPosTowardDirection($$0, $$1, $$4, $$4xx);
      return $$5 == null ? null : movePosUpOutOfSolid($$0, $$5);
   }, $$3);
}

movePosUpOutOfSolid. ชื่อบอกอยู่แล้ว ถ้าตำแหน่งที่เลือกอยู่ในบล็อกทึบ เกมจะดันมันขึ้นไปจนกว่าจะอยู่ในอากาศ

มันคือ optimization: แทนที่จะเสียเวลาข้ามตำแหน่งที่อยู่ใต้ดิน เกมจะดันมันขึ้นมาผิวดิน ฉลาดไหม? ใช่ แต่มันสร้างความลำเอียงอย่างมหาศาล: ม็อบชอบที่สูง

ลองนึกดู คุณมีบล็อกมากมายใต้ผิวดิน เกมสุ่ม 10 ตำแหน่ง ตำแหน่งที่อยู่ในบล็อกจะถูกดันขึ้น พื้นที่หนาแน่น (ใต้เนินเขา) ให้ตำแหน่งที่ใช้ได้มากกว่าพื้นที่โปร่ง ผลลัพธ์: ม็อบจะไปทางเนินเขามากกว่าในเชิงสถิติ

เชื่อผมสิ เราจะใช้ประโยชน์จากสิ่งนี้ใน 2 นาที

การคัดเลือก: การประกวดบล็อกที่ดีที่สุด

10 ตำแหน่ง, ผู้ชนะคนเดียว, การประกวดคะแนน:

public static Vec3 generateRandomPos(Supplier<BlockPos> $$0, ToDoubleFunction<BlockPos> $$1) {
   double $$2 = Double.NEGATIVE_INFINITY;
   BlockPos $$3 = null;
   for(int $$4 = 0; $$4 < 10; ++$$4) {
      BlockPos $$5 = (BlockPos)$$0.get();
      if ($$5 != null) {
         double $$6 = $$1.applyAsDouble($$5);
         if ($$6 > $$2) {
            $$2 = $$6;
            $$3 = $$5;
         }
      }
   }
   return $$3 != null ? Vec3.atBottomCenterOf($$3) : null;
}

ตำแหน่งที่มีคะแนนดีที่สุด ชนะ และถ้าเรารู้เกณฑ์การให้คะแนน เราก็สามารถทำให้ตำแหน่งที่เราอยากให้ชนะชนะได้ มันเหมือนโกงการเลือกตั้ง


ความชอบของม็อบ (หรือ "ทำไมวัวของคุณถึงข้ามถนน")

ม็อบแต่ละตัวมีความชอบต่างกัน และนั่นเปลี่ยนทุกอย่าง

ม็อบ ชอบอันนี้
สัตว์ (วัว, แกะ, หมู) หญ้าและแสง (hipsters)
มอนสเตอร์ (ซอมบี้, โครงกระดูก) ความมืด (edgelords)
เต่า น้ำ, ถ้าไม่ได้ก็ทราย, ถ้าไม่ได้ก็แสง
Hoglins crimson_nylium; เกลียด warped_fungus
Striders เฉพาะลาวาเท่านั้น ไม่มีอะไรอื่น
Silverfish บล็อกที่ infestable (ตามเหตุผล)
Guardians น้ำ + แสง (พวกขี้งก)
Mooshrooms Mycelium + แสง (เห็ด)
ผึ้ง อากาศ ใช่ พวกมันชอบอากาศ
// สัตว์: มองลงไป ถ้าเป็นหญ้า ได้คะแนนเต็ม
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return $$1.getBlockState($$0.below()).is(Blocks.GRASS_BLOCK) ? 10.0F : $$1.getPathfindingCostFromLightLevels($$0);
}

// มอนสเตอร์: ตรงกันข้ามเป๊ะ
public float getWalkTargetValue(BlockPos $$0, LevelReader $$1) {
   return -$$1.getPathfindingCostFromLightLevels($$0);
}

มอนสเตอร์คือแบบ "ถ้ามีแสง คะแนนติดลบ ฉันไปที่อื่น" พวกมันไม่เอาแสงเลย xD

ดังนั้นคุณสามารถ -- ตามตัวอักษร -- นำทางสัตว์ด้วยหญ้าและแสง และนำทางมอนสเตอร์ด้วยความมืด มันทั้งกากและเจ๋งในเวลาเดียวกัน


อัลกอริทึม A* ใน Minecraft (สูตรลับ)

Minecraft ใช้อัลกอริทึม A* (A-star) สำหรับการหาเส้นทาง แต่โมแจงเติมลูกเล่นของตัวเอง:

f(n) = g(n) + 1.5 × h(n)
  • g(n) = เส้นทางที่เดินมาแล้ว (1 ต่อบล็อก, ~1.41 ในแนวทแยง)
  • h(n) = ระยะทางเป็นเส้นตรง
  • 1.5 = เพราะโมแจงชอบของที่กากๆ

ปกติ A* ใช้ f(n) = g(n) + h(n) โมแจงเติมตัวคูณ 1.5 ทำไม? เพื่อให้อัลกอริทึมไปถึงเป้าหมายเร็วขึ้นและตัดกิ่งการค้นหาน้อยลง ผลลัพธ์: เส้นทางที่ได้คือ "ดี" แต่ไม่ใช่ดีที่สุดเสมอไป มันคือ A* ที่เมาๆ

mermaid diagram

รายละเอียดสำคัญ: ม็อบสามารถหาเส้นทางได้แค่ 16 บล็อก (ค่าช่วงติดตามหรือ follow range) ถ้าเป้าหมายไกลเกินไป มันจะเลือกบล็อกที่ใกล้ที่สุดที่มันเข้าถึงได้ นั่นหมายความว่าคุณสามารถสร้างเสาหินที่ไกลเกินเอื้อม และม็อบจะหาเส้นทางไปยังบล็อกที่ใกล้ที่สุดที่ทำให้มันเข้าใกล้เสาหินนั้น -- ทำให้การเคลื่อนที่ของมันคาดเดาได้อย่างสมบูรณ์

สองเอ็กซ์พลอยต์ที่พังเกม

1. การอัปเดตบล็อก = บังคับคำนวณใหม่

public boolean shouldRecomputePath(BlockPos $$0) {
   if (this.hasDelayedRecomputation) return false;
   if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) {
      Node $$1 = this.path.getEndNode();
      Vec3 $$2 = new Vec3(
         ((double)$$1.x + this.mob.getX()) / 2.0,
         ((double)$$1.y + this.mob.getY()) / 2.0,
         ((double)$$1.z + this.mob.getZ()) / 2.0
      );
      return $$0.closerToCenterThan($$2, (double)(this.path.getNodeCount() - this.path.getNextNodeIndex()));
   }
   return false;
}

ทุกครั้งที่มีการอัปเดตบล็อกใกล้เส้นทางของม็อบ จะบังคับให้คำนวณ A* ใหม่ โดยมีคูลดาวน์ 1 วินาที ถ้าคุณวางนาฬิกา 1 วินาทีไว้ใกล้ม็อบ มันจะคำนวณเส้นทางใหม่ตลอดเวลา เทียบได้กับการรีเซ็ต GPS ทุกวินาที

และถ้าคุณทำแบบนี้กับ 50 ม็อบพร้อมกัน? Lag city. RIP TPS

2. ค่าปรับของบล็อก (Pathfinding Malice)

บางบล็อกทำให้ม็อบกลัว ตามตัวอักษร แต่ละบล็อกมีค่าใช้จ่ายที่กำหนดโดย enum:

บล็อก / เงื่อนไข ค่าปรับ
บล็อกน้ำผึ้ง +8 ในการเดินผ่าน
ผงหิมะ เดินผ่านไม่ได้
ประตูปิด เดินผ่านไม่ได้
ไฟ +16 ในการเดินผ่าน, +8 ในการเดินเลียบ
สัตว์ & ชาวบ้าน ไฟ = -1 (NOPE)
กระบองเพชร / Sweet berry เดินผ่านไม่ได้; ติดกัน = +8
น้ำ +8 ในการเดินผ่านหรือเลียบ
แมกมา +8 ในการเดินเลียบ (เจ็บ)

สัตว์ยิ่งสุดขั้วไปอีก:

protected Animal(EntityType<? extends Animal> $$0, Level $$1) {
   super($$0, $$1);
   this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F);
   this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F);
}

DAMAGE_FIRE ที่ -1.0F, มันคือ "ห้ามเด็ดขาด" สัตว์ยอมกระโดดลงเหวดีกว่าผ่านไฟ ว้าว

แบบฝึกหัด: การประกวดเส้นทาง

ลองนึกภาพชาวบ้านที่ต้องเลือกระหว่างหลายเส้นทาง

  • เส้นทาง A: 15 บล็อก แต่ 6 บล็อกเลียบน้ำ (+8 แต่ละอัน)
  • เส้นทาง B: 18 บล็อก มีน้ำ 2 บล็อก (+8) และติดน้ำ 1 บล็อก (+8)
  • เส้นทาง C: 14 บล็อกตรง... แต่มีไฟ -> ผ่านไม่ได้สำหรับชาวบ้าน
  • เส้นทาง D: 16 บล็อก ติดแมกมา 1 บล็อก (+8) + ติดน้ำผึ้ง 1 บล็อก (+8)
  • เส้นทาง E: 25 บล็อก แต่กระบองเพชรเต็มไปหมด (+8 ทุกอัน) -> 90.82 ต้นทุนรวม LOL

คิดเลขในใจ:

  • เส้นทาง A: 15 บล็อก + 6×8 ค่าน้ำ = 15 + 48 = 63 ... แต่ต้องบวก 1.5×ระยะทางด้วย มาคำนวณจริงกัน
  • เส้นทาง B: ยาวกว่าแต่ค่าปรับน้อยกว่า ต้นทุนรวม = ระยะทางสะสม + ค่าปรับ
  • เส้นทาง D: แมกมาและน้ำผึ้งรวมค่าปรับกัน

ผู้ชนะส่วนใหญ่คือ เส้นทาง B: การอ้อมคุ้มค่าเพราะน้ำแพง

ชาวบ้านก็คือเครื่องคำนวณต้นทุนที่มีขา xD

ม็อบแต่ละตัวมีความชอบของตัวเอง

ชาวบ้าน: "ไฟเหรอ? ไม่เอา THANK YOU BYE" ซอมบี้: "ไฟเหรอ? OK boomer เดินลุยไฟทั้งตัว"

คุณมีถนนที่ม็อบบางตัวเดินได้และบางตัวเดินไม่ได้ คุณสามารถสร้างทางด่วนสำหรับชาวบ้านที่ซอมบี้จะไหม้ตาย


ชาวบ้าน: ความยุ่งเหยิงสูงสุด

โอเค ชาวบ้าน นี่คือสิ่งที่คนส่วนใหญ่เข้าใจผิดมากที่สุดใน Minecraft แต่เมื่อคุณเข้าใจโค้ดแล้ว คุณจะรู้ว่าพวกมันคือเครื่องจักรที่คาดเดาได้พร้อมตารางงานออฟฟิศ

เซ็นเซอร์และความจำ

9 เซ็นเซอร์, ทำงานทุก 20 ticks (1 วินาที) แต่ละตัวสแกนรัศมีรอบชาวบ้านและเก็บผลลัพธ์ในความจำ ชาวบ้านเห็นทุกอย่าง จำทุกอย่าง และทำตามนั้น

แบบ: "มีศัตรูไหม? ของตกพื้นไหม? ผู้เล่นที่จะคุยด้วยไหม?" -- มันเช็คทุกอย่าง

Packages (ช่วงเวลาของวัน)

สมองของชาวบ้านคือ packages ของกิจกรรมที่เปิดใช้งานตามเวลา:

Package เวลา ชาวบ้าน...
Core ตลอด 24 ชม. เปิดประตู, ว่ายน้ำ (80% ของเวลา), และหา POI
Work 8 โมง - บ่าย 3 "ฉันจะไปทำงาน" -- เดินไปที่ทำงาน
Meet บ่าย 3 - บ่าย 5 "แฮปปี้เอาเวอร์!" -- ไปที่ระฆัง, คุยเล่น
Rest 6 โมงเย็น - ตี 6 "ต้องนอน" -- ไปที่นอน
Idle ตี 6 - 8 โมง, บ่าย 5 - 6 โมงเย็น "ฉันขี้เกียจ" -- เดินเล่น, ทำเด็ก, กระโดดบนเตียง
Panic บาดเจ็บ/มีศัตรู "ช่วยด้วย" -- หนี

Package Panic เป็นตัวเดียวที่สามารถขัดจังหวะทุกตัวอื่น แม้ชาวบ้านกำลังนอนหรือทำงาน ถ้ามีซอมบี้ ตื่นตระหนกถ้วนหน้า

Acquire POI: สิ่งที่ทำให้มีเรดสโตนไร้สาย

Set<Pair<Holder<PoiType>, BlockPos>> $$12 = (Set)$$10xx.findAllClosestFirstWithType(
   $$0, $$11, $$8x.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
)

Acquire POI สแกนรัศมี 48 บล็อกเพื่อหา POI (จุดสนใจ) ทั้งหมด มันเก็บ 5 อันที่ใกล้ที่สุด ตรวจสอบว่ามีเส้นทางไปถึง และจับจองอันแรกที่เข้าถึงได้

POI แต่ละอันมีจำนวนสล็อตจำกัด:

  • ที่ทำงาน: 1 สล็อต
  • เตียง: 1 สล็อต
  • ระฆัง: 32 สล็อต

สิ่งที่บ้ามาก: สล็อตถูกจองตอนที่จับจอง ไม่ใช่ตอนที่ไปถึง ชาวบ้านสามารถล็อก composter จากอีกฟากของแผนที่ โดยไม่ต้องเดินไปถึง

คุณเห็นพลังแล้วใช่ไหม?

เรดสโตนไร้สาย ใช่ ไร้สาย

  1. วางชาวบ้านใน minecart พร้อมเส้นทางไปยัง composter
  2. มันจับจอง composter (สล็อตถูกใช้, คนอื่นใช้ไม่ได้)
  3. ชาวบ้านอยู่ไกลเกินกว่าจะคลิก -- bone meal ยังอยู่
  4. คุณพาชาวบ้านคนนี้ไปไหนก็ได้ในโลก มันยังถือสล็อตอยู่
  5. เมื่อคุณอยากเปิดเครื่องของคุณ คุณ ฆ่า ชาวบ้าน
  6. สล็อตถูกปล่อย, ชาวบ้านอีกคนจับจอง composter, นำ bone meal ออก
  7. BLOCK UPDATE -> วงจรเรดสโตนใดๆ ถูกกระตุ้น

คุณสร้างสัญญาณเรดสโตนไร้สาย, ส่งได้ทั่วโลก, โดยไม่ต้องโหลด chunk ระหว่างทางเลย คุณสามารถต่อมันกับ ender pearl stasis chamber, เทเลพอร์ตมาจากไหนก็ได้โดยการฆ่าชาวบ้าน

ที่ผมชอบใช้? มินิเกม "bounty hunter": วางชาวบ้านหลายตัวพร้อม composters, ผู้เล่นต้องฆ่าชาวบ้านที่ถูกต้องเพื่อเปิดทางออก มันบ้ามาก xD

Pathfinding Deadlock (หรือ "ชาวบ้านที่ค้างตลอดกาล")

มีบั๊กที่โคตรดีระหว่าง Acquire POI (ที่เห็นเส้นทาง) กับการนำทางจริง (ที่ไม่ยอมใช้) มันเกิดขึ้นเมื่อบล็อกเหนือที่ทำงานไม่สามารถเดินได้ ผลลัพธ์:

  • Core package: "ฉันอยากจับจอง POI"
  • การนำทาง: "ฉันเดินไปตรงนั้นไม่ได้"
  • ผลลัพธ์: ชาวบ้านค้างอยู่กับที่ ตลอดกาล สู้กับตัวเอง

ชาวบ้านแช่แข็งอยู่กับที่ ใช้เป็นของตกแต่งหรือ "อุปกรณ์ประกอบฉาก" ในบิ้วด์ ถังใส่ชุดเกราะ? ได้ ยามที่ไม่ขยับ? ได้ โหดร้ายไหม? อาจจะ แต่เวิร์คนะ xD


บทสรุป

การหาเส้นทางของม็อบใน Minecraft ไม่ใช่เรื่องสุ่ม มันคือระบบที่กำหนดได้, ใช้คะแนน, คาดเดาได้ และพังได้

สามสิ่งที่ต้องจำ:

  1. บล็อกใต้เท้า = ความลำเอียงด้านความสูง -- เติมหรือทำให้ชั้นใต้ดินว่างเพื่อนำทางม็อบ
  2. ค่าปรับแตกต่างกันในแต่ละม็อบ -- สร้างถนนที่บางตัวใช้และบางตัวใช้ไม่ได้
  3. สล็อต POI ถูกจองจากระยะไกล -- เรดสโตนไร้สายฟรี, เทเลพอร์ต, และอื่นๆ

ซอร์สโค้ดของ Minecraft เป็นเหมืองทองของกลไกที่ถูกใช้น้อยเกินไป ผมใช้เวลาหลายชั่วโมงอ่าน Java ที่ดีคอมไพล์แล้ว และพูดตรงๆเหรอ? ทุกบรรทัดคือ Easter Egg ที่ใช้งานได้ เว้นแต่อันนี้ คุณใช้มันในเซิร์ฟวิฟอลเพื่อทำเรดสโตนไร้สายกับชาวบ้าน เยี่ยมที่สุดยืนยัน

xD

Related Articles