ティミーの伝説 初めての3Dゲームを作ってみましょう プロジェクト設定 まず、新規プロジェクトを作成しましょう。2Dゲームであるため「Universal 3D」を選んでください。プロジェクト名は「legend」にしましょう。 素材を追加 3Dモデル 主人公(Timmy)とゾンビのモデル( Mixamo ) キャラクターのアニメーション ステージ作成のパーツ( Kenney ) 画像、テクスチャ プロトタイプ用のテクスチャ UIのスプライト フォント( Google Fonts ) すべての素材をプロジェクトに追加しましょう。 プロトタイプ用のシーンを構築 本番のシーンを作る前、各部品を個別で開発しなければならない。キャラクターの移動やカメラの操作などを作業用のシーンで開発し、出来上がったら本番シーンに入れると快適に仕事ができる。 それでは、「Scenes」フォルダーを作成し「Prototype」の新しいシーンを追加してください。その後、床面になる「キューブ」を追加し、サイズは [50 x 0.1 x 50] で広い遊ぶ場所を準備しましょう ただし、このままだと、真っ白な床面で動きなどが把握できないので、単純なプロトタイプ用のテクスチャ(greybox_grey_grid)をドラッグドロップで追加しましょう。ただし、床面のサイズは50倍大きくなったので、テクスチャも50回をタイル化しましょう。床の「Material」(マテリアル)を選択し、「Tiling」を 50 x 50 にしてください。 また、坂を上るとジャンプのテストもやりたいので、ランプと箱を追加しましょう。ランプはキューブから作成し、斜めに倒せば良いでしょう: 最後、ただのキューブを作成し、ランプとつながるようにする: 素材を作成する前に、ゲームの操作、面白さ、楽しさを確かめらためのプロトタイプシーンは「 Grayboxing 」(グレーボクシング)という。ある程度コストを削減し、まずゲーム性を確かめるためのシンプルなレベルである。 Timmyの移動を実装 主人公のゲームオブジェクトを作成 主人公のキャラクターの移動を実装しましょう。まず、空のゲームオブジェクトを作成し、「Player」という名前を付けてください。このゲームオブジェクトはプレーヤーの役割にする。 3Dモデルを追加 Characters/Timmy/Models から Timmy のモデルを追加してください。Player の子オブジェクトとして追加しましょう: よく見ると、色が多少おかしい。これはノーマルマップ(※)の問題であるので、解決しましょう。Characters/Timmy/Textures の中にあるノーマルテクスチャ(青い画像)を選択し、ノーマルマップであることを指定し、最後に「Apply」を押してください:   (※)ノーマルマップについては、また「ゲームエンジンII」で勉強する予定 物理処理の準備 キャラクターを物理的に移動するべきなので、コライダーとリジッドボディが必要。Player に Rigidbody と CapsuleCollider を追加してください。なお、コライダーの形がある程度 3Dモデルの体に合わせましょう: スクリプト Inputクラスを使用し、キャラクターを動かしましょう。後で改善するべきですが、とりあえず、WASD のキーで前後左右を移動しましょう: AとDでX軸(赤い軸)で移動し、W とSでZ軸(青い軸)で移動する。なお、各入力は: Input.GetAxis("Horizontal") : 入力の「水平軸」(つまりAとD)の入力をの取得ができる Aの場合は -1 を返す Dの場合は1を返す 入力のない場合は0を返す Input.GetAxis("Vertical") : 入力の「垂直軸」(つまりWとS)の入力をの取得ができる Wの場合は1 を返す Dの場合は-1を返す 入力のない場合は0を返す また、移動速度をUnityで変えられるようにしましょう。この知識を活用し、移動のスクリプト「PlayerMove」を実装: // プレーヤーの移動を処理する public class PlayerMove : MonoBehaviour { // 速度をInspectorで設定 [SerializeField] private float speed = 5.0f; // 物理的に移動するので、リジッドボディが必要 private Rigidbody rbody; // 3D空間の速度ベクトル private Vector3 velocity; private void Start() { // 参照を取っておく rbody = GetComponent(); } 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() はあるベクトルを長さ1(単位ベクトル)にする。今回の処理は以下の図で解析してある: 上下、または左右 のみ で移動する場合は、 GetAxis が -1~1 の数値を返すので、移動速度「speed」を掛けると、問題なく、長さ5の速度ベクトルができる。一方、同時に入力すると(斜め移動) 掛け算の前に単位ベクトルにすると: 実行確認 このスクリプトを「Player」にアタッチし、実行してみてください。移動し、ランプを上り、ジャンプしてみてください。なお、カメラの処理まだ行っていないので、Game ビューと Scene ビューを同時に表示するのはおすすめ。 問題:倒れたり、回転したりする すべての動きをリジッドボディに任せているため、移動だけではなく、回転も自由になっている。今後、回転はスクリプトで実現するので、リジッドボディが回転ができないようにすれば良いでしょう。制約設定で回転を固定しましょう: 問題:ゆっくり落ちる 回転の問題を解決できたが、ランプから飛び出すと、ゆっくりに落ちる。なぜなら、Updateごとに 速度を上書きしている からです!重力による落下をリジッドボディに任せても、縦軸(Y軸)の速度をゼロにしているので、なかなか落ちない! 解決としては、Y軸の速度を保守し、XとZだけを上書きすれば良い。 FixedUpdate を以下のように直しましょう: private void FixedUpdate() { // 現在の速度を取得 Vector3 v = rbody.linearVelocity; // XとZ を上書き v.x = velocity.x; v.z = velocity.z; // 速度を設定 rbody.linearVelocity = v; } これで確認しましょう。 主人公を追いかけるカメラ Timmyの移動できたとしても、操作が確認しにくいので、プレーヤーを追いかけるカメラ( 三人称視点 )を作りましょう 単純なアプローチ 一番簡単なのは「カメラの位置を主人公の位置にしろ!」の仕組みである。これをすぐ試せるので、「CameraController」スクリプトを作成してみましょう: スクリプト // 主人公を追いかけるカメラ public class CameraController : MonoBehaviour { // 追いかけるオブジェクトのTransform [SerializeField] private Transform target; private void Update() { // 自分の位置をターゲットの位置にするだけ transform.position = target.position; } } このスクリプトをカメラにアタッチし、確認しましょう。なお、Unity でカメラの「target」を「Player」にしてください。 改善:セルフィスティック 今の処理はとても単純で、カメラはプレーヤーの足元に行ってしまいました。もう少し賢い作戦を考えてみましょう。「三人称」といえば: セルフィスティックは良いでしょう!シーンにあるカメラの親子関係を変えて、同じ仕組みになるようにしましょう。具体的に: Base(基礎)  :主人公を追いかける根。セルフィスティックなら、「人間が立っている場所」 Height(高さ) :地面からの高さ。だいたいTimmyの頭までにすれば良いでしょう。 Pole(ポール) :スティックのそのもの。上がるか下がることにより、撮影角度を変えられる Camera(カメラ):スティックの端にあるカメラ。適切な距離にし、Timmyが全身が見えるようにしましょう。 これはヒエラルキー(とその位置)はこうなる: なお、「CameraController」のスクリプトは、カメラではなく、実際に動く基礎(Base)アタッチすべき。Main Camera からスクリプトを削除するのは、右側にある「 ⋮ 」を押し、「Remove Component」(コンポーネントを削除)でできる。 では、「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"); を取れれば良いけど、この入力軸が定義されていない。 新しい入力を定義する 入力マネージャーの紹介 ※以下は「入力マネージャー(旧)」に対する話しである。2年生になったら「入力システム(新)」に切り替える予定。 "Horizontal" (水平入力)と "Vertical" (垂直入力)が魔法の言葉ではなく、プロジェクト設定として定義されている入力である。これをトップメニューの「Edit / Project Settings」(編集 / プロジェクト設定)の中にある「Input Manager」(入力マネージャー)から確認ができる。 ここで「Horizontal」という入力名は「Positive」(正の数)と「Negative」(負の数)があるので、‐1~1の間の「軸」であることを確認できる。つまり、Negative Buttonの「a」を押すと、 Input.GetAxis("Horizontal") が -1 を返す、Positive Button の「d」を押すと Input.GetAxis("Horizontal") が1を返す。この行動がすでに使用し、Timmyを動かしました。 その他には、Fire1~Fire3 や Jump の入力名がある。これはデフォルトとして定義されている入力であるが、変えたり、消したり、増やしたりすることが可能である。 「Jump」を確認してみましょう: この入力は「Positive」のみで、「Negative」がない。つまり、この入力は「軸」ではなく「ボタン」である。ボタンは「 Input.GetButtonXXX("入力名") 」で取得でき、「true」または「false」を返す。 GetMouseButtonXXX と同様に3パターンがある: GetButtonDown("入力名") : 押された瞬間に「true」を返す。 GetButton("入力名") :押し続けるときに「true」を返す。 GetButtonUp("入力名") :放した瞬間に「true」を返す。 デフォルト設定としては「Jump」という入力は「space」に設置されているので、スペースキーを押したら GetButtonDown("Jump") は true になる! もちろん、これらあくまで初期設定なので、キーを変えて良いし、名前も変えても大丈夫(例えば、「Kick」「Punch」「Attack」など) もうちょっと見ると、多くの入力は2回が現れる!もう一つの "Horizontal" を確認すると: あれ?「Positive」も「Negative」もない?なにこれ? これは、"Horizontal" に対するもう一つの入力処理である。Typeを詳しくみると、「Joystick」というキーワードが書かれている。 そう!これはコントローラーに対応する入力。A/Dを使えば、"Horizontal" が発動するし、コントローラーの「X Axis」を使うと、同じ "Horizontal" が発動する。 つまり、入力マネージャーが入力ハードウェアを抽象化してくれる機能である! キーワード&マウス、コントローラーを関係なく、C#で「Horizontal」を処理すれば十分。Unity が裏側で入力を通訳してくれる。 カメラ操作の入力を作成 すでに「MouseX」と「MouseY」の入力を定義されているので、これを再利用しましょう。MouseX はマウスの横の動きなので、これを「CamRotY」にしましょう。同様に MouseY を「CamRotX」に書き換えましょう。 これで、後ほどコントローラーの対応を追加したいなら、同じ名前の新しい入力を追加し、適切なボタンやスティックを割り当てることが可能。 スクリプト 360度回転 CameraController を更新しましょう。まず、Y軸まわり(360度回転)を実装するには先ほど定義した「CamRotY」の入力を活用し、Height を回転する。 // 主人公を追いかけるカメラ 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); } } 一応、回転するが、速度が多少遅く感じるので、回転速度を「度 / 秒」単位で調整できるようにしましょう: 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"); を使用し、実装しましょう: 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 )として宣言しても良い。 // 上下の回転 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 の内部的な処理であり、角度が0以下になると、360度に変わり、360は MaxAngle を超えるので、 Mathf.Clamp で 最大角度に飛んでしまう! 解決には、 localRotation の角度を使用せず、内部の変数を使えば良いでしょう: // 上下回転の角度 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軸に沿って動くので、カメラの向きが変わったら、おかしく感じる。 この問題を解決するには、世界に絶対的に動くのではなく、カメラの向きに合わせて相対的に移動すること。 この場合は、カメラ前方(青い矢印)が世界のZ軸と並行になっているので、「W」キーを押すと、正しく移動する 一方、Heightを回すと、カメラ前方の向きが変わり、「W」を押すと、世界に対して斜め(青い矢印の向き)に移動してほしい スクリプト PlayerMove を編集し、世界のXZではなく、Heightの回転を従って移動させましょう。これには、 Transform コンポネントの「forward」と「right」を使いましょう! 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 )は速度ベクトルと同じ。ただし、向きを変えるのは、 入力がある時だけ。 上記の Update() は以下のように変わる: 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でも実装できるが、「一つのスクリプト=1つの責任」の考えを従い、今回はジャンプ専用の「PlayerJump」スクリプトを作成しましょう。 このスクリプトは「Jump」という入力(プロジェクト設定→入力マネージャーを確認)が発動したら、一旦Y軸の速度を変えて、上がるようにしましょう。また、ジャンプ力をUnityで変えられるようにしましょう。 スクリプト using UnityEngine; // ジャンプ機能を追加するスクリプト public class PlayerJump : MonoBehaviour { // ジャンプ力を設定する [SerializeField] private float jumpPower = 5.0f; // 速度を変えるためのリジッドボディ private Rigidbody rbody; // ジャンプしろ! private bool doJump; void Start() { // コンポネント取得 rbody = GetComponent(); } 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; } } これでジャンプを確認しましょう。 問題:多重ジャンプ  ジャンプできるようになったが、空中であってもジャンプができるので、ジャンプし続けば空に飛んでしまう!これを防ぐには、現状にジャンプできるかどうかの確認が必要。    今回は「足元の下に何もなければ(つまり空中であること)、ジャンプができない」ことがジャンプの条件にしましょう。    では、どうやって足元に地面の存在確認できるのか?これを学んだ「 Raycast 」で解決ができる。足元からビーム(レイ)を発射し、何も当たらなければ、空中であることを確認ができる。 では、 PlayerJump を更新し、地面の時だけジャンプできるようにしましょう。まず、地面の確認メソッド: // 現在、地面を触れている? 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 で: void Update() { // ジャンプしたか? if (IsOnFloor() && Input.GetButtonDown("Jump")) doJump = true; } 問題:壁にくっつける 次の試してみてください。台に向かって、ジャンプし、壁を当たってください。引き続きに壁に力を与えると、主人公が落下せず、壁にくっつけてしまう。魔雑のため、滑らずにそこでとまってしまう。 この問題を「Physics Material」で解決ができる。主人公のコライダーが魔雑ないようにし、滑りやすくしましょう。プロジェクトフォルダーの中に新しい「Physics Material」を作成し、名前は「PlayerSlide」にする 魔雑「0」弾み「0」で、どちらにも「最低数値」を使用。 Player のコライダーに「PlayerSlide」をアサインすれば完成 アニメーションを追加 Timmy がずっと「T-ポース」でなかなか面白くないので、走るときとジャンプの時もアニメーションを再生しましょう。このため「アニメーションコントローラー」(Animation Controller)および「アニメーター」(Animator) コンポーネントを使用し、条件でアニメーションを切り替えて、主人公を適切に動かしましょう。 シーン設定 動くのは Timmy なので、Timmy (Playerではなく) に Animator コンポーネントを追加してください。 ここで、「Controller」と「Avatar」を設定しないといけない。 Controller とは Controller(アニメーションコントローラー)はアニメーションの遷移を管理するところである。以下のような画面でアニメーションを挿入し、「Transition」(遷移)で、アニメーションの切り替えが管理できるものである。 Timmy のコントローラーがまだできていないので、新しいのを作りましょう。Characters / Timmy / Animations の中で新しい「アニメーション ➡ アニメーションコントローラー」を作成し、「TimmyController」の名前にしてください: これを「Animator」コントローラーの「Controller」に設定ください。 Avatar とは Avatar は、3Dモデルの「 骸骨 」( スケルトン )である。アバターの「骨」(ボーン)を動かすことにより、モデルが正しく変形する。この情報3Dモデルのデザイナーさんが作るべき。開発者はモデルからデータを抽出するだけ。プロジェクトフォルダでTimmyのモデルを選択しましょう(Characters / Timmy / Models / Timmy) ここで「Rig」を確認しましょう。このモデルからアバターを作るので、いくつかの設定を変えないといけない。 Animation Type:Generic から Humanoid に更新(一般 → 人間っぽい) Avatar Definition:No Avatar から Create From This Model (アバターなし → このモデルから作成) その後は「Apply」ボタンを押してください。大きく変わらないが、ボーンを確認ができる。「Configure」ボタンを押すと アバターが抽出されたことが確認ができる。アバターができたので、Timmy の Animator コンポーネントの Avatar を設定しましょう  アニメーションファイルのアバターも! これで、キャラクターの骸骨(アバター)を設定できたが、次に、このモデルが使うアニメーションにもアバターの設定が必要。Characters / Timmy / Animation の中にいろんなファイルが入っているが、TimmyController 以外はすべてアニメーションである: このアニメーションは「Timmyのアニメーションだよ!」を指定するために、このファイルに作ったばかりのアバターを指定しなければならない。すべてのアニメーションを選択し、また「Rig」を確認しましょう。ここでは Animation Type:Generic から Humanoid に更新(一般 → 人間っぽい) Avatar Definition:No Avatar から Copy From Other Avatar(アバターなし → 他のアバターからコピーする) Source:TimmyAvatar を選択し、同じアバターを利用 最後に、「Apply」を押してください。これで、アニメーションファイルと Timmy の3Dモデルの紐づけができる。 アニメーション 待機/アイドル(Idle) まず、キャラクターが何もしてないときのアイドルアニメーションを作成しましょう。コントローラーの中に「idle」のアニメーションをドラッグドロップで追加 今回のアニメーションが Mixamo からダウンロードしたので、アニメーションの名前は「mixamo_com」になってしまう。これでわかりにくいので、Inspector で「Idle」という名前にしましょう。これがデフォルトアニメーションになるので、このままで実行してみましょう: 「T-ポーズ」ではなく、待機アニメーションになった!ただし、少し長めに待つと、アニメーションが止まってしまいます。なぜなら、デフォルトとしてアニメーションがループしないから。プロジェクトで「idle」を選択し「Animation」の設定を Inspector で更新しましょう: ここで「Loop Time」のチェックが外しているため、アニメーションがループはしない。チェックを入れ、もう一度確認しましょう。 走る(Running) つぎに、走るアニメーションを追加しましょう。プロジェクトから「running」をアニメーションコントローラーに追加し、アニメーション名を「Running」にしましょう。 このアニメーションもループできるように設定を変えてくださいね 。 次、遷移(Transition)を作らないといけない。Idle → Running と Running → Idle それぞれの遷移を「右クリック」→「Make Transition」で作成しましょう: これで実行すると、待機→走る→待機 の繰り返しが続ける。走り始めると、Idle → Running の遷移し、止まったら Running → Idle に戻るようにしましょう。そのため、「今走っているのか?」のパラメータを追加しましょう。アニメーターコントローラーで Bool 型の IsRunning のパラメータを追加してください: これが遷移するかしないかのパラメータにしましょう。Idle → Running で IsRunning は true だったら、遷移する。そして、Running → Idle には、 IsRunning が false の時遷移しましょう: これで実行し、アニメーションコントローラーのパラメータ IsRunning を変えながら、挙動を確認してみてください。 問題:すぐ遷移しない これで問題が明らかになった。 IsRunning を変えても、すぐ遷移しない。アニメーションが終わってから遷移するので、反応が遅い。これを直すには、「アニメーション終了まで待たず、すぐ遷移しろ!」の設定すれば良いでしょう。遷移での「Has Exit Time」(終了時間あり)のチェックを外してください。 スクリプト 最後は、C#からアニメーションコントローラーのパラメターを設定し、走っている間には IsRunning を true にしましょう。PlayerMove スクリプト移動の処理しているので、ここで簡単にパラメターを設定ができる。まず、Animator の参照が必要で: // プレーヤーの移動を処理する public class PlayerMove : MonoBehaviour { // Unity で Animator の参照を設定 [SerializeField] private Animator animator; // (省略) } そして、Update を少し更新するだけ: 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) ジャンプは走るよりも多少複雑である。ジャンプの高さにより、アニメションの長さが変わるので、3つのパーツに分割されている ジャンプ開始(jumping up):ジャンプが始まるときに再生 空中(falling idle):高いところからジャンプすると、ずっとこのアニメーションを再生する 着陸:2種類がある 移動中の着陸:着地→転がって→立つ(falling to roll) 停止中の着陸:着地→立つ(hard landing) これで遷移が難しくなるが、少し考えて遷移を整理しましょう。 赤は遷移のパラメター条件(Condition) 待機、または走行からジャンプキーを押すと、jumping upを再生(トリガー使用) Idle → [JumpTrigger] → JumpUp Running → [JumpTrigger] → JumpUp 現状が関係なく、空中だったら falling idle を再生 Any State →  [IsAir == true] → Falling 空中ではなくったら、かつ、走っていれば、転がる Falling → [isAir == false] [isRunning == true] → Roll → Running 空中ではなくったら、かつ、止まっていれば、hard landing を再生 Falling → [isAir == false] [isRunning == false] → Land → Idle まず、遷移図を作りましょう。アニメションを追加し、わかりやすい名前にしてください: ※falling idle は ループするので、アニメションの設定を忘れずに スクリプト 遷移とその条件ができたら、次、スクリプトでパラメターの設定が必要。ジャンプに関する処理が「PlayerJump」の中にあるので、そのスクリプトを編集しましょう。まず、アニメーターの参照から: using UnityEngine; // ジャンプ機能を追加するスクリプト public class PlayerJump : MonoBehaviour { // Unity で Animator の参照を設定 [SerializeField] private Animator animator; // (省略) } そして、Update で: 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倍の間にお好みの数値で調整し、確かめてください。 着陸したと、また移動中かどうかを確認 以下のように遷移を調整し、Land か Roll の再生中に、走行の状況が変わったら、すぐ適切なアニメーションへ遷移する。 ジャンプポーズでフリーズする  ジャンプ中に、連続にジャンプすれば、JumpTrigger また発動し、結果として、JumpUp のアニメーションへ遷移し、そこで止まってしまいます。    ジャンプができない場合は、JumpTrigger のパラメータをリセットすれば、この問題を解決ができる。    以下のように、PlayerJump のスクリプトを修正すれば、直る: 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 型の トリガーを追加してください ※注目:3Dモデル coin-gold の位置は「親に対して 0, 0, 0」であることを確認してください。3D空間の中で動かすのは、親の Coin である! アニメーション コインが浮くアニメーションを作成しましょう。coin-gold を選択し、アニメーションパネルを表示してください。ここで2つの値をアニメーションで更新しましょう:位置と回転 フレーム 0 60 120 位置 0, 1, 0 0, 0.5, 0 0, 1, 0 回転 0, 0, 0 0, 180, 0 0, 360, 0 タグ 何を拾ったのかを確認したいので、SphereCollider が付いている coin-gold には「Coin」というタグを付けてください プレハブ化 最後、親の「Coin」を新しく作った「Prefabs」フォルダーの中に入れ、プレハブ化にしてください インベントリ つぎ、主人公のインベントリ(アイテム管理クラス)を作成しましょう。このクラスは OnTriggerEnter を活用し、タグを確認する。「Coin」だったら、持っているコインの数を増やす。Debug.Log で確認しましょう: スクリプト using UnityEngine; // アイテムを管理するインベントリ public class Inventory : MonoBehaviour { // コインの数 private int coinCount; // トリガーが発動した private void OnTriggerEnter(Collider other) { // コインなの? if (other.CompareTag("Coin")) { coinCount++; Debug.Log($"コインの数:{coinCount}"); } } } このスクリプトをPlayerにアタッチし、実行してみてください。 アイテム削除 一応、これで正しく数えられることが確認できるが、コインが消えないので、何度も拾うことができてしまう。拾った後に、アイテムをシーンから廃止しましょう。ここの注目ポイントは、 削除するのはコインの「親」 であること(コライダーは子オブジェクトにあるから) // トリガーが発動した private void OnTriggerEnter(Collider other) { // コインなの? if (other.CompareTag("Coin")) { // コライダー「の」トランスフォーム「の」親「の」ゲームオブジェクト Destroy(other.transform.parent.gameObject); coinCount++; Debug.Log($"コインの数:{coinCount}"); } } いくつかのコインを配置し、確認してみてください。 鍵 コインと同じ手順を従い、鍵のプレハブを作成してください。ただし: 親オブジェクトの名前:Key タグ:Key にしてください。そして、 Inventory クラスは: 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}"); } } } インベントリの改善:統一処理を一体化 この処理で、すぐ眼立つのは、似たような処理2回現れる コインの場合:アイテム削除→アイテムを数える→デバッグ用の表示 鍵の場合:アイテム削除→アイテムを数える→デバッグ用の表示 この作り方は大きな問題2つがある: 新しいアイテムを増やすのは大変:何度も同じ処理を追加しないといけない 処理を直すのも手間がかかる:何か直したい場合、1か所ではなく、アイテム1個ずつ直さないといけない。 できる限りに、統一している処理を一体にすると、ゲームの整備が楽になる!このため、新しい C# のクラス「Dictionary」を紹介 なんでも指数として使える"配列"「Dictionary」 今まで、2つのコレクションを学んできた: 配列:決まった数の要素を入れることができる リスト:サイズを決めず、自由に伸びる配列 いずれにしても、それぞれの要素を「指数」でアクセスする形になる。先頭の要素を「0番目」の要素であり、そこから連続に次々の要素をアクセスができる。 // 配列、またはリストは「指数」でアクセス int [] numbers = new int[5]; numbers[0] = 10; numbers[1] = 20; numbers[2] = 30; // など… 連続にデータを処理する場合は、配列、またはリストは最適であるが、今回のインベントリのように、連続で整理するよりも、何かの「キーワード」でアクセスしたい。具体的に、それぞれのアイテムの数を、そのアイテムの名前でアクセスしたい。 // これをやりたい… items["Coin"] = 10; // コイン10個ある items["Key"] = 0; // 鍵がない これがあれば、アイテムごとの変数( coinCount , keyCount )を別々で作成する必要がない。この特別なコレクションに新しい "アイテム" を追加すれば、処理が統一できるようになる。 このコレクションは「ディクショナリー(Dictionary)」といい、リストと同様に、変数を作るのは1つだけで、中には要素を自由に追加ができるが、違いとしては、0から連続にアクセスするではなく、何かの「キー」で要素を探す。 ここで「キー」にできるのは、ほとんどなんでもOK! 整数(int) 文字列(string) 他のオブジェクト(GameObject) なんでも良い(比較で等しいかどかを確認できれば) 書式 ディクショナリーの宣言は、以下のようになる: Dictionary<キーの型、要素の型> ディクショナリー名; そして、新しいディクショナリーを作るには: ディクショナリー名 = new Dictionary<キーの型、要素の型>(); 例えば、文字列→整数のディクショナリーは // 学生の点数 // キー:文字列 // 要素:整数 Dictionary studentScore = new Dictionary(); // 代入 studentScore["太郎"] = 75; studentScore["大原"] = 91; // 読み込む int score = studentScore["太郎"]; Debug.Log($"太郎さんの点数は{score}"); または、列挙→文字列でもOKですし: // 武器の列挙 public enum Weapon { Sword, Pistol, Axe, Rock } // 各武器の日本語表示 // キー:武器(列挙) // 要素:文字列 Dictionary weaponName = Dictionary(); // 代入 weaponName[Weapon.Sword] = "剣"; weaponName[Weapon.Axe] = "斧"; // 読み込む int name = weaponName[Weapon.Pistol]; Inventory クラスの改善 では、Dictionary を使用し、処理を一体化しましょう: using System.Collections.Generic; using UnityEngine; // アイテムを管理するインベントリ public class Inventory : MonoBehaviour { // アイテムの数(タグ → 数) private Dictionary itemCount; private void Start() { // ディクショナリーを作成 itemCount = new Dictionary(); // 有効なアイテムとその初期の数 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倍ぐらい大きくしましょう。 なお、ちゃんとぶつかるように、宝箱はコライダーが必要。3Dモデルの子オブジェクトに BoxCollider を追加し、サイズを調整してください: アニメーション もし、主人公が会議を持っていれば、宝箱が開くので、ふたが開くアニメーションが必要。アニメーションパネルを使用し、「OpenChest」作ってください: フレーム 0 30 lid の 回転 0, 0, 0 -120, 0, 0 なお、ループしないように、出来上がったアニメーションを Project フォルダーで探し、Loop Time のチェックを外してください このまま実行すると、宝箱が勝手に開いてしまうので、アニメーションコントローラーの調整が必要。現状を見ると: ゲーム開始のときに「OpenChest」が実行するので、勝手に開いていしまうことが確認できる。そうしないように、空のアニメーション「Empty」を作成しましょう。アニメーターパネルの中に右クリックし、Create State → Empty を選択してください。 この新しいアニメーションの名前を「Empty」にし、デフォルトアニメーションにしましょう。アニメーションの上に右クリックし、「Set as Layer Default State」を選択しましょう、これで、ゲーム開始の時、何もしない。 今回のアニメーションは、ただの切り替えなので、遷移を使用しない。このため、パラメータを作成する必要なく、Transition も使わない。 スクリプト 今回のスクリプトは: 何か衝突のとき、 衝突したゲームオブジェクトが Inventory があるのかを確認 → なければなにもしない Inventory の中に鍵の数を問い合わせる。 鍵なければ → なにもしない 1つ以上あれば、鍵を消費し、宝箱が開く。 開いていれば、2度もチェックしないこと。 宝箱を開くスクリプト では「OpenWithKey」のスクリプトを作成しましょう // 鍵を持っていれば開く public class OpenWithKey : MonoBehaviour { // すでに開いている? private bool isOpen = false; // 衝突の時 private void OnCollisionEnter(Collision other) { // 開いていれば、何も処理しない if (isOpen) return; // 衝突してきたゲームオブジェクトのInventoryを取得 Inventory inventory = other.gameObject.GetComponent(); // Inventoryのないオブジェクト(ゾンビなど)を衝突する可能性があるので // インベントリが存在することを確認 if (inventory == null) // ない! return; // なにもしない // 鍵の数を求める int count = inventory.GetCount("Key"); // 1つもない? なにもしない if (count < 1) return; // 鍵を消費 inventory.UseItem("Key", 1); // 遷移せず、直接にアニメーションを再生 Animator anime = GetComponent(); anime.Play("OpenChest"); // 2度も実行しないように、開いていることにする isOpen = true; } } 主なポイント①:コンポーネント存在の確認 // 衝突してきたゲームオブジェクトのInventoryを取得 Inventory inventory = other.gameObject.GetComponent(); // Inventoryのないオブジェクト(ゾンビなど)を衝突する可能性があるので // インベントリが存在することを確認 if (inventory == null) // ない! return; // なにもしない 宝箱と何が衝突するかわからない。もちろん、Inventoryコンポーネントを持っているプレーヤーの衝突を期待しているが、もしかしたら他のゲームオブジェクト(落下してきた岩など)も衝突する可能性がある。 そのために、「衝突してきたオブジェクトが Inventory のコンポーネントがあるか」のを確認している。そのコンポーネントがないと、 inventory が null になるので、必ずプレーヤーであることを保証ができる。 主なポイント②:遷移なしのアニメーション再生 // 遷移せず、直接にアニメーションを再生 Animator anime = GetComponent(); anime.Play("OpenChest"); 今まで、パラメータを使用し、遷移でアニメーションを切り替えが行ってきたが、直接に「このアニメーションを再生しろ」のもできる。今回の単純な切り替えで、これで十分。 主なポイント③:在庫確認と消費 // 鍵の数を求める int count = inventory.GetCount("Key"); // 鍵を消費 inventory.UseItem("Key", 1); Inventoryの編集 上記の2つのメソッドまだ作成していないので、実装しましょう。Inventory クラスを編集し、 在庫を返す GetCount のメソッドを作成。引数は確認したいアイテムのタグ名 アイテムを消費する UseItem のメソッドを作成。引数は消費したいアイテムのタグ名とその数 // アイテムの数を返す 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 を使用すれば、画面が大きくなっても、小さくなっても、いつも正しく表示される Canvas Scaler 無し:FullHD から 4K に切り替えると、UIが小さくなってしまう Canvas Scaler で想定サイズを入力すると、4K に切り替えても、UIがのサイズを保持する 今回、上記の設定で進めましょう。 コインの数 画面の右上にコインの数を表示しましょう。 Canvas の中で、以下のようにヒエラルキーを作ってください:   こうなるように:   Coin:Image(画像)にし、背景は半透明の黒にする。この「部品」が画面の右上に表示されるので、アンカーポイントが右上にしてください。 Icon:Image(画像)にし、コインのアイコンにする   Text:TextMeshPro にし、000 ので始まるようにする Coinの画像が付かない? 3Dゲームを作るときに、すべての画像が 3Dモデルで使う「Texture (テクスチャ)」と思われる。テクスチャが UI で使えないので、 スプライト になるように設定しなければならない。 UIで使う画像を選択し、Inspector で設定を変えてください: これで、「Sprite」(2Dゲームと UI)にし、スプライトモードは「Single」(単体)にする。 ※:同じ画像にたくさんのスプライトがある場合はここは「Multiple」(多数)になる 鍵の数 同じ手順を従って、画面の右下に鍵の数が表示できるUIを作成してください スクリプト UI管理するスクリプト「UIManager」を作成し、コインの数と鍵の数を更新するメソッドを実装しましょう。まず、必要なのは、テキストの参照なので: // UIを管理する public class UIManager : MonoBehaviour { // コインのテキスト [SerializeField] private TextMeshProUGUI coinText; // 鍵のテキスト [SerializeField] private TextMeshProUGUI keyText; } そして、アイテムのタグで、数を設定できるメソッド: // アイテム(コイン、鍵)の数を設定 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で紐づけできるように: // アイテムを管理するインベントリ public class Inventory : MonoBehaviour { // UIを更新するため [SerializeField] private UIManager uiManager; //(省略) } そして、拾うときに: // トリガーが発動した 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]); } もちろん、消費のときも… // アイテムを消費する 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 をアタッチし、各種のテキストを設定: そして、Inventory と UIManager の紐づけも これでもう一度確認しましょう。 ゾンビを実装 主人公を追いかける「ゾンビ」を実現しましょう! シーンの設定 ゲームオブジェクトと3Dモデル Timmyと同様に、ゾンビのゲームオブジェクトを作成しましょう。空のゲームオブジェクトを作成し、その下に子オブジェクトとしてZombieのモデルを追加する: ただし、ゾンビは物理的に動かないので、コライダーとリジッドボディが要らない。ゾンビの動かした方については、下記で説明する。 アニメーション 今回は、アニメーションは1つしかなくて、ずっと歩くようにしましょう。ただし、Timmy と同様に「 アバター 」の設定が必要。 そして、ゾンビのアニメーターコントローラーを作成し、「ZombieWalk」のアニメションをデフォルトにする これで実行してみてください(ループを忘れずに~) 自動的に道を探す「ナビゲーション」 人間を制御するキャラクターが、当たり判定を確認するだけで、動きの実装ができる。人間が勝手に障害物などを避けて動く。一方、CPUキャラクターがとても馬鹿であり、障害物を避けたらすることができない。 この問題を解決するには、Unityが「Navigation」(ナビゲーション)を提供する。ナビゲーションとは、自動車のナビと同じように、目的地を設定し、最適なルートを探す処理である。 ナビゲーションは以下の主なコンポーネントを組み合わせて、作成する ナビゲーションメッシュ(NavMesh Surface):ナビの「地図」歩けるところ、歩けないところを指定する エージェント(NavMesh Agent):地図の上に動くもの。一般的に、CPUキャラクターである その他にも、ナビゲーションの影響にあるコンポーネント(NavMesh Link、NavMesh Obstacle、など)もあるが、最低限でこの2つの組み合わせで十分。 ナビゲーションメッシュ(NavMesh Surface) ナビの地図である。 地図を作るのは、シーンを固定してから !シーンが変わったら(壁を増やした、障害物の位置を変えた)、地図も更新しないといけないので、ご注意ください。 まず、地図に使うオブジェクトをグループ化にしないといけないので、シーンを整理し、ゾンビと主人公の間にいくつかの壁を作ってみましょう。空ゲームオブジェクト「Stage」を作成し、中には床面、ランプそして新しい壁を追加してください。 ここで「ナビゲーションメッシュ」を作りましょう。「Stage」を選択し、「NavMesh Surface」を追加してください ここで地図を作る時に必要なパラメータを設定できるが、最も重要なのは、何を基づいて地図を作るのか。これは「Object Collection」から設定ができる。今回は「Stageとその子オブジェクトを使用」にしたいので「Current Object Hierarchy」(現在オブジェクトのヒエラルキー)を選択しましょう。 そして、ナビゲーションメッシュを作成するには「Bake」(焼く)ボタンを押すだけ。このボタンを押すと、Stageとその子オブジェクトのすべてを処理し、歩ける「地図」を水色で表示される: 壁にあまり近づかないよう、周囲に「歩けない」領域が現れる。この地図が焼いているので、 ステージが変わっても地図が変わらない!  1つの壁を削除しても、「歩けない」領域が残る: ナビゲーションメッシュを焼いた後にシーンが変わったら、もう一度「Bake」してください。 エージェント(NavMesh Agent) エージェントは、水色の「地図」(ナビゲーションメッシュ)上で歩くものである。目的地を設定すれば、現在地から最適な道を探し、自動的に動く。 今回のゲームで、ゾンビが動いてほしいので、エージェントにしましょう。ゾンビ(親オブジェクト)に「NavMesh Agent」を追加してください。 ここでエージェントのパラメータを調整ができる。主なパラメータは: Base Offset(基礎オフセット) 足元からの「ずれ」 Speed(速度) 移動速度 Angular Speed(回転速度) 向きを変える速度 Acceleration(加速度) 加速と減速の速さ Stopping Distance(停止距離) 目的地までこの距離になったら止まる Radius(半径) エージェントの「太さ」 Height(高さ) エージェントの「身長」 とりあえず、ゾンビの速度を「1」にし、残りのパラメータをそのままにしましょう。 スクリプト プレーヤーを追いかける スクリプトは、プレーヤーの位置を求め、エージェントの目的地を設定するだけ。「PlayerChase」のスクリプトは以下の通りである: 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(); } private void Update() { // 追いかける // SetDestination はエージェントの目的地を設定するメソッド。 // 引数はその位置である(プレーヤーの位置) agent.SetDestination(player.transform.position); } } このスクリプトを Zombie にアッタッチし、実行してみてください。 Timmyの体力 これで、追いかけるようになったが、ゾンビが主人公を捕まっても、なにもならない。Timmyの体力を実装しましょう。「HitPoints」のスクリプトは以下の通りである: // 体力を管理するクラス public class HitPoints : MonoBehaviour { // 体力 private int hp = 3; } 当たり判定とダメージ ここでトリガーを活用ができる。もし、 HitPoints がダメージの 起源(ゾンビ、罠、炎など)と重なったら、ダメージを受ける。どれぐらい受けるのかをその「ダメージの起源」( DamageSource )で指定ができる まず、ダメージの起源を作成しましょう: // ダメージの起源 public class DamageSource : MonoBehaviour { // ダメージの量 [SerializeField] private int damage; // ダメージの量(読み込む専用) public int GetDamage() { return damage; } } そして、 HitPoints スクリプトは: // 体力を管理するクラス public class HitPoints : MonoBehaviour { // 体力 private int hp = 3; // トリガーに入ったら private void OnTriggerEnter(Collider other) { // ダメージの起源かどうかを確認 DamageSource src = other.GetComponent(); if (src == null) return; // 体力を減らし、0 以下にならないように制限する hp = Mathf.Max(0, hp - src.GetDamage()); } } 確認 確認するには、ゾンビにダメージの起源を追加しなければならない。Zombieに新しい子オブジェクトを追加し、トリガーと DamageSource を追加してください そして、Player に HitPoints を追加し、 Debug.Log で体力を表示してください。 UIの更新(体力) Debug.Log でカッコ悪いので、UIも作成しましょう。画面の左に、3つのハート画像をグループとして配置しましょう そして、 UIManager を更新し、ハートの表示・非表示しましょう。 // 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」 と同じ処理であるが、書き方を短くしただけである。後は、 HitPoints の体力が変わったら、UI を更新するにで、 HitPoints を修正しましょう: 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(); if (src == null) return; // 体力を減らし、0 以下にならないように制限する hp = Mathf.Max(0, hp - src.GetDamage()); uiManager.SetHearts(hp); } } 本番のシーンを作成 今まで、作業用のシーンでゲームに必要な各部品を開発してきた。これで、本番のステージを作れるので、まず、再利用するもを1つのグループにし、プレハブ化にしましょう 共通のゲームオブジェクト(Player、Base(カメラ)、Canvas(UI)と EventSystem)をグループ化し、再利用できるプレハブにする。Zombieは別のプレハブとして保存。 つぎ、新し「Game」シーンを作成し、「Meshes/PlatformKit」から好きなパーツを選んで、ステージを作成してください。「Common」プレハブを挿入し、ゲームを遊びましょう~ 適切にコライダー、ナビゲーションメッシュなどを作成してね~ 課題 Timmyの伝説に新たな機能を追加し、よりも面白くする! 例えば… HP回復アイテムを実装 罠を作成(消えるプラットフォーム→穴に落ちる) ドアを実装 定めたコインの数がないと開かない また、レバーを引くと開く HPゼロのときに、倒れるアニメーションを再生し、ゲームオーバーする 宝箱を開くと、コイン10個が飛んで出てくる サウンドを追加(アイテムを拾うとき、宝箱を開くとき) 移動プラットフォームを実装 ボールを投げ、ゾンビを倒す その他の好きな機能(自由) ルール 1つの機能を実装すると:合格(最低限) 難しいことを頼んでいない! 自分のスキルレベル に合わせて 適切な機能 を選択し、 実装 してください。 2つ以上の機能、または複雑な機能を実装すると:点数向上 基本として、授業の時間で実装するべきが、自宅で完成度を高めたいなら問題ない 一人でやるべき(お友達からコピーするのはNG) ネット、教科書、資料、今まで作ってきたプログラムを参照してもOK ただし、AI(ChatGPT、Geminiなど)はNG ネットで見つけたスクリプトのコピーぺーについて 当然、 把握せずにコピーしないでください プログラムの動きを解析し、分かれば、使ってもOK 説明してほしいなら、先生を呼んでください。 それでも進まないなら、先生に聞いてもOK(ヒントを出す) 提出 提出するのは、プロジェクトの 以下のフォルダのみ : Assets Packages ProjectSettings また、「変更点.txt」を作成し、何を変えたのかを説明してください(速く見つけるため) 例:Timmyが玉を打ち、攻撃できるようになった(スクリプト:○○.cs) この3つのフォルダと「変更点.txt」をZIPファイルに圧縮し、提出フォルダにコピーしてください。