Pharo
Bloc

Pharo6.1のBlocで落ちゲーを作ってみた

はじめに

Pharo では UIフレームワークがどんどん出てきています。「SmalltalkといえばMVCなのに最近のUIは乱れておる」などと言う人たちは横においても、やっぱり新しいUIフレームワークを追いかけるのはしんどいです。しんどい時に事務処理的なコードを書くのはつらいので、楽しく落ちゲーを作ってお勉強してみました。
しょせんオレが楽しく作れる程度のものなのでたいしたことはないです。某宝石が3つずつ上から落ちてきて、タテヨコナナメのどれかで3つ以上揃えば消える、そして連鎖する、例のあの落ちゲー風の、シンプルなやつです。結局、クラス数5個ぽっきりでできちゃいました。てへぺろ。
名前は、某宝石の落ちゲーのパッケージに書いてあったセリフにインスパイアされて、Starfall Night(星降る夜)という名前にしました。

Starfalling Night 画面

インストール

ソースはgithubに転がしてあります。BaselineOfも今のところありません。手抜きでごめんね。(っていうか、BaselineOf書いて置いてみたんだけど、Metacelloから読み込めない。誰かぼすけて。)
https://github.com/tomooda/StarfallNight/tree/master/repository/StarfallNight
これをシェル上でgit cloneしてきて

git clone https://github.com/tomooda/StarfallNight.git

そして Pharo 6.1上で読み込みます。が、まずはBlocをインストールする必要があります。本家のREADMEにあるようにPlayground上で以下のコード片をDo itしてください。(というか、Do itよりも、Inspect itすると終了したことがわかりやすいのでオススメです)

Metacello new
   baseline: 'Bloc';
   repository: 'github://pharo-graphics/Bloc:pharo6.1/src';
   load: #core

次に、StarfallNightを読み込みます。Monticello Browserを開いてください。「+Repository」ボタンを押すとRepository typeを訊かれるので、tonel://を選んでください。すると取り込んでくる元のディレクトリを選択するよう求められるので、さきほどgit cloneしたディレクトリのStarfallNight/repository ディレクトリを選択してください。すると、たぶん、ちゃんと読めるはずです。

とりあえず、Playgroundで

StarfallNightModel example inspect

を実行してください。するとインスペクタが開きますが、それがゲーム画面になっています。
何かキーを押すと星降る夜が始まります。左右で宝石を移動して、上で宝石のローテーション、下で宝石を落とします。あとはまあ、わかりますね?例のあの落ちゲーのシンプル版です。

Bloc

Blocはまだ開発が進められている、Pharoの新しいUIフレームワークです。Spartaを使ってなんかすごいパフォーマンスが出るレンダリングエンジンを使うらしいです。
Blocはgithub上で開発が進められています。
https://github.com/pharo-graphics/Bloc
チュートリアルもちゃんと用意されています。よくわからないけど、カードをめくるゲームみたいです。とりあえず拾い読みした程度ですが、けっこう役に立ちました。おすすめです。

全体像

とりあえず、Blocではモデルとビューが分離されています。モデルではStarfall Nightのルールをコードとして記述します。ビューは、Blocの要素であるBlElementのサブクラスとして定義して、drawSpartaCanvasOn:メソッドで描画します。まあこの辺の感覚はMorphと似てるかもしれません。モデルとビューの連携はAnnouncement機構を使います。今回はモデルが変更されたらビューにそれを通知するだけなのですが、一応画面の意味的に、ボードと落ちてくる宝石と次の宝石それぞれの変更について専用のAnnouncementを作っています。

まずはゲームのモデルを書こう

StarfallNightModel browse

すると、モデルのソースが読めます。もちろん、普通にシステムブラウザでStarfallNightModelを探して選択しても同じです。これの、actionsプロトコルあたりのメソッドを眺めると、どんなゲームかわかると思います。
基本的に落ちて固定化された宝石の盤面をインスタンス変数columnsに格納して、現在落ちている宝石3つ組をcurrentJewels、次に落ちてくる宝石3つ組をnextJewelsに格納しています。落ちている宝石の一番下の宝石のx座標がcolumn、y座標がrowです。
あとはactionsプロトコルにあるように、宝石を左に移動するleftメソッド、右に移動するrightメソッド、床まで落とすdownメソッド、ローテーションさせるrotateでゲームを操作します。
あとは1コマずつ落下していくfallメソッド、宝石が床まで落下したら、タテヨコナナメで消す処理をするvanishTriples、宝石が消えた分詰めるshrink、そして次の宝石を出すnext、といったあたりを実装してあります。まあ、どれも簡単なものです。
あとは、落ちゲーなのでタイマー仕掛けで一定速度で落とす処理をしてあげる必要があります。それがonStepメソッドなのですが、これをBlAnimationというクラスを使ってタイマー仕掛けで一定間隔で叩いてあげます。と言う処理をしているのがgameStartです。このあたり、steppingでお気楽にインターバルタイマーが使えたMorphicとの違いがあります。

gameStart
    self initialize.
    animation
        ifNil: [ animation := BlAnimation new
                duration: 300 milliSeconds;
                beInfinite;
                onFinishedDo: [ self onStep ] ].
    animation isRunning
        ifFalse: [ animation start ]

ちなみにprintOn:で固定化された宝石の盤面を文字で表示するようにしたので、表示がなくてもこれだけでコマンドライン的に遊ぶことができたりします。

表示してみよう

ようするにBlElementのサブクラスを作って、モデルを参照しながら絵を drawSpartaCanvasOn:で描画してあげるだけです。あとはキーボードイベント処理ぐらい。
まずはモデルとの連携ですが、こんな感じ。

starfallNight: aStarfallNightModel
    starfallNight := aStarfallNightModel.
    self
        addEventHandler:
            (BlEventHandler
                on: BlKeystrokeEvent
                do: [ :anEvent | self onKeystroke: anEvent ]).
    self requestFocus.
    starfallNight announcer
        when: StarfallNightBoardUpdatedAnnouncement
        send: #onBoardChange
        to: self.
    starfallNight announcer
        when: StarfallNightNextUpdatedAnnouncement
        send: #onNextChange
        to: self.
    starfallNight announcer
        when: StarfallNightJewelsUpdatedAnnouncement
        send: #onJewelsChange
        to: self

requestFocusしてあげないとイベントが拾えなくて、ハマりました。みなさん、気をつけましょう。
あとは描画ですが、とても雑に書いたコードなんですが、まあ今更恥ずかしがる年でもないので、晒します。こんな感じ。

drawOnSpartaCanvas: aCanvas
    aCanvas fill
        paint: self backgroundPaint;
        path: self boundsInLocal;
        draw.
    1 to: starfallNight numberOfRows do: [ :r | 
        1 to: starfallNight numberOfColumns do: [ :c | 
            | rect |
            rect := aCanvas shape
                roundedRectangle: (((c - 1) * 40) @ ((starfallNight numberOfRows - r) * 40) extent: 39 @ 39)
                radii: (BlCornerRadii radius: 10).
            aCanvas fill
                paint:
                    ((starfallNight at: r @ c)
                        ifNotNil: [ :jewelIndex | self colorAt: jewelIndex ]
                        ifNil: [ Color black ]);
                path: rect;
                draw ] ].
    starfallNight currentJewels
        ifNotNil: [ 1 to: 3 do: [ :index | 
                | r c |
                r := starfallNight row + index - 1.
                c := starfallNight column.
                (r between: 1 and: starfallNight numberOfRows)
                    ifTrue: [ | rect |
                        rect := aCanvas shape
                            roundedRectangle: (((c - 1) * 40) @ ((starfallNight numberOfRows - r) * 40) extent: 39 @ 39)
                            radii: (BlCornerRadii radius: 10).
                        aCanvas fill
                            paint: (self colorAt: (starfallNight currentJewels at: index));
                            path: rect;
                            draw ] ] ].
    starfallNight nextJewels
        ifNotNil: [ 1 to: 3 do: [ :index | 
                | rect |
                rect := aCanvas shape
                    roundedRectangle: ((6 * 40 + 10) @ (160 - (index * 40)) extent: 39 @ 39)
                    radii: (BlCornerRadii radius: 10).
                aCanvas fill
                    paint: (self colorAt: (starfallNight nextJewels at: index));
                    path: rect;
                    draw ] ]

スパルタのキャンバスは、描く種別ごとにキャンバスからビルダーを取り出して、あれこれ設定して、drawというと、絵を描いてくれる、そんな造りになっています。なんかPharoの人たちって本当にビルダーが好きですね。ほのぼのします。

その他

その他のクラスはまあAnnouncementなんで、どうでもいいです。サブクラス作るだけ。
あとは、モデルのクラスにexampleメソッドを書いて動かしているのですが、オレみたいなロートルはついコメントにコードを書きたくなるのですが、それ用のpragmaを使うのが今風なようです。

example
    <script: 'StarfallNightModel example'>
    | model element space |
    model := StarfallNightModel new.
    element := StarfallNightElement new.
    element starfallNight: model.
    ^ element

こいつをInspectしてやれば、Moldable Inspectorの力で、Liveタブをゲーム画面として使うことができます。Rawタブを選べば、普通のインスペクタとしても使えます。べんりー。

まとめ

というわけで、星降る夜は刻を忘れてSmalltalkを楽しみましょう。ちゃんちゃん。