|
Procedural Design Patterns, Part 1 |
This article has been depreciated, as I've discovered a far better way to do things.
My goal with this article is to sort of consolidate the different ideas
I have for procedural generation. I want to call them Design Patterns, which they technically are (or would be with a little cleaning up), but in dealing with procedural design, patterns take on a more concrete and literal sense. If the words "design pattern" is throwing you, think of them as technologies or even design philosophies.
This first collection concerns itself with the primary patterns of procedural design: Blueprint, Template, and Generator.
A blueprint is an immutable design which describes a particular implementation of an object, described through a set of named key-values (usually a hash table or dictionary collection). For instance, the key "hitpoints" will have the value "20". When creating a new object, you can pass the blueprint to initialize it. Reusing the same blueprint should produce the exact same base object every time.
A blueprint by itself may seem redundant or obvious, but chances are you are already using blueprints in your code. Many RPGs are driven by giant tables that describe the data for the different equipment and enemies in the game. These are just blueprints - instructions on how to create multiple versions of each object. Ever short sword you encounter may be its own object, but somewhere there is a data-driven set of instructions of how to create that short sword over and over again.
The important thing to take away from this is that a blueprint is the data needed to create and recreate the same object over and over again. It is not the object itself.
Why Is This Important?
Procedural content generation is largely about information. It's about storing it, inspecting it, changing it, creating it, and design it. The Blueprint is the basic building block as it is the last object in the design process. I may seem like the object itself is where the buck stops, but once you have a blueprint, the design work is done. If you have a large enough blueprint (itself made up of multiple blueprints describing its components), you can create the exact same game every time. The game's job is just to read the blueprint and build itself accordingly.
The goal of procedural design is to create blueprints, not objects.
Give Me An Example.
| Name: | Short Sword |
| Class: | SQGenericMeleeWeapon |
| Damage: | 14 |
| Description: | It is a sword and it is short. Ugly too. |
A blueprint is immutable. That is, once it has been created, it can not be changed. If you want to change it, you essentially have to create a new blueprint based on the old one with the values changed. But a large part of procedural design is in creating not one blueprint, but thousands of them.
A template is similar to a blueprint, but rather than describing how to build an object, it describes how to build a blueprint. Or rather, a class of blueprints. If the same blueprint will build the exact same object every time, a template will build a different blueprint every time.
Take a level generator, for example, where you can specific the randomness of the hallways and rooms. A maze-like level may contain many hallways, while a more traditional living space may consist largely of rooms. The difference between these two types of levels may be as little as changing the number next to the Room Frequency variable. Values between 0 and 10 produce mazes, while values 50, 60, and 70 produce rooms connected by hallways, and the static value of 100 is a level made of nothing but rooms.
If you think about those three different ranges as three class of level types, you can also think of them as three different templates. A template gives potential values, whether it be a range of values (0 to 10), a set of values (50,60,70) or even a static value (100). And each time a template is asked to create a blueprint, one of those potential values is chosen. In the case of the maze template, it will be a different number between 0 and 10 each time.
Why Is This Important?
Templates represent a way of controlling input values and defining classes of blueprints. A single generation algorithm can create many different types of values, but not all of them are playable or predictable. A purely random approach to input values will not yield satisfactory results. However reusing the same generator using different template classes can control the output. More importantly, it can organize it.
A game like Diablo II can be described ENTIRELY out of templates. In fact, I'd wager that 100% of the procedural decisions made in that game can be described using the potential values of a template. That's how powerful templates are. The proper design and implementation of them can literally be all you need to describe a randomly generated game.
Give Me An Example.
| Name: | TSword |
| Class: | (SQGenericMeleeWeapon, SQMagicMeleeWeapon) |
| Damage: | (1...23) |
| Magic Damage: | (1...4) |
| Magic Type: | (fire, water, wind, earth, snot) |
| Icon: | (sword1.png, sword2.png, sword3.png) |
A generator is a process which takes a set of inputs and produces a predictable output. A random number generator is an example. It takes a single input (the random seed) and returns a predictable pseudo randomly generated whole number between -BIG_NUMBER and BIG_NUMBER (and technically, a new seed). A map generator may take multiple inputs (the number of rooms, how big each room can be, how long the hallways are, what kind of rooms there can be, etc) as well as a random seed. A room description generator may use a bunch of objects as inputs, transforming those objects into a textual description.
So long as you pass the same inputs, you will receive the same output.
Generators are essentially the decision making part of procedural design. If you have a set of three values (1,2,3), how does the computer know which one to select? Is the selection purely random? Is it weighted? Should it pull them in numerical order, or in the order they were added to the set? Each one of these decisions is a different algorithm.
All those potential values in the templates are really intended for specific generators. For instance, you declare a range of numerical values and that will return a random number in that range. Do you want it to be whole numbers or floating point decimals? You can declare the generator along with the values in the template. For example:
Damage: RandomIntInRange(1, 23)
Damage: RandomFloatInRange(1.0, 1.99)
Every decision that is made in procedural design can be encapsulated by a particular generator. Everything from picking a random number to generating a level to placing enemies in a level to selecting what magic effect goes on a weapon can be seen as a generator. The majority of generators will be small selection algorithms, like picking a random card from the deck.
Though generators are used primarily for template definitions, they can also be used in object creation. A blueprint is a collection of named values which can be used for generators as inputs, and since the same input produces the same output, the same blueprint should produce the same generator output as well. A blueprint for a level may contain a dozen blueprints of the things which make up the level, including a blueprint for the level generation algorithm.
Why Is This Important?
The impetus behind singling out generators is largely just so that they can have names. A very large and important part of procedural design is making these tiny, apparently random examples. Classifying them allows us to realize where they are being used (rather than a simple rand() buried in the code), keep logs of them for debugging (you'll NEED this later on), or even describe them in a template. Templates are sort of built on this, actually.
The difficulty in procedural design is about declaring the things you need to do before you do them. A lot of randomly generated games include their random aspects in the middle of code for something else, frequently a simple rand() call in the middle of an initializer method somewhere. If one can outsource all the algorithms into generator objects, you can describe this behavior at a higher level, even making it plug-and-play.
Give Me An Example.
Name: TakeFromSet
Inputs: Random Seed, Set of objects
Output: New Random Seed, Object, New set.
Description: Will select a random object from a set. Randomness is unweighted and based on passed random seed. Returns a new seed and object, as well as a copy of the input set with the selected object now removed.
A Note On Random Seeds
Generators are entirely predictable, but their purpose may be to emulate random selection. Of course, random numbers are anything but random, something we can use to our advantage here. Any generator which needs to emulate randomness will have a random seed passed to it. The same seed will produce the same, predictable set of "random" numbers. Thus blueprints will need to keep track of random seeds. Templates, however, can pick a somewhat unpredictable number (like the number of ticks since the computer booted up) to produce unpredictable numbers. The generator doesn't care how the seed is arrived at, but it is important enough that the procedural designer is always aware of where that input value came from.
Any generator which takes a random seed as input will return a new random seed as output. This new seed can be passed to the next generator, which will produce the input seed to the generator after that. By daisy chaining random seeds like this, we only need to keep track of the first random seed in a blueprint.
|