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