ティミーの伝説 初めての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」にしてください。 改善:セルフィスティック 今の処理はとても単純で、カメラはプレーヤーの足元に行ってしまいました。もう少し賢い作戦を考えてみましょう。「三人称」といえば: セルフィスティックは良いでしょう!シーンにあるカメラの親子関係を変えて、同じ仕組みになるようにしましょう。具体的に: INSERT ILLUSTRATION OF Base->Height->Stick->Camera 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 が裏側で入力を通訳してくれる。 IMAGE: Draw keyboard, mouse moving a/d, stick sidways -> pass input manager -> c# is "Horizontal" カメラ操作の入力を作成 すでに「MouseX」と「MouseY」の入力を定義されているので、これを再利用しましょう。MouseX はマウスの横の動きなので、これを「CamRotY」にしましょう。同様に MouseY を「CamRotX」に書き換えましょう。 Image; Visualize how MouseX rotates around Y and mouseY rotates around X これで、後ほどコントローラーの対応を追加したいなら、同じ名前の新しい入力を追加し、適切なボタンやスティックを割り当てることが可能。 スクリプト 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 = transform.localRotation; // このまま使えないので、XYZの回転角度(オイラー角度)にする Vector3 angles = rot.eulerAngles; // Y軸だけを更新 angles.y += Input.GetAxis("CamRotY"); // 回転を更新(オイラー角度 → クオーターニオンに戻す) transform.localRotation = Quaternion.Euler(angles); } } 一応、回転するが、速度が多少遅く感じるので、回転速度を「度 / 秒」単位で調整できるようにしましょう:         マウスでカメラを回転 Timmyをカメラに相対的に移動する ジャンプを実装 アニメーションを追加 アイテムを拾う 宝箱を作成 UIを作成 ゾンビを実装 課題 Timmyの伝説に新たな機能を追加し、よりも面白くする! 例えば… HP回復アイテムを実装 罠を作成(消えるプラットフォーム→穴に落ちる) ドアを実装 定めたコインの数がないと開かない また、レバーを引くと開く HPゼロのときに、倒れるアニメーションを再生し、ゲームオーバーする 宝箱を開くと、コイン10個が飛んで出てくる 移動プラットフォームを実装 ボールを投げ、ゾンビを倒す その他の好きな機能(自由) ルール 1つの機能を実装すると:合格(最低限) 難しいことを頼んでいない! 自分のスキルレベル に合わせて 適切な機能 を選択し、 実装 してください。 2つ以上の機能、または複雑な機能を実装すると:点数向上 基本として、授業の時間で実装するべきが、自宅で完成度を高めたいなら問題ない 一人でやるべき(お友達からコピーするのはNG) ネット、教科書、資料、今まで作ってきたプログラムを参照してもOK ただし、AI(ChatGPT、Geminiなど)はNG ネットで見つけたスクリプトのコピーぺーについて 当然、 把握せずにコピーしないでください プログラムの動きを解析し、分かれば、使ってもOK 説明してほしいなら、先生を呼んでください。 それでも進まないなら、先生に聞いてもOK(ヒントを出す) 提出 提出するのは、プロジェクトの 以下のフォルダのみ : Assets Packages ProjectSettings また、「変更点.txt」を作成し、何を変えたのかを説明してください(速く見つけるため) 例:Timmyが玉を打ち、攻撃できるようになった(スクリプト:○○.cs) この3つのフォルダと「変更点.txt」をZIPファイルに圧縮し、提出フォルダにコピーしてください。