はじめに
オブジェクト指向プログラミングでComputerCraftのAPIを作成するという、マニアックな企画です。
さて、どのくらいの人が興味を持ち、ついてこれるのでしょうか……。
本APIの作成方針
ComputerCraftにおいてAPIを作るときの原則は以下の通りです。
- 一種類のAPIを一つのファイルにまとめて書く。
- h2touchpanel APIならば、ファイル名「h2touchpanel」として保存
- APIファイルの作り方
- APIとして使わせたくない(隠ぺいしたい)関数・変数はローカル宣言しておく
- APIとして提供する関数・変数はこのファイルの中でグローバルでなければならない
- APIの使い方
- このファイルをAPIとして読み込むときには、os.loadAPI(ファイル名)関数を使う
- h2touchpanel.myfunc() のように、テーブル"h2touchpanel"に入っている関数・変数を「.」ドット記法で使用する。
これを踏まえて、h2touchpanelのプログラミング方針は以下のとおり。
(これが一般的な方針かどうかはわかりません。あくまで、このAPIの方針です)
- オブジェクト指向プログラミング
- オブジェクトを表現する以下のテーブルは原則としてlocal宣言
- Coordinate, Rectangle, Button, Panel
- 唯一、Panel.new()のラッパーであるmakePanel()関数のみグローバル化しておく。
- つまり、h2touchpanel APIが提供する関数は、makePanel()のみ。
- あとは、作成したPanelオブジェクトに対するメッセージ送信(「:」コロン記法)によりオブジェクトに用意されているメソッドを利用することで、様々な機能を使えるようにする。
オブジェクト指向APIの使用例
local monitor = peripheral.wrap("top") -- 空ボタン3x3=9個から構成されるタッチボタンパネルを作成 local panel_obj1 = h2touchpanel.makePanel({}, 3, 3, monitor) panel_obj1:draw() -- パネルを画面表示 panel_obj1:pp() -- パネルの情報をprint() local button_obj1 = panel_obj1:pullButtonPushEvent() -- ボタンが押されるのを待つ button_obj1:draw(monitor) -- ボタンを画面表示
これにより、APIを利用する側のプログラムがオブジェクト指向プログラミングであろうが、関数型プログラミングであろうが(それほど)違和感なくこのAPIを利用できます。
作成したオブジェクトの説明
なお、このAPIで作成したオブジェクトについては以下のとおり。
- Coordinate: 座標オブジェクト。
- 座標(x,y)を取扱う。また、2座標間の幅や丈、距離などを計算するメソッドを提供
- Rectangle: 四角形オブジェクト。
- 左上座標、右下座標、塗りつぶし色を取扱う。また、四角形を描画するdraw()メソッドも提供。
- Button: ボタンオブジェクト。
- 上記Rectangleオブジェクトを継承する。Rectangle+中央にラベル表記。プロパティとして、文字色(fgColor)や実行関数(cmd)なども保持。
- ボタンを描画するdraw()や、ボタンに設定されている関数を実行するevalCmd()も提供。
- Panel: パネルオブジェクト。
- Buttonオブジェクトを複数保持。Panelオブジェクト生成時(new)に、「ボタン設定テーブル」「ボタンの列数col」「ボタンの行数row」「モニター情報」を与えることにより、モニター内に適切にボタンを作成・配置する。
- この「適切に」の部分がこのプログラムの一番面倒なところでした。最長関数(泣)。
- 重要メソッドとして、pullButtonPushEvent()がある。
- "button_push"というイベント名(文字列)を返値1、押されたボタンオブジェクトを返値2として返す。
- Buttonオブジェクトを複数保持。Panelオブジェクト生成時(new)に、「ボタン設定テーブル」「ボタンの列数col」「ボタンの行数row」「モニター情報」を与えることにより、モニター内に適切にボタンを作成・配置する。
以降は、ソースコードの具体的な解説ですが、コード量が多すぎるので細かな説明はしていません。
おもにそのコードの意図の解説です。
プログラムの説明
ソースコード全体
- 0.1d, 2014/02/17, Panel Optionの整備、モニターのscaleを指定可能
- 0.1c, 2014/02/11, bugfix(パネル作成時にoptを省略したときの不具合修正)
- 0.1b, 2014/02/11, API仕様変更(cmdにfunctionを指定)
- 0.1a, 2014/02/10, 公開
座標(Coordinate)オブジェクトのソースコード
-- Coordinate Object local Coordinate = {} Coordinate.new = function(_x,_y) local obj = {} obj.x = _x obj.y = _y return setmetatable(obj,{__index = Coordinate}) end Coordinate.pp = function(self) print("(",self.x,',',self.y,")") end Coordinate.width = function(startP, goalP) return math.abs(goalP.x - startP.x)+1 end Coordinate.height = function(startP, goalP) return math.abs(goalP.y - startP.y)+1 end Coordinate.midpoint = function(startP, goalP, floor_flag) local x = startP.x + startP:width(goalP)/2 local y = startP.y + startP:height(goalP)/2 if floor_flag then return Coordinate.new(math.floor(x),math.floor(y)) else return Coordinate.new(x,y) end end
- 最初に座標オブジェクト生成関数(new)の定義
- 生成時の引数として、x, y
- このオブジェクトはプロパティとして、x座標とy座標の値を保持しています。
- オブジェクト生成は次のように、「local coordinate = Coordinate.new(1,1)」
- このオブジェクトのメソッドとして、以下を用意しています。
- coordinate:pp()
- 座標情報をprint()する
- coordinate1:width(coordinate2)
- coordinate1とcoordinate2間の横幅を計算。(2,2)と(4,4)間の横幅は3(X座標の差+1)。
- coordinate1:height(coordinate2)
- coordinate1とcoordinate2間の高さ(丈?)を計算。(2,2)と(4,4)間の高さは3(Y座標の差+1)。
- coordinate1:midpoint(coodinate2)
- coordinate1とcoordinate2の中間地点座標オブジェクトを返す。"floor"フラグを渡すと小数点以下切り捨て。
- coordinate:pp()
四角形(Rectangle)オブジェクトのソースコード
-- Rectangle Object local Rectangle= {} Rectangle.new = function(_startX, _startY, _goalX, _goalY, _color) local obj = {} obj.start = Coordinate.new(_startX, _startY) obj.goal = Coordinate.new(_goalX, _goalY) obj.bgColor = _color return setmetatable(obj,{__index = Rectangle}) end Rectangle.draw = function(self,mon) mon.setBackgroundColor(self.bgcolor) for y=self.start.y, self.goal.y do mon.setCursorPos(self.start.x, y) for x=self.start.x, self.goal.x do mon.write(" ") end end end
- 四角形オブジェクト生成関数(new)の定義
- プロパティ
- start、左上の点(Coordinateオブジェクト)
- goal、右下の点(Coordinateオブジェクト)
- bgColor、塗りつぶし色
- プロパティ
- メソッド
- rectangle1:draw(mon)
- 指定した、モニターあるいはターミナル画面に四角形を描画する。
- 引数としてモニター関数テーブル(peripheral.wrap("top")など)や、ターミナル画面関数オブジェクト(term)を与える。
- rectangle1:draw(mon)
ボタン(Button)オブジェクトのソースコード
-- Button Object local Button = {} Button.new = function(_name, _cmd, _startX, _startY, _goalX, _goalY, _fgColor, _bgcolor) local obj = Rectangle.new(_startX, _startY, _goalX, _goalY, _bgColor) obj.name = _name obj.fgColor = _fgColor obj.cmd = _cmd obj.toggle_flag = false return setmetatable(obj,{__index = Button}) end Button.pp = function(self) local str_cmd = self.cmd if type(self.cmd) == "function" then str_cmd = "a function" elseif not self.cmd then str_cmd = "do-nothing" end print(string.format("%s: (%d,%d)-(%d,%d), %d, %s", self.name, self.start.x, self.start.y, self.goal.x, self.goal.y, self.bgcolor, str_cmd)) end Button.draw = function(self,mon) Rectangle.draw(self, mon)-- draw a rectangle -- draw button label into the rectangle local bWidth = self.start:width(self.goal) local bHeight = self.start:height(self.goal) local name = self.name local label = string.sub(self.name, 1, bWidth) local length = string.len(label) local midpoint = self.start:midpoint(self.goal) mon.setCursorPos(math.floor(midpoint.x - length/2), math.floor(midpoint.y)) mon.write(label) end Button.isWithin = function(self, point) local _within = function(n,start,goal) return n>=start and n<=goal end return _within(point.x, self.start.x, self.goal.x) and _within(point.y, self.start.y, self.goal.y) end Button.evalCmd = function(self) if not self.cmd or self.cmd=="" then return false elseif type(self.cmd) == "string" then return assert(loadstring(self.cmd), "invalid cmd: this string is invalid.")() elseif type(self.cmd) == "function" then return self.cmd() else return assert(false, "invalid cmd: cmd is function or function-string") end end Button.run = Button.evalCmd
- Button = Rectangle + label + cmd みたいなもの
- 四角形を描画して、ボタン名(name)を四角形の中央に表記するのがdraw()
- button:isWithin(coordinate)は、与えた座標coordinateが、buttonの範囲内かどうかを返す。boolean。
- button:evalCmd()は、ボタンに設定された関数を実行(eval)する。
- button:run()は、evalCmd()の別名関数。
パネル(Panel)オブジェクトのソースコード
-- Panel Object local Panel = {} Panel._makeButtons = function(_bCfg, _col, _row, _mon, opt) -- 長すぎるので省略 end Panel.new = function(_bCfg, _col, _row, _mon, opt) if _mon.setTextScale then _mon.setTextScale(0.5) end local obj= {} obj.col=_col obj.row=_row obj.total = _col*_row obj.mon = _mon obj.btns= Panel._makeButtons(_bCfg, _col, _row, _mon, opt) return setmetatable(obj,{__index = Panel}) end Panel.pp = function(self) print("Panel: ",self.col,'x',self.row) print(" Btns: ",#self.btns) for i,b in ipairs(self.btns) do b:pp() end end Panel.draw = function(self) self.mon.setBackgroundColor(colors.gray) self.mon.clear() for i,b in ipairs(self.btns) do b:draw(self.mon) end end Panel.drawPushedButtonEffect = function(self, btn, _sec) self.mon.setBackgroundColor(colors.gray) self.mon.clear() btn:draw(self.mon) sleep(_sec or 1) self:draw() end Panel.pullButtonPushEvent = function(self, mon_side) local pushed_btn = false local whichButton = function(btns,x,y) for i,b in ipairs(btns) do if b:isWithin(Coordinate.new(x,y)) then pushed_btn = b break end end end repeat if self.mon.setTextScale then -- when self.mon is advanced_monitor local event, side, x, y = os.pullEvent("monitor_touch") if not mon_side or mon_side == side then whichButton(self.btns, x, y) end else -- when self.mon is term local event, mouse, x, y = os.pullEvent("mouse_click") whichButton(self.btns, x, y) end until pushed_btn return "button_push", pushed_btn end
- _makeButtons()はPanel.new()内で使用するサブ関数で、パネル内に適切にボタン配置できるようボタン群を生成する。この部分のデバッグがこのAPI作成時間の約半分を占めている(泣)。
- 基本的に、各ボタンの幅・丈は、モニタサイズを均等割りして計算。余った余白部分はmarginとして、端にあるボタンに加える。つまり、端にあるボタンが大きくなるというデザイン。
- ボタンの背景色は、色パターンを指定する。{colors.blue, colors.yellow}と指定したら、青・黄・青・黄……と繰り返す。
- Panel.new()メソッド設計のポイントは、必須の引数を最小限にすることで、できるだけ簡単にパネルを作成できるようにすること。必須以外の引数はオプション(opt)としてテーブルにまとめて与える。オプションなので、当然ながら省略可能。
- opt={leftPos=1, topPos=1, rightPos=10, bottomPos=10, fgColor=colors.white, bgColor={colors.blue, colors.yellow}, scale=0.5}
- タッチパネル表示範囲の指定: leftPos, topPos, rightPos, bottomPos
- ボタンラベルの色: fgColor
- 背景色パターン: bgColor
- モニターのsetTextScale値:scale
- opt={leftPos=1, topPos=1, rightPos=10, bottomPos=10, fgColor=colors.white, bgColor={colors.blue, colors.yellow}, scale=0.5}
- 用意したメソッドは、以下のとおり。
- draw(): パネル描画
- drawPushedButtonEffect(): タッチパネルなので押したかどうかのフィードバックがないと使いづらい。押されたら、そのボタンだけを1秒(引数で指定可能)だけ強調表示する。
- pullButtonPushEvent(): タッチパネルボタンがおされたら、"button_push"イベントとして、押されたボタンオブジェクトを返す。
- タッチパネルがモニターならば、"monitor_touch"イベントを利用する。ターミナル画面ならば、"mouse_click"イベントを利用する。
プログラムの最後に、提供する関数のラッパーを定義
-- ######################## -- API Functions function makePanel(bCnfg, col, row, mon, opt) return Panel.new(bCnfg, col, row, mon, opt) end
おわりに
オブジェクト指向プログラミングだと、そうでないスタイルで書くよりもコード量は一気に増えますね。
でも、オブジェクト指向のスタイルが身についているなら、プログラムの概要がわかりやすく、機能追加も簡単だと思います。
ただし、プログラミング初心者に今回のコードを見せて、いきなり理解しろというのも無理なお話。
あくまで、オブジェクト指向プログラミングを理解している人向けですよね……。
今後の方針としては、長く複雑になりがちなプログラムはオブジェクト指向でプログラミングし、初心者向けの解説を目的としたプログラム、特に短めで見通しの良いプログラムについては関数型のプログラミングを心がけたいと思います。