Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

【補足2】既存の関数を上書きして拡張しよう

はじめに

(※今回の内容は、Luaの「無名関数」についての理解が不十分だと意味不明な可能性があります。
前回の記事とセットでご利用ください)

さて、前回、
「既存の関数に不満があったら書き換えちゃおうぜ!!」などとまとめましたが、
僕個人としては、Turtle APIの中でも、turtle.select()関数に不満があるのですよね。

たとえば、前々回のモニタボタンでタートルを操作するプログラムでは、選択スロットを選ぶボタンとして、「turtle.select(1)」「turtle.select(2)」「turtle.select(16)」を用意したのですが、もっと他のスロットも選びたいじゃないですか。でも、16個ものボタンを用意するわけにはいきません。

たとえば次のように、選択スロットを相対的に指定できる関数が欲しいのです。

  • turtle.selectNext() : 選択スロットを、現在の選択スロットの次のスロットに移す。
  • turtle.selectPrev() : 選択スロットを、現在の選択スロットの前のスロットに移す。

この2つの関数があれば、「turtle.select(1)」「turtle.selectNext()」「turtle.selectPrev()」の3つのボタンを用意するだけでどのスロットでも快適に選ぶことができます。
これが、今日の目標です。

そこで色々試行錯誤したのですが、この2つの関数を作るためには、現在の選択スロットは何番かを調べなくてはなりません。
でもですね、これを調べる方法がないのです。次のような関数がCC公式Wiki見ても見つかりません(もしあったら誰か教えてプリーズ)。

  • turtle.getSelectedSlot(): 現在の選択スロット番号を返す。

見つからないなら無理やり作るしかありませんよね。

Turtle APIの拡張。その方針。

Turtleの選択スロット操作をより便利にするために、Turtle APIを拡張することにしました。次のような方法で実現します。

  • オリジナルのturtle.select()関数を turtle.org_select()としてバックアップをとっておく
  • まず最初に、タートルの選択スロットを1に初期化する。
  • グローバル変数に現在の選択スロット番号を入れておく。
    • このグローバル変数の値を書き換えできるのは、新turtle.select()関数のみ(としたい)
    • このグローバル変数の値を参照できるのは、turtle.getSelectedSlot()関数のみ(としたい)
    • このグローバル変数の値を他の誰かが勝手にいじったらプログラムがグダグダになります。そのため上記のような変数アクセス制限を加えたいのですがLuaには標準で用意されていないのですよね。これがLuaの最大の弱点だと個人的に思います。仕様は上記のように決めたけど僕のLua知識ではアクセス制御の実装は無理。少なくとも今の段階では。だからこれは原則論。プログラムの他の部分でこのグローバル変数の値を直接書き換えたらダメだからね!
  • 新turtle.select()関数を定義。オリジナルの選択スロット変更機能に加えて、グローバル変数にその選択スロット番号を代入する。
  • turtle.getSelectedSlot()関数を定義。グローバル変数の値を参照して、現在の選択スロット番号を返す。
  • turtle.selectNext() 関数を定義。新turtle.select()を使って選択スロットを次のスロットに移す。ただしスロット16の次はスロット1とする。
  • turtle.selectPrev()関数を定義。新turtle.select()を使って選択スロットを前のスロットに移す。ただしスロット1の前はスロット16とする。

つまりは、現在のスロット番号を隠しパラメータとしてグローバル変数に代入し、これを使って一連の関数を実現しようというわけです。

プログラム解説

その前に使用上の注意から

このプログラムはTurtle APIにいくつかの関数を追加しますが、その過程でturtle.select()関数を上書きし、Turtle APIに隠しパラメータを追加します。

Turtle APIの本体である「turtle」という変数は、中にTurtl APIの関数群を詰め込んだテーブルです。その証拠に、以下のプログラムを実行すると中に詰まっている関数群を一覧することができます。

for k,v in pairs( turtle ) do
  print(k," = ",v)
end

このあたり、前回を理解していれば、理解できていますよね?

またこの「turtle」は(トップレベルの)グローバル変数でもあります。CCコンピュータを起動している間、CTRL+Rでリブートをかけるまで、「turtle」の中身は維持されます。

そのためこのプログラムによる「turtle」テーブルの書き換えは、CCコンピュータ起動中にただ一度だけ実行するように注意しなくてはなりません。
このプログラムを2度も3度も実行すると、上書きしたturtle.select()を、さらにturtle.select()上書き、さらに……、などとひどいことになります。

とはいえ、このプログラムの最初にIF文で、「このプログラムがまだ実行されていないときだけ実行する」ロジックを埋め込んでいるので、さほど気にする必要はないのですけどね:P
そしてもし何かあっても、CTRL+RでリブートをかければOKです。

turtle.selectNext()などの新しい関数を使うためには、

  1. まずこの拡張プログラムを単体で実行、その後、新関数を使ったプログラムを実行
  2. この拡張プログラムをコピー&ペーストして、別のプログラム内の先頭部分に埋め込む

のどちらかの手順をとることをここでは想定しています。

1の方法を自動化する方法についてはそのうち紹介しましょう(ヒント:自動実行スクリプトフォルダ)

プログラムのおおまかな解説

  • L16: これがタートルであり(Turtle APIが存在しており)、なおかつ、このプログラムがまだ実行されていない(turtle.org_select()関数がまだ定義されていない)ときにだけこのプログラムを実行するようにしています。
    • 未定義の値を参照しようとすると、「nil」が返ってくることを利用しています。
    • より正確に言うと、「turtle」テーブル内に「org_select」というキーで値が保存されていないときには「nil」が返ってくることを利用しています。
  • L17: オリジナルのturtle.select()関数のバックアップをとっています。
  • L19: 隠しパラメータも、「turtle」テーブル内に保存しています。他のプログラムで直接書き換えしたらダメよ?
  • 以降関数定義部分ですが、関数定義の書式として、「無名関数を作ってテーブルに代入」形式をわざと使っています。ここではこちらの方がピンと来るでしょ?
  • L34: スロット番号16の次は1に、スロット番号1の前は16に、のように循環させるために、ちょっとだけ工夫しています。
    • 別に、個々のturtle.selectNext()、turtle.selectPrev()内でIF文使って条件分岐させてもいいのだけど、それだと少しカッコワルイかなと。つまりはええかっこしいです。
  • L49: このAPI拡張を行ったことをターミナル画面上に「明確に」表示します。 このプログラムを単独で何度も実行するとわかるのですが、このプログラムは、L16のIF文のおかげで一度しか実行されません。

実際のプログラム