ことれいのもり

マップエディタで座標をグリッドにスナップする方法-ゲーム開発で役立つ汎用テクニック

はじめに

ゲーム制作でマップエディターなどを作っているときに、座標をグリッドサイズに合わせてスナップしたいときがあるとおもいます。

この記事ではスナップするときの考え方を紹介します。

特定の言語やゲームエンジンによらずに、どんなときでも使える共通の考え方です!

スナップとは?

スナップとは、最も近いグリッドに移動させることです。

例えば、グリッドサイズが1.0のグリッドがあったとします。

グリッドサイズ1.0

マウスクリックした座標のX座標が、0.7だったとしましょう。

このとき、クリックした座標は0.7ですが、実際に配置したいのはグリッドに沿った座標、つまり0.0~1.0のグリッド(赤背景の四角)です。

![0.0~1.0にスナップする]/assets/image/uploads/articles/image_32_2.png

マウスクリックした座標に配置するのではなく、グリッドに沿って吸着させて配置することを「スナップ」といいます!

スナップするときの考え方

先に結論を伝えると、スナップをするコードは以下のようになります。(C++で書いています)

Vec3 MapEditor::snapToGrid(const Vec3& pos, double gridSize) const
{
    return Vec3{
        std::floor(pos.x / gridSize) * gridSize,
        0.0,
        std::floor(pos.z / gridSize) * gridSize
    };
}

ポイント

コードはC++で書いていますが、重要なポイントは以下の部分です。

std::floor(pos.x / gridSize) * gridSize

pos.xはクリックした座標です。 「グリッドサイズで割った後に」floorで切り捨て、その後グリッドサイズをかけます。 こうすることで、マウスの座標に対して手前のマスに吸着します。 具体的には、0.0~1.0の間をクリックしたら0.0のグリッドに配置され、1.0~2.0なら1.0のグリッドに配置されます。

疑問:なぜstd::floor(pos.x)ではないの?

この計算式をみて、なぜstd::floor(pos.x)だけではないの?と思いませんでしたか? 実は、ただ切り捨てるだけだと、グリッドからずれた場所にスナップされます。 実際に計算してみましょう。

今回、分かりやすいように GridSize = 2.0 で考えてみます。 このとき、クリックしたX座標が3.7だったとします。 つまり、下の画像のような状況です。

グリッドサイズ2.0で3.7をクリックした時

このとき理想的なスナップは、2.0~4.0の部分に色が塗られることです。 では、std::floorだけの場合と正しい場合を見比べてみましょう。

std::floorだけの場合

std::floor(3.7)
→ 3.0

結果は3.0です。

2の倍数ではない、ずれた位置にスナップすることになりますね。

画像で見ると一目瞭然です。

ずれた位置にスナップ

正しいスナップの場合

std::floor(3.7 / 2.0) * 2.0
→ std::floor(1.85) * 2.0
→ 1.0 * 2.0
→ 2.0

結果が2.0になっています。

最初に「グリッドサイズを単位とした値で割った」ことがポイントです!

これにより、グリッドサイズの大きさに影響されない値が算出できます。

あとは、floorで切り捨てた後に元のグリッドサイズのスケールに戻せば問題ありません。

画像で見ても、2.0~4.0のところにスナップしていますね。

2.0~4.0の正しい位置にスナップ

おわりに

グリッドサイズのスナップについて考え方を説明しました。

一度分かればいろいろなところに応用できる考え方だと思います。

ぜひ自分の制作に役立ててください!