見出し画像

Retro-gaming and so on

12歳からはじめる ゼロからの Racketゲームプログラミング教室(大嘘) その1

さて、実はRacketにはとんでもねぇ機能がある。
Lispとして考えてみてもかなり異質な機能だが極めて直感的だ。

まずは次の写真をダウンロードして見て欲しい。

松本いちか

現在、FANZAで一番人気のAV女優である松本いちか、なる人らしい。
まぁ、個人的にはどーでもいいが(笑)。

写真をダウンロードしたらテキトーな作業用フォルダに置いておく。
そしてRacketを開いてここまで書こう。

(define ichika

当然、これは不完全なLispプログラムである。
そしてこの状態で、プルダウンメニューから[挿入] -> [画像の挿入]を選んで、今ダウンロードした松本いちかの写真を選ぶ。


そうすると、どうなるか。
驚く事にこうなる。


Racketは恐ろしい事に写真のまま、と言うオブジェクトを扱えるのである。
カッコを閉じてLispの式として完成させる。


そして、実行ボタンを押して、リスナーにichikaと打ってみよう。


わはははは、すげぇだろ(笑)?
mapしてみようか(笑)。
例えば

(map (lambda (x) ichika) (range 10))

みてぇにして。


松本いちかがmapされとるがな(笑)。
ぶわはははははwwwwww

さて、何故にRacketが恐らく世界で一番人気があるLisp処理系なのか、っつーとこういう飛び道具機能を備えてるからだな。
当然これはSchemeの仕様には含まれてない、Racketならではの機能である。
画像をオブジェクトとして直接扱えて、mapしたりreduce出来たりする、っつーのは、言い方を変えると、往年のBASICより「楽しい開発環境を」と言う事に他ならない。
単一Lisp処理系としてのポテンシャルは、何度も言ってるけど、Racketは極めて優れてる、としか言いようがない。

ところで、こんなに楽しいのならゲームとか簡単に書けてもいいんじゃないか、とか思うでしょ。
僕もそう思った。
ところがこれが簡単に行かなかったのだ。

まず前提。
Racketには公式のゲームが書けそうなツールと、非公式にPythonのpipみたいなカタチで提供されているツールがある。
後者はなかなか強力なツールが提供されてるが、ドキュメンテーションが良くなかったり、更に外部ライブラリが必要だったりするので、今回は見送った。
問題は前者である。
前者もドキュメンテーションが良くない。あるいは古い、ってんで、解読するのにやたら時間がかかってしまったのだ。
Racketで簡単にゲームを書きたい、と言う場合、単純には2htdp/universe2htdp/imagelang/posnと言うライブラリを利用する。

2htdp/universeと言うライブラリはゲームの心臓部を適用するライブラリで、一種のサーバーである。いわゆるイベントループによる画像の更新や、キーボード(や場合によってはゲームパッド)による入力を一手に引き受ける。
が、ちょっと特殊な記述法を必要とするので、慣れるまで疑問符がアタマに浮かんだままとなるだろう。これに付いてはおいおい説明していこう。

2htdp/imageはその名が示す通り、画像処理の為のライブラリである。これによってRacket内部で描画も可能であるが、上で見せた松本いちかの例のように、外部からロードした画像も色々と操る事が出来る。ぶっちゃけ、画像処理の中心はこのライブラリで担って、あとはその「結果」を2htdp/universeに渡す、と考えて良い。

lang/posnと言うライブラリは「座標」を扱うライブラリで、Racketの構造体、structで作られてるモノだ。自分でstructを使って自作しても構わないが、どうせ提供されてるものならそれを使った方が簡易である。よって今回はこれを使ってみよう。

さて、今回のお題。
以前、星田さんが紹介してた「12歳からはじめる ゼロからの Pythonゲームプログラミング教室」って本を利用してみようと思う。
っつっても僕はこの本を読んだ事はない。しかしありがたい事にソースコードが提供されてるんだな。
今回はこのソースコードに同梱されている写真を利用しようと思う。
上のソースコード提供ページからダウンロードすると12saipythonと言うフォルダが出来てる筈だ。
その中にimg6と言うフォルダがある。これは多分、本の第6章で使う素材だろう。そのimg6と言うフォルダを適当な作業フォルダにコピペしよう。
そして、その中の5つの写真を用いる。これらだ。

    

左から、フィールド用画像(chap6-mapfield.png)、ゴール用画像(chap6-mapgoal.png)、キー用画像(chap6-mapkey.png)、勇者用画像(chap6-mapman.png)、壁画像(chap6-mapwall.png)である。

一応ゲームとしては、単純な迷路の中を勇者が歩くだけ、である。当然壁の中は歩けない。ゴールはキーがあればゲーム終了、になるが、一方、キーが無ければゴールにならない。また、勇者とキーが重なった時、勇者がキーを得た、と言う事になる。
ゲームって程ゲームではないが、Racketで簡単なゲームっぽいモノを書く例、としては充分だろう。大体俺、この本読んでねぇし(笑)。

では始めようか。

まずは必要なライブラリを使用するオマジナイを書こう。

(require 2htdp/universe 2htdp/image lang/posn)

次に、worldと言う構造体を定義する。


このworldと言う構造体が2htdp/universeを使う際でのキモとなる。
worldとは何か、と言うと、平たく言うとゲームで扱うオブジェクトの「座標」の管理とか、フラグを管理したりする為の構造体だ。
今回、動くのは主人公である「勇者」、そして「消える」役目のある「鍵」、そして鍵が取られたか否かを判定する「フラグ」が管理要素となる。
従って、Racketのstruct機能を使い、worldを次のようにして定義する。

(struct world (key brave flag))

この3つが今回のゲームで管理せねばならないモノたちで、worldの構成要素となる。

さて、次に、上で見せたように写真を読み込んでみよう。大域変数なので名前を*images*にしてみる。


Python原作に合わせて、この順で並べてみよう。0番がフィールド、1番が壁・・・となっている。
これら要素番号を用いてマップデータを作らないとならないので、順序は大事なのだ(こだわる人ならハッシュテーブルを使ってもいいけどメンド臭いだろう)。

次に、ゲームの画面の大きさを決める大域変数を定義する。幅と高さだな。
取り敢えずPython原作から・・・描画部分のサイズを引っ張ってこよう。

(define *width* 620)
(define *height* 434)

これはPython原作より小さな描画領域だが、理由は後述する。
次。
画像は全て62 x 62のサイズで、当然そうすると、「一歩歩く」と画像は62ピクセル分移動するわけだ。
そこで、それを「一歩」としてステップサイズを決定しよう。

(define *step* 62)

さて、いよいよ、マップを描画する準備をしよう。
まず、マップを構成する「座標」を決定する。
2htdp/universeは画像の左上を基準とせず、画像の真ん中の座標を基準としてる。
つまり、仮に、表示画面の(x, y) = (0, 0)に画像の左上をピッタシ合わせたい場合、画像を「置く」場所は、62 x 62の画像サイズだと(x, y) = (31, 31)を指定しなければならない、と言う事だ。
そして画面サイズは既に620 x 434と想定した。つまり、横に62 x 62の画像は10枚置け、縦に7枚置ける事となる。
それで10 x 7の全ての座標をRacketに計算させ保持させるわけだが、マップを作る為にマッピングして、大域変数として定義しよう。

;; マップ描画の準備

(define *image-posns*
 (
append-map (lambda (y)
        (map (lambda (x)
           (
make-posn x y))
          (range (/ *step* 2) *width* *step*)))
        (range (/ *step* 2) *height* *step*)))

これで写真を置く「ポジション」が全て得られた。
ここで、make-posnlang/posnに含まれる関数で構造体として座標を作る。
また、range関数はPythonのrangeと殆ど一緒だと考えて良いRacketの組み込み関数である。
リスナーで*image-posns*を走らせてみると、全座標が計算されてる事が分かるだろう。

> *image-posns*
(list
 (posn 31 31)
 (posn 93 31)
 (posn 155 31)
 (posn 217 31)
 (posn 279 31)
 (posn 341 31)
 (posn 403 31)
 (posn 465 31)
 (posn 527 31)
 (posn 589 31)
 (posn 31 93)
 (posn 93 93)
 (posn 155 93)
 (posn 217 93)
 (posn 279 93)
 (posn 341 93)
 (posn 403 93)
 (posn 465 93)
 (posn 527 93)
 (posn 589 93)
 (posn 31 155)
 (posn 93 155)
 (posn 155 155)
 (posn 217 155)
 (posn 279 155)
 (posn 341 155)
 (posn 403 155)
 (posn 465 155)
 (posn 527 155)
 (posn 589 155)
 (posn 31 217)
 (posn 93 217)
 (posn 155 217)
 (posn 217 217)
 (posn 279 217)
 (posn 341 217)
 (posn 403 217)
 (posn 465 217)
 (posn 527 217)
 (posn 589 217)
 (posn 31 279)
 (posn 93 279)
 (posn 155 279)
 (posn 217 279)
 (posn 279 279)
 (posn 341 279)
 (posn 403 279)
 (posn 465 279)
 (posn 527 279)
 (posn 589 279)
 (posn 31 341)
 (posn 93 341)
 (posn 155 341)
 (posn 217 341)
 (posn 279 341)
 (posn 341 341)
 (posn 403 341)
 (posn 465 341)
 (posn 527 341)
 (posn 589 341)
 (posn 31 403)
 (posn 93 403)
 (posn 155 403)
 (posn 217 403)
 (posn 279 403)
 (posn 341 403)
 (posn 403 403)
 (posn 465 403)
 (posn 527 403)
 (posn 589 403))
>

座標は全部で、7 x 10の70個の情報となっている。

> (length *image-posns*)
70
>

次に、実際のマップデータ、大域変数*map-data*を作る。
*images*に従って、フィールドが0、壁が1、ゴールが2、鍵が3、として二次元リストで取り敢えずこのように設計する。

;; マップデータ

(define *map-data* '((1 0 1 1 1 1 1 1 1 1)
         (1 0 0 1 2 0 0 1 3 1)
         (1 1 0 1 1 1 0 1 0 1)
         (1 0 0 0 0 0 0 1 0 1)
         (1 0 1 1 1 1 1 1 0 1)
         (1 0 0 0 0 0 0 0 0 1)
         (1 1 1 1 1 1 1 1 1 1)))

当然、*map-data*の座標は先に計算した画像用の*image-posns*が持ってる座標と対応している。
あとで、お互い行き来出来るようにテーブル(変換表)を作成しよう。

次はベースとなる実際のマップの画像出力をする。
基本的には2htdp/imageで提供される、place-images関数を用いる。
place-images関数の書式は

(place-images 写真のリスト 座標のリスト ベースとなるシーン)

となっている。
ここで、座標のリスト、は既に作った。*image-posns*だな。
写真のリストは、*map-data*から作る事にする。何故なら先にも説明したが、*map-data*で記述している0、1、2、・・・と言うのは、全て*images*の要素番号に対応しているから、だ。
さて、ここでベースとなるシーンだが、ここで作るデータである*background*では全く無地で、描画サイズだけを決めるempty-scene関数を用いる。
empty-sceneの書式は以下の通り。

(empty-scene width height)

んで、place-images関数をもうちょっと説明しよう。
平たく言うと、place-images関数が作るのはいわゆる画像レイヤーである。これが意味するのは画像レイヤーを重ね合わせる事が可能だと言う事で、外側のplace-images関数は内側の第3引数に与えられた画像レイヤー、またはplace-images関数が作った画像レイヤーの「上に」描画するようになっている。
つまり、empty-scene関数は無地のレイヤーを提供するので、その上に特定の画像を合成して重ね合わそう、と言うのが今回の目的である。
また、主人公の勇者とキーはここで作ろうとする*background*の上に「重ね合わせて」表示していこう、と言うのが戦略だ。
では*background*の定義を。

;; バックグラウンド

(define *background*
 (place-images (map (lambda (i)
           (list-ref *images* (if (< i 3)
                    i
                    0)))
         (apply append *map-data*))
        *image-posns*
        (empty-scene *width* *height*)))

まず、この変数上、*map-data*は二次元リストである必要がない。よってapply + appendで平坦化して一次元リストに直している(こういう作業を特にフラット化、flattenと呼ぶ)。
平坦化した*map-data*の情報を読みながら*images*から画像を引っ張ってくるのだが、勇者と鍵は今のトコ使わないので、勇者と鍵の情報が引っかかった時に全てフィールド画像に差し替えてる、と言うのがキモだ。
そこで使用画像リストを生成してる、って事だけ分かれば後は簡単だろう。画像の座標情報、*image-posns*に従って無地のempty-sceneの上に画像レイヤが敷かれていくだけ、だ。
Racketのリスナーで*background*を走らせてみると、まだウィンドウは生成されないが、基本マップが生成されるのを見る事が出来るだろう。


ここまで来たら結構「シメシメ」となるのじゃなかろうか。

さて、次に鍵と勇者の表示関数を作りたいトコだが、その前に*map-data*上のデータと*image-posns*上のデータの変換関数を作っておこう。これがないと、壁との衝突判定や、歩数の計算がややこしくなる。つまり、どの方向でもいいが、一歩進むと62ピクセル進むんだ、と言う情報を纏めておいたほうが、少なくともこのゲームの場合、ラクだから、である。
とは言ってもこれを書くのは簡単だ。Racketのハッシュテーブルの力を借りよう。
まずは*map-data*の座標を*image-posns*上のデータに変換する関数から。

;; *map-data* 上の座標を構造体 posn に変換

(define *d-pairs*
 (append-map (lambda (y)
        (map (lambda (x)
           (cons x y))
          (range 0
             (length
              (list-ref *map-data* 0)))))
        (range 0 (length *map-data*))))

(define *d-pairs->posns-table*
 (make-hash (map (lambda (x y)
          (cons x y))
         *d-pairs* *image-posns*)))

(define (d-pair->posn d-pair)
 (
hash-ref *d-pairs->posns-table* d-pair))

まず、大域変数*d-pairs*は、*map-data*上の座標を(i . j)と言うimproper listの形式で生成してる。
別にフツーに(i j)と言うリストを生成しても良いのだが、一々cadrと書くのが嫌で、cdrで要素を取りたいからこのカタチにしてる。どうせ、二次元座標なんで二つしか要素がないからこれで良いのだ。
Racket組み込みのmake-hash関数は連想リストをハッシュテーブルに変換する関数だ。ここでは生成した*d-pairs*の要素をキーとして、値を*image-posns*の要素にした連想リストを作り、それをハッシュテーブルに変換してる。
あとは、ハッシュテーブルから引数d-pairに対応した画像の位置情報を返す関数を書くだけ、である。

> (d-pair->posn '(1 . 0))
(posn 93 31)
>

逆変換関数posn->d-pairも上のデータテーブルが書ければ殆ど同じである。なんせ、キーと値を基本的には入れ替えれば良いだけ、だからだ。

;; *map-posn*上のデータを座標に変換

(define *posns->d-pairs-table*
 (make-hash (map (lambda (x y)
          (cons x y))
         *image-posns* *d-pairs*)))

(define (posn->d-pair posn)
 (hash-ref *posns->d-pairs-table* posn))

これで画像の位置情報から*map-data*上の位置情報を検索する事が出来る。

> (posn->d-pair (make-posn 31 31))
'(0 . 0)
>

さて、次は鍵の配置関数を書こう。
まず引数に鍵フラグがあること。
鍵フラグが偽の時は鍵の画像を表示し、真の時はフィールド画像を表示するようにしてみよう。
そして、画像はたった1個なので、place-images関数じゃなくてplace-image関数を用い、引数で受け取ったsceneレイヤーの上に描画するようにする。
place-image関数の書式は次の通り。

(place-image 画像 x座標 y座標 scene)

従って、次のようになる。

;; キー配置関数

(define (place-key p flag scene)
 (place-image
  (list-ref *images* (if flag
           0
           3))
  (posn-x p)
  (posn-y p)
   scene))

フラグが真の時はフィールド画像を選び、そうじゃなければ鍵画像にする。
なお、posn-xposn-yと言うのはposn構造体からx座標、y座標を取り出す関数で、このテの関数(アクセサと呼ぶ)はRacketで構造体を使うと勝手に自動で定義される関数である。ありがとう!

さて、実験だ。
鍵の位置情報を'(8 . 1)、フラグを#fとする。
あとは、下のレイヤーには*background*を使うだけ、だ。
ちと試してみよう。



関数place-keyflag引数に#fを与えるとキーが表示されてる。
一方、flag引数に#tを与えると、



キーが表示されない。
もう一回繰り返すが、このflag引数はworld構造体の第3引数によって決まる事となる。

さて、次に勇者の表示関数を作ろう。
これも鍵の表示関数と同様、鍵+*background*と言う画像レイヤーの上に表示するものだ。
だから鍵表示関数と基本的に同じに書けばいい。フラグが無いだけ、である。

;; 勇者配置関数

(define (place-brave p scene)
 (place-image
  (list-ref *images* 4)
  (posn-x p)
  (posn-y p)
  scene))

引数pが画像位置情報になる。また引数sceneで下層の画像レイヤーを取る事となる。
これもさして難しくなく、place-image関数の書式にそのまま従っている。

取り敢えず、ここまでで、worldを全部表示する関数を作ってみようと思う。
繰り返すが、画像レイヤーを重ねる、と言うのが重要なのである。

;; world 配置関数
(define (place-world w)
 (place-brave (world-brave w)
       (place-key (world-key w)
            (world-flag w)
            *background*)))

さて、ここまでで「勇者」の移動以外は殆ど書いてしまった。
一回、イベントループでどうなるのか見てみようじゃないか。
2htdp/universeでのイベントループ形成関数は・・・・・・ヘンな名前だがbig-bangと言う。
big-bang関数は第一引数にworld構造体を取り、その位置情報やフラグ等を管理する。
第二引数以降にゲームのコントロールやその他モロモロをツッコんでいくのだが、取り敢えずそれらを後回しにして、イベントループでウィンドウを表示するトコまで持っていってみよう。
なお、クソ真面目に*map-data*から鍵の位置情報を拾ってきても良いのだが、今回は数えた方が早いので、位置情報を直接与えている。

;; main プログラム(第一版)
(big-bang (world (d-pair->posn '(8 . 1)) ;;キーの位置
        (d-pair->posn '(1 . 0)) ;; 勇者の初期位置
        #f)        ;; キーのフラグ
 (to-draw place-world)   ;; world の描画
 (name "Dungeon & Racket") ;; ウィンドウのタイトル指定
)

この状態で実行ボタンを押せばウィンドウが立ち上がって次のような画面が表示されるだろう。


全く勇者は動かないし何も出来ないが、取り敢えず「全レイヤーをウィンドウに描画する」トコまでは終わった。
次回は、勇者の動かし方、そしてゲームの終わらせ方等を説明しよう。


  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事