レガシーコード生産ガイド

私に教えられることなら

Squeak Smalltalkで荷物運びゲームを作るガイド

これはSmalltalk Advent Calendar 2015の7日目の記事です。

はじめに

この記事では、Squeak Smalltalk荷物を運ぶゲームの簡易版を製作した過程などを紹介します。

完成品のプレイ動画です。

www.youtube.com

この記事対象はかなり限定されています。簡単に言えば、少し前の私が読みたかった記事です。

Smalltalk以外でのプログラミングの経験は少しあって、Smalltalkにかなりの興味を持ち、入門書を読み終わった時点です。しかし今までの環境とあまりにも違うので、何を作れるのか、どう踏み込んでいけばよくわからず躊躇していました。そういう風に戸惑っていた私と同じような人を対象とし、モチベーションを上げることを目的としています。

大まかにどう進めたか、どういう問題をどう解決したかを紹介します。詳細な手順やコードはあまり乗せません。

まず前提知識として、自由自在Squeakプログラミングの10章までの内容を必要とします。

コードはGithubリポジトリのpackages以下に、FileTreeで書きだしたものを置いています。

何を作るか

今回作る荷物を運ぶゲーム、「Nimokkun」は、有名なパズルゲームの簡単なクローンです。(「ゲームプログラマになる前に覚えておきたい技術」という本の最初の課題として出され、商標の問題のためこのような名前が使われています。)

まずどういうゲームか把握しましょう。ルールは上記Wikipedia記事より引用すると

  • フィールドは荷物、壁、ゴール地点、そしてプレイヤーが操作する人間からなる。荷物とゴール地点の数は等しい。
  • ゲームの目的は人間を操作し、すべての荷物をゴール地点に運ぶことである。
  • 人間は上下左右に移動することができる。進行方向に荷物がある場合、その荷物を押すことができる。ただし、荷物の進行方向に他の荷物や壁が存在しない場合に限る。荷物を引っ張ることはできない。

というものです。

イメージがわかない場合は、YouTubeなどで動画を探してみるといいでしょう。

それから、マップ(問題)を文字で書けるようにします。完成動画の簡単なマップは、次のような文字列から生まれています。

#########
#.......#
#.......#
#.&&#o..#
#..o@...#
#..#....#
#########

それぞれの文字については、

  • @ プレイヤー
  • & 荷物
  • o ゴール
  • #
  • .

です。

設計開始

次はかんたんにクラス設計を考えてみます。

オブジェクト指向設計についてはいろいろな技法があるようですが、私はほとんど知りません。とりあえずSmalltalk環境では全てがオブジェクトで、オブジェクト同士のメッセージ送信によってできあがるネットワークがプログラムだ、と私は理解しています。よって「Nimokkunゲームを構成するオブジェクト」を想像して書き出してみます。

  • プレイヤー
  • 荷物
  • ゴール

これらを置く場所も必要ですね。私はフィールドと名づけました。

  • フィールド

ゲーム自体と、ゲームのルールという概念もありますね。私はひとまとめにしました。

  • ゲーム

よって最初に分解した結果は次のようになりました。

  • プレイヤー
  • 荷物
  • ゴール
  • フィールド
  • ゲーム

まずはこれらのクラスを作りました。そして製作が進み、「これはなにか別のモノ・概念だ」「これらには共通点がある」と感じたときに別のクラスやスーパークラスを作っていった結果、動画の時点では次のようなクラスになりました。(インデントは継承を表しています)

PhNimField         フィールド

PhNimFieldChip     フィールドに並べるもの(チップ)
  PhNimBackChip    それぞれのチップの背景
  PhNimEntity      ゲームのルールで考慮すべきチップ
    PhNimGoal
    PhNimGround
    PhNimNimotsu
    PhNimPlayer
    PhNimWall

PhNimGame          ゲーム。シーンとルール管理。
                   下記のPhNimPlaySceneに移した方が良さそうなメソッド多々

PhNimGameScene     ゲームシーン。移り変わりの前後処理や、キー入力時の挙動など
  PhNimClearScene    ゲームクリア画面
  PhNimPlayScene     プレイ中

PhNimKeyDispatcher キーを受け取り、対応するメッセージを現在のゲームシーンに送る

PhNimMapData       マップデータ管理。文字からの生成も受け持つ
PhNimEntityCatalog このゲームでフィールドに置けるもの。PhNimMapData構築用

しかし最初からうまく設計しようとすると、一歩も動けなかったでしょう。「これはいい設計だろう」と無闇に分解したあとで消したクラスもいくつかあります。始めは目に見えるもの、簡単にわかるものから分解して、すぐにメッセージ送信に取り掛かったほうが良さそうです。

クラスを作ったので、中身を考えていきます。

何にメッセージを送る?

前述のように、Smalltalkはオブジェクトへのメッセージ送信で全てを作っていきます。私は他のプログラミング言語の経験があったので考えを切り替えるのに抵抗がありました。「メソッド呼び出し」「関数適用」と思っているとなかなか掴みづらいのです。これから先は、モノ同士が何か呼びかけあってる場面を想像してもらえるとわかりやすいでしょう。

呼びかけ合いなので、相手がメッセージを理解できるかに関わらず、とりあえず呼びかけることだけはできます。日本語を知らない方に日本語で話しかけることはできるでしょう。相手が理解できるかは別の話です。話しかけてみて理解してもらえなかったら、共通の表現方法(ジェスチャー、絵など)を用いて改めて意味を伝えたり、お互いに相手の言葉を学んで理解できるようになったりします。

同じように、オブジェクトにまずメッセージを送信し、理解してもらえなかったら対策を取る、という気軽な態度で作っていきました。私にはこのやり方が合っているようです。全体の関係を作り上げて把握するより、個々のオブジェクトを追っていった方が楽でした。

それでは、作ったクラスにメッセージを送っていきます。

マップデータ

まずマップを作ります。次のような文字列のリスト(配列)から生成します。

#(
'#########'
'#.......#'
'#.......#'
'#.&&#o..#'
'#..o@...#'
'#..#....#'
'#########'
)

do:などのenumerationメッセージを使って1文字ずつ見ていき、対応したモノを作れば良さそうです。(自由自在Squeakプログラミングの5,6章の内容です)

マップを読み込み、マップデータを作るのはどのクラスの役目でしょうか?マップデータを文字列のリストから作るので、fromListOfString: 上記マップ内容というメッセージが良さそうです。このメッセージを送りたい相手は誰か、ということです。

最初に分解したオブジェクトのリストは以下でした。

  • プレイヤー
  • 荷物
  • ゴール
  • フィールド
  • ゲーム

「フィールド」を作るべきでしょうか?または「ゲーム」にメッセージを送るべきでしょうか?

少し落ち着いてみると、「マップデータを作る」と考えていることに気づきました。明らかにこれは作る「モノ」です。

私の場合、二次元配列(Matrixクラス)に荷物や壁などを置いて、それぞれが文字を持って……という風にデータ構造の実装から考えてしまう癖がついているので、見落としやすくなっていると感じました。まずオブジェクトを考え、分解していき、最終的にデータというオブジェクト以外では表せないと感じた時に始めてデータ構造を考える方が良いのかもしれません。

完成時点では、ゲームを開始するコードは以下のようになりました。

    |mapdata game|
    mapdata := PhNimMapData fromListOfString: #(
        '#########'
        '#.......#'
        '#.......#'
        '#.&&#o..#'
        '#..o@...#'
        '#..#....#'
        '#########'
    ).
    game := PhNimGame openWithMap: mapdata.

雰囲気を掴んでもらうために、文字列のリストからマップのデータを作る部分を載せておきます。

PhNimMapData >> fromListOfString: strs
    |rows cols|
    rows := strs size.
    cols := self maxStrSize: strs.
    self
        field: (Matrix rows: rows columns: cols);
        loadListOfString: strs.

PhNimMapData >> loadListOfString: strs
    |catalog|
    catalog := PhNimEntityCatalog indexedByCharacter.
    self listOfString: strs do: [:y :x :c| |chip|
        chip := (catalog at: c) new.
        chip under: self defaultChip.
        field
            at: y
            at: x
            put: chip].

PhNimMapData >> listOfString: strs do: aBlock
    strs withIndexDo: [:str :y|
        str withIndexDo: [:c :x|
            aBlock value: y value: x value: c]]

PhNimMapData >> defaultChip
    ^PhNimEntityCatalog defaultChip new.

文字列のリストから、まずサイズに合わせた記録用のMatrixを作ります。listOfString:do:を使って1文字ずつ見ていき、エンティティ(壁、荷物、プレイヤーなど)のカタログから対応したインスタンスを作って、Matrixに置きます。そのためにPhNimEntityCatalogクラスを作りました。

カタログを手で登録するのは面倒だったので、

  • エンティティは全てPhNimEntityのサブクラスで
  • それぞれの対応した文字を持つ

ことにして、PhNimEntity subclassesから自動的に作っています。

移動

マップの表示ができたので、プレイヤーを移動させます。

何にメッセージを送ればいいでしょうか?機能的に考えると、フィールドが各エンティティの座標を管理するので、フィールドに送ったほうが良さそうです。

しかし、「フィールドがプレイヤーを移動させる」よりも「プレイヤーが移動する」捉え方の方が私は自然に感じました。結果的にフィールドにメッセージを送ることになっても、とりあえずプレイヤーに移動するメッセージを送ってみます。プレイヤーはゲームから取得できるので、メッセージの流れは次のようになりました。一歩上に移動させます。

game player moveUp

プレイヤーは自身の位置を知らない設計なので、やっぱりフィールドにメッセージを送ることになりました。よってプレイヤーのmoveUpメソッドの内容は次のようになりました。

PhNimPlayer >> moveUp
    self field moveUp: self

自分が居るフィールドに、自身を上に移動させるようメッセージを送っています。

fieldのmoveUp: chipは、chipを上方向に移動させます。fieldにとっての「上方向」とは何なのかを知っているのはfield自身なので、メソッドは次のような定義にしました。

PhNimField >> moveUp: chip
    self move: chip to: (self upDir).

このようにまず受け手として自然な相手にメッセージを送り、少しずつ自分ができる・わかることをこなし、後は他の相手(自身も含む)にメッセージを送っていくという方針です。最終的にフィールド、プレイヤー、乗る対象、押す対象などがメッセージを送り合い、動くようになりました。

雰囲気を掴んでもらうために、フィールドの、移動・押しに関するメソッドを載せておきます。

PhNimField >> move: chip to: dir
    |target|
    target := self chipFrom: chip dir: dir.
    target isRidable ifTrue: [^chip rideOn: target].
    target isPushable ifTrue: [^chip push: target to: dir].
    ^false

PhNimField >> push: target by: chip to: dir
    (self move: target to: dir) ifFalse: [^false].
    self move: chip to: dir.
    ^true

PhNimField >> push: target by: chip to: dir
    (self move: target to: dir) ifFalse: [^false].
    self move: chip to: dir.
    ^true

PhNimNimotsu >> push: chip to: dir
    ^false

PhNimPlayer >> push: chip to: dir
    ^field push: chip by: self to: dir.

PhNimNimotsuのpush:to:メソッドの実装は、荷物が荷物を押せないようにするためのものです。PhNimPlayerのものは比較用です。メッセージをたらい回し的に送っていき、最終的に処理できる・条件を判断できるモノに任せます。

どう調べる?

Smalltalkの入門記事・書籍でよく目にするのは、何かわからないことがあったらシステムに聞こう、というアドバイスです。

すぐにマニュアルやStackOverflowを見て回り、ネットに繋がって無ければ開発できない、という態度の私にはなかなか難しく感じていました。

同じような方のために、クラスライブラリを調べまわったときの手順を紹介します。

角が丸い四角を描く

どこから調べたらよいでしょうか?MorphではborderColorborderWidthなどのCSSっぽいセレクタ名が使われているので、同じようにborderRadiusなどがあるかもしれないと考えました。

Method Finderで探したところ、radiusでそれっぽいものがひっかかりました。

f:id:phaendal:20151207082920p:plain

MorphicはdrawOn: canvasでなんらかのCanvasにメッセージを送り、描画することができます。よってCanvasクラスへの実装がある#fillRoundRect:radius:fillStyle:が使えそうです。

それぞれの引数に何を渡せばいいのかわからないので、sendersから使用例を探してみます。

f:id:phaendal:20151207082933p:plain

FormCanvas>>#frameRoundRect:radius:width:color:を見ると、どうやらradiusはPoint、fillStyleは単にColorのインスタンスを渡せば良さそうです。

以下は最終的に私が書いた、角が丸い四角でエンティティのフレーム(塗りつぶしていますが)を描くコードです。

PhNimEntity >> drawFrameOn: canvas
    canvas
        fillRoundRect: self frameBounds
        radius: self frameRadius
        fillStyle: self frameColor

PhNimEntity >> frameBounds
    ^self bounds insetBy: 1@1

PhNimEntity >> frameRadius
    ^3@3

PhNimEntity >> frameColor
    ^self color

キーボードフォーカス

私はWorkspaceにコードを書き、Morphで表示しながら開発していますが、そのMorphがキー入力を受け取るためにはWorkspaceのキーボードフォーカスを移さないといけません。デスクトップなどをクリックして改めてMorphをクリックして、とフォーカスを移すのが面倒なので、マウスオーバーでフォーカスを奪えるようにします。

まず目に見えるもの、今わかるものから調べていきます。Workspaceからキーボードフォーカスを奪う動作は、デスクトップをクリック、他のシステムウインドウをクリックなどがあります。自由自在Squeakプログラミングでの知識を思い出し、ウインドウのmouseDown:メソッドを調べてみます。まずインスペクタでシステムウインドウのクラスをブラウズします。 PluggableSystemWindowクラスのようです。

f:id:phaendal:20151207082955p:plain

mouseDown:は見当たりません。hierarchyで継承階層を表示して、mouseDown:を探します。

f:id:phaendal:20151207083006p:plain

一つ上のSystemWindowクラスにありました。ソースを見てみると、一番最初に何やら関係がありそうなコードがあります。

mouseDown: evt

    | wasActive |
    (wasActive := self isActive) ifFalse: [
        evt hand releaseKeyboardFocus.
        self activate].
    "あとは省略"

自分がアクティブで無いのなら、HandMorph?からキーボードフォーカスを解放し、自分をactivateするようです。

activateメソッドを見てみると、何やら長くてよくわからないコードです。さらにSqueak5.0だと実装が見当たらないメッセージ(takeKeyboardFocus)を送信していたりとなかなか手強そうです。

wantsKeyboardFocusなどのそれっぽいメッセージも見当たりますが、とりあえずactivateは置いておいて、releaseKeyboardFocusを使ってみます。

念の為sendersから送信例を見てみます。

f:id:phaendal:20151207083022p:plain

デスクトップ(PasteUpMorph)のmouseDown:でも使われていたりして、それっぽいですね。

結局NimokkunのモーフのmouseDown: eventevent hand releaseKeyboardFocusというメッセージを送信したところ、フォーカスを得ることができました。mouseMove:にも書くと、カーソルを重ねるだけでフォーカスが移りました。フォーカスは明示的に奪うものではなく、カーソルが重ねられたものに自動的に移り、他に移したくない場合は保持しておく、という使い方なのかもしれません。

あやふやな理解ですが、とりあえず動かせます。こういう場合、私は他の環境だとスッキリせずに不安が残ったりしますが、Smalltalkだと気になった時点で深く潜っていけばいい、と思えるので気が楽です。また、sendersで使用しているコードを参考にできるのも心強いです。

確認する

私がSmalltalkを触り始めた直後は、他の環境でのprint文デバッグのように、Transcript >> show:やprint itで何か情報を表示して確認していました。最初はインスペクタやデバッガが非常にとっつきにくく感じていたからです。

しかし意識して使うようにすると、これほど便利なものはないと思うようになりました。インスペクタの見方やデバッガの使い方は「自由自在Squeakプログラミング」で解説されていますので、参照してください。きっと世界が変わります。

ただし、MorphicのdrawOn:などにself haltを仕込む際はかなり注意した方が良いでしょう。haltで描画ループが止まらず、デバッガが際限なく開き続けることがあります。

f:id:phaendal:20151207083102p:plain

描画周りやイベントハンドリングで何かよくわからない事が起きているときは、インスペクタで生成したMorphicを開いて外から確認できるようにした方がいいかもしれません。少なくとも何か怪しいときはとりあえず実行前にイメージを保存しておきましょう。私みたいに1時間打ち込み続けたコードが失われることがあります。(changesで復元する方法はよくわかりませんでした)

もうひとつ、ゲーム制作の経験がある方には当たり前かもしれませんが、フィールドと配置したチップなどは簡単にMorphicを生成して視認できるようにしておくと良いでしょう。今回もそうなのですが、そのコードをそのまま使えたりもします。

下は触り始めの頃に試しにダンジョンを自動生成してみたときの確認画像です。(Squeakで作ったものをPharoに移植した時のものです)

f:id:phaendal:20151207083224p:plain

各部屋をランダムに繋げて、ちゃんと全てが繋がっているかを確認しています。前に他の環境で作ったときは、テキストでがんばって表現したり、(それ自体がバグだらけの)テストを必死に書いていました。こういう風に目で見えるようにすると簡単に確認できて、心から大丈夫そうだと自信が持てたり、何よりモチベーションが上がります。あまりGUIプログラミングに縁が無かった方は特に挑戦してみてください。

次は何をする?

他の動的な開発環境と同じように、設計し、必要があれば調べて、実装し、確認して修正、のループを繰り返して作りあげていきました。

一つアドバイスとして、他の環境だと コードを書く → (コンパイルやリロードなど何らかの作業 →) 確認する の繰り返しになりますが、Smalltalk環境のデバッガなどを上手く使えば、「コードを書きながら確認する」という感覚になっていきます。自動リロードやビルド、テストという話ではなく、そのまま書きながら確認するという感覚です。これを味わってみることをおすすめします。

Smalltalkでは、例えば存在しないメソッド(A)を起動するメソッド(B)を作っても、Bを起動させるメッセージを送らない限りは問題になりません。例えば今表示して動いているMorphがあるとします。それのクリックした際の動作をバグがある内容に書き換えても、クリックするまでは問題が起きません。表示したままで書き換えて試せばいいということです。

私はそういう部分を書き換えるとどうしてもMorphを一度閉じて開き直したくなっていましたが、その必要は無いと理解すると開発のテンポがとても良くなりました。同じような感じ方をする人は是非試してみてください。

おすすめのドキュメント

最後に、手を動かし始めの頃に特に参考になったドキュメントを紹介します。

書籍では、自由自在Squeakプログラミングを読み終わった直後から「ケント・ベックSmalltalkベストプラクティス・パターン」に目を通しています。最初の方から早速使えるパターンが登場します。

おわりに

ステージや歩数の概念が無いなど、かなり簡単なつくりでした。それらを備えたバージョンを是非作ってみてください。

同じようにSmalltalkに興味を持ち、踏み込んでいく人が現れたら心強いなと思います。この記事がその後押しになれば幸いです。

広告を非表示にする