読者です 読者をやめる 読者になる 読者になる

日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

四角形を動かす、描画する

Clojure play-clj arcade-clj

arcade-clj シリーズ。全体の目次はこちら

元記事での追加機能

かいつまんで言えば自キャラの下ごしらえとして四角を描画してそいつを動かせるようにしましょうと言う内容。

  • 四角形を八方向に動かせるようにする
  • 四角形が動けるのは画面内だけ(=画面外に出ないようにする)
  • 画面をリサイズ可能にし、四角形もリサイズ後の画面を動けるようにする
    • 目次にもあるのだが、リサイズ対応は断念した。
    • 高さがフルに動けて幅が 70% までにしてるらしいが理由が良くわからなかったのでこちらも今回はスルー。 理由が分かった段階で改めて実装する。

動かせるようにする辺りはぼちぼちゲームっぽくなってきたかも。

まずは四角を描画してみる

元記事では SDL API で四角を描画するようにしてて、play-clj で近いものだと shape があるのだが、 残念ながらチュートリアルにあるような位置サイズ変更方法 ができなかったので、texture から同色同サイズの四角画像を読み込んで動かすようにする。 rect.png は desktop/resources 以下に置くこと。

(defn- create-player []
  (assoc (texture "rect.png")
         :x 64 :y 64 :width 32 :height 32))

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    (create-player))

    ;; 以降省略
   )

f:id:shinmuro:20160527205858p:plain

コードは省略してしまったが、main-screen の背景は黒に戻した。

すごくどうでも良い事

スクショ見て気づいたかもしれんけどこの記事書いてる途中で Win10 にアップデートした。

何というかネットで騒がれてるほど大した事なかった。アップデート後いくつか設定戻されてたけどまあ何とかなったし。 けど勝手にアプデスケジュール組まれるのは確かにどうかと思う。自分は押し負けてアップデートしちゃったけど。 まあ無料だし多少不具合が残ってても Windows Update で安定してくるだろうしまあいいかと。

Entity System について

さて、脱線ついでと言う訳でもないんだが、 :on-show に指定している関数で texture タイプではあるものの再度 entity を返すようになったので ここらで一旦 play-clj の Entity System について自分なりの理解を整理しておく。

Entity System については公式のチュートリアルに説明はある。

  • n 個の entity をベクタとして持つ(entities)
  • イベントハンドラ関数の戻り値として entities を前提としている
  • entity になれるものは play_clj.entities.XXXXEntity と言う Clojure の record
    • shape, label, texture など
      • API doc に entity based と書かれているもの
  • entity は record なので HashMap と同じ感覚で key,value として色んな情報を一緒に持たせられる
  • entity として返されないオブジェクトも存在する
    • font, music, sound など
    • この辺りの管理は asset-manager で行う事になるがこれはまた改めて書く

play-clj.repl には entities や screen に保持されてる情報を REPL 上で確認できるユーティリティ関数の es があるので (-main) 実行して画面を出した後で試しに見てみるとどんなものが入ってるのか よりイメージし易いと思う。

四角を動かす

斜め移動の判定

元記事見ると XOR 使って判定してる。なるほどなあ。 *1

(defn- xor?
  [x y]
  (if x
    (if y false true)
    (if y true false)))

(defn- diagonal? []
  (and (xor? (key-pressed? :up) (key-pressed? :down))
       (xor? (key-pressed? :left) (key-pressed? :right))))

論理 XOR は Clojure では標準では無いので 2 項限定でとりあえず自作。 xor? にしたのは true/false しか返さない述語関数の慣習に倣ったもの。

動かすコード

(defn- move-player [e]
  (let [speed 720
        vol (if (diagonal?)
              (/ 1 (Math/sqrt 2.0))
              1)
        moved (* speed vol (.getDeltaTime Gdx/graphics))
        dx (match [(key-pressed? :left) (key-pressed? :right)]
             [true true]   0
             [false false] 0
             [true false] (- moved)
             [false true] moved)
        dy (match [(key-pressed? :up) (key-pressed? :down)]
             [true true]   0
             [false false] 0
             [true false]  moved
             [false true]  (- moved))]
    (-> (update e :x + dx)
        (update :y + dy))))

speed は適当。これに上下左右時 or 斜め時の x/y 移動量と前フレームとの時間差を掛けあわせて移動量を算出。 斜め時の x/y 移動量計算式は元記事からそのまま借用。

x/yの動く方向を含めた移動量が dx, dy になり、その判定に Rust はパターンマッチングを使って いて、これは Rust 学習してた時に結構便利だなーと思っていた。そういえば Clojure にも ライブラリがあった事を思い出し今回初めて使ってみた。 こういう複雑な分岐したい時とかは確かに便利かも。

動かすコードを呼び出すコード

:on-key-down で呼び出せばいいのは想像付くと思うのでその部分だけ載せる。

  :on-key-down
  (fn [screen entities]
    (letfn [(is-key [k] (= (:key screen) (input-keys k)))]
      (cond
        (is-key :space)  (set-screen! arcade-clj-game alt-screen)
        (is-key :escape) (app! :exit)
        (or (some is-key [:up :down :left :right])
            (diagonal?))
        (move-player (first entities))
        
        :else entities)))

is-key はともかくその中の input-keys って何だ?と思うだろう。 これは key-code の関数版を自作したものと思ってもらって良い。 と言っても中身は libGDX の Input.Keys に定義されている static field と key-code で使える keyword を単純に手作業でマッピングした hash-map でしかない。

マクロにしてるのはどうも 実行速度が理由っぽい。 試しに単純に key-codeinput-key を繰り返し実行したものを time マクロで計測してみると 確かに key-code の方が速い。

入力の遅延はゲーム性に直結する話なのでそれはそれで分かるが、ここは自分がコードを書く時の書 きやすさを取った。

実はちょっと実装は異なるが play-clj にも関数版は play-clj.util/key-code* として存在してる事に 作った後で気づいた。入力遅延が気になるレベルで発生したら修正するかも。

画面内のみで動くようにする

この段階で動かしてみると分かるが、画面外へもお構いなしに動かせてしまう。

元記事でも後者は対応していたので合わせて対応しておく。 元記事は親 Rectangle の中に自機となる Rectangle 入れてそれで見るようにしてるけど SDL は知らんけど play-clj は画面サイズ取得できるんでチェックするコードを追加するだけにした。

(defn- constraint-bound-x [{:keys [x width] :as e}]
  (cond
    (neg? x) (assoc e :x 0)
    (> (+ x width) (game :width)) (assoc e :x (- (game :width) width))
    :else e))

(defn- constraint-bound-y [{:keys [y height] :as e}]
  (cond
    (neg? y) (assoc e :y 0)
    (> (+ y height) (game :height)) (assoc e :y (- (game :height) height))
    :else e))

(defn- move-player [e]
  (let [speed 720
        vol (if (diagonal?)
              (/ 1 (Math/sqrt 2.0))
              1)
        moved (* speed vol (.getDeltaTime Gdx/graphics))
        dx (match [(key-pressed? :left) (key-pressed? :right)]
             [true true]   0
             [false false] 0
             [true false] (- moved)
             [false true] moved)
        dy (match [(key-pressed? :up) (key-pressed? :down)]
             [true true]   0
             [false false] 0
             [true false]  moved
             [false true]  (- moved))]
    (-> (update e :x + dx)
        (update :y + dy)
        constraint-bound-x
        constraint-bound-y)))

もうちょい上手い書き方あるかも。

画面のリサイズ対応(→断念)

これは現時点では断念した。

  • play-clj の場合デフォルトでリサイズ可
  • リサイズした場合、texture な四角もサイズが変わってしまう

枠内で動かせるようになっただけで結構満足してしまった。。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.6.0 · shinmuro/arcade-clj · GitHub

*1:項目別のリンクが無かったので元を見てみたい場合はページ内で let diagonal を検索すれば参照可能。