Jump to content

Minecraft NPC Villages Explained (Or, Why Are My Villagers Clumping Together?)


elantzb

Recommended Posts

Minecraft NPC Villages Explained (Or, Why Are My Villagers Clumping Together?)

 

Disclaimers:

This explanation is current as of April 4, 2013, and pertains to vanilla Minecraft(no mods affecting the files mentioned within.)

The names of all class files mentioned are current MCP mappings, and all code will be pseudo.

This part only covers village expansion, more can be explained if people ask.

 

Introduction

 

So, one day I decided that I wanted to build a massive city, and populate it with NPC villagers. But with my villagers sticking together in one building like rare-earth magnets, and ignoring the gorgeous mansions that were waiting across the street, I knew that something was wrong.

I began perusing forum posts for answers.

 

Most of the results I found mentioned burying villagers alive, locking them up, or using various things to attract them. (Suggesting that they may like to gather around pumpkins, animals, prisoners, etc.)

 

Others who understood more of how the invisible radius of a village works had better ideas, but there was a lot of micro-managing that did not account for some of the ways the code actually works.

 

So, I began reading the Java code for my answers. (Thank you, MCP)

 

The Explanation

 

Everything starts in VillageCollection.java

 

This class keeps track of the existing villages, and the positions of each villager in a list named villagerPositionsList. This list is mostly useless.

 

In the class's tick() function, it iterates through each village that exists, calling their respective tick() functions, which call their Villagers' updateAITick() functions.

 

VillageCollection then calls its function dropOldestVillagerPosition(), which removes the villager position from the top of the list. This is probably the only important use of this list.

 

The oldest villager position is passed to VillageCollection.addUnassignedWoodenDoorsAroundToNewDoorsList()

 

I should mention that each Village object keeps track of the wooden doors that belong to it, a list of VillageDoorInfo objects named villageDoorInfoList.

 

The function VillageCollection.addUnassignedWoodenDoorsAroundToNewDoorsList() checks a list named newDoors and adds any *new* wooden doors to it that are within 16 blocks horizontally and 4 blocks vertically of the oldest villager position.

 

Doors that already exist in a Village's villageDoorInfoList have their internal timer reset. This will be explained later.

 

But, doors are only added to newDoors if they are "valid." addUnassignedWoodenDoorsAroundToNewDoorsList() calls VillageCollection.addDoorToNewListIfAppropriate(), which looks at the 5 blocks on either side of the door's base. It counts the number of "outside" blocks, that being blocks which the sun can shine on during the day.

A door is "valid" if the number of outside blocks on one side *does not equal* the number on the other side.

 

Returning from VillageCollection.dropOldestVillagerPosition(), the next step in tick() is to call VillageCollection.addNewDoorsToVillageOrCreateVillage().

 

If any doors in newDoors are close enough to an existing Village(within the Village's radius plus 32, squared), then that Village gets the door.

 

Otherwise, a new Village is created and that door is given to it to handle.

 

That sums up most of what happens inside of VillageCollection.java

 

//within VillageCollection
function tick
{
      foreach Village, call Village.tick()

      call dropOldestVillagerPosition()
      call addNewDoorsToVillageOrCreateVillage()
}

function dropOldestVillagerPosition
{
      pos = villagerPositionList.pop()
      call addUnassignedWoodenDoorsAroundToNewDoorsList(pos)
}

function addUnassignedWoodenDoorsAroundToNewDoorsList(pos)
{
      foreach door within 16 of pos.x and 16 of pos.z and 4 of pos.y
      {
             if door is in a village then door.timer = 0
             else call addDoorToNewListIfAppropriate(door)
      }
}

function addDoorToNewListIfAppropriate(door)
{
      put 5 "outside" blocks on one side of door into list1
      put 5 "outside" blocks on other side of door into list2

      if list1.length is not equal to list2.length
      call newDoors.add(door)
}

function addNewDoorsToVillageOrCreateVillage
{
      foreach door in newDoors
      {
            find a village where (distance between village center and the door)^2 < (32 + village radius)^2

            if village found call village.addDoorInfo(door)
            else create new village.addDoorInfo(door)
      }
}

 

Now I will explain the relevant code in EntityVillager.java

 

Most of the Villager's actions spawn from the function EntityVillager.updateAITick()

 

It updates its position to VillageCollection's villagerPositionList, and sets its home to the nearest village.

 

If no village is found, the Villager will wander until it finds one.

 

The Villager's wander area is centered on the Village's center, with a radius that is 60% of the Village's radius.

 

//within EntityVillager
constructor EntityVillager
{
      set wander area to home village with radius of village.radius * 0.6
}

function tick()
{
      set home village to closest village
      do villager stuff
}

 

And then onto Village.java

 

Every time a Village's tick() function is called, it takes care of all sorts of code pertaining to population count and iron golems.

 

It also calls Village.removeDeadAndOutOfRangeDoors(), which removes door info from villageDoorInfoList for any door that does not exist, or any door whose internal timer has passed 1200 ticks.

 

The function then calls Village.updateVillageRadiusAndCenter(), which is also called when a new VillageDoorInfo is added to villageDoorInfoList.

 

This method recalculates the Village's center by getting the mean of all the doors' positions. Then, it sets the Village radius to

the distance between the center and the most distant door(unless it is within 32 blocks, 32 is the minimum radius).

 

//within Village.java
function tick
{
      call removeDeadAndOutOfRangeDoors()
}

function removeDeadAndOutOfRangeDoors
{
      foreach door in villageDoorInfoList
      if block at door is not a door OR if door.timer > 12000 then remove door
}

 

That is how the code works.

 

Why I Call This Code "Broken"

 

The code I have examined is meant to let the radius of a Village grow organically, as explained above.

 

But, as I mentioned in the Introduction, this doesn't work.

 

Village radius is based on the location of the furthest door in the Village, and doors are added by chance when a Villager is near them.

 

But because the doors' internal timers expire before being noticed by passing Villagers, the radius shrinks.

 

And Villagers prefer to stay within 60% of the Village's radius to its center, so they rarely see distant doors.

 

I've been lucky enough to have a village of radius 35 (minimum is 32), even with lots of micro-managing.

 

 

Hope I helped someone, please tell me how I can improve this post!

Yes, my ideas can be naive. But I'm hoping that speaking my mind will inspire those who know what they are doing!

Link to comment
Share on other sites

I was hoping that this thread would garner more attention seeing as how long it took me to put together, is there a better place to post/pin this?

Yes, my ideas can be naive. But I'm hoping that speaking my mind will inspire those who know what they are doing!

Link to comment
Share on other sites

  • 2 weeks later...
  • 11 months later...

I've started playing Minecraft recently and I run into this issue.

The first google search sent me straight here but I see that this topic is a year old so I was wondering if someone found a workaround for this.

I found the issue in the issue tracker: https://bugs.mojang.com/browse/MC-78

Will post a link back to here from there.

This is a very good analisys of the problem. Thanks for the explanation.

Link to comment
Share on other sites

I've started playing Minecraft recently and I run into this issue.

The first google search sent me straight here but I see that this topic is a year old so I was wondering if someone found a workaround for this.

I found the issue in the issue tracker: https://bugs.mojang.com/browse/MC-78

Will post a link back to here from there.

This is a very good analisys of the problem. Thanks for the explanation.

 

I'm not sure about the specific fix to the clumping problem, but I think it may be possible to override the vanilla villager AI with your own.  Every EntityLiving has a public list of AI tasks called "tasks".  I haven't tried it, but I suspect you could clear the vanilla EntityVillager AI tasks with a new EntityAITasks (or maybe just clear the taskEntries list within the tasks) and then build up a list of custom AI using the addTask method.

Check out my tutorials here: http://jabelarminecraft.blogspot.com/

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Announcements



×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.