Chap-10. scheme でシューティングゲームを作ってみる

Sec-1.前提とすること
Sec-2.プログラムの骨組み
Sec-3.curses ライブラリについて
Sec-4.読み飛ばしについて
Sec-5.ミリセコンドの時間計測について
Sec-6.ゲームについて

Sec-1.前提とすること

        ーー 初稿は2016-06、2019-07 に時点修正、2023-07 に guile-3.0.9 で動作確認 ーー

何か前提をもうけないと、プログラムが作れない。それで以下のとおりにします。

ざっとこんな感じです。
ゲームの骨格は、某大学のアマチュアラジオクラブさんのサイト(らしい)を見ました。今はそのサイトが消滅しています。
それ故、Sec-2.でプログラムの骨格(昔、上記で見たもの)を載せておきます。

なお、本物のキャラクター端末(Xを立ち上げない時の画面)では動きません。動くのはターミナルエミュレータだけです。
キーオートリピートを制御するのに、xset コマンドを使っている関係です。



Sec-2.プログラムの骨組み

 まず、下の図を見てください。プログラムの骨格(概要)を示しています。(某大学のウェブサイトにあったやり方です)

game algo

要するに、キー入力を待っていて、入力があれば(J、L、space)それに応じて処理をし、先に進む。入力がなければ先と書いてあるところに進んで、その後の処理をする。それを、グルグル回りながら続ける。
簡単に言えば、ゲームの骨格はこれだけです。

後は、シューティングゲームらしくなるように

こんな具合に、少しずつ改良していくだけです。



Sec-3.curses ライブラリについて

 画面を制御するのに、どうしてもcurses ライブラリが必要です。
当該プログラムを最初に作った頃(2014年夏頃)には、まだ SCM を主たる処理系として利用していた。この中にはcurses ライブラリがあるが、私はうまく使いこなせなかった。他の処理系にも同様にライブラリが見られるが、やはり使いこなせない。ライブラリを使いこなすためにドキュメントを懸命に読むくらいなら、作った方が早そうです。
それで、「curses ライブラリもどき」を自作することにした。
 本来のcurses ライブラリはフレームバッファに必要な事柄を書き込んでいき、一定のサイクルでこれをディスプレイに描画しているらしい。そうすれば、キャラクター端末だけではなく、ドットディスプレイにも使える。しかし、自作するにはこれはちと難しすぎる。そこで、自作するライブラリもどきはキャラクター端末専用にした。この場合は、VT100用のエスケープシーケンスを直接画面に出してゆけば良いはずである。
 ネット上にはVT100エスケープシーケンスについてのウェブサイトが沢山あるので、それらを適当に参考とした。
また、エスケープシーケンスだけではうまくいかないものについては、それぞれネット上で情報を集めてエスケープシーケンスもどきを作った。これには、カーソルの視覚・消去(visible)、エコーの制御(echo)、バッファリングの制御(cbreak)がある。また、キーオートリピートの制御は X11 のコマンド xset を利用した。



Sec-4.読み飛ばしについて

 一般に以下のようなプログラムだと、入力があるまでプログラムは入力待ち状態で停止する。

  int main(int ac, char **av){
    int   c;
    while((c = getchar()) != EOF)
      putchar(c);
    return 0;
  }

 上記のプログラムで、キー入力しないと、そこで止まったままになる。
しかしシューティング・ゲームだと、キーボードからの入力がなければこれを放っておいて、先に進む必要がある。
そこで、読み飛ばす・・・というか、入力がなければ放っておいて先に進む処理を実現しなくてはならない。ネットで検索すると、どうも

  $ stty -icanon min X  time Y

というコマンドがこれを実現するらしい。SCM で試してみたら、うまく読み飛ばすことが出来ます。他の処理系でも、いくつかはこれが出来ました。
ただ、やり方がちょっと面倒だし、処理系ごとに出来たり出来なかったりする。つまり不安定。

できればどんな処理系でも安定して出来るやり方が望ましい。それで別の方法を考えることにしました.

scheme のドキュメントをよく読んでみると char-ready? という関数があります。これって、ひょっとしたら読み飛ばせるようにするものか?試してみるとうまく行きます。

(define (getch)
  (if (char-ready? $ip) (read-char $ip) #f))

注:$ip はカレント入力ポート。

 このような関数を作れば、基本的にどの処理系でも読み飛ばすことが出来ました。それで、ゲームではこの関数( char-ready? )を利用することにしました。





Sec-5.ミリセコンドの時間計測について

 次に、プログラム中で時間を計測することを考えます。時間を計測しながらプログラムの進行速度を調整するためです。時間以外の方法を採用すると(例えばループ回数等)高速 CPU だとゲームが早く動き、低速なCPUだとゲームがゆっくり動いてしまいます。これでは調子悪い。おそらくゲーム速度の調節は時間でやるしかないです。
時間計測の単位はミリセコンドくらいが良さそうです。で、各処理系のドキュメント類を読みました。
ここでまた問題が出ました。SCM ではどうやらミリセコンドの時間計測が出来ないようです。他の guile 、chicken 、gauche はすべてミリまたはそれ以下の細かい単位で計測出来ます。結局 SCM でゲームを作ることは諦め、同時にこの時点で「自分が使う主たる処理系」を SCM から guile に変えることにしました。(汗)
各処理系の時間計測の関数は以下のとおりです。

gosh           (sys-gettimeofday) ; 多値で返す
guile          (gettimeofday)
racket         (current-inexact-milliseconds)
chicken        (current-milliseconds)

 これらの関数をそのまま使っていてはコードが処理系依存になってしまいます。そこで自分の関数を作り、これをインターフェイスにして処理系の違いを吸収しました。自分の関数はずばり stop-watch という名前にしました。
これはデータ構造のなかに手続きを持つもので、いわゆる「メッセージ渡し」によって必要な処理をします。また、複数の実体を作ることが出来ます。例として

  (define time1 (stop-watch))
  (define time2 (stop-watch))
  (time1 'set)
  -----
  -----
  (time2 'set)
  -----
  -----
  (time2 'get)
  -----
  (time1 'get)
  -----
  -----

 等とすると、time1 time2 と二つのストップウォッチが出来ます。'set で計測開始。'get でその時までの経過時間を測ります。それぞれ time1 time2 は独立して動きます。もしも興味があれば、プログラム本体をご覧ください。



Sec-6.ゲームについて

 「J」キーと「L」キーで自分が左右に動き、スペースバーでビーム発射です。
敵は一定数動くとワープして別の場所に現れます。ワープする場所、歩く歩数、右に動くか左に動くか、の三点は乱数により決めています。
自分が打つビームは一画面に3つです。4つ目は最初の1個が画面から消えないと打てません。
敵が動く速度やビームの数、敵が打つ爆弾の数は定数で調整出来ますが、自分で試した限りでは、これくらいが一番面白いと思います。
自分の動くスピードはキー入力次第なので、調節できません。ちょっとゆっくりですが、ご勘弁ください。
敵か自分がやられたら、3秒間スリープして、scheme のプロンプトに戻ります。
後は、README をお読みください。では

プログラム。Slackware 14.2 + xfce + guile-2.2.5 及び Devuan 2.0 + xfce + guile-2.2.5 で動きました。 game script
解凍するのにルート権限はいりません。個人のディレクトリで解凍・展開すればオケです。それで動きます。


プログラムは Slackware-current、Slackware-15.0 及び guile-3.0.9 で動作確認しています。(2023-夏から秋)