Like many gamedev enthusiasts, I’ve only finished a handful of computer games and, perhaps not surprisingly, most of those were never released even on my homepage either. Usually the point of game development at an enthusiast or hobbyist level is not to create gaming masterpieces anyway, but rather to simply learn about the process and have fun working the code (or graphics, design, et cetera), and maybe even develop a few reasonably simple games.
Over the years I’ve spend quite a bit of time learning the gamedev concepts and recently I was able to apply some of them in a project, intended to test Haskell’s gamedev capabilities for hobbyist or small team game development (related post). Specifically I was interested to see how the difference between Haskell and Object-Oriented languages changed the design and implementation of the game code and engine, hopefully lending itself to the eventual development of languages tailored towards game development.
I set out to develop one of my favorite arcade remakes–yet another variation of Geometry Wars. The premise would be to keep the game as simple as possible, only the basic gameplay would be present with a couple of levels of progression. Enough to test all of the simple elements of gameplay ready for a more substantial project. As you may have guessed from the absence of the obligatory screenshot it didn’t work out as well as I expected.
Luckily I did pick up a couple of things while working on the game, and better yet I may even be able to rewrite the code and find that screenshot yet. It all comes down to a couple of quirks in Haskell’s syntax which didn’t blend well with my usual game architecture. Obviously the architecture would need to be modified to suite the idiomatic style of Haskell, but I was reasonably certain (and still are) that the architecture is the simplest implementation for both Object-Oriented languages as well as functional languages, based almost solely on the requirements of the high level game data structures.
The Haskell code for these structures was essentially based directly on records. The records originally contained only functional values, but I quickly realized how crazy that was, which lead me to the current design which uses records full of IORefs, at least for the mutable slots. (If its not immediately obvious why the IORefs are needed, it might help to know that game objects quite often need to reference each other simply because the operations are related between the two…bullets from the player ship for example.)
Finally, after working with this approach for a while it eventually sunk in that I could probably write the same code in Python in not only half the time, but also quicker for the same number of overall bugs. Not because the bugs are easier to catch in Python…they really, really aren’t. But because Python allows me to test many more variations of the game. At least more variations than Haskell seems to, all of which makes me wonder if I haven’t quite tapped into the idiomatic code still.
So I’m not quite out of ideas yet. I believe I can tweak my accessors to reduce the namespace damage caused by them (at the expense of some verbosity, however…why the hell hasn’t this been fixed yet anyway). And there’s even some clever usage of typeclasses which could simplify the use of common algorithms between game objects. If all goes to plan I’ll have some code to post next time…
– Lorenz
Tags: Gamedev, Haskell, Programming
For the longest time, I was baffled how something as complex as a game could be written in Haskell. Even writing a configuration file for a game (or any software really) seems, at first, to be an impossible task.
But later, as I worked with it, the big picture became clear. The idea in Haskell isn’t to eliminate IO. That’s impossible. A program made from pure functions is rarely useful. Instead, monads (and monad transformers) allow the programmer to put his or her own restrictions on how much of the outside world touches your program.
The first thing I’d do if I were writing any sort of complex game-looking program in Haskell would be to create a type to manage the state of your program: GameState. This type will be a monad and a member of MonadIO, to allow you give you access to the lovely Turing-based world on the other side of the void.
From there, you expose only the IO calls you need. If you need a timer in the game, then you provide for a function sleepMilli :: Int -> GameState (). But you probably don’t need general disk access for the game, so you hide openFile. The only time you’ll need to read and write files is for config files, maps, and saved games, but those simply become parameters for generating your game state object: loadGame :: String -> IO GameState, which takes the name of a save file and loads it.
Using a GameState object is also useful for maintaining mutable objects in your game. Replacing all the IORefs with STRefs or similar makes the guarantee Turing stays on the other side, while still providing you a ton of flexibility.
From there, all your generic algorithms: collision detections, state-based game AI, victory conditions are just functions of your game state.
I generally stick my entire program state in one data record (call it GameState, maybe) and then put that in an IORef. Then I can keep my code as purely functional as possible. I still haven’t figured out a good way to deal with interrupt-driven IO in an elegant way in Haskell.
Please, don’t use IORefs in this way. Instead, give objects unique IDs, and maintain a table of relationships between objects by their unique ID.
There’s a common notion among outsiders and new Haskellers that Haskell “can’t handle something as complex as X.” This is absolutely true if you’re using the IO monad to do work that is not IO.
> Please, don’t use IORefs in this way. Instead, give objects unique IDs, and maintain a table of relationships between objects by their unique ID.
IOW to turn the game state into something like a relational database. It’s good for serialization e.g. one could implement save game via “deriving (Read)”, but slow for lookups. It fits the idea of expressions quite well and the flexibility is great but for a real game wouldn’t it be better if relationships were by pointer under the covers? I imagine one would create a datatype that lies on top of the expression oriented fkey style database, similar to a view in RDMS terms. As a n00b this hurts my head but it seems like I’m thinking in the right direction. “Tying the knot” appears to be required in the general case.
http://haskell.org/wikisnapshot/TyingTheKnot.html
I don’t believe I’ve quite gotten to the point where I think I’m comfortable writing a more game specific type (monad or otherwise) yet, but it’s definitely an interesting prospect. It might actually have more far flung implications that you mentioned once you move the IO into higher level tasks, and just expose those to the game/engine type.
As I understand it the IO types are “more or less” just renamed ST types anyway, allowing true IO (externally controlled side effects) to be separated from the pure functional core. Since real side effects which are type constrained so they don’t escape a block of code are, from the outside at least, still referentially transparent and therefore functional. But your right, as the code base grows, some guarantees about IO could become essential.
Right now I’m actually more concerned with the overhead involved in using monadic code like readIORef. Eventually I think I’ll learn the operations involved in combining this kind of code better, but for now I think it might be more useful just to get something working and go from there.
Heh, that tying the know thing reminds of a post that appeared on planet haskell a while back…Infinite loops in Haskell.
I stumbled onto this post on the records syntax. I think it summarizes my issues with the syntax quite nicely.
Haskell Records Considered Grungy
Hi Lorenz, thanks for exploring this, please continue. I’ve looked at a few of the haskell games (on hackage) and one game framework (FunGEN) for ideas. It might be instructive to see and compare your evolving code attempts. The OO approach seems natural (and newspeak seems very promising) but natural-feeling functional solutions still seem possible. Maybe they will look like functional reactive programming. I wonder.
Yes, I will have to check those out at some point. And speaking of FRP, I actually considered it for my game project before I started this stuff, but not understanding Arrows and due to the frametime delta mechanism being a little strange (from my perspective) I decided to have a crack at a conventional engine first.
When I get to the postmortem however, I fully intend to have another look at the FRP architecture.
Look at FRP:
Reactive animations:
http://conal.net/fran/tutorial.htm
The above work set the corner stone of FRP.
The Yampa arcade:
http://www.haskell.org/yale/papers/haskell-workshop03/yampa-arcade.pdf
I think the Yampa arcade (and Yampa in general) are not quite functional as we’d like, but they are an interesting approach.
The idea of FRP is that pure functional values CAN be used to denote dynamic things.