While writing the procedural design patterns articles, I had a sort of epiphany which pulled everything together. Partly, it was a manner of representing everything. I realized that the most important data structure I could have was a list, and upon that realization, saw that Lisp's function format may be the one stop shop I needed. The other half of the equation was in realizing what the connection was between a blueprint/template and a generator. While I originally thought that a generator would be anything that made a decision, a more appropriate metaphor would be something which designs. As such, the purpose of a blueprint is not to dictate those decisions, but instead dictate the domain those decisions take place. I also decided to drop the template name and stick with the blueprint metaphor.
Hopefully, this will make sense in a minute.
A generator is an object which designs another object. And by designs, I mean makes meaningful decisions about the make up and purpose of the object. A random number function isn't a generator. A dungeon designer, however, is. In general, every object type in the game which is created procedurally will have at least one generator.
Each generator takes as input a Blueprint, which I'll talk about in a moment. What it outputs differs on the generator and it'll be up to the program code or the generators themselves how this output will be used. A dungeon designer might return a complex Dungeon object which contains all the objects that make up the floors, entities, items, and actions that comprise that dungeon. A society generator may instead build another Blueprint, which is then fed into other generators.
A Blueprint is just a group of named expressions - an associative array, dictionary, or hashtable would be enough of an implementation. Blueprints are named and stored globally, so that they can refer to each other, and will generally store what type of Blueprint they are as well as which Generator they are intended for.
The purpose of a Blueprint is to dictate the domain of a generator. To give an example, a random number generator may be able to create a pseudo random number in the range of all possible real numbers. However, you are only interested in the numbers between 7 and 141. That's the domain.
Another way to think about it is collectable card games. The rules don't change, but the nature of the game that is played is dependent entirely on the make up of the two player's deck of cards. As new expansions are released, the set of total possible cards increases, but each individual game is still only concerned with what's in the two decks. Those decks represent the domain of the game, while the total number and variety of cards represents the domain of ALL games.
This domain is created, at its most simple, via static values. Think of it as a card game in which the decks are shuffled the same way every time such that the same cards always appear in the same order. Assuming the players play the best game they can, the result will be the same game each time it is played.
A larger domain is represented by an expression that presents different values every time it is evaluated, similar to shuffling a deck or even using a different one. The domain is simply all the legal, potential values of that expression.
It is in this way that domains represent a type of class of objects. Though they may differ from one individual to the next, taken together, their values all fall within the same specified, named domain. Going back to the card game, your deck can be said to be a class (in fact, in most CCG circles, specific decks are given names and status) even though it will result in a different game each time due to shuffling.
Perhaps most important is that Blueprints can define themselves in terms of other Blueprints. A deck of cards may be a Blueprint for a class of games, but those cards can, themselves, be Blueprints for a class of cards.
The expressions used to evaluate a named property on a Blueprint use a subset of Lisp. Basically, there are a few basic values (atoms, they are called, like numbers or strings) and one complex data type: lists. Pretty much everything is a list, including functions - the first element in a function list is the function name and everything else are parameters.
To give an example: (print (if (- (+ 1 2) 4) "yay!" "boo!")).
Yeah, Lisp uses parentheses like drug addicts use excuses, but it has several rather major advantages. The first is that it is easy to parse, easy to model, and easy to evaluate. It's all just lists. While parsing, you build a nested list that mirrors the input exactly. Evaluation is just a matter of evaluating each element in the list recursively (atoms evaluate to themselves).
The second advantage is that it is functional as opposed to iterative. The needs of a blueprint do not really require lots of iterative programming. Every property is a single expression, representing a single return value - even though that return value can be lists themselves.
Finally, the real advantage is that lists, and nested lists, represent a major part of defining a domain. There will be times when you'll want to specify a set of values personally, and then cross reference and delete objects from it based on another set of values. The real power and complexity of the Blueprint comes from lists. The fact that the functional aspects of the language can be expressed as lists just makes the system easier to build.
When a Blueprint is passed to a Generator, a special version of the Blueprint is created called a Master. If a Blueprint represents the entire range of potential values, a Master is just a single value in that range. In other words, all the named properties are evaluated down to their result. So that (randomInRange 1 10) is replaced with 4. A Master represents a single object, and as such, will have its randomSeed property set (if it hasn't been already).
A Master Blueprint is just a Blueprint where all the properties evaluate to themselves and has the random seed set. As such, it can be treated as a Blueprint in every way (albeit one with a very small domain). When a Master is asked to create a Master, it will simply return itself. Using a Master will return more predictable values than if each properties were re-evaluated every time.
I should point out that lists, even nested lists, which do not feature function calls will end up being left alone. As such, they can be used in Master Blueprints, or rather, can be transferred to Master Blueprints without problem. Some generators may use a list of possible values (like the cards in a deck) for selection. An obvious example would be a level generator that has a giant list of entities that can appear in the level. Each time it tries to create one, it'll pick an enemy blueprint from the list and pass it to an enemy generator (which will create a master, then return an enemy based on that master).
Property References - Blueprints should have the ability to refer to the properties of other known blueprints. For instance, an Orc Archer blueprint might set its strength to BasicOrc.strength. These are soft links that will be evaluated when a Master is created. Situations where two properties refer to each other, thus creating infinite recursion, should be watched for, but can also easily be checked.
User Defined Functions - Likewise, Blueprints might use properties that are not needed for a Master, to provide reusable elements. These properties should not be evaluated by themselves, nor become part of the Masters. Creating a set of function definitions separate from the named properties may be all that is required - though for functions to work, local variables and parameters need be implemented. I imagine this will create Blueprints which act primarily as object oriented collections of functions.
Global Variables - It will be necessary to create global lists to share between various objects. For instance, you might want to specify a set of specific Orc equipment that each Orc enemy uses a subset of. Or simply, you may need global variables for whatever reason. Much like the functions, I imagine that you can use named properties on a global Blueprint for this purpose (like OrcGlobals.equipmentSet). So it looks like object orientated programming, instance variables, and shared properties are already taken care of by the Blueprint model as is.
Blueprint Modifiers - It would be beneficial to have Blueprints with the sole purpose of modifying other blueprints. While a Blueprint might have the ability to refer to self, a modifier would instead refer to the object to be modified.
For instance, on a Weak modifier: strength = (* original.strength 0.75)
I'll have to consider modifiers in more detail. It could be that restrictions need to be put into place, such as only working on Master Blueprints or not being considered a Blueprint as to not accidentally get passed to a Generator by itself. Something I'll need to cover.
With the exception of how modifiers are implemented, I'm pretty excited with the way Blueprints have come together. I consider this a complete idea, ready for implementation. What's remarkable about this is that it doesn't appear to be particularly difficult to write. Once you have parsed the text into a list of expressions, the rest should be almost trivial to implement. And it is almost completely separate from any of the other code in a game, so that it can be developed and tested independently and without forward knowledge of how it will be used.
I know I've seen some comments in the forums about the various iterations of this blueprint/template concept with the main complaint that it only specifies which objects are to be used, not what to do with them. And that's something I should be addressing in future articles on this site. But I believe the problem of domain definition is a full half the equation, or at least it is for non-simple sets or when expandability is factored in. And to have an agnostic, universal solution to this problem is very exciting.
I'd really like comments on this. Things seem to fit together far too perfectly and I'm sure I've missed something. So pop on down to the forums or email me your comments.
|