Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Neversoft co-founder Mick West explores making your own mini-languages for games by creating Whimsy, a graphical DSL based on the abstract paintings of Parappa creator Rodney Alan Greenblat.
[In this technical article, originally published in Game Developer magazine, Neversoft co-founder Mick West explores making your own mini-languages for games by making Whimsy, a DSL that creates art based on the abstract paintings of Parappa creator Rodney Alan Greenblat.]
A domain-specific language (DSL, also called a "little language") is a language that's intended for a very specific use. It can be a programming language such as the Turtle aspects of LOGO that defines a very limited set of actions (drawing lines).
Alternatively, it can be a data definition language that encapsulates the representation of some presentable data, such as graphics or sound. HTML can be thought of as a domain-specific language as it's limited to describing the presentation of a web page.
This article looks at the potential uses of DSLs in games. I'll look at a specific domain and create a language for it, as well as discuss some of the problems I encountered.
A DSL differs from a general-purpose language in that the latter must support a large amount of functionality, including: variables, data structures, conditional expressions, looping constructs, and functions. General-purpose programming languages may also support various forms of abstraction, object-oriented programming, lambda expressions, and so on.
DSLs can be classified as either internal or external. An internal DSL is simply an extension of an existing general-purpose language. You can think of an internal DSL as simply being a set of functions, data structures, and conventions applied to an existing language, such as C++ or Ruby.
This set of functionally is still specific to one problem domain. A typical internal DSL might be one used to define state transitions for AI using a set of query functions and a switch statement. Many games have implemented this kind of system in the game code in C++, as AI is often the responsibility of a programmer rather than a designer.
An external DSL is an independent language that has been entirely created for this specific purpose. Generally, a DSL program will be a text file, which is then interpreted (or perhaps compiled) by some part of the game engine or tool chain.
Again, AI is a common usage of a DSL-when programmers hand off AI to a designer, they will frequently make it more data-driven, often to the extent that they supply a "little language" to script the AI transitions.
In experimenting with domain-specific languages for this article, I defined my domain as the works of Rodney Alan Greenblat, the artist responsible for the unique characters and world design in Parappa the Rapper, Um Jammer Lammy, and the upcoming Major Minor's Majestic March.
Greenblat has a large body of artwork with a very distinctive whimsical style. For my specific domain, I picked the artwork from his Elemental tour, a collection of semi-abstract paintings in a distinctive brightly colored and geometric style.
The idea was this: If such a style of artwork were to be used in a video game, then it might be very useful to have a DSL that encapsulated that style and allowed for easy creation of similar pieces for use in-game.
The first step in creating a DSL is to get a rough idea of the elements that the domain comprises. Looking at the Elemental works, we can see a number of common aspects. There are concentric oval shapes, with petals adjoined to various sections.
Many of the works have segmented circles with colored circles inside them. There are little propellers and various other shapes that repeat both within individual works and within Greenblat's overall collection.
Figure 1: Rodney A. Greemblat's "Lunar Module" is part of the domain for a new domain-specific language.
I decided the best way to approach creating this DSL would be to pick one piece and attempt to replicate parts of it. I chose the painting "Lunar Module" (see Figure 1).
Many common elements hold the piece in its style: solid circles, concentric ovals with color gradients, petals, and stars. Being even more selective, I isolated one corner of the painting, the purple box with blue petals (see Figure 2).
Figure 2: This detail from the lower left corner of "Lunar Module" serves as the basis for the code used to replicate its style.
For my first iteration, I decided on "build" rather than "extend." I wanted the creation process to be interactive so I could edit the "code" in real time and immediately see results. I was also not sure about the level of abstraction I was going to use, and I wanted the language to be very loose, unconstrained by syntax requirements.
I also wanted to have more control over the speed of execution, so I decided to build a language parser using C++. I decided to call this language Whimsy and use .whimsy as a file extension to identify programs in that language (for example, lunar.whimsy).
The initial exploratory coding was fairly straightforward. The code would read in a file, split it into lines, split the lines into tokens, and then simply parse the lines with a series of "if" statements; a segment of the code is shown in Listing 1. (The complete code can be downloaded here.)
Listing 1: Ad Hoc DSL Parsing Code
if (token == string("rectangle"))
{
debug_log("NEW RECTANGLE");
CWhim *p_whim = new CRectangle();
Add(p_whim);
if (!grouping)
m_parse_context.clear();
m_parse_context.push_back(p_whim);
}
if (token == string("at"))
{
float x = (float)atof(tokenizer.NextToken().data());
float y = (float)atof(tokenizer.NextToken().data());
m_parse_context.back()->SetPosition(Vector2(x,y));
}
At its most basic description, Greenblat's painting is composed of colored shapes. There are concentric shapes within shapes, and some shapes have other shapes attached to them. There are shapes that split other shapes, and shapes that are lists of other shapes. It seemed to me that some form of hierarchical list of shape objects was needed.
I created an abstract base class of shape called a Whim (class CWhim), and from this I derived an abstract ConvexShape object and then derived other shapes, collections of shapes, and sub-shapes from these base classes. The "world" of a painting is just a std::vector container of these objects.
After the obvious primitives of rectangles and circles, I needed a way to create the ovals. I created a CWhim called CSuperEgg. A SuperEgg is a term for the solid version of a SuperEllipse, which is a shape defined by the equation (x/a)r +(y/b)r=1, where a and b are the length of the axes, and r defines the curvature. (See Wolfram in Resources for a full explanation.) I added a SuperEgg keyword to the parser and a little bit of code to read the parameters, and created the object.
Equations like the one above produce very nice squared ovals, but they are too precise to match the more freeform ovals in the paintings. To create a less precise shape, I created a new "distort" object, which simply takes a parent object and overrides the render function to add some periodic noise displacement from the center point. At a low frequency, it simulates the hand-drawn look of the original and can be applied to any of the ConvexShape primitives.
Next I added the petals. These simply took a parent ConvexShape object and positioned themselves at specified points along the perimeter. All ConvexShape objects (including the distorted shapes) have a member function that returns points at a given angular or linear distance around the perimeter, so the attachment points and base profiles of the petals can be calculated easily.
The height of the petals is specified as a multiple of the width of the base. I found that as much as possible it was best to keep all numbers relative to some other object or part of an object, as it makes dependent changes far easier.
Finally, I added an Inner shape, which simply takes one ConvexShape and creates a copy of it inside, shrunk by a certain ratio and optionally distorted. Using multiple Inner shapes made it very easy to reproduce the concentric multi-colored ovals.
These few primitives allowed me to reproduce, in part, a segment of "Lunar Module" (see Figure 3). The code used to generate the mock-up is shown in Listing 2.
Figure 3: Reproducing the detail using SuperEgg, Inner and Petal primitives of the Whimsy language.
Listing 2: Whimsy Code
superegg 0.15,0.10,3.5 at .3,.7 size 1.2 black distort .01
petals 14 0.05 size 1.8 petalblue
inner .88,.01 tvpurple
superegg .1,.2,2 at .20,.7 size .4 distort .01 tvlime
inner .65,.01 tvyellow
inner .45,.01 tvlightyellow
superegg .1,.2,2 at .3,.7 size .5 distort .01 tvblack
inner .85 tvbrown
inner .80 tvred
inner .75 distort .03 tvorange
inner .70 tvyellow
superegg .1,.2,2 at .4,.7 size .4 distort .05 tvblue
inner .6 distort .2 tvdarkblue
inner .4 petalblue
This fairly short program gives a reasonable facsimile of a small segment of the painting. It's lacking the rectangular highlights in the petals, and the blue shape on the right is just a dummy, since I did not get around to triangle shapes. It's also missing other elements such as the legs and the streamer on the top right.
Clearly, a lot more has to be added to the language to be able to create the rest of the painting or the others in the series.
But already we have a quite powerful set of primitives. Although each set is only a few lines of code, you can get a variety of results from fairly simple code. This kind of power is something that often does not come up until you put a tool such as this DSL in the hands of an artist and give him or her a chance to play around with the parameters and the code. (Listing 3 and Figure 4 contain more examples of Whimsy.)
Listing 3: Five Whimsy Creations
superEgg .1,.05,4 at .2,.2 green distort 0.01
petals 8 blue
superegg .1,.1,2 at .5,.2 grey distort .02
inner .5 orange distort .02
petals 12 blue size 3
circle at .35,.35 size .05 distort .04 petalblue
petals 8 size 20 petalblue
superegg .1,.05,4 at .2,.45 white distort .1
inner .5 distort .01 yellow
superegg .1,.1,3.0 at .5,.45 brown
superegg .1,.1,2.0 at .5,.45 teal
superegg .1,.1,1.0 at .5,.45 darkblue distort .02
petals 20 .5 yellow size 3
superegg .1,.1,0.5 at .5,.45 darkgreen
petals 50 orange
superegg .1,.1,0.25 at .5,.45 purple
Figure 4: Some examples of the flexibility of the Whimsy primitives.
One rather surprising result was that I was able to duplicate the star-like objects using a tiny distorted circle with eight very long petals. Ideally these would have been created from four intersecting brushstrokes, but the distorted petals works pretty well.
The stars raise a central issue. There are 14 stars in the painting. Clearly it's inefficient to create each one by hand by cutting and pasting the same circle and petal code. You really want a Star function that can manage the position, color, and size parameters. We need some kind of macro or function definition functionality.
Although such functionality would not be particularly hard to add, if we had started out by extending an existing language rather than creating our own unique interpreted language, then we would already have this functionality, as well as looping constructs, expression evaluation, variables, and more.
I could quite easily have implemented the Whimsy programs in C++ as a series of function calls with very similar functionality. The problem is that I would have had to recompile and run the program every time I made a change. Right now, the results of a change are displayed the instant a file is modified, which provides a far more interactive experience, and rapid iterations.
I could have implemented Whimsy as an extension to a powerful scripting language, such as Ruby or Python. This would have allowed me to still have the near-instant feedback; but the downside is the syntax is more restrictive.
By implementing an original language, you can express it in any form you like. If you base it on Python, for example, you have to accept many of the restrictions of Python. Ruby is better, but it still comes with some syntactical constraints. There may also be performance implications of using a language like Ruby.
One reason I like the idea of defining an original DSL is that it can be more easily extended from a text-based editing tool to a graphical-based editing tool. Listings 2 and 3, for example, show that there are a lot of numerical parameters. These are tedious to change by hand, and it's a logical extension to allow some form of direct graphical editing of things like size and position.
You can think of this technique as a graphical interface to the text. GUI tools can be used to edit the relatively high-level DSL code, and still have it human readable (and editable). Used in this way, a DSL can be a useful intermediate step in tool development.
[EDITOR'S NOTE: This article was independently published by Gamasutra's editors, since it was deemed of value to the community. Its publishing has been made possible by Intel, as a platform and vendor-agnostic part of Intel's Visual Computing microsite.]
Read more about:
FeaturesYou May Also Like