【ゲーム制作】スロープの上を移動

本稿では、ゲームでキャラクターを移動させるときに必要な考え方と、具体的な計算方法について共有します。
今回はスロープ(坂道)の移動についてです。

AI に作らせて終わり…ではなく、「ゲームの仕組みを基礎からしっかりと学びたい!」というゲーム開発者を目指している人や、既にゲーム開発の仕事に就いていて、足りない部分の知見を補完したい人向けの内容です。

対象者
・ゲームのメカニズムに興味がある。
・中学校の数学の授業で習う四則演算を習得している。
・AI を使って自主的に学習を進められる。

勉強が必要な理由
興味があれば開いて内容をご覧ください
筆者はゲーム開発に限らず、あらゆる場面でもっと AI を活用すべきだと思っています。
簡単な作業は全て AI に任せることで、人間はもっと高度な仕事に専念できるようになります。
そうすることで、もっと高品質で、今までやりたくでも、雑務を処理するので精一杯で、できなかったことができるようになります。
少なくとも、その機会が増えます。

AI が稼いだ分の時間を、もっと高度な仕事に割り当てることになるので、コストは減りません。
AI を使って開発の時間を減らすことはできますが、それだと低品質なままです。
今は「高度な仕事って何?」というのが、多くの人に見えていないので、AI で乱造した低品質なものが世の中に大量にあふれ出しているという状態になっています。
これは物事の過渡期では仕方のない事です。

ゲームでよく問題になるのは、物量です。

・やりたいことがもっと沢山あったのに、時間も金も人もいなくて削るしかなかった…。
・もっと最適化できたのに、時間もアイデアもなかった…。
・プロモーションに時間をかけたかったのに…。
・3D モデルの見た目をもっと良くできたのに…。
・世界観をもっと深く作り込みたかったのに…。

なんていうのは、ゲーム開発で必ず出て来る問題です。
AI に任せると、人力では不可能なスピードで作ってしまいます。
人間は品質のチェックや、表現のこだわりにもっと時間を使えるようになります。

今までは人力で雑なものを作って、それを人間がブラッシュアップするというプロセスでした。
今後は AI が雑なものを作って、それを人間と AI がブラッシュアップするというプロセスになると思います。

ブラッシュアップに割く時間が増えれば、それだけ高品質なものを、より多く作れるようになるということです。

…というのは理想論で、お金を出す人 (スポンサーや企業の偉い人) は「AI 使って爆速で作れるなら、時間は半分でいいよね?」と言い出すので、現実はそう単純ではありません。
この手の人間には、高品質にするにはもっと時間が必要だ!と言っても理解できません。
なので、AI を使って今までと同レベルのものを短期間で作るようになるだけです。
実際、今はそうなっています。

いずれにしても、AI を活用する場面が増える…という流れが変わることはないと思います。

AI を使いこなすには、人間側に基礎知識や、いわゆる「見る目」が必要です。
AI を使いこなす能力がなければ、AI に的確な指示を出したり、AI の成果物を適切に判断することもできません。

結局、AI を使う人間側を教育する必要があります。

未来は AI がトレンドを分析して、勝手にゲームを作ってリリースするようになると思います。
それによって起きる未来も予想できます。

→ 人間が興味をなくす
→ 人間が人力で作ったものが特別視される
→ AI 使わない技術が必要になる
→ あるいは、ゲームで遊ぶ人がほぼいなくなる
→ あるいは、アナログゲームに回帰する

スロープに沿って移動

最初の移動方法は、どうして「×」なのか?

興味があれば開いて解説をご確認ください
そういうゲームなら問題ない

1回目のケースを「×」としていますが、スロープを駆け下りるときに、スロープに沿って滑らかに駆け下りないとダメな場合はNGです。
そうではないなら、どっちでもありません。

どっちでもない場合、ゲームの遊びを決める人がどう思うか?仕様でどう規定されているか?…で決まります。
仕様書に記載がない場合は、決定権がある人に聞くしかありません。
自分ひとりで作っているゲームなら、自分次第です。


gemini 画伯作

考え方

簡単な図形の問題です。
解決方法はひとつではありません。

例えば、接地した地形にタグが付与されているかどうかを調べて、「40°のスロープ」タグが付与されているなら、そのタグ専用の移動処理に切り替えることができます。

スロープに合わせて「スプライン」と呼ばれる線を引いておき、「40°のスロープ」タグがある地形に乗ったら、そのスプラインに沿って移動させる…といった処理の仕方もあります。

スロープの移動でスプラインを使うかと言われると、以下の理由から「あまりなさそう」という意見になります。

1. スロープが沢山ある場合に全てのスロープにスプラインを引くのが大変。
2. スロープに沿って移動させる程度ならコードで処理した方が実装もテストも楽。

「スプライン」の使い道としては、レールアクションと呼ばれるような、ジェットコースターのようなレールに沿って進ませる遊びや、複雑なカメラワークになります。


gemini 画伯作

今回はそのような仕組みがないことを前提とします。

1. 現在の地面の座標を計算する

もし、キャラクターが空中にいるなら、計算する必要はありません。
空中をフワフワと浮遊しているなら、この処理は必要ありません。
重力の仕組みがある場合、着地してから計算を始めます。

まず、着地した地面の位置(座標)を、これから先の計算で使える状態にします。


もし、キャラクターの位置を絵の中心に設定している場合(↑の画像の赤い線の位置)、キャラクターの足元の位置(↑の画像の青い線の位置)で計算する必要があります。

→ X 軸
↓ Y 軸

キャラクターの位置が (cx, cy) で、キャラクターの高さが height なら、(cx, cy + height / 2)
逆に、↑に行くほど値が大きくなる座標系なら、(cx, cy – height / 2)

足すのか、引くのかは、Y 軸の向きが上になっているか、下になっているかで変わります。
画面のピクセルを使う座標系(Webブラウザなど)は下向きで、ゲームエンジンは上向きが多いです。

キャラクターの位置を足元に設定しているなら、この計算は不要です。

キャラクターがいる現在の地面の位置は (cx, cy) で、cy は前述の足元の位置に修正済みとします。

gemini 画伯作

2. 移動先の地面の座標を計算する

移動を続けた場合の、次の移動先の地面の位置 (dx, dy) を計算します。

dx = cx + xの移動量

dx は単純に移動量を足すか、引くだけです。

dy を算出するには、地面にぶつかるまで、1ピクセルずつ「これは地面か?」と問い合わせる必要があります。
ただ、今のゲーム制作では、座標を1ピクセル単位で計算することはほとんどありません。
自分でその仕組みを作れば可能ですが、既存のゲームエンジンでそれをやるのは、けっこうめんどくさいです。


gemini 画伯作

今のゲーム制作では、座標は 3D のバーチャルな空間を使います。
そこに地面や壁などを配置して、キャラクターがスポーン(出現)する位置を設定します。
2D ゲームの場合も、計算は 3D 空間で、その 3D 空間を 2D 用のカメラで映すことで 2D ゲームの見た目になるという仕組みです。


gemini 画伯作

既存のゲームエンジンのような仕組みでは、レイキャストという機能を使うことで、キャラから地面までの距離を調べることができます。
レイキャストの「レイ」はレーザーみたいな、まっすぐどこまでも伸びていく光線と同じです。
光線を画面に表示することもできますが、通常は何も表示されません。
このレイは計算用です。


gemini 画伯作

キャラから地面にまっすぐレイを飛ばして、レイが地面に当たったときに、キャラからレイが当たった地面までの距離が分かります。
例えば、Unity や Unreal Engine などのゲームエンジンには、レイキャストがあるので、それを使うだけです。

レイキャストにはベクトルの計算が必要になりますが、「ベクトルが何か?」を知っている必要はありません。
※ベクトル(vector)は高等学校の数学で習います。
ベクトルについて詳しく知りたいなら、AI が教えてくれます。


gemini 画伯作

以下は AI が提示してくれた Unity と Unreal Engine 用のサンプルコードです。
※コードをざっと見て問題なさそうなので掲載していますが、動作確認まではしていません。

// Unity 用
using UnityEngine;

public class GroundCheckUnity : MonoBehaviour
{
    [SerializeField] private float rayLength = 2.0f; // レイの長さ
    [SerializeField] private LayerMask groundLayer; // 地面のレイヤーを指定する場合

    void Update()
    {
        // キャラクターの足元をスタート地点にする
        Vector3 rayStart = transform.position;
        Vector3 rayDirection = Vector3.down; // 真下

        // レイキャストの衝突情報を格納する構造体
        RaycastHit hit;

        // レイを可視化(デバッグ用)
        Debug.DrawRay(rayStart, rayDirection * rayLength, Color.red);

        // レイキャスト実行 (位置, 方向, 衝突結果, 長さ, レイヤーマスク)
        if (Physics.Raycast(rayStart, rayDirection, out hit, rayLength, groundLayer))
        {
            // 地面に当たった場合の処理
            GameObject hitObject = hit.collider.gameObject;
            float distanceToGround = hit.distance;
            Vector3 hitPoint = hit.point;

            Debug.Log($"地面を検知: {hitObject.name}, 距離: {distanceToGround}, 衝突点: {hitPoint}");
        }
        else
        {
            // 空中にいる場合など
            Debug.Log("地面が見つかりません");
        }
    }
}

↑の distanceToGround がレイの長さ

// Unreal Engine 用
#include "MyCharacter.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h" // デバッグ描画用

void AMyCharacter::CheckGroundUnderfoot()
{
    UWorld* World = GetWorld();
    if (!World) { return; }

    // 1. レイの開始位置と終了位置の設定
    // キャラクターの足元からスタートする(カプセルの中心などからでもOK)
    FVector StartLocation = GetActorLocation(); 
    FVector EndLocation = StartLocation + FVector(0.f, 0.f, -200.f); // 200ユニット真下まで

    // 2. トレースの設定(自分自身を無視するなど)
    FHitResult HitResult;
    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this); // キャラクター自身への衝突を無視

    // 3. 衝突チャンネルの指定 (ここでは一般的な Visibility または Camera/Pawn などを指定)
    ECollisionChannel TraceChannel = ECC_Visibility;

    // 4. レイキャスト(ライントレース)の実行
    bool bHit = World->LineTraceSingleByChannel(
        HitResult,
        StartLocation,
        EndLocation,
        TraceChannel,
        Params
    );

    // 5. デバッグ用レイの描画 (エディタ上でのみ可視化)
    DrawDebugLine(
        World, 
        StartLocation, 
        EndLocation, 
        bHit ? FColor::Green : FColor::Red, 
        false, 
        -1.f, 
        0, 
        1.f
    );

    // 6. 結果の処理
    if (bHit)
    {
        AActor* HitActor = HitResult.GetActor();
        float DistanceToGround = HitResult.Distance;
        FVector HitPoint = HitResult.ImpactPoint;

        if (HitActor)
        {
            UE_LOG(LogTemp, Warning, TEXT("地面を検知: %s, 距離: %f, 衝突点: %s"), 
                *HitActor->GetName(), 
                DistanceToGround, 
                *HitPoint.ToString()
            );
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("地面が見つかりません"));
    }
}

↑の DistanceToGround がレイの長さ

dy = cy + レイの長さ

(dx, dy) は移動先の座標なので、この座標にキャラを移動させます。

まとめ

スロープの移動先の座標を算出するには、キャラの足元から地面までの距離を測る必要がある。
測り方はひとつではない。
・有名なゲームエンジンを使う場合は、レイキャスト機能があるので、それを使う。
・キャラの足元から地面までの距離が分かるなら、やり方はなんでも良い。
※ただし、ゲームの動作が重くならないよう注意。


gemini 画伯作

お気づきでしょうか?
問題点
中学校の数学で習う「三平方の定理」や「ピタゴラスの定理」をよく理解している方は、前項の処理方法では問題があると気づくかも知れません。

「何故問題があるのか?」を考えるときに必要なのが下図です。

1 : 1 : √2

縦と横の辺の長さが 1 のとき、斜め(スロープ)の辺の長さが √2 (ルート 2) になるという決まりです。
ルート 2 は 2 乗すると 2 になる数です。

プログラム言語の多くは乗数に ^ を使います。

2 の 2 乗 = 2 ^ 2 = 2 * 2 = 4

√2 の 2 乗 = √2 ^ 2 = √2 * √2 = 2

ルート 2 の 2 乗は 2 です。

2 乗すると 2 になる数は計算が非常にめんどくさいので、いちいち計算しないで済むようルート(平方根)という記号で表現します。
そのため、平方根を計算するアルゴリズムも重いので、ゲームでは平方根の計算はしないで、2 乗したまま使うことが多いです。
実際の数は 1.4 より大きい数です。
割り切れない数 (無理数) なので、小数点以下が無限に続きます。

√2 > 1

前項では、平らな地面を移動したときの X 方向の移動量を、スロープを降りる場合でも、同じ移動量で計算していましたが、この方法は正確ではありません。

前項の方法でスロープを移動すると、「なんか坂道で移動が速くなるな…」と感じるかも知れません。
現実だと下り坂では速足になりがちなので、違和感はないかも知れません。
感覚は人それぞれですが、理屈としては不正確です。

常に理屈通りにすることが、ゲームにおいての正しさになるとは限りません。
重要なのは、プレイして「楽しい」ことです。
プレイして楽しいなら、細かい理屈は重要ではないです。
理屈は問題解決を早めるためのツールです。
違和感を感じたときに、その原因を知っていれば秒で解決できますし、原理原則を知っていれば、どうして違和感があるのか?という問題の答えをすぐに思いつけるかも知れません。

スロープ上を進むときの正確な移動量は、上図の青色の部分です。
要するに、前項の移動量だと多めに進んでしまいます。

正確に計算する必要がある場合、もしくは、「なんか違和感があるから改善したい…」と感じる場合、スロープの上を移動したときの移動量に変換する必要があります。


gemini 画伯作

修正するために必要なこと
スロープ上を進むときの正確な距離を計算するには、まず、スロープの角度を計算する必要があります。


claude 画伯作

この図の、オレンジ色のテキスト「何度?」を求めます。

この計算にはアークタンジェントを使います。
アークタンジェントが何か?を知っている必要はありません。
プログラムでは atan2 という名前の関数を使うのが慣習になっています。

atan2 について (2 は何なのか?…など) 気になるようでしたら、AI に聞けば詳しく教えてくれます。

x = dx – cx
y = dy – cy

// JavaScript
const radian_angle = Math.atan2(y, x);
// C#(Unity)
const double radian_angle = System.Math.Atan2(y, x);
// Standard C++
#include <cmath>
const double radian_angle = std::atan2(y, x);
// Unreal Engine C++
const double radian_angle = FMath::Atan2(y, x);

ゲームでよく使われるプログラム言語のコードにしました。

注意点としては、これらのコードで求められる角度はラジアンと呼ばれるものです。
度数(degree)ではありません。
プログラム言語では、通常、度数は使わないので、ラジアンをそのまま別の計算に使うことができますが、動作確認をするときに度数を知りたい状況が出て来ると思います。
ラジアンを度数に変換する方法は AI に聞いてください。

修正方法

最初に計算した (dx, dy) だと多めに進んでしまうため、スロープ上の正確な距離を計算します。
スロープ上を青い線の距離だけ進んだ時の (dx, dy) を計算するには、三角関数を使います。

高等学校の数学で習いますが、「角度と距離が分かっているときの座標を計算するのに必要…」とだけ覚えれば良いです。
「どういう状況で使うのか?」を覚えるだけです。


gemini 画伯作

上図の青い線の距離は分かっていて、スロープの角度を計算する方法は前項の「修正するために必要なこと」で説明しました。

次も暗記ですが、x 座標を求めるには、三角関数の cos (コサイン)、y 座標を求めるには sin (サイン) とだけ覚えておきます。
数学の教科書だとコサイン・シータ…という呼び方をしますが、シータの部分にスロープの角度をあてはめます。
実際の計算式は以下のようになります。

dx = cx + cos(スロープの角度) * スロープの上を進む距離
dy = cy + sin(スロープの角度) * スロープの上を進む距離

プログラミングで使う角度はラジアンです。
前項で計算したラジアンの角度をそのまま当てはめれば良いです。


gemini 画伯作

まとめ
下りのスロープを正確な距離進むには、以下の順番で処理する必要があります。
1. 現在位置 (cx, cy) から水平に真っすぐ進んだ位置から地面までの距離を算出。
2. 地面までの距離を使って仮の移動先 (dx, dy) を算出。
3. (cx, cy) と (dx, dy) からアークタンジェントを使ってスロープの角度を算出。
4. 三角関数を使って正確な距離から本当の (dx, dy) を算出。

claude 画伯作
応用

以下はご自身で考えてみてください。

スロープを上る場合の計算方法

スロープが途中で途切れている場合の移動方法(下図)。

応用

以下は JavaScript で作った本稿のサンプルプログラムです。

スロープ移動のサンプル

上記サンプルのコードを見れば、どのような処理をしているかが分かります。
コードを見る方法はブラウザによって違いますが、ページのソースを表示、開発者用ツール、コンソール、デバッグなどをたどれば見つかると思います。
サンプルが使用している game.js は筆者が作成したマイクロライブラリで、以下の記事で紹介しています。

サンプルプログラムの作成では、以下の範囲で生成 AI と AI エージェントを使用しています。

Gemini
・canvas を使った仕組みの提案
・canvas を JavaScript 制御する方法の提案
※エフォートはデフォルトです。

Claude Code
・メッド内の細かい計算処理の実装。
・コードの修正
※Sonnet 5.5 中以上~Opus 5.5 低

筆者
・プログラム設計の考案
・AI への相談と作業指示
・AI が生成したコードのチェック
・動作確認

画像生成
Gemini
Claude Code
※本稿では使用していませんが、↑でうまく生成できないときは、ChatGPT や、ローカルで Qwen Image を使ったりもします。
※本稿のアイキャッチ画像は Gemini で生成したものです。

記事の内容を AI に解説してもらった

少し前までは日本語がおかしいところがあったんですが、完璧になりましたね…。

以下も AI で作った解説動画です。
本稿をベースにしている点は同じですが、音声解説とは視点が違います。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です