|
Functional Programming and Game Design |
Been a while since I wrote a blog. I've been busy learning my kanji, which doesn't really deserve any sort of reflection or even updates (I'm at 1800, by the way). I'm getting kind of exhausted with the kanji thing now, so I'm pulling back on the speed of learning in order to concentrate on reviewing, and to give myself more time for other projects. Like this one.
To give a brief overview of functional programming, it is basically a methodology for writing code which has no side effects. That is, it doesn't change the state of the program. Rather than modifying objects directly, it returns modified copies of objects (like strings in many programming languages). Another benefit is that functional programming is deterministic. If you enter the same parameters to a function, you should always get the same output.
The benefits to functional programming are extremely strong. First, it's easy to debug and produces fewer errors. Since a function exists inside its own tiny little pocket universe, it makes things like unit testing really simple and obvious. Finding bugs is as simple as keeping track of the input and making sure the correct output is being produced. You can't, for example, change a variable on an object being used somewhere else, thus causing a bug in a completely different part of the code. If a function has a bug, it is in the function (unless you do something like overflow a buffer in C, but even the effect of that should be somewhat minimized).
Another big advantage of functional programming is that, if it is deterministic, it doesn't matter when or where the function is performed. Same inputs yield same outputs, so whether you perform it here or there, the result is the same. Some functional languages like Haskell use this to perform lazy evaluation (only evaluating functions when the results are used, leading to the ability to perform operations on infinite arrays and stuff like that).
A similar advantage is that functional code is already concurrently ready. There's no need for locks since a functions never touch anything outside of the function. The most concurrent languages out there (Erlang, Clojure) are functional.
The long and short of it is, the more functional your code is, the better. But it can't all be functional - especially with games - as side effects like getting input or changing the pixels in a window are more of less fundamental. Functional programming is also largely at odds with object orientated programming (something that is at once a great success and a failed experiment), which is built around the mutability of objects.
My personal interest in functional languages comes from the simple alien nature of it all. I've been programming for twenty years, but I've never really done a functional language. While it appears to be a superior way to solve a lot of problems, for my own personal satisfaction, it is novel and it forces me to change how I would otherwise solve simple problems. Like game development.
If you were to write a mostly functional game, nearly everything you take for granted changes. For instance, the basic approach to doing a game update would be to pass in the (immutable) previous game state with the game input and then pop out a new (immutable) game state. For a turn based game, this is actually easy. For a real time game, I'm honestly not sure what you'd do.
My best guess at this time is that most of the objects will be deterministic within their own states. So you can predict the location of a monster in the jumping state as long as you have the initial values for that state and the start time. You can pass in the current clock, figure out the duration of the jumping state, and then figure out the position based on that. To maintain a series of immutable game states, you'd only need to update with a new game state when objects change to a different state.
For a simulation, like Sim City or The Settlers, that means that the entire game is deterministic and only changes when the player inputs a command. You'd want to keep getting new game states as the sims change states, but even that can be predicted by stepping through the simulation. But when you've got a multiplayer game where each player can greatly affect the simulation (like an RTS with 6 players, each one sending commands to dozens of units a second), it feels like it would be even more super complicated. In other words, I'm not sure how to deal with change - the more change there is, the more difficult the problem becomes in my eyes.
On the bright side, my Three Hundred mechanic #009 - Time Shock is perfect for a functional game. There's only one move per turn, meaning that the game state changes according to only one input at a time. Keeping track of the previous immutable game states is practically part of the design, as you are creating a tree of dead and active game states every time you create a time shock. The long and the short of it is, the only non-functional pieces you'd need would be collecting the next move's input and then displaying the game on the screen.
I really feel like functional game design would also be appropriate for a sim-like game (#029 - Tiny Crawl World?) would be well served with functional programming - there's precious little player input. I have no idea how I'd go about doing something like that though.
One thing I've determined that functional programming is perfect for, however, is procedural content generation. I've always sort of been on this side, but when it came down to implementing my Blueprints system, I wavered considerably on the issue of mutable data. I really liked the idea that I could set variables randomly and then create other values based on them, but the only way I could figure out how to do that was to introduce variables - which also required constructors and destructors.
But my vision of a procedural game world hasn't changed. The whole thing is a tree. A world is made up of attractions (towns and dungeons), which are made up of rooms and floors, which contain encounters built from enemies, carrying items and weapons. You could describe a procedural game world as a Lisp expression: (world (town (shop (inventory shopkeeper) (house (floorplan occupant)) ...)).
I always wanted the process to be deterministic, but the entire extent of that is how I treated the random seed. You see, your typical random number generator has a side effect of updating the random seed after each call. Because of this, the order in which you get random numbers matters. I didn't have a solution for the order than blueprints were evaluated (it was largely defined by the blueprint's dependencies on various other properties), but I had hoped that if I simply started with a specific random seed at the top level, then the process would be the same throughout - but then the problem happens that you have to recreate the entire world every time.
You couldn't for example, recreate a dungeon floor-plan without populating it with game entities, because that would affect the random number generator's output. At the very least, you'd have to create an entire branch extending from a node instead of just parts of a node - which means that you couldn't generate an overworld map with locations of all the dungeons without creating the dungeons as well.
One solution to all this is to pass the seed to each function and keep the process entirely self contained and deterministic based on the values passed to it. The function for creating a world does not affect the random number values for creating a dungeon. Making each step as self contained as possible.
One of the side effects of this is that the process changes slightly. The goal of the blueprints phase is to create that tree representation of the game world. It needs to define all the inputs to each function and keep them in a tree itself. If we know the inputs, we can recreate any node or subtree at will. So the blueprints aren't used to create the objects themselves. They are used to create a sort of abstract syntax tree of a game world. That tree is then used to create the game objects. So, you don't pass a blueprint to a generator - you pass the root node that a blueprint should create.
It's at this point that I realized that the blueprint process isn't perfect. Rather than thinking about blueprints as finished objects, I needed to think about them as recipes. That is, they are functions themselves, and they accept various inputs - not the least of which would be a random seed. For instance, you could pass in a difficulty value that would affect how the blueprint defines its values. You could pass in a set of rooms for a dungeon to set up. Though the blueprints could define these things by themselves, it is much more interesting to think of a player being able to pass these things to a blueprint (such as #059 - PGC Cards #3).
I haven't quite figured out how this new way of looking at things will change things. But it seems like I'm looking at a multi-stage process defined by multiple different trees. The blueprint tree stage can be something where a player defines the inputs to the blueprints, or the blueprints themselves have things hardcoded. I'm thinking that there's be a series of inputs which could be grayed out when they are set in stone by the blueprint. In situations where you want no player interaction, all inputs would be defined by the blueprints. In situations where you want player interaction, you would leave those inputs open for the player to fill at will. There would need to be some way for blueprints to check the validity of each input before the process begins, I think.
From the blueprint tree, a different tree is created - which represents a single, immutable set of inputs into the various generators. Because the inputs are retained, any piece of the abstract syntax tree could be recreated in full or in part - a distinction that blueprints couldn't have previously made as they were before.
|