ニコ生ゲームを作ろうと思ったら物理的に考える その2
今回も物理演算を利用したゲームをやっていきます。
物体の操作などを追加していきます。
以下がサンプルです。音は抜いています。
■目次
- 重力の設定
- 物体の操作 速度指定
- スリープ状態
- フリック操作の速度
- 速度の減衰 damping
- 反発係数 restitution
- 物体生成位置の重なり
- 物体のリストアップ
- 親エンティティの乗り換え
重力の設定
今回は部屋を真上から見下ろして掃除をするゲームです。
見下ろしの視点では重力をゼロにします。
見たままですが、388行目に重力gravityの設定があります。
let b2 = require("@akashic-extension/akashic-box2d");
let world = {
gravity: [0, 0],
scale: 50,
sleep: true,
};
let box2d = new b2.Box2D(world);
物体の操作 速度指定
前回は物体を生成するだけで無理やりゲームにしていました。
今回は物体を操作していきます。
物体の操作は4種類あります。力を加える、衝撃を加える、速度を指定、位置を指定。
それぞれのざっくりした説明をします。
- 力を加える .ApplyForce()
力を加えた後はずっと加速する。重力が追加されるようなイメージ。
止めるには力をリセットする必要がある?
使い所がわからない。レースゲームのアクセルとか?加速弾とか? - 衝撃を加える .ApplyImpulse()
ある力を短い時間与えたことになる。一瞬で加速しその後は加速しない。
簡単にちょっと動かしたいのに向く。
同じ衝撃でも動かす物の重さ(密度と面積)によって速度が変わってしまう。
エアリアルサッカー、ドアラッシュのプレイヤーに使用。 - 速度を指定する .SetLinearVelocity()
直後の速度が決まる。その後は加速しない。
動きをイメージしやすい。
重さが違っても同じ速度になるが、重さによって与えた衝撃の大きさが違うことになり、違和感が生まれることもある。
エアリアルサッカーのキーパーとドアラッシュの壁とドアに使用。今回も使用。 - 位置を指定する .SetPosition()
ワープする。
移動前の速度はそのまま。
操作というより設置のイメージが強い。
こちらの方の記事にも少し比較が書かれています。
今回は速度の指定を使いますが、790行目の以下の書き方です。
dust[i].b2Body.SetLinearVelocity(box2d.vec2(vx*g.game.fps, vy*g.game.fps));
まず b2Body.SetLinearVelocity() という書き方で速度を設定します。
カッコの中はX方向の速度とY方向の速度を指定しますが、ここで box2d.vec2(x, y) という書き方が出てきます。
エンティティの座標 x, y などは普段ピクセル数を使っていますが、box2d.vec2を使うとbox2dで使える座標や速度に変換してくれます。
ただしこの b2Body.SetLinearVelocity() はこのままでは動きません。
次の章も合わせてお読みください。
スリープ状態
スリープの解除
物体にはスリープ状態があり、スリープ状態の物体は速度を設定しても動きません。
スリープ状態を解除するのは .b2Body.SetAwake(true) と書きます。
前章の速度の設定と合わせると以下のセットになります。789行目です。
dust[i].b2Body.SetAwake(true);
dust[i].b2Body.SetLinearVelocity(box2d.vec2(vx*g.game.fps, vy*g.game.fps));
速度の .SetLinearVelocity() と同じく位置の .SetPosition() もスリープでは動きません。
逆に衝撃の .ApplyImpulse() は動きます。強制的にスリープが解除されます。
力の.ApplyForce()も多分動くほうだと思います。
スリープの意味
物体は動きがなくなると自動的にスリープ状態に移行します。
物体を生成した直後も動きがないのでスリープになってしまいます。
なぜスリープという面倒なものがあるのかは物理演算を大幅に省略するためです。
物体が数個と少なければ無くしてもいいですが、多いなら極力スリープにすべきです。
自動スリープの設定
自動でスリープにする設定は、重力と同じ場所にあります。
let world = {
gravity: [0, 0],
scale: 50,
sleep: true,
};
sleep: true のところが、「動きがなければ自動でスリープに入る」という設定です。
物体の数が少ないのであれば false に変えても大丈夫でしょう。
もう一つ、物体を個別にスリープに入らないように設定することもできます。
box2d.createBodyDef に allowSleep: false, を追加します。
let bodydef = box2d.createBodyDef({
type: b2.BodyType.Static, // 静止:Static 運動:Kinematic 動的:Dynamic
linearDamping: 0, // 速度の減衰率 0~3ぐらい
angularDamping: 0, // 回転速度の減衰率 0~3ぐらい
allowSleep: false,
userData: 0,//識別用
});
あとからこの設定を追加・操作することも多分できます。こちらです。
body.b2Body.SetSleepingAllowed(false);
ただ、 .SetLinearVelocity() と .SetPosition() のときだけなのであれば、
毎回 .b2Body.SetAwake(true); してあとは自動スリープに任せてもいい気もします。
フリック操作の速度
この項目は物理演算とは直接関係ありません。
フリック速度を算出してゲーム内で利用したのですが、意外とめんどくさかったです。
ドラッグ中の.onPointMove.addではev.point.x、ev.StartDelta.x; 、ev.prevDelta.xの3つが使えます。
その瞬間に動かした量は ev.prevDelta.x なので速度vxは以下のように行けそうですが、
touch.onPointMove.add(function(ev) {
vx = ev.prevDelta.x;
});
これはデバイスによって大きく差が出たりして、うまくいきません。
おそらく.onPointMove.addの発生する頻度がデバイスによって違うのだと思います。
そもそも速度にするには1秒あたりに進んだ量や、1フレームあたりに進んだ量というように時間の幅を決める必要があります。
ev.startDelta.xの方を使って、以下のようなイメージの処理に変えます。
touch.onPointMove.add(function(ev) {
bx = ev.startDelta.x;
});
touch.onUpdate.add(function() {
vx = bx - ax;
ax = bx;
});
touch.onPointUp.add(function(ev) {
ax = 0;
by = 0;
});
これであれば touch.onUpdate.add が1フレーム(1/30秒)ごとに処理されて時間の幅が定まります。
ただこれでもスムーズさが足りません。XとYの速度に基づいて向きを示す四角いエンティティを表示させていますが、ガタガタしてしまいます。
このため、2フレーム分の速度で平均化するため以下のような処理も追加しています。
762行目に類似処理があります。
let touchx = [0,0,0];
let touchy = [0,0,0];
touch.onPointMove.add(function(ev) {
touchx[3] = ev.startDelta.x;
touchy[3] = ev.startDelta.y;
});
touch.onUpdate.add(function() {
if (touchx.length > 3) touchx.shift();
if (touchy.length > 3) touchy.shift();
let vx = (touchx[2] - touchx[0])/2;
let vy = (touchy[2] - touchy[0])/2;
});
touch.onPointUp.add(function(ev) {
touchx = [0,0,0];
touchy = [0,0,0];
});
最後に b2Body.SetLinearVelocity で使用するための補正をかけます。
上記で算出した vx は1フレームあたりに進むピクセル数です。
しかし、box2dで使う単位は秒速メートル[m/s]のようです。
ピクセル数をメートルに変換するのはbox2d.vec()に入れればOKです。
そして1フレームを1秒に変換するため g.game.fpsをかけておきます。
結果、790行目のようになります。
dust[i].b2Body.SetLinearVelocity(box2d.vec2(vx*g.game.fps, vy*g.game.fps));
時間の単位は公式などから明確な説明を見つけられなかったんですが、何となく合ってるのでこれで大丈夫だと思います。
速度の減衰 damping
物体に速度を設定すると、減衰率も設定しないといつまでも同じ速度で動きます。
今回の物体は摩擦ですぐ止まっている感じにしたいので、減衰率 linearDamping を3に設定します。500行目にあります。
let bodydef = box2d.createBodyDef({
type: b2.BodyType.Dynamic, // 静止:Static 運動:Kinematic 動的:Dynamic
linearDamping: 3, // 速度の減衰率 0~3ぐらい
angularDamping: 3, // 回転速度の減衰率 0~3ぐらい
userData: 1,//識別用
});
ついでに回転速度の減衰率 angularDamping も設定しておきます。
これがないといつまでも回り続けるハンドスピナーのようになります。
今回は見下ろし型だったので、速度の減衰率で摩擦を表現しています。
重力があるタイプの横視点であれば、速度の減衰率で空気抵抗を表現できます。
エアリアルサッカーではボールをふわっとさせるのに若干減衰率を強くしていました。
また、box2d.createFixtureDef にも摩擦係数を設定できますが、こちらは物体と物体の摩擦なので今回はあまり影響がありません。
反発係数 restitution
反発係数はゴミが壁際に集まりやすいように、低めに0.2にしました。
let fixturedef = box2d.createFixtureDef({
density: 1.0, // 密度
friction: 0.5, // 摩擦係数
restitution: 0.2, // 反発係数
shape: box2d.createRectShape(width, height) // 形状
});
この値は現実世界では基本的に1より小さくなりますが、あえて2とかにすると動きが加速して面白くなります。ジャンプ台のようなこともできます。
逆に0にすれば張り付くように跳ねないようにすることもできます。
ただし、衝突する両方の物体の反発係数を0にしないとだめのようです。
物体生成位置の重なり
物体は同じ位置に生成させることができますが、
時が動いた次の瞬間にどちらかまたは両方が弾かれます。
今回のゲームでは中央にいくつか障害物の物体を配置し、多数のゴミの物体をランダムに配置します。
ランダムだと位置が重なってしまうので障害物を避けて配置する必要があるかなと思ったんですが、結論から言うとうまい具合に障害物の外に弾かれてなんとかなりました。
ただし、外周の壁の中に埋め込まれると外側に弾かれてしまって操作できなくなる可能性はあります。このため壁よりは内側に配置するようにしています。
また、複数の物体に挟まれて埋め込まれた場合は埋め込まれたまま止まってしまうこともあります。
物体のリストアップ
物理世界にいくつの物体があるかは box2d.bodies.length で簡単に取得できます。
711行目にあります。
今回はちりとりに入れたゴミの物理的な実体を削除していたので、物体全体の数から壁と障害物の数を引いてすべてのゴミが無くなったか判定しました。
box2d.bodies が物体のリストなので以下のような使い方もできます。
for (let i = 0; i < box2d.bodies.length; i++) {
box2d.bodies[i].b2Body.SetAwake(true);
}
親エンティティの乗り換え
物理エンジンとは直接関係ありませんが、今回ゴミの物理的な実体を削除したあとにエンティティの方は残しました。
ちりとりに収納された感じにするために、ちりとりのエンティティに付いて動くように
親エンティティを変更しました。691行目がそれです。
chiritori.append(dust[i].entity);
初期の説明にあったエンティティを生成する際に記載していたscene.append(entity) と同じですね。
親エンティティを変えるというより、親エンティティに追加する感じです。
今回は以上です。
この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。