物理エンジンの最適化:Collision Layerとビット演算の仕組みをCPU視点で解説 – 物理エンジン原理その2 [Godot, Unity, C#]
August です.

今回のブログ割と短くなると考える.
物理エンジンシリーズその1 を続いて, 今回はレイヤー・マスクとその原理であるビットマスクについて一緒に見てみたい.
余談だが, この記事を書こうと思ったのは, ゲーム会社新卒採用用のプログラミングテストで, Bitシフト演算に関する問題が出てきて,
それに苦労したから, 勉強・復習がてらブログとしてまとめたいと考える.
目次
Layer Maskでできること
原理を説明する前に, まず「なぜこれ知る必要ある?」から説明したい.
一言で言えば,
「無駄な衝突判定をキャンセルして, 処理落ちを防ぐため」
ゲーム内に「弾丸」が100個, 「敵」が100体いるとしよう.
もし何も設定しなければ, コンピュータは馬鹿正直に全ての組み合わせ,「当たったのは敵? それとも他の弾丸?」など, 100(弾) × 100(敵) = 10,000回以上の計算をしようとする.
ここに「背景」や「アイテム」も加わると, 計算量は指数関数的に増え, スマホは熱くなり, フレームレートはガタ落ちするし, そもそも「弾丸どうしで反応するな」って設置したくてもいちいち書かなければならない.
そこで, 「お前はこれとだけ当たればいい, 他は無視しろ」と命令する必要がある. これを管理するのがLayerとMaskの関係である.
- Layer (レイヤー):
「自分は何者か?」を表す名札 (例:私は「敵チーム」です) - Mask (マスク):
「誰と当たりたいか?」を表す招待リスト (例:私は「味方チーム」と「壁」にだけ反応)
物理エンジンは, 衝突計算という重い計算を始める前に, このLayerとMaskを見て「あ, こいつら無関係だわ」と判断したら, 0.0001秒で処理をスキップ(門前払い)する.
この「門前払い」の判定こそが, 今回解説するビット演算そのものである.
さらに, 「なぜゲームエンジンに設定できるレイヤーの上限が, 32なのか」もついでに分かるようになる.
Unity vs Godot: アプローチの違い
物理エンジンの設定画面を見ると, エンジンの設計思想がよく分かる.
特に Unityの「Collision Matrix」 と Godotの「Collision Layer/Mask」 は, その最たる例の一つだと考える.
1. Unity: 中央集権型 (Collision Matrix)
Unityユーザーにはおなじみの, 巨大なマトリックス表.


これは「世界全体のルール」を1箇所で管理するアプローチ. 安全で分かりやすい? が, 1つの大きな制約がある
1つのGameObjectは, 1つのLayerにしか所属できない.
つまり, 「水 (Water)」であり「毒 (Poison)」でもある沼を作りたい場合, Unityでは WaterAndPoison という新しいレイヤーを作るか, タグで判定するなどの工夫が必要になる.
「あれとこれが当たる」という関係性をマトリックスで一元管理するため, 複雑な属性を持つオブジェクトが増えると, マトリックスの管理が指数関数的に面倒になることがある.
2. Godot: 分散型 (Layer & Mask)
対してGodotは, 各オブジェクトが個別に「自分は何者か (Layer)」「何と当たるか (Mask)」をビット単位で保持している.
これは物理エンジンの内部ロジック (ビット演算) をそのままユーザーに露出させている形.

メリット:
1つのオブジェクトが複数のレイヤーに同時に所属できる.
例えば, 弾丸に Projectile, LightSource, Magic の3つのビットを立てておけば, それぞれに対応するマスクを持つ壁や敵と適切に衝突する.
デメリット?:
個人的にこれをデメリットとして見ないが,
要は, 物体一個一個に対応するマスクを設定しないといけない. (或いは共通する設定を保存して呼び出す)
「直感的」なのはUnityの表(本当か?)だが, 「機械的・論理的」なのはGodotの方式だと考える.
どちらも最終的には同じ計算(ビット演算)に帰結する.
Layer Maskの原理:ビットマスク
ビットって何? って思った人はまずそれに関して検索した方がいい. 僕より遥かに説明が上手い人らが解説してくれる.
簡単に言うと,
我々が使う通常計算機において, 全てのデータは, 二進数[0,1]で表現される.
これの単位が, ビットである.
つまり, 1bit = 0 or 1 = True or False
通常変数(Primitives)の根本的な表現
情報系なら常識同然な知識だが, int, floatなどのprimitive型は, それぞれ所要するビットサイズも異なる.
C#において, intやfloatは, 4byte = 32 bitかかる. bool型は, 本来1bitで表現できる. が, これは本当はある意味意味を持たない.
なぜなら, CPUは「ビット単位でメモリにアクセスできない」から.
CPUが一度に掴める最小単位は「バイト (8bit)」(実際に, 一般的に64BitのCPUが一度のフェッチで掴む量は, 64Bit = 8 Byte.)
つまり, たった1bitの true/false を表現するために, プログラム上では贅沢にも8bit分(1バイト)の領域を確保してしまう.
これをn個のフラグ(状態異常, レイヤー, アイテム判定など)でやろうとすると, n回のメモリアクセスと無駄な領域が必要になる.
解決策:ビットマスク
Bitmask(ビットマスク)は, まさにこれを解決するためにある.
bool をバラバラに持つのではなく, 「1つの int (32bit) の中に, 32個のスイッチを詰め込んでしまおう」 という発想.
これなら, たった1回の処理で32個分までの判定を一気に扱える.
「なぜゲームエンジンに設定できるレイヤーの上限が, 32なのか」=> この32は, これが原因である.
でもこれどうやって管理する?
ここで「ビット演算」が出てくる.
怖くないビット演算
「うわ, 計算だ」とブラウザバックしないでほしい.
実はやっていることは, 「ID決め」と「合成」と「確認」, 怖いのは記号だけ.
以下の5つのレイヤーがあるとして, どうやってスイッチを管理するか見ていこう.
[ 地形, 味方, 味方の弾, 敵, 敵の弾]
1. シフト演算 (<<):IDを決める
各レイヤーに「会員番号」を振る作業.1 << n と書けば, 「1を左へ n個 ズラす」という意味になる. これがそのままレイヤーのIDになる.
// 定義
// 一番右(0番目)から順にスイッチを割り振るイメージ
int LAYER_地形 = 1 << 0; // ...00001 (1)
int LAYER_味方 = 1 << 1; // ...00010 (2)
int LAYER_味方_PROJ = 1 << 2; // ...00100 (4)
int LAYER_敵 = 1 << 3; // ...01000 (8)
int LAYER_敵_PROJ = 1 << 4; // ...10000 (16)2. OR演算 (|):マスクを作る(合成)
複数のレイヤーを対象にしたい場合,
足し算…ではなく OR演算(|) を使う.
ビットマスクにおいては「合成(リストアップ)」と考えていい.
例:味方の飛び道具 (LAYER_味方_PROJ) の当たり判定を作ってみよう.
仕様:「地形」と「敵」の両方に当たる.
// マスクの作成 (地形 と 敵 をくっつける)
int hitMask = LAYER_地形 | LAYER_敵;
// 計算の中身:
// 00001 (地形: 1)
// | 01000 (敵: 8)
// --------
// 01001 (Mask値: 9)
これで「地形と敵に当たる」というリストができた.
3. AND演算 (&):衝突判定(フィルター)
これが物理エンジンの内部で行われている判定の正体.
「自分のマスク(リスト)」の中に「相手のレイヤー」が含まれているか?を確認する作業.
AND演算がTrueになるのは, 両方とも1である場合のみ.
さっき作ったマスク(01001 = 地形と敵)を持つ弾が, 何かにぶつかったとしよう.
ケースA:味方(LAYER_味方 = 2)に当たった場合
味方レイヤーは ...00010 (2)
判定式: (Mask & Target)
01001 (弾のMask)
& 00010 (味方のLayer)
-------
00000 (結果はゼロ = false)
結論:共通点がないので衝突しない(すり抜ける)
ケースB:敵(LAYER_敵 = 8)に当たった場合
敵レイヤーは ...01000 (8)
01001 (弾のMask)
& 01000 (敵のLayer)
-------
01000 (結果はゼロではない = true)
結論:衝突検知! -> ダメージ処理へ
C# (Unity / Godot) での実装例
UnityやGodotのインスペクターでポチポチ設定しているのは, 裏でこの計算を自動化してくれているに過ぎない.
スクリプトで動的に「今は味方だけ狙いたい!」といった制御をする場合, 以下のように書ける.
※GodotのC#でもUnityでも, 基本的な書き方は全く同じ.
[System.Flags] // これを付けるとInspectorで複数選択できるようになる
public enum TargetLayer {
None = 0,
Terrain = 1 << 0, // 地形
Ally = 1 << 1, // 味方
AllyProj = 1 << 2, // 味方の弾
Enemy = 1 << 3, // 敵
EnemyProj = 1 << 4, // 敵の弾
}
void CheckCollision(TargetLayer target) {
// 自分の攻撃対象マスク (地形 + 敵)
TargetLayer myMask = TargetLayer.Terrain | TargetLayer.Enemy;
// ビット演算で判定
// (0ではない = マスクに含まれている)
if ((myMask & target) != 0) {
// Hit!
// ダメージ処理などをここに書く
}
}物理判定以外での活用例
「ビットマスクとか物理エンジンの中だけの話でしょ?」と思うかもしれないが,
「複数の状態を同時に管理する」場面ならどこでも使える.
知っておくと便利な2つのパターンを紹介する.
1. RPGの状態異常 (バフ・デバフ)
毒, 麻痺, 沈黙… これらを bool isPoison, bool isSleep と管理するのはコードが汚れる上に, Inspectorでの管理も面倒だ. [System.Flags] をつけた enum なら, 1つの変数で管理でき, エンジンのInspector上でチェックボックスとして編集できるようになる.
[System.Flags]
public enum StatusEffects {
None = 0,
Poison = 1 << 0,
Silence = 1 << 1,
Paralyse = 1 << 2,
}
// プレイヤーの状態
StatusEffects currentStatus = StatusEffects.Poison | StatusEffects.Paralyse;
// 判定:行動可能か? (HasFlagは便利だが, ビット演算の方が高速)
if ((currentStatus & StatusEffects.Paralyse) != 0) {
return; // 動けない
}
// 治療:毒だけ治す (反転させてAND = 毒だけ消す)
currentStatus &= ~StatusEffects.Poison;まぁ…強いていえば, これは
ビット演算符号が熟知していない人にとって, 非常に読みにくいではある.
2. オートタイル (地形の自動接続)
2Dゲームで壁を並べた時, 自動的に「角」や「T字路」の画像に切り替わるアレ.
実はこれもビットマスクを使うと, 驚くほど単純な足し算で実装できる.
自分の「上・右・下・左」に壁があるかをチェックし, それぞれにIDを振る.
- 上がある = 1
- 右がある = 2
- 下がある = 4
- 左がある = 8
これらを合計すると, 0〜15のユニークな数字が生まれる.
例えば「上(1)と右(2)」に壁があれば 1+2=3.
あとは tile_3.png という名前の「右上の角画像」を用意して読み込むだけ. 複雑な if 分岐は一切いらない.
結論
Layer MaskやCollision Matrixの画像(記事冒頭)を見直してみてほしい.
あれはただの表に見えるが, エンジン内部では巨大な int 同士の AND 演算を高速で行っているだけなのだ.
- Layer = 自分のID (1bitだけ立っている)
- Mask = 当たりたい相手リスト (複数のbitが立っている)
- Layer & Mask != 0 = 衝突
常に2個以上のbool変数を管理・比較する場合, if (isPoisoned || isParalyzed || isSleep) と書くよりも, ビットマスクを使ったほうがCPUにも優しく, コードも拡張性が高い.
食わず嫌いせず, シフト(<<)とOR(|)とAND(&)を使いこなしてみよう.
ではまた.

