Tuesday, August 7, 2012

DIY fences

Behold! A fence!


Now, fences aren't all that exciting. Minecraft's had them for almost two years now. No, the cool bit is how they were scripted.

The 3-D model

Unlike items and mobs, custom block models cannot be imported from just any Blender or Collada file. To keep things manageable, they are essentially a 16x16x16 chunk scaled down to the size of a block. You define them as a list of boxes, where all coordinates are integers, limited from 0 to 16. Every face of every box can be textured individually, the same way a normal block can.

For example, here's a snippet of the Lua script that defines the materials:

define_material({ name = "fence.0000",
            on_place = place_fence, on_remove = remove_fence,
            custom_model = { { 7, 7, 0,   9, 9, 15,   "fence" } } })

define_material({ name = "fence.000E",
            on_remove = remove_fence,
            custom_model = { { 7, 7, 0,    9, 9, 15,  "fence" }, 
                             { 10, 8, 10,  15, 8, 12, "bark" } } })

A fence can connect to its neighbors in any combination of the four cardinal directions, so we need 16 different models in total. You're free to pick any name you like, but I've used a naming system here that shows in which directions the fence extends. "Fence.0000" is just a post, a single box with the 'fence' texture. "Fence.000E" extends to the east, and consists of the same post, and another box with the much darker 'bark' texture. We also need "Fence.00W0", "Fence.00WE", "Fence.0N00", and so on.

(Ordering the models by binary counting will come in handy later.)

Examples of fence models

All this info is sent to the client on login. So if you join a server, there's no need to install anything, you automatically get to see whatever cool custom blocks they designed for their map.

This is nice for static decoration, but if you want to give the player the opportunity to build his or her own fences, there's a little more scripting involved.

(Note: the idea for custom block models isn't quite new, Minetest recently added "nodeboxes" that can do the same thing.)

Scripting the behavior

Every fence material gets a number. We'll say 'fence.0000' is material number 100, and the others are numbered incrementally. If the player places a fence, the value in the chunk's array gets changed from 0 (air) to 100, and a fence post appears. So far so good, but the second fence placed right next to it also looks like a post. When this happens, we want to connect the two. So the first one should change from 100 to 104 ('fence.0N00'), and the other to 108 ('fence.S000').

You might have noticed we had already provided an 'on_place' and 'on_remove' callback for the fence materials. The first one, place_fence, looks like this:


local function is_fence(a, b)
  return b >= a and b < (a + 16)
end

local function place_fence(p, id)
  local e = p + vec(1, 0, 0)
  local n = p + vec(0, 1, 0)
  local w = p + vec(-1, 0, 0)
  local s = p + vec(0, -1, 0)
  
  if (is_fence(id, get_block(e))) then
    id = id + 1
    change_block(e, get_block(e) + 2)
  end
  if (is_fence(id, get_block(w))) then
    id = id + 2
    change_block(w, get_block(w) + 1)
  end    
  if (is_fence(id, get_block(n))) then
    id = id + 4
    change_block(n, get_block(n) + 8)
  end  
  if (is_fence(id, get_block(s))) then
    id = id + 8
    change_block(s, get_block(s) + 4)    
  end
  
  change_block(p, id)
end

'place_fence' only gets called for "fence.0000". The parameters are the position, and the material ID (in this case, 100). We use this ID to check the four neighbors (e, w, s, and n), to see if they are fences as well. This check is rather simple; we've registered 16 materials, so they should be somewhere in the range 100 to 116.

For every fence we find, we adjust the model of that neighbor block so it connects to the one we're about to place. And of course we adjust 'id' as well, we need to connect them both ways.

Removing a fence is very similar, but instead of adding a given value for each direction, we subtract it:

local function remove_fence(p, id)
  local e = p + vec(1, 0, 0)
  local n = p + vec(0, 1, 0)
  local w = p + vec(-1, 0, 0)
  local s = p + vec(0, -1, 0)
  
  if (is_fence(id, get_block(e))) then
    change_block(e, get_block(e) - 2)
  end
  if (is_fence(id, get_block(w))) then
    change_block(w, get_block(w) - 1)
  end    
  if (is_fence(id, get_block(n))) then
    change_block(n, get_block(n) - 8)
  end  
  if (is_fence(id, get_block(s))) then
    change_block(s, get_block(s) - 4)    
  end
  
  change_block(p, 0)  
end

No need to adjust 'id', at the end we're going to place an air block there anyway.


More to come

Of course, you can build much more with this API than just fences. Doors, stairs, window shutters, pipes, ladders, wooden beams, rails, you name it. How about fences that connect to walls as well? With gates? I'll leave all that to your imagination for now, and in the next few posts I'd like to show other parts of the API, such as the user interface and items.