Luaレッスン24「(3.2)物理エンジンへようこそ(その2)」和訳

出典:http://compasstech.com.au/TNS_Authoring/Scripting/script_tut24.html

レッスン24「(3.2)物理エンジンへようこそ(その2)」

前回のレッスンでは、物理エンジンについてざっと紹介しましたが、今後さらに応用してゆくわけですから、ほかにも学ぶべきコマンドは数多くあります。また、スクリプトの設計に利用できる(利用すべきである)構造的手法も存在します。今回のレッスンでは、前回と同じ課題に取り組みます。画面上を跳ね回るボールに関する課題ですが、もっと将来的に応用の利く効率的な方法を学びます。

1. 全体を見る:スクリプトの構造を眺める

platform.apilevel = "2.0"
require "physics"
require "color"
timer.start(0.01)
local W
local H
local space
local newBody
local newShape
local pos
local vel
local velX
local velY
local posX
local posY
local width
local gravity
local mass
local inertia
local elasticity
local friction
function init(gc)
     W = platform.window:width()
     H = platform.window:height()
     space = physics.Space()
     mass = 100
     width = W/10
     gravity = 9.8
     elasticity = 1
     friction = 1
     space:setGravity(physics.Vect(0, gravity))
     inertia = physics.misc.momentForCircle(mass, 0, width,      physics.Vect(0,0))
     newBody = physics.Body(mass, inertia)
     newBody:setVel(physics.Vect(1000,1000))
     newBody:setPos(physics.Vect(0, 0))
     newShape = physics.CircleShape(newBody, width, physics.Vect(0,width/2))
     newShape:setRestitution(elasticity)-- [optional]
     newShape:setFriction(friction)
     space:addBody(newBody)
     space:addShape(newShape)
     on.paint = paint
     paint(gc)
end
function paint(gc)
     pos = newBody:pos()
     vel = newBody:vel()
     velX = vel:x()
     velY = vel:y()
     posX = pos:x()
     posY = pos:y()
     if posX > W - width then
          velX = -1 * math.abs(velX)
          posX = W - width
     elseif posX < 0 then
          velX = math.abs(velX)
          posX = 0
     end
     if posY > H - width then
          velY = -1 * math.abs(velY)
          posY = H - width
     elseif posY < 0 then
          velY = math.abs(velY)
          posY = 0
     end
     newBody:setPos( physics.Vect(posX, posY) )
     newBody:setVel( physics.Vect(velX, velY) )
     gc:setColorRGB(color.orange)
     gc:fillArc(posX, posY, width, width, 0, 360)
end
function on.timer()
     space:step(0.01)
     platform.window:invalidate()
end
function on.resize()
     on.paint = init
end

上のスクリプトをTI-Nspire Script Editorに貼り付けて実行してみてください。オレンジ色の円が画面上を跳ね回るはずです。

すべての変数があらかじめローカル変数として定義されていることにまず注目してください。パフォーマンスに関しては、ローカル変数のほうがグローバル変数よりも常にずっと効率的です。後続のすべての函数用としても定義されています。前回のレッスンでは、たとえばwやhをon.resize函数内のローカル変数として定義しましたが、それだと、当の函数内でしか定義されないことになり、もう一度使用するためには再度定義しなければなりません。今回の方法のほうが、多くの点で効率的です。

今回のスクリプトは、initという名前のユーザー定義函数を最初に置くという、まったく新しい構造をしています。驚くようなことではありませんが、これは初期条件の指定と変数の定義とを行う場所です。しかし最後の数行をよく見てください。普通のon.paint函数が、次に定義するユーザー定義函数paintに相当するものとして、ここに定義されています。この函数は、動くボールなど、画面上の実際のレイアウトを処理する函数です。最後にtimer函数を定義しています。

以上で全部定義されました。しかし実際は何もコールされていません。末尾に置いたのがresize函数です。このresize函数がon.paintをコールして、on.paintをinit函数へダイレクトする働きをします。ですからこれが最初に実行されて、要求どおりにすべてが定義、初期化されるということです。そのあとは、on.paintがコールされるたびに、そのon.paintが、今回作成したpaint函数へダイレクトされます。resize函数はリセットの役割も兼ねています。画面のサイズが変化したとき、あるいはresize函数がコールされたときには、毎回、init函数をコールする(1回だけ)ところまでon.paint函数が戻って、最初からやり直しとなります。

よく考えてみてください。init函数は1回しか実行されません。そのあとコールはpaint函数へ向かい、そこで、動くボールの定義とコントロールとが行われます。よく考えてみないとわからないかもしれません。実はわたくしも最初はそうでした。

2. init:セットアップする
ここでは、物理ライブラリーへアクセスするためのrequire "physics"、および事前定義済みの基本色のライブラリーへアクセスするためのrequire "color"を使います。

次いで、我々の作成するボディーのための仮想空間であるスペースを定義します。このスペースにおける重力を定義します。重力は、画面の底辺に向かってモノを引き付ける力です。0、9.8、100などのように値をいろいろ変えて、どうなるか確かめてください。

この段階で設定するもう1つのオプションに「慣性」があります。もっと正確に言えば、我々の作成するボディー(円など)に着せるシェイプの慣性モーメントのことです。円の場合、このプロパティーの引数には、質量、内径、外径、および円中心からのシェイプのオフセットがあります。今回は、内径は0に、外径はwidthに設定しましたので、中身の詰まったボールということになります。値をいろいろ変えてみてください。たとえば内径を外径と同じくした場合は、中身の詰まっていない円ということです。どういう振舞をするでしょうか? 

初期位置は、ベクトルを使ってベロシティーと同じ方法で設定します。

以上でボディーの定義が済みましたので、ボディーにシェイプを着せることができるようになりました。シェイプの選択肢としては、セグメント、ボックス、円、ポリゴンがあります。前述したように、シェイプは弾性や摩擦などの属性を持っています。こうした属性については今回のレッスンでは追加しませんので、この手順は省略して、ボディーと同じxy座標を持つシェイプにリンクするだけでも構いません。しかし、通例どおり設定して、あとで属性を追加することにしました。円シェイプは、その属性として、ボディー、半径、円中心からのシェイプのオフセット値を引数として取り込みます。ここでは、円の半径を考慮してベクトルを使って設定します。

実はシェイプを作成するときに問題が起きましたので、特に反撥力(弾性)および摩擦などの属性を指定しても良いでしょう。弾性1のシェイプは完全弾性体です。すなわち衝突しても運動エネルギーはまったく失われません。1より小さくすると弾性も減ります。1より大きくすると、少し面白い挙動を見せますが、普通はエラーになります。摩擦の値が0より大きい場合も、衝突時にエネルギーが失われることになります。値をいろいろ変えて、どうなるか確かめてみてください。

最後に、space:addBody(newBody)およびspace:addShape(newShape)というコマンドでボディーとシェイプとをスペースに投入します。仕上げとして、上述したようにpaintをリダイレクトし、タイマーを始動させれば、準備完了です。

3. paint:見た目と動きとを派手にする
今回定義したユーザー定義函数であるpaintは、前回のシンプル版で使用したon.paintとほぼ同じです。今回は、require "color"コマンドを使用しています。このコマンドを使うと、color.orange、 color.yellowなどのように色の名前で指定するだけで色が指定できます。

位置をposXおよびposYに設定しさえすれば、どんなシェイプでも使用することは可能ですし、イメージを使用することもできます。しかし、今回はシェイプを円として定義しましたので、この段階で今回のボディーにどんなシェイプを着せようとも、物理的に表示される動作は、円の動作と同じになります。

4. さあ、始めよう
timer函数も前回と変わっていませんが、init函数を実行する手段として、on.paint函数を含んでいるon.resize函数がポイント・バックされていることに注意してください。

今回のスクリプトの場合、on.resize函数の中にon.paint = initを記述しても、実際には大きな利点はありません。何らかの函数の内部に記述せずにスクリプトの末尾に記述したとしても、動作はまったく変わりません。ただし、新しいウィンドウ寸法に応じてスクリプトがリセットされ、正しい比率ですべてが頭から再開されるわけですから、ページのサイズをいざ変更しようとした場合にはメリットがあります。

いろいろ変えて試してみてください。

動きを一時停止するにはどうすれば良いでしょうか? 

on.enterKey函数を追加して、動きを停止、再開してみてください。

on.escapeKey函数で全部リセットしてから再開するにはどうすれば良いでしょうか? 

Playerについてはどうでしょうか? 

Playerにはキーボードがありませんので、たとえば左下のボタンを使って動きを停止、再開するにはどうすれば良いでしょうか? 

さらに、マウスをクリック(on.mouseUp)して動きをリセットし、クリックした場所からボールの動きを再開するにはどうすれば良いでしょうか? 

これらについては次のレッスンで取り上げます。さまざまな色の複数のボールをジャグリングしてみます。楽しいですよ!