ニコ生ゲームを作ろうと思ったら物理的に考える その6
今回は新たに3つのジョイントを紹介していきます。
以下がサンプルです。音は抜いています。
■目次
- 回転ジョイントの基本 .Joints.b2RevoluteJointDef()
- 背景にピン止めする
- ジョイントを回転させる
- 回転の可動範囲を設定する
- 特定の角度に制御する
- 複数の関節を組み合わせる
- SetFixedRotationとの組み合わせ
- ピストンジョイントの基本 .Joints.b2PrismaticJointDef()
- ピストンを動かす
- ピストンの可動範囲を設定する
- ピストンの現在位置を読み取る
- バネの動きをさせる
- 固定するジョイントの基本 .Joints.b2WeldJointDef()
回転ジョイントの基本
回転ジョイントは角度の制御や滑車や車輪を作ることができます。
回転させるだけならb2Body.SetangularVelocityでもできますが、中心以外を軸にして回転させるのは面倒です。
また、他の物体との干渉で回るようにする場合も回転ジョイントが適しています。
回転ジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef()を使って「回転ジョイントの設定」を作ります。
let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();
そしてInitializeを実行して物体と回転の中心を指定します。
def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));
順に、固定する物体、回転する物体、回転の中心座標です。
このコマンドはマウスジョイントにはありませんでした。
物体はb2Bodyで指定します。body1とbody2の名前は適宜変わります。
回転の中心座標はゲーム全体の座標で指定します。固定する物体の座標からの位置を計算する必要はありません。
また、中心座標がそれぞれの物体から外れた位置にあっても構いません。
最後に「回転ジョイントの設定」でジョイントを作ります。
box2d.world.CreateJoint(def);
背景にピン止めする
物体と物体を固定せずに背景にピン止めすることもできます。
initializeの1つ目にbox2d.world.GetGroundBody()を指定します。
let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();
def.Initialize(box2d.world.GetGroundBody(), body.b2Body, box2d.vec2(x, y));
︙
box2d.world.CreateJoint(def);
円形の物体の中心を背景に固定すれば滑車やコンベアのようなものも作れます。
ジョイントを回転させる
ジョイントを回転させる場合以下の3つのパラメータを設定します。
- maxMotorTorque
回転に加えられる最大の回転力
回転を阻むものがあっても必要な力がこの値以下なら無理やり回転する
この値以上が必要なら回転が止まる
値の基準は1000などだが、10~100000のように大きく変えて探るのがおすすめ - motorSpeed
回転のスピード
0.1程度あれば回転しているのがわかる
maxMortoTorqueを大きくしてもこのスピード以上では回転しない
プラスで時計回り マイナスで反時計回り - enableMotor
モーターを有効にするかどうか
maxMotorTorqueとmotorSpeedの設定でモーターを有効にするかどうか
trueかfalseを指定
無効にすると重力や他の物からの干渉で自由に回転する
mortorSpeedを0にしてenableMotorをtrueにしていると回転しない
ジョイントを作る際に設定するには以下のように記載してからCreateJointをやります。
let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();
def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));
def.maxMotorTorque = 1000;
def.motorSpeed = 1;
def.enableMotor = true;
box2d.world.CreateJoint(def);
画面をタッチした時のように後でモーターの動きを操作する場合は、CreateJoint時に名前をつけておいて以下のように記載します。
let joint = box2d.world.CreateJoint(def);
︙
joint.SetMotorTorque(2000);
joint.SetMotorSpeed(2);
joint.EnableMotor(false);
回転の可動範囲を設定する
膝の関節のように可動範囲を設定することもできます。
以下の3つのパラメータを設定します。
- lowerAngle
可動範囲の最小値
回転する角度を制限する
モーターの力や他の物体から受ける力でも基本的にこれ以下に回転しない
ラジアン角度で指定するのでMath.PIで180度
6*Math.PIのように360度以上を指定すると、0度ではなく3回転分になる
マイナスも可 - upperAngle
可動範囲の最大値
基本的にこれ以上に回転しない - enableLimit
可動範囲を有効にするかどうか
lowerAngleとupperAngleの設定で回転を制限するかどうかを設定
trueかfalseを指定
Motorの設定と合わせると以下のように記載します。
let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();
def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));
def.maxMotorTorque = 1000;
def.motorSpeed = 1;
def.enableMotor = true;
def.lowerAngle = Math.PI*0;
def.upperAngle = Math.PI*1;
def.enableLimit = true;
box2d.world.CreateJoint(def);
画面をタッチした時のように後でモーターの動きを設定する場合は、CreateJoint時に名前をつけておいて以下のように記載します。
joint.SetLimits(Math.PI*0, Math.PI*2);
joint.EnableLimit(false);
SetLimitはカッコの中で2つの値をlower、upperの順に指定します。
特定の角度に制御する
関節ランではクリック位置に応じて腰と膝の角度を制御していました。
回転ジョイントで「90度に保つ」のように特定の角度に制御することは可能です。
ただしスマートな方法ではなく、upperlimitとlowerlimitを制御したい角度に指定していました。
let pointx = 0;
let controlx = 0;
touch.onPointDown.add(function(ev) {
pointx = ev.point.x;
controlx = (pointx - g.game.width/2) / 300;
if (controlx > 1) controlx = 1;
if (controlx < -1) controlx = -1;
setcontrol();
});
touch.onPointMove.add(function(ev) {
pointx += ev.prevDelta.x;
controlx = (pointx - g.game.width/2) / 300;
if (controlx > 1) controlx = 1;
if (controlx < -1) controlx = -1;
setcontrol();
});
function setcontrol() {
joint.SetLimits(Math.PI * (0.1 * controlx), Math.PI * (0.1 * controlx));
}
このやり方だと角度を変更した瞬間は現在の角度が範囲外になってしまい、あまりいい感じがしません。
次の章の複数の関節では明らかに問題が発生しました。
複数の関節を組み合わせる
関節ランでは人間の関節を再現するため複数の関節を組み合わせていました。
ジョイントの初期設定は簡単で、胴体ー右もも、右ももー右ふくらはぎ、右ふくらはぎー右足の3つを作るだけです。
ただ一つ痛々しい問題がありました。膝が逆に曲がってしまうことです。
複数の関節を強制的に制御しているので、膝の可動範囲の設定が正しくても腰より膝の優先順位が下がると容赦なく逆に曲がります。
これを防ぐために2つ対策を実施しました。
1つ目は角度の上下限に現在のジョイントの角度を含める方法です。
現在の角度は joint.GetJointAngle() で取得できます。これと目標の角度の2つをSetLimits()に入力します。
現在の角度の目標の角度大小を判定して、どちらをlowerにするかも判定します。回転方向も変わってくるのでSetMotorSpeed()の値を切り替えます。
この辺は関節ランの807行目あたりにあります。
2つ目はジョイントに登録する物体の順序を変える方法です。
複数のジョイントを作る順番ではなく、ジョイントのInitializeに登録する2つの物体の順序です。
最初は胴体に足のパーツを追加していくイメージで、
腰(胴体, もも)、膝(もも, ふくらはぎ)、足首(ふくらはぎ, くつ)
の順番でジョイントを作っていました。
これだと優先順位が腰>膝>足首になる気がしたので腰と膝の順序を逆にしました。
腰(もも,胴体)、膝(ふくらはぎ,もも)、足首(ふくらはぎ、くつ)
色々試してたどり着いただけなので、なぜこれがいいのか本当にこれがいいのかはわかりません。順序が混在している方がいいような気がします。
また、順序を逆にすると回転角度の正負も逆にする必要があります。
色々やっている感じでは今回のコードには両方とも必要な対策だったと思います。
SetFixedRotationとの組み合わせ
回転ありの物体の一部の角度を固定すると、理想とする動きを再現しやすくなります。
天秤ゲームでは左右の皿が常に垂直になるように、関節ランでは胴体を前傾姿勢で固定するようにしました。
これらは一部の物体のみSetFixedRotationをtrueにして実現しています。
しかし、物体が細かく振動するという割と深刻な問題がありました。
たぶんジョイントで加わる力と回転固定で加わる力がぶつかり合って安定しないのだと思いますが、根本的な解決策は見つけられませんでした。
関節ランの方は動きが面白くなったことにして放置しました。
しかし、天秤ゲームの方は皿の上のものが滑って落下していました。振動によって常に浮いた状態だったので摩擦を上げても効果はなく、皿の端に壁を作って対応しました。
ピストンジョイントの基本
ピストンジョイントを使うと一方向のみに動きを限定することができます。
動きのイメージはレールのほうが近いかもしれません。
モーターを使えばピストン運動をさせたりバネのように反発させることもできますが、モーターが無効ならレール上を自由に動くカーテンのようになります。
ピストンジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef()で設定を作ります。
let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
そして回転ジョイントと同じくInitializeを実行しますが、先に軸の設定をします。
どちらの方向にピストンが動くかはb2Vec2というベクトルを使います。
ベクトルはいつもよく使っているbox2d.vec2(x, y)ではうまくいきません。
記載方法は、
右向き(xがプラスになる) let axis = new b2.Box2DWeb.Common.Math.b2Vec2(1, 0);
下向き(yがマイナスになる) let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);
のようにしてaxisという名前のベクトルを作ります。
斜め方向の場合は、角度をdirとして以下のようにします。
let axis = new b2.Box2DWeb.Common.Math.b2Vec2(Math.cos(dir/180*Math.PI), Math.sin(dir/180*Math.PI));
(1, 0)のところを(2, 0)のように大きくすると動きが変わってきますが、どうなっているのかよくわかりません。あまり実用性はないと思います。
軸が決まったら以下のようにInitializeします。
def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
順に、固定する物体、移動する物体、アンカーの位置、軸、です。
3つ目のアンカーの位置となっていますが、回転ジョイントの回転軸と違って特に場所は指定しなくても動きが決まるはずなので、正直意味不明です。
離れた場所に指定してもその場所に動くわけではありません。しかしピストンの動きがおかしくなるので、上の例のように動かす物体の場所body2.b2Body.GetPosition()が良いと思います。
最後に設定でジョイントを作ります。
box2d.world.CreateJoint(def);
ピストンを動かす
ピストンに動力を与えるには以下の3つのパラメータを設定します。
回転ジョイントと似ているところは詳細を省略します。回転のときは力がTorqueでしたがこちらはForceになります。
- maxMotorForce
移動に加えられる最大の力
値の基準は1000などだが、10~100000のように大きく変えて探るのがおすすめ - motorSpeed
移動のスピード
1程度あれば移動しているのがわかる
プラスで軸の方向 マイナスで軸の逆の方向 - enableMotor
モーターを有効にするかどうか
trueかfalseを指定
ジョイントを作る際に設定するには以下のように記載してからCreateJointをやります。
let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);
def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
def.maxMotorForce = 1000;
def.motorSpeed = 1;
def.enableMotor = true;
box2d.world.CreateJoint(def);
あとから操作する場合は、以下のように記載します。
let joint = box2d.world.CreateJoint(def);
︙
joint.SetMotorForce(2000);
joint.SetMotorSpeed(2);
joint.EnableMotor(false);
ピストンの可動範囲を設定する
可動範囲の設定にはTranslationの上下限を設定します。
Translationと聞くと翻訳しか思いつきませんが、移動という意味もあるようです。
軸の方向にどれだけ進んだかを示します。
- lowerTranslation
可動範囲の最小値
通常の座標の単位ではなく物理世界の単位で指定
通常の単位で50の場合、 50 / world.scale のようにして変換する
マイナスにすると軸とは逆の方向まで進める - upperTranslation
可動範囲の最大値 - enableLimit
可動範囲を有効にするかどうか
Motorの設定と合わせると以下のように記載します。
let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);
def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
def.maxMotorForce = 1000;
def.motorSpeed = 1;
def.enableMotor = true;
def.lowerTranslation = -10 / world.scale;
def.upperTranslation = 50 / world.scale;
def.enableLimit = true;
box2d.world.CreateJoint(def);
あとから操作する場合は、回転ジョイントと同じように以下のように記載します。
joint.SetLimits(-20 / world.scale, -100 / world.scale);
joint.EnableLimit(false);
ピストンの現在位置を読み取る
ピストンの移動量によってイベントを起こす場合、GetJointTranslationを使います。
スイッチのように押されたときに処理を実施する場合以下のような条件を使います。
scene.onUpdate.add(function () {
if (joint.GetJointTranslation()*world.scale < 10) {
︙
}
});
バネの動きをさせる
motorForceとmotorSpeedが固定だと弾力のある感触は表現できません。
ピストンの変化量とMotorSpeedを連動させるとバネっぽくなります。
let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);
def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
def.maxMotorForce = 1000;
def.motorSpeed = 1;
def.enableMotor = true;
def.lowerTranslation = -100 / world.scale;
def.upperTranslation = 100 / world.scale;
def.enableLimit = true;
let joint = box2d.world.CreateJoint(def);
scene.onUpdate.add(function () {
joint.SetMotorSpeed(-0.2 * joint.GetJointTranslation()*world.scale);
});
world.scaleでの変換は必須ではありませんが、変化量のイメージがしやすくなります。
この辺のやり方は公式だとこちらにもあります。
書き方は割と違いますが、ブラウザで動かせるサンプルがあるのがわかりやすいです。
固定するジョイントの基本
サンプルコードは用意していませんがウェルドジョイントもシンプルで使えそうです。
物体と物体を溶接のように固定してくっつけるだけです。
複数の物体をくっつけて複雑な形状を実現する使い方もありますが、ゲーム中に接触したら付着するという動的な使い方ができます。
以下ようにInitializeだけで固定することができます。
let def = new b2.Box2DWeb.Dynamics.Joints.b2WeldJointDef();
def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition());
let joint = box2d.world.CreateJoint(def);
また、追加でStiffnessという値も設定することができます。
たぶん、 def.stiffness = 1 や SetStiffness(1)で使えると思います。
使ったことがないので値の基準はわかりません。
Stiffnessの値以上の力が加わったら溶接が外れるので、破壊表現等にも使えそうです。
Stiffnessの説明ですが間違っていた可能性があります。説明を見ると柔らかさ寄りの話で、壊れるものではなさそうです。どこかにそう書いていた記憶があるんですが確認できませんでした。実際に動かして試そうとしたのですが、これもうまくいきませんでした。厳密に言うと、あっているのか間違っているのかもわかりません。
単純に外すときはマウスジョイントと同じようにDestroyJointでいけると思います。
box2d.world.DestroyJoint(joint);
ジョイントの紹介も今回で終わりです。
まだ紹介していない中では滑車のプーリーがありますが、まああまり使うことはないかと思います。
この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。