ニコ生ゲームを作ろうと思ったら物理的に考える その4
今回は物体の運動と衝突の場合分けを紹介していきます。
以下がサンプルです。音は抜いています。
■目次
- 物体を運動させる
- 衝突の有無を場合分けする
- 衝突の場合分けを変更する
- 衝突処理の重さについて
物体を運動させる
今回のゲームはコイン落としです。
コインを押す壁は物体としてコインを押しつつ、決められた動きが必要です。
この運動する物体の設定には box2d.createBodyDef のtypeに b2.BodyType.Kinematic を使います。サンプルコードの563行目に以下の記載があります。
let bodydef = box2d.createBodyDef({
type: b2.BodyType.Kinematic,
linearDamping: 0,
angularDamping: 0,
userData: "table",
});
以前出てきた設定のStaticだとSetLinearVelocityなどを使うことができません。
SetPositionは使えますが、毎フレームSetPositionを使ってシームレスに動かしても、
衝突された時に静止した物体として衝突したことになり動きの勢いが加わりません。
例えばバットでボールを飛ばすゲームの場合は SetPosition以外の方法が必要です。
今回の動く壁はゆっくりコインを押し出すだけなのでStaticとSetPositionでも良かったのですが、587行目のようにSetLinearVelocityを使いました。
もう一つの設定DynamicはSetLinearVelocityを使えますが、他の物体に押されて動きがずれてしまいます。他の物体を強制的に動かすような物体には使えません。
SetLinearVelocity のデメリットとしては動きのイメージがしづらいところですね。SetPositionなら行きたいところを直接指定できます。
往復運動などの周期的な動きも注意が必要です。SetLinearVelocityの場合は周期を終えたときに元の場所に戻っていないと、繰り返すうちにどんどんずれていきます。
衝突の有無を場合分けする
今回のサンプルではコインが存在する段の違いを衝突処理の場合分けで表現しました。
1段目のコインは動く壁に接触し、2段目のコインは動く壁には接触しません。
公式の情報
残念ながらこの辺のことはこちらの基本紹介ページには書いてありません。
こちらのサンプルプログラムで使われていて、コードにコメントが有るのですが…
正直なところ記載内容とコメント内容と実際の挙動の対応がよくわかりません。
さらにBox2dの公式までさかのぼるとこちらに英語で説明があります。
がしかし実際にどう書けばどうなるかの例がありません。
というわけでいくつかのパターンでやってみます。
フィルターの基本
衝突の場合分けをするには box2d.createFixtureDef に filter の情報を記載します。
以下のような形です。
let fixturedef = box2d.createFixtureDef({
density: 1.0, // 密度
friction: 0.5, // 摩擦係数
restitution: 0.3, // 反発係数
shape: box2d.createRectShape(width, height), // 形状
filter: {
groupIndex: 1,
categoryBits: 1,
maskBits: 1
}
});
filterのなかに groupIndex categoryBits maskBits の3つを入れています。
categoryBits と maskBits は基本的にセットですが、常に3つが必要とは限りません。
大まかな説明をすると以下の通りになります。
- groupIndex:ざっくりグループを分ける際の番号 プラスやマイナスの整数
- categoryBits:細かく衝突対象を設定する際に自分につける番号 プラスの整数
- maskBits:細かく衝突対象を設定する際の衝突対象の番号 プラスの整数
フィルターを使わない場合
filterの値がない物体同士は衝突判定を持ちます。これまでやってきたとおりです。
片方どちらかがDynamicの物体ならはじきあって以下のイメージになります。
ざっくりグループで分ける場合
出てくる物体をグループ1、グループ2のように分け、それぞれのグループ内だけで衝突すればいい場合、groupIndexを使うだけでできます。
例として3つの物体を下の設定で作ります。
四角:filter: { groupIndex: 1}
丸 :filter: { groupIndex: 1}
菱形:filter: { groupIndex: 2}
下のように、菱形だけが他と衝突しなくなります。
衝突しないようにする場合
groupIndexを0かマイナスにすると「衝突しない」になります。
filter: { groupIndex: -1} の物体が2つあってもそれ同士は衝突しません。
四角:filter: { groupIndex: -1}
丸 :filter: { groupIndex: -1}
菱形:filter: { groupIndex: 1}
上のような場合では四角と丸は衝突しません。
菱形だけgroupIndexをプラスにしていますが、やはりgroupIndexが違うためどの組み合わせも衝突しません。
ここでちょっと混乱するんですが、公式のサンプルコードだとgroupIndexが-1の物体同士で衝突させています。上で書いたルールと異なりますが、公式のコードを再現するとやはり衝突しないので、仕様変更前のコードだったのかもしれません。
自分のコピーとは衝突させない場合
大量の弾をばらまいて壁に衝突させるが、弾同士は衝突してほしくないというような場合、groupIndexだけでは設定が困難です。
そこで categoryBits と maskBits を使って衝突対象をひも付けます。
四角:filter: { categoryBits: 1, maskBits: 2 }
丸1:filter: { categoryBits: 2, maskBits: 1 }
丸2:filter: { categoryBits: 2, maskBits: 1 }
上のように丸同士は衝突しませんが、両方とも四角とは衝突します。
categoryBitsがそれぞれの物体のIDのようなものです。
maskBitsでそのIDを指定していると衝突します。
このフィルター設定には注意点がいくつかあります。
- 互いにmaskBitsで指定し合う
片方からmaskBitsで指定しただけでは衝突しません
以下の例は衝突しません
四角:filter: { categoryBits: 1, maskBits: 2 }
丸 :filter: { categoryBits: 2, maskBits: 4 } - categoryBitsとmaskBitsは両方揃えて記載する
どちらかがかけていたり両方ない場合は基本衝突しません。
groupIndexがそろっている場合は衝突するケースもあります。 - 値を0にしたものは衝突しない
以下の例は衝突しません
四角:filter: { categoryBits: 1, maskBits: 0 }
丸 :filter: { categoryBits: 0, maskBits: 1 } - 値は 1, 2, 4, 8,… のように2の自乗のみ使うとわかりやすい
上記以外を使うとややこしいので、これらの値を使うのがおすすめです。
2の15乗の32768まではつかえるはずです。
3や5も使えるのですが、複数の衝突対象が指定されて複雑な条件になります。
複雑な指定は次の章で説明しています。
複数の衝突対象を設定する場合
ここからは進んだことがやりたい時に確認してください。
そうでなければ無視してください。
1, 2, 4, 8…だけで指定していると、複数のcategoryBitsに対して衝突を指定できません。
複数の指定をする場合は2進数を使うとやりやすくなります。
2進数は最初に「0b」(ゼロビー)をつけて、その後に「0」「1」のどちらかだけを使って記載します。
例 : 0b0001 、 0b001011011 、 0b11111111111111111
そして衝突先のcategoryBits と 衝突元のmaskBits の値の
同じ桁に両方「1」があれば衝突します。
例1: 先 0b0001 元 0b0011 → 右から1桁目が両方1なので衝突する
例2: 先 0b1100 元 0b0011 → どの桁も両方1になっていないので衝突しない
物体の例を作るとこのような形です。
四角:filter: { categoryBits: 0b0001, maskBits: 0b0110 }
丸 :filter: { categoryBits: 0b0010, maskBits: 0b0001 }
菱形:filter: { categoryBits: 0b0100, maskBits: 0b0001 }
この例では四角が丸と菱形に衝突し、丸と菱形同士は衝突しません。
これだけだと複雑な例とは言えませんが、物体の種類が増えても対応できます。
桁数はたぶん16桁まで大丈夫です。
桁数が違うものが混ざっていても右端からの桁数で比較されます。
その他の注意点
ここは非常に細かい法則についての記載です。
なにか問題が発生したときだけ確認してください。
groupIndexがそろっているとmaskBItsよりも優先される
groupIndexが1の物体同士はmaskBItsがなんであれ衝突します。
groupIndexが-1の物体同士はmaskBitsがなんであれ衝突しません。
groupIndexがそろっていないとmaskBItsが優先される
groupIndexが-1の物体とgroupIndexが-2の物体の衝突はmaskBItsの値次第です。
groupIndexが0でそろっているとmaskBItsが優先される
groupIndexが0の物体同士の衝突はmaskBItsの値次第です。
0のときのみ例外としてmaskBitsが優先されます。maskBitsがなければ衝突しません。
filterを指定しないと既定の値が採用される
既定は以下のような値です。
filter: {
groupIndex: 0,
categoryBits: 0b1,
maskBits: 0b1111111111111111
}
これはfilterの項目を全く記載せず省略した時の話です。
filter: { } のように中が空のfliterを記載するのは別で、こちらは全く衝突しません。
衝突の場合分けを変更する
[2022/4/10追記]こちらの章にfixtureを削除する方法の記載を追加しました。
衝突の条件を変更する場合は衝突判定であるfixtureを削除して新しく追加します。
.b2Body.CreateFixture() を使います。
fixtureの削除
fixtureの削除には.b2Body.DestroyFixture() を使いますが、どのfixtureなのかを指定する必要があります。
fixtureに名前をつけていればそれを指定してもいいのですが、うまく名前を使い回せないこともあるので、今回は単に物体についているfixtureを.b2Body.GetFixtureList()で指定します。
body.b2Body.DestroyFixture(body.b2Body.GetFixtureList());
これでbodyのfixtureを削除できます。fixtureが一つだとどれということもありません。
.GetFixtureList()はfixtureのリストを取得できますがいわゆる配列とは違うようです。
.GetFixtureList()の記載だけでリストの最初のfixtureを指定したことになるそうです。
リストの最初というのは、色々検証してみたところ一番最後に登録したfixtureのことのようです。
fixtureの登録
新しくfixtureを物体に登録するには.b2Body.CreateFixtureを使います。
body.b2Body.CreateFixture(fixturedef);
カッコの中に入れるのは「fixture」ではなく「fixturedef」です。
fixturedefはfixtureの設定データのようなものです。
ちなみに let newfixture = body.b2Body.CreateFixture(fixturedef) のように書けば、fixtureに newfixture という名前をつけることもできます。
filterの異なるfixturedefの準備
今回のサンプルコードでは段階に分けてコインのfixturedefを3つ用意しています。
投入するコインの移動中は衝突をなくし、上端に到達した段階で上の壁とコイン同士で衝突するように変更します。(673行目でfixturedef1に変更。)
そして、動く壁よりも下に移動した段階で、動く壁とも衝突するように変更します。
(1015行目でfixturedef2に変更。)
投入中のコイン 596行目
let fixturedef0 = box2d.createFixtureDef({
︙
filter: {
groupIndex: -1, //飛んでいるときはコイン同士は当たらない
categoryBits: 0b100,
maskBits: 0b000
}
});
動く壁の上のコイン 607行目
let fixturedef1 = box2d.createFixtureDef({
︙
filter: {
groupIndex: 1,
categoryBits: 0b100,
maskBits: 0b001
}
});
動く壁の下のコイン 618行目
let fixturedef2 = box2d.createFixtureDef({
︙
filter: {
groupIndex: 1,
categoryBits: 0b100,
maskBits: 0b011 //テーブルに当たるようになる
}
});
動く壁 569行目
let fixturedef = box2d.createFixtureDef({
︙
filter: {
// groupIndex: 1,
categoryBits: 0b010,
maskBits: 0b100
}
});
上と左右の壁 545行目
let fixturedef = box2d.createFixtureDef({
︙
});
filter記載なし
SetFilterDataというものもあるようなのですが使い方がいまいちわかりませんでした。
この方法を使えば物体を一時的にStaticにして静止させることもできそうですね。
衝突処理の重さ
物理の衝突は結構重く、動作がカクカクしてきます。
今回のコインの数はもとはもう少し多かったのですが、重さを見て調整しました。にもかかわらず一部の方のプレイに支障をきたしており、ギリギリアウトの領域です。
台の上に配置される物体が60個程度。画面内に投入される物体で最大100個弱になります。
※[2022/4/10追記] 物体の判定が重複していたため実際には200個程度ありました。
200個ぐらいまでは大丈夫かもしれません。
この状況でニコ生ゲーム中の処理の重さはどうなるかというと、PCは多少カクつくもののプレイにはまず問題ありません。
スマホでも問題なくプレイできる方がいる一方で、一部の方は重くてプレイできないようです。
このゲームのデザイン的に多少カクついても眺めていればいいのですが、完全に止まってしまうようだとプレイに支障をきたします。処理が追いつかなくて、ランキング用のスコア送信がスキップされることも起きている気がします。
ゲームによってぜんぜん違うと思いますが、100個の自由に動く物体というのは一つの基準になりそうです。エアリアルサッカーも動作が完全に止まるのを防ぐために100人までにプレイヤーを制限した記憶があります。
シビアなタイミングの操作が要求されるゲームの場合は更に物体または衝突の数を減らして動作を軽くする必要があるでしょう。
エアリアルサッカーはこれに該当しますが…マルチであることによりさらに重くなっているようで、物体が少なくてもカクツキを抑えきれていません。
また、akashic-sandboxでの動作確認よりも生放送中の方が処理が重くなる傾向があります。放送で重さを確認した後に修正することも考えたいところですが、スコアボードを運用していると根本的な修正は悩ましいですね。
今回はここまでです。
公式が出しているbox2dの情報が、逆引きリファレンスのこちらにありました。
シンプルになっていてわかりやすいです。以前はなかったと思うのですが、気づかなかっただけかもしれません。
サンプルコードもこちらにあります。
このタイプのサンプルコード紹介サイトはブラウザ上でコードを書き換えて、右上の更新マークを押せば反映させることができます。
コードの違いと実際の動きを確認したいときに便利です。
ただ、エンティティの位置の算出方法がちょっとよくわからないような…
こちらのAkashic Engine 逆引きリファレンスは順次記事が増えるようです
また、大きな変更事項はこちらでアナウンスされるとのことです。
どちらも公式のプルダウンメニューから飛べます。
この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。