【ゲーム制作】正規化とは?|ベクトルの正規化をC++でわかりやすく解説

サムネイル画像 ゲーム制作

今回は、ゲーム制作でよく出てくる「ベクトルの正規化」について解説していきます。

「正規化って何?」
「斜め移動するとキャラが速くなるのはなぜ?」
「ベクトルを正規化するってどういう計算をしているの?」

こんな疑問はありませんか?

ゲーム制作を進めていくと、キャラクターの移動や弾の発射、敵のAI処理などで「正規化」という言葉がかなり頻繁に出てきます。最初は数学っぽくて難しそうに見えますが、やっていることはそこまで複雑ではありません。

この記事を読み終えると、あなたは正規化の意味・なぜ必要なのか・計算方法・初心者がハマりやすいミスをしっかり理解できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

正規化とは?

正規化(normalize)とは、ベクトルの向きはそのままに、長さを1にする操作のことです。

長さが1のベクトルは「単位ベクトル」とも呼ばれます。

たとえば、「右に3、上に4」というベクトル (3, 4) の長さは 5 です。これを正規化すると (0.6, 0.8) になり、向きは同じまま長さだけが 1 になります。

つまり正規化は、「どれだけ進むか」を取り除いて、「どの方向に進むか」だけを取り出す操作というイメージです。

なぜ正規化が必要?

正規化が必要な理由は、方向だけを取り出して、速度を自分でコントロールしたいからです。

ゲームでキャラクターを動かす時、「右に1、上に0」なら長さは 1 ですが、「右に1、上に1」の斜め移動だと長さは約 1.414 になります。

つまり、正規化せずにそのまま速度を掛けると、斜め移動のほうが速くなってしまうわけです。

そこで、方向ベクトルを正規化して長さを 1 に揃え、そこに速度とdeltaTimeを掛けることで、どの方向でも同じ速さで移動させることができます。deltaTimeの仕組みについては、deltaTimeの解説記事で詳しく書いています。

正規化の計算方法

正規化の計算は、以下の2ステップです。

① ベクトルの長さ(magnitude)を求める

長さ = √(x * x + y * y)

② 各成分を長さで割る

正規化ベクトル = (x / 長さ, y / 長さ)

たとえば (3, 4) なら、長さは √(9 + 16) = 5 なので、正規化すると (3/5, 4/5) = (0.6, 0.8) になります。

C++で正規化を実装してみる

では、実際にC++でベクトルを正規化するコードを書いてみましょう。

▼main.cpp


#include <iostream>
#include <cmath>

struct Vec2 {
    float x;
    float y;
};

Vec2 Normalize(const Vec2& v) {
    float length = std::sqrt(v.x * v.x + v.y * v.y);
    if (length == 0.0f) return {0.0f, 0.0f};
    return {v.x / length, v.y / length};
}

int main() {
    Vec2 dir = {3.0f, 4.0f};
    Vec2 normalized = Normalize(dir);

    std::cout << "正規化前: (" << dir.x << ", " << dir.y << ")\n";
    std::cout << "正規化後: (" << normalized.x << ", " << normalized.y << ")\n";

    float checkLength = std::sqrt(normalized.x * normalized.x + normalized.y * normalized.y);
    std::cout << "正規化後の長さ: " << checkLength << "\n";

    return 0;
}

実行結果

正規化前: (3, 4)
正規化後: (0.6, 0.8)
正規化後の長さ: 1

向きはそのままで、長さがちゃんと 1 になっているのが確認できます。

正規化しないとどうなる?

正規化の必要性をより実感するために、正規化しないまま斜め移動した場合を見てみましょう。

▼main.cpp


#include <iostream>
#include <cmath>

int main() {
    float speed = 200.0f;
    float deltaTime = 0.016f;

    // 右だけ入力
    float rightX = 1.0f * speed * deltaTime;
    float rightY = 0.0f * speed * deltaTime;
    float rightDist = std::sqrt(rightX * rightX + rightY * rightY);

    // 右上に入力(正規化なし)
    float diagX = 1.0f * speed * deltaTime;
    float diagY = 1.0f * speed * deltaTime;
    float diagDist = std::sqrt(diagX * diagX + diagY * diagY);

    std::cout << "右移動の距離: " << rightDist << "\n";
    std::cout << "斜め移動の距離: " << diagDist << "\n";

    return 0;
}

実行結果

右移動の距離: 3.2
斜め移動の距離: 4.52548

このように、正規化しないと斜めに移動した時だけ約1.41倍速くなってしまいます。方向ベクトルを正規化してから速度を掛ければ、この問題は解消されます。

【重要】私が実際に正規化で困った体験談

私も自主制作の2Dアクションゲームで、正規化を知らずにかなり悩んだことがあります。

キャラクターの8方向移動を実装していた時に、横や縦に動く分には問題なかったのですが、斜めに動くとなぜか足が速くなるという現象が起きました。最初はアニメーション速度がおかしいのかと思って原因を探していたのですが、実際には移動処理のベクトルを正規化していなかったのが原因でした。

方向ベクトルを Normalize してから速度を掛ける形に直したところ、どの方向に動いても同じ速度になってかなりスッキリしました。

もうひとつ困ったのが、キャラクターが止まっている時にゼロベクトルを正規化してしまい、NaN(非数)が出たことです。移動入力がない時は方向ベクトルが (0, 0) になるので、それを正規化するとゼロ除算になってしまいます。これで座標が一瞬で壊れて、しばらく原因がわかりませんでした。

ログ出力で座標を表示してみたらNaNになっていたので、そこから原因を特定できました。こういう時は、C++でのデバッグのやり方の記事のようにブレークポイントやログを使うとかなり追いやすいです。

正規化使用時のよくあるエラーと対処法

  • ゼロベクトルを正規化してしまう
    長さが 0 のベクトルを割ると NaNinf になります。正規化の前に必ず長さが0でないかチェックしましょう。
  • 正規化と速度の掛ける順番を間違える
    先に速度を掛けてから正規化すると、速度が 1 になってしまい、ほとんど動きません。正規化してから速度を掛けるのが正しい順番です。
  • 2Dと3Dで計算式を混同する
    2Dなら √(x² + y²)、3Dなら √(x² + y² + z²) です。次元に合った式を使いましょう。

実践例:敵の方向に弾を飛ばす

正規化の定番の使い方として、「プレイヤーから敵の方向に弾を飛ばす」例を見てみましょう。

▼main.cpp


#include <iostream>
#include <cmath>

struct Vec2 {
    float x;
    float y;
};

Vec2 Normalize(const Vec2& v) {
    float length = std::sqrt(v.x * v.x + v.y * v.y);
    if (length == 0.0f) return {0.0f, 0.0f};
    return {v.x / length, v.y / length};
}

int main() {
    Vec2 player = {100.0f, 100.0f};
    Vec2 enemy  = {400.0f, 300.0f};

    // プレイヤーから敵への方向ベクトル
    Vec2 diff = {enemy.x - player.x, enemy.y - player.y};
    Vec2 dir = Normalize(diff);

    float bulletSpeed = 500.0f;
    float deltaTime = 0.016f;

    Vec2 bullet = player;

    for (int i = 0; i < 5; i++) {
        bullet.x += dir.x * bulletSpeed * deltaTime;
        bullet.y += dir.y * bulletSpeed * deltaTime;
        std::cout << "弾の位置: (" << bullet.x << ", " << bullet.y << ")\n";
    }

    return 0;
}

実行結果

弾の位置: (106.656, 104.992)
弾の位置: (113.312, 109.984)
弾の位置: (119.968, 114.976)
弾の位置: (126.624, 119.968)
弾の位置: (133.28, 124.96)

方向ベクトルが正規化されているので、弾はプレイヤーと敵の距離に関係なく、常に一定の速度で飛んでいきます。この弾が敵に当たったかどうかは、AABBの当たり判定円形の当たり判定と組み合わせて判定するのが一般的です。

注意点

  • ゼロベクトルの正規化は必ずガードを入れること
  • std::sqrt は毎フレーム呼ぶとコストがあるので、大量のオブジェクトを扱う場合は工夫が必要な場合もある
  • まずは2Dの移動処理にだけ入れて、慣れたらAI・弾道・カメラなどにも広げるのがおすすめ

挙動がおかしい時は、正規化後のベクトルの長さをログ出力して 1 になっているか確認するとかなり原因を追いやすいです。

まとめ

  • 正規化は、ベクトルの向きはそのままに、長さを1にする操作
  • 正規化しないと斜め移動が速くなるなどの問題が起きる
  • 計算は 各成分をベクトルの長さで割る だけ
  • ゼロベクトルの正規化はNaNになるので必ずガードする
  • 正規化 → 速度を掛ける → deltaTimeを掛ける の順番が基本

正規化は、ゲーム制作を進めるうえでかなり重要な基本概念です。

最初は数学的で難しそうに見えるかもしれませんが、「方向だけを取り出して、長さを自分で決められるようにする操作」と考えるとかなり理解しやすくなります。

ここまで読んでくださり、ありがとうございました。

関連記事