Smalltalk-72で遊ぶOOPの原点:ジョイスティックの動きをキー押下で(雑に)真似る「stick」

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


キーの押下の回数をスティックの傾きに代用する

実装をなるべくサボるため^^; 指定したキーの連打の回数でジョイスティックの傾きの度合いを表す仕様にしました。

  1. stickインスタンスは、アクティベート( ≒ 参照)されるとスティックの定められた方向 (x軸 or y軸) の傾きを表す数値を返す。
  2. あらかじめ定められたキーの押下をなんらかの方法で知ることで、傾きを表す数値(インスタンス変数 val )を増減させる。

面倒なのでジョイスティックのボタン(発射ボタン)も stickインスタンスにまとめてしまいます。すみません。

  1. stickインスタンスは、定められたキーが増減の2キーならスティックを、1キーのみなら発射ボタンとして振る舞う。
  2. 発射ボタンとして振る舞う stickインスタンスは、あらかじめ定められたキーの押下をなんらかの方法で知ると、状態を表真偽値(インスタンス変数 val )にtrueをセットする。
  3. アクティベート( ≒ 参照)されると、val を返す。同時に valfalse にリセットされる。

問題は、「キーの押下をなんらかの方法で知る」というところです。

まず、キーセンサー keysens アクションを用意します。ゲーム中にプレイヤーがキーを押下するとそれぞ stickクラスに知らせ( stick process kbd )あとは stick 側で良きに計らってもらうことにします。

to keysens (repeat (kbck ? (stick process kbd) done). #keysens)

なおこの keysensアクションは spaceshipインスタンスと同様にあらかじめ spacewar に登録しておくことで、ゲーム中に繰り返し呼ばれるようにしておきます。

Smalltalk-72では、インスタンス(そしてインスタンスを返すインスタンス生成能を持つ普通のクラス)はメソッド途中でリターン( )アクションへのメッセージ式で処理の中断と返り値を明示しない限りインスタンス自身(SELF)を返しますが、関数的に用いられるアクションは最後に評価した値(なければ nil )を返します。そのため、この spacewar への登録とその後のハンドリングに備えて、メソッドの終わりに自身の参照 #keysens を追加しています。

さて。クラス stick には kmap というクラス変数(256要素の配列 vector 256)を持たせます。そして stickインスタンス生成の際に指定されたキーのコード + 1 の場所に生成したインスタンスを保持しておき(スティックの場合は2キーそれぞれに)、これを前述の process 時に逆引きの辞書として使います。該当するキーにアサインされたインスタンスが見つかればそれに handle <キーコード> を送信し、見つからなければ無視します。

メッセージ handle <キーコード> を受け取ったインスタンスは、自分が発射ボタンなら(指定されたキー keys の数が 1 なら)valtrue を、そうでない場合は <キーコード> が最初の文字と一致するなら val をデクリメント、そうでなければインクリメントします。

アクティベートされても何もメッセージを受け取らなかったとき(≒ 参照されたとき)は、valtrue なら valfalse にリセットして true を、そうでなければ(つまり、false時の発射ボタンやスティックなら) val をそのまま返します。

今更ですが念のための注意として、spaceship もそうなのですが、 ただアクティベートされただけで何か処理をする(特に値を返す)ようなインスタンスの書き方や使い方は、Smalltalk-72 ではあまり想定されていないらしく、いろいろと問題を引き起こします。あくまで Smalltalk-71 の元のコードの見た目に寄せるためだけの遊びの一環としてとらえていただければさいわいです。

to stick x y i kcode : keys val : kmap (
    %delete ? (%all ? ("kmap _ nil)
        :#x.
        for i to 256 do ("y _ kmap[i]. eq #x #y ? (kmap[i] _ nil)). )
    (null kmap ? ("kmap _ vector 256))
    %process ? ("x _ kmap[1+:kcode].
        null #x ? (!false)
        x handle kcode. )
    isnew ? (
        (1 = :keys length ? ("val _ false) "val _ 0).
        for i to keys length do (kmap[1+keys[i]] _ #SELF))
    %print ? (disp _ '(stick '. disp _ 39. disp _ keys. disp _ 39. disp _ ') ')
    %handle ? (
        :kcode.
        1 = keys length ? ("val _ true)
        1 = keys[1 to 2] find first kcode ? ("val _ val - 1) "val _ val + 1)
    eq val true ? ("val _ false. !true)
    !val
)

こちらのコードで宇宙船がキーで操作できることを確認しましょう。航跡が残るように display の残像を消す処理はコメントアウトしてあります。(残像を消す処理を入れたのは早すぎましたね…^^;)

"SSIZE _ 6. "MOVELAG _ "FRAMELAG _ 0. "SPSCALE _ 1.0. "DIRSCALE _ 1.0. "LSCALE _ 1.0.
@ erase. disp display. disp clear
"s1x _ stick 'jl'. "s1y _ stick 'ki'. "s1but _ stick ','.
"s1 _ spaceship 'Jimmy' s1y s1x s1but.
spacewar delete all.
spacewar schedule keysens
spacewar schedule s1.
spacewar run

かなり根気がいりますが2艇以上でもいけそうです。

@ erase. disp display. disp clear
"s1x _ stick 'jl'. "s1y _ stick 'ki'. "s1but _ stick ','.
"s1 _ spaceship 'Jimmy' s1y s1x s1but.
"s2x _ stick 'ad'. "s2y _ stick 'sw'. "s2but _ stick 'x'.
"s2 _ spaceship 'Beth' s2y s2x s2but.
spacewar delete all.
spacewar schedule keysens
spacewar schedule s1. spacewar schedule s2.
spacewar run

スケジュールされたオブジェクトのアクティベートに「step」メッセージを使用する へ続く )