※本記事にはアフィリエイトリンクが含まれています。
Amazonのアソシエイトとして、ゲームのタネは適格販売により収入を得ています。
▶ この回の完成形を、この場で遊ぶ
ブラウザで動く『3Dの1部屋から脱出』です。Unity 6.3 LTS の完成プロジェクトと同じルール(制限時間 40 秒、光るコア 3 つ、候補 6 か所から毎回 3 か所、部屋の真ん中から見回すだけ)です。
記事中の 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 点は、3D の世界では「カメラから奥へまっすぐ伸びる線」に対応します。公式マニュアルのIntroduction to raycastingでも、カメラの視界の各点は世界の中の線に対応する、と説明されています。

- 画面の点: マウスの位置はピクセルで、画面の左下が (0, 0) です(Camera.ScreenPointToRay)。
- レイ:
ScreenPointToRayが返すRayは、カメラの位置ではなく近クリップ面(今回は 0.3m 先)から出発します。 - 当たり: Physics.Raycast は、レイが Collider に当たれば true を返します。
out RaycastHit hitには、当たったcollider・point(位置)・distance(レイの出発点からの距離)が入ります(RaycastHit)。
準備(Unity 6.3 LTS)
- Unity 6 の公式ページの案内に沿って、Unity Hub から Unity 6.3 LTS を入れます。LTS は長期サポート版です。執筆時点の最新パッチは 6000.3.25f1(2026年9月24日公開)で、第4回と同じ 6.3 系です。
- 上の unity-room-escape.zip を解凍し、Hub の「Add」→「Add project from disk」でそのフォルダを開きます。記事に載せていない進行役と部屋の生成コードも必要なため、まずは ZIP の完成プロジェクトを使ってください。
- 入力は 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)。 - Project Settings → Tags and Layers で、User Layer 6 に Pickup を追加します(ZIP は設定済み)。コアと出口だけをこのレイヤーにします(レイヤーの割り当て)。

ルール(数字は記事・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 で確認してください。自分で手動配置に変える場合は、生成処理と進行役の参照も合わせて変更する必要があります。
- Main Camera:位置 (0, 1.6, 0)、Projection は Perspective、Field of View 60、Clipping Planes の Near 0.3。
ClickRayとCameraLookを付けます(ZIP は付けてあります。外れていても RoomGame が足します)。 - 空の GameObject「RoomGame」:
RoomGame.csを付けます。HUD(コア数・残り秒・「当たった」表示)はOnGUIで文字を描くだけなので、Canvas は使いません。 - 部屋:Cube を 6 つ(Floor・Ceiling・Wall_N/S/E/W)。Cube には最初から BoxCollider が付いています(GameObject.CreatePrimitive)。
- 箱:Cube を 2 つ(Crate_A・Crate_B、0.8m 角)。Default レイヤーのままにして、レイを止める役にします。
- コア:Sphere を 3 つ(Core_1〜3、直径 0.6m)。SphereCollider 付き。Layer を Pickup にし、
RayTarget(種類 Core)を付けます。 - 出口:薄い 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)か画面のボタンで、いつでも最初からやり直せます。


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


つまずきポイント(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 とクリックを扱う」学習に沿うものを選びました。価格は変動するため、リンク先でご確認ください。
- 楽しく学ぶUnity「3Dゲーム」作りのきほん Unity6対応(森巧尚)— 3D のシーン・カメラ・当たり判定を、次の一歩として本で固めたいときに。
楽しく学ぶUnity「3Dゲーム」作りのきほん Unity6対応(Amazon)
※本リンクはアフィリエイトリンクです。
楽しく学ぶUnity「3Dゲーム」作りのきほん Unity6対応(楽天市場)
※本リンクはアフィリエイトリンクです。 - Unityの教科書 Unity 6完全対応版(北村愛実)— エディタ操作とスクリプトの付け方を、最初から通して確認したいときに。
Unityの教科書 Unity 6完全対応版(Amazon)
※本リンクはアフィリエイトリンクです。
Unityの教科書 Unity 6完全対応版(楽天市場)
※本リンクはアフィリエイトリンクです。 - Logicool G304 LIGHTSPEED ワイヤレスゲーミングマウス — 今回のような「狙ってクリック」と右ドラッグの見回しを、テストプレイで何度も繰り返すときに。
Logicool G304 LIGHTSPEED ワイヤレスゲーミングマウス(Amazon)
※本リンクはアフィリエイトリンクです。
Logicool G304 LIGHTSPEED ワイヤレスゲーミングマウス(楽天市場)
※本リンクはアフィリエイトリンクです。
参考
- Unity 6 / Unity 6.3 LTS(Unity 公式)
- Unity 6000.3.25f1 リリースノート(Unity 公式)
- Unity ダウンロードアーカイブ(Unity 公式)
- Camera.ScreenPointToRay(Unity 6.3 スクリプトリファレンス)
- Physics.Raycast(Unity 6.3 スクリプトリファレンス)
- RaycastHit(Unity 6.3 スクリプトリファレンス)
- LayerMask.GetMask(Unity 6.3 スクリプトリファレンス)
- Physics.IgnoreRaycastLayer(Unity 6.3 スクリプトリファレンス)
- Collider(Unity 6.3 スクリプトリファレンス)
- Introduction to raycasting(Unity 6.3 マニュアル)
- Cast a ray from a camera(Unity 6.3 マニュアル)
- Assign GameObjects to layers(Unity 6.3 マニュアル)
- Layers and layerMasks(Unity 6.3 マニュアル)
- Input System パッケージ(Unity 6.3 マニュアル)
- Input System 1.20 マニュアル
- Directly Reading Device States(Input System 1.20)
- Mouse クラス(Input System 1.20)
- Keyboard クラス(Input System 1.20)
- Input.mousePosition(旧 Input Manager の注意書き)
- Object.FindFirstObjectByType(Unity 6.3 スクリプトリファレンス)
- Component.TryGetComponent(Unity 6.3 スクリプトリファレンス)
- GameObject.CreatePrimitive(Unity 6.3 スクリプトリファレンス)
- GameObject.SetActive(Unity 6.3 スクリプトリファレンス)
- Material.color(Unity 6.3 スクリプトリファレンス)
- Debug.DrawLine(Unity 6.3 スクリプトリファレンス)
次に足してよい一言
ClickRay の maxDistance を 4m にして「近くのコアしか届かない」にすると、見回す順番を考えるゲームになります。もう一つ足すなら、クリックすると 5 秒減る「ハズレ玉」です。RayTarget の Kind を 1 つ増やすだけで作れます。歩き回る移動や物語は、まだ足さなくて大丈夫です。
前回:1波の防衛を完成させる|Godotで学ぶ敵の出現と砲台の射程
前の Unity 回:1戦闘のターン制を完成させる|Unityで学ぶ自分の番と相手の番
次回予定:Godot で 3 枚のカードで戦う 1 対戦(データと表示を分ける)