I've always had a little bit of an urge to try out game development. It wasn't something I associated with Haskell in the past. But recently, I started learning a bit about game architecural patterns. I stumbled on some ideas that seemed "Haskell-esque". I learned about the Entity-Component-System model, which suits typeclasses rather than object-oriented design.
So I've decided to do a few articles on writing a basic game in Haskell. We'll delve more into these architectural ideas later in the series. But to start, we have to learn a few building blocks! The first couple weeks will focus on the basics of the Gloss library. This library has some simple tools for creating 2D graphics that we can use to make a game. Frequent readers of this blog will note a lot of commonalities between Gloss and the Codeworld library we studied a while back. In this first part, we'll learn some basic combinators.
If you're looking for some more practical usages of Haskell, we have some tools for you! Download our Production Checklist to learn many interesting libraries you can use! You can also read our Haskell Web Skills series to go a bit more in depth!
A Basic Gloss Tutorial
The get started with the Gloss library, let's draw a simple picture using the
display function. All this does is make a full screen window with a circle in the middle.
-- Always imported import Graphics.Glass main :: IO () main = display FullScreen white (Circle 80)
All the arguments here are pretty straightforward. The program opens a full screen window and displays a circle against a white background. We can make the window smaller by using
InWindow instead of
FullScreen for the
Display type. This takes a window "name", as well as dimensions for the size and offset of the window.
windowDisplay :: Display windowDisplay = InWindow "Window" (200, 200) (10, 10) main :: IO () main = display windowDisplay white (Circle 80)
The primary argument here is this last one, a
Picture using the
Circle constructor. We can draw many different things, including circles, lines, boxes, text, and so on. The
Picture type also allows for translation, rotation, and aggregation of other pictures.
We can take our drawing to the next level by using the
animate function. Instead of only drawing a static picture, we'll take the animation time as an input to a function. Here's how we can provide an animation of a growing circle:
main = animate windowDisplay white animationFunc animationFunc :: Float -> Picture animationFunc time = Circle (2 * time)
The next stage of our program's development is to add a model. This allows us to add state to our animation so that it is no longer merely a function of the time. For our next example, we'll make a pendulum. We'll keep two pieces of information in our model. These are the current angle ("theta") and the derivative of that angle ("dtheta"). The
simulate function takes more arguments than
animate. Here's the skeleton of how we use it. We'll go over the new arguments one-by-one.
type Model = (Float, Float) main = simulate displayWindow white simulationRate initialModel drawingFunc updateFunc where simulationRate :: Int simulationRate = 20 initialModel :: Model initialModel = (0,0) drawingFunc :: Model -> Picture drawingFunc (theta, dtheta) = … updateFunc :: ViewPort -> Float -> Model -> Model updateFunc _ dt (theta, dtheta) = ...
The first extra argument (
simulationRate) tells us how many model steps per second. Then we have our initial model. Then there's a function taking the model and telling us how to draw the picture. We'll fill this in to draw a line at the appropriate angle.
drawingFunc :: Model -> Picture drawingFunc (theta, dtheta) = Line [(0, 0), (50 * cos theta, 50 * sin theta)]
Finally, we have an updating function. This takes the view-port, which we won't use. It also takes the amount of time for this simulation step (
dt). Then it takes a current model. It uses these to determine the new model. We can fill this in with a little bit of trigonometry. Then we'll have a working pendulum simulation!
updateFunc :: ViewPort -> Float -> Model -> Model updateFunc _ dt (theta, dtheta) = (theta + dt * dtheta, dtheta - dt * (cos theta))
Playing a Game
The final element we need to make a playable game is to accept user input. The
play function provides us what we need here. It looks like the
simulate function except for an extra function for handling input. We're going to make a game where the user can move a circle around with the arrow keys. We'll add an extra mechanic where the circle keeps trying to move back towards the center. Here's the skeleton:
type World = (Float, Float) main :: IO () main = play windowDisplay white 20 (0, 0) drawingFunc inputHandler updateFunc drawingFunc :: World -> Picture drawingFunc (x, y) = ... inputHandler :: Event -> World -> World inputHandler event (x, y) = ... updateFunc :: Float -> World -> World updateFunc dt (x, y) = ...
World will represent the current location of our circle. The drawing function will draw a simple circle, translated by this amount.
drawingFunc :: World -> Picture drawingFunc (x, y) = translate x y (Circle 20)
Now for our input handler, we only care about a few inputs. We'll read the up/down/left/right arrows, and adjust the coordinates:
inputHandler :: Event -> World -> World inputHandler (EventKey (SpecialKey KeyUp) Down _ _) (x, y) = (x, y + 10) inputHandler (EventKey (SpecialKey KeyDown) Down _ _) (x, y) = (x, y - 10) inputHandler (EventKey (SpecialKey KeyRight) Down _ _) (x, y) = (x + 10, y) inputHandler (EventKey (SpecialKey KeyLeft) Down _ _) (x, y) = (x - 10, y) inputHandler _ w = w
Finally, let's write our "update" function. This will keep trying to move the circle's coordinates towards the center of the frame:
updateFunc :: Float -> World -> World updateFunc _ (x, y) = (towardCenter x, towardCenter y) where towardCenter :: Float -> Float towardCenter c = if abs c < 0.25 then 0 else if c > 0 then c - 0.25 else c + 0.25
And that's it, we have our miniature game!
Hopefully this article gave you a good, quick overview on the basics of the Gloss library. Next week, we'll start making a more complicated game with a more interesting model!