Creating Virtual Worlds
by Tels


This article talks about level design. "Uh-oh, not another one!" you might think. But I promise this one will be different ;)

I don't want to discuss bad and good design - although my topic automatically avoids some very bad designs, or so I hope. I will rather write about the final stage, getting the level into the computer.

When you have planned, drawn on paper, discussed, planned again, thrown it into the round file, planned yet another time, drawn on paper, finalized it - then the glorius moment comes and you turn on that evil little beeping, blinking machine in the corner and want to bring your level to life. Thats what this article is all about. Follow me to the next room, er, section...

Prependix A - What you need for the examples

Some technical background as well as basic explanations follow. You can skip this and read it later on, when you want to try the examples or don't understand what I'm talking about.

This is not just an idea, or a proof of concept. I have already implemented this and tried it. Although I haven't had the time to make an entire level, and there is still much work left, it is basically proven to be usable ;)

The things I will discuss apply more to indoor design of single or multiplayer maps, although it could be adapted for outdoor design. Also it applies to the Thief Universum, but other engines are (mostly) more simple, so you can easily adapt it.

My project CoW supports what I want to talk about. Screenshots, examples etc will be interspersed in the text. Currently I can only support Thief and Thief Gold. Thief II will be supported as soon as it and the editor are released. The editor for System Shock 2 will probably never be released, so I can't support it. If you want to try the examples, you will need a copy of Thief or Thief Gold. So grab it now!

The engine I use as a basis (Dark Engine from Looking Glass Studios) works with brushes, but my method can easily adapted to work with polygon/vertice based enignes. The Dark Engine uses a method called CSG, where you shape your world from a vast solid universe (which is, in this case, you guessed it, solid) by using air, solid and water brushes. You have cubes, wedges, pyramids and cylinders, and that's practically it.

You also have objects you can (and must) place in the world. The terrain is static, so you have to use objects for interaction. Also there is a rather low limit of 1024 terrain polygons per scene, and objects are the only method to break that limit.

Also the DarkEngine has some special rules. Lightning is crucial, you cannot place lights wherever you want, since the Thief Universum works only when there are enough shadows to hide in. So a lighting/shadowing design must be kept in mind when desinging the world. Also, since Thief is all about avoiding combat, you need to have enough locations where the player can hide or retreat. Often you have to redo the proprotions of a room, because it is too small or too big.

Another complication are the sound rooms. A sound room has to be placed around each terrain, so that you hear footsteps, and the sound propagation can be calculated. Dark Engine features real sound, that travels accross doors, is muted when you close the door, doesn't pass walls, echos down hallways, etc., lets you exactly hear if the enemy is left, right or behind you. Did you get ever killed in half-Life because you could not determine whether that noise was above, below, behind or right under you? Then you know what fake sound propagation is :) Laying these sound rooms is tedious, and often leads to errors. In a world where the player relays off the sound to locate the enemy, sound room placement is crucial.

CoW is written entirely in Perl, and since its tedious to fiddle around with Perl code for larger projects, a GUI is already in the works.

There is a more or less strict line between the core and the vworld concept, so that it would be relatively easy to convert it to a different engine.

All the code snippets are available as Perl scripts, which you can run and look at the output. Since the frame code is always the same, I wrote a quick wrapper, which just calls the single tutorial files. Use the wrapper with

perl examplenumber[enter]
and replace the example number with, well the example number given next to each code snippet. ;)

Let's begin!

Foreword - How it was, how it shouldn't be, how it should be...

Normally you are paid as a more or less talented individual to implement the level idea into a working game world. That applies wether you build a single player level, a multiplayer one, a strategy map, or a massive multiplayer online world. You then normaly would build the level out of single vertices, polygons, or brushes like cubes/wedges etc. After laying the general layout, you add lights, objects, add story elements and puzzles, connect buttons, etc.

At some point your lead designer approaches you (imagination of this approach is left up to the reader :-) and asks you to make a few changes. Add one more room, don't use that texture, no, this object is too big, we need fewer polygons. You practically redo your level a couple of times. Given a hard timeframe, probably buggy tools, lame programmers that won't implement your cool ideas into the scripting language but eat pizza instead - all that lets you grow gray hair. So we want to do it a bit different this time...

Instead of bothering with single brushes, or objects or polygons, we will only work with Cells.

Basically it goes like this:

Our world consists of a cube cell, which is split up into child cells. This is done recursively until we no longer want to split, and then the cell is generated as a room (or left empty).

Each cell has properties to describe its appearance, size, name etc. The parameters that describe the cell's room are inherited by all its child cells, where they can be overwritten.

Also each cell has Things that can be placed in it. Things can also be inherited by child cells, to make it easier to place a thing in each of the subcells.

But let's start simple and make a world that consists of only a single room:

Section 1 - A single room

$room = { texture => $txt->texture("blustn"), };         # line 1
$world = cell ( name => "Hello World", room => $room );  # line 2
$bunch = worldAsBunch ( 0, 0, 0, 32, 32, 16, $world);    # line 3

Example 1

1: A single room

Thats it! You may wonder now, what has happened? And where are all the details?

Okay, for the slow ones :o)

We described a room in line 1, and the only thing we set was the texture, which is named 'blustn'.

In line 2 we described our world, gave it a name and said it should use the room described earlier. The call cell() will fill in any parameters we left with good default values.

In line 3 we constructed the world as a bunch, which contains all the gory brushes and objects from our world and can then be saved to a mission file. The wrapper will do the rest for us. In line 3 the 6 parameters are X,Y and Z of the world's center and its width, length and height.

Just FYI, here is the output from the script:

VWorld Tutorial Wrapper v0.0.1 (C) Copyright by Tels 2000. All Rights Reserved.

Call as perl [section] where section is 1..x and defaults to 1.

Reading tutorial section 1...
Texture families: 8
flavours: 59 links: 242
Tutorial Section 1

Hello World: 0 0 0 (32 32 16)
Debug on for 'Hello World'
Unrotated 0 out of 1 cubes (1 brushes).
Optimizing: air's (1)
Removed 0 rendundand cubes by comparing 0 times.
Stuffing meself into VBR:
'Hello World' contains:
1 brushes, 1 rooms, 0 flows, 0 objects and 0 lights in 3 groups.
Generating object system: 0 links (0 with data), 0 properties.
Done, enjoy data/vtut_1.mis!
Destroyed 'Hello World'

That's it! Load the mission into DromEd, type light_bright[Enter] in the lower right command window (since we haven't added any lights/lamps, and you would otherwise not see anything), then portalize, build the room database, and then hit [ALT]+[G] to go into Game Mode. Walk around and enjoy the empty space a bit.

Now onto to some more complex things...

Section 2 - Two rooms

Our world is a cell, and we now want to split it into two rooms. First we add some more parameters to the room, to make it look more interesting.

$room = { texture => $txt->texture("wood"), 
          floortexture => $txt->texture('brick'),
          ceilingtexture => $txt->texture('rufgry'),
  };                                                     # line 1

Also we describe two cells, which will become the first and second room:

$room1 = cell ( name => "Room 1", size => 16 );          # line 2
$room2 = cell ( name => "Room 2", size => 16 );          # line 3

In our world description, we add these two cells as children, and the rest stays the same:

$world = cell ( name => "Hello World", room => $room, 
  div => inX(),                                          # line 4 
  cells => [ $room1, $room2, ]);                         # line 5

Example 2

2: Two rooms

In line 4 we tell the world cell that it should be split into subcells along the X-axis. In line 5 we define the two sub cells.

That's it!

The purple cubes encompassing our rooms are the Sound rooms, which are generated automatically by CoW. Forget that I ever talked about them.

Now you may wonder how we can split cells. Well, although you could split them in every conceivable way, I settled on splitting them always into cubes along the three axes. Funny thing is, you can describe nearly all possible designs with that, and even if you could split your cell into cylinders, that wouldn't work, since the Dark Engine can only have cubic sound rooms. Oh well! :)

But let's move to a more complex example.

Section 3 - Three rooms over the edge

Now we want to have the upper room extending to the right. Also, there is still a wall between the two room, so me must make a way for the player to pass through. Giving him a big hammer won't cut it here (pun intended ;).

Now if we just double the world's size along the Y axis (aka its width), we get two rooms that are still equal in size. Thats not going to work. There is more than one solution but we'll go with the following: What we do now is to split the lower cell (which contains room 2) into two cells. The left one contains room 2, and the second one is just an empty cell which fills up the space:

$lower = cell ( name => "lower part", size => 16, 
   div => inY(),                                         # line 1
   cells => [ $room2, emptyCell (16), ], );              # line 2

And instead of room 2, we add $lower to the world:

$world = cell ( name => "Hello World", room => $room, 
  div => inX(),                                          # line 3 
  cells => [ $room1, $lower, ]);                         # line 4

That's all the changes we need for now.

3: Two rooms, with corner

As you can see in line 2, you don't need to store the result of the cell (or in this case emptyCell()) call, but you can insert it directly. Oh the wonders of Perl :-)

Section 4 - Another Brick in the Wall

Okay, but there is still that nasty wall! Let's do some destruction!

$room2 = cell ( name => "Room 2", size => 16, 
  nowall => aNorth(), );                                 # line 1

We just tell room 2 that it shouldn't have a wall at the north side. Well, that won't work like you'd expect. Can you guess why? Because room1 still has its wall at the south side. But if we add nowall => aSouth() to room 1, its entire south wall will be left off. That won't look good, since room 1 is much bigger than room 2 and thus we would have a distorted room 1.

Two solutions:

  • Split room 1 into two rooms, and leave only the wall of the left room off. Not so optimal.
  • Add clobberwall => aNorth() to room 2. This does exactly what we want, it just clobbers up the wall to the northern room 1.
    $room2 = cell ( name => "Room 2", size => 16, 
      nowall => aNorth(), clobberwall => aNorth(), );        # line 1

    4: Two rooms, with corner and no wall between them

    That's all! Onwards!

    Section 5 - Doorways to Heaven

    Now let's make a doorway instead of the missing wall. Also, we want to have 4 rooms instead of the empty lower right corner of our world.

    A doorway is just a Thing you can place in a room. There are special functions for the most important things and their most important places, aka door and window frames, that reside at the center of a wall. You can really place them anywhere you want, but more on that later on.

    Define the door:

    $mydoor = { framethick => 0.25, texture => $txt->texture('wdplank'),
                floortexture => $txt->texture('rooftile'),
                width => 4, height => 8, };                  # line 1

    Please note that we don't define its length. It will then automatically get a length of twice the wall thickness, and that's what you expect (if you expect the Right Thing[tm], anyway. ;)

    Okay, but we must tell CoW to place the door somewhere. We have to decide whether we want to place it in room 1 or room 2. Tough decisicon, since we really want it to be placed between the rooms. Uh-oh. But don't freak out! :)

    Since a Thing can be placed anywhere inside a cell's border (and really even outside, if you think that's a proper place), we can just add it to one of the rooms at the border to the other, and all will be well. Also, we want it at the center of the north wall of room 2, and given only room 1, we wouldn't know where that center would be. So we have to add it to room 2. Also we should remove the nowall and clobberwall properties:

    $room2 = cell ( name => "Room 2", size => 16, 
      things => [ openDoor ( aNorth(), $mydoor ),] );        # line 2

    5: Two rooms, with corner and a door between them

    Section 6 - Redefine the World

    Now I will show you what true powers lie in this concept. :o)

    Let's pretend you want to add 4 rooms instead of the empty space at the right lower corner, also room 2 should be only half the size it is now.

    If you had build your level/world the traditional way, you would be screwed. You would have to delete room 1 and redo it, and you would have to move the door and redo room 2.

    Not so with our concept. Simply add 4 cells to the lower room instead of the empty one:

    $lower = cell ( name => "lower part", size => 16, div => inY(),
       cells => [ $room2 ], );                                # line 1
    addCells ($lower, 4, "4,4", "room##num##" );              # line 2 

    In line 1 we split it into Y, give it a name etc. You should be familiar with this already. In line 2 we use the function addCells to add 4 cells. This is a bit easier then writing the same cell definition four times. In the upcoming GUI, you would select "Split into cells" and then enter 4. First parameter is the cell to add cells to, the next is the number of cells to add. The third and fourth parameter are the names (comma separated) and the sizes (also comma separated). If you don't give enough names/sizes, the last one will be repeated until all are defined. The ##num## will be replaced later on with an actual number, you will see this frequently in my examples to generate unique names.

    Whew! That's nearly all. The only thing left is to increase the world size, because otherwise each of the new rooms would be only 4 feet wide.

    $bunch = worldAsBunch ( 0, 0, 0, 80, 32, 16);            # line 3

    Now you may start wondering about the size of the rooms and the world, and if all this calculating isn't a bit too complicated, and how you calculate the world size, anyway. Also, we haven't yet made room 2 half the size as it should be.

    Well, to make it short, the cells' sizes are not absolute sizes. They are relative sizes. Also the thing placement coordinates are relative. It all boils down to Einstein, afterall :o)

    To calculate a cell's/room's actual size, all the sizes of the subcells are taken, summed up and then each of the cells' sizes is divided by this sum.

    So, two cells with size 16 will be equal in size, and get exactly one half of the size of the mother cell, whatever that size may be. If they both have 4 as a size, the result will be the same, no matter how big the mother cell is. If one of the cells is 16, and the other is 32, then the second one will be always twice the size of the first cell. You can also specify (absolute) minimum and maximum values, so that a hallway will never grow, for instance, larger than 16 foot, no matter how big you make your world.

    Since our room 2 currently gets 1/2 of the size of the lower part, and we want it only 1/4 (half the size it is now), we can't just set is size to 8. It would get then 8/(16+8) and thats 1/3 of the size. If you like fractions, you can set the room 2 size to 1/4, and the lower_right cell's size to 3/4 if you like. 4 and 12 work, too. Just pick the method you like.

    $room2->{size} = 4;                                      # line 4

    Whew! You have just resized/move almost anything in your world! Now, wasn't that easy?

    Let me show you one more thing. Of course we need doors to the new rooms. Each of them should have a door on the east side. We could now add a door to each of them. But remember how I talked about inheritance? Well, just give the mother a door and all the children will inherit it. Not quite how it works in real life, though.

    But one more problem remains. Since all the rooms in the lower cell are contained in one cell, room 2 will inherit the door, too. This will not look nice, since there is no room west of room 2 where the door could lead. We can do two things to overcome this. We could split the lower cell into two parts, room2 and a cell containing all the other rooms. This may become neccessary. But for now we can just give room2 the "noinherit" property and it won't inherit any things.

    $mydoor->{inherit} = true();                            # line 5
    $room2->{noinherit} = true();                           # line 6
    $lower->{things} = [ openDoor(aEast(),$mydoor) ];       # line 7

    6: 6 rooms, with a corner and a lot of doors between them

    In line 5 we set the flag inherit of our door to true, so sub cells will inherit it. Also, this will avoid the mother cell getting the door, too. In line 6 we disable inheritance for room 2, so that it doesn't get the door. In line 7 we simple add the door to our lower side as the only thing.

    Remember, the doors will move to the proper places if you resize the rooms!

    That's all, folks!

    Section 7 - Links, routes and other unsorted things

    Sometimes you want some things placed relative to each other. CoW already supports this and you can position, for instance a chair in front of the table. Whenever you move the table around, the chair will follow. (This works of course only prior to generating the level, once this is done, the chair/table become static. Although the Dark Engine is flexible enough to make a chair magically follow a given table...)

    Another, more exciting feature is the automatic patrol routes. This is accomplished by CoW generating a structure along with the world which contains so called TerrainPoints (short TP). These are simply points in 3D space, that can be refered to by path and name of the cell they belong too. Per default each center of a cell and thing you add becomes a TP.

    Although this doesn't sound too useful yet, it proves to be a very exciting feature, because the TP database presents the containment-relationship of the cells. For instance, the kitchen is contained in the lower story, which is contained in Lord Bafford's manor, which is contained in the south part of the city, which in turn, and so on.

    So, if you want an AI to patrol from the kitchen to the garden, then back inside to the sitting room, you need only to give CoW the names, and the TP database will provide you with the 3D coordinates to visit. From that you can generate a patrol route automatically.

    In the Dark Engine, patrol routes are rather easy, and yet they are not easy. When you build your level in the editor to be used by the game engine, you have to make it build a pathfinding database. This database enables the AI to find its way on its own. You need only to place some markers, link them and the AI will happily traverse from marker to marker, avoiding pitfalls, climbing stairs and even opening doors. But placing markers is tedious, and linking them is complicated and error-prone. Adding/removing some patrol points or creating another identical patrol route is nearly impossible without tearing your hair out.

    The automatic patrol route generator of CoW will generate all the proper markers and links from the TP database. So whenever your terrain changes - which will be a lot given the fact that CoW makes this so easy - the route will still be alright. Even switching the kitchen with the sitting room will not make it invalid. Rerouting an AI is also very easy.

    Flooding a cell will automatically flood all children of the given level, so setting entire parts of your world under water is very easy.

    Future - More ideas

    Another very nice feature of this entire VWorld stuff is the containment-relationship of the cells. Although the Dark Engine doesn't support this, I can well imagine a game which uses this info like this:

    Player: Go to the kitchen!
    NPC: Which kitchen? There are two of them.
    Player: The one in the south wing.
    NPC: On my way. [shuffles away]

    And since the game knows that the south wing consists of some stories and that story 0 contains two rooms, one of them is named "kitchen", the NPC instantly knows to which room you refer to, and how he/she can get there. That's like the automatic patrol routes I mentioned in section 7.


    If this sounds like easy, automated level generation is around the corner, I should add some realistic points, too.

    There was an GUI in the works, but the only guy working on it no longer has time, unfortunately. The GUI would make many things much more easier, because even for me it is somewhat tiresome to describe the level without actually seeing the cellsplits. If you implement it from a paper version, this is, however, a straight-forward conversion and can be done without a GUI.

    The file format used in Thief/System Shock is undocumented, as well as all other internals and DromEd. Nearly all we know was found out by fans and I cannot say how I glad I am how many people devote so much time to this "hobby". But even now CoW does not understand the files fully, which means it can not do certain things, like generating creatures (AI, NPC etc) nor particle effects (so the famous torches are out of reach). The DromEd is buggy (at least) and often causes corrupted missions, crashes or other obscure things. While this should be in theory no concern with CoW, CoW still depends heavily on the editor. We just do not know enough, and there seems to be so little time.

    But the future does look interesting, indeed.

    Some wonderfull cityhouses, generated by a script done by Xarax. It took him about 3 minutes to generate the 16 houses with a script, and 40 minutes to place and Grid snap them in DromEd.

    Castle, an example castle done by me in about 10 hours (including developing time to implement some stuff into CoW)

    A temple, included as tutorial 8. Done in 2 hours.

    Afterword - Thanx

    I want to thank the fine folks at the TTLG forums (especially the Editor Guild) for all their help. Especially Xarax, Thumper, and Totality proved very valuable.

    I do hope someone finds this useful (I certainly have re-invented the wheel, but at least I had fun :-). All your comments, suggestions, criticism or hints are welcome - flames will be forwarded to Dave Null ;)

    If you want to use this in a game of your own, or want to add support for a game engine other than the Dark Engine, I certainly want to hear from you. I am looking forward to learning. There remains a lot of work to be done.

    Please have the appropriate amount of fun.


    Bibliography, Downloads

    Here are some links to get you started:

    The CoW homepage
    Xarax site
    Thief - The Dark Project page
    Thief, System Shock forums, Editor Guild
    Samples for this tutorial (Download latest CoW BETA, they are included in the dir tutorial/)


    Dark Engine
      The graphic/level engine from Thief, Thief Gold, Thief II, System Shock II.
    Sound room
      A cubicle that wraps around a given piece of terrain and makes you hear footsteps, propagates your and AI/object sound through the world. If two sound rooms overlap, sound travels across the border, no overlap means no sound is traveling. No sound room can contain the center of another sound room.
      A part of our world as well as the entire world. In my example always a cubicle. Can contain (is divided into) child cells, or can just be generated as a Room.
      A cell can be generated as a room (with/or without walls). This is done by the Room Generator.
    Room Generator
      Takes the dimensions and center of a Cell and generates a room out of it by placing Brushes and Things.
      A thing can consist either of Terrain or one or more Objects. Things are what you place in the VWorld concept inside the Rooms/Cells.
      This is the static part the world consists of. In the Dark Engine, this is Brushes. Terrain polygons are limited to 1024 per scene in the Dark Engine.
      Out of brushes the Terrain is generated. Only a limited amount of different brushes (cubes/wedges/cylinders/pyramids/dodecahedron) are available. You can, however, use some logical AND/OR/XOR combinations by using brushes that turn solid into water, or water into air without effecting other types of terrain. This enables you to build some even more complicated setups with fewer brushes.
      An object is placed in the world, and does not consist of Terrain. An object can have a lot more polygons than the terrain, typically 300-600 for an AI object. Also objects have properties and interact with the world by using pre-compiled scripts.
    DromEd Multibrush
      A multibrush consists of Objects and Terrain and is something like a portable part of your level, which can be imported (at least parially) into other levels. CoW can generate VBR files (multibrushes) as well as full MIS (level) files.
    Grid Snap
      The grid is very essential in DromEd. You have basically to snap each brush to the grid, so that vertieces and edges snap to each other. If you don't do this, you will run into problems in the optimizing phase. This all is very time consuming and frustrating. Objects are not effected by this. CoW automatically generates snapped Terrain, although for hand-editing or placing generated stuff, you have to use the grid.

    Discuss this article in the forums

    Date this article was posted to 5/17/2000
    (Note that this date does not necessarily correspond to the date the article was written)

    See Also:
    Level Design

    © 1999-2011 All rights reserved. Terms of Use Privacy Policy
    Comments? Questions? Feedback? Click here!