マガジン

庭の配置の衝突判定:重ならないように置く

遊びをつくる

執筆にClaudeを使用しています。

チェスのナイトから枝分かれした影が盤面へ伸び、先読みの候補を表す版画
記事のテーマをもとに制作したAI生成イラスト。

庭づくりのゲームで、木のすぐ隣に池を置こうとする場面を考えてみます。置けるのか、置けないのか、少しずらせば置けるのか。この判断をプログラムがどう行うかが、衝突判定(二つの物が重なっているかを調べる処理)です。庭ゲームでは、この判定の方針が遊び心地を大きく左右します。 「余白の庭」は、木・石・草・花・池・灯りを置く、時間制限なしの庭づくりです。確認できているのは、置いたものを移動したり大きさを調整したりできること、戻す/やり直す(Z/Y)があること、最大80個まで置けることです。「重なる位置には置けない」という仕様や、判定の中身については、現在の案内に記載がありません。 そこでこの記事では、庭ゲームで使える最小の衝突判定を図解の代わりに言葉とコードで示します。そのうえで、確認できている仕様から「設計から読み取れること」を整理します。重なりの扱いを実際にどうしているかは、作品を開いて確かめる手順にしてあります。

この記事の目次
  1. 「重なる」を数式にする:円と四角
  2. 総当たりで足りる理由は「80個」にある
  3. 試作:置けない場所を赤く見せる
  4. 余白の庭の仕様から読み取れること
  5. 自分で確かめる観察の手順
  6. 次の行動

「重なる」を数式にする:円と四角

最も単純なのは、すべてのものを円とみなす方法です。中心の座標と半径を持たせ、二つの中心の距離が「半径の和」より小さければ重なっている、と判定します。

距離の計算には平方根が出てきます。ただ、比べるだけなら両辺を二乗して「距離の二乗 < 半径の和の二乗」で足ります。平方根を避けられるので、判定を何度も繰り返す場面で軽くなります。

四角の場合は、横方向と縦方向を別々に見ます。左右の範囲が重なり、かつ上下の範囲も重なっていれば、四角どうしは重なっています。この方式は軸に沿った四角(AABB)と呼ばれます。どちらかの軸で離れていれば、その時点で重なっていないと分かります。

庭の部品で考えると、次のような割り当てが自然です。

  • 木や石や灯り:円で近似する
  • 池:横長なら四角、丸ければ円で近似する
  • 草や花:小さな円、または判定の対象にしない

見た目は不定形でも、判定は単純な図形で十分な場合があります。判定用の図形と、画面に見える絵は別物として持てるからです。

総当たりで足りる理由は「80個」にある

新しく置く候補の位置が決まったら、すでに置いてあるすべてのものと一つずつ比べます。これが総当たりです。一つでも重なれば「置けない」と分かり、そこで比較を打ち切れます。

確認できている上限は最大80個です。すでに79個あるところへ80個目を置く場合でも、比較は最大79回で済みます。一回の比較は掛け算と足し算が数回なので、操作の合間に毎回行っても重い処理になりにくい規模です。

個数が数千、数万になるなら、画面を升目に区切って近くのものだけを比べる方法(空間分割)が必要になります。上限が80個なら、まず総当たりで正しく動かすのが堅実な選択です。この上限の設定自体にも、判定を単純に保てる利点があると読み取れます。ただし、上限を決めた理由が処理の軽さにあるのかは、案内からは分かりません。

試作:置けない場所を赤く見せる

次のHTMLは、円どうしの判定だけで「置けない場所」を示す最小の試作です。保存して開くと、マウスを動かす位置に半透明の円が出ます。置ける位置なら青、既存の円や枠に重なる位置なら赤で表示します。クリックすると、置ける場合だけ配置されます。

html
<canvas id="c" width="360" height="240" style="border:1px solid #888"></canvas>
<script>
const cv = document.getElementById('c'), g = cv.getContext('2d');
const items = [];   // { x, y, r }
const R = 20;       // 置きたいものの半径

function canPlace(x, y, r, skip) {
  if (x - r < 0 || y - r < 0 || x + r > cv.width || y + r > cv.height) return false;
  for (const it of items) {
    if (it === skip) continue;               // 移動中の自分自身は無視
    const dx = x - it.x, dy = y - it.y, d = r + it.r;
    if (dx * dx + dy * dy < d * d) return false;
  }
  return true;
}
function draw(ghost) {
  g.clearRect(0, 0, cv.width, cv.height);
  for (const it of items) {
    g.beginPath(); g.arc(it.x, it.y, it.r, 0, 7);
    g.fillStyle = '#6a9'; g.fill();
  }
  if (ghost) {
    g.beginPath(); g.arc(ghost.x, ghost.y, R, 0, 7);
    g.fillStyle = ghost.ok ? 'rgba(60,120,200,.5)' : 'rgba(220,60,60,.5)';
    g.fill();
  }
}
function pos(e) {
  const b = cv.getBoundingClientRect();
  return { x: e.clientX - b.left, y: e.clientY - b.top };
}
cv.addEventListener('pointermove', e => {
  const p = pos(e); draw({ ...p, ok: canPlace(p.x, p.y, R) });
});
cv.addEventListener('click', e => {
  const p = pos(e);
  if (canPlace(p.x, p.y, R)) items.push({ ...p, r: R });
  draw();
});
draw();
</script>

注目したいのは次の三点です。

  1. 判定は < で比べているため、ちょうど接する位置は「置ける」扱いです。隙間なく並べたいなら < のままにし、必ず余白を空けたいなら半径に少し足します。
  2. skip を渡しているのは、移動やサイズ変更で自分自身と比べて「重なっている」と誤判定するのを防ぐためです。試作では使っていませんが、移動を実装するときに必要になります。
  3. 枠の外に出る判定も同じ関数に入れてあります。「ほかのものに重なる」と「庭の外に出る」は、どちらも置けない理由なので、一か所にまとめると見通しがよくなります。

余白の庭の仕様から読み取れること

確認できている仕様から、判定まわりで考えるべき点を整理します。以下はあくまで「設計から読み取れること」で、実際の内部処理を述べるものではありません。

  • 移動と大きさ調整があること:置く瞬間だけでなく、置いたあとの変更でも重なりが生じえます。判定を「置く時」だけに入れるか、「動かす時・大きさを変える時」にも入れるかは、設計上の分かれ目です。大きさを変えると半径が変わるため、同じ位置でも判定結果が変わりえます。
  • 戻す/やり直す(Z/Y)があること:重なりを許す設計でも許さない設計でも、操作を一つずつ巻き戻せることは試行錯誤を支えます。置けない場所を考え込む必要が薄れ、気軽に置いて確かめられます。
  • 時間制限がないこと:配置の正しさを競う作りではないと読み取れます。そのため、重なりを厳しく禁じるか、緩く許すかは、庭の「らしさ」の問題として選べる余地があります。
  • 保存があること:保存した庭を引き継げるなら、読み込んだ庭の中に重なりがありうるかどうかも、判定の方針とつながります。

案内に書かれていない点は次のとおりです。ここは推測で埋めず、作品を開いて確かめてください。

  • 重なる位置には置けないのか、重なっても置けるのか
  • 置けない場合に、見た目でどう伝えるのか
  • 種類ごとに判定の扱いが違うのか(たとえば草や花は重ねられるのか)

自分で確かめる観察の手順

作品を開いて、次の順に調べると、判定の方針が分かります。結果はここには書きません。ご自身で記録してください。

準備:紙かメモに、下の表の列を作ります。

  • 木と木、近づけ方:少しずつ寄せる、置けたか:、画面の変化:
  • 木と池、近づけ方:池の端に木を寄せる、置けたか:、画面の変化:
  • 石と灯り、近づけ方:ほぼ同じ位置に置く、置けたか:、画面の変化:
  • 草と花、近づけ方:ほぼ同じ位置に置く、置けたか:、画面の変化:

手順

  1. 各組み合わせを、離れた位置から少しずつ近づけて置きます。各組み合わせにつき3回ずつ、近づけ方を変えて試します。
  2. 置いたものを動かして、ほかのものに重ねます。置く時と動かす時で扱いが違うかを見ます。
  3. 大きさ調整を使い、隣のものに届くまで大きくします。そのとき何が起きるかを記録します。
  4. 上限の80個に近づいたあとの挙動も、時間があれば見ます。

結果の読み方

  • 近づけると途中で止まる、または置けない合図が出る場合は、置く時・動かす時に判定をしている設計と考えられます。
  • 大きさ調整だけ重なりを許す場合は、判定が「位置」中心で、大きさ変更後の再判定が緩い設計と考えられます。
  • 種類によって重なる・重ならないが分かれる場合は、判定の図形か、判定の対象外かを種類ごとに持っている設計と考えられます。
  • どこでも重なる場合は、見た目の重なりを許し、自由な配置を優先する設計と考えられます。

起こりやすい誤り:一度試しただけで結論を出すと、置く位置の端数の違いで結果が変わるのを見落とします。同じ組み合わせを近づけ方を変えて繰り返してください。また、見た目の絵の大きさと判定の大きさが一致しているとは限りません。絵の端で止まらなくても、判定が内側にあるだけかもしれません。

次の行動

まず上の試作を保存して、円の半径や < の向きを変えながら「置けない」の境界が動くのを確かめてみてください。次に余白の庭を開き、表の手順で重なりの扱いを記録します。試作と見比べると、自作のゲームでどの方針を選ぶかが決めやすくなります。