# ティミーの伝説

初めての3Dゲームを作ってみましょう

# プロジェクト設定

まず、新規プロジェクトを作成しましょう。2Dゲームであるため「Universal 3D」を選んでください。プロジェクト名は「legend」にしましょう。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/XfWimage.png)

### 素材を追加

- 3Dモデル 
    - 主人公（Timmy）とゾンビのモデル（[Mixamo](https://www.mixamo.com/)）
    - キャラクターのアニメーション
    - ステージ作成のパーツ（[Kenney](https://kenney.nl/))
- 画像、テクスチャ 
    - プロトタイプ用のテクスチャ
    - UIのスプライト
- フォント（[Google Fonts](https://fonts.google.com/))

すべての素材をプロジェクトに追加しましょう。

## プロトタイプ用のシーンを構築

本番のシーンを作る前、各部品を個別で開発しなければならない。キャラクターの移動やカメラの操作などを作業用のシーンで開発し、出来上がったら本番シーンに入れると快適に仕事ができる。

それでは、「Scenes」フォルダーを作成し「Prototype」の新しいシーンを追加してください。その後、床面になる「キューブ」を追加し、サイズは \[50 x 0.1 x 50\] で広い遊ぶ場所を準備しましょう

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/ou9image.png)

ただし、このままだと、真っ白な床面で動きなどが把握できないので、単純なプロトタイプ用のテクスチャ（greybox\_grey\_grid）をドラッグドロップで追加しましょう。ただし、床面のサイズは50倍大きくなったので、テクスチャも50回をタイル化しましょう。床の「Material」（マテリアル）を選択し、「Tiling」を 50 x 50 にしてください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/mTNimage.png)

また、坂を上るとジャンプのテストもやりたいので、ランプと箱を追加しましょう。ランプはキューブから作成し、斜めに倒せば良いでしょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/grdimage.png)

最後、ただのキューブを作成し、ランプとつながるようにする：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/oCnimage.png)

素材を作成する前に、ゲームの操作、面白さ、楽しさを確かめらためのプロトタイプシーンは「[Grayboxing](http://www.neilblevins.com/art_lessons/greybox/greybox.htm)」（グレーボクシング）という。ある程度コストを削減し、まずゲーム性を確かめるためのシンプルなレベルである。

# Timmyの移動を実装

## 主人公のゲームオブジェクトを作成

主人公のキャラクターの移動を実装しましょう。まず、空のゲームオブジェクトを作成し、「Player」という名前を付けてください。このゲームオブジェクトはプレーヤーの役割にする。

### 3Dモデルを追加

Characters/Timmy/Models から Timmy のモデルを追加してください。Player の子オブジェクトとして追加しましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/27Gimage.png)

よく見ると、色が多少おかしい。これはノーマルマップ（※）の問題であるので、解決しましょう。Characters/Timmy/Textures の中にあるノーマルテクスチャ（青い画像）を選択し、ノーマルマップであることを指定し、最後に「Apply」を押してください：

<table id="bkmrk-%C2%A0-%EF%BC%88%E2%80%BB%EF%BC%89%E3%83%8E%E3%83%BC%E3%83%9E%E3%83%AB%E3%83%9E%E3%83%83%E3%83%97%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AF%E3%80%81%E3%81%BE%E3%81%9F" style="border-collapse:collapse;width:100%;border-width:0px;"><colgroup><col style="width:60.0664%;"></col><col style="width:39.8919%;"></col></colgroup><tbody><tr><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/cHaimage.png)

（※）ノーマルマップについては、また「ゲームエンジンII」で勉強する予定

</td><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Mgximage.png)

</td></tr></tbody></table>

### 物理処理の準備

キャラクターを物理的に移動するべきなので、コライダーとリジッドボディが必要。Player に Rigidbody と CapsuleCollider を追加してください。なお、コライダーの形がある程度 3Dモデルの体に合わせましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/kLiimage.png)

## スクリプト

Inputクラスを使用し、キャラクターを動かしましょう。後で改善するべきですが、とりあえず、WASD のキーで前後左右を移動しましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/MKBimage.png)

ＡとＤでＸ軸（赤い軸）で移動し、Ｗ とＳでＺ軸（青い軸）で移動する。なお、各入力は：

- `Input.GetAxis("Horizontal")`: 入力の「水平軸」（つまりAとD）の入力をの取得ができる 
    - Ａの場合は -１ を返す
    - Ｄの場合は１を返す
    - 入力のない場合は０を返す
- `Input.GetAxis("Vertical")`: 入力の「垂直軸」（つまりＷとＳ）の入力をの取得ができる 
    - Ｗの場合は１ を返す
    - Ｄの場合は-１を返す
    - 入力のない場合は０を返す

また、移動速度をUnityで変えられるようにしましょう。この知識を活用し、移動のスクリプト「PlayerMove」を実装：

```c#
// プレーヤーの移動を処理する
public class PlayerMove : MonoBehaviour
{
    // 速度をInspectorで設定
    [SerializeField]
    private float speed = 5.0f;
    
    // 物理的に移動するので、リジッドボディが必要
    private Rigidbody rbody;

    // 3D空間の速度ベクトル
    private Vector3 velocity;

    private void Start()
    {
        // 参照を取っておく
        rbody = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        // 何も入力がない前提で、速度をゼロにする
        velocity = Vector3.zero;
        
        // A/S（水平入力）を取得し、3D空間のX軸に割り当てる
        velocity.x = Input.GetAxis("Horizontal");
        
        // W/D (垂直入力)を取得し、3D空間のZ軸に割り当てる
        velocity.z = Input.GetAxis("Vertical");
        
        // 単位ベクトルにする
        velocity.Normalize();
        
        // 速度を乗算する
        velocity *= speed;
    }

    private void FixedUpdate()
    {
        // 速度を設定
        rbody.linearVelocity = velocity;
    }
}
```

##### `Normalize()`とは

`Normalize()`はあるベクトルを長さ１（単位ベクトル）にする。今回の処理は以下の図で解析してある：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/ZmOimage.png)

上下、または左右 **<span style="text-decoration:underline;">のみ</span>** で移動する場合は、`GetAxis` が -1～1 の数値を返すので、移動速度「speed」を掛けると、問題なく、長さ５の速度ベクトルができる。一方、同時に入力すると（斜め移動）

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Uxvimage.png)

掛け算の前に単位ベクトルにすると：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/EYDimage.png)

#### 実行確認

このスクリプトを「Player」にアタッチし、実行してみてください。移動し、ランプを上り、ジャンプしてみてください。なお、カメラの処理まだ行っていないので、Game ビューと Scene ビューを同時に表示するのはおすすめ。

#### 問題：倒れたり、回転したりする

すべての動きをリジッドボディに任せているため、移動だけではなく、回転も自由になっている。今後、回転はスクリプトで実現するので、リジッドボディが回転ができないようにすれば良いでしょう。制約設定で回転を固定しましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/VhNimage.png)

#### 問題：ゆっくり落ちる

回転の問題を解決できたが、ランプから飛び出すと、ゆっくりに落ちる。なぜなら、Updateごとに<span style="color:rgb(186,55,42);">**速度を上書きしている**</span>からです！重力による落下をリジッドボディに任せても、縦軸（Y軸）の速度をゼロにしているので、なかなか落ちない！

解決としては、Y軸の速度を保守し、XとZだけを上書きすれば良い。`FixedUpdate` を以下のように直しましょう：

```c#
private void FixedUpdate()
{
    // 現在の速度を取得
    Vector3 v = rbody.linearVelocity;
    
    // XとZ を上書き
    v.x = velocity.x;
    v.z = velocity.z;
    
    // 速度を設定
    rbody.linearVelocity = v;
}
```

これで確認しましょう。

# 主人公を追いかけるカメラ

Timmyの移動できたとしても、操作が確認しにくいので、プレーヤーを追いかけるカメラ（<span class="HwtZe" lang="ja"><span class="jCAhz ChMk0b"><span class="ryNqvb">三人称視点</span></span></span>）を作りましょう

## 単純なアプローチ

一番簡単なのは「カメラの位置を主人公の位置にしろ！」の仕組みである。これをすぐ試せるので、「CameraController」スクリプトを作成してみましょう：

### スクリプト

```c#
// 主人公を追いかけるカメラ
public class CameraController : MonoBehaviour
{
    // 追いかけるオブジェクトのTransform
    [SerializeField] 
    private Transform target;

    private void Update()
    {
        // 自分の位置をターゲットの位置にするだけ
        transform.position = target.position;
    }
}
```

このスクリプトをカメラにアタッチし、確認しましょう。なお、Unity でカメラの「target」を「Player」にしてください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Hu0image.png)

## 改善：セルフィスティック

今の処理はとても単純で、カメラはプレーヤーの足元に行ってしまいました。もう少し賢い作戦を考えてみましょう。「三人称」といえば：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/ULoimage.png)

セルフィスティックは良いでしょう！シーンにあるカメラの親子関係を変えて、同じ仕組みになるようにしましょう。具体的に：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/wPBimage.png)

- Base（基礎） ：主人公を追いかける根。セルフィスティックなら、「人間が立っている場所」
- Height（高さ） ：地面からの高さ。だいたいTimmyの頭までにすれば良いでしょう。
- Pole（ポール） ：スティックのそのもの。上がるか下がることにより、撮影角度を変えられる
- Camera（カメラ）：スティックの端にあるカメラ。適切な距離にし、Timmyが全身が見えるようにしましょう。

これはヒエラルキー（とその位置）はこうなる：

<table class="align-center" id="bkmrk--3" style="border-collapse:collapse;border-width:0px;"><colgroup><col style="width:50%;"></col><col style="width:50%;"></col></colgroup><tbody><tr><td colspan="2" style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/79Zimage.png)

</td></tr><tr><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/kdEimage.png)

</td><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/emYimage.png)

</td></tr><tr><td style="border-width:0px;">[![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/3zVimage.png)](https://class.illogic.games/uploads/images/gallery/2026-05/3zVimage.png)

</td><td style="border-width:0px;">[![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Kapimage.png)](https://class.illogic.games/uploads/images/gallery/2026-05/Kapimage.png)

</td></tr></tbody></table>

なお、「CameraController」のスクリプトは、カメラではなく、実際に動く基礎（Base）アタッチすべき。Main Camera からスクリプトを削除するのは、右側にある「**⋮**」を押し、「Remove Component」（コンポーネントを削除）でできる。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/gLwimage.png)

では、「CameraController」をBaseにアタッチし、もう一度確認しましょう。

## カメラをマウスで回転

カメラは主人公に追いかけても、周りを自由に見たいので、マウスでカメラを回転させたい。今のカメラの作りだと簡単に責任を分けることができる！

- Height オブジェクトをY軸まわりに回転させると、360度の回転ができる。
- Pole オブジェクトをX軸まわりに回転させると、上下の角度を変えることができる。

IMAGE: Height rotating and Pole pivoting

そして、WASDの入力は `Input.GetAxis("Horizontal");` と `Input.GetAxis("Vertical");` で取得できたが、同じようにカメラの回転 `Input.GetAxis("CamRotY");` とカメラの上下角度 `Input.GetAxis("CamRotX");` を取れれば良いけど、この入力軸が定義されていない。

### 新しい入力を定義する

#### 入力マネージャーの紹介

*※以下は「入力マネージャー（旧）」に対する話しである。２年生になったら「入力システム（新）」に切り替える予定。*

`"Horizontal"` （水平入力）と `"Vertical"` （垂直入力）が魔法の言葉ではなく、プロジェクト設定として定義されている入力である。これをトップメニューの「Edit / Project Settings」（編集 / プロジェクト設定）の中にある「Input Manager」（入力マネージャー）から確認ができる。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Xb5image.png)

ここで「Horizontal」という入力名は「Positive」（正の数）と「Negative」（負の数）があるので、‐１～１の間の「軸」であることを確認できる。つまり、Negative Buttonの「a」を押すと、`Input.GetAxis("Horizontal")` が -1 を返す、Positive Button の「d」を押すと`Input.GetAxis("Horizontal")` が１を返す。この行動がすでに使用し、Timmyを動かしました。

その他には、Fire1～Fire3 や Jump の入力名がある。これはデフォルトとして定義されている入力であるが、変えたり、消したり、増やしたりすることが可能である。

「Jump」を確認してみましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/CUcimage.png)

この入力は「Positive」のみで、「Negative」がない。つまり、この入力は「軸」ではなく「ボタン」である。ボタンは「`Input.GetButtonXXX("入力名")`」で取得でき、「true」または「false」を返す。`GetMouseButtonXXX` と同様に３パターンがある：

- `GetButtonDown("入力名")`： 押された瞬間に「true」を返す。
- `GetButton("入力名")`：押し続けるときに「true」を返す。
- `GetButtonUp("入力名")`：放した瞬間に「true」を返す。

デフォルト設定としては「Jump」という入力は「space」に設置されているので、スペースキーを押したら `GetButtonDown("Jump")` は true になる！

もちろん、これらあくまで初期設定なので、キーを変えて良いし、名前も変えても大丈夫（例えば、「Kick」「Punch」「Attack」など）

もうちょっと見ると、多くの入力は２回が現れる！もう一つの "Horizontal" を確認すると：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/aFlimage.png)

あれ？「Positive」も「Negative」もない？なにこれ？

これは、"Horizontal" に対するもう一つの入力処理である。Typeを詳しくみると、「Joystick」というキーワードが書かれている。

そう！これはコントローラーに対応する入力。A/Dを使えば、"Horizontal" が発動するし、コントローラーの「X Axis」を使うと、同じ "Horizontal" が発動する。

<span style="color:rgb(35,111,161);">**つまり、入力マネージャーが入力ハードウェアを抽象化してくれる機能である！**</span>

キーワード＆マウス、コントローラーを関係なく、C#で「Horizontal」を処理すれば十分。Unity が裏側で入力を通訳してくれる。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/2csimage.png)

#### カメラ操作の入力を作成

すでに「MouseX」と「MouseY」の入力を定義されているので、これを再利用しましょう。MouseX はマウスの横の動きなので、これを「CamRotY」にしましょう。同様に MouseY を「CamRotX」に書き換えましょう。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/tXcimage.png)

これで、後ほどコントローラーの対応を追加したいなら、同じ名前の新しい入力を追加し、適切なボタンやスティックを割り当てることが可能。

### スクリプト

#### 360度回転

CameraController を更新しましょう。まず、Y軸まわり（360度回転）を実装するには先ほど定義した「CamRotY」の入力を活用し、Height を回転する。

```c#
// 主人公を追いかけるカメラ
public class CameraController : MonoBehaviour
{
    // 追いかけるオブジェクトのTransform
    [SerializeField] 
    private Transform target;

    // 360度回転に使うTransform
    [SerializeField] 
    private Transform height;
    
    private void Update()
    {
        // 自分の位置をターゲットの位置
        transform.position = target.position;
        
        // 回転処理
        Rotate360();
    }

    // 360度の回転する
    private void Rotate360()
    {
        // トランスフォームの回転を取得
        // Quaternion（クオーターニオン・四元数）は回転を表すクラスである
        // Quaternionについては、「ゲーム数学」でまた勉強する
        Quaternion rot = height.localRotation;
        
        // このまま使えないので、XYZの回転角度（オイラー角度）にする
        Vector3 angles = rot.eulerAngles;
        
        // Y軸だけを更新
        angles.y += Input.GetAxis("CamRotY");
        
        // 回転を更新（オイラー角度 → クオーターニオンに戻す）
        height.localRotation = Quaternion.Euler(angles);
    }
}
```

一応、回転するが、速度が多少遅く感じるので、回転速度を「度 / 秒」単位で調整できるようにしましょう：

```c#
using UnityEngine;

// 主人公を追いかけるカメラ
public class CameraController : MonoBehaviour
{
    // 回転速度（度／秒）
    [SerializeField] 
    private float rotSpeed = 10;

    // （省略）
  
    // 360度の回転する
    private void Rotate360()
    {
        // トランスフォームの回転を取得
        // Quaternion（クオーターニオン・四元数）は回転を表すクラスである
        // Quaternionについては、「ゲーム数学」でまた勉強する
        Quaternion rot = height.localRotation;
        
        // このまま使えないので、XYZの回転角度（オイラー角度）にする
        Vector3 angles = rot.eulerAngles;
        
        // Y軸だけを更新
        angles.y += Input.GetAxis("CamRotY") * rotSpeed * Time.deltaTime;
        
        // 回転を更新（オイラー角度 → クオーターニオンに戻す）
        height.localRotation = Quaternion.Euler(angles);
    }
}
```

#### 上下回転

これで主人公まわり360度の回転が実現できたが、今度、カメラの上下角度を変えたい。このため、「Pole」をX 軸まわりに回転すれば良いでしょう。`Input.GetAxis("CamRotX");` を使用し、実装しましょう：

```c#
using UnityEngine;

// 主人公を追いかけるカメラ
public class CameraController : MonoBehaviour
{
    // 上下角度を変えるために使うTransform
    [SerializeField] 
    private Transform pole;

    // （省略）
  
    private void Update()
    {
        // 自分の位置をターゲットの位置
        transform.position = target.position;
        
        // 回転処理
        Rotate360();
        RotateUpDown();
    }
    
    // 360度の回転する
    private void Rotate360()　// （省略）
    
    // 上下の回転
    private void RotateUpDown()
    {
        // ポールの回転を取得し、X軸まわりだけ回転する
        Vector3 angles = pole.localRotation.eulerAngles;
        angles.x -= Input.GetAxis("CamRotX") * rotSpeed * Time.deltaTime;
        pole.localRotation = Quaternion.Euler(angles);
    }
}
```

これで上下の回転できたが、カメラが逆さまになるのが望ましくないので、「最低角度」と「最大角度」を設定しましょう。これは自由に変わらないので、定数（`const`）として宣言しても良い。

```c#
// 上下の回転
private void RotateUpDown()
{
    //　最低と最大角度
    const float MinAngle = -30f;
    const float MaxAngle = 60f;
    
    // ポールの回転を取得し、X軸まわりだけ回転する
    Vector3 angles = pole.localRotation.eulerAngles;
    angles.x -= Input.GetAxis("CamRotX") * rotSpeed * Time.deltaTime;
    
    // 制限
    angles.x = Mathf.Clamp(angles.x, MinAngle, MaxAngle);
    pole.localRotation = Quaternion.Euler(angles);
}
```

#### 問題発生

あれ？上はちゃんと制限されているが、下の方に行くと、また上に飛んでしまう？

一旦制限を取り消し、`Debug.Log` で角度を確認してみると「 - 角度」ではなく、回転一周まわって、また360度から始まってしまう！これが、`Quaternion` の内部的な処理であり、角度が０以下になると、360度に変わり、360は `MaxAngle` を超えるので、`Mathf.Clamp` で 最大角度に飛んでしまう！

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/UTDimage.png)

解決には、`localRotation` の角度を使用せず、内部の変数を使えば良いでしょう：

```c
// 上下回転の角度
private float angleX = 0;

// (省略) 

// 上下の回転
private void RotateUpDown()
{
    //　最低と最大角度
    const float MinAngle = -30f;
    const float MaxAngle = 60f;
    
    // 角度を更新し、制限
    angleX -= Input.GetAxis("CamRotX") * rotSpeed * Time.deltaTime;
    angleX = Mathf.Clamp(angleX, MinAngle, MaxAngle);
    
    // ポールの回転を取得し、X軸まわりだけ回転する
    pole.localRotation = Quaternion.Euler(angleX, 0, 0);
}
```

# Timmyがカメラに相対的に移動する

これでカメラが自由に回転できるようになったが、今度何かおかしい現象が発生。

カメラが主人公の後ろにある場合は、何とか正しく動きますが、真横、または正面から見ると入力する、主人公の動きが変に感じる。なぜなら、キャラクターがカメラの位置を関係なく、世界のXZ軸に沿って動くので、カメラの向きが変わったら、おかしく感じる。

この問題を解決するには、世界に絶対的に動くのではなく、カメラの向きに合わせて相対的に移動すること。

<table id="bkmrk-%E3%81%93%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E3%82%AB%E3%83%A1%E3%83%A9%E5%89%8D%E6%96%B9%EF%BC%88%E9%9D%92%E3%81%84%E7%9F%A2%E5%8D%B0%EF%BC%89%E3%81%8C%E4%B8%96%E7%95%8C" style="border-collapse:collapse;width:100%;height:389.5px;"><colgroup><col style="width:50%;"></col><col style="width:50%;"></col></colgroup><tbody><tr style="height:389.5px;"><td style="height:389.5px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/NI7image.png)

</td><td style="height:389.5px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Vlnimage.png)</td></tr><tr><td>この場合は、カメラ前方（青い矢印）が世界のZ軸と並行になっているので、「W」キーを押すと、正しく移動する

</td><td>一方、Heightを回すと、カメラ前方の向きが変わり、「W」を押すと、世界に対して斜め（青い矢印の向き）に移動してほしい</td></tr></tbody></table>

## スクリプト

`PlayerMove` を編集し、世界のXZではなく、Heightの回転を従って移動させましょう。これには、[Transform コンポネントの「forward」と「right」を使いましょう！](https://nekojara.city/unity-object-direction)

```c
using UnityEngine;

// プレーヤーの移動を処理する
public class PlayerMove : MonoBehaviour
{
    // カメラのY軸回転状況を知りたい
    [SerializeField] 
    private Transform cameraYRot;

    // （省略）
    
    private void Update()
    {
        // 何も入力がない前提で、速度をゼロにする
        velocity = Vector3.zero;
        
        // A/S（水平入力）を取得し、3D空間のX軸に割り当てる
        // velocity.x = Input.GetAxis("Horizontal");

        // W/D (垂直入力)を取得し、3D空間のZ軸に割り当てる
        // velocity.z = Input.GetAxis("Vertical");
        
        // Heightオブジェクトの向きを従って移動
        velocity += cameraYRot.right * Input.GetAxis("Horizontal");
        velocity += cameraYRot.forward * Input.GetAxis("Vertical");
        
        // 単位ベクトルにする
        velocity.Normalize();
        
        // 速度を乗算する
        velocity *= speed;
    }

    // （省略）
}
```

`cameraRotY` はカメラが Y 軸まわるオブジェクトなので、「Height」にして、実行してみてください。

### Timmyの向きを進行方向に合わせる

せっかくなので、Timmyが移動中に走っている向きに合わせて、回転しましょう。すでに「forward」を学んだので利用できる。Timmyの前方（`transform.forward`）は速度ベクトルと同じ。ただし、向きを変えるのは、**<span style="color:rgb(186,55,42);">入力がある時だけ。</span>**

上記の `Update()` は以下のように変わる：

```c#
private void Update()
{
    // 何も入力がない前提で、速度をゼロにする
    velocity = Vector3.zero;
    
    // Heightオブジェクトの向きを従って移動
    velocity += cameraYRot.right * Input.GetAxis("Horizontal");
    velocity += cameraYRot.forward * Input.GetAxis("Vertical");
    
    // 入力あれば、速度ベクトルはゼロではないはず
    if (velocity != Vector3.zero)
    {
        // 単位ベクトルにする
        velocity.Normalize();
        
        // 前方の向きを変える
        transform.forward = velocity;
        
        // 速度を乗算する
        velocity *= speed;
    }
}
```

# ジャンプを実装

ジャンプは前後左右の移動とは関係ないため、別のスクリプトとして実装ができる。同じPlayerMoveでも実装できるが、「一つのスクリプト＝１つの責任」の考えを従い、今回はジャンプ専用の「PlayerJump」スクリプトを作成しましょう。

このスクリプトは「Jump」という入力（プロジェクト設定→入力マネージャーを確認）が発動したら、一旦Y軸の速度を変えて、上がるようにしましょう。また、ジャンプ力をUnityで変えられるようにしましょう。

## スクリプト

```c#
using UnityEngine;

// ジャンプ機能を追加するスクリプト
public class PlayerJump : MonoBehaviour
{
    // ジャンプ力を設定する
    [SerializeField] 
    private float jumpPower = 5.0f;

    // 速度を変えるためのリジッドボディ
    private Rigidbody rbody;

    // ジャンプしろ！
    private bool doJump;
    
    void Start()
    {
        // コンポネント取得
        rbody = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // ジャンプしたか？
        if (Input.GetButtonDown("Jump"))
            doJump = true;
    }
    
    private void FixedUpdate()
    {
        // ジャンプしていなければ、なにもしない
        if (!doJump)
            return; // メソッド終了
        
        // 現在の速度を取得
        Vector3 v = rbody.linearVelocity;
        
        // Yだけを上書き
        v.y = jumpPower;
        
        // 速度を設定
        rbody.linearVelocity = v;
        
        // ジャンプしたので、doJump を false に戻す
        doJump = false;
    }
}
```

これでジャンプを確認しましょう。

#### 問題：多重ジャンプ

<table id="bkmrk-%E3%80%80%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%97%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%81%8C%E3%80%81%E7%A9%BA%E4%B8%AD%E3%81%A7%E3%81%82" style="border-collapse:collapse;width:100%;border-width:0px;"><colgroup><col style="width:69.3397%;"></col><col style="width:30.6206%;"></col></colgroup><tbody><tr><td style="border-width:0px;"> ジャンプできるようになったが、空中であってもジャンプができるので、ジャンプし続けば空に飛んでしまう！これを防ぐには、現状にジャンプできるかどうかの確認が必要。

 今回は「足元の下に何もなければ（つまり空中であること）、ジャンプができない」ことがジャンプの条件にしましょう。

 では、どうやって足元に地面の存在確認できるのか？これを学んだ「`Raycast`」で解決ができる。足元からビーム（レイ）を発射し、何も当たらなければ、空中であることを確認ができる。

</td><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/8b6image.png)

</td></tr></tbody></table>

では、`PlayerJump` を更新し、地面の時だけジャンプできるようにしましょう。まず、地面の確認メソッド：

```c#
// 現在、地面を触れている？
private bool IsOnFloor()
{
    // 確認する距離。ここは適切に調整
    const float CheckDistance = 0.2f;
    
    // レイが始まる位置。現在位置からもう少し上から始まる
    Vector3 origin = transform.position + Vector3.up * 0.01f;
    
    // origin から下に向かい、決まった距離のビームを発射
    // 何か当たったら「true」を返す
    return Physics.Raycast(origin, Vector3.down, CheckDistance);
}
```

そして、Update で：

```c#
void Update()
{
    // ジャンプしたか？
    if (IsOnFloor() && Input.GetButtonDown("Jump"))
        doJump = true;
}
```

#### 問題：壁にくっつける

次の試してみてください。台に向かって、ジャンプし、壁を当たってください。引き続きに壁に力を与えると、主人公が落下せず、壁にくっつけてしまう。魔雑のため、滑らずにそこでとまってしまう。

この問題を「Physics Material」で解決ができる。主人公のコライダーが魔雑ないようにし、滑りやすくしましょう。プロジェクトフォルダーの中に新しい「Physics Material」を作成し、名前は「PlayerSlide」にする

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/0l0image.png)

魔雑「０」弾み「０」で、どちらにも「最低数値」を使用。 Player のコライダーに「PlayerSlide」をアサインすれば完成

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/AULimage.png)

# アニメーションを追加

Timmy がずっと「T-ポース」でなかなか面白くないので、走るときとジャンプの時もアニメーションを再生しましょう。このため「アニメーションコントローラー」（Animation Controller）および「アニメーター」(Animator) コンポーネントを使用し、条件でアニメーションを切り替えて、主人公を適切に動かしましょう。

## シーン設定

動くのは Timmy なので、Timmy (Playerではなく) に Animator コンポーネントを追加してください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/32wimage.png)

ここで、「Controller」と「Avatar」を設定しないといけない。

#### Controller とは

Controller（アニメーションコントローラー）はアニメーションの遷移を管理するところである。以下のような画面でアニメーションを挿入し、「Transition」（遷移）で、アニメーションの切り替えが管理できるものである。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/cmQimage.png)

Timmy のコントローラーがまだできていないので、新しいのを作りましょう。Characters / Timmy / Animations の中で新しい「アニメーション ➡ アニメーションコントローラー」を作成し、「TimmyController」の名前にしてください：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/zlCimage.png)

これを「Animator」コントローラーの「Controller」に設定ください。

#### Avatar とは

Avatar は、3Dモデルの「<span class="HwtZe" lang="ja"><span class="jCAhz"><span class="ryNqvb">骸骨</span></span></span>」（<span class="HwtZe" lang="ja"><span class="jCAhz ChMk0b"><span class="ryNqvb">スケルトン</span></span></span>）である。アバターの「骨」（ボーン）を動かすことにより、モデルが正しく変形する。この情報3Dモデルのデザイナーさんが作るべき。開発者はモデルからデータを抽出するだけ。プロジェクトフォルダでTimmyのモデルを選択しましょう（Characters / Timmy / Models / Timmy）

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/fwIimage.png)

ここで「Rig」を確認しましょう。このモデルからアバターを作るので、いくつかの設定を変えないといけない。

- Animation Type：Generic から Humanoid に更新（一般 → 人間っぽい）
- Avatar Definition：No Avatar から Create From This Model （アバターなし → このモデルから作成）

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Ns6image.png)

その後は「Apply」ボタンを押してください。大きく変わらないが、ボーンを確認ができる。「Configure」ボタンを押すと

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/3ORimage.png)

アバターが抽出されたことが確認ができる。アバターができたので、Timmy の Animator コンポーネントの Avatar を設定しましょう

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/uIPimage.png)

#### アニメーションファイルのアバターも！

これで、キャラクターの骸骨（アバター）を設定できたが、次に、このモデルが使うアニメーションにもアバターの設定が必要。Characters / Timmy / Animation の中にいろんなファイルが入っているが、TimmyController 以外はすべてアニメーションである：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/ynkimage.png)

このアニメーションは「Timmyのアニメーションだよ！」を指定するために、このファイルに作ったばかりのアバターを指定しなければならない。すべてのアニメーションを選択し、また「Rig」を確認しましょう。ここでは

- Animation Type：Generic から Humanoid に更新（一般 → 人間っぽい）
- Avatar Definition：No Avatar から Copy From Other Avatar（アバターなし → 他のアバターからコピーする）
- Source：TimmyAvatar を選択し、同じアバターを利用

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/87rimage.png)

最後に、「Apply」を押してください。これで、アニメーションファイルと Timmy の3Dモデルの紐づけができる。

## アニメーション

### 待機／アイドル（Idle）

まず、キャラクターが何もしてないときのアイドルアニメーションを作成しましょう。コントローラーの中に「idle」のアニメーションをドラッグドロップで追加

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/bsTimage.png)

今回のアニメーションが Mixamo からダウンロードしたので、アニメーションの名前は「mixamo\_com」になってしまう。これでわかりにくいので、Inspector で「Idle」という名前にしましょう。これがデフォルトアニメーションになるので、このままで実行してみましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/Kvqimage.png)

「T-ポーズ」ではなく、待機アニメーションになった！ただし、少し長めに待つと、アニメーションが止まってしまいます。なぜなら、デフォルトとしてアニメーションがループしないから。プロジェクトで「idle」を選択し「Animation」の設定を Inspector で更新しましょう：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/821image.png)

ここで「Loop Time」のチェックが外しているため、アニメーションがループはしない。チェックを入れ、もう一度確認しましょう。

### 走る（Running）

つぎに、走るアニメーションを追加しましょう。プロジェクトから「running」をアニメーションコントローラーに追加し、アニメーション名を「Running」にしましょう。<span style="text-decoration:underline;">このアニメーションもループできるように設定を変えてくださいね</span>。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/GGcimage.png)

次、遷移（Transition）を作らないといけない。Idle → Running と Running → Idle それぞれの遷移を「右クリック」→「Make Transition」で作成しましょう：

[![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/l5fimage.png)](https://class.illogic.games/uploads/images/gallery/2026-05/l5fimage.png)

これで実行すると、待機→走る→待機 の繰り返しが続ける。走り始めると、Idle → Running の遷移し、止まったら Running → Idle に戻るようにしましょう。そのため、「今走っているのか？」のパラメータを追加しましょう。アニメーターコントローラーで Bool 型の IsRunning のパラメータを追加してください：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/iXYimage.png)

これが遷移するかしないかのパラメータにしましょう。Idle → Running で IsRunning は true だったら、遷移する。そして、Running → Idle には、 IsRunning が false の時遷移しましょう：

<table id="bkmrk--15" style="border-collapse:collapse;width:100%;border-width:0px;"><colgroup><col style="width:49.0857%;"></col><col style="width:50.9918%;"></col></colgroup><tbody><tr><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/4Yyimage.png)

</td><td style="border-width:0px;">[![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/BIqimage.png)](https://class.illogic.games/uploads/images/gallery/2026-05/BIqimage.png)

</td></tr></tbody></table>

これで実行し、アニメーションコントローラーのパラメータ IsRunning を変えながら、挙動を確認してみてください。

#### 問題：すぐ遷移しない

これで問題が明らかになった。 IsRunning を変えても、すぐ遷移しない。アニメーションが終わってから遷移するので、反応が遅い。これを直すには、「アニメーション終了まで待たず、すぐ遷移しろ！」の設定すれば良いでしょう。遷移での「Has Exit Time」（終了時間あり）のチェックを外してください。

### スクリプト

最後は、C#からアニメーションコントローラーのパラメターを設定し、走っている間には IsRunning を true にしましょう。PlayerMove スクリプト移動の処理しているので、ここで簡単にパラメターを設定ができる。まず、Animator の参照が必要で：

```c#
// プレーヤーの移動を処理する
public class PlayerMove : MonoBehaviour
{
    // Unity で Animator の参照を設定
    [SerializeField]
    private Animator animator;

    // （省略）
}
```

そして、Update を少し更新するだけ：

```c#
private void Update()
{
    // 何も入力がない前提で、速度をゼロにする
    velocity = Vector3.zero;

    // Heightオブジェクトの向きを従って移動
    velocity += cameraYRot.right * Input.GetAxis("Horizontal");
    velocity += cameraYRot.forward * Input.GetAxis("Vertical");
    
    // 入力あれば、速度ベクトルはゼロではないはず
    bool isRunning = velocity != Vector3.zero;
    if (isRunning)
    {
        // 単位ベクトルにする
        velocity.Normalize();
        
        // 前方の向きを変える
        transform.forward = velocity;
        
        // 速度を乗算する
        velocity *= speed;
    }

    // アニメーターのパラメターを設定
    animator.SetBool("IsRunning", isRunning);
}
```

Unity で PlayerMove の Animator 参照を設定し、走ってみましょう！

### ジャンプ（Jump）

ジャンプは走るよりも多少複雑である。ジャンプの高さにより、アニメションの長さが変わるので、３つのパーツに分割されている

- ジャンプ開始（jumping up）：ジャンプが始まるときに再生
- 空中（falling idle）：高いところからジャンプすると、ずっとこのアニメーションを再生する
- 着陸：２種類がある 
    - 移動中の着陸：着地→転がって→立つ（falling to roll)
    - 停止中の着陸：着地→立つ（hard landing）

これで遷移が難しくなるが、少し考えて遷移を整理しましょう。**<span style="color:rgb(186,55,42);">赤は遷移のパラメター条件（Condition）</span>**

- 待機、または走行からジャンプキーを押すと、jumping upを再生（トリガー使用） 
    - Idle → **<span style="color:rgb(186,55,42);">\[JumpTrigger\]</span>** → JumpUp
    - Running →<span style="color:rgb(186,55,42);"> **\[JumpTrigger\]**</span> → JumpUp
- 現状が関係なく、空中だったら falling idle を再生 
    - Any State → **<span style="color:rgb(186,55,42);">\[IsAir == true\]</span>** → Falling
- 空中ではなくったら、かつ、走っていれば、転がる 
    - Falling → **<span style="color:rgb(186,55,42);">\[isAir == false\] \[isRunning == true\] </span>**→ Roll → Running
- 空中ではなくったら、かつ、止まっていれば、hard landing を再生 
    - Falling → **<span style="color:rgb(186,55,42);">\[isAir == false\] \[isRunning == false\] </span>**→ Land → Idle

まず、遷移図を作りましょう。アニメションを追加し、わかりやすい名前にしてください：

*※falling idle は ループするので、アニメションの設定を忘れずに*

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/tiCimage.png)

#### スクリプト

遷移とその条件ができたら、次、スクリプトでパラメターの設定が必要。ジャンプに関する処理が「PlayerJump」の中にあるので、そのスクリプトを編集しましょう。まず、アニメーターの参照から：

```c#
using UnityEngine;

// ジャンプ機能を追加するスクリプト
public class PlayerJump : MonoBehaviour
{
    // Unity で Animator の参照を設定
    [SerializeField]
    private Animator animator;

    // (省略)
}
```

そして、Update で：

```c#
void Update()
{
    // 着地なのか？
    bool onFloor = IsOnFloor();
    
    // ジャンプしたか？
    if (onFloor && Input.GetButtonDown("Jump"))
    {
        animator.SetTrigger("JumpTrigger");
        doJump = true;
    }
    
    // アニメーターのパラメター設定
    animator.SetBool("IsAir", !onFloor);
}
```

### 遷移の改善、微調整

これで各アニメションの再選、遷移の実現ができたが、違和感のときもある。転がるのが長いし、立つアニメションも長いので、着地のすぐ後にまた走り始めると、変に見える場合もある。以下の微調整で少し見かけが改善できる。

#### 転がるアニメーションを速くする

転がるのが長いので、スピードアップしましょう。アニメーターコントローラーで「Roll」のアニメーションを選択し、Inspector で Speed を調整してみましょう。1.5倍～2.5倍の間にお好みの数値で調整し、確かめてください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/fUsimage.png)

#### 着陸したと、また移動中かどうかを確認

以下のように遷移を調整し、Land か Roll の再生中に、走行の状況が変わったら、すぐ適切なアニメーションへ遷移する。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/jDZimage.png)

#### ジャンプポーズでフリーズする

<table id="bkmrk-%E3%80%80%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%97%E4%B8%AD%E3%81%AB%E3%80%81%E9%80%A3%E7%B6%9A%E3%81%AB%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%97%E3%81%99%E3%82%8C%E3%81%B0%E3%80%81j" style="border-collapse:collapse;width:100%;border-width:0px;"><colgroup><col style="width:50%;"></col><col style="width:50%;"></col></colgroup><tbody><tr><td style="border-width:0px;">![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/rrvimage.png)</td><td style="border-width:0px;"> ジャンプ中に、連続にジャンプすれば、JumpTrigger また発動し、結果として、JumpUp のアニメーションへ遷移し、そこで止まってしまいます。

 ジャンプができない場合は、JumpTrigger のパラメータをリセットすれば、この問題を解決ができる。

 以下のように、PlayerJump のスクリプトを修正すれば、直る：

</td></tr></tbody></table>

```c#
void Update()
{
    // 着地なのか？
    bool onFloor = IsOnFloor();

    // ジャンプしたか？
    if (onFloor && Input.GetButtonDown("Jump"))
    {
        // アニメータの"JumpTrigger"パラメータを設定
        animator.SetTrigger("JumpTrigger");
        doJump = true;
    }
    // そうじゃない場合は、リセットする
    else
        animator.ResetTrigger("JumpTrigger");

    
    // アニメーターのパラメター設定
    animator.SetBool("IsAir", !onFloor);
}

```

# アイテムを拾う

3D シーンの配置できるコインと鍵を拾って、Timmy が持っているアイテムを追跡しましょう。

## コイン

シーンにコインを一つ作ろう（後ほどプレハブにして、何個も配置できるようにする）。まず、「Coin」の名前で空のオブジェクトを作成する。拾う処理を確認しやしくするため、Timmyの近くに配置してください。

次、「Coin」の子オブジェクトとして、3Dモデルを追加しましょう。Meshes の中にある「coin-gold」を「Coin」の中に挿入し、位置は 0, 0, 0 にしてください。

最後、3Dモデルとの接触を確認できるため、SphereCollider 型の トリガーを追加してください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/vR9image.png)

<span style="color:rgb(186,55,42);">**※注目：3Dモデル coin-gold の位置は「親に対して 0, 0, 0」であることを確認してください。3D空間の中で動かすのは、親の Coin である！**</span>

### アニメーション

コインが浮くアニメーションを作成しましょう。coin-gold を選択し、アニメーションパネルを表示してください。ここで２つの値をアニメーションで更新しましょう：位置と回転

<table class="align-center" id="bkmrk-%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0-0-60-120-%E4%BD%8D%E7%BD%AE-0%2C-" style="border-collapse:collapse;width:58.2143%;border-width:1px;"><colgroup><col style="width:20.6826%;"></col><col style="width:23.7531%;"></col><col style="width:26.0079%;"></col><col style="width:29.4881%;"></col></colgroup><tbody><tr><td>フレーム</td><td>0</td><td>60</td><td>120</td></tr><tr><td>位置</td><td>0, 1, 0</td><td>0, 0.5, 0</td><td>0, 1, 0</td></tr><tr><td>回転</td><td>0, 0, 0</td><td>0, 180, 0</td><td>0, 360, 0</td></tr></tbody></table>

### タグ

何を拾ったのかを確認したいので、SphereCollider が付いている coin-gold には「Coin」というタグを付けてください

### プレハブ化

最後、親の「Coin」を新しく作った「Prefabs」フォルダーの中に入れ、プレハブ化にしてください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-05/scaled-1680-/wckimage.png)

## インベントリ

つぎ、主人公のインベントリ（アイテム管理クラス）を作成しましょう。このクラスは `OnTriggerEnter` を活用し、タグを確認する。「Coin」だったら、持っているコインの数を増やす。Debug.Log で確認しましょう：

### スクリプト

```c#
using UnityEngine;

// アイテムを管理するインベントリ
public class Inventory : MonoBehaviour
{
    // コインの数
    private int coinCount;

    // トリガーが発動した
    private void OnTriggerEnter(Collider other)
    {
        // コインなの？
        if (other.CompareTag("Coin"))
        {
            coinCount++;
            Debug.Log($"コインの数：{coinCount}");
        }
    }
}
```

このスクリプトをPlayerにアタッチし、実行してみてください。

### アイテム削除

一応、これで正しく数えられることが確認できるが、コインが消えないので、何度も拾うことができてしまう。拾った後に、アイテムをシーンから廃止しましょう。ここの注目ポイントは、**<span style="color:rgb(186,55,42);">削除するのはコインの「親」</span>**であること（コライダーは子オブジェクトにあるから）

```c#
// トリガーが発動した
private void OnTriggerEnter(Collider other)
{
    // コインなの？
    if (other.CompareTag("Coin"))
    {
        // コライダー「の」トランスフォーム「の」親「の」ゲームオブジェクト
        Destroy(other.transform.parent.gameObject);

        coinCount++;        
        Debug.Log($"コインの数：{coinCount}");
    }
}
```

いくつかのコインを配置し、確認してみてください。

## 鍵

コインと同じ手順を従い、鍵のプレハブを作成してください。ただし：

- 親オブジェクトの名前：Key
- タグ：Key

にしてください。そして、`Inventory` クラスは：

```c#
using UnityEngine;

// アイテムを管理するインベントリ
public class Inventory : MonoBehaviour
{
    // コインの数
    private int coinCount;

    // 鍵の数
    private int keyCount;

    // トリガーが発動した
    private void OnTriggerEnter(Collider other)
    {
        // コインなの？
        if (other.CompareTag("Coin"))
        {
            // コライダー「の」トランスフォーム「の」親「の」ゲームオブジェクト
            Destroy(other.transform.parent.gameObject);
            coinCount++;
            Debug.Log($"コインの数：{coinCount}");
        }
        
        // 鍵なの
        else if (other.CompareTag("Key"))
        {
            Destroy(other.transform.parent.gameObject);
            keyCount++;
            Debug.Log($"鍵の数：{keyCount}");
        }
    }
}
```

## インベントリの改善：統一処理を一体化

この処理で、すぐ眼立つのは、似たような処理２回現れる

- コインの場合：アイテム削除→アイテムを数える→デバッグ用の表示
- 鍵の場合：アイテム削除→アイテムを数える→デバッグ用の表示

この作り方は大きな問題２つがある：

- 新しいアイテムを増やすのは大変：何度も同じ処理を追加しないといけない
- 処理を直すのも手間がかかる：何か直したい場合、１か所ではなく、アイテム１個ずつ直さないといけない。

できる限りに、統一している処理を一体にすると、ゲームの整備が楽になる！このため、新しい C# のクラス「Dictionary」を紹介

### なんでも指数として使える"配列"「Dictionary」

今まで、２つのコレクションを学んできた：

- 配列：決まった数の要素を入れることができる
- リスト：サイズを決めず、自由に伸びる配列

いずれにしても、それぞれの要素を「指数」でアクセスする形になる。先頭の要素を「0番目」の要素であり、そこから連続に次々の要素をアクセスができる。

```c#
// 配列、またはリストは「指数」でアクセス
int [] numbers = new int[5];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
// など…
```

連続にデータを処理する場合は、配列、またはリストは最適であるが、今回のインベントリのように、連続で整理するよりも、何かの「キーワード」でアクセスしたい。具体的に、それぞれのアイテムの数を、そのアイテムの名前でアクセスしたい。

```c#
// これをやりたい…
items["Coin"] = 10; // コイン10個ある
items["Key"] = 0;   // 鍵がない
```

これがあれば、アイテムごとの変数（`coinCount`, `keyCount`）を別々で作成する必要がない。この特別なコレクションに新しい `"アイテム"`を追加すれば、処理が統一できるようになる。

このコレクションは「ディクショナリー（Dictionary）」といい、リストと同様に、変数を作るのは１つだけで、中には要素を自由に追加ができるが、違いとしては、０から連続にアクセスするではなく、何かの「キー」で要素を探す。

ここで「キー」にできるのは、ほとんどなんでもOK！

- 整数（int）
- 文字列（string）
- 他のオブジェクト（GameObject）
- なんでも良い（比較で等しいかどかを確認できれば）

#### 書式

ディクショナリーの宣言は、以下のようになる：

```c#
Dictionary<キーの型、要素の型> ディクショナリー名;
```

そして、新しいディクショナリーを作るには：

```c#
ディクショナリー名 = new Dictionary<キーの型、要素の型>();
```

例えば、文字列→整数のディクショナリーは

```c#
// 学生の点数
// キー：文字列
// 要素：整数
Dictionary<string, int> studentScore = new Dictionary<string, int>();

// 代入
studentScore["太郎"] = 75;
studentScore["大原"] = 91;

// 読み込む
int score = studentScore["太郎"];
Debug.Log($"太郎さんの点数は{score}");
```

または、列挙→文字列でもOKですし：

```c#
// 武器の列挙
public enum Weapon
{
    Sword,
    Pistol,
    Axe,
    Rock
}

// 各武器の日本語表示 
// キー：武器（列挙）
// 要素：文字列
Dictionary<Weapon, string> weaponName = Dictionary<Weapon, string>();

// 代入
weaponName[Weapon.Sword] = "剣";
weaponName[Weapon.Axe] = "斧";

// 読み込む
int name = weaponName[Weapon.Pistol];
```

### Inventory クラスの改善

では、Dictionary を使用し、処理を一体化しましょう：

```c#
using System.Collections.Generic;
using UnityEngine;

// アイテムを管理するインベントリ
public class Inventory : MonoBehaviour
{
    // アイテムの数（タグ → 数）
    private Dictionary<string, int> itemCount;

    private void Start()
    {
        // ディクショナリーを作成
        itemCount = new Dictionary<string, int>();
        
        // 有効なアイテムとその初期の数
        itemCount["Coin"] = 0;
        itemCount["Key"] = 0;
    }

    // トリガーが発動した
    private void OnTriggerEnter(Collider other)
    {
        // 相手のタグを求める
        string itemTag = other.tag;

        // そのタグあるかないかを確認
        // "ContainsKey" は、ディクショナリーの中に
        // 定めた「キー」があると、「true」を返す
        bool isItem = itemCount.ContainsKey(itemTag);

        // 有効なアイテじゃなければ、何もしない
        if (!isItem)
            return; // 終了

        // コライダー「の」トランスフォーム「の」親「の」ゲームオブジェクト
        Destroy(other.transform.parent.gameObject);
        itemCount[itemTag]++;
        Debug.Log($"{itemTag}の数：{itemCount[itemTag]}");
    }
}
```

# 宝箱を作成

このステップで、宝箱のアニメーションを作成し、開く。ただし、主人公が事前に鍵を持っていることを確認しなければならない。また、遷移のないアニメーションの切り替えも紹介する。

## 宝箱の準備

シーンに宝箱のモデルをシーンに追加しましょう。Coin と Key と同様に、空のゲームオブジェクトを作成し、3Dモデルを子オブジェクトとして追加した方がおすすめ。

サイズが多少小さいので3Dモデル（子オブジェクト）のサイズは 2倍ぐらい大きくしましょう。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/image.png)

なお、ちゃんとぶつかるように、宝箱はコライダーが必要。3Dモデルの子オブジェクトに BoxCollider を追加し、サイズを調整してください：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/CbRimage.png)

### アニメーション

もし、主人公が会議を持っていれば、宝箱が開くので、ふたが開くアニメーションが必要。アニメーションパネルを使用し、「OpenChest」作ってください：

<table class="align-center" id="bkmrk-%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0-0-30-lid-%E3%81%AE-%E5%9B%9E%E8%BB%A2-0" style="border-collapse:collapse;width:55.3571%;height:53px;border-width:1px;"><colgroup><col style="width:50.0397%;"></col><col style="width:25.0199%;"></col><col style="width:25.0199%;"></col></colgroup><tbody><tr><td class="align-center">フレーム</td><td class="align-center">0</td><td class="align-center">30</td></tr><tr><td class="align-center">lid の 回転</td><td class="align-center">0, 0, 0</td><td class="align-center">-120, 0, 0</td></tr></tbody></table>

なお、ループしないように、出来上がったアニメーションを Project フォルダーで探し、Loop Time のチェックを外してください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/uwEimage.png)

このまま実行すると、宝箱が勝手に開いてしまうので、アニメーションコントローラーの調整が必要。現状を見ると：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/TqRimage.png)

ゲーム開始のときに「OpenChest」が実行するので、勝手に開いていしまうことが確認できる。そうしないように、空のアニメーション「Empty」を作成しましょう。アニメーターパネルの中に右クリックし、Create State → Empty を選択してください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/gacimage.png)

この新しいアニメーションの名前を「Empty」にし、デフォルトアニメーションにしましょう。アニメーションの上に右クリックし、「Set as Layer Default State」を選択しましょう、これで、ゲーム開始の時、何もしない。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/6Yoimage.png)

今回のアニメーションは、ただの切り替えなので、遷移を使用しない。このため、パラメータを作成する必要なく、Transition も使わない。

## スクリプト

今回のスクリプトは：

- 何か衝突のとき、
- 衝突したゲームオブジェクトが Inventory があるのかを確認 → なければなにもしない
- Inventory の中に鍵の数を問い合わせる。
- 鍵なければ → なにもしない
- １つ以上あれば、鍵を消費し、宝箱が開く。
- 開いていれば、２度もチェックしないこと。

### 宝箱を開くスクリプト

では「OpenWithKey」のスクリプトを作成しましょう

```c#
// 鍵を持っていれば開く
public class OpenWithKey : MonoBehaviour
{
    // すでに開いている？
    private bool isOpen = false;
    
    // 衝突の時
    private void OnCollisionEnter(Collision other)
    {
        // 開いていれば、何も処理しない
        if (isOpen)
            return;
        
        // 衝突してきたゲームオブジェクトのInventoryを取得
        Inventory inventory = other.gameObject.GetComponent<Inventory>();

        // Inventoryのないオブジェクト（ゾンビなど）を衝突する可能性があるので
        // インベントリが存在することを確認
        if (inventory == null) // ない！
            return; // なにもしない  

        // 鍵の数を求める
        int count = inventory.GetCount("Key");
        
        // １つもない？ なにもしない
        if (count < 1)
            return;
        
        // 鍵を消費
        inventory.UseItem("Key", 1);
        
        // 遷移せず、直接にアニメーションを再生
        Animator anime = GetComponent<Animator>();
        anime.Play("OpenChest"); 
        
        // ２度も実行しないように、開いていることにする
        isOpen = true;
    }
}
```

#### 主なポイント①：コンポーネント存在の確認

```c#
// 衝突してきたゲームオブジェクトのInventoryを取得
Inventory inventory = other.gameObject.GetComponent<Inventory>();

// Inventoryのないオブジェクト（ゾンビなど）を衝突する可能性があるので
// インベントリが存在することを確認
if (inventory == null) // ない！
    return; // なにもしない  
```

宝箱と何が衝突するかわからない。もちろん、Inventoryコンポーネントを持っているプレーヤーの衝突を期待しているが、もしかしたら他のゲームオブジェクト（落下してきた岩など）も衝突する可能性がある。

そのために、「衝突してきたオブジェクトが Inventory のコンポーネントがあるか」のを確認している。そのコンポーネントがないと、`inventory` が `null` になるので、必ずプレーヤーであることを保証ができる。

#### 主なポイント②：遷移なしのアニメーション再生

```c#
// 遷移せず、直接にアニメーションを再生
Animator anime = GetComponent<Animator>();
anime.Play("OpenChest"); 
```

今まで、パラメータを使用し、遷移でアニメーションを切り替えが行ってきたが、直接に「このアニメーションを再生しろ」のもできる。今回の単純な切り替えで、これで十分。

#### 主なポイント③：在庫確認と消費

```c#
// 鍵の数を求める
int count = inventory.GetCount("Key");

// 鍵を消費
inventory.UseItem("Key", 1);
```

### Inventoryの編集

上記の２つのメソッドまだ作成していないので、実装しましょう。Inventory クラスを編集し、

- 在庫を返す `GetCount` のメソッドを作成。引数は確認したいアイテムのタグ名
- アイテムを消費する `UseItem` のメソッドを作成。引数は消費したいアイテムのタグ名とその数

```c#
// アイテムの数を返す
public int GetCount(string item)
{
    // アイテム存在しなければ、ゼロを返す
    if (!itemCount.ContainsKey(item))
        return 0;
    return itemCount[item];
}

// アイテムを消費する
public void UseItem(string item, int count)
{
    // 現在の数を確認
    int nowCount = GetCount(item);
    
    // 足りたら、消費する
    if (nowCount >= count)
        itemCount[item] -= count;
}
```

「OpenWithKey」を宝箱のコライダーにアタッチし、確認してみましょう。

# UIを作成

インベントリの中にあるアイテム（コイン、鍵）を画面上で表示し、主人公の体力も可視化しましょう。

## Canvas作成

ヒエラルキーに新しい Canvas を追加してください。

#### Canvas Scaler を紹介

Canvas を作成するときに、同時に「Canvas Scaler」のコンポーネントを作成される。このコンポーネントは想定する画面のサイズに合わせて、自動的にCanvasを拡大縮小する。

例えば、UIを開発したときに、想定していた画面サイズは FullHD (1920 x 1080) だった。ただ、プレーヤーは 4K（3840 x 2160）で遊ぶとしたら、画面がすごく小さくなってしまう。

この場合は Canvas Scaler を使用すれば、画面が大きくなっても、小さくなっても、いつも正しく表示される

<table id="bkmrk-canvas-scaler-%E7%84%A1%E3%81%97%EF%BC%9Aful" style="border-collapse:collapse;width:100%;"><colgroup><col style="width:99.881%;"></col></colgroup><tbody><tr><td>![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/63Limage.png)

Canvas Scaler 無し：FullHD から 4K に切り替えると、UIが小さくなってしまう

</td></tr><tr><td>[![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/H7Mimage.png)](https://class.illogic.games/uploads/images/gallery/2026-06/H7Mimage.png)

Canvas Scaler で想定サイズを入力すると、4K に切り替えても、UIがのサイズを保持する

</td></tr></tbody></table>

今回、上記の設定で進めましょう。

### コインの数

画面の右上にコインの数を表示しましょう。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/gtHimage.png)

Canvas の中で、以下のようにヒエラルキーを作ってください：

<table id="bkmrk-%C2%A0-%E3%81%93%E3%81%86%E3%81%AA%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%EF%BC%9A-%C2%A0-coin%EF%BC%9Aim" style="border-collapse:collapse;width:100%;border-width:0px;"><colgroup><col style="width:23.7092%;"></col><col style="width:76.2491%;"></col></colgroup><tbody><tr><td style="border-width:0px;">[![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/KRJimage.png)](https://class.illogic.games/uploads/images/gallery/2026-06/KRJimage.png)

こうなるように：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/z1simage.png)

</td><td style="border-width:0px;">- Coin：Image（画像）にし、背景は半透明の黒にする。この「部品」が画面の右上に表示されるので、アンカーポイントが右上にしてください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/o0pimage.png)

- Icon：Image（画像）にし、コインのアイコンにする

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/3bHimage.png)

- Text：TextMeshPro にし、000 ので始まるようにする

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/iFyimage.png)

</td></tr></tbody></table>

#### Coinの画像が付かない？

3Dゲームを作るときに、すべての画像が 3Dモデルで使う「Texture (テクスチャ)」と思われる。テクスチャが UI で使えないので、**スプライト**になるように設定しなければならない。

UIで使う画像を選択し、Inspector で設定を変えてください：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/OBhimage.png)

これで、「Sprite」（2Dゲームと UI）にし、スプライトモードは「Single」（単体）にする。

※：同じ画像にたくさんのスプライトがある場合はここは「Multiple」（多数）になる

### 鍵の数

同じ手順を従って、画面の右下に鍵の数が表示できるUIを作成してください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/0xnimage.png)

## スクリプト

UI管理するスクリプト「UIManager」を作成し、コインの数と鍵の数を更新するメソッドを実装しましょう。まず、必要なのは、テキストの参照なので：

```c#
// UIを管理する
public class UIManager : MonoBehaviour
{
    // コインのテキスト
    [SerializeField] 
    private TextMeshProUGUI coinText;
    
    // 鍵のテキスト
    [SerializeField] 
    private TextMeshProUGUI keyText;
}
```

そして、アイテムのタグで、数を設定できるメソッド：

```c#
// アイテム（コイン、鍵）の数を設定
public void SetCount(string item, int count)
{
    // タグで確認
    switch (item)
    {
        // コイン
        case "Coin":
            coinText.text = $"{count:000}";
            break;
        // 鍵
        case "Key":
            keyText.text = $"{count:000}";
            break;
    }
}
```

#### Inventoryの編集

そして、Inventory スクリプトを更新し、鍵、またはコインを拾ったら、UIを更新しましょう。まず、Unityで紐づけできるように：

```c#
// アイテムを管理するインベントリ
public class Inventory : MonoBehaviour
{
    // UIを更新するため
    [SerializeField] 
    private UIManager uiManager;
    
    //（省略）
}
```

そして、拾うときに：

```c#
// トリガーが発動した
private void OnTriggerEnter(Collider other)
{
    // 相手のタグを求める
    string itemTag = other.tag;

    // そのタグあるかないかを確認
    // "ContainsKey" は、ディクショナリーの中に
    // 定めた「キー」があると、「true」を返す
    bool isItem = itemCount.ContainsKey(itemTag);

    // 有効なアイテじゃなければ、何もしない
    if (!isItem)
        return; // 終了

    // コライダー「の」トランスフォーム「の」親「の」ゲームオブジェクト
    Destroy(other.transform.parent.gameObject);
    itemCount[itemTag]++;
    
    // UIを更新
    uiManager.SetCount(itemTag, itemCount[itemTag]);
}
```

もちろん、消費のときも…

```c#
// アイテムを消費する
public void UseItem(string item, int count)
{
    // 現在の数を確認
    int nowCount = GetCount(item);
    
    // 足りたら、消費する
    if (nowCount >= count)
        itemCount[item] -= count;
    
    // UIを更新
    uiManager.SetCount(item, itemCount[item]);
}
```

### アタッチ、紐づけ

次、Canvas に UIManager をアタッチし、各種のテキストを設定：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/Pqcimage.png)

そして、Inventory と UIManager の紐づけも

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/PATimage.png)

これでもう一度確認しましょう。

# ゾンビを実装

主人公を追いかける「ゾンビ」を実現しましょう！

## シーンの設定

### ゲームオブジェクトと3Dモデル

Timmyと同様に、ゾンビのゲームオブジェクトを作成しましょう。空のゲームオブジェクトを作成し、その下に子オブジェクトとしてZombieのモデルを追加する：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/yPcimage.png)

ただし、ゾンビは物理的に動かないので、コライダーとリジッドボディが要らない。ゾンビの動かした方については、下記で説明する。

#### アニメーション

今回は、アニメーションは１つしかなくて、ずっと歩くようにしましょう。ただし、Timmy と同様に「[アバター](https://class.illogic.games/books/b9228/page/8cb22#bkmrk-avatar-%E3%81%A8%E3%81%AF)」の設定が必要。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/o61image.png)

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/dFAimage.png)

そして、ゾンビのアニメーターコントローラーを作成し、「ZombieWalk」のアニメションをデフォルトにする

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/Tmgimage.png)

これで実行してみてください（ループを忘れずに～）

### 自動的に道を探す「ナビゲーション」

人間を制御するキャラクターが、当たり判定を確認するだけで、動きの実装ができる。人間が勝手に障害物などを避けて動く。一方、CPUキャラクターがとても馬鹿であり、障害物を避けたらすることができない。

この問題を解決するには、Unityが「Navigation」（ナビゲーション）を提供する。ナビゲーションとは、自動車のナビと同じように、目的地を設定し、最適なルートを探す処理である。

ナビゲーションは以下の主なコンポーネントを組み合わせて、作成する

- ナビゲーションメッシュ（NavMesh Surface）：ナビの「地図」歩けるところ、歩けないところを指定する
- エージェント（NavMesh Agent）：地図の上に動くもの。一般的に、CPUキャラクターである

その他にも、ナビゲーションの影響にあるコンポーネント（NavMesh Link、NavMesh Obstacle、など）もあるが、最低限でこの２つの組み合わせで十分。

#### ナビゲーションメッシュ（NavMesh Surface）

ナビの地図である。<span style="color:rgb(186,55,42);">**地図を作るのは、シーンを固定してから**</span>！シーンが変わったら（壁を増やした、障害物の位置を変えた）、地図も更新しないといけないので、ご注意ください。

まず、地図に使うオブジェクトをグループ化にしないといけないので、シーンを整理し、ゾンビと主人公の間にいくつかの壁を作ってみましょう。空ゲームオブジェクト「Stage」を作成し、中には床面、ランプそして新しい壁を追加してください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/6Ywimage.png)

ここで「ナビゲーションメッシュ」を作りましょう。「Stage」を選択し、「NavMesh Surface」を追加してください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/ywVimage.png)

ここで地図を作る時に必要なパラメータを設定できるが、最も重要なのは、何を基づいて地図を作るのか。これは「Object Collection」から設定ができる。今回は「Stageとその子オブジェクトを使用」にしたいので「Current Object Hierarchy」（現在オブジェクトのヒエラルキー）を選択しましょう。

そして、ナビゲーションメッシュを作成するには「Bake」（焼く）ボタンを押すだけ。このボタンを押すと、Stageとその子オブジェクトのすべてを処理し、歩ける「地図」を水色で表示される：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/7aDimage.png)

壁にあまり近づかないよう、周囲に「歩けない」領域が現れる。この地図が焼いているので、<span style="color:rgb(186,55,42);">**ステージが変わっても地図が変わらない！** </span>１つの壁を削除しても、「歩けない」領域が残る：

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/LCPimage.png)

ナビゲーションメッシュを焼いた後にシーンが変わったら、もう一度「Bake」してください。

#### エージェント（NavMesh Agent）

エージェントは、水色の「地図」（ナビゲーションメッシュ）上で歩くものである。目的地を設定すれば、現在地から最適な道を探し、自動的に動く。

今回のゲームで、ゾンビが動いてほしいので、エージェントにしましょう。ゾンビ（親オブジェクト）に「NavMesh Agent」を追加してください。

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/E0Oimage.png)

ここでエージェントのパラメータを調整ができる。主なパラメータは：

<table class="align-center" id="bkmrk-base-offset%EF%BC%88%E5%9F%BA%E7%A4%8E%E3%82%AA%E3%83%95%E3%82%BB%E3%83%83%E3%83%88%EF%BC%89" style="border-collapse:collapse;width:72.8571%;border-width:1px;"><colgroup><col style="width:41.8666%;"></col><col style="width:58.0789%;"></col></colgroup><tbody><tr><td class="align-left">Base Offset（基礎オフセット）</td><td class="align-left">足元からの「ずれ」</td></tr><tr><td class="align-left">Speed（速度）</td><td class="align-left">移動速度</td></tr><tr><td class="align-left">Angular Speed（回転速度）</td><td class="align-left">向きを変える速度</td></tr><tr><td class="align-left">Acceleration（加速度）</td><td class="align-left">加速と減速の速さ</td></tr><tr><td class="align-left">Stopping Distance（停止距離）</td><td class="align-left">目的地までこの距離になったら止まる</td></tr><tr><td class="align-left">Radius（半径）</td><td class="align-left">エージェントの「太さ」</td></tr><tr><td class="align-left">Height（高さ）</td><td class="align-left">エージェントの「身長」</td></tr></tbody></table>

とりあえず、ゾンビの速度を「1」にし、残りのパラメータをそのままにしましょう。

## スクリプト

### プレーヤーを追いかける

スクリプトは、プレーヤーの位置を求め、エージェントの目的地を設定するだけ。「PlayerChase」のスクリプトは以下の通りである：

```c#
using UnityEngine;
using UnityEngine.AI;

// プレーヤーを追いかける
public class PlayerChase : MonoBehaviour
{
    // 追いかけるプレーヤー
    private GameObject player;
    
    // エージェントの目的地を設定するため
    private NavMeshAgent agent;
    
    private void Start()
    {
        // 後ほどプレハブにするので、Unityで紐づけせず、
        // 実行中にプレーヤーを探す
        player = GameObject.Find("Player");

        // NavMeshAgentのコンポネント取得
        agent = GetComponent<NavMeshAgent>();
    }

    private void Update()
    {
        // 追いかける
        // SetDestination はエージェントの目的地を設定するメソッド。
        // 引数はその位置である（プレーヤーの位置）
        agent.SetDestination(player.transform.position);
    }
}

```

このスクリプトを Zombie にアッタッチし、実行してみてください。

### Timmyの体力

これで、追いかけるようになったが、ゾンビが主人公を捕まっても、なにもならない。Timmyの体力を実装しましょう。「HitPoints」のスクリプトは以下の通りである：

```c#
// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力
    private int hp = 3;
}
```

### 当たり判定とダメージ

ここでトリガーを活用ができる。もし、`HitPoints` がダメージの<span class="HwtZe" lang="ja"><span class="jCAhz ChMk0b"><span class="ryNqvb">起源（ゾンビ、罠、炎など）と重なったら、ダメージを受ける。どれぐらい受けるのかをその「ダメージの起源」（`DamageSource`）で指定ができる</span></span></span>

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/kcDimage.png)

まず、ダメージの起源を作成しましょう：

```c#
// ダメージの起源
public class DamageSource : MonoBehaviour
{
    // ダメージの量
    [SerializeField] 
    private int damage;
    
    // ダメージの量（読み込む専用）
    public int GetDamage()
    {
        return damage;
    }
}
```

そして、`HitPoints` スクリプトは：

```c#
// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力
    private int hp = 3;

    // トリガーに入ったら
    private void OnTriggerEnter(Collider other)
    {
        // ダメージの起源かどうかを確認
        DamageSource src = other.GetComponent<DamageSource>();
        if (src == null)
            return;
      
        // 体力を減らし、0 以下にならないように制限する
        hp = Mathf.Max(0, hp - src.GetDamage());
    }
}
```

### 確認

確認するには、ゾンビにダメージの起源を追加しなければならない。Zombieに新しい子オブジェクトを追加し、トリガーと `DamageSource` を追加してください

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/KPTimage.png)

そして、Player に `HitPoints` を追加し、`Debug.Log` で体力を表示してください。

## UIの更新（体力）

`Debug.Log`でカッコ悪いので、UIも作成しましょう。画面の左に、3つのハート画像をグループとして配置しましょう

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/nuXimage.png)

そして、`UIManager` を更新し、ハートの表示・非表示しましょう。

```c#
// UIを管理する
public class UIManager : MonoBehaviour
{
    // コインのテキスト
    [SerializeField] 
    private TextMeshProUGUI coinText;
    
    // 鍵のテキスト
    [SerializeField] 
    private TextMeshProUGUI keyText;

    // ハートの配列
    [SerializeField] 
    private GameObject [] hearts;

    // 表示したいハートの数
    public void SetHearts(int count)
    {
        for(int i = 0; i < hearts.Length; i++)
            hearts[i].SetActive(i < count);
    }

    // （省略）
}

```

`SetHearts` は[ブロック崩しゲームの「SetLives」](https://class.illogic.games/link/24#bkmrk-%E6%AE%8B%E6%A9%9F-1)と同じ処理であるが、書き方を短くしただけである。後は、`HitPoints` の体力が変わったら、UI を更新するにで、 `HitPoints` を修正しましょう：

```c#
using UnityEngine;

// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力を表示するため
    [SerializeField] 
    private UIManager uiManager;
    
    // 体力
    private int hp = 3;

    private void Start()
    {
        uiManager.SetHearts(hp);
    }

    // トリガーに入ったら
    private void OnTriggerEnter(Collider other)
    {
        // ダメージの起源かどうかを確認
        DamageSource src = other.GetComponent<DamageSource>();
        if (src == null)
            return;
      
        // 体力を減らし、0 以下にならないように制限する
        hp = Mathf.Max(0, hp - src.GetDamage());
        uiManager.SetHearts(hp);
    }
}
```

# 本番のシーンを作成

今まで、作業用のシーンでゲームに必要な各部品を開発してきた。これで、本番のステージを作れるので、まず、再利用するもを１つのグループにし、プレハブ化にしましょう

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/uxnimage.png)

共通のゲームオブジェクト（Player、Base（カメラ）、Canvas（UI）と EventSystem）をグループ化し、再利用できるプレハブにする。Zombieは別のプレハブとして保存。

つぎ、新し「Game」シーンを作成し、「Meshes/PlatformKit」から好きなパーツを選んで、ステージを作成してください。「Common」プレハブを挿入し、ゲームを遊びましょう～

適切にコライダー、ナビゲーションメッシュなどを作成してね～

![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/7u6image.png)

# 課題

Timmyの伝説に新たな機能を追加し、よりも面白くする！

例えば…

- HP回復アイテムを実装
- 罠を作成（消えるプラットフォーム→穴に落ちる）
- ドアを実装 
    - 定めたコインの数がないと開かない
    - また、レバーを引くと開く
- HPゼロのときに、倒れるアニメーションを再生し、ゲームオーバーする
- 宝箱を開くと、コイン10個が飛んで出てくる
- サウンドを追加（アイテムを拾うとき、宝箱を開くとき）
- 移動プラットフォームを実装
- ボールを投げ、ゾンビを倒す
- その他の好きな機能（自由）

#### ルール

- １つの機能を実装すると：合格（最低限） 
    - <span style="text-decoration:underline;"><span style="color:rgb(224,62,45);text-decoration:underline;">難しいことを頼んでいない！</span></span>
    - <span style="color:rgb(185,106,217);">自分のスキルレベル</span>に合わせて<span style="color:rgb(53,152,219);">適切な機能</span>を選択し、<span style="color:rgb(22,145,121);">実装</span>してください。
- ２つ以上の機能、または複雑な機能を実装すると：点数向上
- 基本として、授業の時間で実装するべきが、自宅で完成度を高めたいなら問題ない
- 一人でやるべき（お友達からコピーするのはNG） 
    - ネット、教科書、資料、今まで作ってきたプログラムを参照してもOK
    - <span style="text-decoration:underline;"><span style="color:rgb(224,62,45);text-decoration:underline;">ただし、AI（ChatGPT、Geminiなど）はNG</span></span>
- ネットで見つけたスクリプトのコピーぺーについて 
    - 当然、<span style="text-decoration:underline;"><span style="color:rgb(224,62,45);text-decoration:underline;">把握せずにコピーしないでください</span></span>
    - プログラムの動きを解析し、分かれば、使ってもOK
    - 説明してほしいなら、先生を呼んでください。
- それでも進まないなら、先生に聞いてもOK（ヒントを出す）

#### 提出

提出するのは、プロジェクトの<span style="text-decoration:underline;"><span style="color:rgb(224,62,45);text-decoration:underline;">以下のフォルダのみ</span></span>：

- Assets
- Packages
- ProjectSettings

また、「変更点.txt」を作成し、何を変えたのかを説明してください（速く見つけるため）

- 例：Timmyが玉を打ち、攻撃できるようになった（スクリプト：○○.cs）

この３つのフォルダと「変更点.txt」をZIPファイルに圧縮し、提出フォルダにコピーしてください。