This is the second part in an ongoing series about building a roguelike game in Common Lisp. The first part is here.
This corresponds to Part 2 in Trystan Spangler’s tutorial and in Steve Losh’s version.
The code for this part is available here.
Summary
We’ll talk a bit about the structure of the game: the game loop, scenes, and rendering.
We will set up four different “scenes” to the “game”, and have a way to “win” and “lose”. At the end of this section, the game will look like this:
The Game Loop
Virtually every video game has a simple loop that goes like this:
setup()
while not game_over:
render(game_state)
input = get_user_input()
update(input, game_state)
Each pass through this loop is called a frame. Most games accept input asynchronously, but roguelikes are synchronous games and work very simply. They render the game state, then wait for the user to press a single key, change the game state based on that input, then they loop.
Depending on what state the game is in, the same input may have different results. During normal play, pressing the up key may move your character up one tile. On the title screen, it may select from a different item, or be the first character in a cheat code.
To keep these very different sets of display logic and state update logic separated, we introduce a concept called Scenes.
Scenes
Scenes are another fairly universal concept in game development. Usually, a game will have a title screen scene, a game over scene, a gameplay scene, maybe a map, etc.
In our program, like Trystan’s, we use objects to represent the scenes. We use generic methods to dispatch display and input handling functions.
The Code
Game Loop
The outer loop of our program looks like this:
(defun gameloop () (let* ((window charms:*standard-window*) (scene (initialize))) (loop named :game-loop while *running* do (progn (charms:clear-window window) (display-scene scene window) (setf scene (handle-input scene (charms:get-char window :ignore-error t))))))) (defun main () (charms:with-curses () (charms:disable-echoing) (charms:enable-raw-input :interpret-control-characters t) (gameloop)))
main
is the entry point into our program. There is a little bit of cl-charms ritual, but it mostly just sets up,gameloop
which handles the actual loop as described above.
At a high level, gameloop
calls initialize
to set up the initial state of the game. Then, it calls display-scene
and handle-input
in a loop until the game is over.
initialize
initialize
will be responsible for the initial setup of the game state. It also returns the initial scene for the game. For now, the only state of the game that we’ll store is whether or not the game is running so we can know when to terminate the game loop. We store this in a defparameter
called *running*
.
The initial scene of the game is a class we’ve called start-scene
. We will describe each of our scenes, and the methods available to them, below. We expect initialize
to set up the game’s initial scene and return it.
(defparameter *running* nil) (defun initialize (window) (setf *running* t) (make-instance 'start-scene))
display-scene
display-scene
is a defgeneric
that takes a scene (the current one, usually) and a cl-charms
window and renders that scene to the window.
handle-input
handle-input
is a defgeneric
which takes in the current scene and applies input (a keystroke) to it. It is expected to return a modified scene instance, which will become the new scene.
For now, all the implementations of handle-input
either return themselves, unmodified, or an entirely new scene. In the near future, we will store more state in the scene, and this data will be updated by handle-input
.
Scenes
The scenes in our program are represented as a class hierarchy. The topmost is simply start-scene
. Our interface is two defgenerics
, display-scene
and handle-input
.
For now, the scene has no slots. In the future, perhaps we will use the scene to hold the game state.
(defclass scene () ()) (defgeneric display-scene (scene window)) (defgeneric handle-input (scene key))
We will have four specialized scenes in our game:
- A title scene, called
start-scene
, which will explain the game, display copyright info, etc. - A gameplay scene, called
play-scene
, which will show our dungeon map, our player, and the game as it is being played. - A gameover scene for having won the game called
win-scene
- A gameover scene for having lost, called
lose-scene
start-scene
uses display-scene
to display a rudimentary title screen. It has two valid input keys: Space, which starts a new game by initializing a play-scene
; and Q, which will terminate the game by SETFing *running* to nil.
(defclass start-scene () ()) (defmethod display-scene ((s start-scene) window) (draw-string window 0 2 " CORYS ROGUELIKE") (draw-string window 0 4 " Press [Space] to start") (draw-string window 0 5 " Press [Q] to quit")) (defmethod handle-input ((s start-scene) key) (case key (#\Space (make-instance 'start-scene)) (#\Q (progn (setf *running* nil) s)) (otherwise s)))
play-scene
will soon be the meat of the game, but for now, it just shows a message and the “game” consists of pressing either Escape to win (by switching to a win-scene
) or Enter to lose (by switching to a lose-scene
).
(defclass play-scene () ()) (defmethod display-scene ((s play-scene) window) (draw-string window 0 0 " You are having fun.") (draw-string window 0 1 "-- press [Esc] to lose or [Enter] to win --")) (defmethod handle-input ((s play-scene) key) (case key (#\Escape (make-instance 'win-scene)) (#\Newline (make-instance 'lose-scene)) (otherwise s)))
win-scene
and lose-scene
both simply display a message and wait for the play to press Enter to instantiate and return a new start-scene
.
(defclass win-scene () ()) (defmethod display-scene ((s win-scene) window) (draw-string window 0 0 " !! YOU WIN !!") (draw-string window 0 1 "-- press [Enter] to restart --")) (defmethod handle-input ((s win-scene) key) (case key (#\Newline (make-instance 'start-scene)) (otherwise s))) (defclass lose-scene () ()) (defmethod display-scene ((s lose-scene) window) (draw-string window 0 0 " You lose.") (draw-string window 0 1 "-- press [Enter] to restart --")) (defmethod handle-input ((s lose-scene) key) (case key (#\Newline (make-instance 'lose-scene)) (otherwise s)))