ID:36068
 

A BYONDscape Classic!

Dream Tutor: All Together Now

by Lummox JR

Quite a few games in BYOND use a technique called autojoining to make walls and other terrain look like they have natural edges and corners. Autojoining walls don't look like a single block repeated over and over.

Consider, for example, the walls and water in Shadowdarke's Tanks, or the dirt in Leftley's Lode Wars, or even the webs in ACWraith's WebCrawl. These are all examples of autojoining. Wherever players have the ability to alter their environment, it's nice to maintain the same kind of smooth appearance. Even if players don't alter the environment, maybe you have randomly generated maps. Or, maybe you just want walls to look contiguous and don't want to bother setting the icon state for every single turf in your map. Autojoining is a pretty straightforward technique that can add a lot of flair to your game; it's uncomplicated enough that it was even used in two of the winning entries in the 4K Challenge of 2002.

The seminal work on this subject was done by Pmikell, veteran of the BYOND community. His tutorial is a good place to find further information. It's also good background for this column, so you might even want to read it first. (He did 16-state joining a little differently than I did, and used different algorithms for joining, but it's definitely worth reading.)

Where Do I Start?

The key concept I'm going to mention all throughout this column is connections. The idea of autojoining is that you're telling your game to indicate visually (through a change of icon state) that two turfs next to each other are somehow connected. For example, if you have two wall tiles next to each other, you'd want them to appear as a single seamless wall. Thus they're connected.

The first thing to decide about autojoining is which directions can form connections between turfs. In the very simplest of systems, only straight connections may be possible: North, south, east, and west. Diagonals aren't even considered. This might be done for something like roadways.


Figure 1: 16-state autojoining road icons and bit flags

If you only connect in those 4 directions, you have 16 possible icon states. This is easy enough to calculate as 24, because each of the 4 cardinal directions can be either connected or not connected.

As you might guess if you're familiar with them, autojoining is done with bit flags. Every direction is counted as connected or disconnected, which in binary is a 1 or a 0. I'm not going to do a refresher course on binary right now, but here's how it breaks down: You assign each direction a number, starting with 1 and multiplying by 2 for each direction after that. To make things simple we'll go in a circle, so let's say north is 1, east is 2, south is 4, and west is 8. By adding these numbers together, we can show that several directions are connected at the same time. A T-junction going north, east, and south would be represented by a 7 (1+2+4). This can then be translated directly to an icon state.

turf
  var/joinflag=0

  proc/AutoJoin()
    MatchTurf(NORTH, 1)
    MatchTurf(EAST, 2)
    MatchTurf(SOUTH, 4)
    MatchTurf(WEST, 8)
    icon_state = "[joinflag]"

  proc/MatchTurf(direction, flag)
    // match to the same type
    if(istype(get_step(src, direction), type))
      joinflag |= flag      // turn on the bit flag
    else
      joinflag &= ~flag     // turn off the bit flag

If you're not familiar with binary and with bit flags, then the |= and &= and ~ operators will mean nothing to you. I'm not going to bother explaining them here, though, because that will only make things confusing. You don't really need to know what these operators do yet, just as long as you notice the comments alongside each one.

Instead I'll describe what I'm doing in the AutoJoin() and MatchTurf() procs. AutoJoin() calls MatchTurf() for each direction, and tells the other proc which bit flag (1, 2, 4, 8) goes with that direction. MatchTurf() then just checks to see if the turf in that direction is the same, and if it is then it makes a connection by setting the bit flag.

Connections don't go both ways. You're only doing this for one turf at a time. To fully join turfs, you should call AutoJoin() for any of that turf in range(1,src), for the turf that just changed.

turf
New()
for(var/turf/road/T in range(1, src))
T.AutoJoin()

turf/road

More Than 16

However, what you see above is way too simplistic for things like walls or water. What if you have a 2×2 group of walls and you want them to look like one big wall? With 16-state icons like we just covered, this might look like a square ring, with a hollow section in the center. A larger group of walls would look more like a waffle. To deal with this, we're gonna have to handle some diagonals.

In particular, what we want to do is make wall icons that have solid corners in addition to what we used before. A solid corner will exist when you have another wall to either side, and then one on the diagonal. For example, in our 2×2 block the southwest corner wall tile will have another wall to the north, one to the east, and one northeast.

You might guess that this would take 256 icons (28, for 8 directions), a ridiculously large number. Fortunately, we're not at that point yet. 256-state icons would be more like the 16-state roads above, but also running in diagonal directions; they're not meant for solid corners. So don't panic yet.

In fact, there are only 47 possible icon states for what we need. Why so few? It turns out that a diagonal connection really only matters if the horizontal and vertical connections to either side are good.


Figure 2: The southwest wall tile doesn't connect directly to the northeast unless there are also walls to the north and east.

Now, our bit flags do indeed cover 8 bits, because there are 4 cardinal directions and 4 diagonals. Starting from the north and going clockwise, that means north is 1, northeast is 2, east is 4, southeast is 8, and so on. However, the diagonal bit flags (2, 8, 32, 128) can be left off if they don't make a valid connection, so a lot of the numbers from 0 to 255 will never be used at all.

turf
var/joinflag=0

proc/AutoJoin()
// cardinal
MatchTurf(NORTH, 1)
MatchTurf(EAST, 4)
MatchTurf(SOUTH, 16)
MatchTurf(WEST, 64)
// diagonal
MatchTurf(NORTHEAST, 2, 5)
MatchTurf(SOUTHEAST, 8, 20)
MatchTurf(SOUTHWEST, 32, 80)
MatchTurf(NORTHWEST, 128, 65)
icon_state = "[joinflag]"

proc/MatchTurf(direction, flag, mask=0)
if((joinflag & mask) == mask && istype(get_step(src, direction), type))
joinflag |= flag // turn on the bit flag
else
joinflag &= ~flag // turn off the bit flag

And now it's time to explain a little bit about those | and & operators. These are binary OR and AND. For a|b (OR), when bits in either number are turned on, the result will have those same bits turned on. For a&b (AND), the bits must be set in both numbers to be on in the result. That (joinflag&mask)==mask test basically says: If all the bits in the mask are turned on in joinflag, we can proceed.

A bit mask is something you use when you want to test bit flags, or turn certain ones on or off as a group. Notice the second set of calls to MatchTurf(): They each have 3 arguments instead of 2. The third argument is a bit mask. Normally this is 0, and (joinflag&0)==0 is always true so basically we just ignore that test for any of the 4 cardinal directions. But for the diagonals, we want to know if there are other connections to either side. For northeast, it's important to know if there are other connections to the north (1), and east (4), and those flags add up to 5. If the bit flags 1 and 4 are turned on, then bit flag 2 (northeast) can be used; otherwise we'll have to leave it off. The other masks, you'll see, are also combinations of directions: 4+16 for east and south, 16+64 for south and west, and 1+64 for north and west.

And now that you've got your autojoining set up, here's a look at what those 47 icon states would look like (plus one with no name as a default icon):


Figure 3: 47-state autojoining wall icons and bit flags

I know what you're probably thinking now, because I've thought it myself: How in the world am I going to set up all those icons?

Well, it's not all bad news. Break up each icon into 4 quadrants. Notice that each quadrant of the icons above is an outer corner, inner corner, solid corner, horizontal edge, or vertical edge. Chances are your icons will have no significant differences along the seams of those quadrants (like the center, or center north) that would keep you from mixing and matching corners to fit. So make up 5 icons to start:

  • 0: All outer corners. This icon is for walls (or water, or whatever) that stand alone.
  • 17: Vertical. This icon is for walls that go north and south only.
  • 68: Horizontal. This icon is for walls that go east and west only.
  • 85: Intersection (inside corners only). This icon is for walls that go north, south, east, and west, but have no diagonal connections at all. A simple intersection.
  • 255: Solid. This icon is for walls that are completely surrounded on every side.

And the corners of those 5 icons can be broken up and reassembled into the 47 icons you need. The splicing process is still a royal pain in the butt, but now it's so much easier. To simplify things even further, I've created a utility to do this splicing for you.

Little Details

It may not be convenient for you to call AutoJoin() manually every time you want to check a turf for changes, and you might not want to autojoin every single time a turf is created, so it's a good idea to use a var to control whether you want to handle autojoining for this turf or not.

turf
var/autojoining

New()
for(var/turf/T in range(1, src))
if(T.autojoining) T.AutoJoin()

That isn't so bad. You can turn on the autojoining flag for turfs if you want them to change on their own.

The way autojoining has been handled up to this point, it doesn't really account for things that are almost the same type, like where you might have /turf/wall/secret that has no density but still looks like a regular /turf/wall. The istype() check we've been doing up to this point is inadequate, because it only checks if one turf is a subtype of the other. Instead what we need is to specify some sort of common type to autojoin with. To do this we're gonna have to change the MatchTurf() proc to do this matching for us.

turf
var/joinflag = 0
var/autojointype // leave this null to match exact same type only

proc/MatchTurf(direction, flag, mask=0)
if((joinflag & mask) == mask)
var/turf/T = get_step(src, direction)
// want to join to the borders of the map?
// just change T && to !T ||
if(T && (T.type == type || (autojointype && istype(T, autojointype))))
joinflag |= flag // turn on the bit flag
return
joinflag &= ~flag // turn off the bit flag

Now MatchTurf() behaves differently: It checks to see if two turfs are exactly alike in type, or if they share a common type you specify. So our walls can now autojoin like so:

turf/wall
autojoining = 1
autojointype = /turf/wall // all /turf/wall subtypes join each other
density = 1
opacity = 1

secret
density=0

It's really that easy. Once you get the early setup and the bit flag and icon messes out of the way, the code that handles all this isn't too hard at all.

Because of the way this whole system is arranged, you can use it not just for walls, but also for water or other obstacles. Tanks uses autojoining for walls, water, and excavated trenches, but this can even be used for scenery and other non-interactive elements of the game.

B(e)yond Turfs

Think it's too limiting that this autojoining algorithm only supports turfs? Well, it can be used with objs and mobs too. It's just gonna require a little more work. Three things we have to do differently: First, we have to look in turf contents for the matching items. Second, we have to account for what happens when the thing moves. And since a new obj or mob won't be created when we delete it, as would happen when we replace a turf, we have to handle Del() too.

atom/movable
var/joinflag = 0
var/autojoining
var/autojointype // leave this null to match with the exact same type only

New()
for(var/atom/movable/A in range(1, loc))
if(A.autojoining)
A.AutoJoin()

Del()
var/oldloc = loc
loc = null // make this disappear
Moved(oldloc) // and autojoin items where it used to be
..()

Move()
var/atom/oldloc = loc
. = ..()
if(.)
Moved(oldloc)

proc/Moved(atom/oldloc)
if(loc)
for(var/atom/movable/A in range(1, loc))
if(A.type==type || (autojointype && istype(A, autojointype)))
A.AutoJoin()
if(oldloc)
for(var/atom/movable/A in range(1, oldloc))
if(A.type==type || (autojointype && istype(A, autojointype)))
A.AutoJoin()

proc/AutoJoin()
// cardinal
MatchThing(NORTH, 1)
MatchThing(EAST, 4)
MatchThing(SOUTH, 16)
MatchThing(WEST, 64)
// diagonal
MatchThing(NORTHEAST, 2, 5)
MatchThing(SOUTHEAST, 8, 20)
MatchThing(SOUTHWEST, 32, 80)
MatchThing(NORTHWEST, 128, 65)
icon_state = "[joinflag]"

proc/MatchThing(direction, flag, mask=0)
if((joinflag & mask) == mask)
var/turf/T=get_step(src, direction)
if(T)
for(var/atom/movable/A in T)
if(A.type==type || (autojointype && istype(A, autojointype)))
joinflag |= flag // turn on the bit flag
return
joinflag &= ~flag // turn off the bit flag

And now you have something that will autojoin the icons for objs and mobs, should you be demented enough to try it. You might use something like this for a pile of treasure, for example, where the treasure spread out over multiple tiles is meant to look like one big pile, but as you pick it up piece by piece the pile shrinks.

Snapped Shut

And that's all you need to know to implement autojoining turfs (and other atoms). The effect is really worth the extra effort of setting up all those icons, since it gives your game a more polished look.


Using IconCutter

The IconCutter utility is meant to be a timesaver for setting up autojoining icons. It was originally designed for use with 47-state autojoining only, but now it supports other types, including some exotic join types you'll probably never use. It works by taking icon states you provide and cutting out the 16×16 corners of each, then splicing them together in all permutations for the final result. Splicing (or drawing!) the other icons by hand is much more difficult.

There is only one rules you need to follow: Make your icons so that they can be split along the seams of each 16×16 corner. That is, the northwest corner of the horizontal icon should match up perfectly with the northeast corner of the outside corner icon, and so forth.

To use the program, first you need to make your source icons as described above:

  • 0: All outer corners. This icon is for walls (or water, or whatever) that stand alone.
  • 17: Vertical. This icon is for walls that go north and south only.
  • 68: Horizontal. This icon is for walls that go east and west only.
  • 85: Intersection (inside corners only). This icon is for walls that go north, south, east, and west, but have no diagonal connections at all. A simple intersection.
  • 255: Solid. This icon is for walls that are completely surrounded on every side.

The source icon file can have these states listed as "0", "17", etc., but as of version 2 you can also use multiple joinable icon states like "water68" and "water85" while others might be "trees0" and "trees255". IconCutter can separate these and splice all the water, all the trees, etc.

In the original version of IconCutter you had to compile the project yourself with your icon as a source file, but now it has a simple browser-based interface that's a snap. Just load the file from anywhere. Once your icon file is loaded, select the autojoin types you'll use, and click a button to complete the process. Once the joining is complete, you can save your finished icon file.

Quicker Joining

Autojoining on the cheap is pretty easy. First you just need a list of bit flags for each possible direction, and then you match up each direction accordingly. After that, just do a little bitwise arithmetic and you're done.

// dir_bitflags[dir] = bitflag
var/list/dir_bitflags = list(1, 16, 0, 4, 2, 8, 0, 64, 128, 32)

turf
proc/AutoJoin()
for(var/d = 1 to 10)
if(!dir_bitflags[d]) continue
var/turf/T = get_step(src, d)
// want to join to the borders of the map?
// just change T && to !T ||
if(T && (T.type == type || (autojointype && istype(T, autojointype))))
joinflag |= dir_bitflags[d]
/*
Bitwise math for 47-state joining:
Leave cardinal directions (1+4+16+64=85) on, but AND others with
neighboring bits.
*/

joinflag &= ((joinflag << 1) & ((joinflag << 7) | (joinflag >> 1))) | 85
icon_state = "[joinflag]"

As you'll see, MatchTurf() has been eliminated entirely. By removing it, we've removed a little of the overhead and also taken out that pesky test with mask.

Exotic Joins

256-state joining, where all directions may connect independently of the others, is relatively uncommon because a corner join usually needs some appearance of width, which would need another icon to either side. (If you developed those side icons, you'd need 16 of them, for a total of 272 states.) However, 256-state joining is quite simple, and just avoids some of the bit tests that most other forms use. WebCrawl uses 256-state joins quite effectively, since the connections are only 1 pixel wide. Using the snippet in the section above, you could simply remove the bitwise math for 47-state joins

Besides the common 47-state joining, or the simple 16- and 256-state, there are other types of autojoining available. If you want something like 47-state joining but where a corner "connects" as long as just one of the adjacent directions connects also, then 161-state joining (a hybrid of 47 and 256) is for you. 161-state joins can also handle odd situations like railings that might stop at a corner.

To build this in IconCutter you need two additional states: 187 and 238.

  • 187: Bending vertical. This icon is for walls that go north and south, but will be connecting to other walls diagonally adjacent.
  • 238: Bending horizontal. This icon is for walls that go east and west, but will be connecting to other walls diagonally adjacent.

Those represent partial corners. Think of the way fluid in a test tube tries to "climb" the side of the tube forming a U-shaped depression. This is the edge of that U. The next tile over will have an inside corner, which is the middle of the U. This allows you to spread your inside corners along a wider area and get smoother joining. The only significant downside is that your icon will be more than three times the size.

The bitwise math for 161-state joining is simple:

// 161-state join
joinflag &= (joinflag << 1) | ((joinflag << 7) | (joinflag >> 1)) | 85

Another hybrid type is 82-state joining, which merges 16- and 161-state joining. The difference with this is, it treats inside corners and filled corners as the same like 16-state joining, but it allows a partial corner like 161-state joining. This type of join is rare and you'll probably never need it, but if you do, this is the math for it:

// 82-state join
joinflag &= ((joinflag << 1) ^ ((joinflag << 7) | (joinflag >> 1))) | 85
Possibly one of the greatest BYONDscape classics. Many programmers will find this very useful when wanting to make an impressing flow of terrain in his or her project.

On another hand, a typo that needs to be fixed is still here. On this line:

var/list/dir_bitflags = list(1, 16, 0, 4, 2, 8, 0, 64, 128, 16)

The last '16' should be a 32.

var/list/dir_bitflags = list(1, 16, 0, 4, 2, 8, 0, 64, 128, 32)
I thought this article had a section called "Foomerian Autojoining?" Oh well.

And on a site note, maybe that "Did you know?" box with the tip about stddef.dm should be updated to instead point at http://bwicki.byond.com/?StddefDm, since the file is no longer publicly available otherwise.
I don't believe "Foomerian Autojoining" was invented until after this article was published.

Besides which, there really isn't any "good" way to do it, just a demo that I released for a hacky 13-tile autojoining method, and another post further on trying to polish it up a bit. I don't think there's been any further developments than that, aside from Lummox including features in IconCutter and PilerTiler for it.
Unknown Person wrote:
The last '16' should be a 32.

Good catch, UP! I went to fix it but apparently someone already did.
Audeuro wrote:
And on a site note, maybe that "Did you know?" box with the tip about stddef.dm should be updated to instead point at http://bwicki.byond.com/?StddefDm, since the file is no longer publicly available otherwise.

Added. Thanks!