The code for this part is available here.
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 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 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,
initialize to set up the initial state of the game. Then, it calls
handle-input in a loop until the game is over.
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
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 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 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
The scenes in our program are represented as a class hierarchy. The topmost is simply
start-scene. Our interface is two
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
- A gameover scene for having lost, called
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
(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)))
lose-scene both simply display a message and wait for the play to press Enter to instantiate and return a new
(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)))