Update after three months
I have an answer below using netwire-5.0.1
+ sdl
, in a structure of Functional Reactive Programming using Arrows and Kleisli Arrows for I/O. Though too simple to be called a "game", it should be very composible and very extendable.
Original
I am just learning Haskell, and trying to make a small game out of it. However, I would like to see what structure a small(canonical) text game can be. I also try to keep the code as pure as possible. I am now struggling to understand how to implement:
- The main loop. There is an example here How do I write a game loop in Haskell? but it seems that the accepted answer is not tail recursive. I am not exactly sure whether this matters. In my understanding, the memory usage will grow, right?
- State transition. I think this is quite related to the first one, though. I tried a bit using
State
, and something in http://www.gamedev.net/page/resources/_/technical/game-programming/haskell-game-object-design-or-how-functions-can-get-you-apples-r3204 , but although individual components may work and update in finite steps, I don't see how it can be used in an infinite loop.
If possible, I would like to see a minimal example which basically:
- Asks the player to input something, repeatedly
- When some condition is met, change state
- When some other contition is met, exit
- Theoretically can run for infinite time without blowing the memory
I don't have any postable code because I cannot get the very basic stuff. Any other material/examples I found on the web either use some other libraries, like SDL
or GTK
to drive events. The only one written totally in Haskell I found is http://jpmoresmau.blogspot.com/2006/11/my-first-haskell-adventure-game.html , but that one does not look like a tail recursion in its main loop too(Again, I don't know if it matters).
Or, probably Haskell is not intended to do things like this? Or probably I should put the main
in C?
Edit 1
So I modified a small example in https://wiki.haskell.org/Simple_StateT_use and made it even simpler(and it does not meet my criteria):
module Main where
import Control.Monad.State
main = do
putStrLn "I'm thinking of a number between 1 and 100, can you guess it?"
guesses <- execStateT (guessSession answer) 0
putStrLn $ "Success in " ++ (show guesses) ++ " tries."
where
answer = 10
guessSession :: Int -> StateT Int IO ()
guessSession answer =
do gs <- lift getLine -- get guess from user
let g = read gs -- convert to number
modify (+1) -- increment number of guesses
case g of
10 -> do lift $ putStrLn "Right"
_ -> do lift $ putStrLn "Continue"
guessSession answer
However, it will eventually overflow the memory. I tested with
bash prompt$ yes 1 | ./Test-Game
and the memory usage starts growing linearly.
Edit 2
OK, I found Haskell recursion and memory usage and gained some understanding about the "stack"... So is there anything wrong about my testing method?
Foreword
After 3 months of digging through numerous websites and trying out some small projects, I finally get to implement a minimalistic game (or is it?), in a very, very different way. This example exists merely to demonstrate one possible structure of a game written in Haskell, and should easily be extended to handle more complex logic and gameplay.
Full code and tutorial available on https://github.com/carldong/HMovePad-Tutorial
Abstract
This mini game has only one rectangle, which the player can move left and right by pressing Left and Right key, and that is the whole "game".
The game is implemented using
netwire-5.0.1
, withSDL
handling graphics. If I understand correctly, the architecture is fully functional reactive. Almost everything is implemented by Arrow composition, with only one function exposed inIO
. Therefore, I expect the reader to have basic understanding of the Arrow syntax of Haskell, since it is used extensively.The implementation order of this game is chosen to make debugging easy, and the implementation itself is chosen to demonstrate different usage of
netwire
as much as possible.Continuous time semantic is used for I/O, but discrete events are used to handle game events within the game logic.
Set up SDL
The very first step is to make sure SDL works. The source is simple:
If everything works, there should be a white rectangle appearing on the bottom of the window appearing. Note that clicking the
x
will not close the window. It has to be closed by Ctrl+C or killing.Set up Output Wires
Since we do not want to implement all the way to the last step and find that nothing can be drawn on screen, we are doing the output part first.
We need the Arrows syntax:
Also, we need to import some stuff:
We need to understand how to construct Kleisli Wires: Kleisli Arrow in Netwire 5?. A basic structure of a interactive program using Kleisli Wires is shown in this example: Console interactivity in Netwire?. To construct a Kleisli Wire from anything with type
a -> m b
, we need:Then, since I did not get
trace
to work under Arrow processes, a debug wire is made to print objects to console:Now it is time to write some functions to be lifted into wires. For output, we need a function that returns a
SDL.Surface
with proper rectangle drawn given the X coordinate of the pad:Be careful, this function does destructive updates. The surface passed in will be blitted onto the window surface later.
Now we have the surface. The output wire is then trivil:
Then, we put wires together, and play with them a bit:
Finally, we change
main
and drive the wires properly:Note that if you like, you can also make another wire to handle the main window surface too (and it is easy and better than my current implementation), but I was too late and lazy to add that. Check out the interactive example I mentioned above to see how simple
run
can get (it can get even simpler if inhibition is used instead ofquitWire
in that example).When the program is run, its appearance should be the same as before.
Here is the complete code:
Input Wires
In this section, we are going to construct wires that gets player input into the program.
Since we will use discrete events in the logic part, we need a data type for game events:
As comment suggested, the
Monoid
instance only applies for this particular game since it has only two opposite operations: left and right.First, we will poll events from SDL:
Obviously enough, this function polls events from SDL as a list, and inhibits when the
Quit
event is received.Next, we need to check whether an event is a keyboard event:
We will have a list of keys that are currently pressed, and it should update when a keyboard event occurs. In short, when a key is down, insert that key into the list, and vice versa:
Next, we write a function to convert a keyboard event to a game event:
We fold on the game events and get a single event (really, really, game specific!):
Now we can start making wires.
First, we need a wire that polls events:
Note that
mkKleisli
makes wire that does not inhibit, but we want inhibition in this wire since the program should quit when it is supposed to. Therefore, we usemkGen_
here.Then, we need to filter the events. First, make a helper function that makes a continuous time filter wire:
Use
mkFW_
to make a filter:Then, we need another convenient function to make a stateful wire from a stateful function of type
b -> a -> b
:Next, construct a stateful wire that remembers all key status:
The last piece of wire segment fires the game event:
To actively fire discrete events (netwire events) that contain game events, we need to hack netwire a bit (I think it is still quite incomplete) since it does not provide a wire that always fires events:
Comparing to the implementation of
now
, the only difference isnever
andalways
.Finally, a big wire that combines all input wires above:
An example of debugging is also shown in this wire.
To interface with the main program, modify
gameWire
to use the input:Nothing else needs to be changed. Well, interesting, isn't it?
When the program is run, the console gives a lot of output showing the current game events being fired. Try pressing left and right, and their combinations and see whether the behavior is expected. Of course, the rectangle will not move.
Here is a huge block of code:
"Game" Logic --- Finally putting everything together!
First, we write an integrating function of the X position of the pad:
I hard coded everything, but those are not important for this minimalistic example. It should be straightforward.
Then, we create the wire that represents the current position of the pad:
hold
holds at the latest value of a stream of discrete event.Next, we put all logic things in a big logic wire:
Since we have one state about the X coordinate, we need to modify the output wire:
Finally, we chain everything in the
gameWire
:Nothing needs to be changed in
main
andrun
. Wow!And this is it! Run it and you shou be able to move the rectangle left and right!
A GIGANTIC block of code (I am curious how long will a C++ program that does the same thing be):