学習記事一覧 · Unity教科書_Unity6対応

Unity入門:3Dで的を狙え!Igaguriゲームを作りながら学ぼう!

題材・出典: SBクリエイティブ刊『Unityの教科書 Unity 6完全対応版』付属サンプルに基づく学習解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍付属案内・Readme・出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)

対象読者:Unity学び始めの方・C#をこれから覚えたい方 このブログでは、実際のIgaguri(いがぐり)ゲームのコードを読み解きながら、Unityの3D物理演算・レイキャスト・パーティクルを学びます。


Unity 6 で試すときの前提(チェックリスト)

この記事のコードは Unity 6 を想定した書き方です。手元のプロジェクトで次を確認してから進めるとスムーズです。

  • Input Systemusing UnityEngine.InputSystem を使います。Window → Package ManagerInput System があり、Edit → Project Settings → Player → Other Settings → Active Input HandlingInput System Package または Both になっていること。
  • Camera.main:メインカメラの Tag が MainCamera であること。付いていないと Camera.mainnull になり、発射時にエラーになります。
  • いがぐりプレハブRigidbodyParticleSystem が同じオブジェクトに付いていること(無いと GetComponentnull になります)。

記事の目次

この記事はやや長めです。目的に合わせてジャンプして読み分けてください。

おすすめの読み方

セクション一覧

ポイント一覧


このゲームで何ができるの?

今回題材にするのは、画面をクリックした方向に「いがぐり」を発射して、的に当てる3Dアクションゲームです。

  • 🖱️ マウスでクリックした方向にいがぐりが飛んでいく
  • 🌰 いがぐりは物理演算で自然な放物線を描く
  • 💥 的に当たると動きが止まり、パーティクル(爆発エフェクト)が再生される

クリックした方向にいがぐりが飛び、的に当たる様子(イメージ)

説明(学習のヒント):クリック方向に飛ばす・当たりで止める、という体験のイメージです。実装では 生成側(Generator)弾ごとの挙動(Controller) の2クラスに分かれています。

たった 2つのスクリプト(合計約30行) でこれが実現できます。3Dレイキャスト物理演算という、3Dゲーム開発の重要テクニックが凝縮されています。一緒に読み解いていきましょう!


プロジェクトの構成

講習用の Unity プロジェクトは、フォルダ Igaguri がプロジェクトのルートです(例:リポジトリ 202601 の直下に Igaguri/ がある構成)。その中の Assets が、教材で触れるファイルの置き場所です。

Igaguri/                          # Unity プロジェクトのルート
  Assets/
    ├── IgaguriGenerator.cs       # いがぐりを生成・発射するスクリプト
    ├── IgaguriController.cs      # いがぐりの動きと衝突を制御するスクリプト
    ├── igaguriPrefab.prefab      # いがぐりのプレハブ(Rigidbody・ParticleSystem 付き)
    ├── GameScene.unity           # この章で開いて動作確認するシーン
    ├── Sky.mat                   # 空などに使うマテリアル(ほか環境用アセットあり)
    └── _Tanks/                   # Unity 公式チュートリアル「Tanks」一式
          ├── Scripts/            # ゲーム管理・タンク移動・射撃・UI など(.cs が多数)
          ├── Prefabs/
          ├── Art/
          └── …                 # 環境・UI など(本章ではフォルダの存在を押さえればよい)

本章では、Tanks サンプルが用意したシーン・環境・オブジェクトの上に、オリジナルのいがぐり発射用スクリプトとプレハブを足しています。_TanksScripts だけでなく Prefabs や Art を含む大きなアセット一式です。いがぐり用のコードは Assets 直下に置かれ、教材の主役はそこだと考えてください。


クラス関係(この記事で登場する範囲)

自作クラスが少ない記事では、下のコードと対応しやすいよう、図に主要なフィールドとメソッドも載せます(MonoBehaviour は Unity 組み込みのため中身は省略)。

classDiagram direction TB class MonoBehaviour class IgaguriGenerator { GameObject igaguriPrefab +Update() } class IgaguriController { Rigidbody rb ParticleSystem ps +Awake() +Shoot(Vector3) +OnCollisionEnter(Collision) +Start() } MonoBehaviour <|-- IgaguriGenerator MonoBehaviour <|-- IgaguriController IgaguriGenerator ..> IgaguriController : "Instantiate と Shoot"

説明(学習のヒント)空のオブジェクトに付く IgaguriGenerator が生成し、プレハブ側IgaguriController に「飛ばす・当たったら止める」処理を持たせています。


コードを全部見てみよう

IgaguriGenerator.cs(いがぐりの生成と発射)

using UnityEngine;
using UnityEngine.InputSystem;  // 入力を検知するために必要!!
public class IgaguriGenerator : MonoBehaviour
{
    public GameObject igaguriPrefab;
    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            GameObject igaguri = Instantiate(igaguriPrefab);
            Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.value);
            igaguri.GetComponent<IgaguriController>().Shoot(ray.direction * 2000);
        }
    }
}

IgaguriController.cs(いがぐりの物理と衝突)

using UnityEngine;
public class IgaguriController : MonoBehaviour
{
    private Rigidbody rb;
    private ParticleSystem ps;
    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        ps = GetComponent<ParticleSystem>();
    }
    public void Shoot(Vector3 dir)
    {
        rb.AddForce(dir);
    }
    void OnCollisionEnter(Collision collision)
    {
        rb.isKinematic = true;
        ps.Play();
    }
    void Start()
    {
        Application.targetFrameRate = 60;
    }
}

RigidbodyParticleSystemAwake() で1回だけ取得してフィールドに保持しています。GetComponent<T>() は呼ぶたびに内部で検索が走るため、衝突のたびに呼ぶよりキャッシュする方が無駄がありません(今回は衝突も1回程度ですが、習慣として覚えておくとよいです)。

補足:Application.targetFrameRate の置き場所
上のサンプルでは学習の簡単さのため、いがぐりプレハブの Start() に書いています。ところがプレハブが生成されるたびに Start() が走るため、同じ設定が何度も実行されます。フレームレートはゲーム全体で1回でよい設定なので、実用的には空の GameObject に GameManager などを付け、Awake()Application.targetFrameRate = 60; だけ書く形が適切です。

この2つのスクリプトには、合計6つの学びポイントがあります。順番に見ていきましょう!


ポイント①:2Dと3Dの物理コンポーネントの違い

rb.AddForce(dir);  // rb は Awake() で GetComponent<Rigidbody>() 済み

chapter6では Rigidbody2D を使いましたが、今回は Rigidbody(3D版) です。

コンポーネント 対応する空間 主な用途
Rigidbody2D 2D(X・Y軸) 2Dゲームの物理演算
Rigidbody 3D(X・Y・Z軸) 3Dゲームの物理演算

AddForce() の使い方は同じですが、3Dでは Vector3(X・Y・Z の3要素)でベクトルを扱います。これにより、奥行き(Z軸)方向への力も加えられるようになります。


ポイント②:Camera.main.ScreenPointToRay() で画面クリックを3D空間の方向に変換する

Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.value);
igaguri.GetComponent<IgaguriController>().Shoot(ray.direction * 2000);

これがこのゲームの核心です。画面上のクリック座標(2D)を3D空間の「方向」に変換しています。

画面(2D座標)
ScreenPointToRay()
Ray(カメラから飛び出す光線)
  └─ ray.direction = 光線の方向ベクトル(正規化済み)

Ray(レイ) はカメラの位置から画面のクリック点を通って3D空間に伸びる「光線」のようなものです。この光線の方向 ray.direction にいがぐりを飛ばすことで、「クリックした方向に発射する」という直感的な操作が実現できています。

ray.direction * 2000 → 同じ方向に、2000倍の力を加える

💡 ray.direction は長さ1の正規化されたベクトルです。これに大きな数を掛けることで発射の強さを調整しています。数値を大きくすると速く飛びます。

💡 AddForce の第2引数で ForceMode.Impulse を指定すると、「一瞬だけ勢いよく飛ばす」挙動になりやすく、弾の発射のイメージに近づけられます。省略時は Force(時間あたりの力)です。まずは既定のまま数値で調整し、必要なら rb.AddForce(dir, ForceMode.Impulse); を試してください。


ポイント③:公開メソッドでいがぐりを「外から操作する」

// IgaguriController.cs
public void Shoot(Vector3 dir)
{
    rb.AddForce(dir);
}
// IgaguriGenerator.cs
igaguri.GetComponent<IgaguriController>().Shoot(ray.direction * 2000);

IgaguriControllerShoot() メソッドは public で定義されています。これにより、別のスクリプト(IgaguriGenerator)から呼び出せます。

IgaguriGenerator(発射を指示する側)
    └─ .GetComponent<IgaguriController>()
         └─ .Shoot(方向ベクトル)  ← この橋渡しで2スクリプトが連携

メソッドに Vector3 dir という引数を持たせることで、「どの方向に飛ばすか」を呼び出し元が自由に指定できます。発射ロジックと「どこから飛ばすか」の決定を分離した、再利用しやすい設計です。


ポイント④:OnCollisionEnter() で3D衝突を検知する

void OnCollisionEnter(Collision collision)
{
    rb.isKinematic = true;
    ps.Play();
}

OnCollisionEnter は3D空間で物体が衝突したときに自動的に呼ばれるメソッドです。

コールバック 次元 呼ばれるタイミング
OnCollisionEnter 3D 物体に物理的に衝突した瞬間
OnCollisionEnter2D 2D 物体に物理的に衝突した瞬間
OnTriggerEnter 3D トリガー領域に触れた瞬間
OnTriggerEnter2D 2D トリガー領域に触れた瞬間

何かに当たった瞬間に2つの処理が走ります。


ポイント⑤:isKinematic = true で物理演算を止める

rb.isKinematic = true;

isKinematictrue にすると、そのRigidbodyは物理演算の影響を受けなくなります

isKinematic = false(デフォルト)→ 重力・力・衝突の影響を受ける(飛んでいる状態)
isKinematic = true              → 物理演算を無視して静止する(刺さった状態)

いがぐりが何かに当たった瞬間に isKinematic = true にすることで、当たった場所に「刺さった」ように静止する演出が実現できています。コードを書き換えることなく、実行中に物理状態を動的に切り替えられるのがポイントです。


ポイント⑥:ParticleSystem.Play() でエフェクトを再生する

ps.Play();

ParticleSystem はUnityの標準エフェクトシステムです。炎・爆発・煙・きらめきなど、様々な視覚的エフェクトを表現できます。.Play() を呼ぶだけでエフェクトが再生されます。

メソッド 動作
Play() パーティクルを再生する
Stop() パーティクルを停止する
Clear() 表示中のパーティクルを消去する

いがぐりのプレハブに事前に ParticleSystem コンポーネントをアタッチしておくことで、「当たった瞬間に .Play() を1行呼ぶだけ」という非常にシンプルなコードでエフェクトが実現できています。


2つのスクリプトの役割分担

【IgaguriGenerator】              【IgaguriController】
空のGameObjectにアタッチ          いがぐりプレハブにアタッチ
   ↓                                    ↓
クリックを検知                    Shoot()で力を加えて発射
ScreenPointToRay で方向を計算     OnCollisionEnter で衝突を検知
Instantiate でいがぐりを生成       → isKinematic = true で静止
Shoot() を呼んで方向を渡す         → ParticleSystem.Play() でエフェクト

コードの流れを整理しよう

sequenceDiagram participant Gen as IgaguriGenerator participant Igu as IgaguriController Gen->>Gen: クリック検知 Gen->>Gen: Instantiate Gen->>Igu: Shoot Igu->>Igu: AddForce Note over Igu: OnCollisionEnter Igu->>Igu: Kinematic とパーティクル

説明(学習のヒント):上から時系列で読みます。Shoot のあとは いがぐり1個ごとOnCollisionEnter が走るイメージです。

【IgaguriGenerator の毎フレーム処理フロー】
① クリックされた?
   └─ YES → igaguriPrefab を Instantiate(生成)
            → ScreenPointToRay でクリック方向の Ray を取得
            → IgaguriController.Shoot(ray.direction * 2000) を呼ぶ
【IgaguriController の処理フロー】
Shoot(dir) が呼ばれた時
   └─ Rigidbody.AddForce(dir) → 指定方向に力を加えて飛ばす
② 何かに衝突した時(OnCollisionEnter)
   └─ isKinematic = true → 物理演算を止めて静止
   └─ ParticleSystem.Play() → エフェクトを再生

自分でカスタマイズしてみよう!

挑戦①:発射の強さを変える

igaguri.GetComponent<IgaguriController>().Shoot(ray.direction * 2000);
// 3000 にすると速く、1000 にするとゆっくり飛ぶ

挑戦②:いがぐりを連射できないようにする

// IgaguriGenerator に変数を追加して、最後の発射から一定時間後だけ発射できるようにする
float cooldown = 0;
void Update()
{
    this.cooldown -= Time.deltaTime;
    if (Mouse.current.leftButton.wasPressedThisFrame && this.cooldown <= 0)
    {
        this.cooldown = 1.0f; // 1秒のクールダウン
        // ... 発射処理
    }
}

挑戦③:衝突したオブジェクトの名前をログ出力する

void OnCollisionEnter(Collision collision)
{
    Debug.Log("衝突相手: " + collision.gameObject.name);
    rb.isKinematic = true;
    ps.Play();
}

さらに発展(消滅・スコア・サウンド・距離ボーナス)

ここからは章末の発展課題として取り組める内容です。基本の2スクリプトが動いてから順に試すとよいです。

発展①:命中から1秒後にいがぐりを消す

連射してもシーンに残骸が溜まらないようにします。OnCollisionEnter の末尾(エフェクト再生のあと)に1行足すだけです。

Destroy(gameObject, 1f);  // 1秒後にこのオブジェクト自身を削除

Destroy の第2引数は秒数の遅延です。パーティクルが途中で切れる場合は、パーティクルを子オブジェクトに分ける親だけ Destroy するなどの工夫が必要になることがあります(まずは挙動を見て調整してください)。

発展②:スコアを加算して画面に表示する(TextMeshPro)

  1. Hierarchy で右クリック → UI → Text - TextMeshPro でテキストを作成し、名前を ScoreText などにする。
  2. 空の GameObject に次のスクリプトをアタッチし、Inspector で scoreText に上記 UI を割り当てる。
using UnityEngine;
using TMPro;
public class ScoreManager : MonoBehaviour
{
    public static ScoreManager Instance;
    [SerializeField] private TextMeshProUGUI scoreText;
    private int score = 0;
    void Awake()
    {
        Instance = this;
    }
    public void AddScore(int points)
    {
        score += points;
        scoreText.text = "スコア: " + score;
    }
}
  1. IgaguriControllerOnCollisionEnter で、静止・パーティクルのあとに例えば次を呼びます。
if (ScoreManager.Instance != null)
    ScoreManager.Instance.AddScore(10);

複数の的で点数を変えたい場合は、的ごとにスクリプトで点数を持たせOnCollisionEntercollision.gameObject から GetComponent で読む、といった拡張ができます。

発展③:命中時にサウンドを鳴らす

いがぐり(ボール)プレハブAudioSource を追加し、クリップを割り当てます。命中のフィードバックは「飛ばしている側」で鳴らすのが分かりやすいです。

// フィールド例(Awake で取得)
private AudioSource audioSource;
void Awake()
{
    rb = GetComponent<Rigidbody>();
    ps = GetComponent<ParticleSystem>();
    audioSource = GetComponent<AudioSource>();
}
void OnCollisionEnter(Collision collision)
{
    rb.isKinematic = true;
    ps.Play();
    if (audioSource != null)
        audioSource.Play();
}

AudioSourcePlay On Awake はオフにして、命中時だけ Play() する使い方が一般的です。

発展④:的の中心に近いほど高得点(距離ボーナス)

事前準備:Hierarchy で的オブジェクトを選び、Inspector 上部の TagTarget を追加して割り当てます。Tag が付いていないと CompareTag("Target") が常に偽になります。

的がおおむね平らな的(正面から撃つ)前提では、中心に近いほど高得点・外周(端)に近いほど低得点とすると、ダーツのイメージで直感に合います。式では「中心からの距離」を radius(的の端までのおおよその距離) で割り、端で比率0・中心で比率1になるようにしてから点数にします。こうすると 外周付近では0点に近づき、調整の意図が説明しやすいです。

本記事では radius は数値を手で決める形にしています。Prefab のスケールを変えてもコードを直さずに追従させたい場合は、コライダの bounds.extents から推定する方法があります。補足:Igaguri 距離ボーナス(radius を Bounds.extents から) にまとめています。

中心の座標には collision.collider.bounds.center を使います。Transform.position はピボット(原点)の位置なので、モデルによっては足元やポールの根本になり、的の板の中心とずれがちです。一方、bounds.center はそのコライダのワールド座標での包囲ボックスの中心なので、ポールの上に載せた的でも「狙い面の中心」に近い基準になりやすいです。厚みのある立体や斜めからの衝突では、同じ「表面の当たり」でも距離の意味が変わる点は教材として触れておくとよいです。

補足:的に厚みがあると最高点が取りづらい
距離は 表面の衝突点bounds.center(コライダ全体のおおよそ中心)との 3D 距離です。コライダが厚いと、正面の見た目の「ど真ん中」に当てても 厚みの半分ほどの距離が必ず乗るため、closeness が 1 に近づきにくくなります。コードを複雑にせずに済ませるには、見た目用のメッシュはそのままにして、当たり判定だけ 薄い Box Collider(手前の面に合わせ、厚み方向のサイズを小さくした箱)を使う方法が手軽です。Target タグは、実際に当たる薄いコライダが付いている GameObject に付けます(子オブジェクトにコライダだけある構成なら、その子にタグを付ける。CompareTagcollision.gameObject を見るためです)。

void OnCollisionEnter(Collision collision)
{
    if (!collision.gameObject.CompareTag("Target"))
        return;
    if (collision.contactCount < 1)
        return;
    Vector3 targetCenter = collision.collider.bounds.center;
    Vector3 collisionPoint = collision.GetContact(0).point;
    float distance = Vector3.Distance(collisionPoint, targetCenter);
    float radius = 10.0f; // 中心から的の端までのおおよその距離(Scene のスケールに合わせて調整)
    int maxPoints = 100;    // 中心付近ヒット時の最高点の目安
    float closeness = Mathf.Clamp01(1f - distance / radius); // 端≈0、中心≈1
    int hitScore = Mathf.RoundToInt(closeness * maxPoints);
    if (ScoreManager.Instance != null)
        ScoreManager.Instance.AddScore(hitScore);
    rb.isKinematic = true;
    ps.Play();
}

radius的の大きさ(中心から外周までの距離の目安)に合わせて 手で変えます大きすぎると端に当てても比率が残り、点数が高めに出やすくなります。小さすぎると中心から少しずれただけですぐ0点近くなります。distanceradius を超えた場合は Clamp01 で比率0になり、外側の命中は0点です。maxPoints は中心ヒット時の上限だけを変えたいときに調整します。Scene 上のスケールを見ながら試してください。

例(調整の目安)maxPoints = 100radius = 7f なのに、端に当てても 70 点台のように高く残ることがあります。hitScore はおおよそ (1 - distance/radius) * maxPoints なので、端での distanceradius に比べてまだ小さい(=radius を実際の「中心から端まで」より大きく取りすぎている)と起きやすいです。端の点数を下げたいときは radius少しずつ小さくして試し、的の見た目の外周に近い命中で 0 点に近づくところまで合わせます。薄いコライダや bounds.center の取り方で、端でも distance が小さめに出る場合もある、と前段の補足と合わせて考えてください。

発展②の固定10点や発展③のサウンドと同時に使う場合は、OnCollisionEnter を1つにまとめ、上の処理を順に並べればよいです。


まとめ

今回のIgaguriゲームから学んだことを整理しましょう。

  • RigidbodyParticleSystemAwake() で取得してフィールドに保持すると、GetComponent の無駄な繰り返しを避けられる
  • Rigidbody(3D)は Rigidbody2D(2D)と使い方は同じで、3D空間(Vector3)で動作する
  • Camera.main.ScreenPointToRay() で画面クリック座標を3D空間の光線(Ray)に変換できる
  • ray.direction は方向ベクトルで、これに大きな数を掛けて発射力にする
  • public メソッドに引数を持たせると、呼び出し元から発射方向を柔軟に指定できる
  • OnCollisionEnter は3D物体が衝突した瞬間に自動で呼ばれる(2D版は OnCollisionEnter2D
  • Rigidbody.isKinematic = true にすると実行中に物理演算を動的に止められる
  • ParticleSystem.Play() を1行書くだけでエフェクトを再生できる

2Dゲームから3Dゲームへの拡張として、座標の扱いが Vector2 から Vector3 になり、ScreenPointToRay という3D特有のテクニックが登場しました。「動く→コードを読む→カスタマイズする」 のサイクルを繰り返すことが、Unity上達の近道です。

次のステップとして、当たった的をカウントするスコア機能や、一定数当てたらゲームクリアになる仕組みを追加してみましょう!


最終更新:2026年4月