実践ガイド

3Dの1部屋から脱出を完成させる|Unityで学ぶカメラとレイ

※本記事にはアフィリエイトリンクが含まれています。

Amazonのアソシエイトとして、ゲームのタネは適格販売により収入を得ています。

▶ この回の完成形を、この場で遊ぶ

ブラウザで動く『3Dの1部屋から脱出』です。Unity 6.3 LTS の完成プロジェクトと同じルール(制限時間 40 秒、光るコア 3 つ、候補 6 か所から毎回 3 か所、部屋の真ん中から見回すだけ)です。

3Dの1部屋から脱出 はじめるでスタート
このブラウザではキャンバスを表示できません。

記事中の C#・ダウンロード ZIP と同じルールです。はじめる(または画面を一度クリックしてから R)で開始。左クリックで拾う、A/D・←→・右ドラッグで見回し。スマホはタップで拾う、横になぞるか ◀ ▶ で見回し。

Unity の完成プロジェクト

上で遊んでいるものと同じゲームです。ZIP を解凍し、Unity 6.3 LTS(執筆時点の最新パッチは 6000.3.25f1)でフォルダを開いて Assets/Scenes/Main.unity を Play。

unity-room-escape.zip をダウンロード

はじめに

「3D の物をクリックしたいのに、何も反応しない…画面の点と 3D の物って、どうつながっているの?」——そんな疑問向けです。

この記事では、Unity 6.3 LTS で、3Dの1部屋から脱出する小さなゲームを完成させます。部屋の真ん中に立ったまま見回し、40 秒以内に光るコア 3 つをクリックで拾います。3 つそろうと北の壁の出口が赤から緑に変わり、出口をクリックすればクリア。間に合わなければ時間切れで、R やボタンで何度でもやり直せます。

今週身につけること:カメラとレイ。 クリックした画面の点を、カメラが「奥へ伸びる見えない光線(レイ)」に変えます。これが Camera.ScreenPointToRay です。そのレイが最初に当たった Collider(当たり判定の形)を Physics.Raycast で調べます。どのレイヤーに当ててよいかは LayerMask で決めます。

これまでの完成ゲームは 2D だけでした。今回が連載で最初の 3D です。前回(第5回)は Godot で敵の出現と射程を扱いました。今週はエンジンを Unity だけに切り替えます。

完成形と、やらないこと

  • 作るもの: 12m × 12m の部屋 1 つ。光るコア 3 つ(候補 6 か所からランダム)、箱 2 つ、出口ゲート 1 つ。40 秒タイマー、コア数、クリア(残り秒数とベスト)、時間切れ、やり直し。Unity ZIP と、同じルールのイン記事プレイ。
  • やらないこと: 歩き回る移動、ジャンプ、物語や会話、アイテム欄、セーブ、Godot 併記。
  • 持ち帰る1アイデア: 3D の物をクリックするときは、画面の点 → カメラのレイ → 最初に当たった Collider の順に考える。見た目ではなく Collider に当たる。
3Dの1部屋から脱出の開始画面。はじめるボタン、コア0/3、残り40.0秒、赤い出口
開始画面(イン記事プレイ/ZIP と同じルール)。正面の赤い板が出口です。

カメラとレイを先に図で見る

3D の画面は、カメラから見た景色を平らな画面に写したものです。だから画面の 1 点は、3D の世界では「カメラから奥へまっすぐ伸びる線」に対応します。公式マニュアルのIntroduction to raycastingでも、カメラの視界の各点は世界の中の線に対応する、と説明されています。

カメラから画面の点を通るレイが伸び、手前の箱で止まる例と、コアに当たる例を横から描いた図
レイは最初に当たった Collider で止まります。箱の向こうには届きません。
  1. 画面の点: マウスの位置はピクセルで、画面の左下が (0, 0) です(Camera.ScreenPointToRay)。
  2. レイ: ScreenPointToRay が返す Ray は、カメラの位置ではなく近クリップ面(今回は 0.3m 先)から出発します。
  3. 当たり: Physics.Raycast は、レイが Collider に当たれば true を返します。out RaycastHit hit には、当たった collider・point(位置)・distance(レイの出発点からの距離)が入ります(RaycastHit)。

準備(Unity 6.3 LTS)

  1. Unity 6 の公式ページの案内に沿って、Unity Hub から Unity 6.3 LTS を入れます。LTS は長期サポート版です。執筆時点の最新パッチは 6000.3.25f1(2026年9月24日公開)で、第4回と同じ 6.3 系です。
  2. 上の unity-room-escape.zip を解凍し、Hub の「Add」→「Add project from disk」でそのフォルダを開きます。記事に載せていない進行役と部屋の生成コードも必要なため、まずは ZIP の完成プロジェクトを使ってください。
  3. 入力は Input System パッケージ 1.20.1 を使います。Unity 6.3 向けに公開されている版です(Input System(Unity 6.3 マニュアル))。ZIP には依存パッケージとして設定されています。Unity が必要なパッケージを取得できる環境で開いてください。自分で組む場合は Package Manager から入れ、Project Settings → Player の Active Input Handling を Input System Package (New) にします。古い Input.mousePosition は旧 Input Manager の機能で、公式は新しいプロジェクトでは Input System を勧めています(Input.mousePosition)。
  4. Project Settings → Tags and Layers で、User Layer 6 に Pickup を追加します(ZIP は設定済み)。コアと出口だけをこのレイヤーにします(レイヤーの割り当て)。
入力、Main CameraのClickRayとCameraLook、RoomGame、DefaultレイヤーとPickupレイヤーの部屋オブジェクトの関係図
入力・シーン構造。クリックはカメラがレイに変え、当たった側の RayTarget が RoomGame に知らせます。

ルール(数字は記事・ZIP・プレイで共通)

  • 制限時間 40 秒。0 になったら時間切れ
  • 光るコア 3 つ。候補 6 か所から、毎回 3 か所が選ばれる
  • カメラは部屋の真ん中 (0, 1.6, 0) に固定。縦の視野角 60°。左右だけ見回せる(A/D・←→ は 1 秒に 90°、右ドラッグは 1 ピクセル 0.25°)
  • レイの長さは 30m。Default(壁・床・天井・箱)と Pickup(コア・出口)に当たる。最初に当たったものだけが反応する
  • 3 つ拾うと出口が赤 → 緑。緑の出口をクリックでクリア。スコアは残り秒数(ベストは遊んでいる間だけ記録)

エディタでの組み立て(ZIP と同じ形)

ZIP では、Play を押した瞬間に RoomBuilder.cs が部屋をコードで組み立てます。以下は生成される構成の説明です。ZIP で手動配置すると自動生成された物と重複するため、追加で配置せず、Play 中の Hierarchy で確認してください。自分で手動配置に変える場合は、生成処理と進行役の参照も合わせて変更する必要があります。

  1. Main Camera:位置 (0, 1.6, 0)、Projection は Perspective、Field of View 60、Clipping Planes の Near 0.3。ClickRay と CameraLook を付けます(ZIP は付けてあります。外れていても RoomGame が足します)。
  2. 空の GameObject「RoomGame」:RoomGame.cs を付けます。HUD(コア数・残り秒・「当たった」表示)は OnGUI で文字を描くだけなので、Canvas は使いません。
  3. 部屋:Cube を 6 つ(Floor・Ceiling・Wall_N/S/E/W)。Cube には最初から BoxCollider が付いています(GameObject.CreatePrimitive)。
  4. 箱:Cube を 2 つ(Crate_A・Crate_B、0.8m 角)。Default レイヤーのままにして、レイを止める役にします。
  5. コア:Sphere を 3 つ(Core_1〜3、直径 0.6m)。SphereCollider 付き。Layer を Pickup にし、RayTarget(種類 Core)を付けます。
  6. 出口:薄い Cube(ExitGate、1.6m × 2.4m)を北の壁の手前に。Layer を Pickup にし、RayTarget(種類 Exit)を付けます。

なぜ Collider が必要なのか。レイが調べるのは見た目(メッシュの絵)ではなく、物理の当たり判定だからです。公式のColliderの説明どおり、BoxCollider や SphereCollider はすべて Collider の仲間です。

スクリプト1:ClickRay.cs(画面の点 → レイ → 当たった Collider)

ファイル名:Assets/Scripts/ClickRay.cs。Main Camera に付けます。今週の核です。ZIP の中身とまったく同じ全文です。

using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// 画面のクリック位置からレイ(見えない光線)を飛ばし、最初に当たった Collider を調べる。
/// Main Camera に付けます。
/// </summary>
[RequireComponent(typeof(Camera))]
public class ClickRay : MonoBehaviour
{
    [SerializeField] float maxDistance = 30f;  // レイの長さ(m)。部屋の対角より長ければOK
    [SerializeField] LayerMask hitLayers;      // レイが当たってよいレイヤー(Default と Pickup)
    [SerializeField] RoomGame game;            // 進行役。空なら自動で探す

    Camera cam;

    /// <summary>HUD に出す「最後に当たったもの」</summary>
    public string LastHitText { get; private set; } = "当たった:まだクリックしていません";

    void Awake()
    {
        cam = GetComponent<Camera>();
        // Inspector で未設定(Nothing)なら、壁や箱(Default)とコア・出口(Pickup)に当てる
        if (hitLayers.value == 0)
            hitLayers = LayerMask.GetMask("Default", "Pickup");
        if (game == null)
            game = FindFirstObjectByType<RoomGame>();
    }

    // クリックは「押したフレーム」だけ true なので、Update で読みます
    void Update()
    {
        var mouse = Mouse.current;
        if (mouse == null || !mouse.leftButton.wasPressedThisFrame)
            return;
        if (game != null && !game.IsPlaying)
            return;

        // 1) 画面の点(ピクセル。左下が 0,0)→ 2) カメラから奥へ伸びるレイ
        Vector2 screenPos = mouse.position.ReadValue();
        Ray ray = cam.ScreenPointToRay(screenPos);

        // 3) レイが最初に当たった Collider を調べる(hitLayers 以外は素通り)
        if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, hitLayers))
        {
            LastHitText = $"当たった:{hit.collider.name}({hit.distance:0.0}m)";
            // Scene ビューに黄色い線を1秒表示(Game ビューには出ません)
            Debug.DrawLine(ray.origin, hit.point, Color.yellow, 1f);

            if (hit.collider.TryGetComponent(out RayTarget target))
                target.OnRayClick();
        }
        else
        {
            LastHitText = "当たった:なし";
            Debug.DrawRay(ray.origin, ray.direction * maxDistance, Color.white, 1f);
        }
    }
}

読みどころ:

  • Mouse.current.leftButton.wasPressedThisFrame は、押したフレームだけ true です。だからクリックは Update で読みます。マウスの位置は mouse.position.ReadValue()(Mouse クラス(Input System 1.20))。公式の Physics.Raycast のページにも、同じ書き方で ScreenPointToRay に渡す例があります。
  • LayerMask.GetMask("Default", "Pickup") は、レイヤー名から「当ててよいレイヤーの組」を作ります(LayerMask.GetMask)。Inspector の Hit Layers で選んでも同じです。
  • 当たったら hit.collider.name と hit.distance を HUD に出します。Debug.DrawLine は Scene ビューに 1 秒だけ線を残します(Debug.DrawLine)。

別ゲームで変えるところ: maxDistance(「手が届く距離」にしたいなら 3〜5m)、hitLayers の組み合わせ、当たったときの処理(hit.point に物を置けば、設置型のゲームになります)。

スクリプト2:RayTarget.cs(レイで押される側)

ファイル名:Assets/Scripts/RayTarget.cs。コアと出口に付けます。Inspector で Kind を Core か Exit にします。

using UnityEngine;

/// <summary>
/// レイでクリックされる側。コアと出口の両方に付けます。
/// レイが当たるのは Collider だけ。見た目(Mesh)だけでは当たりません。
/// </summary>
public class RayTarget : MonoBehaviour
{
    public enum Kind { Core, Exit }

    [SerializeField] Kind kind = Kind.Core;
    [SerializeField] RoomGame game;  // 空なら自動で探す

    public Kind TargetKind => kind;

    void Awake()
    {
        if (game == null)
            game = FindFirstObjectByType<RoomGame>();
        // よくある失敗:Collider が無い → クリックしても何も起きない
        if (!TryGetComponent(out Collider _))
            Debug.LogWarning($"{name} に Collider がありません。レイが当たりません。");
    }

    /// <summary>コードから作るとき用(RoomBuilder が使います)</summary>
    public void Setup(Kind newKind, RoomGame owner)
    {
        kind = newKind;
        game = owner;
    }

    /// <summary>ClickRay のレイが当たったときに呼ばれる</summary>
    public void OnRayClick()
    {
        if (game == null)
            return;
        if (kind == Kind.Core)
            game.CollectCore(this);
        else
            game.TryExit();
    }
}

別ゲームで変えるところ: Kind に種類を足します(例:Switch、Enemy)。押されたときの処理は OnRayClick の中だけ書き換えれば、ClickRay 側は触らずに済みます。TryGetComponent は、付いていれば取り出し、無ければ false を返すメソッドです(Component.TryGetComponent)。

スクリプト3:CameraLook.cs(その場で左右を見回す)

ファイル名:Assets/Scripts/CameraLook.cs。Main Camera に付けます。位置は動かさず、Y 軸まわりだけ回します。

using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// 部屋の真ん中に立ったまま、左右だけ見回す(カメラの位置は動かさない)。
/// Main Camera に付けます。
/// </summary>
public class CameraLook : MonoBehaviour
{
    [SerializeField] float keySpeed = 90f;     // A/D・←→ を押している間、1秒に回る角度
    [SerializeField] float dragSpeed = 0.25f;  // 右ドラッグ 1 ピクセルあたりの角度

    float yaw;  // 左右の向き(度)。0 で出口のある北の壁を向く

    void Start()
    {
        yaw = transform.eulerAngles.y;
    }

    void Update()
    {
        var kb = Keyboard.current;
        if (kb != null)
        {
            if (kb.aKey.isPressed || kb.leftArrowKey.isPressed)
                yaw -= keySpeed * Time.deltaTime;
            if (kb.dKey.isPressed || kb.rightArrowKey.isPressed)
                yaw += keySpeed * Time.deltaTime;
        }

        var mouse = Mouse.current;
        if (mouse != null && mouse.rightButton.isPressed)
            yaw += mouse.delta.ReadValue().x * dragSpeed;

        // 上下は固定。Y 軸まわりだけ回す
        transform.rotation = Quaternion.Euler(0f, yaw, 0f);
    }

    /// <summary>やり直し時に正面(出口)へ向け直す</summary>
    public void ResetYaw(float newYaw)
    {
        yaw = newYaw;
        transform.rotation = Quaternion.Euler(0f, yaw, 0f);
    }
}

別ゲームで変えるところ: keySpeed と dragSpeed。上下も見たいなら pitch を足して Quaternion.Euler(pitch, yaw, 0f) にし、-60〜60° くらいに制限します。カメラが回っても、ClickRay はいまのカメラの向きでレイを作るので、そのまま正しく当たります。キーは Keyboard クラスの aKey・leftArrowKey などで読んでいます。

進行役:RoomGame.cs と RoomBuilder.cs(ZIP に全文)

この 2 つはゲームの枠なので、記事では要点だけにします。全文は ZIP にあります。

  • RoomGame は状態を 4 つ持ちます:Ready(準備)→ Playing(プレイ中)→ Clear(クリア)/TimeUp(時間切れ)。ClickRay は IsPlaying のときだけレイを飛ばします。
  • Playing の間だけ timeLeft -= Time.deltaTime で 40 秒から減らします。0 で TimeUp。
  • CollectCore はコアを SetActive(false) にします。GameObject ごと止めると Collider も止まり、次のレイは素通りします(GameObject.SetActive)。
  • 3 つそろうと SetExitOpen(true) で出口を緑に。TryExit は、そろっていなければ「出口はまだ閉じています」と出すだけです。
  • 色は Material.color で変えています。Built-in の Standard でも URP の Lit でも「メインの色」を指します(Material.color)。
  • R キー(Keyboard.current.rKey.wasPressedThisFrame)か画面のボタンで、いつでも最初からやり直せます。
プレイ中。コア1/3、残り27秒、箱をクリックして当たった:Crate_B(2.7m)と表示
途中:箱をクリックすると「当たった:Crate_B」。レイは最初に当たった Collider で止まります。
コア3/3。出口が緑に変わり、出口が開いた、緑の出口をクリックと表示
コアが 3 つそろうと出口が緑に。ここをクリックすれば脱出です。

Play で確認すること(レイが分かったかを見る)

  1. はじめるを押し、正面(出口)の真ん中をクリックします。左下に 「当たった:ExitGate(5.6m)」 と出れば正解です。出口の表面はカメラから 5.9m 先です。レイは近クリップ面(0.3m 先)から出発するので、距離は 5.6m になります。
  2. 床・壁・箱をクリックすると、Floor・Wall_N・Crate_B のように名前と距離が変わります。プレイ中なら何も拾えなくても、クリックするたびにレイが何に当たったかを確認できます。
  3. Play 中に Scene タブへ切り替えます。クリックのたびに、カメラから当たった所まで黄色い線が 1 秒出ます。
  4. コアを 3 つ拾うと出口が緑になります。クリックすれば「脱出成功! 残り ○○ 秒」。待っていれば時間切れ。どちらも「もう一度(R)」で戻れます。
  5. 試しに ClickRay の Hit Layers から Default を外し、Pickupを含む非ゼロのマスクを残して Play。壁や箱をクリックしても「当たった:なし」になり、コアと出口だけに当たります。これが LayerMask の役目です(試したら戻します)。
脱出成功の画面。残り14.6秒、ベスト14.6秒、もう一度ボタン
クリア画面。スコアは残り秒数です。
時間切れの画面。コア1/3、もう一度挑戦しよう、もう一度ボタン
時間切れ画面。同じ「もう一度(R)」でやり直せます。

つまずきポイント(1つ):コアをクリックしても拾えない

よくある原因は 2 つです。コアに Collider が無いか、コアのレイヤーがレイの対象外になっているかです。

  • Collider が無い: 3D モデルを読み込んだだけだと、Collider は付いていません。見た目はあるのに、レイは素通りします。RayTarget は Collider が無いと Console に「Collider がありません」と警告を出します。
  • Ignore Raycast レイヤー/Hit Layers に入っていない: Ignore Raycast レイヤーは、もともとレイを受けない用のレイヤーです(Physics.IgnoreRaycastLayer)。コアをうっかりそこに入れると、クリックはコアを抜けて奥の壁に当たります。

気づき方: 左下の「当たった:」を見ます。コアをクリックしたのに Wall_S など奥の物の名前が出たら、レイはコアを素通りしています。Play 中に Hierarchy でコアを選び、Inspector の Layer と SphereCollider のチェックを確認します。Scene ビューの黄色い線が、コアを突き抜けていないかも見ます。

できたこと

  • 3D の 1 部屋で、40 秒・コア 3 つ・出口のある小さなゲーム(目標・失敗・スコア・やり直し付き)を Unity 6.3 LTS で完成させた。
  • Input System でマウスとキーを読み、カメラをその場で左右に回した。
  • 同じルールのイン記事プレイと Unity ZIP を手に入れた。

身についたこと

  • カメラとレイ: 画面の点(左下が 0,0)→ ScreenPointToRay → 近クリップ面から出るレイ。
  • Physics.Raycast は最初に当たった Colliderだけを返す。hit.collider・hit.distance・hit.point で中身が分かる。
  • LayerMask で「当ててよいレイヤー」を選ぶ。Collider が無い物や、指定したレイヤーマスクに含まれない物には当たらない。Ignore Raycast は既定で除外されるが、明示的な指定は可能。

使い回しメモ

  • スニペット: ClickRay.cs(ScreenPointToRay → Physics.Raycast(LayerMask 付き)→ TryGetComponent)+ RayTarget.cs
  • アイデア: 3D の物を押すときは「画面の点 → レイ → 最初の Collider」。押される側は Collider と専用の小さなスクリプトを持つ。
  • 第二の用途: タワーディフェンスの設置場所クリック(hit.point に砲台を置く)、3D パズルのスイッチ、戦略ゲームのユニット選択、FPS の「調べる」ボタン。

次の自分のゲームで使うなら

見下ろし型の 3D でも同じです。地面(Default)とユニット(Pickup のようなレイヤー)を分け、ClickRay の hitLayers を切り替えるだけで「ユニットを選ぶ」「地面の行き先を選ぶ」の 2 つに使い分けられます。カメラの位置や角度が変わっても、レイはいまのカメラから作られるので、コードはそのままです。

関連商品(PR)

今週の「Unity で 3D とクリックを扱う」学習に沿うものを選びました。価格は変動するため、リンク先でご確認ください。

参考

次に足してよい一言

ClickRay の maxDistance を 4m にして「近くのコアしか届かない」にすると、見回す順番を考えるゲームになります。もう一つ足すなら、クリックすると 5 秒減る「ハズレ玉」です。RayTarget の Kind を 1 つ増やすだけで作れます。歩き回る移動や物語は、まだ足さなくて大丈夫です。

前回:1波の防衛を完成させる|Godotで学ぶ敵の出現と砲台の射程

前の Unity 回:1戦闘のターン制を完成させる|Unityで学ぶ自分の番と相手の番

次回予定:Godot で 3 枚のカードで戦う 1 対戦(データと表示を分ける)

← 記事一覧・検索