This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.
In the three years we've been working on Ugly Baby, we haven't solved all of these problems, but we've had some success when combining simple, modular concepts to produce complex results.
Early successes in programmatically creating level skeletons in Aaaaa! were a boon, so we went back to those roots. It's actually pretty easy to create visually interesting things by taking simple structures and iterating on them. Take, for example, a sphere made out of cubes:
This time, instead of adding complexity to that algorithm, we took the output and iteratively modified it:
The end result doesn't look like a sphere at all:
What's so interesting to us about this simple concept is threefold: first, we can feed slightly different parameters in and get significantly different geometry out; second, in Ugly Baby, we can tie those parameters to the audio stream, so that the music drives the level's appearance; and finally, the modification pass is independent of the base structure, so we could just as easily apply the above to a grid of cubes and get something completely novel and (possibly) useful.
This was promising, so we formalized this. An Ugly Baby level generator consists of three types of modules:
Here's all that in action:
Step 1: The player flies along a linear path, so let's start by simply creating a simple column of blocks along the falling axis.
# Instantiate the column:
sequencer_column = sequencer.Column()
queue = sequencer_column.iterate()
Step 2: This level's like a rails shooter, so let's create something that looks more like a tunnel. Let's swap out the single column for six of them (essentially the edges of a cylinder). This takes some basic parameters, such as the vertical distance between blocks and the number of columns of blocks around the axis.
# Instantiate the cylinder:
sequencer_cylinder = sequencer.Cylinder(layer_delta=4, blocks=6)
queue = sequencer_cylinder.iterate()
Step 3: Apply a scale to every piece that the sequencer generates.
# Change every piece's scale:
mutator.scale(queue, [1, 4, 1])
Step 4: We wrote a mutator node that simply reorients a piece such that one side faces the Z- axis, and apply that to all pieces.
# Turn pieces to face the player's falling (z) axis:
Step 5: An "Every-N" selector node simply grabs one out of every N pieces fed into it. Here, we want to select every fourth piece and use a mutator to turn them red.
# Get a list of every 4th pieces that comes into the queue:
every_4th_piece = selector.every_n(queue, 4)
# Turn those pieces reddish:
mutator.set_color(every_4th_piece, [255, 32, 0])
Step 6: Finally, let's orient them sinusoidally over vertical distance.
# Pan from -45..45 depending on a piece's position along the player's falling axis:
mutator.cyclic_rotate(queue, freq=0.1, low=[-45, 0, 0], high=[45, 0, 0])