2009.11.10
Box2D(Box2DFlashAS3)によるActionScript物理シミュレーション

●第7回:物体の制御2(力を加えることによる移動)

◆前回の補足

前回の最後のサンプルではb2Body.SetXForm()メソッドを使って物体を直接的に制御してみました。
その際に物体の座標位置の取得のために使っているb2Body.GetPosition()メソッドについて補足をさせていただきます。
サンプルの[Move]ボタンによる移動では次のようなイベントリスナーを実行しています。

private function _move_btn_clickHandler(eventObj:Event):void {
    var curPosition:b2Vec2 = _bArrow.GetPosition();
    curPosition.Add(new b2Vec2(0, -0.5))
    _bArrow.SetXForm(curPosition, _bArrow.GetAngle());
}

これは動作的には問題ないのですが修正の余地があります。
b2Body.GetPosition()メソッドは物体の基準位置ベースのワールド座標を返しますが、この座標は基準位置の座標のコピーではなく座標位置を表すプロパティそのものを返します。つまり、b2Body.GetPosition()メソッドで取得したb2Vec2オブジェクトを操作するということは、b2Bodyオブジェクトのプロパティそのものを操作していることになります。
上記イベントリスナーのb2Body.SetXForm()メソッドの行を削除し、次のように修正しても物体の位置を変更できます。

private function _move_btn_clickHandler(eventObj:Event):void {
    var curPosition:b2Vec2 = _bArrow.GetPosition();
    curPosition.Add(new b2Vec2(0, -0.5))
}

b2Body.GetPosition()メソッドによる位置移動

↑グラフィックイメージをクリックすると、swfファイルが別ウィンドウで開きます。

→サンプルファイル【Box2D_06_06_test.zip

ただし、この場合は物体がスリープ状態のときにのみ正常に動作するようで、落下中に[Move]ボタンをクリックしても物体の位置は変わりません。
やはり位置を変更するには、そのためのメソッドを介して基準位置のプロパティを操作すべきだと思うので、b2Body.SetXForm()メソッドを使うのが筋かなと思います。
で、その場合b2Body.GetPosition()メソッドで座標位置を取得する場合、コピーのb2Vec2オブジェクトを取得しておくのがよかろうと思います。
その場合のイベントリスナーは次のようになります。

private function _move_btn_clickHandler(eventObj:Event):void {
    var curPosition:b2Vec2 = _bArrow.GetPosition().Copy();
    curPosition.Add(new b2Vec2(0, -0.5))
    _bArrow.SetXForm(curPosition, _bArrow.GetAngle());
}

swfを別ウィンドウで開く

→サンプルファイル【Box2D_06_06_update.zip

動作的には修正前と特に変わっていないと思いますが、この方がよさそうに思います。

個人的にはここら辺の仕様は、b2Body.GetPosition()メソッドが座標位置のプロパティのコピーを返すようになっていて、b2Body.SetPosition()みたいな座標位置を設定するためのメソッドが用意されている方が自然な感じに思えます。ただ、きっと構造上の様々な事情があるのでしょうね。
さて、今回は物体の重心のワールド座標を返すb2Body.GetWorldCenter()メソッドを使っていくのですが、これもやはり同じような仕様になっています。
その他のメソッドでも同じような仕様のものがあるので、その都度触れていこうと思います。

◆瞬間的な力と継続的な力

今回は力を加えることで物体を制御していきます。
ここでは「力」と大まかに表現していますが、実際にはさまざまな単位(メートル毎秒(m/s)、ニュートン(N)、ニュートン秒(N・s)、ニュートンメートル(N・m)など)で表される作用があります。これらは物体に影響を与えるためのメソッドとして用意されています。
これらの単位を見てピンとくる方、ある程度物理に造詣のある方はこれらのメソッドを理解しやすいのかもしれませんが、私をはじめとする物理に詳しくない方はちょっとキツイものを感じるのではないかと思います。
なので本稿では基本的に、メソッドの結果(移動、回転)に注目して話を進めていこうと思います。

物体に加える力については移動・回転ともに瞬間的な力と継続的な力があります。
例えば、ボールをバットで引っぱたくとボールには瞬間的な力が加えられます。ロケットのジェット噴射などは継続的にロケットに推力を与えています。

◆瞬間的な力による移動

物体に瞬間的な力を加えるには、b2Body.SetLinearVelocity()メソッドとb2Body.ApplyImpulse()メソッドが使えます。

【M】b2Body.SetLinearVelocity(v:b2Vec2) : void-物体の速度を設定する。

引数-v:物体に設定する速度を表すb2Vec2オブジェクト。
戻り値-なし。

【M】b2Body.ApplyImpulse(impulse:b2Vec2, point:b2Vec2) : void-物体に瞬間的な力を加える。

引数-impulse:物体に与える力を表すb2Vec2オブジェクト。/point:力を加えるワールド座標位置を表すb2Vec2オブジェクト。
戻り値-なし。

【M】b2Body.GetLinearVelocity() : b2Vec2-物体の速度を取得する。

戻り値-物体の速度を表すb2Vec2オブジェクト。

【M】b2Body.GetWorldCenter() : b2Vec2-物体の重心のワールド座標位置を取得する。

戻り値-物体の重心のワールド座標位置を表すb2Vec2オブジェクト。

【M】b2Body.IsSleeping() : Boolean-物体がスリープ状態かどうかを取得する。

戻り値-スリープ状態ならtrue、そうでないならfalse。

【M】b2Body.WakeUp() : void-スリープ状態の物体を「起こす」(=スリープ状態を解除する)。

戻り値-なし。

【M】b2Body.PutToSleep() : void-スリープ状態ではない物体をスリープ状態にする。

戻り値-なし。

【M】b2Vec2.Copy() : b2Vec2-元のb2Vec2オブジェクトと同じ値を持つ新たなb2Vec2オブジェクトを返す。

戻り値-元のb2Vec2オブジェクトと同じ値を持つ新たなb2Vec2オブジェクト

b2Body.SetLinearVelocity()メソッドとb2Body.ApplyImpulse()メソッドでは次のような違いがあります。

1.力を設定するか追加するか

b2Body.SetLinearVelocity()メソッドは物体にかかる力(正確には「速度」ですが前述の通り大まかに「力」とまとめて呼んでいます)を設定します。これに対してb2Body.ApplyImpulse()メソッドは力を追加します。
b2Body.SetLinearVelocity()メソッドで力を追加したければ、現在物体にかかっている力をb2Body.GetLinearVelocity()メソッドで取得し、追加したい力を加算してb2Body.SetLinearVelocity()メソッドで設定します。
ここで注意しないといけないのは、b2Body.GetLinearVelocity()メソッドで取得できるb2Vec2オブジェクトも、冒頭の補足で書いたb2Bodyオブジェクトのプロパティそのものを返す点です。
つまりb2Body.GetLinearVelocity()メソッドで取得したb2Vec2オブジェクトを操作しても物体に力を加えることができます。が、前述の通りメソッドを介して動作を設定すべきだと思うので、b2Vec2.Copy()メソッドで複製したものを操作し、b2Body.SetLinearVelocity()メソッドで設定しようと思います。

2.単位

b2Body.SetLinearVelocity()メソッドでは正確には物体の速度をメートル毎秒(m/s)で設定します。これに対してb2Body.ApplyImpulse()メソッドは、物体にかける力をニュートン秒(N・s)(又はキログラムメートル毎秒(kg・m/s))で追加します。
私には、速度の方はともかく力の方の単位はサッパリです。が、b2Body.SetLinearVelocity()メソッドで指定するb2Vec2オブジェクトのx、yの各プロパティに物体の質量(kg)を掛けると同じ感じで移動します。
質量の取得はb2Body.GetMass()で行えます。

3.作用点の設定ができるかできないか

b2Body.SetLinearVelocity()メソッドは常に物体の重心に作用しますが、b2Body.ApplyImpulse()メソッドは第2引数で作用点を指定できます。ここに重心のワールド座標を設定すれば重心に対して作用することになります。
重心以外の位置を指定すると物体に回転力が加わります。これはビリヤードで手玉の中心からずれたところを突くことで回転をかけるのに似ています。

4.スリープ状態の動体を「起こす」か「起こさない」か

b2Body.SetLinearVelocity()メソッドはスリープ状態の物体を起こしません。スリープ中の物体にb2Body.SetLinearVelocity()メソッドを実行して速度を設定してもその時点では物体は動かず、一種エネルギーが蓄積されたような状態になります。その後、スリープ状態を解除することで物体は設定された速度で動き始めます。
b2Body.ApplyImpulse()メソッドは物体を起こした上で力を加えます。そのためスリープ中の物体に対して実行しても、即座に動き始めます。

もしかしたら他にもあるかもしれませんが、とりあえずこのぐらい。

以下、ボタンクリックで両方のメソッドをボタンクリックで実行してみます。まずはシンプルにX方向に5移動させるスクリプトを実行させてみます。
また、b2Body.SetLinearVelocity()メソッドで速度を0にして動体の移動を止めるボタンも用意してみます。以下各ボタンのイベントリスナーのみ掲載します。

スクリプト:7-1

private function _LinVel_btn_clickHandler(eventObj:Event):void {
    _bBall.SetLinearVelocity(new b2Vec2(5, 0));
}
private function _AppImp_btn_clickHandler(eventObj:Event):void {
    _bBall.ApplyImpulse(new b2Vec2(5, 0), _bBall.GetWorldCenter().Copy());
}
private function _Stop_btn_clickHandler(eventObj:Event):void {
    _bBall.SetLinearVelocity(new b2Vec2(0, 0));
}

メソッドのシンプルな実行

↑グラフィックイメージをクリックすると、swfファイルが別ウィンドウで開きます。

→サンプルファイル【Box2D_07_01.zip

動体がスリープ状態のときに[LinearVelocity]ボタンをクリックしても動体は動きません。[ApplyImpulse]ボタンをクリックすると、動体は起こされ動きだします。
[ApplyImpulse]ボタンで動体を起こした状態で[LinearVelocity]ボタンをクリックすると、スピードが変化します。どちらもX方向に5移動させていますが、前述の通り単位が違うので動く量も随分違います。

次は、イベントリスナーの処理を少し変えて、動体がスリープ状態で実行した場合に両者が同じ動作をするようにしてみます。
[LinearVelocity]ボタンのイベントリスナー_LinVel_btn_clickHandler()は、動体がスリープ状態の場合にb2Body.WakeUp()メソッドで起こすようにします。
[ApplyImpulse]ボタンのイベントリスナー_AppImp_btn_clickHandler()は移動量に自身の質量を掛けてb2Body.SetLinearVelocity()メソッドと同じ量だけ移動するようにします。
また、移動を止める別の方法として、b2Body.PutToSleep()メソッドでスリープさせるボタンを追加します。
以下、変更・追加のイベントリスナー。

スクリプト:7-2

private function _LinVel_btn_clickHandler(eventObj:Event):void {
    if (_bBall.IsSleeping()) _bBall.WakeUp();
    _bBall.SetLinearVelocity(new b2Vec2(5, 0));
}
private function _AppImp_btn_clickHandler(eventObj:Event):void {
    _bBall.ApplyImpulse(new b2Vec2(_bBall.GetMass() * 5, 0), _bBall.GetWorldCenter().Copy());
}
private function _Sleep_btn_clickHandler(eventObj:Event):void {
    if (!_bBall.IsSleeping()) _bBall.PutToSleep();
}

swfを別ウィンドウで開く

→サンプルファイル【Box2D_07_02.zip

b2Body.SetLinearVelocity()メソッドとb2Body.ApplyImpulse()メソッドを同じように動作させることに大した意味はありませんが、似た性質を持つメソッドなので理解を深めるためにやってみました。
スリープ中に実行すると同じ動作になりますが、動いているときに実行するとb2Body.SetLinearVelocity()メソッドは一定の速度になるのに対し、b2Body.ApplyImpulse()メソッドは速度が増加していくのが分かります。
停止ボタンの方は、先のイベントリスナーが速度を0にしてからスリープ状態に移行するのに若干のタイムラグがあるのに対し、今回追加したイベントリスナーではダイレクトにスリープ状態になるのが分かります。

さて、次は_LinVel_btn_clickHandler()の処理をb2Body.ApplyImpulse()メソッドと同じように速度を増加させるように変更します。
_AppImp_btn_clickHandler()は作用点を少しずらして動体が回転するようにします。

スクリプト:7-3

private function _LinVel_btn_clickHandler(eventObj:Event):void {
    if (_bBall.IsSleeping()) _bBall.WakeUp();
    var vel:b2Vec2 = _bBall.GetLinearVelocity().Copy();
    vel.Add(new b2Vec2(5, 0));
    _bBall.SetLinearVelocity(vel);
}
private function _AppImp_btn_clickHandler(eventObj:Event):void {
    var c:b2Vec2 = _bBall.GetWorldCenter().Copy();
    c.Add(new b2Vec2(0, 0.1));
    _bBall.ApplyImpulse(new b2Vec2(_bBall.GetMass() * 5, 0), c);
}

既に述べたように、現在の速度や重心の座標を表すb2Vec2オブジェクトのコピーを作っているのがポイントです。
先に書いた停止ボタンのイベントリスナー_Stop_btn_clickHandler()では速度のみ止めていて回転は止まりません。

b2Body.ApplyImpulse()メソッドを重心よりやや下にずれた位置で作用させる

↑グラフィックイメージをクリックすると、swfファイルが別ウィンドウで開きます。

→サンプルファイル【Box2D_07_03.zip

◆継続的な力による移動

物体に瞬間的な力を加えるには、b2Body.ApplyForce()メソッドを使います。

【M】b2Body.ApplyForce(force:b2Vec2, point:b2Vec2) : void-物体に継続的な力を加える。

引数-force:物体に与える力を表すb2Vec2オブジェクト。/point:力を加えるワールド座標位置を表すb2Vec2オブジェクト。
戻り値-なし。

b2Body.ApplyForce()メソッドで指定する力の単位はニュートン(N)。このメソッドはb2Body.SetLinearVelocity()メソッドやb2Body.ApplyImpulse()メソッドと異なり、enterFrameイベントリスナーか何かで継続的に繰り返し実行するためのメソッドです。
このメソッドは実行し続けると、車のアクセルを踏み続けるのに似たイメージで動体を加速させていきます。
以下、ボタンを押している間加速するスクリプト(イベントリスナーのみ)。

スクリプト:7-4

private function _AppFor_btn_mouseDownHandler(eventObj:Event):void {
    addEventListener(Event.ENTER_FRAME, _AppFor_enterFrameHandler);
    stage.addEventListener(MouseEvent.MOUSE_UP, _stage_mouseUpHandler);
}
private function _AppFor_enterFrameHandler(eventObj:Event):void {
    var c:b2Vec2 = _bBall.GetWorldCenter().Copy();
    c.Add(new b2Vec2(0, 0.05));
    _bBall.ApplyForce(new b2Vec2(1, 0), c);
}
private function _stage_mouseUpHandler(eventObj:Event):void {
    removeEventListener(Event.ENTER_FRAME, _AppFor_enterFrameHandler);
    stage.removeEventListener(MouseEvent.MOUSE_UP, _stage_mouseUpHandler);
}

このサンプルでは作用点を中心からずれた位置に設定しているので、加速すると共に回転していきます。

swfを別ウィンドウで開く

→サンプルファイル【Box2D_07_04.zip

◆移動速度の減衰率

さて、ここまでのサンプルでは与えた力(=速度)が時間経過と共に減衰することなくずっと一定のままになっています。
Box2Dでは物体ごとに個別に移動速度の減衰率を設定できます。減衰率は物体を生成する際に、b2BodyDef.linearDampingプロパティで設定できます。

【P】b2BodyDef.linearDamping : Number-減衰率を表す数値。

b2BodyDef.linearDampingプロパティは大抵の場合0(減衰なし)~1の間の小数値で指定しますが、1より大きい値も設定できます。値が大きいほど急ブレーキがかかるイメージになります。
以下のサンプルは前のサンプルに、動体のb2BodyDef.linearDampingプロパティを0.5にする設定を加えたものです。

swfを別ウィンドウで開く

→サンプルファイル【Box2D_07_05.zip

このサンプルでは回転の減衰率を設定していないので回転はずっと一定の速度のままになっています。
今回はこの辺で終わらせていただき、次回は力を加えることによる回転を行おうと思います。
そこで回転の減衰率も扱っていきます。

今回のサンプルでは有効な重力は設定していません(x=0, y=0で設定)。また、動体が画面右端にきたら左端に戻すようなスクリプトが追加してあります。

←b2Body.ApplyImpulse()メソッドの行が改行して表示される場合、それは改行ではなく行溢れの折り返しです。

このサイトへのご意見・ご感想は[takuya@haphands.com]までお願いします!