ぐるっとぐりっど

日曜プログラマがいろいろ試してみたことを、後の自分のためにまとめておく場所

Emacsで簡単にゲームが作れるgamegrid.el

この記事は、Emacsアドベントカレンダー 18日目の記事です。

qiita.com

ちょっとEmacsで、わけあって *1 ドット絵的なものを書きたく、いろいろ調べてるうちにgamegrid.elに出会いました。
gamegrid.elは、Emacs本体に同梱されている、まさにgridなgameを作るためのパッケージです。
gamegrid.elで調べると、いろいろ出てきますが、emacsに内蔵されているpongとかsnake、tetrisなんかに使われています。

そして、使いかたがよくわからず、ぐぐっても使ってみた的記事があっても使い方がよくわからなかった(みつからなかったけど、infoに該当セクションあったりするのかな?)ので、実際使われているものをソースコードリーディングしながら使ってみました。

ソースコードリーディングする上では、snakeゲームのソースコードがわかりやすかった。tetrisはちょっと複雑。
github.com

作ったもの

github.com

f:id:grugrut:20181217225251g:plain
作ったもの

よくある上から降ってくるキャラを避けるゲーム。キー入力周りのところが若干おかしい。

内容と解説

理解しきれてないところもあるので、間違いあればマサカリください。

どうやって描画するか

(defconst sample1-player 0)
(defconst sample1-block 1)
(defconst sample1-blank 7)
(defconst sample1-border 8)
(defconst sample1-space 9)

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L39-L43

gamegridの名のとおり、gridベースなので、x, y座標に対して、そのセルが何であるかを指定することになる。その指定のための定数定義。

(defvar sample1-player-options
  '(((glyph colorize)
     (t ?\040))
    ((color-x color-x)
     (mono-x grid-x)
     (color-tty color-tty))
    (((glyph color-x) [1 0 0])
     (color-tty "red"))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L83-L90

(defun sample1-display-options ()
  "."
  (let ((options (make-vector 256 nil)))
    (dotimes (c 256)
      (aset options c
            (cond ((= c sample1-blank)
                   sample1-blank-options)
                  ((= c sample1-player)
                   sample1-player-options)
                  ((= c sample1-block)
                   sample1-block-options)
                  ((= c sample1-border)
                   sample1-border-options)
                  ((= c sample1-space)
                   sample1-space-options)
                  (t '(nil nil nil)))))
    options))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L127-L143

先の定数あたりも使って、どのセルをどう描画するかを決めるところ。Xの場合は、 [1 0 0]のようにvectorで、r g bを0から1の範囲で設定する。
ターミナルの場合は、"red"のように、指定できる。ターミナルの場合になにが使えるのかは調べてない。
また、Xを使う場合は、画像をxpm形式で指定できるはず。未指定の場合は、デフォルトのブロック画像が使われる。

この辺を駆使するため、以下の初期化処理でいろいろ設定する。

初期化処理と毎ターンの描画

基本的にはメジャーモード書いて作る感じ。それ以外のgamegrid特有の内容だと以下な感じ

(defun sample1-start-game ()
  "."
  (interactive)
  (sample1-reset-game)
  ;;(use-local-map sample1-mode-map)
  (gamegrid-start-timer sample1-tick 'sample1-update-game))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L154-L159

gamegrid-start-timerで何ミリ秒間隔でどの関数をコールするかを設定できる。このタイマー間隔がFPS維持するような仕組になってるかどうかは未調査。
sample1-reset-gameの中では、 (gamegrid-kill-timer)でタイマーを念の為リセット + 以下のような感じで、gamegrid-init-bufferにより、gamegridで描画する範囲と初期値を指定している。

(defun sample1-init-buffer ()
  "."
  (gamegrid-init-buffer sample1-width (+ 2 sample1-height) sample1-space)
  (let ((buffer-read-only nil))
    (dotimes (y sample1-height)
      (dotimes (x sample1-width)
        (gamegrid-set-cell x y sample1-space)))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L173

ついでに、gamegrid-set-cellを使うことで、指定したセルを別の内容に塗り替えることができる。
なので、updateの内容も、変更点を計算して、キャラが移動する場合は、前の時刻のセルを消して、新しい座標のセルを描画する、とかの処理をすることになる。

ゲームの終了

(defun sample1-stop-game ()
  "."
  (interactive)
  (gamegrid-kill-timer)
  (gamegrid-add-score "sample1-scores" sample1-total-cell))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L210-L214

gamegrid-add-scoreを使うことで、ハイスコアランキング(ただしローカル)を簡単に作れて便利

その他

グリフをブロック以外に自分が描画したものも使えるらしいので、試してみたい。

まとめ

なんか旨い使いかたないかなーと思うのですが、更新が激しいものを作るには適してない(結構チューニングが必要)ようなので、ピクロス的なゲームとか作るのには向いてそうです。

*1:挫折したけど途中まで自分も秋口ぐらいからNESエミュレータ作ろとせっせと書いてた。なので、gongoさんのアドベントカレンダーがとても楽しみです