common-lisp
SDL2

CommonLispでゲーム開発〜2Dレンダリング〜

はじめに

今回はSDL2とCommon Lispを使ったゲーム開発の2Dレンダリングについて記載していきたいと思います。

2Dレンダリング (SDL 2.0 日本語リファレンスマニュアル)

2Dレンダリングプログラム

サンプルプログラムの(sdl2-examples:renderer-test)を参考に自分で書いてみました。
ほとんど同じですが・・・。

main.lisp
;; cl-sdl2をロード
(ql:quickload :sdl2)

;; ウィンドウの大きさを定数定義
(defconstant +screen-width+  640)  ; ウィンドウの横幅
(defconstant +screen-height+ 480)  ; ウィンドウの高さ

;; レンダーを黒色でクリアする
(defun clear-render (renderer)
  (sdl2:set-render-draw-color renderer 0 0 0 255)
  (sdl2:render-clear renderer))

;; 線を描画する
(defun line-render (renderer)
  (sdl2:set-render-draw-color renderer 255 0 0 255)
  (sdl2:render-draw-line renderer 150 50 400 50))

;; 複数の繋がった線を描画する
(defun lines-render (renderer)
  (sdl2:with-points ((a 160 160)
                     (b 300 180)
                     (c 400 160))
    (sdl2:set-render-draw-color renderer 0 0 255 255)
    (multiple-value-bind (points num) (sdl2:points* a b c)
      (sdl2:render-draw-lines renderer points num))))

;; ランダムに複数の点を描画する
(defun points-render (renderer)
  (sdl2:with-points ((a (random 800) (random 800))
                     (b (random 800) (random 800))
                     (c (random 800) (random 800)))
    (sdl2:set-render-draw-color renderer 0 255 0 255)
    (multiple-value-bind (points num) (sdl2:points* a b c)
      (sdl2:render-draw-points renderer points num))))

;; 長方形を描画する
(defun rect-render (renderer)
  (sdl2:set-render-draw-color renderer 0 255 0 255)
  (sdl2:render-draw-rect renderer (sdl2:make-rect 300 300 100 100)))

;; 複数の長方形を描画
(defun rects-render (renderer)
  (sdl2:set-render-draw-color renderer 255 0 0 255)
  (multiple-value-bind (rects num)
      (apply #'sdl2:rects*
             (loop :for x :upto 5
                   :for y :upto 5
                   :collect (sdl2:make-rect (+ 400 (* x 10)) (+ 200 (* y 10)) 8 8)))
    (sdl2:render-draw-rects renderer rects num)))

;; 塗りつぶした長方形を描画する
(defun fill-rect-render (renderer)
  (sdl2:set-render-draw-color renderer 255 0 255 255)
  (sdl2:render-fill-rect renderer (sdl2:make-rect 445 400 35 35)))

;; 複数の塗りつぶした長方形を描画する
(defun fill-rects-render (renderer)
  (multiple-value-bind (rects num)
      (apply #'sdl2:rects*
             (loop :for x :upto 5
                   :collect (sdl2:make-rect (+ 500 (* x 10)) 400 8 8)))
    (sdl2:set-render-draw-color renderer 255 255 0 255)
    (sdl2:render-fill-rects renderer rects num)))

;; 各種初期化処理
;; SDL初期化&ウィンドウ初期化
;; ウィンドウの2Dレンダリングコンテキストを生成
(defmacro with-window-renderer ((window renderer) &body body)
  `(sdl2:with-init (:video)
     (sdl2:with-window (,window
                        :title "SDL Sample"
                        :w     +screen-width+
                        :h     +screen-height+
                        :flags '(:shown))
       (sdl2:with-renderer (,renderer ,window :index -1 :flags '(accelerated))
         ,@body))))

;; メイン関数
(defun main ()
  ;; 各種初期化処理
  (with-window-renderer (window renderer)
    ;; ここからイベントループ
    (sdl2:with-event-loop (:method :poll)
      ;; キー入力処理
      (:keyup (:keysym keysym)
              (when (sdl2:scancode= (sdl2:scancode-value keysym) :scancode-escape)
                (sdl2:push-event :quit)))
      ;; 描画処理などのイベント処理
      (:idle ()
             (clear-render renderer)         ; レンダーをクリアする
             (line-render renderer)          ; 線を描画する
             (lines-render renderer)         ; 複数の繋がった線を描画する
             (points-render renderer)        ; ランダムに複数の点を描画する
             (rect-render renderer)          ; 長方形を描画する
             (rects-render renderer)         ; 複数の長方形を描画
             (fill-rect-render renderer)     ; 塗りつぶした長方形を描画する
             (fill-rects-render renderer)    ; 複数の塗りつぶした長方形を描画する
             (sdl2:render-present renderer)) ; レンダリングの結果を画面に反映
      ;; SDL終了イベント
      (:quit () t))))

;; 実行!
(main)

以下、実行中の様子

Screenshot from 2018-05-03 14-33-07.png

それぞれ以下の関数の実行結果です。
* line-render関数 : 一番上の赤い一本線
* lines-render関数 : 上から2番目の青い折れ曲がった線
* points-render関数 : キャプチャ画面ではわかりづらいですが、実行してみると画面全体に小さな点がランダムに表示されるのがわかります。
* rect-render関数 : 緑枠の四角
* rects-render関数 : ウィンドウ中央右にある5つの赤枠の四角
* fill-rect-render関数 : 右下の紫色の四角形
* fill-rects-render関数 : 黄色の5つ並んだ四角

SDLとウィンドウの初期化および、レンダリングコンテキスト生成は以下のようにマクロとして定義してしまった方が後々楽かもしれません。

main.lisp
(defmacro with-window-renderer ((window renderer) &body body)
  `(with-init (:video)
     (with-window (,window
                   :title "SDL Sample"
                   :w     *screen-width*
                   :h     *screen-height*
                   :flags '(:shown))
       (with-renderer (,renderer ,window :index -1 :flags '(accelerated))
         ,@body))))

上記マクロを作ることで、メイン関数内を以下のように簡略化出来ます。

main.lisp
;; メイン関数
(defun main ()
  ;; 各種初期化処理
  (with-window-renderer (window renderer)
    ;; ここからイベントループ
    (with-event-loop (:method :poll)
      ;; キー入力処理
      (:keyup (:keysym keysym)
              (when (scancode= (scancode-value keysym) :scancode-escape)
                (push-event :quit)))
      ;; 描画処理などのイベント処理
      (:idle ()
             (clear-render renderer)      ; レンダーをクリアする
             (line-render renderer)       ; 線を描画する
             (lines-render renderer)      ; 複数の繋がった線を描画する
             (points-render renderer)     ; ランダムに複数の点を描画する
             (rect-render renderer)       ; 長方形を描画する
             (rects-render renderer)      ; 複数の長方形を描画
             (fill-rect-render renderer)  ; 塗りつぶした長方形を描画する
             (fill-rects-render renderer) ; 複数の塗りつぶした長方形を描画する
             (render-present renderer))   ; レンダリングの結果を画面に反映
      ;; SDL終了イベント
      (:quit () t))))

;; 実行!
(main)

各APIの解説

本来ならcl-sdl2のリファレンスマニュアルであるcl-sdl2 API Reference (Quickdocs)The cl-sdl2 Reference Manualを参照するべきなのですが、こちらにはあまり詳しい情報が書かれていないため、ここではSDL 2.0 日本語リファレンスマニュアルを参照しています。
APIの使い方としてはほとんど同じなので、大丈夫だと思うのですが・・・。

with-renderer

ウィンドウの2Dレンダリングコンテキストを生成します。

syntax
(with-renderer (renderer-sym window &key index flags) &body body))

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_CreateRendererSDL_RendererFlagsを参照。

with-event-loop

イベントループマクロです。
このマクロ内にキー操作時の動作や各種イベントを記述していきます。

syntax
(with-event-loop (&key background (method :poll) (timeout nil) recursive) &body event-handlers)

例として、Escキーが押下されたらSDL終了イベントを投げるプログラムを記載します。

KeyOperationEventExample
(:keyup (:keysym keysym)
        (when (scancode= (scancode-value keysym) :scancode-escape)
              (push-event :quit)))

:keyupは、キーが離された時のイベント処理です。(※ キーが押された時は、:keydownを使います。)
keysymは、受信したキーボード入力を保持する変数です。

scancode= / scancode-value / push-event に関しては後述します。

SDL終了イベントは以下のように記述します。

SDLQuitEventExample
(:quit () t)

描画処理などを行いたい場合は、以下のように記述します。

IdleEventExample
(:idle ()
       ;; この中に描画処理などのイベントを記述していきます。
       )

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_Eventを参照。
イベントタイプに関してはSDL_EventTypeを参照。

scancode=

キーの判定処理です。

syntax
;; Generic-Function
(scancode= value scancode-key)

;; Method
(scancode= (scancode integer) scancode-key)

;; Method
(scancode= (keysym sdl-keysym) scancode-key)

scancode-value

keysymをそのスキャンコードの数値に変換します。

syntax
(scancode-value keysym)

push-event

イベントをキューに加えます。

syntax
(push-event event)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_PushEventを参照。
イベント情報は、SDL_Eventを参照。

render-present

レンダリングの結果を画面に反映します。

syntax
(render-present renderer)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderPresentを参照。

set-render-draw-color

作図操作(矩形、線、およびクリア)に使用する色を設定します。

syntax
(set-render-draw-color renderer r g b a)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_SetRenderDrawColorを参照。

render-clear

現在のレンダーターゲットを色で塗りつぶして消去します。

syntax
(render-clear renderer)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderClearを参照。

render-draw-line

現在のレンダーターゲットに直線を描画します。

syntax
(render-draw-line renderer x1 y1 x2 y2)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderDrawLineを参照。

render-draw-lines

現在のレンダーターゲットに複数のつながった直線を描画します。

syntax
(render-draw-lines renderer points num-points)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderDrawLinesを参照。

render-draw-points

現在のレンダーターゲットに複数の点を描画します。

syntax
(render-draw-points renderer points num-points)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderDrawPointsを参照。

render-draw-rect

現在のレンダーターゲットに長方形を描画します。(塗りつぶし無し)

syntax
(render-draw-rect renderer sdl-rect)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderDrawRectを参照。

render-draw-rects

現在のレンダーターゲットに複数の長方形を描画します。(塗りつぶし無し)

syntax
(render-draw-rects renderer rects num-rects)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderDrawRectsを参照。

render-fill-rect

現在のレンダーターゲットに塗りつぶした長方形を描画します。

syntax
(render-fill-rect renderer sdl-rect)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderFillRectを参照。

render-fill-rects

現在のレンダーターゲットに複数の塗りつぶした長方形を描画します。

syntax
(render-fill-rects renderer rects num-rects)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_RenderFillRectsを参照。

with-points

SDL_point構造体のためのLETのような便利なバインディング機能。
2次元上の点(X座標, Y座標)の値をバインディングします。

syntax
(with-points bindings &body body)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_Pointを参照。

points*

直線の端点のSDL_Pointの配列と点の数の2値を返します。
2つの返り値を受け取るには、multiple-value-bindを使用します。

syntax
(points* &rest points)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_Pointを参照。

rects*

描かれる長方形のSDL_Rectの配列と長方形の数の2値を返します。
2つの返り値を受け取るには、multiple-value-bindを使用します。

syntax
(rects* &rest rects)

詳細はSDL 2.0 日本語リファレンスマニュアルSDL_Rectを参照。

make-rect

左上を基点とした長方形を定義します。
必要に応じてガベージコレクションされます。

syntax
(make-rect x y w h)

最後に

とりあえず、簡単な2Dレンダリングやイベントループについて記載しました。
ここに挙げたもの以外にも色んな機能があるので、興味ある人は試してみてください。
2Dレンダリング (SDL 2.0 日本語リファレンスマニュアル)に書かれていることなら大抵できると思います。