Skip to content

Shape Grammars

dashodanger edited this page Mar 8, 2022 · 8 revisions

Shape grammars are a method of procedural generation that Obsidian relies on to create its new rooms. Shape grammars offers a balance between entirely computer-generated architecture and human design by allowing a user to input set of rules and products for which the procedural generation is restricted to, creating a balance of patterns that is still seemingly human-created.

I'm Here to Mod Obsidian, Not To Become a Linguistics Expert!!!

Imagine building something with Lego blocks. When you start from nothing, you virtually have the option of using any block at all as a basis for constructing whatever. As you continue, the amount of possible blocks you can use drastically decreases - for example, you can't place a 2x8 brick over a slope - if you do, the rest of the block will be hanging off the side. If you used the slope block as your first block, your options for what block to place are limited. If you used a brick as your first option, you can keep stacking bricks until it ends in a slope again.

In this sense, we defined a rule and an effect for that rule.

  1. Place an initial 2x8 brick or slope block.
  2. If there are free 2x8 bricks remaining, we can stack more 2x8 bricks or place a slope block.
  3. If you place a slope block, that ends your construction.

Now imagine Obsidian carving out rooms in a similar manner.

How Obsidian Does It

Obsidian's shape rules can be found in games/<game_name>/shapes.lua.

Each rule at minimum contains a probability and a structure. An example:

GROW_SAMPLE_RULE =
{
  prob = 50
  
[[This rule essentially tells Obsidian to look for an area
with two bare floor spaces, and expands it 3-seeds 
(see Obsidian glossary - a seed is a 128x128 grid section) farther.]] 
  structure = {
    "..","11"
    "..","11"
    "..","11"
    "11","11"
  }
}

The structure contains a matrix of symbols, signifying rooms and their elements seen from above. The left side of the structure is the matching pattern, the right side is the change to be made if this rule is picked.

The name of this rule is also significant - Obsidian identifies what sort of action to perform given the elements by checking if the name has the string "ROOT", "EXIT", "GROW", "SPROUT", and "DECORATE".

Rule Patterns and Elements

Here are the various symbols used in both the pattern match and effect, as far as they can be known via example.

Basic Elements

  • . - this element is free - it has nothing in it (no room, no stairs, no whatever).

  • ! - this element is strictly free.

  • ~ - this element is liquid.

  • # - this element is disabled. Usually used only in the effect side. This element forces this area to be walled-off, and the shape grammar cannot open it again.

  • = - this element is a bridge. A bridge prefab will later fill in this entire area. (Currently only found in park outdoors...)

  • 1, 2, 3 - this element is an open room. (Only 1 is actually used, but the other numbers can probably also be used. The numbers are likely used for matching between specific rooms - think a puzzle piece where the 'socket' is different in one side. i.e. a 2 socket can only be plugged-in by another 2)

Staircase elements

  • <, V, >, ^ - this element is a staircase. Must be strictly used in conjunction with 'A' in that the staircase must end in an A element. See the A element below.
  • S - this element is a staircase, but without a defined direction. (there's no example on how to use this, but this was probably used for things like spiral staircases, as the code comment suggests defining some sort of info table for it)

'Magic' (match-only) elements

  • x - This element is a wildcard. It can be anything, such as an open space or walled off. It is usually used to allow shapes to have some wiggle room in connecting to others. If this element is used in the pattern match side, it cannot be changed and must also appear as-is in the same place in the right-hand side. There are other match elements but they don't have examples in use.

Special elements

  • A - effect-only. This element specifies the beginning of a new area in the same room. A new area possesses new floor, ceiling heights, textures, etc.
  • R - effect-only. This element specifies the beginning of a new room entirely. The new room will have different wall textures, monster setups, possibly locked behind a quest (switch or key), can become a cave, outdoor area, etc. This is usually used to specify a new room entirely exists behind a joiner or so.
  • C - this element is to be a monster cage.
  • J - this element is to be a joiner. Joiners are eventually replaced by joiner prefabs.
  • T - this element is a closet. Secrets, item closets, exit closets, trap closets, and start closets are eventually rendered in place.

Diagonals

  • / - this element specifies this is a diagonal transitioning between two kinds of elements. (running from lower-left to upper-right)
  • % - this element specifies this is a diagonal. (running from upper-left to lower-right)

Diagonals require specification on what the diagonal transitions to by declaring a diagonals = {} struct. For example, assume the following room:

structure =
{
  ".....","11111"
  ".....","1/~%1"
  ".....","1~~~1"
  ".....","1%~/1"
  "x111x","x111x"
}

This room specifies four diagonals - the diagonals struct will probably look like this:

diagonals =
{
  "1~","~1" -- Specify the diagonal's attributes in the order of them being read left-right, top-down.
  "1~","~1" -- This means the first diagonal will transition from an open room to a liquid.
}

Rule Passes

  • ROOT - Obsidian will pick a ROOT shape first to spawn a "trunk". A trunk is the start of a fully connected series of rooms.
  • GROW - Obsidian will use this rule to grow new shapes from a ROOT and other GROWn shapes to mutate from a room's space further. GROW rules will operate over each other to build a room until it has reached its target size.
  • SPROUT - Obsidian will use after GROW. Sprout rules are usually reserved for the creation of joiners, allowing a room to escape into a new room and reserving the space beyond for said room's eventual growth.
  • DECORATE - Obsidian will use after SPROUT. Decorate rules are usually reserved for the creation of closets. Closets are relatively large multi-seed rectangles reserved for seed prefabs such as start and exit closets, pictures, wall cages, and so on.
  • FILLER - These rules are reserved for parks or caves. They are simply large generic shapes intended to create and connect spaces for the parks and caves code which run independently from Shape Grammar style generation after the GROW pass.
  • SMOOTHER - These rules are used as a final pass touch. They are used to bevel corners into diagonals (smoothing them out) for outdoors.
  • EXIT - Exists, but not actually used. Exit prefabs are instead placed on closet spaces created by the DECORATE pass. Likely intended from Oblige 7.7 to generate a map backwards from the exit in order to prioritize the creation a large space for boss fights.
  • STREETS - These are rules intended for Streets Mode and are used to layout the actual street areas. This occurs entirely in place of the GROW pass.
  • SIDEWALK - These are rules intended for Streets Mode and are used to generate extra areas around the initial streets to carve up more space outside of the road itself. SPROUT and DECORATE occur after this step in Streets Mode.
  • SQUARE_OUT - These rules are used to mutate a room's shape by morphing certain areas into wider, more square-ish space.

Think of an artificial Christmas tree - you place the base first (ROOT), attach the stem and branches from there (GROW), attach the plastic leaves (SPROUT), attach the lights and Christmas balls (DECORATE), and add a star on top as a finish. (EXIT)

Tips and Common Errors

Shape Openness

Much like the example regarding Lego slope bricks above, it is best for shapes to have as many plain borders as possible so it has more possibilities for matching against more shape rules. Obsidian can accidentally close off a room with a dead ends, forcing the map to become smaller (and therefore terminate early)

No Outdoor Liquid Borders

Liquid tiles apparently cannot be used in the edge of any right-hand side of a rule unless the rule is strictly used indoors (env = "!outdoor"). Obsidian doesn't seem to support resolving how a liquid from another room borders against an outdoor park. For a liquid tile to be used for outdoor shapes, it must be closed off from the edge by an extra non-liquid space.