The Emacs's Gamegrid library #3

Introduction

Previously, how to initialize the library and how to initialize the buffer have been explained.

What still needs to be shown is how to manage inputs from the player, and how to initialize the display, so that the buffer can show the graphics.

Since the former is easier to explain and also fairly quick to implement, I'll leave the display initialization last.

Terminating the game

When the player “dies” or explicitly wants to quit the current game, a number of steps must be taken: to begin with, the game loop must be stopped. However, as I said in a previous article, it's good practice to keep the buffer alive (burying it, at best) so that it's possible to start a new game from there.

This is done by changing the keymap back to the “null” keymap, like this:

(defconst my-gamegrid-game-score-file-name "gamegrid-game-scores")
(defvar my-gamegrid-game-score 0)
	
(defvar my-gamegrid-game-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "q") #'my-gamegrid-game-end-game)
    (define-key map (kbd "n") #'my-gamegrid-game-start-game)
    (define-key map (kbd "p") #'my-gamegrid-game-pause-game)
    (define-key map (kbd "a") #'my-gamegrid-game-move-left)
    (define-key map (kbd "s") #'my-gamegrid-game-move-down)
    (define-key map (kbd "d") #'my-gamegrid-game-move-right)
    (define-key map (kbd "w") #'my-gamegrid-game-move-up)
    map)
  "The in-game keymap.")

(defun my-gamegrid-game-end-game ()
  "End the current game."
  (interactive)
  (gamegrid-kill-timer)
  (use-local-map my-gamegrid-game-null-map)
  (gamegrid-add-score my-gamegrid-game-score-file-name
                      my-gamegrid-game-score))
      

The keymap contains the minimum to play a game: a key to end the game (q), a key to pause game (p), a key to start a new game while playing (n), and finally the keys to move the player character (w; a; s; d).

When ending the game (either with q or by meeting some condition defined by the game designer), the my-gamegrid-game-end-game function stops the game loop (i.e. kills the timer started by my-gamegrid-game-start-game), changes the keymap to the one with just “new game” and “bury buffer”, and finally saves the score to the file defined by my-gamegrid-game-score-file-name.

Having a score is not necessary: the example game I'm building in these articles don't use it, but it's a good idea to show how to do it nonetheless.

Since my-gamegrid-game-start-game calls my-gamegrid-game-reset-game, pressing n during the game is guaranteed to bring it to its initial state, thus starting a new game.

Shortly my-gamegrid-game-pause-game will be explained.

How input works

When Gamegrid needs to update the game state, it does so by calling a function inside an Emacs timer. This is an important concept to remember when processing player inputs.

The reason is, timers are not executed when Emacs is receiving inputs. As such, doing input processing directly inside the functions bound to keys in the keymap will block the game until every input has been processed.

This is poor user experience, and is particularily annoying in games with objects that “move on their own”, like the ball in Pong (though the version of Pong inside Emacs is a bit special).

We can find the solution in other Gamegrid games: the input functions (those bound to keys) will push something into a global list. Then, the update function will pop from this list and update the game accordingly.

This makes the input functions fast enough and the timer will not block because of them (of course, if Emacs is busy doing something else, it will block).

Given that, the input functions would be just these:

(defvar my-gamegrid-game-update-list ())
(defvar my-gamegrid-game-moved nil)

(defun my-gamegrid-game-move-left ()
  "Move the player left."
  (interactive)
  (unless my-gamegrid-game-moved
    (push (cons -1 0) my-gamegrid-game-update-list))
    (setq my-gamegrid-game-moved t))

(defun my-gamegrid-game-move-down ()
  "Move the player down."
  (interactive)
  (unless my-gamegrid-game-moved
    (push (cons 0 1) my-gamegrid-game-update-list))
    (setq my-gamegrid-game-moved t))

(defun my-gamegrid-game-move-right ()
  "Move the player right."
  (interactive)
  (unless my-gamegrid-game-moved
    (push (cons 1 0) my-gamegrid-game-update-list))
    (setq my-gamegrid-game-moved t))

(defun my-gamegrid-game-move-up ()
  "Move the player up."
  (interactive)
  (unless my-gamegrid-game-moved
    (push (cons 0 -1) my-gamegrid-game-update-list))
    (setq my-gamegrid-game-moved t))
      

The unless check (and the setq at the end), are necessary: without them, holding down the key would push and infinite number of conses into the global list.

Aside for potentially blocking Emacs, too many elements will move the player character too many times, which is not what the player wants. With the checks, elements are added only after the update function has been called.

Since the input functions merely push elements into the global list, pausing the game is as simple as preventing the update function from popping elements from the list. As such, to pause the game all it's needed is:

(defvar my-gamegrid-game-paused nil)

(defun my-gamegrid-game-pause-game ()
  "Pause the game."
  (interactive)
  (if my-gamegrid-game-paused
      (setq my-gamegrid-game-paused nil)
    (setq my-gamegrid-game-paused t)))
      

The my-gamegrid-game-paused variable will then be checked in the update function.

Updating the game

In this example game, all there is to do is move a colored square around the enclosed room.

The input functions push a cons on the global list, which indicates where to move the square next: the car is the x coordinate, the cdr the y. Each update, the values of the popped cons are added to the square current position.

So, let's first add the player to the room. It's just a quick addition to the end of my-gamegrid-game-init-buffer:

(defconst my-gamegrid-game-player 3)
	
(defvar my-gamegrid-game-player-x 4)
(defvar my-gamegrid-game-player-y 5)
	
(defun my-gamegrid-game-init-buffer ()
  […] ; Skipping lines defined already
  (gamegrid-set-cell my-gamegrid-game-player-x
                     my-gamegrid-game-player-y
                     my-gamegrid-game-player))
      

Now, the update function:

(defun my-gamegrid-game-update-game (buffer)
  "Update the game.
BUFFER is the buffer in which this function has been called.
It should be `my-gamegrid-game-buffer-name'."
  (unless (or my-gamegrid-game-paused
              (not (string= (buffer-name buffer)
                            my-gamegrid-game-buffer-name))
              (null my-gamegrid-game-update-list))
    (let ((action (pop my-gamegrid-game-update-list)))
      (let ((nx (+ my-gamegrid-game-player-x (car action)))
            (ny (+ my-gamegrid-game-player-y (cdr action))))
        (unless (= (gamegrid-get-cell nx ny) my-gamegrid-game-wall)
          (gamegrid-set-cell my-gamegrid-game-player-x
                             my-gamegrid-game-player-y
                             my-gamegrid-game-floor)
          (gamegrid-set-cell nx ny my-gamegrid-game-player)
          (setq my-gamegrid-game-player-x nx
                my-gamegrid-game-player-y ny
                my-gamegrid-game-moved nil))))))
      

The first unless check tells wether or not it should pop an element from the list. If the game is paused, the current buffer is different than the game buffer, or the list is null, it will not pop from it.

Based on the type of game, the buffer check might not be needed, but it is required whenever the buffer contents need to be changed, so it should be placed before any call to gamegrid-set-cell, if the game allows “background execution”.

The following code is fairly straightforward: the cons is popped and the new player position is calculated. gamegrid-get-cell is used to check if the new position is not a wall. The function will return the “identifier” used with gamegrid-set-cell. Using a number allows for fast comparisons with =.

If there are no walls, the buffer contents are modified so that the square is displayed at the new position. Then, variables are updated with the new value, and setting my-gamegrid-game-moved to nil will allow the input functions to add new elements to the global list.

This is all that's needed to process the player's input. Now, all that's needed to explain is how to set up the display.