top of page

サイト内検索

用語検索や評価テストのお供にどうぞ!

空の検索で57件の結果が見つかりました。

  • ホーム | Unity1gc2

    へようこそ! 【更新履歴】 ゲームエンジン「Unity 」を用いたゲーム制作の教材サイトです。ゲーム自体は簡素ですが、Unityの基本的な操作を中心とした内容になっています。 スマートフォンからでも閲覧できますが、表示が崩れる可能性があります(PC推奨) 随時更新中! ​上部のメニューまたは下部のボタンからどうぞ! [教材で使用しているUnityのバージョン 2021.3.5f1 / 2022.3.4f1] 3Dアクションゲーム編 まずはここから! 2Dランゲーム編 慣れてきた方に 3D脱出ゲーム編 2Dシューティングゲーム編 2Dオンラインゲーム編 ランクアップ! LessonEX 自作ゲームへの道 リンクQ&A サイト内検索 このサイトについて

  • EX EventTrigger | Unity1gc2

    LessonEX EventTrigger EX-1 ボタンの説明を表示 Buttonコンポーネントを使えばボタンへのクリック検知はできますが、それだけでは自由度が足りない時もあります。EventTriggerを使ってマウスオーバー(オブジェクトにマウスが重なっている状態)を検知してみましょう。 ​ ​ ​ まずはUIから適当なButtonとボタンの説明用のText(またはTextMeshPro)を追加してください。説明文の内容は適当で構いません。 ButtonにAdd Componentから「Event Trigger」を追加してください。 新しいスクリプトTestButtonを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestButton : MonoBehaviour { // ボタンの説明文 public GameObject TestMessageObject; // マウスが重なった瞬間に呼ばれる public void TestEnter() { // ボタンの説明文を表示する TestMessageObject.SetActive(true); } // マウスが離れた瞬間に呼ばれる public void TestExit() { // ボタンの説明文を非表示にする TestMessageObject.SetActive(false); } // ボタンが押された瞬間に呼ばれる public void TestPush() { // ボタンが押された Debug.Log("ボタンが押された!"); } } ボタンにマウスが重なった時、離れた時、クリックされた時の3つの関数を用意しています。どの関数も外部から呼べるようにpublicにしてください。​ スクリプトが書けたらボタンにアタッチしておきましょう 。 インスペクターにマウスオーバー時に表示するオブジェクトを指定する項目が表示されるので、先ほど作成したTextオブジェクトをドラッグ&ドロップしてください。 EventTriggerの「Add New Event Type」をクリックすると、関数が呼ばれる条件を選択することができます。 様々な項目がありますが、よく使うと思われるのは以下の項目です。 PointerEnter :マウスカーソルが重なった瞬間 PointerExit :マウスカーソルが離れた瞬間 PointerDown :自身がクリックされた瞬間 PointerUp :自身がクリックされた状態から離れた瞬間 PointerClick :自身がクリックされて同一オブジェクト上で離された瞬間 Drag :オブジェクト上でマウスが押されたまま移動している間 Drop :ドラッグされた他のオブジェクトが自身の上でドラッグ解除された時 Scroll :自身の上でマウスホイールが操作された時 ​ ​ ​ EventTriggerは選択した条件で好きな関数を呼ぶことができます。​まずはPointerEnterを選択しましょう。 PointerEnterでは「自身にマウスカーソルが重なった時」に実行する関数を指定できます。新しい項目を追加して、先ほど作成したTestEnter関数を設定してください。 同じようにPointerExitも設定します。呼び出す関数はTestExitにしておきましょう。 Buttonコンポーネントではボタンが押された時に呼ばれる関数を設定できます。​ ここではTestPush関数を呼ぶように設定してください。操作の流れはEventTriggerと変わりません。 最初から説明文が見えては困るので、説明文のオブジェクトを非アクティブにしておいてください。 ここまでできたら実行して、マウスカーソルが重なっている間だけ説明文が表示され、クリックするとコンソールにログが出力されることを確認してみてください。 実行する関数に引数を設定することで、インスペクターから引数を指定することもできます。 ​ TestButtonスクリプトを赤い部分のコード に変更してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestButton : MonoBehaviour { // ボタンの説明文 //public GameObject TestMessageObject; // マウスが重なった瞬間に呼ばれる public void TestEnter(GameObject TestMessageObject ) { // ボタンの説明文を表示する TestMessageObject.SetActive(true); } // マウスが離れた瞬間に呼ばれる public void TestExit(GameObject TestMessageObject ) { // ボタンの説明文を非表示にする TestMessageObject.SetActive(false); } // ボタンが押された瞬間に呼ばれる public void TestPush() { // ボタンが押された Debug.Log("ボタンが押された!"); } } この変更を行うと、関数を変更したことでボタンのEventTriggerの参照が外れます。もう一度設定してください。 再設定すると先ほどとは異なり引数を設定する枠が表示されます。それぞれに説明文のオブジェクトをドラッグ&ドロップしてください。 設定できたら実行して、先ほどと同じ挙動をすることを確認してみてください。 ​ コンポーネントに変数を持たせるか引数を使うかは状況に応じて使い分けましょう。 EX-2 Spriteの判定 UIであれば上記の方法でマウスオーバーを判定できますが、ModelやSpriteの場合は少し手順を挟みます。 ​ まずは適当なSpriteを追加してください。プロジェクト内のスプライトをシーンにドラッグ&ドロップすると、自動でスプライトオブジェクトが作成されます。 UIと異なる点として、対象にコライダーがアタッチされていないと動作しないという点があります。 SpriteにColliderを設定してください。Spriteにアタッチするものは2DがついているColliderでないと正常に動作しないので注意しましょう。 ​(サンプルではCircle Collider 2Dをアタッチしています。BoxやCapsuleでも構いません) Main CameraにPhysics 2D Raycasterを追加してください(3Dオブジェクトを検知したい場合はPhysics Raycasterをアタッチ) Raycasterのパラメータは特に変更する必要はありません。 次はEventSystemを追加してください。 通常、UIを作成した際に自動で追加されるものです。​すでにEventSystemが存在する場合は追加しなくて構いません。 後はボタンと同じようにEventTriggerとスクリプトをアタッチして設定するだけです。 ​3Dモデルも同じようにマウスオーバー判定を取ることができるので、覚えておきましょう。 EX-3 マウスで移動させる ここからは使用例として、EventTriggerを使ったオブジェクトをドラッグ&ドロップで移動させる処理を実装してみましょう。 ​ EX-2の設定を行った状態で、以下のコードを入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestButton : MonoBehaviour { // ドラッグ中に呼ばれる関数 public void DragTest() { Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); mousePos.z = 0.0f; // Z座標を調整する transform.position = mousePos; } } EventTriggerをリセットして、ドラッグ中にDragTestが実行されるように設定しましょう。 これでスプライトをドラッグで移動させられるようになりました。実行して確認してみてください。 ​ ​ この移動方法は移動中に障害物があってもそのまま貫通してしまいます。次は当たり判定を持ったまま移動させてみましょう。 ​ まずはAdd ComponentからスプライトにRigidBody2Dを追加してください。 ​ ​ 後で移動させる時に備えてRigidBody2Dのパラメータを設定しておきます。 LinearDrag は移動の減衰率です。オブジェクト同士がぶつかった際に大きく飛ばないように大きな値にしています(もっと大きい値でもOK) GravityScale は重力の大きさです。LinearDragを大きくすると重力の影響まで小さくなってしまうので、大きな値にしています。 Collision Detection は移動の計算方式を設定できます。初期値はDiscreteですが、Continuousに変更することで少し正確な判定を取ることができます。ただしDiscreteより処理が重いので扱いには注意が必要です。 このままでは重力でスプライトが落下してしまうので、地面を作成しましょう。インスペクターから「2D Object」→「Sprites」→「Square」を選択してください。 2Dは最初からコライダーがついていないので、コライダーも追加しましょう。 スクリプトは以下のように変更します。移動開始時、移動終了時、Updateの3つの関数を用意しています。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestButton : MonoBehaviour { public Rigidbody2D RB2D; bool IsMove = false; // 移動開始 public void MoveStart() { IsMove = true; RB2D.gravityScale = 0.0f; // 移動中は重力を無視する RB2D.freezeRotation = true; // 移動中は回転しないようにする } // 移動終了 public void MoveEnd() { IsMove = false; RB2D.gravityScale = 10.0f; RB2D.freezeRotation = false; RB2D.velocity = Vector2.zero; // 移動速度をリセットする RB2D.angularVelocity = 0.0f; // 回転速度をリセットする } private void Update() { // 移動中なら移動処理 if (IsMove) { Vector3 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition); pos.z = 0.0f; RB2D.MovePosition(pos); } } } 【プログラムの解説】 ​・Rigidbodyのパラメータ gravityScale は重力の強さ、freezeRotation は回転するかどうかを決めることができます。自分がクリックされた瞬間に重力と回転を止め、離された瞬間に元に戻すことで移動中はオブジェクトが静止するようになります。 ​ ・​Camera.main.ScreenToWorldPoint(Input.mousePosition) で、マウスのスクリーン座標をワールド座標に変換できます。 例えばクリックされた場所にオブジェクトを生成したい時などにも使えます。2Dゲームを作るときは特によく使うので覚えておきましょう。 ​ ・Rigidbodyの関数 MovePosition は引数の座標へ移動させる関数ですが、当たり判定の影響を受けます。これによって他のオブジェクトを貫通して移動できないようにしています。 ​ ​ 最後にインスペクターから設定を行います。RigidBody2Dをpublicにしているので自身にアタッチされているRigidBody2Dをドラッグ&ドロップしてください。 EventTriggerを設定しましょう。 ​ PointerDown(自分の上でクリックされた瞬間)でMoveStart関数を呼びます。 PointerUp(クリックが離された瞬間)でMoveEnd関数を呼びます。 ここまで設定できたら実行して、スプライトをドラッグで移動させてみてください。床を貫通して移動しないか確認してみましょう。 スプライトを何個も複製するとMovePosition関数の挙動がわかりやすいかと思います。 EventTriggerはゲームのシステムとしても、UIのクオリティを上げるためにも活用できるコンポーネントなのでぜひ組み込んでみましょう。 【サンプルで使用している画像】 Rド 様

  • EX Raycast | Unity1gc2

    LessonEX Raycast EX-1 Raycastとは UnityにはRaycast という機能があり、Ray(光線)を発射してそれがヒットしたオブジェクトの情報を取得することができます。 2Dランゲーム編 や3D脱出ゲーム編 でも使用しています。 ​ ​ まずは適当な3Dプロジェクトを開いて、レイの基点となるオブジェクトとその他のオブジェクトをいくつか配置しましょう。 新しいスクリプトRayTestを作成して、レイの基点とするオブジェクトにアタッチしましょう。 RayTestスクリプトを開いて、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class RayTest : MonoBehaviour { void Start() { RaycastHit hit; // コライダーと衝突したらtrueを返す if (Physics.Raycast(transform.position, Vector3.right, out hit)) { // 衝突したオブジェクトを削除する Destroy(hit.collider.gameObject); } } } レイがヒットしたオブジェクトの情報は、RaycastHit型の 変数に格納されます。 ​ Physics.Raycast関数はレイを発射して衝突したオブジェクトの情報を取得します。 ​ 第一引数:レイの基点となる座標(発射地点) 第二引数:レイの方向 第三引数:情報の格納先(RaycastHit型) ​ 第三引数にはoutという少々見慣れない修飾子がついています。outがないと情報の格納先であるhitが初期化されていないためにエラーが発生してしまいます。その時にout修飾子をつけることで「この関数内で初期化するから無視して大丈夫だよ」と教えている訳です(よくわからない場合はとりあえずoutをつけなきゃいけないんだな~くらいの認識でOKです) 第三引数に格納されるのは最初にレイがヒットしたオブジェクトの情報のみです。なのでこの状態で実行するとオブジェクトは1つしか消えません。 第四引数ではレイの長さ(MaxDistance)を設定できます。指定しなかった場合、レイの長さは無限大になります。 試しにレイの長さを極端に短くして、レイが他のオブジェクトにヒットしなくなることを確認してみてください。 第四引数に小さい値を入れるとレイが短くなり、オブジェクトが消えなくなるはずです。 ​ ​ レイが衝突しては困るオブジェクトもあると思います。そういったオブジェクトはLayerをIgnore Raycastに設定しましょう。このレイヤーに設定したオブジェクトはレイが衝突しなくなります。 ​(第四引数を削除して、レイの長さを無限大に戻しておいてください) 第四引数に小さい値を入れるとレイが短くなり、オブジェクトが消えなくなるはずです。 ​ ​ レイが衝突しては困るオブジェクトもあると思います。そういったオブジェクトはLayerをIgnore Raycastに設定しましょう。このレイヤーに設定したオブジェクトはレイが衝突しなくなります。 ​(第四引数を削除してレイの長さを無限大に戻しておいてください) レイがどのように発射されているか確認するために、 シーン内にレイを表示するようにしてみましょう。 RayTestスクリプトに赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class RayTest : MonoBehaviour { void Start() { RaycastHit hit; // コライダーと衝突したらtrueを返す if (Physics.Raycast(transform.position, Vector3.right,out hit)) { // 衝突したオブジェクトを削除する Destroy(hit.collider.gameObject); } // レイを描画 Debug.DrawRay(transform.position, Vector3.right * 100.0f, Color.red, 10.0f); } } Debug.DrawRay関数はレイを描画するためのデバッグ用関数です。Physics.Raycast関数とは引数の構成が違うので気をつけてください。 ​ 第一引数:レイの基点となる座標 第二引数:レイが伸びるベクトル(レイの方向と長さ) 第三引数:レイの色 ​ 第四引数:レイが表示されている時間(今回は10,0fを指定しているので10秒経つと消える) ​ ​​ この状態で実行すると、シーン内にレイが描画されるようになります。 Physics.Raycast関数では最初にレイがヒットしたオブジェクトの情報しか取得できませんが、RaycastAll関数 を使うとレイがヒットした全てのオブジェクトの情報を取得できます。 ​ 今までのRaycast処理はコメントアウトして、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class RayTest : MonoBehaviour { void Start() { //RaycastHit hit; //// コライダーと衝突したらtrueを返す //if (Physics.Raycast(transform.position, Vector3.right,out hit)) //{ // // 衝突したオブジェクトを削除する // Destroy(hit.collider.gameObject); //} //// レイを描画 //Debug.DrawRay(transform.position, Vector3.right * 100.0f, Color.red, 10.0f); // レイが衝突した全てのオブジェクトを削除 RaycastHit[] hits; hits = Physics.RaycastAll(transform.position, Vector3.right); foreach(RaycastHit hit in hits) { Destroy(hit.collider.gameObject); } } } RayCastAll関数で返ってくる配列は必ずしも距離が近い順という訳ではない ので注意してください。 ​ 配列のループはforeach文ではなくfor文でもOKです。 この状態で実行すると、レイがヒットした全てのオブジェクトが消えるようになります。 今まではRay(光線)の名の通り1本の線を発射して判定を取っていましたが​、線だけでなく球体や箱型の判定を取ることもできます。今回は球体を発射するSphereCast関数 を使ってみましょう。 ​ ​ RayTestスクリプトに以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class RayTest : MonoBehaviour { void Start() { // 球の線形判定 RaycastHit hit; if (Physics.SphereCast(transform.position, 1.0f, transform.right, out hit, 5.0f)) { // 衝突したオブジェクトを削除する Destroy(hit.collider.gameObject); } } void OnDrawGizmos() { // 判定を描画 Gizmos.DrawWireSphere(transform.position + Vector3.right * 5.0f, 1.0f); } } OnDrawGizmos関数内にギズモ(シーン内でのみ見える)を描画するコードを書いています。わかりやすくするために表示するだけなので、実際に制作で使う場合は書かなくても構いません。 ​ ​ 基本の使い方はRaycastと変わりません。ただし第二引数は球体の半径になります。 ​ この状態で実行してみると、球体が衝突したオブジェクトが削除されます。 レイと違って球体なので、真横から少しずれた位置にオブジェクトがあっても判定されるようになります。 箱を発射するBoxCast関数や、カプセルを発射するCapsuleCast関数も同じように使えます。行いたい処理によって使い分けましょう。 EX-2 Rayを用いて自動生成 Raycastの使用例を一つ紹介します。Raycastを使うことで起伏のある地面であっても接地した状態でオブジェクトを生成することができます。 ​ 今まで使っていたオブジェクトを削除​して、地面となるCubeを作成してください。カメラも地面全体が見えるように調整してください。 Cubeを組み合わせて起伏のある地面を作ってみてください。 エネミー役のオブジェクトを作成してプレハブ化してください。Cubeでも構いませんし、適当なモデルを使用してもOKです。 空オブジェクトGameSystemを作成してください。このオブジェクトに敵を生成してもらいます。 新しいスクリプトEnemyMakerを作成して、GameSystemにアタッチしておいてください。 ​ EnemyMakerスクリプトを開いて、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyMaker : MonoBehaviour { // 生成するオブジェクト public GameObject Enemy; // 生成する数 public int EnemyNum = 10; // 生成する範囲(半径) public float Radius = 5.0f; // レイの基点となる高さ public float RayOriginY = 10.0f; void Start() { // 敵を生成 for(int i = 0; i < EnemyNum; i++) { // レイの基点を決める Vector3 rayOrigin = Random.insideUnitSphere * Radius; rayOrigin.y = RayOriginY; // レイを発射して衝突した場所を求める RaycastHit hit; if (Physics.Raycast(rayOrigin, -Vector3.up, out hit)) { // レイの衝突点にエネミーを生成 Instantiate(Enemy, hit.point, Quaternion.identity); } else { // 地面と衝突しなかったらやり直す i--; } } } } 【プログラムの解説】 ・Random.insideUnitSphereは半径1の球体の中のランダムな1点の座標を返します。 今回は返ってきた座標のY座標だけを高くしています。 そしてそれを基点として真下にレイを飛ばし、その衝突点をエネミーの生成ポイントとしています。 インスペクター内のEnemyにエネミー役のプレハブをドラッグ&ドロップしておいてください。 実行すると地面に接地した状態でエネミーが自動生成されます。パラメータを調整してエネミーの生成数や範囲を変更してみてください。 今回は単純にレイが衝突した場所に生成していますが、SphereCastなどを使うことで「エネミーを作ろうとしている場所の近くに既にエネミーがいたら生成をスキップする」といった処理もできます。レイが衝突した地面の情報を取得して「地面の種類によって生成する敵を変更する」処理を行うのも良いでしょう。 ​ ​ ​ ​ 他にもRaycastを活用することで様々なゲームを作ることができます。 ​ 例えばエネミーの前方にレイを飛ばして「壁に衝突したら方向転換する」処理や「目の前に足場がなかったら追跡をやめる」処理を実装して賢い敵を作ることも可能です。 他にも「壁に隠れているオブジェクトは検知しない」といった視野の処理を実装することもできます。敵からプレイヤーに向けてレイを飛ばして、障害物にぶつかったら発見処理をしない、という流れで実装できます。 Raycastを使うことで実装できる処理の幅が広がるため、ぜひ活用してみてください。

  • LessonEX | Unity1gc2

    LessonEX 制作に役立つ情報 制作に役立つ短いレッスンの詰め合わせです。ぜひ自分のゲームに取り込んでみてください。 ​ 【注意点】 ・それぞれのレッスンに繋がりはありません。 気になる内容のものだけ見てください。 ​・基礎知識 以外は最低限3Dアクションゲーム編の内容が終わっている前提の内容です。 ・特に理由がない場合2Dで制作していますが、3Dゲームでも基本的に同じです。 ・LessonEXに追加してほしい内容があればアンケート へお願いします。 例:「プレイヤーをジャンプさせたい」「HPバーを作りたい」 汎用的でない案や説明が長くなりすぎる案は追加しない場合があります。正直ググった方が早いよ。 ​ ​ 追加予定表はこちらです →→→→→→→→→→→→→→ ​ 「これを早く実装してほしい!」というものがあったら ​ アンケートへお願いします。 EX追加予定表 実装関連 ゲームの機能として実装できる内容 ポーズ画面 物理マテリアル モンスター図鑑 EventTrigger HD-2Dもどき 剣を振る/ボールを投げる Raycast 複数のカメラを扱う パッケージ関連 制作を助けるUnityの便利機能 Unityでモデリング 最適化関連 ゲームを軽量化する知識 ​特にモバイル向けゲームでは重要 軽量化テクニック集 その他 上記の分類に入らない内容 ​あまり種類はないです よくあるエラー 基礎知識 小ネタ まとめテスト置き場 マニアック 筆者が趣味で作ったゲームのシステム ​難しめな上に汎用性は低いかも (ほぼ筆者の備忘録) ラインを引いて囲む ステップアップ エクストラ的な内容 ​Unityをより極めたい人向け エディタ拡張 シェーダーグラフ

  • EX エディタ拡張 | Unity1gc2

    LessonEX エディタ拡張 EX-1 エディタ拡張とは Unityは便利なエディタですが、作業の効率化のために機能をプログラマー側で拡張したいということもあります。エディタ拡張 を行うことでプログラマー以外の職種の人もデータを編集しやすいようにしたり、ケアレスミスを防げるようにしたりすることができます。 Unityにはオリジナルのウィンドウを作成するなどエディタ拡張向けの機能があるため、ここではいくつかの例を通してエディタ拡張を行ってみましょう。 ​今までのスクリプトとは書き方が異なるため、C#とUnityに慣れてきた人向けの内容です。 ​ ちなみに小ネタ で解説しているAttributeも分類的にはエディタ拡張の一種になります。こちらは初心者でも使いやすいため、積極的に使っていきましょう。 ​ EX-2 エディタ拡張の準備 エディタ拡張を行う際は事前にEditorフォルダを作っておきましょう 。エディタ拡張をするためのファイルはこのフォルダに入れていくことになります。 ​ ​ 名前を間違えると認識されないので注意しましょう。 EX-3 インスペクターの拡張 まずはインスペクターの表示内容を拡張してみましょう。 ​Attributeを用いることである程度同じことができますが、こちらのメリットはインスペクター内で関数を実行したりif文の判定を取ったりすることができる点にあります。 ​ 拡張元になるスクリプトを作成します。新しいスクリプトPlayerを作成して、以下のように入力してください。このスクリプトはEditorフォルダに入れなくても構いません。 サンプルなのでお好みでパラメータを追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // 適当なパラメータ public string Name; // 名前 public int Level = 0; // レベル public int DefaultHP = 0; // 初期体力 public int LevelUpHP = 0; // レベルアップで上昇するHP量 public int DefaultATK = 0; // 初期攻撃力 public int LevelUpATK = 0; // レベルアップで上昇する攻撃力 public int DefaultDEF = 0; // 初期防御力 public int LevelUpDEF = 0; // レベルアップで上昇する防御力 } 今回は扱いがわかりやすいように変数をpublicにしていますが、実際の実装ではprivateにしておいた方がよいでしょう。 ​ Playerスクリプトを適当なオブジェクトにアタッチしてみてください。例のごとく変数の中身がインスペクターに表示されています。 ここからエディタ拡張を行っていきましょう。 今回は扱いがわかりやすいように変数をpublicにしていますが、実際の実装ではprivateにしておいた方がよいでしょう。 ​ Playerスクリプトを適当なオブジェクトにアタッチしてみてください。例のごとく変数の中身がインスペクターに表示されています。 ここからエディタ拡張を行っていきましょう。 ​ Editorフォルダ内に PlayerEditorスクリプトを作成してください。 まずはレベルの値が0以下の場合、不正な値として警告を表示するようにしてみましょう。実際の実装でも値をうっかり入れ忘れるということは起こり得るため、入れ忘れていることがわかりやすいようにします。 PlayerEditorスクリプトを開いて、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; // エディタ拡張をする時に必要 [CustomEditor(typeof(Player))] // 対象となるクラスを指定 public class PlayerEditor : Editor // Editorを継承する必要がある { // インスペクターを表示しているときに実行される public override void OnInspectorGUI() { // 開いているスクリプトを引っ張ってくる Player player = (Player)target; // 最初にメッセージを表示してみる GUILayout.Label("【プレイヤー】"); // 通常表示される内容を表示する base.OnInspectorGUI(); // 値が異常な場合は警告を表示してみる if (player.Level <= 0) { EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning); } } } 新しい要素が多いですが、実装自体はシンプルなので順番に見ていきましょう。 ​ 【プログラムの解説】 ・エディタ拡張を行う場合は最初に using UnityEditor; と記述する必要があるので注意しましょう。 ​ ・[CustomEditor(typeof(Player))] はこのスクリプトを適用する対象を指定することができるアトリビュートです。今回は当然Playerクラスを指定しています。 ​ ・エディタ拡張を行うクラスはEditorを継承する必要があります。using UnityEditor; と同じく忘れないように注意しましょう。 ​ ・OnInspectorGUI 関数はインスペクターの表示内容をカスタマイズしたい時に使用します。 ​ ・Editor側で用意されているTargetというパラメータには拡張される側のクラスが入っています。 Player player = (Player)target; のようにキャストすることで、拡張される側のデータを引っ張ってくることが可能です。 ※ Player player = target as Player; のように記述することもできます ・GUILayout.Label("表示したい文章"); を使用することでインスペクターに文章を表示することができます。アトリビュートでもHeaderを用いることで似たようなことはできますが、こちらは変数に紐付けられている訳ではなく、単体で動作します。 ​ ・base.OnInspectorGUI(); と記述することで、通常のインスペクターで表示される内容をまとめて表示できます。 ・EditorGUILayout.HelpBox("文章"); を使うことでヘルプや警告を表示できます。第二引数で種類を指定できます。 ​ この状態でPlayerの項目を確認すると、Levelが0の時に警告が表示されるようになります。Levelの値を1にすると警告が消えることを確認してください。 もしインスペクターに「Multi-object editing not supported.」と表示された場合、別のインスペクターを開いてから再度確認すると反映されます。 Unity Error! ​ 次は項目のリセットボタンを実装してみましょう。 ​ インスペクター横のリセットボタンを押すことでもリセットできますが、インスペクターにボタンが表示されている方がわかりやすいので、今回はリセットボタンを実装してみます。 ​ PlayerEditorスクリプトを開いて、赤い部分のコード を追加してください。 (前略) // 通常表示される内容を表示する base.OnInspectorGUI(); // 値が異常な場合は警告を表示してみる if (player.Level <= 0) { EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning); } // リセットボタンを表示 if (GUILayout.Button("リセット")) { // 押された時にパラメータをリセットする player.Name = ""; player.Level = 1; player.DefaultHP = 0; player.LevelUpHP = 0; player.DefaultATK = 0; player.LevelUpATK = 0; player.DefaultDEF = 0; player.LevelUpDEF = 0; } } } 【プログラムの解説】 ・GUILayout.Button("表示内容") を使うことでボタンを表示することができます。押された瞬間にtrueを返すため、if文の中に書くことで「ボタンが押されたときの処理」を記述できます。 ​ ​ ここまで書けたら保存して、適当に項目を入力した後にリセットボタンを押してみましょう。項目がリセットされます。 エディタ拡張を使うことで入力欄を表示することもできます。レベルの値を入力することで、そのレベル時のステータスを計算してみましょう。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; // エディタ拡張をする時に必要 [CustomEditor(typeof(Player))] // 対象となるクラスを指定 public class PlayerEditor : Editor // Editorを継承する必要がある { int m_sampleLevel = 0; // ステータス計算用レベル // インスペクターを表示しているときに実行される public override void OnInspectorGUI() { // 開いているスクリプトを引っ張ってくる Player player = (Player)target; // 最初にメッセージを表示してみる GUILayout.Label("【プレイヤー】"); // 通常表示される内容を表示する base.OnInspectorGUI(); // 値が異常な場合は警告を表示してみる if (player.Level <= 0) { EditorGUILayout.HelpBox("レベルの値が0以下です!", MessageType.Warning); } // リセットボタンを表示 if (GUILayout.Button("リセット")) { // 押された時にパラメータをリセットする player.Name = ""; player.Level = 1; player.DefaultHP = 0; player.LevelUpHP = 0; player.DefaultATK = 0; player.LevelUpATK = 0; player.DefaultDEF = 0; player.LevelUpDEF = 0; } // 空白 EditorGUILayout.Space(); // 入力欄を表示 m_sampleLevel = EditorGUILayout.IntField("計算用レベル", m_sampleLevel); // ステータスを計算 int hp = player.DefaultHP + (player.LevelUpHP * m_sampleLevel); int atk = player.DefaultATK + (player.LevelUpATK * m_sampleLevel); int def = player.DefaultDEF + (player.LevelUpDEF * m_sampleLevel); // 計算結果を表示 GUILayout.Label("【推定ステータス】¥nHP:" + hp + "¥nATK:" + atk + "¥nDEF:" + def); } } ※ 半角¥を表示できないので全角で表示していますが、実際は半角で入力してください。 ​ 【プログラムの解説】 ・EditorGUILayout.Space(); と記入することで項目の間に行間を追加することができます。インスペクターを見やすいようにお好みで調整しましょう。 ​ ​ ・EditorGUILayout.IntField 関数を使うことでインスペクターにint型の入力欄を表示できます。他にもFloatFieldやColorFieldなど変数の型ごとに様々な関数があります。 ​ 第一引数に表示名、第二引数に表示する値を指定して使用します。 ​ ​ ​ これで入力したレベルでの推定ステータスを計算できます。計算用レベルの項目に値を入力して確認してみてください。 このようにエディタ拡張をすることで開発を補助する機能を実装することができます。開発中に「こういう機能があったら効率が上がるのにな」と思ったときはエディタ拡張を行うことを考えてみてください。 EX-4 ウィンドウの作成 先ほどはインスペクターの内容を拡張しましたが、オリジナルのウィンドウを作ることもできます。 まずは文章を表示するだけのウィンドウを表示してみましょう。 ​ ​ 新しいスクリプトTestWindowEditorを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; // エディタ拡張をする時に必要 public class TestWindowEditor : EditorWindow // 自作のウィンドウを扱うときに継承 { // ウィンドウを作成 [MenuItem("Window/TestWindow")] static void Open() { // ウィンドウの名前を変更 GetWindow("開発補助ツール"); } // ここにウィンドウのGUI処理を記述 void OnGUI() { // 適当にメッセージを表示してみる EditorGUILayout.LabelField("作業を自動化するウィンドウです"); } } 【プログラムの解説】 ・オリジナルのウィンドウを作成する際は EditorWindowクラスを継承する必要があります。 ​ ・[MenuItem("Window/TestWindow")] で、どこにどういった名前でウィンドウを開くメニューを表示するか設定しています。 ・OnGUI() 関数内にウィンドウを開いている時の処理を記述します。 ​ 後はインスペクターを拡張したときとほぼ同じです。 ​ 「Window」→「TestWindow」を開くと、設定した文章が表示されます。このようにUnityでは簡単に自作ウィンドウを作ることができます。 あとはOnGUI関数内で実行したい処理をお好みで記述するだけです。 今回は「指定したオブジェクトを一定間隔で大量に配置する」機能と「指定したタグのオブジェクトを一括削除する」機能を実装してみましょう。 ​ ​ TestWindowEditorスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; // エディタ拡張をする時に必要 public class TestWindowEditor : EditorWindow // 自作のウィンドウを扱うときに継承 { GameObject m_createObject; // 生成するオブジェクト Vector3 m_startPos; // 生成地点 Vector3 m_offset; // ずらす座標 int m_createNum = 0; // 生成数 string m_tagName = ""; // 検索対象のタグ // ウィンドウを作成 [MenuItem("Window/TestWindow")] static void Open() { // ウィンドウの名前を変更 GetWindow("開発補助ツール"); } // ここにウィンドウのGUI処理を記述 void OnGUI() { // 適当にメッセージを表示してみる EditorGUILayout.LabelField("作業を自動化するウィンドウです"); // 行間を開ける EditorGUILayout.Space(); // オブジェクト一括生成 EditorGUILayout.LabelField("【オブジェクト一括生成】"); // パラメータの設定 m_createObject = EditorGUILayout.ObjectField("生成対象", m_createObject, typeof(GameObject), true) as GameObject; m_startPos = EditorGUILayout.Vector3Field("生成開始地点", m_startPos); m_offset = EditorGUILayout.Vector3Field("ずらす座標", m_offset); m_createNum = EditorGUILayout.IntField("生成数", m_createNum); // 生成ボタン if (GUILayout.Button("生成")) { // for文で一気に生成 Vector3 pos = m_startPos; for (int i = 0; i < m_createNum; i++) { Instantiate(m_createObject, pos, Quaternion.identity); pos += m_offset; } } // 行間を開ける EditorGUILayout.Space(); // タグで検索して削除 EditorGUILayout.LabelField("【タグで検索して削除】"); // タグ指定 m_tagName = EditorGUILayout.TagField("対象のタグ", m_tagName); // 削除ボタン if (GUILayout.Button("削除")) { // 削除処理 GameObject[] objects = GameObject.FindGameObjectsWithTag(m_tagName); foreach(GameObject obj in objects){ DestroyImmediate(obj); } } } } 【プログラムの解説】 ・ObjectField関数を使うことで○○Field関数が存在しない型でも入力欄を表示できます。 ​ ・エディタ内でオブジェクトを削除する際はDestroy関数ではなくDestroyImmediateを使用してください。 ​ ここまで書けたらウィンドウを開いて、各機能が動作するか確認してみてください。 ​ このようにエディタ拡張をすることで、開発効率を上げる機能を実装することもできます。 EX-5 データベースの作成 最後にここまでのまとめとして、大量のモンスターデータを管理するためのデータベース用ウィンドウを作ってみましょう。 ​ ScriptableObjectを用いることで大量のモンスターのパラメータを管理できますが、デフォルトの表示では目的のモンスターデータを見つけたい時に不便な配置になっています。特にデータ数が多い場合は、項目を1つ探すだけでも時間がかかってしまいます(ScriptableObjectについては3D脱出ゲーム編2-2参照 ) 今回は某RPGのモンスターデータ作成を想定して、モンスターのパラメータを見やすく設定するためのエディタを作ってみましょう。 ​ まずはモンスターデータの定義とデータを格納する場所を作ります。 ​ MonsterDataスクリプトを作成して、以下のように入力してください。スクリプトを作るのはEditorフォルダ内でなくても構いません。 using System.Collections; using System.Collections.Generic; using UnityEngine; // 属性の定義 public enum ElementType { enFire, // 火属性 enWater, // 水属性 enWind, // 風属性 enEarth, // 土属性 enLight, // 光属性 enDark // 闇属性 } [System.Serializable] public class Monster { public string Name; // 名前 public ElementType Element; // 属性 public Sprite Image; // モンスター画像 public int HP; // 体力 public int ATK; // 攻撃力 public int DEF; // 防御力 [Multiline(4)] public string Explanation; // 説明文 } [CreateAssetMenu(fileName = "MonsterDataBase", menuName = "CreateMonsterDataBase")] public class MonsterData : ScriptableObject { // モンスターリストの可変長配列 public List Monsters = new List(); } コードの内容は3D脱出ゲーム編2-2 とほとんど変わらないため、解説は省略します。 ​ ただしモンスターデータの定義は構造体ではなくクラスで行ってください 。 ​ Createメニュー内に「CreateMonsterDataBase」の項目が追加されているので、クリックして追加してください。 Editorフォルダ内に MonsterDBEditorスクリプトを作成して、以下のように入力してください。 ​ こちらもあくまで一例なので、中身はお好みで変更してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEditor.IMGUI.Controls; public class MonsterDBEditor : EditorWindow { // 対象データベース static MonsterData m_monsterData; // 名前一覧 static List m_nameList = new List(); // スクロール位置 Vector2 m_leftScrollPos = Vector2.zero; // 選択中No int m_selectNo = -1; // 検索欄 SearchField m_searchField; string m_searchText = ""; // ウィンドウを作成 [MenuItem("Window/MonsterDataBase")] static void Open() { // 読み込み m_monsterData = AssetDatabase.LoadAssetAtPath("Assets/MonsterDataBase.asset"); // 名前を変更 GetWindow("モンスターデータベース"); } // ここにウィンドウのGUI処理を記述 void OnGUI() { // まずは名前一覧を作成 ResetNameList(); // 左右に配置 EditorGUILayout.BeginHorizontal(GUI.skin.box); { // 左側 LeftUpdate(); // 右側 RightUpdate(); } EditorGUILayout.EndHorizontal(); } void LeftUpdate() { // 名前リストのサイズを指定 EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(160), GUILayout.Height(400)); { // 検索欄 m_searchField ??= new SearchField(); GUILayout.Label("名前検索"); m_searchText = m_searchField.OnToolbarGUI(m_searchText); // 検索処理 Search(); // 左側のスクロールビュー m_leftScrollPos = EditorGUILayout.BeginScrollView(m_leftScrollPos, GUI.skin.box); { // データリスト for (int i = 0; i < m_nameList.Count; i++) { // 色変更 if (m_selectNo == i) { GUI.backgroundColor = Color.cyan; } else { GUI.backgroundColor = Color.white; } // ボタンが押されたときの処理 if (GUILayout.Button(i + ":" + m_nameList[i])) { // 対象変更 m_selectNo = i; } } // 色を戻す GUI.backgroundColor = Color.white; } EditorGUILayout.EndScrollView(); // 項目操作ボタン EditorGUILayout.BeginHorizontal(); { if (GUILayout.Button("追加", EditorStyles.miniButtonLeft)) { AddData(); } if (GUILayout.Button("削除", EditorStyles.miniButtonRight)) { DeleteData(); } } EditorGUILayout.EndHorizontal(); // 項目数 GUILayout.Label("項目数:" + m_nameList.Count); } EditorGUILayout.EndVertical(); } void RightUpdate() { // 何も選んでいないなら非表示 if (m_selectNo < 0) { return; } // 右側を更新 EditorGUILayout.BeginVertical(GUI.skin.box); { // 基礎情報を表示 GUILayout.Label("ID:" + m_selectNo + " Name:" + m_nameList[m_selectNo]); // 空白 EditorGUILayout.Space(); // 設定欄を表示 m_monsterData.Monsters[m_selectNo].Name = EditorGUILayout.TextField("名前", m_monsterData.Monsters[m_selectNo].Name); // 列挙型を文字列に置き換えられる m_monsterData.Monsters[m_selectNo].Element = (ElementType)EditorGUILayout.Popup( "属性", (int)m_monsterData.Monsters[m_selectNo].Element, new string[] { "火属性", "水属性", "風属性", "土属性", "光属性", "闇属性" } ); m_monsterData.Monsters[m_selectNo].Image = EditorGUILayout.ObjectField("画像", m_monsterData.Monsters[m_selectNo].Image, typeof(Sprite), true) as Sprite; // 空白 EditorGUILayout.Space(); // ステータス欄 m_monsterData.Monsters[m_selectNo].HP = EditorGUILayout.IntField("体力", m_monsterData.Monsters[m_selectNo].HP); m_monsterData.Monsters[m_selectNo].ATK = EditorGUILayout.IntField("攻撃力", m_monsterData.Monsters[m_selectNo].ATK); m_monsterData.Monsters[m_selectNo].DEF = EditorGUILayout.IntField("防御力", m_monsterData.Monsters[m_selectNo].DEF); // 空白 EditorGUILayout.Space(); GUILayout.Label("図鑑説明"); m_monsterData.Monsters[m_selectNo].Explanation = EditorGUILayout.TextArea(m_monsterData.Monsters[m_selectNo].Explanation); // 値が異常な場合は警告を表示してみる if (m_monsterData.Monsters[m_selectNo].HP <= 0) { EditorGUILayout.HelpBox("初期体力が0以下です!", MessageType.Warning); } } EditorGUILayout.EndVertical(); } // 名前一覧の作成 static void ResetNameList() { // 初期化 m_nameList.Clear(); // 名前を入れていく foreach (Monster monster in m_monsterData.Monsters) { m_nameList.Add(monster.Name); } } // 検索 void Search() { if (m_searchText == "") { return; } // 初期化 int startNum = m_selectNo; startNum = Mathf.Max(startNum, 0); for (int i = startNum; i < m_nameList.Count; i++) { // 文字列が含まれるかチェック if (m_nameList[i].Contains(m_searchText)) { // 終了 m_selectNo = i; return; } } // ヒットしない場合は-1にしておく m_selectNo = -1; } // データの追加 void AddData() { Monster newMonster = new Monster(); // 追加 m_monsterData.Monsters.Add(newMonster); } // データの削除 void DeleteData() { if (m_selectNo == -1) { // 削除できない return; } // 選択位置のデータを削除 m_monsterData.Monsters.Remove(m_monsterData.Monsters[m_selectNo]); // 調整 m_selectNo -= 1; m_selectNo = Mathf.Max(m_selectNo, 0); } } 要素が多いため、一部の解説は省略します。 【プログラムの解説】 ・AssetDatabase.LoadAssetAtPath("Assets/MonsterDataBase.asset"); では、ファイルパスからモンスターデータを読み込んでいます。 この機能はUnityEditorの機能なので、エディタ上でしか動作しません。 ​ ・ EditorGUILayout.BeginHorizontal(); と EditorGUILayout.EndHorizontal(); で囲んだ部分は要素が横に並ぶようになります 。 ​ 必ずしも間を中かっこで囲う必要はありませんが、サンプルでは範囲がわかりやすいように中かっこで囲んでいます。 引数に GUI.skin.box を追加することで範囲を枠で囲んだり、GUILayout.Width(160) 等で範囲を指定することもできます。 BeginVertical とEndVertical は左右ではなく上下に要素を並べることができる関数で、同じように使うことができます。 ​ また、 EditorGUILayout.BeginScrollView(); と EditorGUILayout.EndScrollView();で​スクロールできる領域を表示することもできます。スクロール位置を管理するためのVector2型の変数が必要です(サンプルではm_leftScrollPos) ​ ・m_searchField ??= new SearchField(); の??= はNull合体代入演算子と呼ばれるもので、左辺がnullの時のみ右辺の値を代入するというものです(参照 ) ・SearchField は検索欄になります。using UnityEditor.IMGUI.Controls; が必要なので注意しましょう。また、検索欄が表示されるだけなので実際の検索処理は別で書く必要があります。 ​ ​ 新しい要素は多いですが、実際に使ってみると挙動がわかりやすいと思います。 ​ 実際に「Window」→「MonsterDataBase」を開いて確認してみましょう。左側のデフォルトの配置より、右側のオリジナルのウィンドウの方が一目でデータを確認しやすいと思います。 ​ 大量のデータを扱うゲームを作る際にはぜひエディタ拡張を行ってみてください。エディタ拡張には他にも色々な関数があるため、各自で調べて色々な機能を増やしてみましょう。 ​ エディタ拡張は開発を効率化できますが、メインの開発が進むわけではないため筆者のように エディタ拡張に夢中になってメインの開発を疎かにしないようにしましょう。

  • 3D脱出ゲーム編 Lesson4「UIを実装しよう」 | Unity1gc2

    3D脱出ゲーム編 Lesson4 UIを実装しよう 4-1 アイテム欄の実装 今所持しているアイテムを確認するためのアイテム欄を実装していきます。 ​ ​ まずは素材を仮配置していきましょう。お好みのウィンドウ素材を用意するか、サンプルの素材をダウンロードしてください。 ​ ​ ダウンロードできたらSpriteフォルダにドラッグ&ドロップして追加しましょう。 ここからダウンロード 3Dゲームでは、追加した画像をUIとして使用する際はTexture Typeを変更する必要があります。 追加したウィンドウ画像を選択して、Texture Typeを「Sprite (2D and UI)」に変更してください。変更できたら、右下のApplyボタンを押して反映させましょう。 ウィンドウを作成するときはSprite EditorのSlice機能を使うと便利です。 普通にウィンドウを変形させると左の画像のように四隅が歪んでしまいますが、この機能を使うと右の画像のように四隅の大きさを固定したままウィンドウの大きさを変えることができます。 ​(2Dランゲーム編Lesson4参照 ) ウィンドウのスプライトの設定を変更します。SpriteModeをMultipleに、MeshTypeをFull Rectに変更してください。変更したら右下のApplyボタンをクリックします。 Sprite Editorでウィンドウ画像をスライスすることができますが、Sprite Editorを開こうとすると警告が表示されてしまいます。Sprite​ Editorは2Dゲームの場合デフォルトで導入されていますが、3Dゲームの場合は手動でパッケージをインストールする必要があります。 「Window」→「Package Manager」を開いてください。 左上のPackagesを「Unity Registry」に変更して​​、右上の検索欄に「Sprite」と入力しましょう。「2D Sprite」が左側に表示されるので選択して、右下のInstallボタンをクリックしてください。 ここまでの操作が完了したらPackage Managerを閉じてください。 ​ ​ ウィンドウ画像を選択してSprite Editorを開きましょう。 ウィンドウ画像全体を囲うようにマウスをドラッグしてください。 右下にSpriteウィンドウが表示されるのでLeft、Top、Right、Bottom全てに40を入力してください。ウィンドウ画像が9つに分割されます。 ​ 設定できたら右上のApplyボタンを押して、Sprite Editorを閉じてください。 実際にウィンドウやテキストを配置していきます。特に新しい要素はないので、サンプルの通りに配置していってください。 ​ まずはオブジェクトを管理しやすいように、Canvasの子オブジェクトに空オブジェクトを追加しましょう。名前はItemUIにしておきます。 ​ このオブジェクトは特に設定は必要ありません。アイテム欄のUIはこのオブジェクトの子オブジェクトに配置していきましょう。 ItemUIの子オブジェクトにImageを追加してください。名前はMainWindowにしておきます。 MainWindowのRect Transformを設定してください。 ​特にアンカーを左上に設定 している点に注意しましょう。 ​ 【サンプルの入力例】 PosX : -286 PosY : 138 Width/Height : 160 Scale : 0.8 ​ ImageコンポーネントのSauce Imageには、先ほどスライスしたウィンドウの画像を設定してください。 追加でサブウィンドウを2つ表示します。 ​ MainWindowをコピーしてSubWindow1とSubWindow2を作成してください。 SubWindow1とSubWindow2のRect Transformを設定してください。 ​ 【サンプルの入力例 SubWindow1】 PosX : -310 PosY : 34 Width/Height : 100 Scale : 0.8 【サンプルの入力例 SubWindow2】 PosX : -310 PosY : -48 Width/Height : 100 Scale : 0.8 残りのUIを完成させましょう。 ​ Imageを追加して名前をItemDataBGにしておきます。ItemDataBGの子オブジェクトにTextMeshProを2つ追加して名前をNameとMessageにしましょう。 別でTextMeshProを追加して名前をTextLBRBにしておきます。 ItemDataBGのパラメータを設定してください。黒い半透明の背景が表示されます。 【サンプルの入力例 ItemDataBG】 PosX : 12 PosY : 160 Width : 470 Height : 80 Anchor : 左上 ​ Sauce Image : None(なし) Color : R=0 G=0 B=0 A=155 次にアイテム名を表示する場所であるNameオブジェクトの設定をしていきます。ItemDataBGの子オブジェクトになっている点に注意しましょう。 ​ 【サンプルの入力例 Name】 PosX : -14 PosY : 0 Width : 400 Height : 50 Anchor : 中央 ​ TextInput : (仮入力なのでなんでも可) Font Asset : Corporate-Mincho-ver3 SDF Font Size : 36 Alignment : 左寄せ 中央 アイテムの説明を表示する場所であるMessageオブジェクトの設定をしていきます。こちらもItemDataBGの子オブジェクトになります。 ​ 【サンプルの入力例 Message】 PosX : 130 PosY : 0 Width : 200 Height : 50 Anchor : 中央 ​ TextInput : (仮入力なのでなんでも可) Font Asset : Corporate-Mincho-ver3 SDF Font Size : 20 Alignment : 左寄せ 中央 TextLBRBオブジェクトの設定をしていきます。 ​ 【サンプルの入力例 TextLBRB】 PosX : -240 PosY : -70 Width : 200 Height : 50 Anchor : 中央 ​ TextInput : ↑LB (改行) ↓RB Font Asset : Corporate-Mincho-ver3 SDF Font Size : 18 ​Paragraph : -40 Alignment : 左寄せ 中央 これでアイテム欄の仮配置が完了しました。 ここからはアイテム欄の実装をしていきます。 ​ アイテム欄には所持しているアイテムの画像を表示したいところですが、全てのアイテムの画像を用意するのはかなりの手間です。 また、アイテムを後から追加したい時も、その都度画像を用意しないといけなくなってしまいます。 ​ 今回はメインカメラとは別にサブカメラを配置して、サブカメラに映った内容をUIに表示することでアイテム欄にアイテムの画像を表示してみましょう(EX 複数のカメラを扱う も参照してください) まずはUIに使用するカメラを配置しましょう。​ Cameraを追加してください。名前はUICamera1にしておきます。 Cameraの設定をする前にレイヤーを追加しましょう。 「Add Layer」を選択して、適当な場所にUI_Itemレイヤーを追加しておきます。 それではUI用Cameraの設定をしていきます。 ​ Cameraの座標をX=300 Y=0 Z=0 に設定してください(回転は全て0、大きさは全て1のまま) CameraクラスのProjection内にあるNear(近平面)と0.1に、Far(遠平面)を8に設定してください。カメラの近くにアイテムを置くため近平面を近くして、遠くの情報は必要ないため遠平面も非常に近い距離に設定しています。 RenderingのCulling Maskを設定しましょう。ここで設定したレイヤーのオブジェクトがカメラに表示されます。 ​ デフォルトではEverythingになっていますが、一旦Nothingを選択して全てのチェックを外してから、UI_Itemを選択してください。これでUI_Itemレイヤーに設定されたアイテムだけがカメラに映るようになります。 EmvironmentのBackground Typeを「Solid Collar」に変更してください。後ほどアイテム画像を表示した際に背景の空が表示されないようにしています。 次にカメラの内容を出力するためのRenderTextureを作成します。RenderTextureフォルダを作成してください。 「Create」→「Render Texture」を追加してください。​ Render Textureが生成されるので、名前をItemUI1にしておいてください。​ ​ インスペクターでRender Textureの設定ができますが、デフォルトのままで構いません。 作成したRender TextureをカメラのOutput Textureにドラッグ&ドロップしてください。 ​ これでカメラの内容がItemUI1へ出力されるようになります。 ItemUI1の内容をUIとして表示してみましょう。 ​ MainWindowの子オブジェクトにRaw Imageを追加してください。今まで使っていたImageとは異なる ので注意しましょう。 Raw ImageはSpriteではなくTextureを表示することができます。普段はあまり使いませんが、今回のようにCameraの内容を表示したい時には使用します。 ​ WidthとHeightを120に設定して、TextureをItemUI1(先程作成したRender Texture)に設定してください。 UI用カメラにアタッチされているAudio Listenerを外しておきましょう (Cameraを作成した際に自動でついているコンポーネントですが、Main Cameraに既にアタッチされているため、このままだとエラーが出てしまいます) ​ Audio Listener右端の3つの点をクリックして「Remove Component」をクリックしてください。 Audio Listenerが2つ以上あった場合、以下のような警告が表示されます。シーン上にAudio Listenerが2つ以上存在する時は1つのAudio Listenerが有効になり、それ以外のAudio Listenerは無効になります。 UIカメラが正常に動作しているかどうか確認してみましょう。適当にUIカメラの範囲内にCubeを配置してください。 このままではカメラにCubeが映らないので、LayerをUI_Itemに設定するのを忘れないようにしましょう。 カメラに映ったCubeがウィンドウ内に表示できていればOKです。​確認できたらCubeは削除してください。 残り2つのウィンドウも設定しましょう。 ​ まずはUIカメラを複製してください。​UICamera2の座標をX=320に、UICamera3の座標をX=340にしてください。​他の設定はそのままで構いません。 Render Textureを複製して、それぞれのカメラに設定しましょう。別々のRender Textureに3つのカメラの内容が出力されるようにします。 SubWindowの子オブジェクトにもRaw Imageを追加しましょう。 ​ SubWindow1(上のウィンドウ)のRaw ImageにはItemUI2を、SubWindow2(下のウィンドウ)のRaw ImageにはItemUI3を設定してください。 後は各カメラに合う座標にアイテムの3Dモデルを生成するだけになります。​​まずはアイテムごとにUI表示中の回転量や座標補正を指定できるようにしましょう。 ItemDataスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; // アイテム用のデータベース [CreateAssetMenu(fileName = "ItemDataBase", menuName = "CreateItemDataBase")] public class ItemData : ScriptableObject { // アイテム情報の構造体 [System.Serializable] public struct Item { public string ItemName; // アイテム名 [Multiline(2)] public string ItemExplanation; // アイテム欄で表示する説明文 public GameObject ItemPrefab; // アイテムを捨てた時に生成するオブジェクト // 設置用パラメータ public Vector3 ItemPivot; // アイテムを置いたときに座標を補正する量 // UI用パラメータ public Vector3 UI_Offset; // UIで表示するときに補正する座標 public Vector3 UI_Rotation; // UIで表示するときの回転量 public Vector3 UI_Scale; // UIで表示するときの大きさ } // アイテムリストの可変長配列 public Item[] Items; } 次にUI更新の処理を実装しましょう。 ​ UI_Itemスクリプトを作成して、以下のように入力してください。少々長いですが、特別な処理はありません。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; public class UI_Item : MonoBehaviour { GameManager m_gameManager; [SerializeField] GameObject[] ItemCamera; // UI用カメラ [SerializeField] TextMeshProUGUI ItemNameText, ItemMessageText; [SerializeField] Vector3 ItemOffset; // アイテム生成座標の補正量(共通) // UI用に生成したアイテム GameObject[] m_itemObject; // 表示内容を更新する アイテムインベントリを更新したタイミングで呼ぶ public void UpdateUI() { // 初期化 if (m_gameManager == null) { m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); // 要素数はカメラの数と同じ m_itemObject = new GameObject[ItemCamera.Length]; } // 変数を準備する Vector3 itemPos; int itemNo; int selectNo = m_gameManager.GetSelectItemNo(); // アイテム欄を更新する for (int i = 0; i < ItemCamera.Length; i++) { // 指定スロットのアイテム番号を取得 itemNo = m_gameManager.GetItemID(selectNo); // アイテムを一旦削除 Destroy(m_item Object[i]); // 名前や説明欄の更新 if (selectNo == m_gameManager.GetSelectItemNo()) { // 表示の有無 if (itemNo == -1) { // アイテムがないため説明欄は非表示 ItemNameText.enabled = false; ItemMessageText.enabled = false; } else { // 説明欄を表示 ItemNameText.enabled = true; ItemMessageText.enabled = true; // テキスト更新 ItemNameText.text = m_gameManager.GetItemData().Items[itemNo].ItemName; ItemMessageText.text = m_gameManager.GetItemData().Items[itemNo].ItemExplanation; } } // セレクト番号の更新 selectNo++; if (selectNo > ItemCamera.Length - 1) { selectNo = 0; } if (itemNo == -1) { // アイテムがないためここで終了 continue; } // アイテムを生成 // まずは座標を決める itemPos = ItemCamera[i].transform.position; itemPos += ItemOffset; itemPos += m_gameManager.GetItemData().Items[itemNo].UI_Offset; // 生成 GameObject item = Instantiate(m_gameManager.GetItemData().Items[itemNo].ItemPrefab, itemPos, Quaternion.identity); // アイテムを覚えておく m_itemObject[i] = item; // 回転 item.transform.Rotate(m_gameManager.GetItemData().Items[itemNo].UI_Rotation, Space.World); // 大きさ調整 item.transform.localScale = m_gameManager.GetItemData().Items[itemNo].UI_Scale; // レイヤーをUI用に変更(名前で検索) item.layer = LayerMask.NameToLayer("UI_Item"); } } } 後はアイテム欄を更新したときにUIを更新させるだけです。 GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { // アイテムデータ [SerializeField] ItemData Item_Data; public ItemData GetItemData() { return Item_Data; } // 選択中のアイテム番号(アイテム欄配列の番号) [SerializeField] int SelectItemNo = 0; public int GetSelectItemNo() { return SelectItemNo; } // アイテム欄 [SerializeField] int[] ItemID; // 引数番スロットのアイテムを取得 public int GetItemID(int no) { return ItemID[no]; } // アイテム欄のUI [SerializeField] UI_Item ItemUI; // アイテムを取得する // アイテム欄に空きがあったらtrue なかったらfalseを返す public bool GetItem(int getItemID) { int selectID = SelectItemNo; // 順番にアイテム欄を確認していって、空いている場所にIDを格納 for (int i = 0; i < ItemID.Length; i++) { if (ItemID[selectID] == -1) { // 空きがあるのでアイテムIDを格納 ItemID[selectID] = getItemID; // UIを更新 ItemUI.UpdateUI(); return true; } selectID++; if (selectID > ItemID.Length - 1) { // オーバーしたので0に戻す selectID = 0; } } // 空きがなかった return false; } // 引数番スロットのアイテムを捨てる void ItemDrop() { // アイテムがあるか確認 if (ItemID[SelectItemNo] == -1) { Debug.Log("【エラー】" + SelectItemNo + "番にアイテムがありません!"); return; } // プレイヤーの移動量を取得 Rigidbody playerRb = GameObject.FindGameObjectWithTag("Player").GetComponent(); Vector3 velocity = playerRb.velocity; velocity.y = 0.0f; // 捨てるアイテムを生成 Vector3 itemPos = Camera.main.transform.position; GameObject dropItem = Instantiate(Item_Data.Items[ItemID[SelectItemNo]].ItemPrefab, itemPos, Camera.main.transform.rotation); // 前方に発射 dropItem.GetComponent().ItemDrop(velocity); // アイテム欄のIDをリセット ItemID[SelectItemNo] = -1; // UIを更新 ItemUI.UpdateUI(); } // アイテムを置く public void ItemPlaced(GameObject platformObj, Vector3 addPos, Vector3 addRot) { // 選んでいるアイテムIDを取得 int id = ItemID[SelectItemNo]; // 座標を計算 Vector3 pos = platformObj.transform.position + addPos; pos += Item_Data.Items[id].ItemPivot; // アイテムを生成 回転はプレハブの回転を使用 GameObject placedItem = Instantiate(Item_Data.Items[id].ItemPrefab, pos, Item_Data.Items[id].ItemPrefab.transform.rotation); // アイテムを回転させる placedItem.transform.Rotate(addRot, Space.World); // 生成したアイテムに設置された土台を教える placedItem.GetComponent().ItemPlaced(platformObj); // 生成したアイテムの親オブジェクトを土台にする placedItem.transform.parent = platformObj.transform; // 所持品から番号を削除 ItemID[SelectItemNo] = -1; // UIを更新 ItemUI.UpdateUI(); } void Awake() { // UIを更新(初期化) ItemUI.UpdateUI(); } void Update() { // Bボタンで捨てる if ((Input.GetKeyDown("joystick button 1") || Input.GetKeyDown(KeyCode.Alpha0))) { ItemDrop(); } // 選択アイテムの変更 if ((Input.GetKeyDown("joystick button 4") || Input.GetKeyDown(KeyCode.Alpha1))) { SelectItemNo++; if (SelectItemNo > ItemID.Length - 1) { SelectItemNo = 0; } // UIを更新 ItemUI.UpdateUI(); } if ((Input.GetKeyDown("joystick button 5") || Input.GetKeyDown(KeyCode.Alpha2))) { SelectItemNo--; if (SelectItemNo < 0) { SelectItemNo = ItemID.Length - 1; } // UIを更新 ItemUI.UpdateUI(); } } } Unity側の設定をしていきましょう。 ​ Canvasの子オブジェクトであるItemUIに先ほど作成したUI_Itemスクリプトをアタッチしてください。 Item Cameraは要素数を3にして、UI用カメラを1から順番に設定していきます(順番を間違えると表示がおかしくなるので注意) Item Name TextとItem Message Textにはアイテム名と説明文のTextMeshProを設定します。 ​ Item OffsetはX=0、Y=-1、Z=4を入力してください。 GameManagerのGameManagerコンポーネントにItemUIのパラメータが追加されているので、先ほど追加したItemUIコンポーネントを設定しましょう。 ItemDataBaseを開いて、UI調節用のパラメータを設定してください。 ​ 【本(黄色い本、青い本、赤い本、緑の本)】 UI_Offset : X=0 Y=-0.8 Z=0 UI_Rotation : X=0 Y=60 Z=20 UI_Scale : X=7 Y=7 Z=7 ​ 【宝玉(青の宝玉、赤の宝玉、黄の宝玉、緑の宝玉)】 UI_Offset : X=0 Y=1 Z=0 UI_Rotation : X=0 Y=0 Z=0 UI_Scale : X=3 Y=3 Z=3 ​ 【銀のリンゴ】 UI_Offset : X=0 Y=1 Z=0 UI_Rotation : X=-40 Y=20 Z=0 UI_Scale : X=20 Y=20 Z=20 ​ 【銀のゴリラ】 UI_Offset : X=-0.2 Y=-1.5 Z=2 UI_Rotation : X=-90 Y=140 Z=0 UI_Scale : X=2 Y=2 Z=2 ​ 【銀のパイナップル】 UI_Offset : X=0 Y=-1 Z=0 UI_Rotation : X=-90 Y=0 Z=0 UI_Scale : X=6 Y=6 Z=6 ​ ​ これでアイテム欄の完成です。実際にアイテムを拾って確かめてみてください。 ​アイテムのUI用パラメータはお好みで調整しましょう。 ​ カメラを複数扱うことで表現の幅が広がりますが、処理的には重いものになるため多様しないように気をつけましょう。 4-2 アイテム情報の表示 照準が合っているアイテムの名前を表示したり、調べたアイテムの説明文を表示するためのUIを実装しましょう。 ​ ​ まずはアイテム欄と同じようにCanvasの子オブジェクトに空オブジェクトを追加してください。名前はSearchUIにしておきます。 SearchUIの子オブジェクトにImageを追加してください。 ​ 名前はSearchBGにして、半透明の黒い四角形になるようにColorを調整しましょう(自分で用意したウィンドウ素材を使うのもOK) ​ 【サンプルの入力例】 PosX : 0 PosY : -170 Width : 300 Height : 60 Color : R=0 G=0 B=0 A=200 SearchBGの子オブジェクトに「UI」→「TextMeshPro」を選択して、名前をSearchTextにしましょう。 ​親子関係が少しややこしいので、画像の通りに設定してください。 SearchTextを以下のように設定しましょう。 ​ ​【サンプルの入力例】 PosX : 0 PosY : 0 Width : 600 Height : 60 Text Input : アイテムの名前(仮なので適当でOK) Font Asset : Corporate-Mincho-ver3 SDF Font Size : 34 Alignment : 中央 UIを更新する処理を書いていきます。 UI_Searchスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; // TextMeshProを扱うときに必要 public class UI_Search : MonoBehaviour { [SerializeField] GameObject SearchObject; RectTransform m_searchObjectRectTransform; [SerializeField] TextMeshProUGUI SearchText; [SerializeField] Vector2 NameSize, MessageSize; // メッセージ自動非表示 const float AUTO_OFF_TIME = 3.0f; bool m_isAutoOff = false; void Awake() { // RectTransformを取得しておく m_searchObjectRectTransform = SearchObject.GetComponent(); // 最初は非表示 SearchObject.SetActive(false); } // UIを表示&更新 // mode=false…名前表示モード mode=true…説明文表示モード public void SearchUI_On(string text, bool mode) { if (m_isAutoOff) { return; } // テキストを表示 SearchObject.SetActive(true); SearchText.text = text; // モードに応じて背景のサイズを変える if (mode) { m_searchObjectRectTransform.sizeDelta = MessageSize; } else { m_searchObjectRectTransform.sizeDelta = NameSize; } // Invokeをキャンセル CancelInvoke("SearchUI_Off"); } // UIを非表示にする public void SearchUI_Off() { SearchObject.SetActive(false); m_isAutoOff = false; } // 自動でオフにする public void AutoOff() { Invoke("SearchUI_Off", AUTO_OFF_TIME); m_isAutoOff = true; } } Invoke関数を用いてメッセージを自動で非表示にする処理は、アイテムを3つ所持した状態で4つ目を拾おうとした際の「これ以上アイテムを持てません!」というテキストを自動で非表示にするためのものです。 ​ 後は適切なタイミングで関数を呼ぶだけになります。 GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // アイテム欄のUI [SerializeField] UI_Item ItemUI; // アイテム名表示のUI [SerializeField] UI_Search SearchUI; public UI_Search GetSearchUI() { return SearchUI; } // アイテムを取得する // アイテム欄に空きがあったらtrue なかったらfalseを返す public bool GetItem(int getItemID) { ​~後略~ 次にItemObject スクリプトを開いて、 赤い部分のコード を追加してください。 ~前略~ // アイテムを調べた時の仮想関数 public virtual void ItemCheck() { // 調べた時の処理を行う ItemGet(); } // アイテムを調べた時の基本処理 public void ItemGet() { // 調べられない状態 if (IsCheck) { return; } if (ItemID == -1) { // アイテムを調べた // デバッグ用 アイテム名と説明文をコンソールに出力 //Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation); // 獲得できないアイテムなので説明を表示 m_gameManager.GetSearchUI().SearchUI_On(Explanation, true); m_gameManager.GetSearchUI().AutoOff(); } else { // ② アイテムを取得 // 【ヒント】ゲームマネージャーのGetItem関数を使おう bool isGet = m_gameManager.GetItem(ItemID); // アイテム欄に空きがあったかどうかで分岐 if (isGet) { // アイテムを獲得できた // デバッグ用 獲得したアイテム名をコンソールに出力 //Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得"); // 自分が台に置かれたアイテムだった場合、土台の設定も変更 if (m_platformObj != null) { m_platformObj.GetComponent().ItemTook(); m_platformObj = null; } // アイテム名を削除する m_gameManager.GetSearchUI().SearchUI_Off(); // 自身を削除する Destroy(gameObject); } else { // アイテム欄がいっぱいだった // デバッグ用 //Debug.Log("アイテム欄がいっぱいです"); // アイテムがいっぱいなので拾えない m_gameManager.GetSearchUI().SearchUI_On("これ以上アイテムを持てません!", true); m_gameManager.GetSearchUI().AutoOff(); } } } // アイテムを捨てた時の処理 public void ItemDrop(Vector3 playerVelocity) { ​~後略~ ~前略~ // アイテムを使用した時の仮想関数 public virtual void ItemUse() { } // 自分にカーソルが合った時 public virtual void StartSelect() { // 調べられない状態 if (IsCheck) { return; } // アウトラインを表示する m_outline.enabled = true; // アイテム名を表示 string name; if (ItemID == -1) { // 調べる系のアイテムなので指定した名前を使う name = Name; } else { // 獲得できるアイテムなのでアイテムデータベースから名前を引っ張ってくる name = m_gameManager.GetItemData().Items[ItemID].ItemName; } m_gameManager.GetSearchUI().SearchUI_On(name, false); } // 自分がカーソルから外れた時 public virtual void EndSelect() { // アウトラインを削除する m_outline.enabled = false; // アイテム名を非表示にする m_gameManager.GetSearchUI().SearchUI_Off(); } // アイテムを置いた時の処理 public void ItemPlaced(GameObject platform) ​~後略~ SearchUIオブジェクトにUI_Searchスクリプトをアタッチして、パラメータを設定しましょう。 ​ SearchObjectにはSearchBG(文字の背景)を、SearchTextにはSearchText(文字のテキストオブジェクト)を設定しましょう。 Name Sizeは名前を表示する時の背景の大きさ、Message Sizeは説明文を表示する時の背景の大きさです。 Name SizeはX=300 Y=60に、Message SizeはX=600 Y=100に設定してください。 GameManagerのGameManagerコンポーネントにSearchUIの項目が追加されているので、先ほどアタッチしたSearchUIを設定しましょう。 これでアイテムにカーソルを合わせた際に名前が表示され 、調べた際に対応した説明文が表示されるようになります。 アイテム欄がいっぱいの状態でアイテムを取得しようとした場合は「これ以上アイテムを持てません!」と表示されます。 ​ 実際にゲームを動かして確認してみましょう。 4-3 操作説明の表示 このゲームは少し操作が複雑なので、今できる動作が何か視覚的にわかるようにしましょう。 ​ ​(執筆中!)

  • 3D脱出ゲーム編 Lesson3「ギミックを実装しよう」 | Unity1gc2

    3D脱出ゲーム編 Lesson3 ギミックを実装しよう 3-1 土台の実装 3-1 土台の実装 いよいよゲームの「遊び」の部分である謎解きを実装していきます。前半はコードが多く大変かもしれませんが、後半はほとんどコードを流用するので頑張りましょう。 ​ まずは謎解きの鍵であるアイテムを置く台を実装します。 アイテムを持った状態でXボタン(Iキー)を押すことで、持っているアイテムを台に置くことができます。台に置いたアイテムが正解と一致していた場合、ギミックを解除できます。 ​ ​ 書斎(玄関から一番遠い部屋)に台を置くための本棚を設置してください。本棚と本をCtrlキーでまとめて選んでからコピーしましょう。 追加するのは本棚に限らず机などでも構いません。台を置く場所が作成できればOKです。 本棚のスペースに台を設置しましょう。 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」と選択して、granite_panel をシーン上にドラッグ&ドロップしてください。 台を本棚に設置した場合は、本棚に収まるようにサイズを調整しましょう。 ​ 黄色い本に対応していることがわかりやすいように、マテリアルは本と同じもの「gold」に変更しておきます。 Xボタン(Iキー)が押されたらアイテムを使用する という処理はPlayerItemスクリプトに既に記述しています。 後はアイテムを「使われる側」である土台のスクリプトを書いていきましょう。 新しくItemPlatformスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemPlatform : ItemObject // 【重要!】ItemObjectを継承する { [SerializeField, Header("土台用パラメータ")] int AnswerItemID = -1; // 答えのアイテムID // 答えのIDを変更 public void SetAnswerID(int id) { AnswerItemID = id; } bool m_isItemPlaced = false; // アイテムが置かれているならtrue int m_placedItemID = -1; // 今置かれているアイテムのID // 置かれているアイテムが正解ならtrueになる bool m_isAnswer = false; public bool GetIsAnswer() { return m_isAnswer; } // アイテムを使用した時の処理 public override void ItemUse() { // アイテムが既に置かれているなら中断 if (m_isItemPlaced) { return; } // ゲームマネージャーの取得 GameManager gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); // 選択中のアイテムを取得する int selectItemID = gameManager.GetItemID(gameManager.GetSelectItemNo()); // 選択中のアイテムがないなら中断する if (selectItemID == -1) { return; } // アイテムを設置 // ※後で記述 // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる m_placedItemID = selectItemID; m_isItemPlaced = true; // 答えが合っているか確認する if (AnswerItemID == m_placedItemID) { m_isAnswer = true; } } // アイテムを取る public void ItemTook() { // パラメータを初期化 m_placedItemID = -1; m_isAnswer = false; m_isItemPlaced = false; } } 【プログラムの解説】 ​・クラスを ItemPlatform : ItemObject のように宣言することで、ItemPlatformがItemObjectを継承 することができます。 PlayerItemでItemUse関数を呼び出しているこの時点では、この関数を実行すると何が起きるかはわかっていません。「使う対象が何かは知らないけど、とりあえず持っているアイテムを使います!」と言っているイメージです。 アイテムを使われた側が土台でも扉でも、アイテムを使われた側の挙動はアイテムを使われた側のスクリプトで決めます。 ですが事前に「アイテムを使われた時の関数の名前」だけは決めておかないと、中身を知らずに呼び出そうにも呼び出せません。それを決めるのがItemObjectクラスのItemUse関数です。 ​ 関数の前に virtual とついている点に注目してください。これがついている関数は仮想関数 と呼び、この関数は継承先のクラスで関数の中身を上書きすることができます。 また、関数の中身が一切ありませんが、これは派生先のクラスでそれぞれ中身を決めるので、ここでは空白になっています。このように宣言のみしてある関数を純粋仮想関数 と呼びます。 今回はItemObjectクラスを継承したItemPlatformクラスで、ItemUse関数の中身をオーバーライド (上書き)しています。 例えば足場以外にも、アイテムを使うことでアクションが起きるオブジェクトを実装したい時に簡単に実装することができます。サンプルでは実装していませんが、オリジナルの謎解きを実装する際に活用してみてください。 この時点ではよくわからないかもしれませんが大丈夫です。この後たびたび使うので実際に使って慣れていきましょう。 ​ ​ ​ コードがかけたら保存して、土台にItemPlatformスクリプトをアタッチしてください。 継承した場合、基底クラス(継承元)のパラメータも表示されます。 ItemPlatformスクリプトはItemObjectを継承しているので、同じようにItemIDやNameなどのパラメータが表示されます。さらにその下にItemPlatformのパラメータであるAnswerItemIDが表示されます。 ​ NameとExplantionの内容は自由に入力してください。AnswerItemIDは黄色い本のIDである「0」にしておきましょう。 ​ Outlineスクリプトも忘れずにアタッチしておいてください。 【サンプルの入力例】 Name :黄色い台 Explanation :黄色い台がある… 何か置けそうだ AnswerItemID:0 ゲームを実行して、黄色い土台が調べられることを確認してみてください。 ​ アタッチしたのはItemPlatformスクリプトですが「カーソルが合った状態で調べると詳細表示」という、継承したItemObjectクラスの挙動をしています。これでItemObjectの挙動を引き継ぎつつ、AnswerItemIDという土台用のパラメータを追加することができました。 // ※後で記述 となっていた、アイテムを置く処理を実装しましょう。 ​ まずは置かれる側であるアイテムが「自分が土台に置かれたとき、自分が置かれている台を記憶する」「土台に置かれている状態で取得されたとき、自分が置かれていた台の状態を初期化する」処理を実装します。 ​ ItemObjectスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ public void SetIsCheck(bool flag) { IsCheck = flag; } // ゲームマネージャー GameManager m_gameManager; GameObject m_platformObj = null; // 自分が置かれている土台 void Awake() { // アウトラインの初期化 m_outline = GetComponent(); ​~後略~ ~前略~ // アイテム欄に空きがあったかどうかで分岐 if (isGet) { // アイテムを獲得できた // デバッグ用 獲得したアイテム名をコンソールに出力 Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得"); // 自分が台に置かれたアイテムだった場合、土台の設定も変更 if (m_platformObj != null) { m_platformObj.GetComponent().ItemTook(); m_platformObj = null; } // 自身を削除する Destroy(gameObject); } else ​~後略~ ~前略~ } // 自分がカーソルから外れた時 public virtual void EndSelect() { // アウトラインを削除する m_outline.enabled = false; } // アイテムを置いた時の処理 public void ItemPlaced(GameObject platform) { // 自分が置かれている土台を保存する m_platformObj = platform; } } ​ 自分が土台に置かれたときにその台を記憶し、自分が取得されたときにm_platformObjがnullではない(= 自分が土台に置かれている)場合は、その土台にItemTook関数を実行させることで初期化しています。流れは少し複雑ですが、完璧に理解しなくても大丈夫です。 ​ ​ 次はアイテム設置の肝である「アイテムを生成する」処理を実装します。アイテムを捨てる処理と似ていますが、細かいところが違うので注意しましょう。 GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // アイテム欄のIDをリセット ItemID[SelectItemNo] = -1; } // アイテムを置く public void ItemPlaced(GameObject platformObj) { // 選んでいるアイテムIDを取得 int id = ItemID[SelectItemNo]; // 座標を決める Vector3 pos = platformObj.transform.position; // アイテムを生成 回転はプレハブの回転を使用 GameObject placedItem = Instantiate(Item_Data.Items[id].ItemPrefab, pos, Item_Data.Items[id].ItemPrefab.transform.rotation); // 生成したアイテムに設置された土台を教える placedItem.GetComponent().ItemPlaced(platformObj); // 生成したアイテムの親オブジェクトを土台にする placedItem.transform.parent = platformObj.transform; // 所持品から番号を削除 ItemID[SelectItemNo] = -1; } void Update() { // Bボタンで捨てる ​~後略~ 【プログラムの解説】 ・transform.parent で対象の親オブジェクトの情報を取得、変更できます。 ここでは生成したアイテムの親オブジェクトを土台に変更しています。子オブジェクトは親オブジェクトについていくため、土台が移動した時にアイテムも同じように移動するようになります。 ​ ​ 最後にItemPlaced関数を呼ぶ処理を追加しましょう。 ​ ItemPlatformスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // 選択中のアイテムがないなら中断する if (selectItemID == -1) { return; } // アイテムを設置 gameManager.ItemPlaced(gameObject); // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる m_placedItemID = selectItemID; m_isItemPlaced = true; ​~後略~ コードがかけたら保存して、ゲームを実行してみてください。 ​ 黄色い本を拾ってから、黄色い台にカーソルを合わせてXボタン(Iキー)を押してみましょう。黄色い本を設置できたらOKです。 (場所や回転がおかしいですが、今は気にしないでください) 土台が正解のオブジェクト(黄色い本)が置かれたことを認識できているか確認してみましょう。 ​ ​ インスペクター右上にある3つの点があるボタンをクリックして、デバッグモードにしてみてください。デバッグモードではprivateな変数の中身を確認することができます 。ただし変更はできません。 デバッグモードでパラメータを確認しながら、土台にアイテムを置いてみてください。 アイテムを置く前はIsItemPlaced(アイテムが置かれているかどうか)がfalseですが、アイテムを置くとtrueになります。 ​ 置かれたアイテムが答えと一致しているならIsAnswer(置かれているアイテムが正解かどうか)もtrueになります。 パラメータの変化を確認できたら、デバッグモードは解除しておいてください。デバッグモードは文字通りデバッグには便利ですが、項目が多いので普段は通常モードにしておくことをオススメします。 ​ ​ これでアイテムを置いて、置かれたアイテムが正解かどうか判定する処理が完成しました。 ​ しかし置かれたアイテムの位置や角度がおかしいので、調整できるようにしましょう。座標は土台側だけでなく、アイテム側でも個別に調整できるようにします。 ​ ItemDataスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; // アイテム用のデータベース [CreateAssetMenu(fileName = "ItemDataBase", menuName = "CreateItemDataBase")] public class ItemData : ScriptableObject { // アイテム情報の構造体 [System.Serializable] public struct Item { public string ItemName; // アイテム名 [Multiline(2)] public string ItemExplanation; // アイテム欄で表示する説明文 public GameObject ItemPrefab; // アイテムを捨てた時に生成するオブジェクト // 設置用パラメータ public Vector3 ItemPivot; // アイテムを置いたときに座標を補正する量 } // アイテムリストの可変長配列 public Item[] Items; } パラメータを追加したことで、座標の補正量をアイテム別に指定できるようになりました。黄色い本では使いませんが、後ほど他のアイテムを作るときに使います。 次は土台側から調整できるようにしましょう。ItemPlatformスクリプトに赤い部分のコード を追加してください。 ~前略~ // 置かれているアイテムが正解ならtrueになる bool m_isAnswer = false; public bool GetIsAnswer() { return m_isAnswer; } [SerializeField] Vector3 AddPos, AddRot = Vector3.zero; // アイテムを置く座標と回転に加算する値 // アイテムを使用した時の処理 public override void ItemUse() { ​~後略~ 次は座標や回転の補正をする処理を追加します。 ​ GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // アイテムを置く public void ItemPlaced(GameObject platformObj, Vector3 addPos, Vector3 addRot ) { // 選んでいるアイテムIDを取得 int id = ItemID[SelectItemNo]; // 座標を計算 Vector3 pos = platformObj.transform.position + addPos ; pos += Item_Data.Items[id].ItemPivot; // アイテムを生成 回転はプレハブの回転を使用 GameObject placedItem = Instantiate(Item_Data.Items[id].ItemPrefab, pos, Item_Data.Items[id].ItemPrefab.transform.rotation); // アイテムを回転させる placedItem.transform.Rotate(addRot, Space.World); // 生成したアイテムに設置された土台を教える placedItem.GetComponent().ItemPlaced(platformObj); // 生成したアイテムの親オブジェクトを土台にする placedItem.transform.parent = platformObj.transform; // 所持品から番号を削除 ItemID[SelectItemNo] = -1; } ​~後略~ 【プログラムの解説】 ​ ・Transform.Rotate関数は「オブジェクトを第一引数分回転を加算する」という簡単な関数です。 第二引数でローカル座標とワールド座標、どちらを基準にするか決めることができます(よくわからない人はとりあえずワールド座標にしておくとわかりやすいはずです) ​ ItemPlaced関数に引数を追加したので、ItemPlatformスクリプトでエラーが出ているはずです。 ​ ItemPlatformスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // 選択中のアイテムがないなら中断する if (selectItemID == -1) { return; } // アイテムを設置 gameManager.ItemPlaced(gameObject, AddPos, AddRot ); // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる m_placedItemID = selectItemID; m_isItemPlaced = true; ​~後略~ コードが書けたら保存して、各パラメータを調整しましょう。 ​ 黄色い土台のAddPosを X=0 Y=0.12 Z=0.0 にしてください。これでアイテムが置かれる場所が少し上になります。 回転は元からプレハブの回転を使用するようにしているので、プレハブの回転を調整しましょう。 ​ 黄色い本のプレハブを開いて、Rotationを X=0 Y=0 Z=20 にしてください。 プレハブを変更できたら、ゲームを実行して黄色い本が正常に設置されることを確認してみてください。 これでアイテムを置ける台が完成しました。これが脱出のカギになっていきます。 3-2 絵画の実装 3-2 絵画の実装 「黄色い台に黄色い本が置かれたので正解!」というところまで進みました。 次は対応した土台に置かれたアイテムが正解なら落ちる絵画を作成しましょう。 ​ ​ 「Model」→「Paintings」→「PaintingCollection1」→「Prefabs」と選択して、絵画のモデルをシーン上に追加してください (PaintingCollection2でも構いません。絵柄が違うだけです) ​ 追加した絵画のTransformを調整してください。 ​ ​【サンプルの入力例】 Position X=-17.6 Y=9 Z=-9.8 Rotation X=0 Y=90 Z=0 ​ Scale X=15 Y=15 Z=15 絵画はギミックなので、ヒエラルキー内でもGimmickの子オブジェクトに移動させておきましょう。 絵画は「カーソルを合わせると調べられる」部分は今までと同じですが「対応した土台に置かれているアイテムが正解なら落下する」という固有の処理があります。 ItemPlatformと同じようにItemObjectクラスを継承して、アイテムを調べる処理は流用しましょう。 ​ ​ 新しいスクリプトItemGimmickスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemGimmick : ItemObject // 継承する { [SerializeField, Header("正解判定オブジェクト用")] ItemPlatform[] AnswerPlatforms; // 対応する土台 [SerializeField, Multiline(3)] string AnswerExplanation; // 正解時に説明文を変化させる // 正解時の反応の種類 public enum AnswerPattern { enAnswer_Drop, // 落下する } [SerializeField] AnswerPattern Answer_Pattern; // 正解時の反応 ​ bool m_isAnswer = false; // 正解済みかどうか void Update() { // 正解済みなら実行しない if (m_isAnswer) { return; } // 正解チェック bool answer = true; foreach (ItemPlatform platform in AnswerPlatforms) { // 1つでも不正解があるならこの先で正解処理を実行しない if (platform.GetIsAnswer() == false) { answer = false; } } // 正解なら設定した処理を行う if (answer) { Answer(); m_isAnswer = true; } } void Answer() { // 正解時の処理 switch (Answer_Pattern) { case AnswerPattern.enAnswer_Drop: // 物理演算の有効化 Rigidbody rb = GetComponent(); rb.isKinematic = false; // 前方に飛ばす rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse); // 回転をかける rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse); // 説明欄の更新 // 後で入力 break; } } } 【プログラムの解説】 ​・ItemPlatform[] AnswerPlatforms のように、対応した土台を指定する部分は可変長配列になっています。これは後ほど複数の土台を参照するギミックに対応するために配列にしています。 ​ SerializeField がついているためインスペクターに表示されますが、配列は+ボタンを押す(あるいは要素数を直接入力する)ことで、要素数を増やすことができます。 ・foreach はUnityではなくC++など様々な言語にあるループ文の一種です。 配列の全ての要素にアクセスしたい時に使用します(Wikipedia ) [サンプル] foreach (型 変数 in 配列) { 処理 } ​ ​ ​ 最後の説明欄変更の // 後で入力 となっている部分を完成させましょう。ItemGimmickはItemObjectを継承していますが、ItemGimmickからItemObject内の変数であるExplanation(説明文)にアクセスしようとするとエラーが出てしまいます。 派生クラスから基底クラスの変数や関数にアクセスしたい場合、対象にprotected というアクセス修飾子をつける必要があります。 protectedはpublicとは異なり、外部に公開されることはありませんが「自クラスか派生クラスからはアクセスできる 」という特徴があります(privateでは自クラスからしかアクセスできない) ​ ItemObjectスクリプトを開いて、赤い部分のコードを追加してください。 ~前略~ [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] protected string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; ​~後略~ これでItemGimmickからでも説明文を変更することができます。ItemGimmickスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // 前方に飛ばす rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse); // 回転をかける rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse); // 説明欄の更新 Explanation = AnswerExplanation; break; } } } コードが書けたら保存して、Paintingの子オブジェクトであるFrameに必要なコンポーネントをアタッチしていきましょう(Paintingにアタッチしないように注意! ) FrameにRigidbodyをアタッチしてください。 ​ 絵画が勝手に落ちてしまわないように、Is Knematicにチェックを入れて物理演算を無効化しましょう。 Collision Detection は移動の計算方式を設定できます。初期値はDiscreteですが、Continuousに変更することで少し正確な判定を取ることができます。オブジェクトが貫通しにくくなるなどの効果がありますが、Discreteより処理が重いので扱いには注意が必要です。 FrameにBoxCollider、Outline、ItemGimmickをアタッチしてください。 ItemGimmickのパラメータを設定しましょう。 Name(名前)、Expla nation(説明文)、AnswerExplanation(落下後の説明文)を設定してください。内容は自由です。 【サンプルの入力例】 Name :絵画 Explanation :印象的な絵画が飾ってある AnswerExplanation:絵画が落ちてきた… AnswerPlatforms は要素数を1にして、ヒエラルキーから黄色い土台をElement0へドラッグ&ドロップしてください。 これで落ちる絵画が完成しました。ゲームを実行して確かめてみましょう。 調べるとコンソールに説明文が表示されます。黄色い台に黄色い本を置くと絵画が落下し、説明文が変化することを確認してください。 絵画があった場所に扉のパスワードを隠しておく予定ですが、今は絵画が落下することを確認できればOKです。 3-3 アイテムの追加 3-3 アイテムの追加 ItemPlatformとItemGimmickを使って2つ目のギミックを作ってみましょう。 ​ 2つ目のギミックの構成は以下のようになっています。 ​ ・壁に貼られた絵の通りに4つの台にアイテムを置くと棚が動く ・3種類からランダムで答えが変わる ​ ​ まずはギミックの解除に必要なアイテムを作成しましょう。必要なアイテムは「青い本」「赤い本」「緑の本」「赤の宝玉」「青の宝玉」「黄の宝玉」「緑の宝玉」の7つです。 数は多いですが、複製するので手間はあまりかかりません。 ​ 黄色い本をベースに本の種類を増やしましょう。黄色い本を選んだ状態でCtrl+Dを押して複製しましょう。複製した本の名前をItem_BlueBookに変更します。 3Dアクションゲーム編でも行った措置ですが、Item_BlueBookのプレハブ化を解除しておきましょう。現在のItem_BlueBookはPrefabフォルダ内の黄色い本のプレハブと紐づいた状態ですが、その関係を解除するものになります。 ​ ヒエラルキー内のItem_BlueBookを右クリックして「Prefab」→「Unpack Completely」を選択してください。 MaterialsのElement1を「canvas_book_2_d」に変更してください。これで背表紙の色を青色に変更できます。 黄色い本のItemIDは0なので、その次の1番を青い本のIDとして使用しましょう。ItemObjectコンポーネントのItemIDを1にしてください ​。 青い本の場所をお好みの座標に変更してください。黄色い本のある食堂とは違う部屋に置くのがオススメです。 【サンプルの入力例】 Position X=120.6 Y=7.7 Z=56 Rotation X=0 Y=0 Z=90 ​ 青い本を置けたら、Prefabフォルダ内にItem_BlueBookをドラッグ&ドロップしてプレハブ化しましょう。 ​ ItemDataBaseを選択して、Element1に青い 本のデータを記述しましょう(アイテム名や説明文はお好みで調整しても構いません) ​ 【サンプルの記入例】 ・ItemName :青い本 ・​ItemExplanation :青い表紙の本 中身は漫画のようだ ・ItemPrefab :Item_BlueBook(設定方法は右端の◎ボタンをクリックするか、2-4 を 参照してインスペクターをロック) これで青い本の追加が完了しました。ゲームを実行して青い本を拾ったり捨てたりできるか確認してみましょう。青い本を拾うとGameManagerのItemIDに、青い本のIDである1が入ることが確認できます。 ​ 同じ流れで赤い本と緑の本も追加してみましょう。 ​ ​ 【本追加の流れ】 ① 本のオブジェクトをコピー ② プレハブ化を解除 ③ 表紙のマテリアルを変更(赤い本は「canvas_book_1_d」、緑の本は「canvas_book_3_d」) ④ IDを設定(他と被らないように! ) ⑤ 座標を変更 ⑥ プレハブ化する ​⑦ ItemDataBaseを設定 ​ 【サンプルの記入例 赤い本】 ・ItemName :赤い本 ・​ItemExplanation :赤い表紙の本 中身は小説のようだ ​ ・Position :X=43 Y=3 Z=35 ・Rotation :X=0 Y=45 Z=90 ​・Item ID :2 ​ 【サンプルの記入例 緑の本】 ・ItemName :緑の本 ・​ItemExplanation :緑の表紙の本 中身は絵本のようだ ​ ・Position :X=137 Y=5.5 Z=-18 ・Rotation :X=0 Y=-30 Z=90 ​・Item ID :3 既にプレハブ化されているオブジェクトをプレハブ化しようとすると、以下のようなウィンドウが表示されます。この場合「Original Prefab」を選択してください。 ちなみに「Prefab Variant」は今あるプレハブをベースに新しいプレハブを作成するというもので、プレハブの派生を作るイメージです。派生したプレハブを編集してもオリジナルのプレハブに影響はありませんが、オリジナルのプレハブを編集すると派生したプレハブは影響を受けます。 Unity Tips! 追加した赤い本と緑の本を正常に取得、捨てることができるか確認しておいてください。GameManagerのItemID内にそれぞれの本に対応したIDが表示されます。 全ての本のプレハブを選択して座標を全て0、回転をX=0 Y=0 Z=20 にしておいてください 。 アイテムを設置する際にプレハブの回転を参照しているため、全ての本で回転を合わせておかないと「黄色い本は正常に設置できるが、赤い本は異常な角度で設置される」といった現象を起こす可能性があります。 次は4色の宝玉を用意します。最初は「青の宝玉」を作りましょう。 ​ ​ 「3D Object」→「Sphere」を選択して、球体を追加してください。 名前を「BlueSphere」にして、タグを「Item」にしておきます。​座標と大きさを調整してください。 【サンプルの座標】 Position X=86 Y=5.7 Z=149 ​ Rotation X=0 Y=0 Z=0 Scale X=1.2 Y=1.2 Z=1.2 アイテムがプレイヤーに衝突しないように、レイヤーをDropItemに変更しておいてください(レイヤーについては2-4 参照) 青いマテリアルを作成します。 ​ 新しく「Material」フォルダを作成してください。 Materialフォルダ内に新しいマテリアルを作成してください。 ​ ​ Base Mapを青色に、Metallic Map(金属感)を0.8に、Smoothness(滑らかさ)を1に設定しましょう。設定ができたらBlueSphereにマテリアルをドラッグ&ドロップして貼り付けてください。 BlueSphereにRigidbodyをアタッチしてください。 ​ ​ Drag(抵抗)を2にしましょう。Dragの値を上げることでオブジェクトに抵抗がかかるようになります。空気抵抗などの表現に使用します。今回はオブジェクトが球体なので、抵抗を設定することで遠くに転がっていかないようにしています(Dragの詳細 ) ​ また、最初は物理演算を無効にしたいのでIs Kinematicにチェックをいれておきます。 BlueSphereにOutlineとItemObjectをアタッチしてください。 ​ ​ ItemObjectのItemIDは本のIDと被らないように4にしておきましょう。ItemDataBaseにはプロジェクト内のItemDataBaseを設定します。 これでアイテムの準備ができたので、BlueSphereをプレハブ化してください。 ItemDataBaseを開いて、青の宝珠の情報を入力してください。 ​ ​ 【サンプルの記入例】 ・ItemName :青の宝珠 ・​ItemExplanation :青色の宝玉 ガラスでできている ​・Item Pivot :X=0 Y=0.6 Z=0 これで青の宝玉を拾ったり、捨てたりすることができます。ゲームを実行して確認してみましょう。 青の宝玉をベースに赤、黄色、緑の宝玉も作成しましょう。本の種類を増やした時と流れは同じです。 BlueSphereをコピーして、名前を「RedSphere」に変更します。位置は青の宝玉の隣にしておきましょう。 【サンプルの座標】 ​Position X=86 Y=5.7 Z=144 青の宝玉のマテリアルを複製して色を変更した、赤の宝玉用のマテリアルを作成してください。マテリアルができたら、RedSphereにドラッグ&ドロップして貼っておきましょう。 RedSphereにアタッチされているItem ObjectのIDを5にしておいてください。 ここまでできたらRedSphereをプレハブ化しましょう。RedSphereはBlueSphereをコピーして作ったため、BlueSphereのプレハブから派生したオブジェクトとして扱われています。RedSphereを右クリックして「Prefab」→「Unpack Completely」を選択して、BlueSphereとの関連付けを解除しましょう。 ​ RedSphereをPrefabフォルダ内にドラッグ&ドロップしてプレハブ化しましょう。 ​ 青の宝玉を元に、ItemDataBaseに赤の宝玉のデータを入力してください。 ​ ​ 【サンプルの記入例】 ・ItemName :赤の宝珠 ・​ItemExplanation :赤色の宝玉 ガラスでできている ​・Item Pivot :X=0 Y=0.6 Z=0 赤の宝玉の追加と同じ 手順で、黄色と緑色の宝玉も追加してください​。ItemIDの重複には注意しましょう。 ​ 【サンプルの記入例 黄の宝玉】 ・ItemName :黄の宝珠 ・​ItemExplanation :黄色の宝玉 ガラスでできている ​・Item Pivot :X=0 Y=0.6 Z=0 ​ ・Position :X=86 Y=5.7 Z=129 ​・Item ID :6 【サンプルの記入例 緑の宝玉】 ・ItemName : 緑 の宝珠 ・​ItemExplanation : 緑 色の宝玉 ガラスでできている ​・Item Pivot :X=0 Y=0.6 Z=0 ​ ・Position :X=86 Y=5.7 Z=123.8 ​・Item ID :7 ItemDataBase内に今まで作成した8つのアイテムのデータがしっかり入力できているか確認しておきましょう。 追加した4つの宝玉を拾う&投げる処理が正常に動作するか確認してみてください。 3-4 2つ目のギミック 3-4 2つ目のギミック 追加した本と宝玉を使うギミックを作成しましょう。 土台や正解判定の処理は1つ目のギミックで使ったものを流用するため、コード量は少なめです。 ​ ​ まずは土台を置くための棚を設置しましょう。この棚は隠し部屋の存在を隠す役割も持っています。 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Storage」内の「gabinet.001」をシーン上にドラッグ&ドロップして、Transformを調整してください。 【Transformの設定】 Position : X=89.5 Y=0.2 Z=136.75 Rotation : X=0 Y=-90 Z=0 Scale : X=3.04 Y=5.7 Z=4 追加した棚のLayerを「Ignore Raycast」に変更してください。 ​ LayerをIgnore Raycastに変更したオブジェクトにはレイ(光線)がヒットしなくなります 。アイテムを調べる判定にレイを用いていますが、土台よりも先に棚にレイがヒットしてしまうと、土台を調べることができなくなってしまいます。レイの邪魔になりそうなオブジェクトのレイヤーはIgnore Raycastにしておきましょう。 棚に4つの台を設置しましょう。 ​1つ目のギミックで作成した黄色い台をコピーしてください。 複製した台を棚の子オブジェクトにしてください。設置するアイテムが正解すると棚が移動するため、棚の移動に台がついていくようにしておきます。 台のTransformやマテリアルを調整しましょう。 ​ ​【サンプ ルの入力例】 Position : X=0.55 Y=1.58 Z=0.3 Rotation : X=0 Y=90 Z=0 Scale : X=0.15 Y=0.17 Z=0.4 ​ ​マテリアル(Element0): granite_2_d 台にアタッチされているItemPlatformコンポーネントのパラメータを調整してください。 ​ Answer Item IDは後ほどスクリプトでランダムに変更するのですが、まずはデバッグ用に4(青の宝玉のID)を設定しておきましょう。 ​ 【サンプルの入力例】 Name :石の台 Explanation :石でできた台がある… 何か置けそうだ AnswerItemID:4 Add Pos : X=0 Y=0.2 Z=0 Add Rot :X=0 Y=180 Z=0 ItemPlatformの設定が終わったら、石の台にカーソルを合わせてIキーを押すことでアイテムを設置できるか確認しましょう。 残り3つの石の台も設置します。石の台を複製して、座標とAnswerItemIDを調整するだけです。 ​ ​【右上の台】 Position : X=-0.55 Y=1.58 Z=0.3 AnswerItemID:5 ​【左下の台】 Position : X=0.55 Y=0.84 Z=0.3 AnswerItemID:6 ​【右下の台】 Position : X=-0.55 Y=0.84 Z=0.3 AnswerItemID:7 4つの土台全てに正解のアイテムが置かれたら、棚が移動するようにしましょう。 ​ 棚の移動はスクリプトで行っても良いのですが、復習も兼ねてアニメーションで移動させてみましょう(スクリプトを書くより簡単というのもあります) ​ ​ Animationフォルダを開いて、Animationを作成してください。名前はGabinetWaitにしておきましょう。 ヒエラルキー内にある棚のオブジェクトに対して、作成したGabinetWaitをドラッグ&ドロップしてください。棚と同じ名前のAnimator Controllerが自動で作成されます。 GabinetWaitをダブルクリックしてAnimationウィンドウを開いてください。 ​ Animationウィンドウを開いた状態で棚を選択すると「Add Property」ボタンをクリックできるようになります。 「Add Property」を押して「Transform」→「Position」横の+ボタンをクリックしてください。 ​ これでアニメーション内に座標の項目が追加されます。 GabinetWaitは名前の通り待機中のアニメーションなので、初期座標のままで構いません。 Animationウィンドウ左上のGabinetWaitと表示されている場所をクリックすると「Create New Clip」という項目を確認できます。ここをクリックして、新しいアニメーションを作成しましょう。 ファイル名の項目にはアニメーション名を入力します。今回は移動のアニメーションなので「GabinetMove」にしておきます 。名前を入力できたら保存ボタンをクリックしてください。 GabinetMoveアニメーションには、4つの台に正解のアイテムが置かれた時に移動するアニメーションを設定します 。 ​ 上から2番目のレーンを右クリックすると「Add Key」という項目が表示されます。キーを追加して、棚が奥へ移動するアニメーションを作ってみましょう。移動させるのはX座標だけです。 ​ 【サンプルの入力例】 0フレーム目 : X=89.5 30フレーム目: X=92 60フレーム目: X=111.5 GabinetMoveのLoop Timeにチェックが入っていたら外しておきましょう。Loop Timeのチェックを外すとアニメーションがループしないようになります。 Animator Controllerの設定をしましょう。 ​ 先ほど自動で作成された棚のAnimator Controllerをダブルクリックして、Animatorウィンドウを開いてください。 左上のParametersボタンをクリックして、+ボタンからTriggerを選択してください。名前はMoveにしておきましょう(後でスクリプトで参照するので打ち間違いに注意) ​ Triggerパラメータは名前の通りトリガーのように一瞬だけ有効になるパラメータになります。一瞬だけtrueになって自動でfalseに戻るbool型をイメージすると分かりやすいかと思います。 GabinetWaitを右クリックして「Make Transition」を選択してください。GabinetWaitから矢印を伸ばせるようになるため、GabinetWaitからGabinetMoveにTransition(矢印)を繋げましょう。 作成したTransitionを選択して、以下の措置を行ってください。 ​ ・Has Exit Time(アニメーションの終了まで遷移を待つかどうか)のチェックを外す ・Transition Duration(補間率)を0にする ・Conditions(遷移条件)をMoveにする →Moveトリガーが有効になったらアニメーションが切り替わるようになる 後は4つの台に置かれたアイテムが正解なら、Moveトリガーを有効にして棚を移動させるだけです。正解判定は既に作っているので、追加するのは正解時の動作だけになります。 ​ ItemGimmickスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ [SerializeField, Multiline(3)] string AnswerExplanation; // 正解時に説明文を変化させる // 正解時の反応の種類 public enum AnswerPattern { enAnswer_Drop, // 落下する enAnswer_Move, // 移動する } [SerializeField] AnswerPattern Answer_Pattern; // 正解時の反応 void Update() { ​~後略~ ~前略~ void Answer() { // 正解時の処理 switch (Answer_Pattern) { case AnswerPattern.enAnswer_Drop: // 物理演算の有効化 Rigidbody rb = GetComponent(); rb.isKinematic = false; // 前方に飛ばす rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse); // 回転をかける rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse); // 説明欄の更新 Explanation = AnswerExplanation; break; case AnswerPattern.enAnswer_Move: // 移動アニメーション GetComponent().SetTrigger("Move"); break; } } } 棚に必要なコンポーネントをアタッチしましょう。ItemGimmickとOutlineをアタッチしてください。 ​ ItemGimmickコンポーネントのパラメータを調整しましょう。 ​ ​ 棚は調べられないオブジェクトにしたいので、Is Checkにチェックを入れて調べられないようにしてください。 Answer Platformsの項目を4つにして、子オブジェクトである石の台4つをヒエラルキーからドラッグ&ドロップして指定してください。 ​ Answer_Patternは正解時の動作を指定するので「En Answer_Move」に設定してください。 ここまでできたらゲームを実行しましょう。 左上に青の宝玉 、右上に赤の宝玉、左下に黄の宝玉、右下に緑の宝玉を置いてください。4つの石の台に正しく宝玉を置くと棚が移動するか確認してみましょう。 最後に4つの石の台の答えが複数パターンからランダムに切り替わるようにしましょう。先ほど作成した3冊の本もここで使用します。 ​ まずは答えの配置を示すヒントを壁に設置しましょう。 ​ ヒエラルキーから「3D Object」→「Plane」を選択して、板状のオブジェクトを追加してください。 名前は分かりやすいものにして、Gimmickの子オブジェクトにしておきましょう。 壁に合うようにTransformを調整します。 ​ 【Transitionの入力例】 Position : X=123.9 Y=9.6 Z=-63.74 Rotation : X=90 Y=0 Z=0 Scale : X=1 Y=1 Z=0.6 次にヒントに貼るためのマテリアルを作成しましょう。 答えの配置を示す画像を3種類同梱してあるので、3種類マテリアルを作成します。 ​ Materialフォルダ内に新しいマテリアル「Hint1」を作成してください。 Hint1を選択したら、インスペクター右上の鍵アイコンをクリックしてインスペクターをロックしましょう。ここでインスペクターをロックしておかないと、テクスチャの画像を選択した時にインスペクターがテクスチャのものに切り替わってしまいます。 Hint1のパラメータを設定しましょう。Base MapにSprite内のHint1のテクスチャを、Normal MapにNormalMapのテクスチャをドラッグ&ドロップしてください。 ​ ​ 【サンプルの入力例】 Metallic Map : 0.8 Smoothness : 0.1 Normal Map : 0.8 設定が終わったらインスペクターのロック解除を忘れないようにしてください。 Hint1のマテリアルを複製して、Base MapをHint2とHint3に差し替えただけのマテリアルを作成してください。 ギミックを管理するGimmickManagerを作成しましょう。 GimmickManagerにはゲーム開始時に壁のヒントのマテリアルを変更し、4つの石の台の答えをヒントに合うように設定してもらいます。 GimmickManagerスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GimmickManager : MonoBehaviour { // ギミック2 [SerializeField, Header("ギミック2")] GameObject HintObject; // ヒント用オブジェクト [SerializeField] Material[] HintMaterial; // ヒントのマテリアル [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")] ItemPlatform[] Gimmick2_Platform; // 石の土台 int m_gimmick2No = 0; // パターン番号 // Startより先に実行される関数 private void Awake() { // ギミック2の答えの乱数をふる m_gimmick2No = Random.Range(0, HintMaterial.Length); // ヒントオブジェクトのマテリアルを変更 HintObject.GetComponent().material = HintMaterial[m_gimmick2No]; // 土台に答えを教える(アイテムIDの間違いに注意!) switch (m_gimmick2No) { case 0: Gimmick2_Platform[0].SetAnswerID(2); // 赤い本 Gimmick2_Platform[1].SetAnswerID(7); // 緑の宝玉 Gimmick2_Platform[2].SetAnswerID(4); // 青の宝玉 Gimmick2_Platform[3].SetAnswerID(6); // 黄の宝玉 break; case 1: Gimmick2_Platform[0].SetAnswerID(6); // 黄の宝玉 Gimmick2_Platform[1].SetAnswerID(5); // 赤の宝玉 Gimmick2_Platform[2].SetAnswerID(3); // 緑の本 Gimmick2_Platform[3].SetAnswerID(4); // 青の宝玉 break; case 2: Gimmick2_Platform[0].SetAnswerID(5); // 赤の宝玉 Gimmick2_Platform[1].SetAnswerID(1); // 青い本 Gimmick2_Platform[2].SetAnswerID(4); // 青の宝玉 Gimmick2_Platform[3].SetAnswerID(7); // 緑の宝玉 break; } } } 【プログラムの解説】 ​・Random.Range関数は第一引数から第二引数の範囲で乱数(ランダムな数)を生成する関数です。 ただし注意点が1つあり、引数にfloat型を指定した場合は第二引数(最大値)は戻り値の範囲に含まれますが、int型を指定した場合は第二引数は戻り値の範囲に含まれません ( 参考サイト ) int型で乱数を生成したい場合は、戻り値の範囲に注意してください。 ​ ・MeshRenderer のパラメータであるMaterialを変更することで、マテリアルを動的に変更できます。 ​ ​・ItemPlatform のSetAnswerID関数を使って土台の答えを変更しています。引数には答えとなるアイテムのIDを入れる必要があるのでItemDataBaseのIDと合うようにしましょう。 GameManagerにGimmickManagerコンポーネントをアタッチしてください。 ​ HintObjectには先ほど作成したヒント用のPlaneオブジェクトを指定します。 ​ HintMaterialは要素数を3にして、上から順番にHint1、Hint2、Hint3を指定してください。 Gimmick2_Platformは要素数を4にして、左上の土台、右上の土台、左下の土台、右下の土台の順番に指定してください。 長くなりましたがこれで2つ目のギミックが完成しました。ゲームを実行して確認してみてください ​。 ​ 壁のメモを確認して、メモに合うように石の台にアイテムを置いてみましょう。●は宝玉、□は本を表しています。 壁のヒントは3種類の中からランダムで変化する ので、何回かゲームを実行してギミックの変化をチェックしてみましょう。 ​ ギミックがうまく動かない場合は、GimmickManagerスクリプト内で指定しているアイテムIDが一致しているかどうか、インスペクターに設定した情報が正しいかどうか確認してみてください。 3-5 3つ目のギミック 3-5 3つ目のギミック 3つ目のギミックを作成しましょう。 3つ目のギミックは銀のアイテムを左からしりとりになるように「リンゴ→ゴリラ→ラッパ→パイナップル」と並べることで宝箱が開き、その裏に扉の番号が書いてある、というものです。 スクリプトを流用するので、コード量は少なめです。ギミックはあと少しなので頑張りましょう! ​ まずはカギとなる銀のアイテムを作成しましょう。モデルや設定が違うだけで3-3と流れは同じです。 ​ 「Model」→「Object」内のApple(リンゴのモデル)をシーン内にドラッグ&ドロップしてください。Transitionを調整して、タグを「Item」に変更しましょう。 ​ ​ 【サンプルでの設定】 Position: X=41.2 Y=7 Z=128.5 Scale : X=8 Y=8 Z=8 オブジェクトのレイヤーをDropItemに変更してください。 ​ リンゴは子オブジェクトを持っているため、その子オブジェクトのレイヤーも変更するか選択するウィンドウが表示されます。「Yes, change children」を選択して、子オブジェクトのレイヤーも変更してください。 ​ リンゴを銀色にするために、銀色のマテリアルを用意します。 新しいマテリアルを作成して、名前をSilverにしておきましょう。 ​ SilverマテリアルはBase Mapを灰色に設定して、Metallic MapとSmoothnessを高くすることで金属のような質感を出してみましょう。 ​ Silverマテリアルをリンゴにドラッグ&ドロップして貼っておきましょう。 ​ Unityには様々な形状のコライダー(当たり判定)があります。今回はメッシュの形状に合わせてコライダーを生成するMesh Colliderを使いましょう。 ​ ​ Add Componentから「Mesh Collider」を追加してください。 ​ Mesh ColliderのConvexにチェックを入れておきましょう。 Mesh ColliderのConvexにチェックを入れると、当たり判定の形状が簡略化されます 。 Conve xにチェックが入っていない状態だと、メッ シュの形状に合わせて正確な当たり判定が生成されますが、以下のデメリットがあります。 ​ ・Mesh Collider同士で衝突することができない(貫通する) ・Rigidbodyを使用できない ​ ​ ​ これらの問題が起きた時はConvexにチェックを入れるようにしましょう。 銀のリンゴにRigidbody、ItemObject、Outlineをアタッチしてください。 ​ ​ RigidbodyのIs Kinematicにチェックを入れて物理演算を無効にしましょう。 ItemObjectのItem IDには銀のリンゴを割り当てるIDを指定します(特にオリジナルのアイテムを追加していない場合は8) Item Data Baseにはプロジェクト内にあるItemDataBaseを指定してください。 銀のリンゴをプレハブ化しましょう。プレハブ化を解除していない場合、プレハブの形式を決めるウィンドウが表示されますが「Original Prefab」を選択してください。 ItemDataBaseの項目を増やして、銀のリンゴの情報を入力してください。先ほどItemObjectに設定したIDと一致する場所に入力するようにしてください。 ​ ​ 【サンプルの記入例】 ・ItemName :銀のリンゴ ・​ItemExplanation :銀でできたリンゴ 食べられない ​・Item Pivot :X=0 Y=0.54 Z=0 これで銀のリンゴが完成しました。 しかし、実際にゲーム画面で銀のリンゴを確認すると、銀とはいえない質感になっています。 Unityには反射を表現するReflection Probe(リフレクションプローブ) という機能があります。 公式マニュアル では「全方向に向かってその周囲の球状のビューをキャプチャするカメラのようなもの」と説明されています。リフレクションプローブを用いることで、周囲の状況を反射してマテリアルに映すことができます。 プレイヤーの子オブジェクトに「Light」→「Reflection Probe」を作成してください。 Reflection Probeの座標を原点にしてください。 ​ Typeを「Realtime」に設定しましょう。Refresh Modeを「On Awake」にしておくと、ゲーム開始時に1度だけReflection Probeが初期化されます(「Every Frame」にすることで常に更新できますが、負荷がかなり高いので今回は行いません) Box SizeをX=100 Y=20 Z=100 に設定しましょう。ここではReflection Probeの影響を受ける範囲となる箱の大きさを指定しています。 ​ Resolutionは映り込みテクスチャの解像度です。高いほど綺麗に映り込みますが、小さいアイテムの映り込みなので解像度は低めにしておきましょう。 ゲームを実行するとアイテムに風景が映り込み、銀の質感が出ているはずです。 Reflection Probeをゲーム開始時にのみ更新しているため、よく見ると最初の部屋が映っていますが、リアルタイムで更新するとかなりの負荷がかかるため、あえてこのままにしておきます。 Reflection Probeをより正確にするためには、プレイヤーではなく銀のリンゴ側にReflection Probeを持たせるべきですが、常時更新すると負荷がかなり高いので今回はプレイヤーの子オブジェクトにしました。小さいアイテムのため、反射の雰囲気が出ていればOKとします。 (これらの問題を完璧に解決する場合レイヤーの調整が必要ですが、手間がかかるので解説は割愛します) ​ ​ 残りの銀のアイテムも作成しましょう。銀のラッパだけはヒントのため、台座に接着した状態で出現させます。そのため、必要なのは銀のゴリラと銀のパイナップルです。 ​ 銀のリンゴと同じ手順でアイテムを作成してください。場所はどこでも構いません。 ① 「Model」→「Object」内からモデルをシーン内にドラッグ&ドロップする ​② タグを「Item」に、レイヤーを「DropItem」にする ​③ 銀色のマテリアルを貼る ④ Mesh Colliderをアタッチし、Convexにチェックを入れる ⑤ Rigidbodyをアタッチし、Is Kinematicにチェックを入れる ⑥ ItemObjectをアタッチし、ItemIDとItemDataBaseを設定する(ID重複に注意) ​⑦ Outlineをアタッチする ⑧ オブジェクトをプレハブ化する ​⑨ ItemDataBaseに情報を追加する ​ 【サンプルの記入例 ゴリラ】 ・Item ID : 9 ​ ・ItemName :銀のゴリラ ・​ItemExplanation :銀でできたゴリラ 動くことはない ​・Item Pivot :X=0 Y=0 Z=0 ​ 【サンプルの記入例 パイナップル】 ・Item ID : 10 ​ ・ItemName :銀のパイナップル ・​ItemExplanation :銀でできたパイナップル かなり重たい ​・Item Pivot :X=0 Y=0 Z=0 銀のゴリラのプレハブをダブルクリックして開いて、Y軸周りに90度回転させておいてください。 銀のゴリラと銀のパイナップルを取得、投げる、台に置く処理がそれぞれ正常に動作するか確認しておきましょう。 銀の台を設置しましょう。 ​ 「Model」→ 「Mega Fantasy Props Pack」→「Prefab s」→「Miscellaneous」と選択して、granite_panel をシーン上にドラッグ&ドロップして、Transitionを調整してください。 【Transformの設定】 Position : X=109 Y=6.4 Z=-104.5 Rotation : X=0 Y=0 Z=0 Scale : X=1.5 Y=1 Z=1.5 台に銀のマテリアルを貼りましょう。 銀の台をコピーして、横に3つ並べてください。 右から2番目の銀の台にはラッパを固定しておき、調べられるだけにします。 「Model」→「Object」内のTrumpetをシーン上にドラッグ&ドロップし、右から2番目の台の子オブジェクトにしてください。 ​ 【Transformの設定】 Position : X=0 Y=0.45 Z=0 Rotation : X=-90 Y=-45 Z=0 Scale : X=3 Y=3 Z=3 右から2番目の銀の台にItemObjectとOutlineをアタッチしてください。 ​ItemObjectコンポーネントには名前と調べたときの説明文を入力しましょう。 ​ 【サンプルの入力例】 Name :銀の台 Explanation :銀の「ラッパ」が置かれた台座だ 外すことができないようだ… これでヒントとなるラッパが置かれた台は完成です。 残りの台にはアイテムを置けるようにします。 ​ ​ Ctrlキーを押しながら残り3つの台を選んで、ItemPlatformとOutlineをアタッチしてください。 ItemPlatformにアイテム名と説明を入力しましょう。また、 AddPosをX=0 Y=0.2 Z=0、AddRotをX=0 Y=180 Z=0 に調整してください。 ​ 【サンプルの入力例】 Name :銀の台 Explanation :銀でできた台がある… 何か置けそうだ AnswerItemIDだけは台ごとに異なるため、左から銀のリンゴ、銀のゴリラ、銀のパイナップルのIDになるように設定しましょう。 これで銀の台が完成しました。実際にアイテムを置いて、台として動作するか確認してみましょう。 銀の台に置いたアイテムが正解なら開く宝箱を作成しましょう。 ​ 「Model」→ 「Mega Fantasy Props Pack」→「Prefab s」→「Storage」を開いて、chest(宝箱)をシーン上にドラッグ&ドロップしてください。 宝箱のTransitionを調整して、銀の土台の横に移動させてください。 ​ 【Transformの設定】 Position : X=109 Y=6.25 Z=-119 Rotation : X=0 Y=-90 Z=0 Scale : X=3 Y=4 Z=4 宝箱の子オブジェクトに宝箱の蓋があるため、回転を調整して宝箱を閉じてください。 ​ 【Transformの設定】 Rotation : X=20 Y=0 Z=0 宝箱が開くアニメーションを作成しましょう。2つ目のギミックで棚を動かすアニメーションを作ったときと流れは同じです。 ​ 新しいアニメーションChestCloseを作成して、Chestにドラッグ&ドロップしてください。 作成したChestCloseアニメーションをダブルクリックして、Animationウィンドウを開いてください。 Animationウィンドウを開いた後にヒエラルキー内のChestを選択することで「Add Property」のボタンを押せるようになります。 アニメーション には子オブジェクトの状態も対象にすることができます。 「Add Property」をクリックしてから、chest_top→Transform→Rotation横の+ボタンをクリックしてください。 ChestCloseアニメーションは閉じた状態のため、特に何か動きを設定する必要はありません。 Animationウィンドウ左上にあるアニメーション名が表示されている部分をクリックして「Create New Clip」を選択してください。 ファイル名にChestOpenと入力して、保存ボタンをクリックしてください。 ChestOpenは宝箱が開くアニメーションのため、キーを使用してアニメーション を作成しましょう。 ​ ChestCloseと同じようにchest_topのRotationの項目を追加してください。 Rotationの項目を追加したら、宝箱が開くアニメーションを作成してください。 ​ 【サンプルの入力例】 0フレーム目 :X=20 40フレーム目:X=-80 60フレーム目:X=-70 ChestOpenを確認して、LoopTimeにチェックが入っていたら外しておいてください。 chestにアニメーションをドラッグ&ドロップした時に自動で作成されたAnimator Controllerをダブルクリックしてください。 Animatorウィンドウを開いたら左上のParametersを選択して、Trigger型のパラメータを作成しましょう。名前はOpenにしておいてください。 ChestCloseステートを右クリックして「Make Transition」を選択し、ChestCloseからChestOpenに繋がるTransitionを作成してください。 作成したTransitionの設定を行いましょう。 ​ ・Has Exit Timeのチェックを外す ・Transition Durationを0にする ・Conditions下部のプラスボタンを押して、遷移条件をOpenに設定する 後は2つ目のギミックの棚と同じように、対応した土台全てに置かれたアイテムが正解ならアニメーションを再生するように設定するだけです。 ​ ​ ItemGimmickスクリプトを開いて、青い部分のコード を埋めてください。 【ヒント】棚を移動させるenAnswer_Moveの処理を参考にしましょう using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemGimmick : ItemObject // 継承する { [SerializeField, Header("正解判定オブジェクト用")] ItemPlatform[] AnswerPlatforms; // 対応する土台 bool m_isAnswer = false; // 正解済みかどうか [SerializeField, Multiline(3)] string AnswerExplanation; // 正解時に説明文を変化させる // 正解時の反応の種類 public enum AnswerPattern { enAnswer_Drop, // 落下する enAnswer_Move, // 移動する // ① 開く演出用の列挙型enAnswer_Openを追加する (ここに入力) } [SerializeField] AnswerPattern Answer_Pattern; // 正解時の反応 void Update() { ~後略~ ~前略~ void Answer() { // 正解時の処理 switch (Answer_Pattern) { case AnswerPattern.enAnswer_Drop: // 物理演算の有効化 Rigidbody rb = GetComponent(); rb.isKinematic = false; // 前方に飛ばす rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse); // 回転をかける rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse); // 説明欄の更新 Explanation = AnswerExplanation; break; case AnswerPattern.enAnswer_Move: // 移動アニメーション GetComponent().SetTrigger("Move"); break; // ② Answer_Patternが①で追加したものと同じなら // 開くアニメーションを再生し、 // 再度調べられないようにする(SetIsChesk関数を使用) (ここに入力) } } } 降参 or 答え合わせの方はこちら 今回は正解時の反応が3種類しかないため直接処理を書きましたが、汎用性を考えるとItemGimmickを継承した派生クラスを作る方がより賢い作り方といえます。ゲームを拡張してさらにギミックを追加したい場合は覚えておきましょう。 ​ ​ chestにItemGimmickとOutlineをアタッチしてください。 ItemGimmickコンポーネントに必要なパラメータを設定しましょう。Answer Platformsの項目を3つにして、ラッパが置かれていない3つの台をドラッグ&ドロップしてください。 ​ 【サンプルの入力例】 Name :宝箱 Explanation :開かないようだ Answer_Pattern :enAnswer_Open ここまで設定すると銀の台に正しくアイテムを置くことで宝箱が開くようになります。ゲームを実行して確認してみましょう。 最後にそれぞれの銀のアイテムの位置を調整しましょう。 サンプルでは銀のリンゴは場所が固定で戸棚の中に隠されており、銀のゴリラと銀のパイナップルは複数の候補のうちランダムな座標に出現するようになっています。 ​ まずは戸棚を作成しましょう。調べる処理などはItemObjectを継承するため、再び書く必要はありません。ただ「調べられた時に左へ移動する」処理を書くだけで戸棚は完成します。ここまでの復習も兼ねて挑戦してみましょう。 ​ ​ Cubeを4つ作成して、それぞれDoor1、Handle1、Door2、Handle2と名前を入力します。Door1の子オブジェクトにHandle1、Door2の子オブジェクトにHandle2を設定してください。 戸棚の扉になるようにそれぞれのTransitionを調整してください。 【Transformの設定 ​ Door1 】 Position : X=42.3 Y=7.58 Z=128.26 Rotation : X=0 Y=0 Z=0 Scale : X=0.2 Y=2.4 Z=4.8 ​ 【Transformの設定 ​ Handle1 】 Position : X=0.7 Y=0 Z=0.4 Rotation : X=0 Y=0 Z=0 Scale : X=1 Y=0.2 Z=0.04 ​ 【Transformの設定 ​ Door2 】 Position : X=42.1 Y=7.58 Z=123.6 Rotation : X=0 Y=0 Z=0 Scale : X=0.2 Y=2.4 Z=4.8 ​ 【Transformの設定 ​ Handle2 】 Position : X=0.7 Y=0 Z=-0.4 Rotation : X=0 Y=0 Z=0 Scale : X=1 Y=0.2 Z=0.04 全てのCubeに対して「wooden-boards-texture-d」のマテリアルを貼ってください。Ctrlキーでまとめて選択してからマテリアルを変更することで、4つのCubeに一気にマテリアルを適用できます。 棚や宝箱と同じように、扉が移動するアニメーションを作成しましょう。アニメーションを作成するのは、向かって右側の扉(Door1)だけで構いません。 棚や宝箱のアニメーション作成を参考にしながら、Door1をZ方向に移動させる アニメーションを作成してください。 Animationウィンドウ左上のアニメーション名をクリックして「Create New Clip」から新しいアニメーションDoorOpenを作成してください。 ​ DoorOpenアニメーションにPositionのパラメータを追加して、Z方向に-4移動するアニメーションを作成しましょう。 ​ 【Animationの設定】 0フレーム目 : Z=128.26 60フレーム目: Z=124.26 Animator ControllerをダブルクリックしてAnimatorウィンドウを開いてください。 ​ Trigger型のパラメータOpenを作成して、Openが有効になるとステートが切り替わるようにしましょう。 DoorOpenアニメーションを確認して、LoopTimeにチェックが入っていたら外しておきましょう。 ItemDoorスクリプトを作成して以下のように入力してください。青い部分 は穴埋めになります。継承を使うことで、必要な処理は「自分が調べられた時の処理」だけになります。 ​【ヒント】①と②はItemPlatformスクリプトを参考にしましょう! using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemDoor (ここに入力) // ① ItemObjectを継承 { // ② ItemObjectクラスのItemCheck関数をオーバーライドする (ここに入力) { // ③ ドアを開けるアニメーションを再生 (ここに入力) // ④ 再度調べられないようにする(SetIsCheck関数) (ここに入力) } } 降参 or 答え合わせの方はこちら PlayerItemスクリプトではレイがヒットしたオブジェクトのItemObjectコンポーネントを取得して、ItemCheck関数を呼び出しています。レイがヒットしたオブジェクトがItemDoorコンポーネントをアタッチしていた場合、ItemDoorコンポーネントはItemObjectコンポーネントとして扱われます。 ItemObjectを継承したItemDoorクラスがItemObjectとして振舞っているイメージです。 ​ ItemDoorクラスはItemObjectクラスのItemCheck関数の中身を上書きして、オーバーライドしたItemCheck関数の処理に置き換えています。上書きされる側の関数にはvirtualをつけて、仮想関数にそておきます。 これによってItemObjectをアタッチしたオブジェクトは調べた時に説明文を表示しますが、ItemDoorをアタッチしたオブジェクトは調べた時に説明文は表示せずに 、ドアを開くアニメーションを再生することになります。 継承については完全に理解する必要はありません。ただ「基底クラスの仮想関数の内容は派生クラスから上書きすることができる 」という点は知っておきましょう。 ​ ​ 後はDoor1にItemDoorとOutlineをアタッチするだけになります。ItemDoorコンポーネントのNameだけは入力しておいてください。 これで戸棚を開けることができるようになりました。 開けた後は再度調べられなくなります。 ​ ゲームを実行して確認してみましょう。 最後は銀のゴリラと銀のパイナップルの出現位置を複数候補からランダムにしましょう。 GimmickManagerスクリプトを開いて赤い部分のコード を追加してください。 ~前略~ [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")] ItemPlatform[] Gimmick2_Platform; int Gimmick2No = 0; // ギミック3 [SerializeField] GameObject GorillaObj, PineappleObj; [SerializeField] Vector3[] GorillaPos, PineapplePos; // 出現位置(候補からランダム) [SerializeField] Vector3 GorillaRot; void Awake() { ​~後略~ ~前略~ break; } // ギミック3 ゴリラの位置をランダムにする GorillaObj.transform.position = GorillaPos[Random.Range(0, GorillaPos.Length)]; // ギミック3 パイナップルの位置をランダムにする PineappleObj.transform.position = PineapplePos[Random.Range(0, PineapplePos.Length)]; } } GameManagerオブジェクト ​のインスペクターにパラメータが追加されているので、対象となるアイテムとランダムな出現先である座標を設定してください。 ​アイテムの出現先の座標はあくまでサンプルの設定例で、自由に項目を増やしたり座標を変更しても構いません。 ​ 【GimmickManagerコンポーネントの入力例】 GorillaObj : シーン内にある銀のゴリラをドラッグ&ドロップ PineappleObj : シーン内にある銀のパイナップルをドラッグ&ドロップ ​ GorillaPos Element0 : X=136 Y=3.32 Z=76 ​GorillaPos Element1 : X=-12.5 Y=4.63 Z=22 ​GorillaPos Element2 : X=51.5 Y=8 Z=66 ​ PineapplePos Element0 : X=36 Y=8.1 Z=-20 Pineapple Pos Element1 : X=131.5 Y=4.63 Z=-61.5 Pineapple Pos Element2 : X=49 Y=5.1 Z=-114 設定ができたらゲームを実行して、銀のアイテムが候補のうちどれかの座標に出現することを確認してみましょう。 これで扉のパスワードを隠す3つのギミックが完成しました。 3-6 扉のパスワードを生成 3-6 扉のパスワードを生成 いよいよ最後のギミック、脱出用の扉を作成します。 その前にまずはランダムな6桁のパスワードを生成して、今まで作成したギミックを解除することで確認できるようにしましょう。 ​ ​ GimmickManagerスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; // TextMeshProを扱うために必要 public class GimmickManager : MonoBehaviour { // 扉の暗証番号 [SerializeField] TextMeshPro[] NumberText = new TextMeshPro[3]; // 番号表示用 [SerializeField] int[] DoorAnswer = new int[6]; // 6桁の答え public int GetAnswer(int num) { return DoorAnswer[num]; } // ギミック2 [SerializeField, Header("ギミック2")] GameObject HintObject; // ヒント用オブジェクト [SerializeField] Material[] HintMaterial; // ヒントのマテリアル [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")] ItemPlatform[] Gimmick2_Platform; // 石の土台 int m_gimmick2No = 0; // パターン番号 // ギミック3 [SerializeField, Header("ギミック3")] GameObject GorillaObj; [SerializeField] GameObject PineappleObj; [SerializeField] Vector3[] GorillaPos, PineapplePos; // 出現位置(候補からランダム) // Startより先に実行される関数 private void Awake() { // 答えをランダム生成 for (int i = 0; i < DoorAnswer.Length; i++) { // int型のRandom.Rangeの挙動に注意! DoorAnswer[i] = Random.Range(0, 10); } // 生成した答えを表示 int num = 0; for (int i = 0; i < NumberText.Length; i++) { // 2桁ずつ表示する NumberText[i].text = "" + DoorAnswer[num] + DoorAnswer[num + 1]; num += 2; } // ギミック2の答えの乱数をふる m_gimmick2No = Random.Range(0, HintMaterial.Length); ​~後略~ 【プログラムの解説】 ​・3Dアクションゲーム編ではTextMeshProUGUIを使用していましたが、今回使用するのは3DオブジェクトのTextMeshProなのでクラス名はTextMeshProになっています。 ​ ・int[] DoorAnswer = new int[6]; のように記述することで、配列の要素数を指定することができます。ここで指定した要素数はあくまで初期値であり、インスペクターから変更できます。 また、int[] Hoge = { 2, 3, 5, 7, 11 }; のように、初期値を指定することもできます。 ・3-4で解説した通り、Random.Range関数は第一引数から第二引数の範囲のうちランダムな値を返す関数です ​。int型を引数に指定した場合、戻り値の範囲に第二引数は含まれません。そのため第一引数を0、第二引数を10にした場合0~9の範囲のランダムな値が返されることになります。 ​ ​ 後はパスワード表示用のTextMeshProオブジェクトを追加するだけです。手順が少し複雑なので、注意して進めましょう。 ​ TextMeshProフォルダ内にマテリアルを作って、名前を「Corporate-Mincho-ver3 SDF - Number」にしてください。 TextMeshProフォルダ内にマテリアルを追加しないと正常に進められないので注意しましょう。名前も間違えないように気をつけてください(コピペ推奨です) 作成したマテリアルのシェーダーをTextMeshPro→SRP→TMP_SDF_URP Litに変更してください。 デフォルトのシェーダーでは3D空間に設置したTextMeshProはライトの影響を受けません。標準のままでは後ほどライトを消して世界を暗くしたときにTextMeshProだけが暗闇に浮かび上がるようになってしまいます。この問題を解決するため、 教材ではこちらのフォーラム で公式によって配布されていたシェーダーを改変したものを同梱しています。 ​ Unity Tips! インスペクターの下部に_MainTexという項目があるので、そこに同梱したフォントのテクスチャをドラッグ&ドロップしてください。 ヒエラルキーから「3D Object」→「Text - TextMeshPro」を追加してください。 オブジェクト名をNumber1にして、絵画の後ろに隠れるように座標や回転を調整しましょう。 ​ 【サンプルの入力例】 PosX=-17.75 PosY=9.3 PosZ=-10 Width=5 Height=5 Rotation X=0 Y=-90 Z=20 TextMeshProのパラメータを調整してください。項目が多いので注意しましょう。 ​・TextInputに「00 」と入力 ​→仮のテキストを入力しておきます。 ​ ・FontAssetを「Corporate-Mincho-ver3 SDF」にする →右端の丸いボタンをクリックすると楽に選択できます。 ​ ・MaterialPresetを「Corporate-Mincho-ver3 SDF - Number」にする →FontAssetを変更してからでないと表示されないので 注意しましょう。 ​ ・FontStyleの太字と斜体ボタンを押す →文字の見た目が変化します。 ​ ​・FontSizeを30にする ​ ・Vertex Colorを赤にする →少し暗めの赤の方がステージに馴染みます。 ​ ・Alignmentを中央に設定​ ​ ここまで設定できたら、絵画の裏に赤い数字が表示されているはずです。 残りの番号も作成しましょう。色や座標が違うだけで基本設定は同じです。 ​ Number1をコピーしてNumber2を作成して、座標や回転を調整してください。 ​ 【サンプルの入力例】 PosX=111.8 PosY=9.8 PosZ=148 Width=8 Height=8 Rotation X=0 Y=90 Z=10 FontSizeを48にして、文字の色を青色に変更してください。 これで隠し部屋の壁に青い番号が表示されます。 Number1を複製してNumber3を作成してください。 Number3は宝箱の蓋の裏に隠すため、Number3をchestの子オブジェクトであるchest_topの子オブジェクトにしてください 。蓋の子オブジェクトにすることで、蓋が開くアニメーションをしても問題なく追従するようになります。 Number3の座標や回転を設定しましょう。親オブジェクトであるchest_topを基準としたローカル座標になっている点に注意しましょう。 ​ 【サンプルの入力例】 PosX=0 PosY=0.157 PosZ=0.365 Width=5 Height=5 Rotation X=-70 Y=180 Z=0 Scale X=0.3 Y=0.25 Z=1 FontSizeを24にして、文字の色を緑色に変更してください。 これで宝箱の裏に緑の番号を隠すことができました。 GimmickManagerのNumberTextに作成したNumber1、Number2、Number3を設定してください。 ここまで設定できたら番号の処理は完成です。 ​ ゲームを実行するとDoorAnswerにランダムな値が格納されます。 今まで作成した3つの謎を解いて、扉のパスワードを見つけることができるか確認してみましょう。​ 3-7 出口の扉を作成 3-7 出口の扉を作成 長くなりましたが最後のギミックとして、6桁のパスワードを入力することで開く扉を作成しましょう。 ​ ​ まずは空オブジェクトを追加して名前をDoorにしておきましょう。 座標は X=164.6 Y=-1.56 Z=11.56 へ移動させてください。 ドア本体を作成しましょう。Doorの子オブジェクトにCubeを追加してください。名前はBoardにしておきましょう。 ​ Boardの座標やマテリアルを調整してください。 ​ ​【サンプルの入力例】 Position : X=0.23 Y=6.94 Z=-2.96 Scale : X=0.5 Y=13.9 Z=6.1 マテリアル(Element0): timber_1_fixed_d Boardの子オブジェクトにCubeを作成して、名前はFrameにしておきましょう。 Frameの座標やマテリアルを調整してください。 ​【サンプルの入力例】 Position : X=-0.4 Y=0.45 Z=0 Scale : X=0.8 Y=-0.03 Z=0.8 マテリアル(Element0): wood_4_d Frameを複製して、四角の枠になるようにしましょう。 ​【サンプルの入力例 下側】 Position : X=-0.4 Y=0.32 Z=0 Scale : X=0.8 Y=0.03 Z=0.8 ​ ​【サンプルの入力例 左側】 Position : X=-0.4 Y=0.385 Z=0.35 Scale : X=0.8 Y=0.1 Z=0.05 ​ 【サンプルの入力例 右側】 Position : X=-0.4 Y=0.385 Z=-0.35 Scale : X=0.8 Y=-0.1 Z=0.05 ディスプレイの中身は一旦後回しにして、先に入力用の ボタンを作成しましょう。 ​ Boardの子オブジェクトにCubeを追加して、名前をButton_0にしてください。 ​Transformとマテリアルを設定しましょう。 ​【サンプルの入力例】 Position : X=-0.75 Y=0.2 Z=0 Scale : X=0.5 Y=-0.08 Z=0.2 マテリアル(Element0): wooden-boards-texture_white_d Button_0の子オブジェクトに3DオブジェクトのTextMeshProを追加して、名前をNumberにしておきましょう。 ​ NumberのPosXを-0.55にしてください。 TextMeshProの設定を行いましょう。 ​ 【TextMeshProの設定】 Text Input : 0 Font Asset : Corporate-Mincho-ver3 SDF Material Preset: Corporate-Mincho-ver3 SDF - Number(Font Assetの設定をしてから選ぶ) Font Size : 10 Vertex Color : 黒に設定 Alignment : 中央 Button_0を複製して1~9のボタンを配置してください。それぞれ座標を調整して、TextMeshProのText Inputに値を入力しましょう。 それでは先に「パスワードを入力して、正解なら扉が開く」処理を実装しましょう。その後にモニターに入力中の値を表示します。 ​ まずは入力中の値を保持して正解なら開くアニメーションを再生するスクリプトを作成しましょう。 FinalDoorスクリプトを作成して、以下のように入力してください。少々長いですがfor文や配列への理解があれば問題ありません。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class FinalDoor : MonoBehaviour { // 現在の番号 int m_nowNumber = 0; // 正解済みならtrue bool m_isNumberOK = false; public bool GetIsNumberOK() { return m_isNumberOK; } // 答え取得用 [SerializeField] GimmickManager Gimmick_Manager; // 入力番号 int[] Number = new int[6]; void Awake() { // 最初にリセット NumberReset(); } // 引数の番号を入力 public void SetNumber(int num) { // 正解済みなら実行しない if (m_isNumberOK) { return; } // 番号入力 Number[m_nowNumber] = num; m_nowNumber++; // 入力が終わっていないなら終了 if (m_nowNumber < Number.Length) { return; } // ここから下は番号を全て入力した時の処理 bool answer = true; // 正解かどうかチェック for (int i = 0; i < Number.Length; i++) { // 1つでも違ったら不正解 if (Gimmick_Manager.GetAnswer(i) != Number[i]) { answer = false; } } // 判定 if (answer) { // 正解 m_isNumberOK = true; // 親オブジェクトのアニメーション開始 transform.parent.GetComponent().SetTrigger("Open"); } else { // 不正解 NumberReset(); } } // 入力状態をリセット void NumberReset() { for (int i = 0; i < Number.Length; i++) { m_nowNumber = 0; Number[i] = -1; } } } 【プログラムの解説】 ・transform.parent で自身の親オブジェクトを取得することができます。ここでは自身の親オブジェクトにアタッチされているAnimatorを取得して、アニメーションの再生を開始しています。 ​ コードが書けたら保存して、Boardにアタッチしておいてください。 ​ インスペクターにGimmickManagerを指定する項目があるので、既に作成してあるGimmickManagerを選択しましょう。 ​ ​ 扉が開くアニメーションを作成しましょう。 ​ Animationフォルダ内にFinalDoorCloseアニメーションを作成して、Doorオブジェクトにドラッグ&ドロップしてください。 FinalDoorCloseアニメーションをダブルクリックしてアニメーションウィンドウを開いてください。アニメーションウィンドウを開いた状態でインスペクター内のDoorオブジェクトをクリックし「Add Property」からTransform→Rotationの項目を追加しましょう。 FinalDoorCloseアニメーションは特に何もしなくて構いません。 ​ 左上のアニメーション名が表示されている場所から「Create New Clip」を選択して新しいアニメーションFinalDoorOpenを作成してください。 同じようにRotationの項目を追加して、ドアが開くアニメーションを作成してください。サンプルでは1秒かけてRotation.yを-120へ変化させています。 ​ クオリティを上げたい人は途中にキーを追加して、最初はゆっくり開くようにするとよいでしょう。 入力側のボタン処理を作成します。入力側は先ほど作成したSetNumber関数を呼ぶだけになります。 ​ FinalButtonスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class FinalButton : ItemObject // 継承する { [SerializeField, Header("扉")] FinalDoor finalDoor; [SerializeField, Header("押すと出力される番号")] int Num = 0; // 正解しているかどうか bool m_isEnd = false; // 自分が調べられた時の関数をオーバーライド(上書き)する public override void ItemCheck() { // ボタンを押すアニメーションを再生 GetComponent().SetTrigger("Push"); // 番号の入力 finalDoor.SetNumber(Num); } void Update() { // 正解済みならAnimatorを止める if (finalDoor.GetIsNumberOK() && m_isEnd == false) { GetComponent().enabled = false; m_isEnd = true; } } } ボタンを押すアニメーションを作成しましょう。流れは今までと変わらないため、細かい説明は省略します。 ​ Animationフォルダ内に新しいアニメーションFinalButtonWaitを作成して、Button_0にドラッグ&ドロップしてください。 Unityのアニメーションには絶対座標と相対座標があります(2Dランゲーム編2-4参照 ) ​ ボタンを押すアニメーションはその場でアニメーションしたいため、相対移動に設定する必要があります。Button_0のAnimatorを選択して、Apply Root Motionにチェックを入れてください。 FinalButtonWaitには特に操作は必要ありません。新しいアニメーションFinalButtonPushを作成して、Add PropertyからPositionの項目を追加してください。 相対移動に設定しているため、最初と最後のフレームは全ての項目を0に設定しておきましょう。 アニメーションの途中にキーを追加して、Xを+0.1するようにしてください。これで全てのボタンに流用できるアニメーションが完成します。 Animator Controllerを開いて、Trigger型のパラメータを追加しましょう。名前はPushにしておきます。 ​ FinalButtonWaitとFinalButtonPushを相互に繋ぐTransitionを作成してください。 追加した2つのTransitionの設定を変更しましょう。 ​ 【FinalButtonWait->FinalButtonPush】 ・Has Exit Timeのチェックを外す ・Transition Duration を0にする ​・Conditions右下のプラスボタンを押して、遷移条件をPushに設定 ​ 【FinalButtonPush->FinalButtonWait】 ・Has Exit Timeにチェックを入れる(アニメーション終了を待つ) ・Exit TimeとTransition Duration を0にする ​・Conditionsには何も設定しない ​ →アニメーションが終わると自動で遷移するようになる 最後に各ボタンに必要なコンポーネントをアタッチしていきましょう。 ​ Button_0を選択してFinalButtonとOutlineをアタッチしてください。FinalDoorには先ほどBoardにアタッチしたFinalDoorコンポーネントを指定します。 他のButton_1~Button_9までをまとめて選択して、Button_0のAnimator Controllerをドラッグ&ドロップしましょう。Apply Root Motionにチェックを入れておきます。 FinalButtonとOutlineもアタッチして、FinalDoorの設定をしておきましょう。 ​ 後は各ボタンに対応したNumを設定していくだけです(Button_1ならNumは1、Button_5ならNumは5) これで最後の扉を開く処理が完成しました。ゲームを実行して確認してみましょう。 ​ GimmickManager内に表示されている答えを見るか、実際に3つの謎を解いて答えを確認して、最後の扉に答えの番号を入力してみてください。扉が開いたらOKです。 3-8 シェーダーグラフ 3-8 シェーダーグラフ 後は入力中の番号をモニターへ出力するだけです。 ​ ​ 普通に表示してもよいのですが、今回はシェーダーグラフ という機能を使って少しだけリッチな表現にしてみましょう。シェーダーグラフはノードを繋げることで視覚的にシェーダーを作ることができる機能です。 HLSLではわかりにくかった計算中の流れを視覚的に確認できるため、シェーダーへの理解も深まります。DirectXでシェーダーが苦手な人もまず挑戦してみましょう。 ​ 今回は白い線が移動することでモニターのように見えるシェーダーを作ります。 Shaderフォルダを作成して「Create」→「Shader Graph」→「URP」→「Lit Shader Graph」を追加してください。 ​※ シェーダーに詳しい人はちょっと違和感を覚えるかもしれませんがわざとです。後で直します。 シェーダーグラフが作成されるので、好きな名前をつけたらダブルクリックで開いてください。 シェーダーグラフの編集画面が開きます。見やすいように位置とサイズを調整したら早速シェーダーを作っていきましょう。 編集画面の適当なスペースで右クリックして「Create Node」を選択してください。 検索欄に「Position」と入力して、Positionノードを選択してください。 編集画面にPositionノードが追加されます。これは描画する場所のワールド座標を返すノードです。 ​ 同じようにSplitノードを検索、追加してください。 ​ ​ Splitノードはベクトルの要素を分割するノードです。例えばVector3型のpositionというデータがあったとして、そこからyの要素だけを使いたい場合などに使います。 PositionノードのOutからSplitノードのInまで、マウスをドラッグして線を繋げてください。 ​ Timeノードを作成してください。 Timeノードはゲームを開始してから経過した時間などを返すノードです。Timeは実行中常に増え続ける値なので、段階的にパラメータを変化させたい時などによく使われます。 ​ Multiplyノードを作成して、TimeノードのTimeとMultiplyノードのAを繋げてください。 ​ ​ MultiplyノードはAとBの乗算を行うノードです。 MultiplyノードのBにあたる値は白いラインがスクロールする速さに該当しますが、このままでは値が2で固定されてしまい調整が面倒です。スクリプトと同じように、後から調整したい値は変数を作って置き換えましょう 。 ​ 左上のシェーダー名が書いてある場所にある+ボタンをクリックすると、追加する変数の型を選択できます。Float型を選択してください。 追加したFloat型のパラメータの名前をSpeedにしておいてください。 ​ Speedを直接ドラッグ&ドロップすることでノードを追加できます。 Speedノードを選択すると右側のNode Settingsで様々な設定をすることができます。Default(初期値)を4に変更しましょう。 ​ 設定できたら、SpeedノードをMultiplyノードのBに繋いでください。 Addノードを追加してください。AddノードはAとBの加算を行うノードです。 ノードを追加できたら、SplitノードのG(ワールド座標のYの要素)とMultiplyノードのOutを繋げましょう。 Multiplyノードを追加して、AddノードのOutをAに繋げてください。 追加したMultiplyノードのBの値は線の数(線の細かさ)を示す値なので、こちらもパラメータとして調整できるようにしましょう。 ​ Speedの値を追加した時と同じように、Float型のパラメータLineを追加してください。 Lineノードを追加してMultiplyノードのBへ繋いでください。 Lineノードを選択して、Node Settingsを変更しましょう。今回はSliderを表示します。 ​ ModeをDefaultからSliderへ変更してください。 これでスライダーを使って値を調整できるようになりました(マテリアルに適応した際に確認できます) Defaultを4、Minを0、Maxを10にしてください。 MultiplyノードのOutとFractionノードのInを繋げてください。 Fractionノードは値の小数点以下の部分だけを抽出するノードです。これによって液晶画面のノイズにあたる部分が完成します。 今のままではラインが目立ちすぎるので調整しましょう。 ​ Colorノードを作成して、色を灰色に設定してください。 Multiplyノードを作成して、FractionノードのOutとColorノードのOutを繋げてください。 Color型のパラメータColorを作成してください。これがベースの色になります。 作成したパラメータColorとMultiplyノードの結果をAddノードを使って加算してください。 ​ ColorノードのDefaultを赤色に設定すると、今までのラインと赤色が合成されたマテリアルが出力されます。 AddノードのOutをFragmentのBase Colorへ繋いでください。これで計算結果が出力されます。 画像のようにノードを繋いだら、左上の「Save Asset」ボタンをクリックして保存してください。シェーダーグラフを変更した場合は保存しないと反映されない ので注意しましょう。 保存したらシェーダーグラフ編集画面を閉じてください。 ​ ​ さっそく作成したシェーダーを使ってみましょう。マテリアルを作成して、名前をRedDisplayにしておきます。 作成したマテリアルにDisplayShaderをドラッグ&ドロップしてください。これでマテリアルにシェーダーが適用されます。 マテリアルを選択すると、シェーダーグラフで設定したSpeedやLineなどのパラメータが表示されています。SpeedとLineはお好みで調整して、Colorは赤に設定してください。 ​Speedを負の数にするとラインの方向が逆になります。 Boardの子オブジェクトにBoxを作成して、Transformを調整してください。サンプルでは名前をDisplayにしています。 先ほど作成したRedDisplayマテリアルをBoxにドラ ッグ&ドロップしましょう。 ​【サンプルの入力例(Boardの子オブジェクトにしてから調整する)】 Position X=-0.5 Y=0.385 Z=0.22 Rotation X=0 Y=0 Z=0 ​ Scale X=0.1 Y=0.1 Z=0.22 これでモニターは完成…としたいところですが、ディレクションライトを消すと微妙な感じになってしまいます。 実は最初に作成した「Lit Shader Graph」はUnityによってライティングが適用されるシェーダーです。そのため、ライトを消すとそれに応じて暗くなってしまいます。 シェーダーグラフ編集画面を開いて、Graph Settings のMaterialを「Lit」から「Unlit」へ変更してください。これでライティングが適用されない設定になります。 ​ 基本的にはマテリアルにライティングを適用したいためLitにしておきますが、今回のようにディスプレイなどマテリアルにライティングを反映したくない場合にはUnlitに変更してください。 設定を変更すると、ディレクションライトが消えても影響を受けないようになります。これでモニターの土台が完成しました。 ​ 確認できたらディレクションライトは戻しておいてください。 入力内容を表示するためのTextMeshProを追加しましょう。 ​ Displayの子オブジェクトに「3D Object」→「Text - TextMeshPro」を追加しましょう。UIのTextMeshProではないので注意してください。 ​ 座標と回転を調整してください。 ​【サンプルの入力例(Displayの子オブジェクトにしてから調整する)】 Position X=-0.52 Y=0 Z=0 Rotation X=0 Y=90 Z=0 TextMeshProコンポーネントを設定していきます。 【TextMeshProの設定】 Text Input : __(アンダーバー2つ) Font Asset : Corporate-Mincho-ver3 SDF Font Size : 8 Vertex Color : 白 Alignment : 中央 RedDisplayマテリアルを複製して、BlueDisplayマテリアルとGreenDisplayマテリアルを作成しましょう。設定の違いは色だけです。 赤いモニターを複製して青と緑のモニターを作成しましょう。 変更するのは座標、名前、マテリアルのみです。TextMeshProの変更は必要ありません。 ​ ​【BlueDisplay(青いディスプレイ)】 Position X=-0.5 Y=0.385 Z=0 ​【GreenDisplay(緑のディスプレイ)】 Position X=-0.5 Y=0.385 Z=-0.22 これでオブジェクトの設定は完了です。 ​ 最後に入力内容を表示するスクリプトを書きましょう。 ​ FinalDoorスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; public class FinalDoor : MonoBehaviour { // 現在の番号 int m_nowNumber = 0; // 正解済みならtrue bool m_isNumberOK = false; public bool GetIsNumberOK() { return m_isNumberOK; } // 番号表示用 [SerializeField] TextMeshPro[] NumberObject; // 答え取得用 [SerializeField] GimmickManager Gimmick_Manager; // 入力番号 int[] Number = new int[6]; void Awake() { // 最初にリセット NumberReset(); NumberUpdate(); } // 引数の番号を入力 public void SetNumber(int num) { // 正解済みなら実行しない if (m_isNumberOK) { return; } // 番号入力 Number[m_nowNumber] = num; m_nowNumber++; NumberUpdate(); // 入力が終わっていないなら終了 if (m_nowNumber < Number.Length) { return; } // ここから下は番号を全て入力した時の処理 bool answer = true; // 正解かどうかチェック for (int i = 0; i < Number.Length; i++) { // 1つでも違ったら不正解 if (Gimmick_Manager.GetAnswer(i) != Number[i]) { answer = false; } } // 判定 if (answer) { // 正解 m_isNumberOK = true; // 親オブジェクトのアニメーション開始 transform.parent.GetComponent().SetTrigger("Open"); // 正解なので〇を表示 SetNumberText("〇〇〇"); } else { // 不正解 NumberReset(); // ×を表示して1秒後に戻す SetNumberText("×××"); Invoke("NumberUpdate", 1.0f); } } // 入力状態をリセット void NumberReset() { for (int i = 0; i < Number.Length; i++) { m_nowNumber = 0; Number[i] = -1; } } // 表示更新 void NumberUpdate() { int num = 0; for (int i = 0; i < NumberObject.Length; i++) { // 1番目 if (Number[num] == -1) { NumberObject[i].text = "_"; } else { NumberObject[i].text = "" + Number[num]; } num++; // 2番目 if (Number[num] == -1) { NumberObject[i].text += "_"; } else { NumberObject[i].text += "" + Number[num]; } num++; } } // 文章を表示 void SetNumberText(string text) { int len = 0; for (int i = 0; i < NumberObject.Length; i++) { // 指定文字目から1文字抜き出す NumberObject[i].text = text.Substring(len, 1); len++; } } } 【プログラムの解説】 ​・StringクラスのSubstring関数は「x文字目からy文字抜き出す」ことができる関数です。 第一引数に開始地点、第二引数に抜き出す文字数を指定します(第二引数を指定しなかった場合、末尾まで抜き出します) ​ ​ 入力できたら保存して、FinalDoorコンポーネントのNumber Objectに赤、青、緑の順番でTextMeshProを設定してください。順番を間違えると表示がずれるので注意しましょう。 ​ これで最後の扉に入力した番号が表示されるようになりました。 ​ 入力に失敗すると×が、成功すると〇が表示されることも確認してください。 ​ 長くなりましたが、これでゲームの核となるギミックが完成しました。 ​ 次のレッスンでは所持しているアイテムやアイテムを調べた時の文章を表示するためのUIを作成していきましょう。 ​ 【評価テスト】 ​(ここにURLが表示される いつか) 評価テスト Next Lesson4「UIを実装しよう」 ページ TOP 3-1 土台の実装 3-2 絵画の実装 3-3 アイテムの追加 3-4 2つ目のギミック 3-5 3つ目のギミック 3-6 扉のパスワードを生成 3-7 出口の扉を作成 3-8 シェーダーグラフ 評価テスト

  • 3Dアクションゲーム編 Lesson4「UIを作ろう」 | Unity1gc2

    3Dアクションゲーム編 Lesson4 UIを作ろう ここまでのLessonでプレイヤーとステージが完成しました。ここでは獲得した星の数などの情報を画面に表示するためのUIを実装していきましょう。 ​ ​ 4-1 TextMeshProの導入 4-1 TextMeshProの導入 実はUnityはテキスト周りは結構不便です。ですが、TextMeshPro を導入することで改善することができます。 ​ ​ ヒエラルキーのUI→Text-TextMeshProを選択してください。 「Inport TMP Essentials」を選択してください。 UIを作る時は2Dモードにすると便利です。シーンウィンドウの上部にある2Dボタンをクリックしてください。 ​ ヒエラルキー内のCanvasをダブルクリックするとCanvasにフォーカスしてくれます。 ​ UIを作る前にTextMeshPro(TMP)の基本的な使い方について解説します。項目が多いですが、その中でも頻繁に使う項目を紹介します。 ​ TextMeshPro(TMP)は他にも本当に色々できるのですが、全ては紹介しきれないので一部だけ紹介します(実際にやる必要はありません) ​ タグを使って文字を装飾できます (タグについて解説しているページ ) 文字にテクスチャを貼ったり、立体感をつけたりできます。 「3D Object」から3D空間上にTMPを配置することもできます。 TMPで日本語を表示する場合、ひと手間かかるので注意してください。日本語の導入方法はこのサイト などを参考にしてください(このゲームのUIでは日本語を使いません) TMPで設定を編集すると、同じフォントを使用している他のTextの設定も変化してしまったりします。これはMaterial Presetを共有しているため起こる現象です。 同じ設定でTextを量産したい時は便利な機能ですが、プリセットを複数作りたい場合もあると思います。 ​ ​ プリセットを複数作る際はTextMeshPro→Resource→Font&Materialsを開いて、FontAssetのマテリアルを選択してください。 インスペクター内の右上のボタンから「Create Material Preset」を選択すると新しいプリセットが追加されます。わかりやすい名前をつけられますが、指定しているフォントアセットの名前を含んでいないと設定できないので注意してください。 ​ 後はTMP内のMaterial Presetからプリセットを変更すれば完了です。 Unity Tips! 4-2 現在の体力を表示 4-2 現在の体力を表示 TMPの使い方を一通り把握したら実際にUIを実装していきましょう。 まずはプレイヤーの体力を表示します。 ​ Canvas上にTextMeshProを追加してください。名前は何でも構いませんが、解説では「HitPointText」にしておきます。 テキストの内容は「HP:99」にしておきます 。ここで設定した文章はあくまで仮で、後からスクリプトで変更します。 大きさや位置はお任せするので、好みの場所に配置してください。アウトラインを設定するとゲーム画面に紛れないのでオススメです。 新しいスクリプト「HitPointUI」を追加して、以下のコードを入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; // TMPを扱う時はこれを追加する! public class HitPointUI : MonoBehaviour { TextMeshProUGUI m_hpTMP; PlayerHitPoint PlayerHP; void Start() { // 自身にアタッチされたTMPを取得 m_hp TMP = GetComponent(); // プレイヤーの体力を保持するPlayerHitPointを取得 // ※"Player"タグがついたオブジェクトから取得している PlayerHP = GameObject.FindGameObjectWithTag("Player").GetComponent(); } void Update() { // 現在HPを表示 m_hp TMP.text = "HP:" + PlayerHP.HitPoint; } } 【プログラムの解説】 ​・スクリプト内でTMPを扱う際は上段に using TMPro; を追加しないと動作しないので注意してください。 ・GameCameraと同じようにタグからプレイヤーを検索して、さらにそのプレイヤーにアタッチされているPlayerHitPointコンポーネントを取得しています。 ​ ​ ​ 入力が終わったらHitPointTextにアタッチして実行してみてください。「HP:99」だったテキストがしっかり現在の体力に置き換わっていることが確認できると思います。 トゲに触れたり落下したりして、数値がちゃんと減るか確認してみましょう。 TextMeshPro→3DのTMP(3Dオブジェクト) TextMeshProUGUI→2DのTMP(UI) ​のことを指します。 間違えやすいので注意 してください。 Unity Tips! 4-3 星の数を表示 4-3 星の数を表示 同じように獲得した星の数も表示しましょう。 ​ 新しいTextMeshProをキャンバスに追加してください。 ​ 仮のテキスト「Star 9/9」を設定しましょう。 ​名前はStarCountTextにしておきます。 ​ 体力と同様、大きさや位置はお任せするので好みの設定にしてください。 新しいスクリプト「StarCountUI」を追加して、以下のコードを入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; // TMPを扱う時はこれを追加する! public class StarCountUI : MonoBehaviour { TextMeshProUGUI m_starTMP; public StarCount Star_Count; void Start() { // TMPを取得 m_star TMP= GetComponent(); } void Update() { // 最大の星の数と持っている星の数を表示 m_star TMP.text = "Star " + Star_Count.GetNowStarCount() + "/" + Star_Count.GetMaxStarCount(); } } 入力できたらスクリプトをStarCountTextにアタッチしてください。 ​ ​ ​ GroundCheckerの時にも一度使用しましたが、外部のコンポーネントを取得する手法を紹介します。 public (コンポーネント名)の変数を用意することでインスペクターから直接対象のコンポーネントを指定できます。 オブジェクト同士を繋げる際、​(特にゲーム制作に慣れていない人にとっては)この方法が一番わかりやすいと思いますので覚えておいてください。 ​ ​ ​ StarCountコンポーネントを保持しているのはGameオブジェクトなので、GameオブジェクトをインスペクターのStar_Countにドラッグ&ドロップしてください。 これでStarCountUIとStarCountが繋がりました。ゲームを実行して星の数を数えられているか確認してみてください。 4-4 タイムを表示 4-4 タイムを表示 最後にGameTimerコンポーネントで数えているタイムを表示しましょう。 ​ 新しいTextMeshProをキャンバスに追加してください。 ​ 仮のテキスト「99:99」を設定しておいてください。名前はTimerTextにしておきます。 ​ 例のごとく場所や装飾はお任せします。 新しいスクリプトTimerUIを作成して以下のように入力してください。青い部分 は穴埋めなので埋めてみましょう。 ​【ヒント】4-2 や4-3 を参考にしましょう! using System.Collections; using System.Collections.Generic; using UnityEngine; // ① TMPを扱うときに必要なものは… (ここに入力) public class TimerUI : MonoBehaviour { // ② TextMeshProを保持するためのprivateな変数 timer_TMP (ここに入力) // ③ GameTimerにアクセスするためのpublicな変数 Game_Timer (ここに入力) void Start() { // ④ TMPを取得して②に代入 (ここに入力) } void Update() { // 現在の時間を表示 string timeString = ""; timeString = Game_Timer.GetMinute().ToString("00"); timeString += ":"; timeString += Mathf.Floor(Game_Timer.GetSecond()).ToString("00"); timer_TMP.text = timeString; } } 降参 or 答え合わせの方はこちら 【プログラムの解説】 ・ToSrting関数はfloat型やint型の値をstring型(文字列型)に変換する関数です。引数に"00"と入力すると桁数が揃うように0を入れてくれます。例えば 3.ToString("00") とすると、03を返します。 ​ これによって、タイムが1桁の時でも表示がずれなくなります。 ・Mathf.Floor関数は引数に設定したfloat以下の最大の整数を返す関数です。小数点以下が含まれていると計算に不都合なので切り捨てています。 ​ ​ ③ で設定した変数はpublicになっているので、StarCountの時と同じようにインスペクターからTimerUIとGameTimerを繋げてみてください。 ​ 実行して経過時間が表示されていたら成功です。 Gameウィンドウの「Play Focused」をクリックして「Play Maximized」を選択するとゲームを全画面プレビューできます。 しかし、これを実行するとCanvasの大きさと画面の大きさが噛み合わず不自然な表示になってしまいます。 実際にゲームを出力した際に画面サイズを変更しても同じ問題が起こってしまうので、ここではその直し方を2つ解説します。 1つ目はアンカーを設定する方法です。今は画像の位置の基準が中央になっていますが、それを左上などに設定することでUIの位置を保ちます。 ​ UIのTransformからアンカーを設定してください。教材の場合、HitPointTextは左上に配置したいのでアンカーを左上に変更します。全てのTextのアンカーを変更して実行すると、それぞれのUIが左上と右上をキープしていることがわかります。 2つ目はCanvasの設定を変更する方法です。ヒエラルキーからCanvasを選択して、Canvas ScalerのUI Scale Modeを「Scale With Screen Size」に変更してください。変更すると表示されるReference Resolutionの値を調整します。 実行すると全画面に合わせてUIが大きくなっていることがわかります。 アンカーを設定する方法はUIの大きさは変わらずに位置を調整してくれます。Canvasを設定する方法は画面サイズに合わせてUIの大きさを調整してくれます。自分のゲームに適していると思う方法を選びましょう(両方同時に使ってもOKです) Unity Tips! Next Lesson5「シーンを作ろう」 評価テスト ​ いよいよ制作も大詰めです。次のLessonではタイトル、リザルト、ゲームオーバーを実装してそれぞれを繋げていきましょう。 ​ 【評価テスト】 https://forms.gle/cmADU74ks1X1P3nq8 ページ TOP 4-1 TextMeshProの導入 4-2 現在の体力を表示 4-3 星の数を表示 4-4 タイムを表示 評価テスト

  • 2Dランゲーム編 Lesson5「ステージセレクトを作ろう」 | Unity1gc2

    2Dランゲーム編 Lesson5 ステージセレクトを作ろう 5-1 複数のステージを作成 5-1 複数のステージを作成 ここまでのレッスンでステージ1は一通り完成しました。 次はステージ2、ステージ3を作成しましょう。 ​ ​ ステージを作成する前に、ゲームを実行するために必要なオブジェクトを事前に全てプレハブ化しておきましょう。 まずはPrefabフォルダ内を整理します。Prefabフォルダ内にGimmickPrefabフォルダを作成して、今まで使ってきたプレハブを入れてください(Ctrlキーを押しながらクリックすると複数選択できます) Prefabフォルダ内にGamePrefabフォルダを作成して、その中に今までStage1シーンで使っていたオブジェクトをドラッグ&ドロップしてプレハブ化してください。 プレハブ化するオブジェクト:MainCamera、Game、UnityChan、Grid、BackGroundA、BackGroundB、Goal これ以降、プレハブ化したオブジェクトを操作する際は基本的にプレハブ側を操作するので注意してください。 ​ プレハブ化できたらScenesフォルダ内に新しいシーンStage2を作成してください。 Stage2シーンを開いて、最初からあるMain Cameraを削除しておいてください。 Stage2シーン内に 、 GamePrefabフォルダ内のプレハブをドラッグ&ドロップで追加してください。Stage1での座標はプレハブをクリックすると確認できるので参考にしてください。Stage1と同じように遊べる状況になったらOKです。 ​ または、Stage1シーンにあるオブジェクトをコピー&ペーストしてStage2シーンに配置しても構いません(その方が楽です) それではステージ2を制作していきましょう。 ステージ1と全く同じ背景では面白くないので、背景画像を違うものに差し替えてみてください。「Sprite」→「BackGround」内にある好きな背景を、BackGroundオブジェクトのSpriteにドラッグ&ドロップすることで変更できます。画像に合わせてScaleやSizeを調整してください。 ​※ 元となったプレハブから改変した項目の左側には青い印がつきます 次はタイルマップをリセットしましょう。 ​ Stage_Mainオブジェクトを選択して、TileMap右端の3つの点があるボタンを押してください。開いたメニューの「Reset」をクリックすることでタイルマップをリセットできます 。 ​ Stage_Back、Decoration、Gimmickも同じようにリセットしてください。 Gimmickだけは直接子オブジェクトを削除する必要があります。 事前にStage1のGridのプレハブ化を解除しておいてください 。これを忘れるとStage1のギミックが全て消えてしまうので注意しましょう。 「Prefab」→「GamePrefab」内のGridをダブルクリックして 開いてください。 Gridプレハブを開いて、Gimmickの子オブジェクトを全て削除してください。 これでStage2シーンのタイルマップが全て初期化されます。 ステージ1と同じように地形、装飾、ギミックを配置していきましょう。設定した背景に合うパーツを配置してみてください。 タイルマップの使い方を忘れた場合はLesson2 を確認してください。特にステージ1と違うパーツを使う場合はSpriteEditorから当たり判定の設定が必要です。 Active Tilemapの項目の右側に警告が出ていますが、これはタイルマップにプレハブを使っていることが原因で出ているものなので気にしなくて構いません。 Active Tilemapを変更した際に警告ウィンドウが出た場合は「Scene」ボタンを選択してください。 タイルマップが複数あるため、設置対象のタイルマップを間違えないように注意してください。 ​ ​ 一通りの地形やギミックを配置してステージ2を完成させましょう。Stage1と同じように地形→装飾→ギミックの順番で配置していくのがオススメです。 ここまでと同じ手順でステージ3も作ってみましょう。 ​ Stage3シーンを作成→プレハブを配置→タイルマップをリセット→ステージ作成 ​ サンプルではステージ3までしか作っていませんが、物足りない方はさらにステージを追加しても構いません。 ​オリジナルのギミックや素材を使って様々なステージを作ってみてください。 5-2 ステージセレクトを作成 5-2 ステージセレクトを作成 ステージが完成したのでステージセレクトを実装して、それぞれのステージを遊べるようにしましょう。まずは前のステージをクリア済みかどうかなどの概念は置いておいて、単純にクリックしたステージを遊べるようにします。 ​ 新しくStageSelectシーンを作成してください。 StageSelectシーンを開いてください。 まずは背景の画像を作成します。「UI」→「Image」から画像を追加してください。 ​ 追加されたCanvasを選択して、UI Scale Modeを「Scale With Screen Size」に設定してください。Reference ResolutionはX=620 Y=480 に設定します。 作成したImageにわかりやすいように名前をつけておいてください(サンプルでは「BG」) 座標は原点にして、Widthには620、Heightには390を入力しましょう。 ​ Sauce Imageに「Sprite」→「UI」→「StageSelect」内にあるStageSelectBGをドラッグ&ドロップしてください。 これで背景画像の作成は完了です。 次にステージを選択するためのボタンを作成します。UnityのUIでは「Button」という機能を使うことで、クリックで動作するボタンを簡単に実装することができます。 ​ ​ ヒエラルキーから「UI」→「Button - TextMeshPro」を選択してください。シーン内にボタンが追加されたらOKです。 ボタンとして使う画像の設定をします。Lesson4でスコア用のウィンドウ画像を設定した時と同じです。 ​ 「Sprite」→「UI」→「StageSelect」内にあるStageButtonを選択してください。 ​ Sprite Modeを「Multiple」、Mesh Typeを「Full Rect」に変更したらApplyボタンをクリックしましょう。 設定が変更できたらSpriteEditorを起動してください。 SpriteEditorが開いたら、ボタン画像全体を囲うようにドラッグしてください。 ​ ドラッグすると右下にSpriteウィンドウが表示されるので、Border内に画像の分割サイズを設定しましょう。 今回はLTRB全てに10を入力してください。 入力できたら右上のApplyをクリックして、SpriteEditorを閉じてください。 Buttonオブジェクトを選択して、名前をわかりやすいものに変更してください(サンプルではStage1) ​ ​ ボタンの大きさを好みに調整して、SauceImageに先ほど設定したスプライトStageButtonを設定しましょう。 ​ また、ImageTypeをTiledに変更してください(しなくてもOK) ImageTypeでは分割した画像の表示方法を設定できます。 Slicedでは中心に該当する画像は引き延ばされ、Tiledでは敷き詰められます(横線の数を数えるとわかりやすいと思います ) ​ 使用する画像によって使い分けてください。 子オブジェクトであるTextも同じように設定していきます。 ​ オブジェクトの名前をわかりやすいものに変更してください(サンプルではStage1Text) テキストの位置を調整して、ステージ名を入力しましょう。フォントはLesson4でダウンロードしたものを使用します。 ​ ​ 他にもフォントサイズやアウトラインなどを好みで調整してください。 ​※ 新しいMaterial Presetを使いたい場合は3Dアクションゲーム編の4-1を参考にしてください 同じようにクリア用のテキストとハイスコア用のTextMeshProを追加してください。 ​ ​位置や表示方法の設定はお好みで構いません。 ここまで実装できたらゲームを実行してみてください。 ボタンをクリックすることでボタンが反応することを確認できます。 ​ ​ しかし、ボタン外に表示されている子オブジェクトをクリックしてもボタンが反応してしまいます。 このままではボタンとして少し不自然なので修正していきましょう。 UIがクリックされたかどうかを検知するためにはRay(レイ)が使われています。レイはLesson1でも説明した通り、見えない光線を発射して衝突したオブジェクトの情報を取得する機能です。 スクリーンがクリックされた時、実はクリック位置から奥方向にレイが発射されています。そのレイがボタンに衝突した時に、ボタンが押されたとして判定しています。 逆に、UIにレイが衝突しないように設定することでクリックに反応しないようにすることができます。 ​ レイが衝突するかどうかを決めている項目は「Raycast Target」になります。この項目にチェックが入っているオブジェクトにはレイが衝突するようになります。 ボタン本体にはレイの衝突判定を行ってほしいので、Raycast Targetはチェックを入れておきます。子オブジェクトであるテキストのRaycast Targetのチェックを外しましょう。 ​ TextMeshProではRaycast Targetの項目は通常表示されていませんが、デバッグモードにすることで見ることができます 。 インスペクターの右上にある3つの点をクリックして、モードを「Normal」から「Debug」に変更してみてください。隠されていた色々な項目が表示されます。 TextMeshPro内のRaycast Targetという項目を探して、チェックを外しましょう。ステージ名、クリア用テキスト、ハイスコア用テキスト全てのRaycast Targetのチェックを外してください 。 設定が終わったら、モードを「Debug」から「Normal」に戻してください。 デバッグモードではprivateにしている変数の中身も見ることができます(編集はできません) ​ 項目が多くてややこしいので普段はノーマルモードにしておいた方が良いですが、デバッグの際にはデバッグモードにすることで効率よくデバッグを行えるようになります。 ​ Unity Tips! ここまでの手順が終わったら実行して、ボタンのクリックが正常に判定できることを確認してください。 ​ ​ 今後ボタンやクリック動作が正常に反応しない時は、まず対象のRaycast Targetのチェックが入っているか確認するようにしましょう。 ボタンをコピーして、ステージ2とステージ3のボタンも作成しましょう。ステージ名のテキストだけを適切な内容に変更しています。 今回はステージが3つだけなのでボタンをそのまま複製していますが、ステージの追加など今後を見越した開発をするならボタンをプレハブ化して、ステージの数だけ生成した方が良いでしょう。ステージが100個になったり、ステージ間に新しいステージを追加したいといった状況を想定すると、ボタンをコピーして配置していくのはかなり大変な作業になってしまいます。単純作業はプログラムに任せることで、短時間で高クオリティのゲームが完成します 。 ​ ​ 「ボタンをプレハブ化して生成する」実装については、近い内容の解説をLessonEXのモンスター図鑑で行っているので、たくさんステージを作成したい場合は参考にしてみてください。 Unity Tips! これでステージセレクトは完成です。 後はお好みで装飾やアニメーションを追加してみてください。 ​ 5-3 ステージ選択ボタンの実装 5-3 ステージ選択ボタンの実装 それでは「ボタンが押されたらシーンを切り替える」処理を実装していきましょう。 Buttonコンポーネントではボタンが押された時に実行する関数を指定することで、ボタンが押された時に様々な処理を実行することができます。 ​ SceneButtonスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; // シーンを扱う時に必要 public class SceneButton : MonoBehaviour { // ボタンが押された時に呼ばれる関数 // 引数には変更先のシーン名を入れる public void SceneChange(string sceneName) { // シーンを呼び出す SceneManager.LoadScene(sceneName); } } 外部から呼び出す関数なのでpublicにしておく点に注意してください。 ​ コードが書けたら保存してください。 StageButtonオブジェクトのButtonコンポーネントにある、On Click()内のプラスボタンをクリックしましょう。 先ほど作成したSceneButtonスクリプトをアタッチしてください。 ​ プラスボタンを押した時に、On Click()内にいくつか項目が追加されたと思います。その中でNoneと表示されている部分に、SceneButtonコンポーネントをドラッグ&ドロップしてください。 ドラッグ&ドロップしたことでNo Functionと表示されているボタンが押せるようになります。 ​ クリックすることでプルダウンメニューが開くので「SceneButton」→「SceneChange」と選択してください。ここでボタンが押された時にどの関数を実行するのか設定することができます。 関数を指定すると、SceneChange関数を宣言した際に作成した引数sceneNameを入力するための枠が追加されます。 ​ ここに遷移先のシーン名を入れることで、SceneChange関数内で入力した引数が使用されるようになります。 ステージ1の遷移先になるシーン名はStage1なので「Stage 1」と入力してください。 ​ ​ ここまでと同じ手順でステージ2、ステージ3のボタンも完成させてみましょう。Buttonは画面を操作するゲームでは欠かせない機能なのでここで慣れておいてください。 これでステージセレクトのボタンは完成です。 ​ しかし、実際にボタンを押してシーンを切り替えようとすると下記のようなエラーが発生してしまうはずです。シーンを扱う過去のレッスンを参考にして、このエラーを解決してみてください 。 降参 or 答え合わせの方はこちら ステージ1、ステージ2、ステージ3のボタンをクリックしてそれぞれのステージへ遷移することを確認してみてください。 これでシーンは切り替わりますが、一瞬で画面が切り替わるのは違和感があります。一度画面を暗転させて、暗転が終わってからシーンを切り替えるようにしましょう。 ​ 画面を暗転させるためには「最前面に真っ黒な画像をだんだん表示させる」→「完全に表示されたらシーンを切り替える」→「遷移先のシーンで最前面に表示した画像をだんだん透明にする」 という流れを作る必要があります。 ​ 少しだけ複雑な手順を踏みますが、フェードの処理は他のゲームでも再利用できるので頑張って作ってみましょう。 ​ ​ ​ まずはフェード用に新しいCanvasを作成してください。名前はFadeCanvasにしておきます。 Canvas下にImageを追加してください。名前はFadeImageにしておきます。 まずはCanvasの設定をしましょう。 CanvasコンポーネントのSort Orderは、複数のCanvasがあった場合の表示優先度を設定する項目です。 これからフェード用に真っ黒な画像を表示しますが、それがステージセレクトやゲームのUIより後ろに表示されると困ります。そのため、フェード用の画像は他のUIより前面に表示されるように設定する必要があります。 ​ Sort Orderの値が大きいCanvasほど前面に表示されます 。0より大きければ何でも構わないのですが、後ほど他のUIを追加する可能性も見越して、5など余裕を持った値にしておきましょう。 ​ ​ Canvas Scalerも他のCanvasと同じ設定にしておいてください。 次は画面全体に画像を引き伸ばしましょう。 今回は今までとは別の方法でサイズを調整します。 ​ FadeImageのRect Transform左上にあるアンカー をクリックしてください。 アンカーをクリックすると色々な項目が表示されます。Altキーを押しながら 左下のボタンをクリックしてください。画面全体にフェード画像が引き伸ばされたらOKです。 Colorを選択して、全ての項目を0にしてください。A(Alpha)の項目は不透明度です。 ​ これで画像が真っ黒かつ透明になります。 ​ 次はフェード処理のスクリプトを書きましょう。 ​ 新しいスクリプトFadeSceneを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; // Imageを扱う時に必要 using UnityEngine.SceneManagement; // Sceneを扱う時に必要 public class FadeScene : MonoBehaviour { bool m_fadeStart = false; // trueなら一連の処理を開始 bool m_fadeMode = false; // falseなら暗くなる trueなら明るくなる float m_alpha = 0.0f; // 画像の不透明度 [SerializeField] float FadeSpeed = 1.0f; // フェードの速度(大きいほど速い) // 遷移先のシーン名を保存 string m_sceneName; // 自身が使用するImageを保存 Image m_image; // フェード開始 public void FadeStart(string sceneName) { // フェード開始の準備をする m_fadeStart = true; m_sceneName = sceneName; // 自分の子オブジェクトにアタッチされているImageを取得する m_image = transform.GetChild(0).GetComponent(); // 自身はシーンをまたいでも削除されないようにする DontDestroyOnLoad(gameObject); } void Update() { // フェードが開始していないなら中断 if (m_fadeStart == false) { return; } // フェード処理 if (m_fadeMode == false) { // 画面を暗くする m_alpha += FadeSpeed * Time.deltaTime; // 完全に暗くなったのでシーンを変更する if (m_alpha >= 1.0f) { SceneManager.LoadScene(m_sceneName); // 明るくするモードに変更 m_fadeMode = true; } } else { // 画面を明るくする m_alpha -= FadeSpeed * Time.deltaTime; // 完全に明るくなったので自身を削除する if (m_alpha <= 0.0f) { Destroy(gameObject); } } // 最後に不透明度を設定する Color nowColor = m_image.color; nowColor.a = m_alpha; m_image.color = nowColor; } } コードが長いため一見難しく見えるかもしれませんが、やっていることは前述した「最前面に真っ黒な画像をだんだん表示させる」→「完全に表示されたらシーンを切り替える」→「遷移先のシーンで最前面に表示した画像をだんだん透明にする」という処理になります。 ​ 【プログラムの解説】 ​・transform.GetChild(x) で、自身のx番目の子オブジェクトを取得することができます。今回は自身の0番目の子オブジェクトなのでFadeImageを取得することになります。 ​・DontDestroyOnLoad(gameObject); を実行すると、引数にしたオブジェクトはシーンの変更に巻き込まれて消えないようになります。これを実行しないとシーンを切り替えた際にStageSelectと一緒にフェード画像も巻き込まれて消えてしまい、フェードが成立しなくなるので注意しましょう。 ​ ​ ​ ​ ここまで書けたら保存して、FadeCanvasにFadeSceneスクリプトをアタッチしてください。 Prefabフォルダ内に他のプレハブと区別できるように新しいフォルダFadePrefabを作成してください。 FadePrefabフォルダ内にインスペクターからFadeCanvasをドラッグ&ドロップして、 FadeCanvasをプレハブ化してください。プレハブ化できたら、インスペクター内のFadeCanvasは削除してください。 最後にSceneButtonスクリプトを修正して、FadeCanvasを生成&初期化できるようにしましょう。 赤い部分のコード を追加・修正してください。 ​ using System.Collections; using System.Collections.Generic; using UnityEngine; //using UnityEngine.SceneManagement; public class SceneButton : MonoBehaviour { [SerializeField] GameObject FadeCanvas; // ボタンが押された時に呼ばれる関数 // 引数には変更先のシーン名を入れる public void SceneChange(string sceneName) { // フェード用のCanvasを作成 GameObject fadeCanvas = Instantiate(FadeCanvas); // FadeSceneを取得してフェードを開始 fadeCanvas.GetComponent().FadeStart(sceneName); } } ボタンが押された時にFadeCanvasを作成して、そのFadeCanvasにアタッチされているFadeSceneに対して「フェードを開始してね、遷移先のシーンの名前は(sceneName)だよ」と教えている形になります。 ​ 以降の不透明度変更やシーン遷移などの処理は全てFadeSceneスクリプトがやってくれるので、後のことは一切気にせずにフェードを開始することができます。 ​ ここまでコードが書けたら保存して、各ボタンのSceneButtonコンポーネント内にあるFadeCanvasに、先ほどプレハブ化したFade Canvasをドラッグ&ドロップしてください。 ​ 3つ全てのボタンに対して設定が完了したら、ゲームを実行してボタンを押したときにフェード処理が動作するか確認してください。 このフェード処理は他のゲームでも使いまわすことができるので、お好みで改造して自分のゲームでもフェード処理を実装してみてください。 ​ ​ 5-4 クリア状況の保存 5-4 クリア状況の保存 ここまでのレッスンでボタンを押すことで各ステージに遷移できるようになりました。 今は全てのステージに自由に移動できますが、次は「プレイ可能フラグをロード 」して「プレイ可能フラグが立っているステージのみプレイできる 」ようにしましょう。 セーブという直観的でない要素を扱うので難しい点もありますが、すぐに全て理解する必要はありません。わかるところから読み解いていってください。 ​ ​ まずはプレイ可能フラグを格納する場所が必要です。ついでにクリア済みフラグとハイスコアも保存できるようにします。 ​ 新しくSaveDataスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class SaveData // : MonoBehaviour が消えているので注意! { // 各ステージに対する情報を格納するための構造体 [System.Serializable] public struct Stage_Data { public bool isPlay; // プレイ可能かどうか public bool isClear; // クリア済みかどうか public int highScore; // ハイスコア } // 各ステージの情報 public Stage_Data[] StageData; } このスクリプトは今までのスクリプトとは色々と違う点に気付いたでしょうか?Unity特有のコードは一切なく、セーブデータを定義するためだけのクラスになっています 。いきなり使いこなすのは難しいかもしれませんが、少しずつ解説していきます。 【プログラムの解説】 ​・[System.Serializable] はアトリビュートの一種で、付与したクラスや構造体はシリアライズ(シリアル化) 可能になります。直観的に理解するのは難しいと思いますが「後でセーブするためにこのコードが必要なんだな」くらいの認識で構いません(参考資料 ) ​ ・​MonoBehaviour はUnityが用意してくれている基本クラスで、スクリプトをアタッチする機能、Start関数やUpdate関数などゲームを作る上で欠かせない処理がまとまっています。 ​ セーブデータにStart関数やUpdate関数は必要ないので、今回はコメントアウトしておきましょう。 ​ ​・struct Stage_Data は構造体 です。構造体は複数の変数を1つにまとめたものです。 ​ 例えばRPGを作っていて勇者や魔法使いがいたとすると、それぞれ体力や攻撃力を持っているはずです。後から格闘家を追加したとしても、同じように体力や攻撃力のパラメータを持っています。 そういった共通の変数を管理する時には構造体を使うと便利です。構造体を使うことで、複数の変数をセットで扱うことができます。 今回はステージの開放状況、クリア状況、ハイスコアを記録していますが、後からクリア回数も記録したくなった時は構造体にパラメータを追加するだけでOKになります。ケアレスミスを防いだり、改良しやすいプログラムにするためにも構造体を活用してみましょう。 構造体についてはC++の教科書なども参考にしてください(参考資料 ) ​ ​・Stage_Data[] は可変長配列 です。この時点ではステージがいくつあるかわからないので、要素数を後から変更できるようにしています。可変長配列についてもC++の教科書を参考にしましょう。 ​ ​ ​ 今までの解説になかった要素がたくさん出てきて混乱したかもしれませんが、ここで行ったことはあくまで「セーブデータのベースを用意しただけ」なので難しく考えないでください。理解できる人は累計クリア回数や取得したスコアの合計などを保存するパラメータを追加してみても良いでしょう。 ​ ​ 次はセーブデータを作成してセーブ&ロードしてみましょう。こちらも全て理解する必要はありません。 ​ SaveManagerスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; // ファイルを扱う時に必要 public class SaveManager : MonoBehaviour { [SerializeField] SaveData NowSaveData; // セーブデータ public SaveData GetSaveData() { return NowSaveData; } string m_filePath; // 書き込み先のファイルパス // 最初に実行 void Awake() { // 自身はシーンをまたいでも削除されないようにする DontDestroyOnLoad(gameObject); // 最初にセーブデータを読み込む m_filePath = Application.persistentDataPath + "/" + ".savedata.json"; bool isLoad = Load(); // ロードに失敗した場合はセーブデータを初期化して新規にセーブする if (isLoad == false) { NowSaveData.StageData = new SaveData.Stage_Data[3]; // ステージ数だけデータを作成 // 簡易的に初期化 NowSaveData.StageData[0].isPlay = true; // ステージ1だけプレイ可能 NowSaveData.StageData[0].isClear = false; NowSaveData.StageData[0].highScore = 0; NowSaveData.StageData[1].isPlay = false; NowSaveData.StageData[1].isClear = false; NowSaveData.StageData[1].highScore = 0; NowSaveData.StageData[2].isPlay = false; NowSaveData.StageData[2].isClear = false; NowSaveData.StageData[2].highScore = 0; // 初期化したデータを使って保存 Save(); } } // 現在の状況をセーブする public void Save() { string json = JsonUtility.ToJson(NowSaveData); StreamWriter streamWriter = new StreamWriter(m_filePath); streamWriter.Write(json); streamWriter.Close(); } // 現在の状況をロードする // ロードに成功したらtrue、失敗したらfalseを返す public bool Load() { // セーブデータがあるか確認 if (File.Exists(m_filePath)) { StreamReader streamReader; streamReader = new StreamReader(m_filePath); string data = streamReader.ReadToEnd(); streamReader.Close(); NowSaveData = JsonUtility.FromJson(data); // ロードできたのでtrueを返す return true; } // セーブデータが見つからなかったのでfalseを返す return false; } } こちらのスクリプトも見慣れないコードが多数あります。ただしほとんどがセーブ&ロード関連なので頻繫には使いません。無理に覚えなくても大丈夫です。 処理の流れとしては「ファイルパスを作成」→「作成したファイルパスを用いてロードする」→「ロードに成功したらそのまま読み込む、失敗したら新しいセーブデータを作成して保存する」といったものになります。 ​ 【プログラムの解説】 ​・m_filePath = Application.persistentDataPath + "/" + ".savedata.json"; では、セーブデータ書き込み先のファイルパスを作成しています。 Application.persistentDataPathはUnityが「永続的なデータはここに保存してね」と推奨しているパスを返してくれます。 ​ ​・ロードに失敗した場合、セーブデータを作成して新規に保存する処理を行っています。 NowSaveData.StageData = new SaveData.Stage_Data[3]; では配列の要素数を設定しています。それぞれのステージ数に応じて変更してください。 ・​Save関数内ではセーブ用の特殊な処理を行っています。 ​今はあまり気にしなくても構いません。 ​ ​・File.Exists 関数は引数に指定したファイルが存在するかどうかを確認して、存在するならtrueを返す関数です。ここでセーブデータが存在するかチェックして、今後の処理を分岐させています。 ​ ロード用の処理については今はあまり気にしなくても構いません。 ​ ​ ​ コードが書けたら保存して、StageSelectシーンに空オブジェクトを追加してください。 ​追加した空オブジェクトの名前はSaveObjectにしておきます。 SaveObjectにはSaveManagerタグを作成して設定しましょう。先ほど作成したSaveManagerスクリプトをアタッチしてください。 後はセーブデータにアクセスして、各ボタンのパラメータを変更できるようにしましょう。 StageButtonスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; // UIを扱う時に必要 using TMPro; // TextMeshProを扱うときに必要 public class StageButton : MonoBehaviour { [SerializeField] int StageNo = 0; // ステージ番号 [SerializeField] GameObject ClearObject; [SerializeField] TextMeshProUGUI HighScoreText; void Start() { // セーブデータを取得する SaveManager saveManager = GameObject.FindGameObjectWithTag("SaveManager"). GetComponent(); // プレイ可能か判定 if (saveManager.G etSaveData().StageData[StageNo].isPlay == false) { // プレイ不可なので押せないようにする GetComponent().interactable = false; } // クリア済みか判定 if (saveManager.GetSaveData().StageData[StageNo].isClear == false) { // 未クリアなので非アクティブにする ClearObject.SetActive(false); } // ハイスコアの表示 int highScore = saveManager.GetSaveData().StageData[StageNo].highScore; HighScoreText.text = "HIGHSCORE:" + highScore; } } 【プログラムの解説】 ​・Buttonクラスのパラメータであるinteractableは、ボタンが押せるかどうかを設定するパラメータです。interactableをfalseにすると、ボタンをクリックしても反応しなくなります。 今回は「プレイ可能フラグがfalseの時にinteractableをfalseにする」ことで、プレイできないステージを開始できないようにしています。 ​ ​ コードが書けたら保存して、Stage1やStage2などそれぞれのボタンにアタッチしてください。StageNoにはそれぞれのステージ番号(Stage1は0番、Stage2は1番)、Clear ObjectとHigh Score Textには適切なオブジェクトをドラッグ&ドロップして設定してください。 ボタンの設定ができたらゲームを実行してみてください。 ​ インスペクターには読み込んだセーブデータが表示されて、Stage1だけが遊べるようになっているはずです。 実際にm_filePathで指定した場所にセーブデータが保存されているか見てみましょう。 ​ (ユーザー名)\AppData\LocalLow\DefaultCompany(Project Settings内のCompany Name)\(プロジェクト名) にセーブデータが保存されているはずです。 メモ帳などのテキストエディタでセーブデータの中身を見ることができます。 ​セーブデータを開いてみると、かなり単純な形で保存されていることがわかります。 改変も簡単にできます。ハイスコアの値を変更して保存してみてください。 ​ 簡単にセーブデータを書き換えられました。 このようにjson形式は単純な書き方なので、ちょっと詳しい人であれば簡単に改変できてしまいます 。 今回は練習なので省略しますが、実際にゲームを配布する際にはセーブデータを暗号化するとよい でしょう。string(文字列)型を暗号化・複合化する方法は色々あるので各自で調べてみてください(参考例 ) Unity Tips! これで一通りの処理が完成しましたが、実は少しだけ不便な点が残っています。 ​ SaveObjectはシーンが切り替わっても消えないように設定しました。しかし、そもそもSaveObjectがStageSelectシーンにあるため、StageSelectシーンを介さないとセーブデータにアクセスできないようになってしまっています 。 このままではデバッグする際にも毎回StageSelectシーンから始める必要が出てしまいます。それでは疲れますし、効率も悪いですね。 ​ Unityにはどのシーンから開いたとしても最初に呼び出される関数を設定する機能があります。ここでSaveObjectを生成して、どのシーンから始めてもSaveObjectが存在するようにしましょう。 ​ ​ 新しくGameStartスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameStart : MonoBehaviour { // どのシーンから開始しても最初に呼ばれる関数 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void Init() { // セーブオブジェクトを生成 GameObject saveObject = (GameObject)Resources.Load("SaveObject"); Instantiate(saveObject); } } 【プログラムの解説】 ・[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] は長いですが、アトリビュートの一種です。この属性を付与した関数は、ゲーム起動時のシーンを開く前に呼ばれます。対象とする関数をstaticにする必要があるので注意しましょう。 このスクリプトはどこにもアタッチしないので、今までとは違う方法で生成するオブジェクトを指定する必要があります。 今回はプロジェクトウィンドウ内から名前でプレハブを取得してみましょう。 ​ プロジェクト内に「Resources 」フォルダを作成してください。打ち間違えるとエラーが起きるので注意してください。 作成したResourcesフォルダ内に、ヒエラルキーからSaveObjectをドラッグ&ドロップしてプレハブ化しましょう。 ​ プレハブ化したらヒエラルキー内のSaveObjectは削除してください。 先ほどInit関数内に記述した (GameObject)Resources.Load("SaveObject"); は、Resourcesフォルダ内にある引数名のオブジェクトを返す関数です。 今まではオブジェクトを指定するときにヒエラルキーからインスペクターへドラッグ&ドロップしていましたが、このような指定方法もあると覚えておいてください。 Resourcesフォルダは便利ですが、何も考えずに使っていいものではありません。 ​ ​ Resourceフォルダに入っているファイルはビルド時にまとめて読み込まれます。読み込んだデータはゲームの起動中は常駐するため、大量のファイルをResourceフォルダに入れているとゲームの起動が遅くなったり、メモリを圧迫したりする危険性があります。特に大きな画像や音声ファイルなどの大容量のファイルを入れていると、メモリ使用量が大幅に増加してしまいます。 そのため Resourceフォルダを使用するのはゲームの起動中に常駐するリソースや、メモリを圧迫しないリソースに絞るようにしましょう 。 Unity Tips! ​ ここまでできたらStageSelectシーンやStage1シーンから実行して、どのシーンからゲームを開始してもSaveObjectが生成されていることを確認してみてください。 5-5 クリア処理の実装 5-5 クリア処理の実装 SaveObjectが完成したので、クリア時の処理を実装していきます。 ​まずは作りかけで終わっていたクリア画面を完成させましょう。 ​ ​ Stage1シーンを開いて「UI」→「Canvas」を選択して作成してください。名前はClearCanvasにしておきます。 ​ Canvasと同時にEventSystemが生成されますが、EventSystemはUIシーンにすでに含まれているためStage1シーンには必要ありません。Stage1シーンにあるEventSystemは削除しておきましょう。 EventSystemはクリックなどの入力に応じてUIに対してレイを発射したりイベントを送信したりしてくれるコンポーネントです。 これがないとボタンをクリックしても反応しなくなってしまいます。 EventSystemはシーン上に1つしか存在できず、複数あった場合警告文が出てしまうので注意しましょう。 Unity Tips! ClearCanvasのUI Scale Modeを「Scale With Screen Size」に変更して、画面サイズが変わってもCanvasのサイズが調整されるようにしてください。 ​ ​ ClearCanvasにクリア時のUIを追加していきましょう。 ​ ​ サンプルでは黒い半透明の背景、クリアテキスト、スコア、ハイスコア、スコア更新時のテキストを表示しています。例のごとくテキストの内容は仮の値にしています。 ​ 配置や設定などはお任せするので、自由にクリア画面を作ってみてください。TextMeshProの設定については3Dアクションゲーム編のLesson4 を参考にしましょう。 ステージセレクトに戻るボタンを配置しましょう。 「UI」→「Button - TextMeshPro」を選択してボタンを追加してください。 ​ ボタンやテキストの位置や設定を調整してください。 サンプルではボタンの背景はステージセレクトのものを流用しています。 これでクリア画面のベースは完成しましたが、一瞬で表示されるだけだと味気ないのでアニメーションを追加しましょう。 ​ ​ Animationフォルダ内にClearAnimationフォルダを作成して、新しいアニメーションを追加してください。追加したアニメーションをClearCanvasにドラッグ&ドロップしましょう。 Animationでは子オブジェクトも対象にすることができます 。今回ではClearCanvasの子オブジェクトになっているテキストやボタンにもキーを設定できるというわけです。 ​ 追加したアニメーションをダブルクリックしてAnimationウィンドウを開いてください。ClearCanvasを選択した状態で「Add Property」をクリックすると、子オブジェクトの名前も表示されることが確認できます。 ​ 自由にキーを配置して、クリア画面のアニメーションを作成してください。 ​※ ハイスコア更新のテキスト(サンプルだと「New Record!!」のテキスト)は後ほどスクリプトでアクティブ状態を変更するので、アニメーションでは変更しないようにしてください クリアアニメーションを確認して、ループ再生になっていた場合はLoop Timeのチェックを外しておいてください。 ​ 次はクリア用の処理を作っていきます。 ​ ​ 新しいスクリプトUI_Clearを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; public class UI_Clear : MonoBehaviour { [SerializeField] TextMeshProUGUI ScoreText, HighScoreText; [SerializeField] GameObject NewRecordObject; public void SetScoreText(int score,int highScore,bool isNewRecord) { // スコアテキストの更新 ScoreText.text = "SCORE:" + score; HighScoreText.text = "HIGH SCORE:" + highScore; // 新記録が出た場合のみアクティブ if (isNewRecord) { NewRecordObject.SetActive(true); } else { NewRecordObject.SetActive(false); } } } // スコアテキストの更新 内のtextはお好みで設定してください。 ​ プログラムが書けたら保存して、ClearCanvasにUI_Clearをアタッチしてください。 インスペクター内に表示された項目に、ヒエラルキーから適切なオブジェクトをドラッグ&ドロップして設定しましょう。 ・Score Text → スコアの値を表示しているテキスト ・High Score Text → ハイスコアの値を表示しているテキスト ​・New Record Object → ハイスコア更新時に表示されるテキスト(オブジェクト) ステージセレクトに戻るためのボタンに、以前作成したSceneButtonスクリプトをアタッチしてください。FadeCanvasには 「Prefab」→「FadePrefab」内のFadeCanvasをドラッグ&ドロップして設定します。 SceneButtonコンポーネントの準備ができたら、ステージセレクトと同じように ボタンがクリックされた時に呼ばれる関数を設定していきましょう。 ​ ① On Click()内のプラスボタンをクリックする ② On Click()内の左下(Noneと表示されている所)にアタッチしたScene Buttonコンポーネントをドラッグ&ドロップする ③ On Click()内の右上(No Functionと表示されている所)をクリックして「SceneButton」→「SceneChange」を選択する ④ On Click()内の右下に遷移先のシーン名「StageSelect」を入力する これでClearCanvasの準備は完了です。 ​ Prefabフォルダ内にClearPrefabフォルダを作成して、その中にClearCanvasをドラッグ&ドロップしてプレハブ化しましょう。 ​ プレハブ化したらヒエラルキー内のClearCanvasは削除してください。 GameManagerスクリプトを開いて赤い部分のコード を追加してください。少し長いですが、処理的にはあまり難しくありません。 ~前略~ // スコアを加算 public void AddScore(int score) { Score += score; // 現在のスコアを渡してUI更新 m_uiScore.ScoreUpdate(Score); } // スコアを取得 public int GetScore() { return Score; } [SerializeField, Header("クリアキャンバス")] GameObject ClearCanvasObject; [SerializeField, Header("ステージ番号")] int StageNo; [SerializeField, Header("解放されるステージ番号")] int NextStageNo; // ゲームクリア処理 public void GameClear() { // SaveObjectからセーブデータを取得 SaveData saveData = GameObject.FindGameObjectWithTag("SaveManager"). GetComponent().GetSaveData(); // セーブデータから現在のステージのハイスコアを取得する int highScore = saveData.StageData[StageNo].highScore; // 現在のスコアがハイスコアを上回っていたら新記録フラグを立てる bool isNewRecord = false; if(Score > highScore) { isNewRecord = true; } // クリアキャンバスを生成する GameObject clearCanvas = Instantiate(ClearCanvasObject); // 生成したクリアキャンバスにスコア、ハイスコアを教える clearCanvas.GetComponent().SetScoreText(Score, highScore, isNewRecord); // セーブデータの更新 // クリアフラグを立てる saveData.StageData[StageNo].isClear = true; // 次のステージの挑戦可能フラグを立てる saveData.StageData[NextStageNo].isPlay = true; // ハイスコアが出た場合は現在のスコアをハイスコアとして保存する if (isNewRecord) { saveData.StageData[StageNo].highScore = Score; } // セーブ処理 GameObject.FindGameObjectWithTag("SaveManager"). GetComponent().Save(); } // Awakeなので注意 void Awake() { // UIシーンを合成 SceneManager.LoadScene("UI", LoadSceneMode.Additive); } void Start() ~後略~ 処理の流れは「タグを使ってセーブデータを 取得」→「ハイスコアを更新したか判定」→「ClearCanvasを生成、テキストを更新」→「取得したセーブデータを更新」→「セーブ処理」となっています。 ​ コードが書けたら保存して、クリアした瞬間にGameClear関数が呼び出されるようにしましょう。 ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // クリア演出 void GameClearAnimation() { // もう終わっている場合は中断 if (m_claerWait) { return; } // アニメーションを変更 m_animator.SetTrigger("Clear"); // カメラ演出 Camera.main.GetComponent().CameraClearAnimation(); // クリア処理を行う m_gameManager.GameClear(); // ゲームクリア演出終了 m_claerWait = true; } ~後略~ これでクリアした瞬間にClearCanvasが作成されたり、ハイスコアが保存されたりするようになりました。 ​ コードが書けたら保存して、Gameのプレハブをダブルクリックして開いてください。 インスペクターに先ほど追加した変数が表示されているので、ClearCanvasObjectにClearCanvasのプレハブをドラッグ&ドロップしてください。 プレハブ側を編集すると、同じプレハブから生成した全てのオブジェクトに変更が反映されます。Stage1やStage2に1つずつ設定していかなくても、一括でクリアキャンバスの設定が完了しました。 ClearCanvasを選択したときなどにインスペクターが切り替わってうまくドラッグ&ドロップできない場合は、インスペクター右上にある鍵のアイコンをクリックすることでインスペクターをロックすることができます。 再度クリックすることで解除できます。 ロックされている間は他のオブジェクトをクリックしてもインスペクターの内容が変更されることはありません。 ​ スムーズにインスペクターを操作するためにも覚えておくと良いでしょう。 Unity Tips! Stage1シーンに戻って、GameManagerに現在のステージ番号と解放されるステージ番号を設定してください。 Stage2シーンとStage3シーンも同じように設定します。 ​ ​・Stage1 … StageNo=0 NextStageNo=1 ・Stage2 … StageNo=1 NextStageNo=2 ・Stage3 … StageNo=2 NextStageNo=2 ( 最後のステージは自身のステージ番号と同じにする) GameManagerの設定ができたら実行して、ステージをクリアすると次のステージが遊べるようになることを確認してみてください。 また、ゲームを再プレイして、クリア状況やハイスコアが保存されていることも確認してみてください。 5-6 ゲームオーバー処理の実装 5-6 ゲームオーバー処理の実装 最後にゲームオーバーの処理を実装します。と言っても、シーンを遷移する処理は完成しているので特に難しい処理などはありません。 ​ これでゲームの処理は一通り完成なので頑張りましょう! ​ ​ ゲームクリアと同じように「UI」→「Canvas」からGameOverCanvasを作成してください。 ゲーム中のUI用キャンバス(優先度0)とフェード用キャンバス(優先度5)の間になるように、優先度を設定してください。ここではSort Orderを1としておきます。 Canvas ScalerのUI Scale Modeを「Scale With Screen Size」に変更して、画面サイズを設定するのを忘れないようにしてください。 クリアキャンバスと同じように、GameOverCanvas下にUIのパーツを設置していきましょう。 ​ サンプルでは半透明の黒い背景、ゲームオーバーテキスト、リトライボタン、ステージセレクトに戻るボタンを設置しています。 ボタンがクリックされた時の処理を設定しましょう。 ​ リトライを演出するには SceneManager.LoadScene(現在のシーン名); という処理を行う必要があります。ですが、今のSceneButtonスクリプトにはそのような処理が実装されていないので、SceneButtonスクリプトを改造して対応しましょう。 SceneButtonスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class SceneButton : MonoBehaviour { [SerializeField] GameObject FadeCanvas; // ボタンが押された時に呼ばれる関数 // 引数には変更先のシーン名を入れる public void SceneChange(string sceneName) { // 名前が空白だった場合、現在のシーンの名前を使う if (sceneName == "") { sceneName = SceneManager.GetActiveScene().name; } // フェード用のCanvasを作成 GameObject fadeCanvas = Instantiate(FadeCanvas); // FadeSceneを取得してフェードを開始 fadeCanvas.GetComponent().FadeStart(sceneName); } } 【プログラムの解説】 ​・遷移先のシーン名が空白だった場合、現在のシーン名を置き換えて使用するように改造しています。 SceneManager.GetActiveScene().name で現在のシーン名を取得することができます。 ​ ​ コードが書けたら保存して、各ボタンの設定をしてください。 ​ 遷移先のシーン名はリトライボタンは空白 (現在のシーン名に置き換わる)、ステージセレクトに戻るボタンは「StageSelect」 にしましょう。 ボタンの設定ができたらGameOverCanvasをプレハブ化してください。 ​ プレハブ化できたらヒエラルキー内にあるゲームオーバーキャンバスは削除しておきましょう。 後はゲームオーバー時にGameOverCanvasを生成するだけになります。 ​ GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // スコアを取得 public int GetScore() { return Score; } [SerializeField, Header("ゲームオーバーキャンバス")] GameObject GameOverCanvasObject; [SerializeField, Header("ゲームオーバー画面生成時間")] float GameOverTime = 1.0f; // ゲームオーバー処理 public void GameOver() { // 少し待ってからゲームオーバーキャンバスを生成する Invoke("MakeGameOverCanvas", GameOverTime); } // ゲームオーバーキャンバス生成 void MakeGameOverCanvas() { Instantiate(GameOverCanvasObject); } [SerializeField, Header("クリアキャンバス")] GameObject ClearCanvasObject; [SerializeField, Header("ステージ番号")] int StageNo; ​~後略~ Invoke関数はクリアアニメーションの時にも使用したもので、名前が(第一引数)の関数を(第二引数)秒後に実行してくれる関数です。 ゲームオーバーになった瞬間にゲームオーバーキャンバスが出てしまうと、プレイヤーが吹き飛ばされる演出が見えなくなってしまうので、サンプルでは1秒後にキャンバスを生成しています。 ​ ​ 続いて、PlayerMoveスクリプトに赤い部分のコード を追加してください。 ~前略~ // ゲームオーバー public void GameOver() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_GameOver); // ゲームオーバー処理 m_gameManager.GameOver(); // アニメーションを変更 m_animator.SetTrigger("GameOver"); // 重力をリセット m_player_rb2d.velocity = Vector2.zero; // 重力を戻す m_player_rb2d.gravityScale = m_defGravity; // 左上に力を加える m_player_rb2d.AddForce(new Vector2(-5.0f, 7.0f), ForceMode2D.Impulse); // コライダーを無効にする GetComponent().enabled = false; } // ゲームクリア public void GameClear() { ​~後略~ コードが書けたら保存して、Gameプレハブを開いてください。 インスペクターに先ほど追加したゲームオーバーキャンバス設定用の変数が表示されているので、ゲームオーバーキャンバスのプレハブを設定してください。 プレハブにゲームオーバーキャンバスを設定できたら、ゲームを実行してください。 ゲームオーバーになると数秒後にゲームオーバーキャンバスが表示されたら完成です。リトライボタンを押すとステージを再挑戦でき、ステージセレクトボタンを押すとステージセレクトに戻ることも確認しておきましょう。 評価テスト 評価テスト 評価テスト これでゲームとして一通り遊べる状態になりました。 次のレッスンではタイトル画面やエフェクト、効果音を実装してゲームを完成させましょう。 ​ 【評価テスト】 https://forms.gle/z8SVhEEBwb1DkUtw9 Next Lesson6「クオリティを上げよう」 ページ TOP 5-1 複数のステージを作成 5-2 ステージセレクトを作成 5-3 ステージ選択ボタンの実装 5-4 クリア状況の保存 5-5 クリア処理の実装 5-6 ゲームオーバー処理の実装 評価テスト

  • 3D脱出ゲーム編 Lesson2「アイテムを実装しよう」 | Unity1gc2

    3D脱出ゲーム編 Lesson2 アイテムを実装しよう 2-1 基底クラス 2-1 基底クラス ここからはゲームの肝であるアイテムを作っていきます。どうしても少しややこしい部分になってしまいますが、頑張って作っていきましょう。 ​ ​ このゲームには調べられるものがたくさんあります。例えば台や戸棚、出口のボタンなどです。これらはそれぞれ調べた時の処理は違いますが「カーソルが合った時の処理」は全て同じになります。 こういった共通の処理やパラメータがある時は基底クラス を使ってみましょう。 ​ ItemObjectクラスにカーソルが合った時の処理を書き、ItemObjectクラスを継承 した派生クラス に調べた時の固有の処理を書きます(戸棚が開く、ボタンを押すなど)ItemObjectクラスをいう基底クラスを継承することによって、カーソルが合った時の処理を何度も書く必要がなくなります。 基底クラスの恩恵は現時点ではわかりにくいと思いますが、Lesson3の終盤あたりになってくるとわかりやすくなるはずです。難しい人はあまり気にしなくても構いません。 それではまず基底クラスであるItemObjectクラスを作成しましょう。いつも通りItemObjectスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemObject : MonoBehaviour { [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; void Awake() { // アウトラインの初期化 m_outline = GetComponent(); m_outline.OutlineMode = Outline.Mode.OutlineVisible; m_outline.OutlineColor = Color.red; m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH; m_outline.enabled = false; } // アイテムを調べた時の仮想関数 public virtual void ItemCheck() { // 調べた時の処理を行う ItemGet(); } // アイテムを調べた時の基本処理 public void ItemGet() { // デバッグ用 アイテム名と説明文をコンソールに出力 Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation); } // 自分にカーソルが合った時 public virtual void StartSelect() { // アウトラインを表示する m_outline.enabled = true; } // 自分がカーソルから外れた時 public virtual void EndSelect() { // アウトラインを削除する m_outline.enabled = false; } } 【プログラムの解説】 ​・Multiline(3)はアトリビュートの一種で、インスペクターにstring型を表示する際に指定した行数表示できるようにするものです。今回ではインスペクターに3行表示できるようになっています。 ​ あくまでインスペクターでの表示を制御するものであって、4行以上入力できないわけではないので注意してください。 ・const のついた変数は定数 として扱われます。定数は宣言した時のみ値を指定でき、処理の途中で値を変更することはできません。プログラムの途中にうっかり値を変えてしまわないように、固定の値は定数にしておくといいでしょう。 ・public virtual void ItemCheck など virtual がついている関数がいくつかあります。 これは仮想関数 というもので、基底クラスを継承した派生クラスで関数の中身を再定義することができる関数です。現時点では何を言ってるのかわからないかもしれませんが、Lesson3で詳しく説明するので今はあまり気にしなくて構いません。C++でも継承、仮想関数の授業はやっているはずなので詳しくはC++の教材を確認してください。 ​ ・¥nは改行文字で、string内で入力することで改行することができます。​ このサイトでは¥を直接半角入力できないので、全角で表示しています。実際に記述するときは半角にしてください 。 ​ ここまで書けたら保存してください。試しに黄色い本を配置してみましょう。 ​ ​ プロジェクトウィンドウ内の「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」→「Books」を選択し、book_singleをドラッグ&ドロップで食堂の机の上に配置してください。 座標はX=61.4 Y=5.2 Z=-120で、回転はX=0 Y=-90 Z=90に設定します​。 配置したbook_singleの名前を「Item_YellowBook」にして、新しいタグ「Item」を追加して設定してください(タグの作り方は3Dアクションゲーム編の1-6 を確認してください)​ 本の表紙の色を変更します。Mesh RendererのMaterials内のElement1をgoldに変更してください。​表紙のマテリアルが変わったらOKです。 黄色い本以外にも今後様々なアイテムやギミックを設置していきます。インスペクターで見やすいように1つの親オブジェクトにまとめてしまいましょう。 ​ ​ 新しい空オブジェクトGimmickを作成して、Item_YellowBookを子オブジェクトにしてください。 ​以降も追加したギミックは随時Gimmickの子オブジェクトにしていきます。 Item_YellowBookのプレハブ化を解除しておきましょう。Item_YellowBookを右クリック→「Prefab」→「Unpack Completely」を選択してください。 黄色い本に先ほど作成したItemObjectコンポーネントと同梱したOutlineコンポーネントをアタッチしてください。OutlineはC#スクリプトの方を選択してください。 (ItemObjectは継承せずにそのまま使用しても動作するように作っていきます) インスペクターに先ほど追加したNameとExplanationが表示されているので、適当に名前と説明文を入力してください。これはテスト用なので適当でも構いません。 ​ Outlineには何もしなくてOKです。 次はアイテムを調べるためのカーソルを実装しましょう。 ​ ​ UnityにはSphereCast という機能があります。これは「透明な球体の当たり判定を指定した方向に発射して、最初に衝突したオブジェクトの情報を取得する」というものです。 これでカメラの前方向に球体を発射して、衝突したオブジェクトの情報を取得することで前方にあるアイテムを調べられるようにしましょう。 Unityには他にも球体ではなく線を発射するRaycastという機能もありますが(2Dランゲーム編1-6で解説 )線ではなく球体を発射した方が判定を広く取れるため、今回はSphereCastを使用します。 ​ それではPlayerItemスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerItem : MonoBehaviour { GameObject m_cameraObject; / / メインカメラ GameObject m_hitObject; // 選択中のオブジェクト const float SPHERE_RADIUS = 0.8f; // SphereCastで発射する球体の半径 const float SPHERE_MAX_DISTANCE = 16.0f; // SphereCastで球体を発射する距離 void Awake() { // メインカメラを取得する m_cameraObject = Camera.main.gameObject; } void Update() { // 球体を発射する RaycastHit hit; if (Physics.SphereCast(m_cameraObject.transform.position, SPHERE_RADIUS, m_cameraObject.transform.forward, out hit, SPHERE_MAX_DISTANCE)) { // 今見ているオブジェクトと違う場合は選択終了 if (m_hitObject != hit.collider.gameObject && m_hitObject != null) { EneSelect(); } // 衝突したオブジェクトを取得 ItemObject itemObject = hit.collider.gameObject.GetComponent(); if (itemObject != null) { // 選択中の処理 itemObject.StartSelect(); m_hitObject = hit.collider.gameObject; // 決定時の処理 if ((Input.GetKeyDown("joystick button 0") || Input.GetKeyDown(KeyCode.Return))) { // アイテムに応じた処理 itemObject.ItemCheck(); } // アイテム使用時の処理 if ((Input.GetKeyDown("joystick button 2") || Input.GetKeyDown(KeyCode.I))) { // アイテムに応じた処理 itemObject.ItemUse(); } } } else { // どのオブジェクトにもヒットしていないので選択終了 if (m_hitObject != null) { EneSelect(); } } } // 選択終了 void EneSelect() { m_hitObject.GetComponent().EndSelect(); m_hitObject = null; } } 【プログラムの解説】 ​・RaycastHit はSphereCastを行ってヒットしたオブジェクトの情報を格納する場所になります。 ・Physics.SphereCast は前述の通り、球体を発射して衝突したオブジェクトの情報を取得する関数です。 ​ 第一引数に発射の始点、第二引数に発射する球体の半径、第三引数に発射する方向ベクトル、第四引数に衝突したオブジェクト情報の格納先(RaycastHit)、第五引数に球体を発射する距離を指定します。 SphereCastを行って何かにヒットした場合はtrueを返し、しなかった場合はfalseを返します。if文の中で SpheteCastを行っているのは、何かにヒットした時だけ処理を行うためです。 ​ ・(Input.GetKeyDown("joystick button 0") || Input.GetKeyDown(KeyCode.Return) でゲームパッドのAボタンが押されたときかエンターキーが押された時の判定を行っています。 ​ ​ ​ プログラムが書けたら保存して、プレイヤーにPlayerItemをアタッチしてください。 今のままではどこを注目しているかわかりにくいので、カーソルを表示しましょう。 ​ ​ 「UI」→「Image」を選択してください。 自動でCanvasが追加されるので、画面サイズが変わってもCanvasの大きさを自動調整するようにしましょう。UI Scale Modeを「Scale With Screen Size」にして、Xを800、Yを600に設定してください。 追加したImageの座標を中心にして、WidthとHeightを50に設定してください。Source ImageはCursorにしましょう。 Canvasの中央に赤いカーソルが表示されたら完成です。​ それではゲームを実行して、先ほど設置した黄色い本にカーソルを合わせてみてください。 ​ 赤いアウトラインが表示されたらSphereCastで判定が取れているということになります。カーソルを合わせた状態でAボタン(Enterキー)を押して、ログが出力されることも確認してください。 2-2 アイテムを拾う(前編) 2-2 アイテムを拾う(前編) カーソルの判定が取れたので、次はアイテムを拾ってみましょう。このゲームではアイテムを3つまで持ち歩くことが可能です。所持しているアイテムを管理するために、それぞれのアイテムにID(番号)を割り振り、配列に入っているIDで所持アイテムを管理するようにしましょう。 まずはアイテムリストを作成しましょう。 ​ ItemDataスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; // アイテム用のデータベース [CreateAssetMenu(fileName = "ItemDataBase", menuName = "CreateItemDataBase")] public class ItemData : ScriptableObject { // アイテム情報の構造体 [System.Serializable] public struct Item { public string ItemName; // アイテム名 [Multiline(2)] public string ItemExplanation; // アイテム欄で表示する説明文 public GameObject ItemPrefab; // アイテムを捨てた時に生成するオブジェクト } // アイテムリストの可変長配列 public Item[] Items; } 見慣れない要素がたくさんあって戸惑うかもしれませんが、そこまで難しい内容ではないので1つずつ確認していきましょう。 【スクリプトの解説】 ​・CreateAssetMenu はクラス名の前に記述することで、 スクリプトからアセットを作成できるようにするアトリビュートです。 [CreateAssetMenu(fileName = "アセットのデフォルト名" , menuName = "Createメニューに表示される内容")] ​ 使用するクラスには public class ItemData : ScriptableObject のようにScriptableObjectクラスを継承する必要があるので注意しましょう。 作成したアセットは他のスクリプトから参照することができます。今回のようなアイテム一覧であったり、キャラクターのステータスの管理などに活用できます。 (公式リファレンス ) ​ ​ ​・public struct Item は構造体 です。構造体は複数の変数を1つにまとめたものです。 今回はアイテム名、説明文、捨てた時に生成するオブジェクトという3つの変数をまとめたものを「Item」と名付けています。 構造体についてはC++の教科書なども参考にしてください(参考資料 ) ​ ​・public Item[] Items; は前述したItem構造体の可変長配列 です。可変長配列についてもC++の教科書を参考にしてください。 ​ 配列の0番に黄色い本の情報、4番に赤の宝玉の情報…といった風に各番号にアイテムの情報が入っています。この番号が所持アイテムを管理する際のアイテムIDになります。 それではデータベースを作成しましょう。 ​ ​ プロジェクト内で右クリックして「Create」→「CreateItemDataBase」を選択してください。アイテムの情報を管理するアセットが追加されます。 インスペクターには先ほど作成したアイテムリストが表示されています。 Itemsの+ボタンを押すと要素数を増やすことができます。要素数を増やしておきましょう。 まずはElement0に黄色い本のデータを記述しましょう(アイテム名や説明文はお好みで調整しても構いません) 【サンプルの記入例】 ・ItemName :黄色い本 ・​ItemExplanation :黄色い表紙の本 中身は美術書のようだ ・ItemPrefab :(未設定) 次は先ほどの黄色い本にアイテム番号とアイテムリストの情報を設定します。 ​ ItemObjectスクリプトを開いて赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemObject : MonoBehaviour { [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")] int ItemID = -1; // アイテム識別用の番号 [SerializeField] ItemData ItemDataBase; // アイテムリスト [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; void Awake() { // アウトラインの初期化 m_outline = GetComponent(); m_outline.OutlineMode = Outline.Mode.OutlineVisible; m_outline.OutlineColor = Color.red; m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH; m_outline.enabled = false; } // アイテムを調べた時の仮想関数 public virtual void ItemCheck() { // 調べた時の処理を行う ItemGet(); } // アイテムを調べた時の基本処理 public void ItemGet() { if (ItemID == -1) { // アイテムを調べた // デバッグ用 アイテム名と説明文をコンソールに出力 Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation); } else { // アイテムを取得 // デバッグ用 獲得したアイテム名をコンソールに出力 Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得"); // 自身を削除する Destroy(gameObject); } } ​~後略~ 【プログラムの解説】 ​・Tooltip("文章") はアトリビュートの一種で、インスペクター内で注釈を表示することができます。変数名にマウスカーソルを合わせると確認できます。用途を忘れそうなパラメータに記述しておくと良いでしょう。 ・ItemDataBase.Items[ItemID].ItemName でアイテムリストの配列にアクセスして、アイテムリスト0番の名前を表示しています。同じように説明文やプレハブにもアクセスできます。 ​ ​ コードが書けたら保存して、黄色い本のインスペクターを確認してみましょう。アイテムIDとアイテムリストを指定する項目が増えているはずです。 アイテムIDは先ほど黄色い本の情報を入れた場所、つまり今回は0番になります。ItemDataBaseは先ほど作成したItemDataBaseをプロジェクトからドラッグ&ドロップしてください。 ​ アイテム名と説明文はItemDataBaseから引っ張ってくるので、空白にして構いません。 ゲームを実行して、黄色い本を調べてみてください。黄色い本が取得でき(ているように見え) たらOKです。 現時点では取得したアイテムを管理する処理がないためオブジェクトが消えるだけですが、次は所持アイテムを管理する機能を実装しましょう。 2-3 アイテムを拾う(後編) 2-3 アイテムを拾う(後編) カーソルの判定が取れたので、次はアイテムを拾ってみましょう。 2-2ではアイテムのオブジェクトが消えるだけでしたが、次は取得したアイテムを所持アイテムとして記憶するようにします。 ​ GameManagerスクリプトを作成してください。歯車のアイコンになりますがこれはUnityの仕様で、動作に違いはありません。 GameManagerスクリプトには以下のように入力してください。少し長いですが、アイテム管理のベースとなる部分なので頑張って入力しましょう。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { // アイテムデータ [SerializeField] ItemData Item_Data; public ItemData GetItemData() { return Item_Data; } // 選択中のアイテム番号(アイテム欄配列の番号) [SerializeField] int SelectItemNo = 0; public int GetSelectItemNo() { return SelectItemNo; } // アイテム欄 [SerializeField] int[] ItemID; // 引数番スロットのアイテムを取得 public int GetItemID(int no) { return ItemID[no]; } // アイテムを取得する // アイテム欄に空きがあったらtrue なかったらfalseを返す public bool GetItem(int getItemID) { int selectID = SelectItemNo; // 順番にアイテム欄を確認していって、空いている場所にIDを格納 for (int i = 0; i < ItemID.Length; i++) { if (ItemID[selectID] == -1) { // 空きがあるのでアイテムIDを格納 ItemID[selectID] = getItemID; return true; } selectID++; if (selectID > ItemID.Length - 1) { // オーバーしたので0に戻す selectID = 0; } } // 空きがなかった return false; } void Update() { // 選択アイテムの変更 if ((Input.GetKeyDown("joystick button 4") || Input.GetKeyDown(KeyCode.Alpha1))) { SelectItemNo++; if (SelectItemNo > ItemID.Length - 1) { SelectItemNo = 0; } } if ((Input.GetKeyDown("joystick button 5") || Input.GetKeyDown(KeyCode.Alpha2))) { SelectItemNo--; if (SelectItemNo < 0) { SelectItemNo = ItemID.Length - 1; } } } } 【プログラムの解説】 ・​GetItem関数内でアイテムの取得処理を行っています。引数には獲得するアイテムのIDを指定します。 一見難しく見えるかもしれませんが、アイテム欄の配列を順番に見ていって-1の場合、 そこにアイテムIDを格納するというだけの関数です。 アイテム欄に空きが見つかってアイテム欄にIDを格納できた場合、GetItem関数はtrueを返します。逆にアイテム欄がいっぱいだった場合はfalseを返します。 空オブジェクトを作成して、GameManagerスクリプトをアタッチしてください。 ​ タグは「GameController」にしておきます(Unityがデフォルトで用意してくれているタグ) Item_DataにはプロジェクトからItemDataBaseをドラッグ&ドロップしてください。 ​ int[] ItemID; の部分がアイテム欄を格納するためのint型の配列になります。 インスペクターでアイテム欄を3つに拡張して、値を-1にしておきましょう。値が-1の時は空欄(アイテムなし)として扱います。 後はアイテムを取得するときにGetItem関数を呼び出すだけになります。 ​ ​ ItemObjectスクリプトを開いて、赤い部分 のコードを追加してください。 青い部分 は穴埋めになります。3Dアクションゲーム編も参考にして埋めてみましょう。 ​【ヒント】① はタグでオブジェクトを検索して、コンポーネントを取得する処理です。 ​【ヒント】② は先ほど作成したGetItem関数を使用します。引数にはアイテムIDを使います。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemObject : MonoBehaviour { [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")] int ItemID = -1; // アイテム識別用の番号 [SerializeField] ItemData ItemDataBase; // アイテムリスト [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; // ゲームマネージャー GameManager m_gameManager; void Awake() { // アウトラインの初期化 m_outline = GetComponent(); m_outline.OutlineMode = Outline.Mode.OutlineVisible; m_outline.OutlineColor = Color.red; m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH; m_outline.enabled = false; // ① ゲームマネージャーを取得 // 【ヒント1】GameManagerのタグ名は「GameController」です // 【ヒント2】コンポーネントを取得する関数はGetComponentです m_gameManager = (ここに入力) } // アイテムを調べた時の仮想関数 public virtual void ItemCheck() { // 調べた時の処理を行う ItemGet(); } // アイテムを調べた時の基本処理 public void ItemGet() { if (ItemID == -1) { // アイテムを調べた // デバッグ用 アイテム名と説明文をコンソールに出力 Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation); } else { // ② アイテムを取得 // 【ヒント】ゲームマネージャーのGetItem関数を使おう bool isGet = (ここに入力) // アイテム欄に空きがあったかどうかで分岐 if (isGet) { // アイテムを獲得できた // デバッグ用 獲得したアイテム名をコンソールに出力 Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得"); // 自身を削除する Destroy(gameObject); } else { // アイテム欄がいっぱいだった // デバッグ用 Debug.Log("アイテム欄がいっぱいです"); } } } ​ ​~後略~ 降参 or 答え合わせの方はこちら コードが書けたら保存して、ゲームを実行してみましょう。 インスペクターにGameManagerを表示した状態で、黄色い本を拾ってみてください。本を拾った瞬間にItemIDのElement 0が-1から0に変化したらOKです。コンソールにも「黄色い本を取得 ​」と出力されます。 アイテム欄の0番が-1(空欄)から0(黄色い本のID)に変化したことで「現在黄色い本を持っている」ことをGameManagerが覚えたことがわかります。 ​ 今はアイテム欄のUIがないのでわかりにくいかもしれませんが、これでアイテムを取得する処理は完成です。 アイテム欄がいっぱいだった時の処理も正常に動作するか確認してみましょう。 Prefabフォルダを作成して、Prefabフォルダ内にItem_YellowBook(黄色い本)をドラッグ&ドロップしてプレハブ化してください。アイテム欄をいっぱいにするため、本は3冊追加します。 ゲームを実行して、黄色い本を全て拾ってみてください。 3冊目まではIDを格納できますが、4冊目を拾おうとしても拾えず、コンソールに「アイテムがいっぱいです」と出力されるはずです。 これでアイテムが3つまでしか持てないことを確認できました。 動作を確認できたら、追加した3冊の黄色い本は削除しておいてください 。 2-4 アイテムを捨てる 2-4 アイテムを捨てる ここまでの内容でアイテムを3つまで持つことができるようになりました。 ですが、このゲームにはギミック解除に使わないダミーのアイテムがあります。ダミーのアイテムがアイテム欄を3つとも占有してしまうとギミックを解けない、いわゆる「詰み」の状態になってしまいます。 ​ ​ 詰み防止のため、持っているアイテムを捨てる処理を実装しましょう。アイテムを持っているときにBボタンが押されたら、アイテムのオブジェクトを生成して前方向に飛ばします。同時にアイテム欄の対応した場所のIDを-1(空欄)に戻しましょう。 黄色い本は前回のLessonでプレハブ化したので、これをアイテムデータベースに登録しましょう。 ​ まずはItemDataBaseを選択して、右上の鍵アイコンをクリックしてロックしましょう。ロックすることで、他の場所をクリックしてもインスペクターが切り替わらないようになります。 ​ この後、黄色い本のプレハブを選択した時にインスペクターが切り替わらないようにロックをかけておきましょう。 Prefabフォルダ内の黄色い本のプレハブを、黄色い本の項目のItemPrefabにドラッグ&ドロップしてください。 ​ プレハブの設定ができたらインスペクターのロックを外しましょう。 次は「アイテムを持っているときにBボタン(0キー)が押されたら、プレハブを生成して前方に発射する」という処理を書いてみましょう。 ​ まずは発射される側であるItemObjectスクリプトに、自身を発射する関数を追加します。赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemObject : MonoBehaviour { [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")] int ItemID = -1; // アイテム識別用の番号 [SerializeField] ItemData ItemDataBase; // アイテムリスト [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; const float ITEMDROP_POWER = 20.0f; // アイテムを捨てる時にかける力 const float ITEMDROP_TORQUE = 60.0f; // アイテムを捨てる時にかける回転量 // ゲームマネージャー GameManager m_gameManager; void Awake() { ~後略~ ~前略~ else { // アイテム欄がいっぱいだった // デバッグ用 Debug.Log("アイテム欄がいっぱいです"); } } } // アイテムを捨てた時の処理 public void ItemDrop(Vector3 playerVelocity) { // リジッドボディの取得 Rigidbody rb = GetComponent(); // 物理演算を有効にする rb.isKinematic = false; // カメラの前方向に飛ばす rb.AddForce((Camera.main.transform.forward * ITEMDROP_POWER) + playerVelocity, ForceMode.Impulse); // ランダムに回転 rb.AddTorque(Random.onUnitSphere * Random.Range(-ITEMDROP_TORQUE, ITEMDROP_TORQUE)); } // アイテムを使用した時の仮想関数 public virtual void ItemUse() { } ~後略~ Rigidbodyクラスの関数であるAddForce(引数ベクトルの力を加える)とAddTorque(引数ベクトルの回転を加える)を使って、オブジェクトを捨てる演出を実装しました。 ​ 【プログラムの解説】 ​・isKinematic は物理演算が有効かどうか設定するパラメータになります。 ​ ・ Camera.main.transform.forward でメインカメラの前方向を取得できます。 ​ メインカメラの前方向​に飛ばす力(ITEMDROP_POWER)を乗算して、プレイヤーの移動力と合わせることで発射の方向を決めています。 ​ 第二引数であるForceModeで力の加え方を決めることができます。例えば今回使用したForceMode.Impulseは瞬間的に力を加えるモードです(参考サイト ) ​ ​・Random.onUnitSphere は半径1の球体の表面上のランダムな1点を返します。これによってランダムな方向のベクトルを生成しています。 ​ ​ ​ コードが書けたら保存して、黄色い本のプレハブをダブルクリックしてください。 黄色い本のプレハブを開いたらインスペクターの「Add Component」からRigidbodyをアタッチしてください。 追加したRididbodyのパラメータを設定します。 ​ ​ Is Kinematicにチェックを入れると、物理演算が行われないようにできます。チェックを外すといつでも物理演算を再開できます。普段は物理演算を無効にしておいてオブジェクトを捨てた時だけ有効にするため、ここではチェックを入れておきましょう。 後はボタンが押された時にオブジェクトを捨てる処理を実装するだけになります。 ​ GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ selectID++; if (selectID > ItemID.Length - 1) { // オーバーしたので0に戻す selectID = 0; } } // 空きがなかった return false; } // 引数番スロットのアイテムを捨てる void ItemDrop() { // アイテムがあるか確認 if (ItemID[SelectItemNo] == -1) { Debug.Log("【エラー】" + SelectItemNo + "番にアイテムがありません!"); return; } // プレイヤーの移動量を取得 Rigidbody playerRb = GameObject.FindGameObjectWithTag("Player").GetComponent(); Vector3 velocity = playerRb.velocity; velocity.y = 0.0f; // 捨てるアイテムを生成 Vector3 itemPos = Camera.main.transform.position; GameObject dropItem = Instantiate(Item_Data.Items[ItemID[SelectItemNo]].ItemPrefab, itemPos, Camera.main.transform.rotation); // 前方に発射 dropItem.GetComponent().ItemDrop(velocity); // アイテム欄のIDをリセット ItemID[SelectItemNo] = -1; } void Update() { // Bボタンで捨てる if ((Input.GetKeyDown("joystick button 1") || Input.GetKeyDown(KeyCode.Alpha0))) { ItemDrop(); } // 選択アイテムの変更 if ((Input.GetKeyDown("joystick button 4") || Input.GetKeyDown(KeyCode.Alpha1))) { SelectItemNo++; if (SelectItemNo > ItemID.Length - 1) { SelectItemNo = 0; } } ~後略~ 【プログラムの解説】 ​・Rigidbodyのパラメータであるvelocityは「対象にかかっている力」を表すパラメータです。例えばプレイヤーが前方に移動しながらアイテムを投げると、棒立ちで投げた時と比べてより強い力で前方に発射されることになります。 ・Instantiate関数はオブジェクトを生成する関数になります。3Dアクションゲーム編では大砲の弾を発射する時 に使用しました。 ​ 第一引数に生成するゲームオブジェクト、第二引数に生成する座標、第三引数に回転を指定します。 ​ ​ ​ コードが書けたら保存して、ゲームを実行してみてください。アイテムを拾った状態でBボタン(0キー)を押すとアイテムを捨てることができます。 アイテムを捨てるとGameManagerのアイテム欄の0が-1に変化することを確認してください。 アイテムを持っていない状態でアイテムを捨てようとするとエラー文がコンソールに出力されます。 これで一通り完成ではあるのですが、捨てたアイテムがプレイヤーのコライダーと衝突してしまう点が少し気になります。特に壁際でアイテムを捨てると、不自然な挙動になってしまいます。 Unityではレイヤー を使うことで、特定のオブジェクトとの当たり判定を無視することができます。捨てたアイテムがプレイヤーに衝突しないように設定してみましょう。 まずはアイテムとプレイヤーを区別するレイヤーを作成しましょう。 ​ 適当なオブジェクトのLayerをクリックして「Add Layer」を選択してください。 レイヤーの名前を入力する項目が開くので、User Layer7に「Player」、User Layer8に「DropItem」と入力してください。 プレイヤーを選択してLayerを「Player」に変更してください。 子オブジェクトのレイヤーも変更するかどうか確認されますが、変更しなくて構いません。 黄色い本のプレハブを開いて、レイヤーを「DropItem」に変更しましょう。 レイヤー同士の衝突判定を設定します。 「Edit」→「Project Settings…」→「Physics」を選択してください。 ​下の方に「Layer Collision Matrix」という、どのレイヤー同士が衝突判定を行うか指定する項目があります。DropItemとPlayerは衝突判定をしないようにチェックを外しておいてください。 ゲームを実行して、プレイヤーと捨てたアイテムが衝突しないようになったか確認してみましょう。 ​ 最後にささいなことですが、捨てた直後のアイテムを空中ですぐ拾えないように、捨ててから1秒立つまでは拾えないようにしましょう。 ​ ItemObjectスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemObject : MonoBehaviour { [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")] int ItemID = -1; // アイテム識別用の番号 [SerializeField] ItemData ItemDataBase; // アイテムリスト [SerializeField] string Name; // カーソルを合わせた時に表示する名前 [SerializeField, Multiline(3)] string Explanation; // 調べた時に表示されるメッセージ Outline m_outline; const float SELECT_OUTLINE_WIDTH = 16.0f; const float ITEMDROP_POWER = 20.0f; // アイテムを捨てる時にかける力 const float ITEMDROP_TORQUE = 60.0f; // アイテムを捨てる時にかける回転量 [SerializeField] bool IsCheck = false; // 調べることが可能かどうか public void SetIsCheck(bool flag) { IsCheck = flag; } // ゲームマネージャー GameManager m_gameManager; void Awake() { // アウトラインの初期化 m_outline = GetComponent(); m_outline.OutlineMode = Outline.Mode.OutlineVisible; m_outline.OutlineColor = Color.red; m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH; m_outline.enabled = false; // ① ゲームマネージャーを取得 // 【ヒント1】GameManagerのタグ名は「GameController」です // 【ヒント2】コンポーネントを取得する関数はGetComponentです m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); } // アイテムを調べた時の仮想関数 public virtual void ItemCheck() { // 調べた時の処理を行う ItemGet(); } // アイテムを調べた時の基本処理 public void ItemGet() { // 調べられない状態 if (IsCheck) { return; } if (ItemID == -1) { // アイテムを調べた // デバッグ用 アイテム名と説明文をコンソールに出力 Debug.Log("アイテム名:" + Name + "\n説明文:" + Explanation); } else { // ② アイテムを取得 // 【ヒント】ゲームマネージャーのGetItem関数を使おう bool isGet = m_gameManager.GetItem(ItemID); // アイテム欄に空きがあったかどうかで分岐 if (isGet) { // アイテムを獲得できた // デバッグ用 獲得したアイテム名をコンソールに出力 Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得"); // 自身を削除する Destroy(gameObject); } else { // アイテム欄がいっぱいだった // デバッグ用 Debug.Log("アイテム欄がいっぱいです"); } } } // アイテムを捨てた時の処理 public void ItemDrop(Vector3 playerVelocity) { // リジッドボディの取得 Rigidbody rb = GetComponent(); // 物理演算を有効にする rb.isKinematic = false; // カメラの前方向に飛ばす rb.AddForce((Camera.main.transform.forward * ITEMDROP_POWER) + playerVelocity, ForceMode.Impulse); // ランダムに回転 rb.AddTorque(Random.onUnitSphere * Random.Range(-ITEMDROP_TORQUE, ITEMDROP_TORQUE)); // しばらく調べられないようにする IsCheck = true; // 1秒後に調べられるようになる Invoke("CheckWait", 1.0f); } // Invokeで呼び出す関数 void CheckWait() { IsCheck = false; } // アイテムを使用した時の仮想関数 public virtual void ItemUse() { } // 自分にカーソルが合った時 public virtual void StartSelect() { // 調べられない状態 if (IsCheck) { return; } // アウトラインを表示する m_outline.enabled = true; } // 自分がカーソルから外れた時 public virtual void EndSelect() { // アウトラインを削除する m_outline.enabled = false; } } 【プログラムの解説】 ・Invoke関数 は指定した関数を一定秒数後に呼び出すことができる関数です。k2Engineではタイマーを設定するなどしていたと思いますが、Unityでは単純に一定秒数後に呼び出すだけならInvoke関数を使うと便利です。第一引数に関数名、第二引数に待機時間を設定します。 Invoke(呼び出す関数の名前,何秒後に呼び出すか); ※ 関数名で検索しているので名前間違いに注意! ※ 2Dランゲーム編に 追加の解説 があります これでアイテムを捨てた後、1秒間はカーソルが合わないようになりました。 Invoke関数は呼び出す関数を名前で指定するため関数の名前を後から変えにくかったり、安定性に少し問題があったりとデメリットもありますが、簡単に一定秒数後に処理を実行したい場合には便利なので覚えておきましょう(代替手段としてコルーチンというものもあります) ​ 次はいよいよアイテムを使ったギミックを実装していきます。 ​ 【評価テスト】 ​(しこたまURLを貼ります) ​ 評価テスト Next Lesson3「ギミックを作ろう」 ページ TOP 2-1 基底クラス 2-2 アイテムを拾う(前編) 2-3 アイテムを拾う(後編) 2-4 アイテムを捨てる 評価テスト

  • 2Dランゲーム編 Lesson4「UIを作ろう」 | Unity1gc2

    2Dランゲーム編 Lesson4 UIを作ろう 4-1 UI用シーンの作成 4-1 UI用シーンの作成 スコアや現在位置を表示するためのUIを作成しましょう。UIのデザインについてはあくまでサンプルなので、みなさんの好みに合わせて調整してください。 ​ ​ 3Dアクションゲーム編ではメインシーンにそのままUIを作成していました。しかし、ステージを複数作成したい場合、この方法では小さな修正でも全てのシーンを修正する必要が出てきてしまいます。 ​ 例えば、UIに新しいボタンを1つ追加したい時も、全てのステージをいちいち開いてボタンの追加作業をしなければならないということです。この方法では、ステージが増えるほど作業が大変になってしまいます。 たくさんのステージを作成してもUIが使いまわせるように、今回はUI用のシーンを作ってメインシーンに合成する手法 を取りましょう。 ​ これでステージを量産しても、元となっているUIシーンが同じなので修正が簡単にできるようになります。Unityでは簡単に複数のシーンを合成することができます。 UI用のシーンを作成しましょう。 ​ Scenesフォルダ内にUIシーンを作成してください。 UI ​シーンをダブルクリックして開いてください。 ​ シーンには標準でメインカメラがありますが、シーンを合成する時にメインカメラが複数あると不具合が起きてしまう ので削除しておきましょう。 カメラがないためGame内のプレビューは表示されませんが問題ありません。 まずはスコア表示用のウィンドウを設置しましょう。 ウィンドウを作成するときはSpriteEditorのSlice機能を使うと便利です。 普通にウィンドウを変形させると左の画像のように四隅が歪んでしまいますが、この機能を使うと右の画像のように四隅の大きさを固定したままウィンドウの大きさを変えることができます。 ウィンドウ画像をスライスして使う準備をしましょう。 「Sprite」→「UI」→「Main」内のMedium-boxというウィンドウ画像を選択して、SpriteModeを「Multiple」に、MeshTypeを「Full Rect」に変更してください。 設定を変更したら「Apply」ボタンを押しましょう。 次はSprite Editorを開いてください。 ​ ​ ウィンドウ画像全体を緑のラインで囲むようにドラッグしてください。 画像全体を囲うと、右下にSpriteという枠が出てきたと思います。 L、T、R、B 全てに「8」を入力してください。画像が9分割されていたらOKです。 この設定をすることで画像を拡大しても四隅の縦横比(アスペクト比 )は保たれたままになります。 ​ ​ 設定できたら右上の「Apply」を押してSprite Editorを閉じてください。 ヒエラルキーから「UI」→「Image」を選択してください。画像と同時にCanvasやEventSystemも自動で追加されます。 ​ ImageのSauce Imageに先ほど設定したmedium-boxをドラッグ&ドロップしてください。 ​ ※ 直接シーン上にスプライトをドラッグ&ドロップしてしまうと、スプライトとしてオブジェクトが追加されてしまうので注意! ・Imageの名前をわかりやすいものに変更しておきます(教材ではScoreWindow) ​・座標、幅、大きさを調整して左上にウィンドウを配置してください ​ PosX=-220 PosY=155 Width=80 Height=30 ​ Scale=2 ウィンドウサイズが変わっても大きさを調整できるように、Canvasの設定を変更してください。 Canvasを選択してUI Scale Modeを「Scale With Screen Size」に変更、Reference Resolutionを適切な値に調整してください。 ​ ​(3Dアクションゲーム編の4-4 も参考にしましょう) スコア表示用のテキストを追加しましょう。 ​ ヒエラルキーから「UI」→「Text - TextMeshPro」を選択して追加しましょう。 ​ TextMeshProのインポートをするウィンドウが開くので「Inport TMP Essentials」を選択してください。 デフォルトのフォントでは雰囲気に合わないので、今回はドットのフリーフォントを使用します。 以下のURLからフォント素材をダウンロードしてください。 ​【使用するフォント→PixelMplus】https://itouhiro.hatenablog.com/entry/20130602/font フォントがダウンロードできたらフォルダを解凍しましょう。 ​ フォルダ内の「PixelMplus12-Regular.ttf」をプロジェクト内にドラッグ&ドロップしてください。 これでフォント素材を追加できましたが、変換しないとTextMeshProで使うことができない ので注意しましょう。 左上の「Window」→ 「TextMeshPro」→「Font Asset Creator」を選択してください。 Font Asset Creatorが開いたら、Sauce Font Fileに使用するフォント素材を設定してください。 ​ 他のパラメータはそのままで構いませんが、お好みで変更してもOKです。 Select Font Assetに「LiberationSans SDF」を設定してください。これでUnityで使えるように変換する文字を指定できます。 このゲームのUIでは日本語を使いませんが、もし日本語を使いたい場合は3Dアクションゲーム編同様このサイト を参考にしてください。 設定が終わったら「Generate Font Atlas」を選択してください。しばらく待つと変換が完了します(設定によって時間がかかる場合があります) 変換が終わったら「Save」を押して素材を保存してください 。 それではTextMeshPro側の設定をしていきましょう。 ​ 先ほど追加したTextMeshProオブジェクトの名前をわかりやすいものに変更してください(サンプルではScoreText) ScoreTextをScoreWindowの子オブジェクトにしましょう。これによってテキストをウィンドウの中央に調整しやすくなり、またウィンドウが移動するとテキストの方も一緒に動くので演出も作りやすくなります。 インスペクターでScoreTextの設定をしていきます。 ・Alignmentを中央揃えに変更して、ウィンドウの中央になるように座標を調整します(PosX、PosY共に0で構いません) ・ここで表示するテキストはダミーなので適当な数字にしておきます ・Font Assetに先ほど変換したフォント素材をドラッグ&ドロップで設定してください ・Font Sizeなどもお好みで調整してください サンプルではUnderlayのパラメータを調整して文字に影を落としています(追加するかはお好みで) 次はプレイヤーの進行状況を示すバーを作成しましょう。 ​ ​ ​ 「Sprite」→「UI」→「Main」内のhealth-bar-boxという画像を選択して、SpriteModeを「Multiple」に、MeshTypeを「Full Rect」に変更してください。 ウィンドウ画像では画像を9つにスライスしましたが、3つにスライスすることもできます。 ​ Sprite Editorを開いて、全体を緑のラインで囲うようにドラッグしてください。 ​ Spriteウィンドウが開いたら、LとRの値を10にしてください。画像が左右と中央に分割されます。これで画像を拡大しても左右の部分は縦横比が保たれるようになります。 設定が終わったら右上の「Apply」をクリックしてSprite Editorを閉じてください。 ​ ​ ヒエラルキーから「UI」→「Image」で新しい画像を追加してください。画像を追加できたらインスペクターから設定をしていきます。 ​ ・Imageの名前をわかりやすいものに変更してください(サンプルではBarBox) ・座標と幅を調整 してください PosX=0 PosY=150 Width=260 Height=15 ・Sauce Imageに「Sprite」→「UI」→「Main」内の「health-bar-box」を設定してください ​・Image Typeが「Sliced」になっていることを確認してください(画像を設定すると自動で変更されるはず) 次はバー本体を追加しましょう。 ​ ​ ヒエラルキーから「UI」→「Image」で新しい画像を追加してください。追加されたImageにわかりやすい名前をつけておいてください(サンプルではBar) BarをBoxBarの子オブジェクトにしてください。 インスペクターの設定をしましょう。 ​ ・幅を調整 してください Width=242 Height=9 ・Sauce Imageに「Sprite」→「UI」→「Main」内の「health-bar」を設定してください Image Typeを「Filled」に変更してください。この設定にすると画像を一部分だけ描画することができるようになります。 Fill Methodでは画像をどのように描画するか設定することができます。Fill Methodを「Horizontal」に変更してください。 ​ ​ 設定できたらFill Amountのスライダーを操作して、バーが増減しているように見えることを確認してください。 Fill Amountが1.0の時は画像が100%表示されます。0.5にすると半分、0にすると表示されなくなります。 後ほどFill Amountの値をスクリプトから操作して、進行度バーを作っていきます。 ​ ​ 今回はバーを進行状況を示すものとして作成しましたが、FilledはHPバーなどあらゆるバーに応用することができます 。ぜひ自分のゲームにも使ってみてください。 Fill Methodを変更することで円形ゲージも簡単に作ることができます。実際に設定を変更して、どのように描画されるか確認してみましょう。 Unity Tips! 次はユニティちゃんのアイコンを配置しましょう。 ユニティちゃんのアイコンはアニメーションできるようになっているので ​、まずはその設定をしていきます。流れは宝石のアニメーションを作成した時と同じです。 ​ ​ 「Sprite」→「UI」→「Main」内の「UniIc1」を選択して、Sprite ModeをMultipleに変更してください。 設定が終わったらApplyボタンをクリックして、Sprite Editorを開いてください。 左上のSliceボタンをクリックしてください。 Typeを「Grid By Cell Count」に変更して、Cに2を入力しましょう。画像が左右に分割されます。 ​ Sliceボタンをクリックしてから、右上のApplyボタンをクリックしてください。 Applyが終わったらSprite Editorを閉じてください。 ​ ヒエラルキーから「UI」→「Image」を選択して画像オブジェクトを追加してください。 ​ 名前はわかりやすい名前で構いません(サンプルではUniIcon) ​ アイコン画像もバーと同じようにBarBoxの子オブジェクトにしてください。 インスペクターからUniIconのパラメータを調整してください。 ​ ・座標と幅を調整してください PosX=0 PosY=2.5 Width=24 Height=24 ​・Sauce ImageにUniIc1の画像を設定してください 画像のパターンを切り替えるアニメーションを作成しましょう。 ​ Animationフォルダ内に「UIAnimation」フォルダを作成してください。 UIAnimationフォルダ内にアニメーションを作成して、ヒエラルキー内のUniIconオブジェクトにドラッグ&ドロップしてください(サンプルでのアニメーション名はUniIconAnim) アニメーションをダブルクリックしてアニメーションウィンドウを開いてください。 ​ ヒエラルキー内のUniIconオブジェクトをクリックしてから「Add Property」→「Image」→「Sprite」を選んで項目を追加してください。 1枚目(赤枠のユニティちゃん)の次のフレームに2枚目(白枠のユニティちゃん)をドラッグ&ドロップしてください。 デフォルトで他のキーが配置されていた場合は削除して「1フレーム目に赤枠のユニティちゃん、2フレーム目に白枠のユニティちゃん」だけになるようにしてください 。 このままでは画像の切り替わりが早すぎて目に悪そうです。 単純にアニメーションの速度を変えるだけなら、サンプルレートを調整すると楽に変更できます。サンプルレートは1秒間に打てるキーの数で、値を小さくすることでアニメーションを遅くすることができます。 ​ アニメーションウィンドウの右端にある3つの点が描かれたボタンをクリックして「Show Sample Rate」を選択してください。 左側にSamplesというサンプルレートを指定する枠が表示されるので、1を入力してからEnterを押してください。 ​アニメーション速度が変わったらOKです。 アニメーションがループ再生する設定になっているか確認しておいてください。 ​ ​ 最後にスタートとゴールを示すアイコン画像を配置しましょう。特殊な設定は必要なく、単純にオブジェクトを追加するだけです。 ​ まずはヒエラルキーから「UI」→「Image」を選択して2枚のImageを追加 してください。 今までと同じようにBarBoxの子オブジェクトにしましょう。 UI画像はヒエラルキー内で下にあるオブジェクトが前面に表示されます。ユニティちゃんのアイコンが前面に表示されるように、順番を並び替えてください。 インスペクターからそれぞれの画像の設定を行ってください。 ​ ​【スタートアイコン】 ・座標と幅を調整してください PosX=-120 PosY=22 Width=20 Height=20 ​・Sauce Imageに「Sprite」→「UI」→「Main」内の「arrow」を設定してください 【ゴールアイコン】 ・座標と幅を調整してください PosX=116 PosY=22 Width=16 Height=20 ​・Sauce Imageに「Sprite」→「UI」→「Main」内の「flag」を設定してください これでUI画像の仮配置が完了しました。 まだUIの更新処理を実装していませんが、先にStage1シーンにUIシーンを合成してみましょう。 ​ ​ GameManagerスクリプトを開いて赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class GameManager : MonoBehaviour { // ゲームの状態 public enum GameState { enGameState_Play, // プレイ中 enGameState_GameOver, // ゲームオーバー enGameState_Clear, // ゲームクリア } ​~後略~ ~前略~ // スコアを取得 public int GetScore() { return Score; } // Awakeなので注意 void Awake() { // UIシーンを合成 SceneManager.LoadScene("UI", LoadSceneMode.Additive); } void Start() { } void Update() { ​~後略~ 【プログラムの解説】 ・3Dアクションゲーム編で解説した通り、スクリプトでシーンを扱う際は最初に using UnityEngine.SceneManagement; を追加する必要があるので注意しましょう。 ・Awake関数はStart関数より先に実行される関数です。 ​Start関数には後ほどUI更新処理を書くので、UIシーンのロードはAwake関数に書いています。 ・LoadScene関数の第二引数にはLoadSceneModeを指定することができます。 第二引数が未入力の場合はSingle(シーンを上書き)モードになっていますが、Additiveモードを指定することで、シーンを合成することができます 。 ​ ​ 早速確認したいところですが、このまま実行するとエラーが発生してしまいます。 ビルド対象のシーンにUIシーンを追加してください。3Dアクションゲーム編のLesson5-5を参考にしましょう。 Scenes In BuildにUIシーンを追加できたら、Stage1シーンに切り替えて実行してみてください(UIシーンを実行しても何も起きません) ​ Stage1シーンに先ほど作成したUIシーンが合成されていたらOKです。 ヒエラルキーを確認するとStage1シーンにUIシーンがしっかり合成されていることが確認できます。 4-2 スコアの更新 4-2 スコアの更新 シーンの合成ができたところで、まずはスコアを更新しましょう。 3Dアクションゲーム編ではUpdate関数で更新していましたが、今回は軽量化も意識した別の方法でスコアを更新します。 ​ そもそもUpdate関数というのは毎フレーム実行される関数なので、Update関数内の処理が増えれば増えるほどゲームは重くなります 。 毎フレーム描画を更新するより、スコアの値が変わった瞬間だけ描画を更新した方が無駄な処理を実行しなくて済みます。幸いなことに「スコアの値が変わった瞬間」は既に関数になっているので、これを活用しましょう。 まずはUI更新用のスクリプトを作成しましょう。 ​ 新しいスクリプトUI_Scoreを作成して以下のように入力してください。青い部分 は穴埋めです。 ​ ​【ヒント】 3Dアクションゲーム編のLesson4 を参考にしてください! using System.Collections; using System.Collections.Generic; using UnityEngine; // ① TextMeshProを扱う時に必要 (ここに入力) public class UI_Score : MonoBehaviour { TextMeshProUGUI m_text; // Awakeなので注意 // Awake関数はStart関数より先に実行される void Awake() { // ② 自身にアタッチされているTextMeshProを取得して、m_textに代入 (ここに入力) } public void ScoreUpdate(int score) { // 引数を使ってスコアを更新 m_text.text = "" + score; } } 降参 or 答え合わせの方はこちら コードが書けたら保存して、UIシーンのScoreTextオブジェクトにUI_Scoreスクリプトをアタッチしてください(ここからは複数のシーンを編集していくことになるので、今どのシーンを見ているか注意しましょう) 後から検索しやすいようにScoreTextオブジェクトにScoreTextタグを設定してください。タグの追加方法は過去のレッスンを参考にしてください。 後はスコア増加のタイミングでScoreUpdate関数を呼ぶだけになります。 GameManagerスクリプトを開いて赤い部分のコード を追加してください。 ~前略~ [SerializeField, Header("スコア")] int Score = 0; // スコア用UIを保存 UI_Score m_uiScore; // スコアを加算 public void AddScore(int score) { Score += score; // 現在のスコアを渡してUI更新 m_uiScore.ScoreUpdate(Score); } // スコアを取得 public int GetScore() { ​~後略~ ~前略~ // Awakeなので注意 void Awake() { // UIシーンを合成 SceneManager.LoadScene("UI", LoadSceneMode.Additive); } void Start() { // ScoreTextタグからUI_Scoreを取得 m_uiScore = GameObject.FindGameObjectWithTag("ScoreText").GetComponent(); // 初期値に更新 m_uiScore.ScoreUpdate(Score); } void Update() { } } 【プログラムの解説】 ・オブジェクトの検索は合成したシーンも対象になります。今回はStage1シーンにあるGameManagerコンポーネントがUIシーンにあるScoreTextを検索しています。 ​ ​ 一連の処理ではAwake関数とStart関数の呼ばれる順番の違いを活用しています(よくわからない人は気にしなくてもOKです) ​ ここまでコードが書けたら保存して、Stage1シーンを実行してみてください。 ​ 宝石を取得することでUIのスコアが更新されるようになったらOKです。 4-3 進行度バーの更新 4-3 進行度バーの更新 次は今どこまで進んでいるかを示すバーを更新できるようにしましょう。 ​ ​ このバーを実装するために必要な座標は「スタート地点」「ゴール地点」「現在地点」 の3つです。スタート地点とゴール地点の座標を元に、現在地点がステージの何%の場所なのかを計算してみましょう。 スコアと違ってこちらは常に変化するので、Update関数で更新しましょう。 ​ UI_Progressスクリプトを追加して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; // UIを扱う時に必要 public class UI_Progress : MonoBehaviour { [SerializeField] Image BarImage; [SerializeField] GameObject UniIcon; [SerializeField] Vector3 BarStartPos, BarEndPos; // 進行度計算に必要な情報 Vector3 m_startPos; // スタート位置 Vector3 m_goalPos; // ゴール位置 GameObject m_player; // 現在位置 // スタートからゴールまでの距離(X) float m_range = 0.0f; void Start() { // プレイヤーを検索 m_player = GameObject.FindGameObjectWithTag("Player"); // スタート位置を取得 m_startPos = m_player.transform.position; // ゴール位置を取得 m_goalPos = GameObject.FindGameObjectWithTag("Goal").transform.position; // スタートからゴールまでの距離を計算 m_range = m_goalPos.x - m_startPos.x; } void Update() { // プレイヤーがいないなら中断する if (m_player == null) { return; } // 進行度の計算 float r = m_player.transform.position.x - m_startPos.x; float fill = r / m_range; // バーの更新 BarImage.fillAmount = fill; // アイコンの更新 Vector3 pos = Vector3.Lerp(BarStartPos, BarEndPos, fill); pos.z = 0.0f; UniIcon.transform.localPosition = pos; } } 【プログラムの解説】 ​・ImageコンポーネントのfillAmount に0.0~1.0の範囲の値を入れることでバーの長さを変化させることができます。0.0では全く表示されず、0.5では半分、1.0でバー全体が表示されます。 ​・Leap関数は今まで何度か使用していますが、第一引数の座標と第二引数の座標の間を第三引数で補完してくれる関数です。第三引数が0.5だと、第一引数と第二引数のちょうど中間の座標を返します。 バーの一番左の座標と一番右の座標を使って、アイコンの位置を補完しています。 ​ ​ ​ コードが書けたら保存して、UIシーンのBarBoxオブジェクトにアタッチしてください。 ​ インスペクター内にパラメータが表示されているので設定しましょう。 ​ ・BarImageにヒエラルキーからBarオブジェクトをドラッグ&ドロップ ・UniIconにヒエラルキーからUniIconオブジェクトをドラッグ&ドロップ ・BarStartPosを設定 X=-120 Y=0 Z=0 ・BarEndPosを設定 ​ X=120 Y=0 Z=0 ここまで設定できたらStage1シーンに切り替えて実行してみてください。 ※ Playarタグを検索しているため、UIシーンを起動するとプレイヤーがいないことによるエラーが発生するので注意 ​ プレイヤーの進行に応じて進行度バーが更新されたらOKです。 評価テスト これでUIの実装は完了です。 意図的に右上のスぺースは開けています。LessonEXのポーズ画面 を参考にしてポーズボタンなどを追加してみると良いでしょう。 ​ 次のレッスンではステージ2やステージ3を作成して、ステージセレクトからステージを移動できるようにしましょう。また、ステージのクリア状況を保存してステージ1→ステージ2…と進めるようにもしていきます 。 ​ 【評価テスト】 https://forms.gle/Sv7dvz8wCTmcqs4U6 Next Lesson5「ステージセレクトを作ろう」 ページ TOP 4-1 UI用シーンの作成 4-2 スコアの更新 4-3 進行度バーの更新 評価テスト

  • 2Dランゲーム編 Lesson3「ルールを作ろう」 | Unity1gc2

    2Dランゲーム編 Lesson3 ルールを作ろう 3-1 ゲームステートの実装 3-1 ゲームステートの実装 まずは今がプレイ中なのか、ゲームオーバー中なのか、ゲームクリア中なのかを判別するための変数を作成しましょう。 ゲームの状態はGameManagerスクリプトで管理するようにします。 ​ ​ GameManagerスクリプトを開いて赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { // ゲームの状態 public enum GameState { enGameState_Play, // プレイ中 enGameState_GameOver, // ゲームオーバー enGameState_Clear, // ゲームクリア } [SerializeField, Header("ゲームの状態")] GameState m_gameState = GameState.enGameState_Play; // ステートを設定 public void SetState(GameState state) { m_gameState = state; } // ステートを取得 public GameState GetState() { return m_gameState; } [SerializeField, Header("スコア") ] int Score = 0; // スコアを加算 public void AddScore(int score) { Score += score; } // スコアを取得 public int GetScore() { return Score; } void Start() { } void Update() { } } 評価テスト 【プログラムの解説】 ・enumは列挙型 と言い、複数の定数を一つにまとめておくことができます。今回のパターンではenGameState_Play=0、enGameState_GameOver=1、enGameState_Clear=2 といった風に内部的に値が分けられます。 列挙型についてはC++の教科書なども参考にしてみてください。 ​ ​ ​ コードが書けたら保存して、GameManagerのインスペクターを確認してみてください。 ゲームの状態を表す変数が追加されています。変数の型を列挙型にした場合、インスペクターでの表示も列挙型になります。 3-2 ゲームオーバーの実装 3-2 ゲームオーバーの実装 それでは適切なタイミングでゲームの状態を変更してみましょう。今までは仮実装だったゲームオーバーを本格的に実装していきます。 ​ ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ [SerializeField, Header("レイの始点")] GameObject m_rayOrigin; // 坂に触れているかどうか bool m_isSlope = false; // 坂道かつ空中にいるかどうか bool m_isSlopeAir = false; // 重力の初期値を保存 float m_defGravity = 0.0f; // 角度変更タイマー float angleTimer = 0.0f; const float ANGLE_LIMIT = 0.1f; // アニメーションコントローラー Animator m_animator; // アニメーション管理用フラグ bool m_airFlag; // ゲームマネージャー GameManager m_gameManager; void Start() { // 自身にアタッチされているRigidBody2Dを取得する m_player_rb2d = GetComponent(); // 自身にアタッチされているAnimatorを取得する m_animator = GetComponent(); // GameControllerタグつきのオブジェクトにアタッチされているGameManagerを取得する m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); // 空中ジャンプ回数を初期化 m_airJumpCount = MaxAirJump; // 重力の初期値を保存 m_defGravity = m_player_rb2d.gravityScale; } ​ まずはゲームの状態を管理するためにGameManagerコンポーネントを取得しています。タグの名前を間違えないように注意してください。 ~前略~ // Fixedなので注意! void FixedUpdate() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // 常に右へ移動する Vector3 move = Vector3.zero; move.x = MoveSpeed * Time.deltaTime; transform.Translate(move); } void Update() { ​~後略~ ~前略~ // ジャンプアニメーション切り替え if (m_groundCheck.GetIsGround() == false || Input.GetKeyDown(KeyCode.Space)) { m_airFlag = true; } if (m_airFlag && m_groundCheck.GetIsGround() && m_player_rb2d.velocity.y == 0.0f) { m_airFlag = false; // アニメーションも戻す AnimationReset(); } } // ジャンプ void Jump(bool airJump) { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // 加わっている力を一旦リセット m_player_rb2d.velocity = Vector2.zero; // 上方向に力を加える m_player_rb2d.AddForce(new Vector2(0.0f, JumpPower), ForceMode2D.Impulse); ​~後略~ ~前略~ // 坂道判定 void Slope() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // タイマー減少 if (angleTimer > 0.0f) ​~後略~ 関数の途中にreturn; を書くとそこで処理が中断され、以降のコードは実行されなくなります 。 ​ 今回はゲームの状態がプレイ中でない場合は中断して、自動移動、ジャンプ、坂道判定の処理が実行されないようにしました。 ~前略~ // ゲームオーバー public void GameOver() { // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_GameOver); } ​~後略~ 最後にゲームオーバーになったらゲームステートを切り替えます。 ここまで入力できたら保存して、ゲームオーバーになるとGameStateがGameOverに切り替わることを確認してみてください。 また、ゲームオーバーになると右への自動移動や ジャンプ操作などが停止することも確認してみましょう。 次はゲームオーバーの演出を作りましょう。ただし、今作るのはユニティちゃんとUI演出のみになります。 ​ ​ まずは1-7 で作成をスキップしたゲームオーバーとゲームクリアのアニメーションの遷移設定をしましょう。 ​ AnimatonフォルダからプレイヤーのAnimatorControllerをダブルクリックして開いてください。 Parametersのプラスボタンをクリックして、新しいパラメータを追加しましょう。 ​ ​ 今まではBoolを使ってきましたが、今回はTriggerを使用します。Triggerはfalse/trueの概念がなく、実行した一瞬だけ有効になるパラメータになります 。例えるなら、実行した瞬間trueになって、その後自動でfalseに戻るbool型をイメージしてください。 ​ Trigger型のパラメータ「GameOver」と「Clear」を作成してください。 今回は「Any State」というステートを使用します。Any(どれでも)の名の通り、このステートから繋いだステートはどこからでも遷移することができます 。RunやJumpなどからチマチマTransitionを繋いでいっても良いのですが、かなり大変なので「どこからでもいいから遷移したい!」という時はAny Stateを使用しましょう。 ​ ​ Any Stateを見やすい場所へ移動させて、GameOverとClearへTransitionを繋いでください。 Transitionの設定をしてください。 ​ 【Any State→GameOver】 Transition Duration : 0.1 (0にするとエラーが出ます) 【Any State→Clear】 Transition Duration : 0.0 これでどこからでも遷移できるステートが完成しました。ゲームオーバーになった瞬間にパラメータを変更しましょう。ついでにユニティちゃんを左上に軽く飛ばす演出も加えます。 ​ ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // ゲームオーバー public void GameOver() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_GameOver); // アニメーションを変更 m_animator.SetTrigger("GameOver"); // 重力をリセット m_player_rb2d.velocity = Vector2.zero; // 重力を戻す m_player_rb2d.gravityScale = m_defGravity; // 左上に力を加える m_player_rb2d.AddForce(new Vector2(-5.0f, 7.0f), ForceMode2D.Impulse); // コライダーを無効にする GetComponent().enabled = false; } ​~後略~ 【プログラムの解説】 ・SetTriggerは今まで使っていたSetBoolと使い方は同じです。ここでアニメーションをGameOverへ切り替えています。名前の打ち間違いには注意しましょう。 ・ほとんどのコンポーネントはenabled というパラメータを持ちます。これはコンポーネントのアクティブを示す値で、falseにすることでコンポーネントを一時的に無効にすることができます。 ​ ここではゲームオーバーになった瞬間にプレイヤーの当たり判定を無効にすることで、地形への衝突を防いでいます。 ​ ​ ここまで書けたら保存して、地形やギミックに衝突することでゲームオーバーになってみてください。 ゲームオーバーになったらカメラと背景の移動が止まるようにしましょう。 ​ ​ GameCameraスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameCamera : MonoBehaviour { GameObject m_player; [SerializeField] Vector3 CameraAddPos = Vector2.zero; [SerializeField] Vector2 CameraMaxPos = Vector2.zero; [SerializeField] Vector2 CameraMinPos = Vector2.zero; // ゲームマネージャー GameManager m_gameManager; void Start() { // ゲームマネージャーを取得 m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); // Playerタグのついたオブジェクトをターゲットにする m_player = GameObject.FindGameObjectWithTag("Player"); // 最初に座標更新しておく CameraUpdate(); } void Update() { // プレイヤーがいないなら何もしない if (m_player == null) { return; } // ゲームオーバーなら何もしない if (m_gameManager.GetState() == GameManager.GameState.enGameState_GameOver) { return; } // 座標の更新 CameraUpdate(); } これでゲームオーバーになるとカメラの移動が止まるようになりました。 ​ ​ 同じように背景の移動も停止させてみましょう。 BG_Scrollスクリプトを開いて「ゲームステートがプレイ中でない時は移動しない」スクリプトを書いてください。上記のGameCameraスクリプトのコードを参考にしましょう。 降参 or 答え合わせの方はこちら これでゲームオーバーの処理は一旦完成にしておきます。 ​後のLessonでゲームオーバーのUIを実装していきます。 3-3 落下時にゲームオーバーにする 3-3 落下時にゲームオーバーにする 現在では壁かギミックに衝突することでゲームオーバーになっていますが、穴に落下した際にもゲームオーバーになるようにしましょう。 考え方は3Dアクションゲーム編と同じです。 ​ ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // ゲームマネージャー GameManager m_gameManager; [SerializeField, Header("落下判定になる高さ")] float Y_Border = -10.0f; void Start() { ​~後略~ ~前略~ // ジャンプアニメーション切り替え if (m_groundCheck.GetIsGround() == false || Input.GetKeyDown(KeyCode.Space)) { m_airFlag = true; } if (m_airFlag && m_groundCheck.GetIsGround() && m_player_rb2d.velocity.y == 0.0f) { m_airFlag = false; // アニメーションも戻す AnimationReset(); } // 落下判定 if(transform.position.y <= Y_Border) { // ゲームオーバーにする GameOver(); } } // ジャンプ void Jump(bool airJump) { ​~後略~ 落下判定はUpdate関数内に書いてください。 ​ これでプレイヤーのY座標がボーダー以下になったらゲームオーバーになります。 ​ 落下判定のボーダーはインスペクターで調整してください。 3-4 ゲームクリアの実装 3-4 ゲームクリアの実装 ゲームクリアを実装していきましょう。 まずは「ゴールラインにユニティちゃんが触れたら、ゲームステートをClearに変更する」という処理を実装していきます。 ​ ​ 「Sprite」→「Gimmick」からGoalの画像を選択して、Mesh Typeを「Full Rect」に変更してください。これで背景と同じように画像がループする設定になります。 Goalの画像をシーン上にドラッグ&ドロップしてください。 ゴールラインの画像を調整しましょう。 Draw Typeを「Tiled」に変更してHeightの値を大きくすると、縦にループする画像になります。座標や大きさ、優先度などもお好みで変更してください。 ​ GoalオブジェクトにGoalタグを追加して設定してください。今は使いませんが、後ほどUIを作成する際に必要になります。 ​ また、レイが衝突しないようにLayerを「Ignore Raycast」に変更しておいてください。 GoalオブジェクトにBox Collider 2Dをアタッチしてください。 ​ Is Triggerにチェックを入れ、Sizeを調整してください。SizeはSprite RendererのSizeと同じにするとちょうどよくなります。 それでは「ゴールラインにユニティちゃんが触れたら、ゲームステートをClearに変更する」処理を実装していきましょう。 ​ まずはPlayerMoveスクリプトにクリア用の関数を追加します。 ​ PlayerMoveスクリプトを開いて、赤い部分のコードを追加してください。 ~前略~ // ゲームオーバー public void GameOver() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_GameOver); // アニメーションを変更 m_animator.SetTrigger("GameOver"); // 重力をリセット m_player_rb2d.velocity = Vector2.zero; // 左上に力を加える m_player_rb2d.AddForce(new Vector2(-5.0f, 7.0f), ForceMode2D.Impulse); // コライダーを無効にする GetComponent().enabled = false; } // ゲームクリア public void GameClear() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_Clear); } void AnimationReset() { // アニメーションのパラメータをリセットする m_animator.SetBool("JumpFlag", false); m_animator.SetBool("AirJumpFlag", false); m_animator.SetBool("FallFlag", false); } } Goalスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Goal : MonoBehaviour { private void OnTriggerEnter2D(Collider2D collision) { // もしプレイヤーと衝突したら… if (collision.CompareTag("Player")) { // ゲームクリアにする collision.GetComponent().GameClear(); } } } コードが書けたら保存して、GoalオブジェクトにGoalスクリプトをアタッチしてください。 ​ ​ ゲームを実行して、ゴールするとゲームステートがClearに切り替わることを確認してみてください。 ​ ​ よりクオリティを高めるために、ゴール演出を実装しましょう。 ゴールラインに触れる→しばらくゆっくり右へ移動→ユニティちゃんがクリアアニメーションをする という流れにしていきます。 ​ ​ まずはクリアアニメーションに使う変数を追加しましょう。 ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ [SerializeField, Header("落下判定になる高さ")] float Y_Border = -10.0f; // クリア演出用 [SerializeField, Header("クリア演出用"),Tooltip("クリアアニメーションまで待つ時間")] float ClearRunTime = 1.0f; [SerializeField, Tooltip("クリアアニメーションまで走る速さ")] float ClearRunSpeed = 3.0f; // 演出フラグ bool m_claerRun, m_claerWait; void Start() { ~後略~ 【プログラムの解説】 ・Tooltipはアトリビュートの一種で、変数名にマウスカーソルが重なった時に注釈を表示させることができます。 ​ ​ 自分で作った変数の用途を後で忘れることはよくあるので、複雑な用途の変数に注釈をつけて確認しやすいようにしておくと良いでしょう。 ​ ​ 追加した変数を使って、クリア演出を実装します。 内部的な流れとしては ゲームクリアステートになって、地面に接地していたらしばらく歩く→歩き終わったらクリアアニメーション再生 という流れです(完璧に理解する必要はありません) ​ ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ​細かい部分が変わっているので注意してください。 ~前略~ // Fixedなので注意! void FixedUpdate() { // ゲームオーバーなら中断 if (m_gameManager.GetState() == GameManager.GameState.enGameState_GameOver ) { return; } // クリア演出の移動待機が終わっていたら中断 if (m_claerWait) { return; } // 常に右へ移動する Vector3 move = Vector3.zero; move.x = MoveSpeed * Time.deltaTime; // クリア中ならゆっくり移動するように補正する if (m_gameManager.GetState() == GameManager.GameState.enGameState_Clear) { move.x /= ClearRunSpeed; } transform.Translate(move); } ~後略~ ​ ​ ここではゴールした後しばらくの間、右へゆっくり移動する処理を実装しています。 ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。​ ~前略~ // 落下判定 if(transform.position.y <= Y_Border) { // ゲームオーバーにする GameOver(); } // ゲームクリア演出 // クリア中かつ移動中でない時 if(m_gameManager.GetState() == GameManager.GameState.enGameState_Clear && m_claerRun == false) { // 接地したら移動待機状態になる if (m_groundCheck.GetIsGround()) { m_claerRun = true; // (ClearRunTime)秒待ってからアニメーションする Invoke("GameClearAnimation", ClearRunTime); } } } // ジャンプ void Jump(bool airJump) { ~後略~ ~前略~ // ゲームクリア public void GameClear() { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // ゲームの状態を変更する m_gameManager.SetState(GameManager.GameState.enGameState_Clear); } // クリア演出 void GameClearAnimation() { // もう終わっている場合は中断 if (m_claerWait) { return; } // アニメーションを変更 m_animator.SetTrigger("Clear"); // ゲームクリア演出終了 m_claerWait = true; } void AnimationReset() { ~後略~ ​​【プログラムの解説】 ・Invoke関数 は指定した関数を一定秒数後に呼び出すことができる関数です。k2Engineではタイマーを設定するなどしていたと思いますが、Unityでは単純に一定秒数後に呼び出すだけならInvoke関数を使うと便利です。第一引数に関数名、第二引数に待機時間を設定します。 Invoke(呼び出す関数の名前,何秒後に呼び出すか); ※ 関数名で検索しているので名前間違いに注意! ​ ​ ここまで書けたら保存してください。 Invoke 他にも便利なInvokeに類似する関数があります。 ​ InvokeRepeating関数 は第一引数に指定した名前の関数が(第二引数)秒後に呼び出され、その後(第三引数)秒ごとに繰り返し呼ばれるというものです。 例えば InvokeRepeating("Hoge", 5, 1); と指定すると、関数Hogeが5秒後に呼ばれ、それ以降は1秒ごとに繰り返し呼ばれ続けるようになります。 InvokeRepeating(呼び出す関数の名前, 呼び出す秒数, 何秒ごとに呼び出すか); ​ CancelInvoke関数 はInvokeの実行を止めることができます。前述したInvokeRepeating関数の実行を止める時にも使えます。 CancelInvoke(止める関数の名前); ​ ​ ただしInvoke関数は引数や戻り値を使用できず、処理を途中で止めたり変更することができないので、高度なタイマー処理を実装するには不向きです。 ​引数などを使いたい場合はコルーチンを使うようにしましょう(参考サイト ) Unity Tips! ​ ​ コードが書けたら保存して、実際にゴールしてみてください。 ​ゴールするとしばらくゆっくり移動して、クリアアニメーションが再生されます。 ​ ​ クリア時はユニティちゃんのポーズが見えやすいようにカメラを拡大させましょう。 ​ カメラの拡大演出もアニメーションで制御します。 ​ ​ Animationフォルダ内にCameraAnimationフォルダを作成してください。 ​ ​ 「Create」→「Animaton」を選択して、CameraAnimationフォルダ内にCameraWaitアニメーションを作成してください。 ​ 作成したアニメーションをヒエラルキー内のMain Cameraにドラッグ&ドロップしてください。 ​ ​ CameraWaitアニメーションをダブルクリックして、Animatonウィンドウを開いてください。 ヒエラルキー内のMain Cameraをクリックした後、アニメーション名をクリックして「Create New Clip…」を選択すると新しいアニメーションを作成することができます ​。作成するアニメーションの名前をCameraClearにして保存してください。 ​ CameraWaitアニメーションは通常時の待機用アニメーションなので、中身を作る必要はありません。 ​ ​ それでは、クリア時に再生するズームアニメーションを作成していきましょう。編集しているアニメーションがCameraClearアニメーションになっていることを確認してください。 ​ ​ 「Add Property」を選択して、「Camera」→「Orthographic size」横のプラスボタンをクリックして項目を追加してください。 ​ ​ パラメータを設定していきます。新しいキーを追加して、中間に配置してください。 ​ ​ 最初のキーの値は初期値である6にしておきます。 ​ ​ 中間のキーは大きくズームするために2にしておきます。 ​ ​ 最後のキーはバウンドするように3にしておきます。 ​ ​ サンプルゲームでは中間のキーを12フレーム目、最後のキーを20フレーム目に配置しています。 キーの配置やパラメータはお好みで変更して構いません。再生ボタンでプレビューしながら調整してください。 ​ このままでは中央にズームするだけで、肝心のユニティちゃんが画面から外れてしまいます。 ​ カメラの移動も同時に実行するようにしましょう。 「Add Property」→「Transform」→「Position」と選択して、座標の項目を追加してください。 ​ 最初のキーの値は初期値でOKです。 ​ ​ 最後のキーでX座標を-5してください。 ​ ​ 移動方法を相対移動にするために、Main CameraのAnimatorのApply Root Motionにチェックを入れてください(相対移動については2-4を参照) ​ ​ クリア時のズームアニメーションがループしないように、CameraClearアニメーションのLoop Timeのチェックを外しておいてください。 ​ ​ アニメーションの遷移を設定していきましょう。 プロジェクト内に自動で追加されたMain Camera(Animator Controller)をダブルクリックして、Animatorウィンドウを開いてください。 ​ ​ 新しくトリガー型のパラメータを追加してください。 ​名前はClearにしておきます。 ​ ​ CameraWaitからCameraClearに伸びるTransitionを作成してください。 ​ Has Exit Timeのチェックを外し、Transition Durationを0にしておきます。 ​ 遷移する条件に先ほど作成したClearを設定してください。 ​ ​ ゲームクリアした瞬間にパラメータを操作するスクリプトを書きましょう。 ​ ​ GameCameraスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameCamera : MonoBehaviour { GameObject m_player; [SerializeField] Vector3 CameraAddPos = Vector2.zero; [SerializeField] Vector2 CameraMaxPos = Vector2.zero; [SerializeField] Vector2 CameraMinPos = Vector2.zero; // ゲームマネージャー GameManager m_gameManager; // アニメーター Animator m_animator; // クリアアニメーションフラグ bool m_clearAnimFlag = false; void Start() { // ゲームマネージャーを取得 m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent(); // 自身にアタッチされているアニメーターを取得 m_animator = GetComponent(); // Playerタグのついたオブジェクトをターゲットにする m_player = GameObject.FindGameObjectWithTag("Player"); // 最初に座標更新しておく CameraUpdate(); } void Update() { // プレイヤーがいないなら何もしない if (m_player == null) { return; } // ゲームオーバーなら何もしない if (m_gameManager.GetState() == GameManager.GameState.enGameState_GameOver) { return; } // クリアアニメーション後なら何もしない if (m_clearAnimFlag) { return; } // 座標の更新 CameraUpdate(); } void CameraUpdate() { // カメラの位置を設定 transform.position = m_player.transform.position + CameraAddPos; // 座標がオーバーしないように調整 Vector3 cameraPos = transform.position; cameraPos.x = Mathf.Clamp(cameraPos.x, CameraMinPos.x, CameraMaxPos.x); cameraPos.y = Mathf.Clamp(cameraPos.y, CameraMinPos.y, CameraMaxPos.y); transform.position = cameraPos; } // クリアアニメーション public void CameraClearAnimation() { // アニメーション再生 m_animator.SetTrigger("Clear"); // アニメーションフラグを立てる m_clearAnimFlag = true; } } ​ ​ CameraClearAnimation関数はpublicになっているので、適切なタイミングで外部から呼び出しましょう。 ​ PlayerMoveスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // クリア演出 void GameClearAnimation() { // もう終わっている場合は中断 if (m_claerWait) { return; } // アニメーションを変更 m_animator.SetTrigger("Clear"); // カメラ演出 Camera.main.GetComponent().CameraClearAnimation(); // ゲームクリア演出終了 m_claerWait = true; } ~後略~ ​​【プログラムの解説】 ・Camera.main でメインカメラ(画面に表示しているカメラ)を取得することができます。 これでゲームクリアになった瞬間にカメラのアニメーションも再生されるようになりました。 ​ 実行してクリア時にカメラがズームすることを確認してみてください。ユニティちゃんが画面から外れてしまう場合は、CameraClearアニメーションのX座標の移動量を調整してください。 評価テスト ​ ​ これで一通りのゲームのルールは実装できました。 ​ 次のレッスンではUIを作成して、スコアや進行状況を確認できるようにしてみましょう。 ​ 【評価テスト】 https://forms.gle/7vg494no4AongsJ38 Next Lesson4「UIを作ろう」 ページ TOP 3-1 ゲームステートの実装 3-2 ゲームオーバーの実装 3-3 落下時にゲームオーバーにする 3-4 ゲームクリアの実装 評価テスト

  • EX 小ネタ | Unity1gc2

    LessonEX 小ネタ ページ数節約用 ページを作るほどでもないかなって感じの小ネタ集です。 ​ 項目ごとに関連性はないので、気になる内容だけ見てください。 EX-1 Attributeについて EX-1 Attributeについて 変数やクラスにAttribute (属性)をつけることで、特殊な挙動を設定することができます。ここでは制作を補助するAttributeを中心に紹介していきます。 ​ よく使うのは [SerializeField] で、これを付与した変数はprivateでもインスペクター内に表示されるようになります。 ​ 移動速度やジャンプ力など、後から調整したいパラメータにつけておくと便利です。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField] int TestNum; } 変数をpublicにしておくとどこから編集されるかわからない、というリスクがあります。ですが変数をprivateにするとインスペクターから値を編集できなくて不便です。 ​ そういった問題を解決したい時は [SerializeField] を使いましょう。 ​ ​ [Header("テスト")] を使うことで、変数名の前に文章を表示することができます。日本語も使用可能です。 ​ また、アトリビュートは,(カンマ)で区切ることで一度に複数付与することができます。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数") ] int TestNum; } 移動用パラメータや攻撃用パラメータなど、項目ごとにHeaderを使うと後から見やすいインスペクターになります。 ​ ​ [Range(0, 100)] を使うことで、第一引数から第二引数の範囲内に収まるスライダーを表示することができます。サンプルでは0~100の範囲で動かせるスライダーを作っています。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数")] int TestNum; // 指定した範囲内に収まるスライダー [Range(0, 100)] public int TestSlider; } ここで指定できる範囲はあくまでインスペクター内で有効 です。スクリプトから範囲外の変数を代入したとしても、自動で範囲内に収めてくれる訳ではないので注意してください。 ​ ​ [Tooltip("テスト")] を使うことで、インスペクター内の変数にマウスカーソルが重なっている時にメッセージを表示することができます。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数")] int TestNum; // 指定した範囲内に収まるスライダー [Range(0, 100), Tooltip("マウスが重なった時に表示") ] public int TestSlider; } 改行コード(¥n)を使うと改行もできます。 特に複雑な用途の変数は、後から見返したときにどういった変数か忘れてしまうこともあるので、Tooltipを使って注釈を書いておくと良いでしょう。 ​ ​ ​ string型の変数はAttributeを付与しなかった場合1行しか記入できませんが、[Multiline] を使うことで複数行記入することができるようになります。 引数に行数を設定することもできます。ただし行数を0から数えているので、10行書けるようにしたい場合は11を引数にする必要があります。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数")] int TestNum; // 指定した範囲内に収まるスライダー [Range(0, 100), Tooltip("マウスが重なった時に表示\n改行も可")] public int TestSlider; // 指定行数記入できるstring [Multiline(11)] public string TestString; } [TextArea(2,11)] を使うことで最小行数と最大行数を指定することもできます。サンプルでは最初は1行分の欄しかありませんが、行数を増やすと10行目まで拡張されていきます。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数")] int TestNum; // 指定した範囲内に収まるスライダー [Range(0, 100), Tooltip("マウスが重なった時に表示\n改行も可")] public int TestSlider; // 指定行数記入できるstring [TextArea(2,11)] public string TestString; } クラスに設定できるAttributeもあります。 クラスに [DisallowMultipleComponent] を付与することで、同じオブジェクトに同じコンポーネントが複数アタッチできないようにできます。同じオブジェクトにうっかり同じコンポーネントを複数アタッチしてしまうことがありますが、それをエラー文で防げるようになります。 using System.Collections; using System.Collections.Generic; using UnityEngine; // 複数アタッチできないようにする [DisallowMultipleComponent] public class EXK_Test : MonoBehaviour { // インスペクターに表示されるprivate変数 [SerializeField, Header("テスト用変数")] int TestNum; // 指定した範囲内に収まるスライダー [Range(0, 100), Tooltip("マウスが重なった時に表示\n改行も可")] public int TestSlider; // 指定行数記入できるstring [TextArea(2,11)] public string TestString; } 関数に設定するAttributeも解説します。 [RuntimeInitializeOnLoadMethod()] と記述すると、設定した関数はゲーム起動時のシーンを開く前に呼ばれます。クラスをオブジェクトにアタッチする必要はありません。 ただし対象とする関数をstaticにする必要があるので注意しましょう。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Test : MonoBehaviour { [RuntimeInitializeOnLoadMethod()] static void GameInit() { Debug.Log("どのシーンから開始しても呼ばれる"); } } この関数はどのシーンからゲームを開始しても呼ばれます。どのシーンにも共通で生成したいオブジェクトがある場合や、初期化したいものがある場合に使ってみてください。 Attributeを使うことでインスペクターの操作が楽になるので、ぜひ活用してみましょう。 EX-2 アニメーションステートが難しい人へ EX-2 アニメーションステートが難しい人へ 過去のLessonではアニメーションステートを使ってアニメーションを遷移させていました。​ ですが「アニメーションステートは複雑で扱いにくい… 」という感想を持った人もいると思います。そういった人向けに簡単にアニメーションを管理する方法を解説します。 ​ まずはアニメーションに対応したパラメータを用意します。 今回はパラメータの型にTriggerを使用します。これは名前の通りトリガーのように一瞬だけ有効判定されるもので、有効にした瞬間だけtrueになって、直後自動的にfalseになるbool型をイメージするのがわかりやすいかと思います。 Any State から全てのステートへ伸びるトランジションを作成してください。 ​ Any StateはAny(どれでも)の名の通り、どのステートからでも繋げられるステートになります。 トランジションの設定も今までと変わりません。 Conditionsには対応したTrigger型のパラメータを指定してください。 もし複数の遷移条件を同時に満たした場合、Transitionsの上部にある項目が優先されます。ドラッグ&ドロップで並び替えることが可能です。 後はAnimatorを取得して、SetTrigger関数でパラメータを変更するだけです。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EXK_Player : MonoBehaviour { Animator m_animator; void Start() { // 自分にアタッチされたアニメーターを取得 m_animator = GetComponent(); } void Update() { // キーに応じてアニメーションを変更 if (Input.GetKeyDown(KeyCode.Alpha1)) { m_animator.SetTrigger("Idle"); } if (Input.GetKeyDown(KeyCode.Alpha2)) { m_animator.SetTrigger("Run"); } if (Input.GetKeyDown(KeyCode.Alpha3)) { m_animator.SetTrigger("Jump"); } if (Input.GetKeyDown(KeyCode.Alpha4)) { m_animator.SetTrigger("Damage"); } if (Input.GetKeyDown(KeyCode.Alpha5)) { m_animator.SetTrigger("Clear"); } } } KeyCode.Alpha1 などは1キー、2キーなどを示すKeyCodeになります。 ​ これで比較的直感的にアニメーションを操作することが可能です。 ただしどのステートからでも同じ設定で遷移してしまうので注意してください。「待機状態から歩き状態に切り替える時だけはこうやって遷移したい!」という条件がある場合は、今まで通り各トランジションを繋ぐ必要があります。 EX-3 検索欄の隠し機能 EX-3 検索欄の隠し機能 Unityの検索欄には隠し機能があります。 ヒエラルキー内の検索欄はオブジェクトの名前だけでなく、コンポーネント名を入力することで「そのコンポーネントがアタッチされているオブジェクト」を検索することができます 。 ​RigidbodyなどUnity標準のコンポーネントだけでなく、自分で作ったコンポーネントも検索できます。 ​ ページ TOP EX-1 Attributeについて EX-2 アニメーションステートが難しい人へ EX-3 検索欄の隠し機能

  • 2Dランゲーム編 Lesson6「クオリティを上げよう」 | Unity1gc2

    2Dランゲーム編 Lesson6 クオリティを上げよう 6-1 タイトル画面の作成 6-1 タイトル画面の作成 タイトル画面を作成しましょう。やっていることは3Dアクションゲーム編とほとんど変わりません。 ​ ​ まずはTitleシーンを作成してください。 Titleシーンを開いて、タイトル画面を自由に作ってください。 ​ タイトルロゴは「Sprite」→「UI」→「Title」内にあります。 お好みでアニメーションや素材を追加しても構いません。サンプルではテキストのa値(不透明度)を変更することで、文字が現れたり消えたりするアニメーションを実装しています。 サンプルで行っているループする背景の実装方法です。アニメーションで実装する方法もありますが、ここではスクリプトを使って実装する方法を解説します。 ​ まずは背景をキャンバスより僅かに大きくしてから、右側の画面外にコピーして2枚目の背景を設置します。 新しいスクリプトUI_BGLoopを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class UI_BGLoop : MonoBehaviour { [SerializeField] Vector3 StartPos, EndPos; [SerializeField] float MoveTimer = 0.0f; [SerializeField] float MoveSpeed = 0.2f; RectTransform m_rectTransform; void Start() { // 自分のRectTransformを取得 m_rectTransform = GetComponent(); } void Update() { // 移動処理 MoveTimer += Time.deltaTime * MoveSpeed; m_rectTransform. anchoredPosition = Vector3.Lerp(StartPos, EndPos, MoveTimer); // タイマーをリセット if (MoveTimer >= 1.0f) { MoveTimer = 0.0f; } } } 処理の内容としてはVector3クラスのLerp関数を使って、開始地点から終了地点までを移動し続けるだけです。 右側の画面外から左に移動し続け、左側の画面外に出たら右側の画面外に戻る…といった動作を繰り返し続けます。 この処理を2枚の画像で行うことによって、背景がループしているように見せています。 ​ ​ UIの座標を操作するときはRectTransformを取得して 、そこから座標を変更するようにしましょう 。 RectTransformには「position」と「anchoredPosition」の2つのパラメータがありますが、それぞれ原点の位置が異なります。positionの場合原点はキャンバスの左下になります。 anchoredPositionの場合はキャンバス上のアンカーに指定した場所が原点になります。アンカーが中央なら、原点もキャンバスの中央になります。 後は2枚の背景画像にUI_BGLoopスクリプトをアタッチするだけです。 ​ 両方ともStartPos(移動開始地点)とEndPos(移動終了地点)を設定しましょう。 ​ また、1枚目の背景(現在キャンバス内に表示されている方)にはMoveTimerに0.5を入力してください。これによって1枚目の背景の開始地点がStartPosとEndPosの中間になります。 これで背景のスクロール処理は完成です。他のゲームでも処理を流用できるので「UIが味気ないな…」と思ったときはぜひ実装してみてください。 サンプルでは横の座標を決め打ちしていますが、様々なプラットフォームを考慮するなら画面サイズを取得するパラメータ Screen.width と Screen.height を使ってみてください。 Unity Tips! クリックされたらStageSelectシーンに切り替わるようにしましょう。 Titleスクリプトを作成して、以下のように入力してください ​。青い部分 は穴埋めです。 using System.Collections; using System.Collections.Generic; using UnityEngine; // ① シーンを扱うときに必要 (ここに入力) public class TItle : MonoBehaviour { [SerializeField] GameObject FadeCanvas; [SerializeField] string SceneName; void Update() { // ② 左クリックされたらシーン切り替え処理 if ( (ここに入力) ) { string sceneName = SceneName; // 名前が空白だった場合、現在のシーンの名前を使う if (sceneName == "") { sceneName = SceneManager.GetActiveScene().name; } // フェード用のCanvasを作成 GameObject fadeCanvas = Instantiate(FadeCanvas); // FadeSceneを取得してフェードを開始 fadeCanvas.GetComponent().FadeStart(sceneName); } } } 降参 or 答え合わせの方はこちら コードの内容は基本的にSceneButtonと同じになっています。 コードが書けたら保存して、適当な空オブジェクトを作成してください(サンプルでは名前をTitleにしています)そして、作成した空オブジェクトにTitleスクリプトをアタッチしましょう。 インスペクターはSceneButtonと同じように設定しておきます。FadeCanvasにはFadeCanvasのプレハブを、SceneNameには遷移先のシーン名「StageSelect」を設定しましょう。 最後にTitleシーンをビルド対象に設定しましょう。 ​ Build Settingsを開いて、Titleシーンをリストに追加してください。 ​ 3Dアクションゲーム編でも解説しましたが、ゲームを起動したときに最初に開くシーンは「Scenes In Build」の一番上になります。Titleシーンは開始時に開いてほしいシーンなので、ドラッグしてリストの一番上に動かしましょう。 これでタイトルシーンの実装は完了しました。実行して、クリックすることでステージセレクトに遷移することを確認してみてください。 6-2 BGMの実装 6-2 BGMの実装 次はBGMを実装しましょう。 ​ サンプルではLesson1でも紹介したsuperpowers-asset-packs からお借りしています。もちろん好きな配布サイトのBGMを使用しても構いません。 【サンプルで使用しているBGM】 ・タイトル…「top-down-shooter」→「music」→「theme-4.ogg」 ・ステージセレクト…medieval-fantasy」→「music」→「theme-5.ogg」 ・ステージ1…「rpg-battle-system」→「music」→「theme-2.ogg」 ・ステージ2…「medieval-fantasy」→「music」→「theme-4.ogg」 ・ステージ3…「rpg-battle-system」→「music」→「 theme-9.ogg」 ​ ​ Soundフォルダを作成して、その中にBGMフォルダを作成しましょう。BGMフォルダ内に使用したいBGMファイルをドラッグ&ドロップしてください。 追加したBGMファイルのLoad Typeを「Streaming」に変更してください。 ​ デフォルトではステレオ再生(複数のスピーカーで立体的に再生、音質が良い)になっていますが、今回は軽量化を優先するためモノラル再生(1つのスピーカーで再生、音質が良くない)に変更しましょう。Force To Monoにチェックを入れるとモノラル再生になります。 追加したBGMをそれぞれのシーンで再生できるようにしましょう。3Dアクションゲーム編と同じように実装してもよいのですが、シーンの切り替わりでBGMがぶつ切りになると違和感があるので、画面と同じようにBGMもフェード処理するようにしましょう。 ​ ​ 新しくBGM_Managerスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class BGM_Manager : MonoBehaviour { AudioSource m_audioSource; // BGMのフェード float m_volume = 0.0f; // 現在のボリューム bool m_fadeMode = false; // フェードの種類 false=だんだん大きく true=だんだん小さく bool m_isFade = false; // フェード処理中ならtrue [SerializeField] float FadeSpeed = 1.0f; // フェードの速度(大きいほど速い) // BGMのフェード開始 public void FadeStart(bool mode) { // 初期設定 m_fadeMode = mode; m_isFade = true; // 自身にアタッチされているオーディオソースを取得 m_audioSource = GetComponent(); // モードに応じて音量を初期化 if (mode == false) { m_volume = 0.0f; } else { m_volume = 1.0f; } } void Update() { // フェード中でないなら中断 if (m_isFade == false) { return; } if (m_fadeMode == false) { // 音量を大きくする m_volume += FadeSpeed * Time.deltaTime; // 音量を設定 m_audioSource.volume = m_volume; if (m_volume >= 1.0f) { // 音量が最大になったら終了 m_isFade = false; } } else { // 音量を小さくする m_volume -= FadeSpeed * Time.deltaTime; // 音量を設定 m_audioSource.volume = m_volume; if (m_volume <= 0.0f) { // 音量が最小になったら終了 m_isFade = false; } } } } 対象がオーディオソースのボリュームになっただけで、画面のフェード処理とほとんど同じです。 コードが書けたら保存して、Titleシーン内に適当な空オブジェクトを追加してください。オブジェクトの名前は分かりやすいようにBGMにしておきます。 追加したオブジェクトに以下の設定を行ってください。 ​ ・新しいタグ「BGM」を追加して、BGMオブジェクトに設定 ・BGM_Managerスクリプトをアタッチ ・「Add Component」からAudioSauceをアタッチ ​ ・AudioClipにタイトル用のBGMを設定 ・Play On AwakeとLoopにチェックを入れる 後はフェード処理したいタイミングでFadeStart関数を呼び出すだけになります。今回は画面のフェード処理に合わせてBGMもフェード処理させましょう。 ​ FadeSceneスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // フェード開始 public void FadeStart(string sceneName) { // フェード開始の準備をする m_fadeStart = true; m_sceneName = sceneName; // 自分の子オブジェクトにアタッチされているImageを取得する m_image = transform.GetChild(0).GetComponent(); // BGMタグが設定されたオブジェクトからBGM_Managerを取得する BGM_Manager bgm_Manager = GameObject.FindGameObjectWithTag("BGM"). GetComponent(); // BGMを小さくするフェード開始 bgm_Manager.FadeStart(true); // 自身はシーンをまたいでも削除されないようにする DontDestroyOnLoad(gameObject); } ​~後略~ ~前略~ // フェード処理 if (m_fadeMode == false) { // 画面を暗くする m_alpha += FadeSpeed * Time.deltaTime; // 完全に暗くなったのでシーンを変更する if (m_alpha >= 1.0f) { SceneManager.LoadScene(m_sceneName); // 明るくするモードに変更 m_fadeMode = true; // BGMタグが設定されたオブジェクトからBGM_Managerを取得する BGM_Manager bgm_Manager = GameObject.FindGameObjectWithTag("BGM"). GetComponent(); // BGMを大きくするフェード開始 bgm_Manager.FadeStart(false); } } else ​~後略~ コードが書けたら保存して、BGMオブジェクトをプレハブ化してください。ヒエラルキーから削除する必要はありません。 プレハブ化したBGMオブジェクトをそれぞれのシーンに追加して、AudioClipだけはシーンに応じたものに変更してください。 これでBGMのフェード処理は完成です。実行して、シーンが切り替わる時にBGMの音量が変化することを確認してみてください。 6-3 SEの実装 6-3 SEの実装 SEも実装しましょう。 ​ 効果音の素材もBGMと同じようにsuperpowers-asset-packs からお借りしています。種類は多いですが、こちらも好きな効果音を各自で用意してください。 ​ 【サンプルで使用しているSE】 ・ゲーム開始…「rpg-battle-system」→「sound」→「9.ogg」 ・ボタンクリック(ステージセレクトやゲームオーバーなど)… 「ninja-adventure」→「sounds」→「8.ogg」 ・ジャンプ…「 ninja-adventure」→「 sounds」→「1 8.ogg」 ・宝石獲得…「space-shooter」→「sounds」→「gold-3.wav」 ・磁力アイテム獲得…「ninja-adventure」→「sounds」→「power-up.ogg」 ・矢の発射…「medieval-fantasy」→「sounds」→「woosh-1.wav」 ・ダメージ… 「rpg-battle-system」→「sound」→「12.ogg」 ・落下… 「 ninja-adventure」→「 sounds」→「11 .ogg」 ・ゲームオーバー… 「ninja-adventure」→「sounds」→「game-over-2.ogg」 ​ ・ゴール…「western-fps-2d」→「sounds」→「gun-2.ogg」 ・ゲームクリア… 「rpg-battle-system」→「sound」→「33.ogg」 ​ ​ Soundフォルダ内にSEフォルダを作成しましょう。SEフォルダ内に使用したいSEファイルをドラッグ&ドロップしてください。効果音もForce To Monoにチェックを入れて、モノラル再生に変更しておいてください。 今回は効率化のために効果音再生の処理を統一します。 ​ 3Dアクションゲーム編6-2 で紹介したOneShotAudioClipスクリプトをそのまま流用しましょう。過去に記述したことがある場合は、エクスプローラーからそのままドラッグ&ドロップすれば書く手間を省けます。書いていない場合は3Dアクションゲーム編6-2 を確認して、スクリプトを追加してください。 新しい空オブジェクトOneShotSEを作成して、先ほど追加したOneShotAudioClipとAudioSauceをアタッチしてください。 AudioSauceコンポーネントはPlay On Awakeのチェックを外しておきましょう。 OneShotSEをResourcesフォルダ内にドラッグ&ドロップしてプレハブ化しておきましょう。プレハブ化できたらヒエラルキー内のOneShotSEは削除しておいてください。 効果音の再生処理を関数にします 。 GameManagerスクリプトを開いて、以下のコードを追加してください。関数として追加できるならコードの場所はどこでも構いません。 ~前略~ // 効果音再生関数 どこからでも呼べる static public void PlaySE(AudioClip clip) { GameObject oneShotObj = Instantiate((GameObject)Resources.Load("OneShotSE")); oneShotObj.GetComponent().PlaySE(clip); } ​~後略~ 【プログラムの解説】 ・static public 修飾子をつけた関数は ( クラス名).(関数名) でどこからでも呼べるようになります 。例えば広範囲のスクリプトで同じ関数を実行したい時などに便利です。 ​ ​ これで引数に再生したいAudioClipを追加するだけで効果音を再生することが可能です。 ​ 試しにプレイヤーのジャンプ音を追加してみましょう。PlayerMoveスクリプトに赤い部分のコード を追加してください。 ~前略~ // 演出フラグ bool m_claerRun, m_claerWait; // 効果音 [SerializeField, Header("効果音素材")] AudioClip m_jumpSE; void Start() { ​~後略~ ~前略~ // ジャンプ void Jump(bool airJump) { // プレイ中でないなら中断 if (m_gameManager.GetState() != GameManager.GameState.enGameState_Play) { return; } // 加わっている力を一旦リセット m_player_rb2d.velocity = Vector2.zero; // 上方向に力を加える m_player_rb2d.AddForce(new Vector2(0.0f, JumpPower), ForceMode2D.Impulse); // アニメーション m_animator.SetBool("FallFlag", false); if (airJump) { m_animator.SetBool("AirJumpFlag", true); } else { m_animator.SetBool("JumpFlag", true); } // 効果音再生 GameManager.PlaySE(m_jumpSE); } ​~後略~ プレイヤーのプレハブをダブルクリックして開き、インスペクターに表示されているm_jumpSEにジャンプ音として使用したいAudioClipを設定してください。 これでジャンプ音の設定は完了です。同じ手順で他の効果音も追加できるので、ぜひ色々なポイントで効果音を鳴らしてみてください。 効果音のボリュームを関数で設定できるようにOneShotAudioClipスクリプトを改造しましょう。特に宝石の取得音など、音量が大きすぎると困る効果音にオススメです。 OneShotAudioClipスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ public void PlaySE(AudioClip audioClip, float volume = 1.0f ) { // 自分にアタッチされているAudioSourceを取得 m_audioSource = GetComponent(); // オーディオクリップを設定 m_audioSource.clip = audioClip; m_audioSource.volume = volume; // 再生 m_audioSource.Play(); // 再生フラグを立てる IsPlay = true; } ​~後略~ 【プログラムの解説】 ​・float型の引数volumeに1.0fの初期値が設定されています。​これは「引数が指定された場合はその値を扱うけど、指定しなかった場合は初期値(1.0f)を使うよ 」という意味になります。 初期値を設定しなかった場合、効果音を再生する度に必ずボリュームを指定する必要が出てしまうため少し面倒です。使っても使わなくてもいい引数を宣言したいときは初期値を指定するようにしましょう。 ​ 再生処理の方もこれに合わせて改造します。 GameManagerスクリプトを開いて、赤い部分のコード を追加してください。 ~前略~ // 効果音再生関数 どこからでも呼べる static public void PlaySE(AudioClip clip, float volume = 1.0f ) { GameObject oneShotObj = Instantiate((GameObject)Resources.Load("OneShotSE")); oneShotObj.GetComponent().PlaySE(clip, volume ); } ​~後略~ これで効果音を再生するときにボリュームを指定できるようになりました。特に指定したくない時は第二引数を入れなければ自動的に1.0fが代入されるようになります。 サンプルでは宝石の効果音を再生するときに音量を大きく下げて、ゲームオーバーやクリアの効果音は音量を上げています。 Unity Tips! 6-4 エフェクトの実装 6-4 エフェクトの実装 最後に宝石の取得時とゲームオーバー時にエフェクトを再生するようにしましょう。 ​ ​ 「Sprite」→「Effect」内のGetスプライトを選び、Sprite Modeを「Multiple」に変更してください。変更できたらSprite Editorを開いてください。 Sprite Editorを開いたら、左上のSliceをクリックしてください。 Typeを「Grid By Cell Count」に変更して、Cに5を入力してください。エフェクトが5枚に分割されたら、Sliceボタンをクリックしましょう。 ​ 最後に右上のApplyボタンをクリックしたら、画像の分割は完了です。Sprite Editorを閉じてください。 分割したスプライトGetをシーン上にドラッグ&ドロップしてください。後でプレハブ化するので、シーンはどこでも構いません。 アニメーションの保存先を選べるのでAnimationフォルダ内のEffectAnimationを選択しておきます。名前は自分がわかりやすいもので構いません。 ​ アニメーション名を決めたら右下の保存ボタンを押して保存しましょう。 作成したアニメーションがループ再生になっていないか確認してください。Loop Timeにチェックが入っていた場合はチェックを外しておきましょう。 再生が終わったエフェクトがシーン上に残り続けてはいけないので、エフェクトの再生が終わったら自動で消えるようにしましょう。現在Unityにそのような機能はないため、スクリプトで実装します。 ​ 新しいスクリプトAnimEndDestroyを追加して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; // アニメーションが終了したら自動で消えるエフェクト public class AnimEndDestroy : MonoBehaviour { float m_effectLength = 0.0f; // エフェクトの長さ float m_timer = 0.0f; // エフェクト再生タイマー void Start() { // アニメーションの長さを取得 m_effectLength = GetComponent().GetCurrentAnimatorStateInfo(0).length; } void Update() { m_timer += Time.deltaTime; // もしアニメーションが終了していたら自身を削除する if(m_effectLength < m_timer) { Destroy(gameObject); } } } 【プログラムの解説】 ・AnimatorクラスのGetCurrentAnimatorStateInfo関数で、現在再生中のアニメーションについての情報を取得することができます。 (第一引数には取得するアニメーションのレイヤーを指定します) ​ ​ コードが書けたら保存して、AnimEndDestroyスクリプトを先ほど追加したGetアニメーションのオブジェクトにアタッチしてください。 次はエフェクトをプレハブ化しましょう。 ​ Prefabフォルダ内にEffectPrefabフォルダを作成し、そこへヒエラルキーからGetオブジェクトをドラッグ&ドロップしてください。プレハブ化できたらシーン上のGetオブジェクトは削除しておきます。 後は宝石の取得時にGetエフェクトを生成するだけです。 ​ ​ Gemスクリプトを開いて、赤い部分のコードを追加してください。 ​※ 効果音の処理は6-3で実装したものですが、実装できていない人は同じようにコードを追加してください。 ~前略~ // 効果音 [SerializeField, Header("効果音素材")] AudioClip m_getSE; [SerializeField] float GemSE_volume = 1.0f; // エフェクト [SerializeField] GameObject GetEffect; void Update() { if (m_magnet) ​~後略~ ~前略~ private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーが衝突したら… if (collision.CompareTag("Player")) { // スコアを加算 GameObject.FindGameObjectWithTag("GameController").GetComponent().AddScore(Point); // 効果音再生 GameManager.PlaySE(m_getSE, GemSE_volume); // エフェクト再生 Instantiate(GetEffect, transform.position, Quaternion.identity); // 自身を削除する Destroy(gameObject); } // 磁力ゾーンが衝突したら… else if (collision.CompareTag("Magnet")) ​~後略~ Gemのプレハブを選択して、Get EffectにGetエフェクトのプレハブを設定してください。 ゲームを実行して、宝石の取得時にエフェクトが再生されることを確認してみてください。エフェクトが小さい場合はプレハブ側のScaleを調整してみてください。 教材を見ながら、同じ手順でダメージのエフェクトも作成してみましょう。 ​ 【手順】 ① 画像を分割 ② シーン上に追加 ③ アニメーションの設定 ④ AnimEndDestroyコンポーネントの追加 ⑤ プレハブ化 ⑥ PlayerMoveスクリプトの改造 ⑦ インスペクターで生成するプレハブを設定 ​ ​ ダメージエフェクトはプレイヤーより前面に表示されてほしいので、プレイヤーのOrder in Layerより、ダメージエフェクトのOrder in Layerの値が大きくなるようにしてください。 PlayerMoveスクリプトを開いて、ゲームオーバー時にGemスクリプトと同じようにエフェクトを再生するように改造しましょう。 降参 or 答え合わせの方はこちら Playerのプレハブにダメージエフェクトのプレハブを設定できたらOKです。 ダメージを受けたときにエフェクトが再生されることを確認してみてください。エフェクトの生成位置や大きさはお好みで調整してかまいません。 ダメージエフェクトを再生できたら、2Dランゲーム編は終了になります!お疲れ様でした! 6-5 スマートフォン向けにビルドする 6-5 スマートフォン向けにビルドする 2Dランゲーム編では単純な2Dゲームの作り方だけでなく、演出にこだわったり後から拡張しやすいように工夫したりすることを意識して解説してきました。ほとんどの要素をプレハブ化しているので、後からステージを追加することも容易かと思われます。 LessonEXを見て追加できそうな要素はぜひ追加してみましょう。最初はポーズ画面の実装がオススメです。 皆さんがオリジナルのゲームを作る際にもこの教材を活かして「プレイヤーだけでなく制作側にも優しい 」ゲームを作ってみてください。 ​ ​ それでは完成したゲームをビルドしてみましょう。AndroidとIOSで設定方法が違うので、使用したい端末に合わせて教材を進めていってください。 → Androidの場合 ​ → IOSの場合 6-5 Androidの場合 【Androidの場合】 ​ まずはUnityにAndroidへ出力するためのモジュールが追加されているか確認してください。 Unity Hubの「インストール」から、使用しているバージョンのUnityにAndroidの表記があることを確認してください。 ない場合は右上のボタンをクリックして「モジュールを追加」を選択し「Android Build Support」「Android SDK&NDK Tools」「Open JDK」をインストールしてください。 次はスマートフォン側の準備をします。 ​ ​ Androidの設定を開いて、デバイス情報内のビルド番号を7回タップして開発者モードに変更してください(7回じゃない場合もあります) 設定のシステム→開発者向けオプション内のUSBデバッグをオンにしておきます。 スマートフォンとPCをケーブルで繋いでください。その際にスマートフォン側にUSBデバッグの許可の確認が表示されるので「許可」を選択します。 Build Settingsを開き、プラットフォームをAndroidに変更します。 変更すると右下に 「Switch Platform」が表示されるので、クリックしてください(少し時間がかかります) 左下のPlayer Settingsでゲーム名やアイコンを変更できます。アイコン素材は同梱していないので、アイコンを変更したい場合は各自用意してください。 スマートフォンなので回転の設定もできます。 ​ 今回は「横画面固定で、左右どちら側が下になっても自動で回転する 」ように設定していきます。 Default Orientationを「Auto Rotation」にして、Portrait(縦)とPortrait Upside Down(スマートフォンの上側が下)のチェックを外しておいてください。 設定が終わったらビルドしましょう。 スマートフォンをPCに繋いだ状態で、Build Settings右下の「Build And Run」を選択してください。保存先を聞かれるので適当な場所を選択してください。ただしAssetフォルダ下を指定してはいけません 。 ビルドが終わったら自動的にスマートフォン側でゲームが起動します。実機でビルドしながらUIの大きさや難易度を調整していってください。 ​【参考サイト】https://miyagame.net/unity-real-machine-android/#index_id6 6-5 IOSの場合 【IOSの場合】 ​ 環境がないので確認できていません。実行でき次第追記します。 (参考になりそうなサイト ) ページ TOP 6-1 タイトル画面の作成 6-2 BGMの実装 6-3 SEの実装 6-4 エフェクトの実装 6-5 スマートフォン向けにビルドする 6-5 Androidの場合 6-5 IOSの場合

  • EX 複数のカメラを扱う | Unity1gc2

    LessonEX 複数のカメラを扱う EX-1 RenderTexture ゲームには「どこからどこまでをゲーム画面に描画するか」を指定する、メインカメラ があります。メインカメラに映ったオブジェクトが実際のゲーム画面に表示されます。 ​ では複数のカメラを作成して、そのカメラに映ったものを描画するにはどうすれば良いのでしょうか。Unityにはメインカメラとは別でサブカメラを作成して、サブカメラに映ったものをテクスチャとして表示する機能があります。サブカメラを使ってステージを上から見た光景をゲーム内に表示してみましょう( ​深く知りたい方は HLSLシェーダーの魔導書のオフスクリーンレンダリングのページも参照してください) ​ ​ まずは適当にオブジェクトを置いたシーンと、サブカメラの内容を表示するためのモデルを用意してください。 ヒエラルキーから「Camera」を選択してカメラを追加してください。 作成したカメラの座標や回転を調整して、上から見る形にしてください。シーンビューの右下にプレビューがあるため参考にしましょう。 サブカメラの内容を出力するためのRender Texture を作成しましょう。 ​ プロジェクト内で右クリックして「Create」→「Render Texture」を選択してください。 RenderTextureにはサイズや描画方法などを設定する項目があります。カラーフォーマットやサイズを指定する点はDirectXの内容と変わりません。 今回は何も変更しなくて構いません。 サブカメラのTargetTextureに作成したRenderTextureをドラッグ&ドロップしてください。これでサブカメラの内容がRenderTextureに表示されるようになります。 3Dアクションゲーム編6-2 で解説していますが、サウンド処理における「耳」の役割を果たすAudio Listenerは同一シーン上に2つ以上存在することができません。カメラには自動的にAudio Listenerがアタッチされているため、サブカメラのAudio Listenerを外しておきましょう。 コンポーネント右上の3つの点をクリックして「Remove Component」を選択してください。 Audio Listenerが複数ある状態でゲームを実行すると「オーディオリスナーが2つあるよ」という警告が出るので注意しましょう。ちなみにオーディオリスナーは1つを残して無効化されます。 RenderTextureの内容を表示したいオブジェクトにRenderTextureをドラッグ&ドロップすると、自動でマテリアルが作成されます。 Materialsフォルダ内にはRenderTextureを表示するためのマテリアルが作成されています。お好みで調整してください。 ゲームを実行するとサブカメラの内容がRenderTextureに表示されています。 モデルを動かすとRenderTextureの表示内容も更新されます。確認してみましょう。 他にもRenderTextureを使うことで面白い演出を作ることができます。 ​ 適当なオブジェクトを選択して「Layer」→「Add Layer…」を選択してください。 空いている場所に適当なレイヤー名を入れてください。名前を指定することでレイヤーとして使えるようになります。 再度適当なオブジェクトを選んで、追加したレイヤーを選択してください。 メインカメラを選択してCulling Maskから先ほど追加したレイヤーのチェックを外してください。これでメインカメラに描画されなくなります。 この状態で実行するとメインカメラには映らず、サブカメラには映るオブジェクトが完成します。メインカメラとサブカメラのCulling Maskの設定を変えることでこのように不思議な演出を作ることができます。 RenderTextureの内容をオブジェクトではなくUIに表示する方法も解説します。 「UI」→「Raw Image 」を追加してください。ImageではなくRaw Imageなので注意しましょう。 Raw ImageはSpriteではなくTextureを表示することができます。例えば今回のようにカメラの内容であったり、スクリプトで動的に生成した画像であったりと、特殊なものを表示する時に使います。基本はImageを使うので、必要な時に思い出す程度でOKです。 ​ ​ あとはRenderTextureをRaw ImageのTextureにドラッグ&ドロップするだけでOKです。 これでサブカメラの内容をUIに表示することができました。 ​ ​ ​ カメラを複数配置することで面白い演出を作ることができますが、当然描画回数は増えるため使いすぎに注意しましょう。

  • 2Dランゲーム編 Lesson2「ステージを作ろう」 | Unity1gc2

    2Dランゲーム編 Lesson2 ステージを作ろう 2-1 タイルマップ 2-1 タイルマップ UnityにはTileMap という機能があります。グリッド上にタイルを配置することでステージを作ることができる機能です。 RPGツクールやマリオメーカーあたりがイメージしやすいと思います。 (画像元サイト) ​ ​ タイルマップに配置する素材(画像でいうブロックやコイン)をタイルパレットと言います。 配布した素材にもタイルパレット画像が同梱されています。 まずはタイルパレットの設定をして、タイルマップが使えるようにしてみましょう。 ​ ​ まずはタイルパレットを保存するためのPalletフォルダを作成してください。 「Sprite」→「TileMap」内にある画像素材を選択してください。 SpriteModeをMultuple、PixelsPerUnitを16に変更して、Applyをクリックしてください。 上部の「Window」→「2D」→「Tile Pallet」を選択して、タイルパレットウィンドウを開きます。 ​ 「Create New Pallet」を選択してパレット名を入力してCreateをクリックします。 ​ ​ クリックすると保存先を聞かれるので、先ほど作ったPalletフォルダにしておきましょう。 パレットを作成できたら、タイルパレット用の画像をドラッグ&ドロップしてください。パレットの作成先を聞かれますが、先ほどと同じようにPalletフォルダにしておきます。 ​ タイルが1000枚以上あるため時間がかかりますが、完了するとパレットが表示されます。 ヒエラルキーから「2D Object」→「Tilemap」→「Rectangular」を選択してタイルマップを追加してください。 ​ 四角形で構成されたタイルマップが作成されます。 Hexagonalは六角形のグリッド、Isometricは斜め四角形のグリッドになります。 ​ 今回のゲームでは使いませんが、ぜひこれを使ったゲームも作ってみてください。 Unity Tips! 今まで使っていた地面は必要なくなるので削除しておきましょう。 タイルパレットを使いやすい場所に移動させてください。 ​ 地面のタイルを選択して、タイルマップ上に地面を描いてみましょう。 範囲塗りツールを選択すると広範囲を一気に塗ることができます。 ​ 他にもスポイトや消しゴムツールも使えます。 タイルを選んだ状態で { キー または } キー を押すと回転させることができます。Shift+{ キーを押すと左右反転させることができます。 ​ 上り坂のタイルを反転させて下り坂にしてみましょう。 短くていいので、タイルパレットを使った簡単なステージを作ってみましょう。 ​ 【注意すること】 ・上り坂と下り坂を配置するようにしてみてください 。 ​・パレット上部に様々な装飾がありますが、後で配置するので今は地形だけ 作ってみてください。 地形が完成したら実行して遊んでみてください。 ​ …残念ながらユニティちゃんは地形を貫通して落下してしまいました。作成したタイルマップに当たり判定がないため、ユニティちゃんが着地できなかったようです。 ​ タイルマップにAddComponentから「Tilemap Collider 2D」を追加してください。 ​ 地面として判定できるようにGroundタグもつけておきましょう。 これでタイルマップに当たり判定が追加されました。 しかし、パレットの凹凸を律儀に反映しているため坂道がガタガタになってしまっています。このままでは壁と判定されてゲームオーバーになってしまう理不尽な坂道になってしまうので、当たり判定を修正しましょう (ゲームオーバー判定になるかは実行速度にもよります) タイルパレットに使用した画像を選択して、Sprite Editorを開いてください。 Sprite Editorが開いたら、左上のSprite Editorと表示されているボタンをクリックして「Custom Physics Shape」に変更してください。 ​ このモードではスプライトの当たり判定を手動で設定することができます。 使用している坂道のタイルを選択して、範囲内でマウスをドラッグしてください。 ​ 4つの点で構成された四角い当たり判定が作成されます。この点を動かして、坂道の当たり判定を設定していきましょう。 ​ 左上以外の点は隅に移動させます。 ​ ​ 左上の点は必要ないので、選択した状態でDeleteキーを押して削除しましょう。 坂の上部も同じように当たり判定を設定していきます。 ​ 坂のつなぎ目の部分は当たり判定を作ってほしくないので、点を全て端に寄せておきます。 緩やかな坂も同じように当たり判定を作成していきます。 坂の当たり判定が作成できたら、右上のApplyをクリックしてください。 Tilemap Collider 2Dをリセットすると、Sprite Editorで設定した当たり判定が反映されます。 まだ当たり判定がおかしい場合は再度Sprite Editorを開いて調整してください。 Sprite Editorで当たり判定を調整してもまれにガタついてしまうことがあります。現在の状態ではタイルごとに当たり判定があるため、タイルの境界にひっかかってしまうようです。 タイルマップに「Composite Collider 2D」を追加してください。これは当たり判定を合成してくれるコライダーです。 Composite Collider 2Dを扱うためにはRigidBody 2Dが必要です。Composite Collider 2Dを追加した時に自動で追加されているはずです。 ですが、このままではステージが重力を持って落下してしまいます。Body TypeをStatic(静的)にして動かないようにしてください。 坂を登っている時などにタイルマップがちらついてしまう問題を修正しましょう。これはUnityが自動でアンチエイリアス をかけて、タイルの輪郭をぼかしてしまっていることが原因です。 上部の「Edit」→「Project Settings…」を選択してください。 Quality内にあるAnti Aliasingを「Disabled」に変更するとアンチエイリアスがかからなくなります。このゲームに限らず、ドット絵を扱うゲームであればとりあえず変更しておくとよいと思います。 これで一通りの設定は完了しました。 ​ タイルマップを使って1ステージ分の地形を完成させてみましょう。 後々ゴールを置くため、終点の地形は広めに取っておいてください。後からギミックや装飾を置けるように、道中も広めにスペースを取っておくと良いでしょう。 ステージに高さや長さを出したい場合は、Main Cameraが移動できる範囲を調整してください。 2-2 地形を装飾 2-2 地形を装飾 タイルパレットの上部には装飾に使えそうなパーツがあります。せっかくなので使ってみましょう。 ​ しかし、現在地形に使っているタイルマップに装飾を置いてしまうとTilemap Collider 2Dによって当たり判定が生成されてしまいます。 ​ タイルマップは1つである必要はなく、用途によって分けることができます。 装飾用にもう一つタイルマップを作成して、そこに装飾用のタイルを配置していきましょう。 ​ ​ Gridを右クリックして「2D Object」→「Tilemap」→「Rectangular」から新しいTileMapを作成してください。装飾用とわかりやすいように名前はDecorationにしておきます。 作成したタイルマップの描画優先度を-1にしておきましょう。Order in Layerの値が大きい方が前面に描画されます。 ついでにプレイヤーの描画優先度を1にしておきます。これで装飾→地形→プレイヤーの順番で描画されます。 Active Tilemapを地形ではなく装飾用タイルマップに変更してください。ここで接地対象のタイルマップを選ぶことができます。 装飾用タイルマップに装飾を配置してみてください。Decorationタイルマップには当たり判定を設定していないので、ユニティちゃんが衝突することはありません。 ​ Order in Layer(描画優先度)を-1にしているので、装飾はユニティちゃんの後ろに描画されます。ユニティちゃんの前面にも装飾を配置したい場合は、新しく前面装飾用のタイルマップを作り優先度を2以上にしてみてください。 ​ ​ ステージに装飾を配置できたらOKです。 同じ装飾が並ぶと単調に見えるので、Shift+{ で左右反転させながらパーツを置いてみましょう。あまり置きすぎると後ほど設置するギミックが見えなくなるので、ほどほどで構いません。 2-3 背景を設定 2-3 背景を設定 いつまでも真っ青な背景のままでは寂しいので、背景を設置しましょう。 ​ 「Sprite」→「BackGround」から背景画像を全て選択してください。そして、MeshTypeを「FullRect」にしてApplyをクリックしてください。これはループする画像を配置する時に必要な設定になります。 設定できたら背景に使いたい画像をシーン上にドラッグ&ドロップしてください。 配布した背景素材の中でも前景と遠景があるので、まずは前景(手前側に表示するもの)を配置しましょう。 配置した画像に前景として区別できる名前をつけておきましょう。Order in Layer(描画優先度)を-4にしてプレイヤーや地形より後ろに表示されるようにします。 スプライトのDrawModeを「Tiled」に変更してください。Widthの値を大きくすると左右でループする背景になります。 ScaleやPositionも調整して、適当な場所に背景を設置しましょう。 前景をコピー&ペーストして遠景も作成しましょう。Spriteを差し替えて、Order in Layerを前景よりもさらに後ろになるように設定します。 このままでも良いですが、背景とステージでスクロールがずれるようにして奥行きを出しましょう。 ​ ​ 新しくBG_Scrollスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class BG_Scroll : MonoBehaviour { PlayerMove m_plsyerMove; [SerializeField,Header("移動速度補正 大きいほど遅くなる)")] float Division = 0.0f; void Start() { // PlayerMoveを取得 m_plsyerMove = GameObject.FindGameObjectWithTag("Player").GetComponent(); } void Update() { // 移動量を計算 float move = (m_plsyerMove.MoveSpeed / Division) * Time.deltaTime; // 移動する transform.Translate(new Vector3(move, 0.0f, 0.0f)); } } 移動量の計算にプレイヤーの移動速度を組み込むことで、プレイヤーの移動速度を変更すると背景の移動速度も変わるようにしています。 ​ ​ コードが書けたら保存して、背景にアタッチしてください。前景と遠景でそれぞれ移動速度のパラメータを調整してください。 ​ ​ Division…前景 6 遠景 40 これで移動中に背景が少しずれるようになりました。スクロールで背景の端が見えてしまう場合はWidthの値を大きくしてください。 ​ ​ ゲームオーバーになっても背景が移動し続けている問題がありますが、これは後ほど修正するので今は気にしないでください。 2-4 宝石を作成 2-4 宝石を作成 触れるとスコアが増える宝石を作成しましょう。 ​ ​ 「Sprite」→「Gimmick」内に宝石の素材があります。 宝石の画像を全て選択して、SpriteTypeをMultipleに変更してください。変更したらApplyを忘れずにしておきましょう。 使いたい宝石の画像を選んでSpriteEditorを開いてください。 左上のSliceをクリックして、Typeを「Grid By Cell Count」に変更します。 その下のColumn&RowをC=4 R=1にしてください。これで横4、縦1に画像がスライスされます。 Sliceボタンをクリックして、右上のApplyボタンをクリックしてください。 ​ 設定できたらSprite Editorを閉じてください。 ​ 宝石の右側にある三角ボタンをクリックすると、画像が分割されていることがわかります。 Typeを「Automatic」にすると1枚の画像を自動で分割してくれます。 全てのパーツをバラバラに取り込むより、1枚の画像を分割して使用した方が処理は軽くなります。素材の管理もしやすくなるので、ぜひSprite Editorを活用してみてください。 Unity Tips! 1枚でアニメーションになっている画像は、シーン上に配置するだけで自動でアニメーションを作成してくれます。 ​ 使いたい宝石の画像をシーン上にドラッグ&ドロップしてください。アニメーションを作成する場所を聞かれるのでAnimatonフォルダを指定して、名前をGemAnimatonにして保存しましょう。 これで宝石が自動でアニメーションするようになりました。 ​ ​ デフォルトでは宝石がかなり小さいので、大きく調整してください。後でプレハブ化するため座標は適当で構いません。 大きさを調整したら実行してみてください。宝石が自動でアニメーションしていることが確認できます。 ​ しかし、宝石の光るアニメーションの頻度が高く目がチカチカしてしまいます。アニメーションを調整しましょう。 ​ Animatonフォルダを開いてください。 先ほど作成した宝石のアニメーションがありますが、ユニティちゃんのアニメーションと混ざってわかりにくいので、新規にGemAnimatonフォルダを作成してドラッグ&ドロップして分けておきます。 GemAnimatonをクリックしてアニメーションウィンドウを開いてください。 ​ 宝石が光る部分を囲んで選択し、右へ移動させてください。間隔はお好みで構いません。 これで完成でも良いのですが、宝石が全く動かないと背景に紛れて見えにくい気がします。宝石が上下に揺れるアニメーションを作りましょう。 ​ ​ ヒエラルキー内の宝石をクリックして、Add PropertyからPositionの項目を追加してください。 アニメーションの移動に使う座標には絶対座標 と相対座標 があります。 ​ 絶対座標 はオブジェクトの初期位置がどこであっても必ず設定した座標から始まり、設定した座標へ移動します。初期設定では絶対座標を使うようになっています。 ​ 宝石は後ほど量産して大量に設置するのですが、この設定だとステージ中の宝石が一点の座標へ移動してしまいます。 相対移動 相対座標 はオブジェクトの初期位置を原点として扱います。オブジェクトがどこにあっても、指定した量だけ移動するようにできます。 ​ これで宝石をステージ中に設置してもその場で上下に揺れるアニメーションを再生できるようになります。 アニメーションが 相対座標を使用するようにするには、AnimatorコンポーネントのApply Root Motionにチェックを入れる必要があります。 初期位置→少し上の位置→初期位置 と移動させたいので、キーを3つ作成します。 X=0,Y=0を基準に、上下に移動するアニメーションを設定してください。中間のキーのY座標はお好みで調整してください。 ここまで設定できたら実行して、宝石がその場で上下に揺れていることを確認してみてください(宝石をコピー&ペーストで増やすとわかりやすいです) ユニティちゃんが宝石に触れたら取得できるようにしましょう。 ​ 宝石にCircle Collider 2Dを追加します。 ユニティちゃんが弾かれないようにIs Triggerにチェックを入れ、 Radius(半径)を調整してください。取得用の当たり判定は少し大きめにした方が「取れたはずなのに取れなかった!」というストレスをプレイヤーに与えなくて済みます。 宝石のコライダーが坂道判定に引っかからないように、LayerをIgnore Raycastにしておいてください(1-6で詳しく解説しています) Gemスクリプトを作成して、以下のように入力してください。今はユニティちゃんが触れたら宝石が消えるだけで、実際にスコアを加算する処理は後ほど書きます。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Gem : MonoBehaviour { [SerializeField,Tooltip("獲得できるポイント")] int Point = 0; private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーが衝突したら… if (collision.CompareTag("Player")) { // スコアを加算 // (後で記述) // 自身を削除する Destroy(gameObject); } } } 【プログラムの解説】 ​・Tooltip はアトリビュートの一種で、インスペクター内で変数にカーソルを合わせた際に注釈を表示することができます。複雑な用途の変数に使うのがオススメです。 コードが書けたら保存して、宝石にGemスクリプトをアタッチしてください。インスペクターで獲得できるポイントが設定できるので、お好みのポイント量にしておきましょう。 このままでは宝石は触れたら消えるだけのオブジェクトになってしまうので、ポイントを取得できるようにしましょう。 ​ ​ ヒエラルキーから空オブジェクトを作成してください。名前はGameにしておきます。 GameオブジェクトのタグをGameControllerにします。 (このタグはUnityのデフォルトとして作成されているタグですが、特に用途は定められていません。ですが、スコアや進行状況の管理などをするオブジェクトに設定することを目的に用意されているようです) 新しいスクリプトGameManagerを作成してください。 ​ スクリプトの名前をGameManagerにするとアイコンが歯車になりますが、他のスクリプトと挙動は変わりません(アイコンが変わるのは恐らく仕様) GameManagerスクリプトに以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { [SerializeField] int Score = 0; // スコアを加算 public void AddScore(int score) { Score += score; } // スコアを取得 public int GetScore() { return Score; } void Start() { } void Update() { } } コードが書けたら保存して、Gameオブジェクトにアタッチしてください。 ​ 後は宝石に触れた瞬間にスコアを加算するだけです。 ​ Gemスクリプトの先ほど // (後で記述) とした部分を埋めて、スコアを加算できるようにしてみてください。 【ヒント】GameオブジェクトにはGameControllerタグが設定されています! 降参 or 答え合わせの方はこちら 実行して、宝石を取得すると設定したポイント分のスコアが加算されることを確認してみてください。今はUIがないのでインスペクターでの確認になります。 宝石は1種類でも構いませんが、今までの手順を踏むことで獲得ポイントが異なる宝石を作ることができます。Gemスクリプトはそのまま再利用できます。 ぜひ宝石の種類を増やしてみてください。 2-5 宝石を並べる 2-5 宝石を並べる 作った宝石をステージにたくさん並べましょう。 ​ 3Dアクションゲーム編のようにプレハブ化して1つずつ配置していってもよいのですが、100個も200個も宝石を置くとなると流石に大変です。2-1で地形のパーツをタイルパレットに並べて配置していきましたが、同じようにオブジェクトをタイルパレットにできる「GameObject Brush」という機能があります。これを使って宝石を配置していきましょう。 ​ ​ まずはPrefabフォルダを作成して、宝石を全てプレハブ化してください。 Tile Palletを開いて、「Create New Pallet」から新しいパレット「Gimmick」を作成してください。 パレットの作成先は当然Palletフォルダにしましょう。 Palletフォルダ内にGimmickパレットが追加されるので、ダブルクリックしてパレット編集画面を開いてください。 パレット内に先ほどプレハブ化した宝石をドラッグ&ドロップしていきましょう。 追加した宝石をLayer1の子オブジェクトにしてください。また、宝石の座標もマスの中央になるように調整しましょう。 パレットが完成したらシーンに戻りましょう。Scenesボタンを押すとパレット編集画面を閉じることができます。 ヒエラルキー内のGridを右クリックして「2D Object」→「Tilemap」→「Rectangular」から新しいタイルマップを作成してください。名前は「Gimmick」にしておきます。 Active Tilemap(設置対象のタイルマップ)をGimmickに変更します。その左下にある、使用するタイルパレットの種類もGimmickに変更してください。 下部にある「Default Brush」を「GameObject Brush」に変更してください。 ​ ​ ここまで設定できたら、シーン上に宝石を設置してみましょう。地形を設置した時と操作は変わりません。 Gimmickタイルマップの子オブジェクトを確認すると、宝石が作成されていることが確認できます。 GameObject Brushはタイルを設置するのではなく、グリッドに沿ってオブジェクトを作成するブラシになります。 作成されているのはプレハブなので、プロジェクト内のプレハブを編集すれば設置した宝石全てに変更を反映させることができます。 ​ ​ GameObject Brushを使って宝石を何個か配置してみてください。後でギミックも作成するため、ステージの最後まで宝石を設置する必要はありません。 2-6 槍を作成 2-6 槍を作成 触れるとゲームオーバーになる槍を作成しましょう。考え方は3Dアクションゲーム編でトゲを作った時と変わりません。 槍もGimmickパレットに追加して、GameObject Brushで設置できるようにします。 ​ ​ 「Sprite」→「Gimmick」内にある槍の画像をシーン上にドラッグ&ドロップしてください。 インスペクターから槍の設定をします。たくさんの項目を一気に設定するので、順番にやっていきましょう。 ​ ・Layerを「Ignore Raycast」にする ・Scaleを調整する ・Order in Layerを-1にして、地形より後ろに表示されるようにする ​・Box Collider 2Dを追加 → is Triggerにチェックを入れる → Sizeを調整する (宝石とは異なり、プレイヤーにマイナスになる当たり判定は小さめにした方が 遊びやすいゲームになります) 新しいスクリプト「DamageObject」を作成して、以下のように入力してください。青い部分 は穴埋めなので埋めてみてください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class DamageObject : MonoBehaviour { private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーが衝突したら… if (collision.CompareTag("Player")) { // PlayerMove内のGameOver関数を呼び出す! (ここに入力) } } } 降参 or 答え合わせの方はこちら コードが書けたら保存して、槍にDamageObjectスクリプトをアタッチしてください。 ​ ​ 実行して、プレイヤーが槍に衝突するとゲームオーバーになることを確認してみてください。 槍をプレハブ化して、シーン上にある槍は削除しておきましょう。Gimmickパレットへの追加は他のギミックを完成させてからまとめて行います。 2-7 矢を作成 2-7 矢を作成 飛んでくる矢を作成しましょう。発射用判定にプレイヤーが接触すると、矢の移動が開始します。 ​ 「Sprite」→「Gimmick」内にある矢の画像をシーン上にドラッグ&ドロップしてください。 インスペクターから矢の設定をします。槍と同じく一気に設定するので、順番に行ってください。 ​ ・Layerを「Ignore Raycast」にする ・Scaleを調整する(Xを負の数にすると左右反転します) ​・Box Collider 2Dを追加 → is Triggerにチェックを入れる → Sizeを調整する (槍と同じように小さめに調整 ) ​・DamageObjectスクリプトをアタッチ Arrowの子オブジェクトに空のゲームオブジェクトArrowStartを追加してください。これが矢の移動開始判定になります。 ArrowStartオブジェクトの設定をしてください。 ​ ・Layerを「Ignore Raycast」にする ・Positionを調整する(矢より大きく左に) ​・Box Collider 2Dを追加 → is Triggerにチェックを入れる → Sizeを調整する (Yを大きくして、ステージを縦断するようにする ) ArrowStartオブジェクトの当たり判定にプレイヤーが衝突したら、親オブジェクトである矢が移動を開始するようにしましょう。 Arrowスクリプトを作成して、以下のように入力してください。青い部分 は穴埋めになります。 これはプレイヤーが移動開始の指示を受けることで移動開始する、矢のスクリプトになります。 ​ ​【ヒント】もしプレイヤーのX座標がDeletePosX以下になったら、自身を削除しましょう! using System.Collections; using System.Collections.Generic; using UnityEngine; public class Arrow : MonoBehaviour { [SerializeField] float MoveSpeed = 5.0f; // 矢が削除される移動量 const float DeleteMoveX = 50.0f; // 矢が削除されるX座標 float DeletePosX = 0.0f; // 移動開始フラグ bool MoveStartFlag = false; void Update() { // 移動開始フラグが立っていたら移動する if (MoveStartFlag) { // 左へ向かって移動 transform.Translate(new Vector3(-MoveSpeed * Time.deltaTime, 0.0f, 0.0f)); } // 削除する座標に到達していたら自身を削除する (ここに入力) } // 移動開始関数 public void MoveStart() { // 移動開始フラグを立てる MoveStartFlag = true; // 削除されるX座標を決める DeletePosX = transform.position.x - DeleteMoveX; } } 降参 or 答え合わせの方はこちら コードが書けたら保存して、矢(Arrow)にArrowスクリプトをアタッチしてください。 ​ ​ 後はプレイヤーが衝突したタイミングでMoveStart関数を呼びだすだけになります。 ​ ArrowTriggerスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ArrowTrigger : MonoBehaviour { // 衝突した瞬間に呼ばれる private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーが衝突したら… if (collision.CompareTag("Player")) { // 親オブジェクト(矢)の移動を開始する transform.parent.GetComponent().MoveStart(); // 判定が終わったので自身を削除 Destroy(gameObject); } } } コードが書けたら保存して、矢の当たり判定(ArrowStart)にArrowTriggerスクリプトをアタッチしてください。 ​ ​ 実行して、矢が飛んでくるか確認してみてください。矢が飛んでくる速度が気に入らない人はArrowのMoveSpeedを調整しましょう。 ​ ​ ここまで実装できたら矢をプレハブ化して、ステージ上にある矢は削除してください。 2-8 磁石アイテムの作成 2-8 磁石アイテムの作成 触れるとしばらく宝石を引き寄せるようになる磁石アイテムを作成しましょう。編集するスクリプトは多いですが、コードはそこまで長くありません。 ​ ​ 「Sprite」→「Gimmick」内にあるアイテムの画像をシーン上にドラッグ&ドロップしてください。 インスペクターからアイテムの設定をしましょう。 ​ ・Layerを「Ignore Raycast」にする ・Scaleを調整する ​・Circle Collider 2Dを追加 → is Triggerにチェックを入れる → Radiusを調整する (プレイヤーにプラスになる効果なので大きめに) 宝石を引き寄せる効果を発動するために、プレイヤーの子オブジェクトに判定を追加しましょう。 ​ 「Sprite」→「Effect」内にあるMagnetの画像をシーン上にドラッグ&ドロップしてください。 ヒエラルキー内で追加した画像をユニティちゃんの子オブジェクトに設定してください。 インスペクターからMagnetの設定をしてください。 ​ ​・新しいタグ「Magnet」を作成して設定する (タグの追加方法は1-5を参照) ・Layerを「Ignore Raycast」にする ・PositionとScaleを調整する ​・Colorを選択して、Aの値を調整する 100→50ほどでOK ​ AはAlpha(不透明度)の値で、小さくなるほど透明になります Circle Collider 2Dを追加して、Is Triggerにチェックを入れてください。 Radiusを大きく調整しましょう。この判定に触れた宝石がプレイヤーに向かって移動するようになります。 このままでは少し寂しいので、エフェクトが回転するようにしましょう。スクリプトで実装してもよいですが、今回はアニメーションで実装します。 ​ ​ Animatonフォルダ内にEffectAnimatonフォルダを作成してください。 Animatonを作成して、名前をMagnetAnimationにしてください。 ​ MagnetAnimatonをヒエラルキー内のMagnetオブジェクトにドラッグ&ドロップしましょう。 同フォルダ内にAnimatorControllerが自動で作成されればOKです 。 MagnetAnimatonをダブルクリックしてAnimatonウィンドウを開いてください。 ​ ​ Magnetオブジェクトをクリックすると「Add Property」ボタンが押せるようになります。「Add Property」をクリックしてRotationの項目を追加してください。 1つ目のキーはZ=0 、2つ目のキーはZ=360に設定してください。これでオブジェクトが一回転するようになります。 ​ 2つ目のキーを左右に移動させることで回転速度を調整できます。お好みの速度に調整してください。 この状態で再生してみると、回転の最初と最後に減速がかかっていることが確認できます。デフォルトでは移動が滑らかになるように、Unity側が補正をかける仕様になっています。アニメーションが等速でループするようにしましょう。 ​ 左下の「Curves」ボタンを押してカーブ編集に切り替えてください。グラフを見ると最初と最後が減速するように補正されています。 ​ ​ グラフの右端の点を右クリックして、Left Tangentを「Linear」に変更してください。 左 端の点を右クリックして、Right Tangentを「Linear」に変更してください。 グラフが直線になればOKです。この状態で実行してみると、Magnetオブジェクトが等速で回転するようになっています。 MagnetAnimatonのLoopTimeにチェックが入っていなかった場合はチェックを入れておいてください。 オブジェクトの設定ができたので、スクリプトを書いていきましょう。 まずは宝石がプレイヤーへ向かって移動する処理を実装します。 ​ Gemスクリプトを開いて、赤い部分のコード を追加してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Gem : MonoBehaviour { [SerializeField,Tooltip("獲得できるポイント")] int Point = 0; [SerializeField] float MoveSpeed = 20.0f; // 引き寄せフラグ trueならプレイヤーへ向かって接近 bool m_magnet = false; // プレイヤーのゲームオブジェクトを保存できるようにして // 何回も取得しなくてもいいようにする GameObject m_player; void Update() { if (m_magnet) { // プレイヤーへ向かって移動する Vector3 move = m_player.transform.position - transform.position; move.Normalize(); move = move * MoveSpeed * Time.deltaTime; transform.Translate(move); } } private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーが衝突したら… if (collision.CompareTag("Player")) { // スコアを加算 GameObject.FindGameObjectWithTag("GameController").GetComponent().AddScore(Point); // 自身を削除する Destroy(gameObject); } // 磁力ゾーンが衝突したら… else if (collision.CompareTag("Magnet")) { if (m_magnet == false) { // プレイヤーを検索 m_player = GameObject.FindGameObjectWithTag("Player"); // プレイヤーへ向かって移動開始 m_magnet = true; } } } } 【プログラムの解説】 ・目標とする座標へ向かって移動するための計算方法はk2Engineを使っていた時と変わりません。 ① 目標地点 - 現在地点 を行い、現在地点から目標地点へ向かうベクトルを求める ​ ② 求めたベクトルを正規化(Normalize)して向きベクトルにする ​ ③ 向きベクトルに移動速度を乗算して移動量とする ​ ​・プレイヤーへの移動を開始した瞬間にプレイヤーを検索してメンバ変数に保存しておくことで、何度も取得する手間を省くことができます。 GameObject.FindGameObjectWithTag("Player") をUpdateで毎フレーム行っても構いません。ですが、小さな負荷が積み重なるとゲームが重くなるリスクがあるため、Update内でGameObjectの検索を毎フレーム行うのは控えた方が良いでしょう。 ​ ​ ここまで入力できたら保存して、実行してみてください。Magnetの当たり判定に触れた宝石がプレイヤーへ向かって移動することが確認できます。 ​ 移動速度が遅い場合は宝石のプレハブを選択してMoveSpeedを調整してください。同じコンポーネントを持つプレハブは、一括選択することでまとめて値を変更できます。 Unityにはオブジェクトをアクティブにしたり、非アクティブにしたりする機能があり、 オブジェクト名の左側にあるチェックボックスでオブジェクトのアクティブを切り替えることができます 。 ​ ​ Magnetオブジェクトを非アクティブにしましょう。 チェックが外れて非アクティブになったオブジェクトは透明になり、アタッチされているコンポーネントも全て無効になります。Updateなども全て止まります。 ​ 普段はMagnetオブジェクトを非アクティブにしておいて、アイテムを取得したらしばらくの間アクティブになるようにしましょう。 アクティブの状態もスクリプトから変更可能です。 非アクティブになっているオブジェクトはFindで取得できない ので注意してください。 ​ また、非アクティブにしてもオブジェクトは完全に消えたわけではないので、本当に不要になったオブジェクトは削除するようにしましょう。ゲームが重くなる原因になります。 Unity Tips! PlayerMoveでアイテムの取得状況を管理しても良いのですが、わかりにくくなってしまうので新しいスクリプトを作成しましょう。 ​ ​ Player I temスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerItem : MonoBehaviour { [SerializeField, Header("磁石アイテムの有効時間")] float MagnetLimit = 3.0f; [SerializeField, Header("磁石引き寄せ判定オブジェクト")] GameObject MagnetObject; // 磁石アイテムタイマー float m_magnetTimer = 0.0f; void Update() { // タイマーが0より大きい(磁石アイテム有効時間中) if (m_magnetTimer > 0.0f) { m_magnetTimer -= Time.deltaTime; // 効果時間が終了 if (m_magnetTimer <= 0.0f) { // 引き寄せ判定オブジェクトを非アクティブにする MagnetObject.SetActive(false); } } } // 磁石アイテムを取得した時に呼ぶ関数 public void GetMagnetItem() { // 取得したため引き寄せ判定用オブジェクトをアクティブにする MagnetObject.SetActive(true); // タイマーを設定 m_magnetTimer = MagnetLimit; } } 【プログラムの解説】 ・GameObjectクラスのSetActive関数は前述したオブジェクトのアクティブ、非アクティブを切り替える関数になります。 引数にtrueを入れるとアクティブに、falseを入れると非アクティブになります。 ​ ​ コードが書けたら保存して、ユニティちゃんにアタッチしてください。 ​ ユニティちゃんのインスペクター内に SerializeField のアトリビュートを設定した変数が表示されています。MagnetObject にはヒエラルキーから ​Magnetオブジェクトをドラッグ&ドロップしてください。 最後にプレイヤーがアイテムの当たり判定に衝突したら、先ほどのGetMagnetItem関数を呼び出してMagnetオブジェクトをアクティブにしましょう。 ​ ​ Magnet I temスクリプトを作成して、以下のように入力してください。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class MagnetItem : MonoBehaviour { private void OnTriggerEnter2D(Collider2D collision) { // プレイヤーと衝突したら… if (collision.CompareTag("Player")) { // 磁石アイテム取得関数を呼ぶ collision.GetComponent().GetMagnetItem(); // 自身を削除する Destroy(gameObject); } } } コードが書けたら保存して、磁石アイテムにアタッチしてください。 ​ ​ これでプレイヤーがアイテムを取得するとMagnetオブジェクトがアクティブになり、一定時間が経過すると非アクティブになります。 アイテムはこれで完成でも良いのですが、アイテムが背景に紛れないようにアニメーションを追加してみましょう。 ​ Animatonフォルダ内にItemAnimatonフォルダを追加してください。MagnetItemアニメーションを作成して、Itemオブジェクトにドラッグ&ドロップしましょう。 Itemオブジェクトを選択してAdd Propertyから好きな項目を追加して自由にアニメーションを作ってみてください。 ​ Positionを操作する場合は相対移動になるように、AnimatorコンポーネントのApply Root Motionにチェックを入れてください(2-4参照) サンプルゲームでは一定間隔でアイテムの画像を拡大・縮小しています。 アニメーションが完成したら、ItemオブジェクトをPrefabフォルダ 内にドラッグ&ドロップしてプレハブ化しましょう。 ​ プレハブ化できたらシーン内にあるオブジェクトは削除してください。 これでこのLessonで実装するギミックは終了ですが、今までの知識を応用してぜひオリジナルのギミックも作ってみてください。 2-9 Gimmickパレットの設定 2-9 Gimmickパレットの設定 今まで作ったギミックである槍、矢、磁石アイテムをGimmickパレットに設定してGameObject Brushで設置できるようにしましょう。 ​ もし他に作成したギミックがあるなら同じ手順で設定してください。 ​ ​ Palletフォルダ内にあるGimmickパレットをダブルクリックして開いてください。 ​ ​ Gimmickパレット内に今まで作ったギミックのプレハブをドラッグ&ドロップしましょう。 追加したギミックをLayer1の子オブジェクトにしてください。 ​ ギミックの座標もグリッドの中央になるように調整しましょう。 Gimmickパレットの設定ができたらScenesボタンを押してステージの画面に戻ってください。 ​ ​ Active TileMap(設置対象のタイルマップ)とその下の使用するタイルパレットを「Gimmick」に変更してください。 ​ 下部の使用するブラシが「Default Brush」になっていたら「GameObject Brush」に変更してください。 ​ ​ これでギミックを設置できるようになりました。設置したいギミックを選択して、タイルマップ内に設置してみましょう。 2-10 下から登れる足場 2-10 下から登れる足場 最後におまけとして「下からはジャンプで登れるけど、上からは落ちない足場」を実装してみましょう。 ​ 一見難しそうに見えるかもしれませんが、Unityでは「Effector 2D」を活用することで簡単に実装できます。 ​ まずは下から登れる足場専用のTileMapを追加しましょう。 ​ ヒエラルキー内のGridを右クリックして「2D Object」→「Tilemap」→「Rectangular」から新しいタイルマップを作成してください。名前はStage_Backにしておきます。 設置対象のTileMapをStage_Backに、使用するPalletをStageに変更しましょう。 下部の使用ブラシもDeleteBrushに変更してください。 ​ ​ パレットの右上に下から登れそうな足場があるので設置してみましょう。 タイルマップを選択してTagを「Ground」、Layerを「Ignore Raycast」に変更してください。 ​ 「Tilemap Collider 2D」 ​を追加して当たり判定を設定してください。 Tilemap Collider 2D の「 Used By Effector」にチェックを入れてください。 ​ Add Componentから「Platform Effector 2D」を追加してください。これは当たり判定が指定した角度外からの衝突は無視するようになるコンポーネントです。 ​ Surface Arcの値を変更すると角度を調整できます。デフォルトで は180になっていますが、100あたりに調整してください。 これで下から登れる足場の完成です。 足場の当たり判定の形はデフォルトで大丈夫なはずですが、気に入らない場合はCustom Physics Shape機能を使って手動で当たり判定を調整してください。 ​ ​ またレイヤーをIgnore Raycastに変更しているため、坂道判定が無効になっています。現在の仕様では下から登れる足場に坂道は使用できないので注意してください。 2-11 ステージの作成 2-11 ステージの作成 今までのLessonを参考にしながら、タイルマップを使ってステージを一通り作ってみてください。 ・ステージ1なのであまり難しくしすぎないようにしましょう ・宝石を使ってプレイヤーを誘導するようにすると遊びやすくなります 坂や崖に当たり判定の端が衝突した場合、プレイヤーが左右へ大きく吹っ飛ぶ現象が起こることがあります。 ​ 気になる方はPlayarMoveスクリプトのFixedUpdate関数内に、X軸への力を0にする以下の 処理を追加してください。FixedUpdate関数内であればどこでもOKです。 Unity Tips! ステージが完成したらLesson2は終了です。 ​ ​ 次のレッスンではゲームクリア、ゲームオーバーといった「ゲームのルール」を一通り実装していきましょう。 ​ 【評価テスト】 https://forms.gle/RUu4tty5fAcx5WC86 評価テスト Next Lesson3「ルールを作ろう」 ページ TOP 2-1 タイルマップ 2-2 地形を装飾 2-3 背景を設定 2-4 宝石を作成 2-5 宝石を並べる 2-6 槍を作成 2-7 矢を作成 2-8 磁石アイテムの作成 2-9 Gimmickパレットの設定 2-10 下から登れる足場 2-11 ステージの作成 評価テスト

河原電子ビジネス専門学校
​ゲームクリエイター科

bottom of page