LÖVEでゲームを作ろう 4 クロージャー

昨日の記事で書いたGUIツールキットを今日も開発していて、あらためてレキシカルスコープとクロージャーの威力を感じた。今日は、suiツールキットの実装について書こうと思う。*1

suiはMoonScriptで実装されている。suiのウィジェットは単なる関数を詰め合わせたテーブルで、suiはウィジェットを作るビルダー関数の詰め合わせのテーブルだ。たとえば、幅と高さ、描画を行うコールバック関数を引数に取り、フレームウィジェットを返すビルダー関数 sui.frame はこうなっている。

sui.frame = (width, height, draw) ->
	obj = {}
	obj.size = -> return bang(width), bang(height)
	obj.draw = draw
	return obj

ここで、bangは次のような、渡されたオブジェクトが関数なら評価して返り値を渡し、そうでなければそのままのオブジェクトを返す関数。

bang = (obj) ->
	if type(obj) == 'function'
		obj! -- obj!はobj()と同じ
	else
		obj

これで、一種の遅延評価を実現している。こうすると、実引数に渡された値が普通の値でもクロージャーでも上手く動く。
ウィジェットを作るビルダー関数の中には、他のウィジェットを受け取って、新しいウィジェットを返すものがある。例えば、次のビルダーはウィジェットの色を変更する。

sui.fc = (color, widget) ->
	obj = copy(widget)
	draw = obj.draw
	obj.draw = (x, y) ->
		c = bang(color)
		if c == nil
			return draw x, y
		r, g, b, a = graphics.getColor()
		graphics.setColor c
		draw x, y
		graphics.setColor r, g, b, a
	return obj

ウィジェットを一旦コピーして、描画を行う関数だけ置き換えている。他にも、ウィジェットイベントハンドラーを追加するビルダーや、複数のウィジェットをまとめて表示するウィジェットを返すビルダーもある。

単純な機能のビルダー関数を組み合わせて、簡単に新しいビルダー関数を作ることができる。次の例は、既存のイベントハンドラーを追加するビルダー関数を組み合わせて、クリックイベントのイベントハンドラーを追加するビルダー関数だ。

sui.clicked = (handler, widget) ->
	mousedown = nil
	sui.mousepressed (x, y, button) -> mousedown = button,
		sui.global_mousereleased (x, y, button) -> mousedown = nil,
			sui.mousereleased (x, y, button) -> if mousedown == button then handler(x, y, button),
				widget

mousedownという変数に状態を保存して、クリックの過程を管理している。mousedownはローカル変数なので全体に影響することはないし、このビルダー関数一つでどのウィジェットもクリックイベントに対応できる。

まだまだビルダー関数は足りないが、作るのも簡単で楽しい。最初はUIだけに使うつもりだったが、ゲーム全体の描画にも使えるようにしたいと思う。

*1:昨日思いついて作り始めたばかりのsuiツールキットなので、これから内部仕様は大きく変わるかもしれないし、明日には飽きて開発やめているかもしれない。所詮簡単なUI用なので