今回はUnityでシンプルな最速タイムを競うゲームの作成と、ゲームオーバー時にゲームの最初からリスタートする機能を作成してみたいと思います。
今回の機能を作成すると以下のような感じのものが出来上がります。
シンプルな最高タイムを競うゲームですが、色々な障害物と動きを付ければ楽しめるものが出来上がるかもしれません。
ゲームの舞台の作成
まずはゲームの舞台を作成していきたいと思います。
床と壁の作成
ヒエラルキー上で右クリックから3D Object→Cubeを選択し、名前をFloorとします。
インスペクタのTransformのPositionのXとYを0、Zを20、ScaleのXを10、Yを1、Zを50とします。
このFloorをキャラクターが移動出来る床とします。
次に床の端にキャラクターが移動すると無限空間に落ちていってしまうので透明な壁を作成して落ちないようにします。
無限空間内にコライダを設置してキャラクターを検知し、落ちたらゲームオーバーにするという風にも出来ますが、今回は壁を作って落ちないようにしています。
ヒエラルキー上で右クリックから3D Object→Cubeを3つ作成し、名前をWallとします。
3つのWallのTransformを変更し、キャラクターのスタート地点の手前、床の両端に配置し、キャラクターが床から落ちないようにします。
それぞれのWallのTransformは以下のように設定しました。
実際の壁は以下のようになります。
壁がこのままあると圧迫感があり邪魔なので、3つのWallのインスペクタそれぞれのMesh Rendererコンポーネントの横のチェックを外し、壁を透明にします。
これで壁が出来ました。
ゴール付近には壁を設置しませんでしたが、これはゴールに到達したらキャラクターをその場で止める為、必要ないからです。
ゴール領域の作成
次はゴールと判定する領域を作成します。
ヒエラルキー上で右クリックから3D Object→Cubeを選択し、名前をGoalAreaとします。
GoalAreaのTransformのScaleを変更し、ゴールと判定する領域のサイズに変更します。
ゴール付近の壁を作らなかったので、無限空間に落ちないようにキャラクターが到達できる範囲をカバーするようにします。
このままだとGoalAreaはただの衝突する壁となるので、インスペクタのBox ColliderコンポーネントのIs Triggerにチェックを入れ、衝突しないようにし、キャラクターを検知する範囲として使用します。
ゴール領域のマテリアルの作成
GoalAreaは衝突しない空間になりましたが、見た目がただの壁のように見えます。
そこでGoalAreaゲームオブジェクト用のマテリアルを作成し設定することにします。
Assetsフォルダ内で右クリックからCreate→Materialを選択し、名前をGoalAreaとします。
GoalAreaマテリアルのインスペクタでAlbedo横のColorを押し、Bを255、Aを100とし透明な青色にします。
GoalAreaゲームオブジェクトのMesh RendererコンポーネントのMaterialsのElement0にGoalAreaマテリアルをドラッグ&ドロップします。
GoalAreaは以下のような感じになりました。
障害物と操作キャラクターの配置
障害物と操作キャラクターの配置をしていきます。
ヒエラルキー上で右クリックから3D Object→Cubeを選択し、名前をObstacleとします。
ObstacleのTransformのPositionやScaleを変更し、Obstacleを選択した状態でCtrl+Dキーを押して複製し、複数の障害物を置きます。
各ObstacleのBox ColliderのIs Triggerにチェックを入れ、物理的に当たらないようにします。
次に操作キャラクターを配置します。
操作キャラクターはスタンダードアセットのThirdPersonControllerを使用します。
Assets/Standard Assets/Characters/ThirdPersonCharacter/Prefabs/ThirdPersonControllerをヒエラルキー上にドラッグ&ドロップします。
キャラクターのスタート位置は空中からスタートするようにしておきます(登場感を演出する為)。
ヒエラルキー上のThirdPersonControllerを選択し、インスペクタのTagをPlayerに変更しておきます。
後でこのタグを使ってプレイヤーを検知します。
現在の時間と最高タイムを表示するUIの作成
現在の時間と最高タイムを表示するUIを作成します。
ヒエラルキー上で右クリックからUI→Canvasを選択し、名前をTimerとします。
Canvas ScalerのUI Scale ModeをScale With Screen Sizeに変更し、ゲーム画面のサイズに合わせてUIのサイズを自動調整します。
Timerを選択した状態で右クリックからUI→Textを選択し、名前をCurrentTimeText、もうひとつTextを作成し、名前をFastestTimeTextとします。
TextコンポーネントのParagraphのAlignmentを変更します。
CurrentTimeTextは左上に表示し、
FastestTimeTextは右上に表示します。
ゲーム用メッセージアニメーションの作成
ゲームのスタート時とゴール時に表示するテキストのアニメーションを作成します。
ヒエラルキー上で右クリックからUI→Canvasを選択し、名前をMessageTextとします。
MessageTextのインスペクタのCanvas ScaleコンポーネントのUI Scale ModeをScale With Screen Sizeとします。
MessageTextを選択した状態で右クリックからUI→Textを選択します。
Textを選択し、インスペクタのAdd ComponentからUI→Effects→Outlineを取り付けます。
Rect TransformのAnchor Presetsでstretch stretchを選択し、MessageTextのサイズに合わせてTextが伸縮するようにします。
TextコンポーネントのFont Size、Color、Alignmentを設定します。
ヒエラルキー上のMessageText子要素のTextを選択した状態でAnimationタブのCreateボタンを押し、TextAnimという名前で保存します(AnimationウインドウはUnityメニューのWindow→Animation→Animationを選択)。
TextゲームオブジェクトにはAnimatorコンポーネント、Textアニメーターコンポーネントが自動で設定されます。
Animationウインドウで赤い丸の録画ボタンを押し、テキストのアニメーションを作成します。
0:00フレームでOutlineのEffect Distanceの値を調整します。
以下のような感じになります。
次に1:00フレームに移動し、OutlineのEffect ColorをTextのColorに近い色にし、Effect Distanceを調整します。
次に2:00フレームに移動し、Rect TransformのTopとBottomを変更し、TextのColorのAを0にします。
この時に確実にTextのColorのAを0にして完全に透明になるように設定する必要があります(後からスクリプトで判定する為)。
AssetsフォルダのTextAnimを選択し、インスペクタのLoop Timeのチェックを外しアニメーションがループしないようにします。
MessageTextのアニメーションは以下のようになりました。
ここまで出来たらMessageTextゲームオブジェクトをAssetsフォルダにドラッグ&ドロップしてプレハブにします。
ヒエラルキー上のMessageTextゲームオブジェクトは削除します。
スクリプトの作成
ゲームの舞台が出来たので後はスクリプトでゲームの管理、時間の計測、障害物の移動、ゴール判定等を行います。
ゲームの管理スクリプト
ゲーム全体を管理するスクリプトGameManagerスクリプトを作成し、MainCameraゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class GameManager : MonoBehaviour { // スタート用テキストプレハブ [SerializeField] private GameObject textPrefab; [SerializeField] private TimeManager timeManager; private bool finished; // Start is called before the first frame update void Start() { InstantiateMessage("ゲームスタート"); } public void Goal() { finished = true; // ゴール用テキスト表示 InstantiateMessage("ゴール!!"); // 最高タイムの更新 timeManager.UpdateFastestTime(); StartCoroutine(ReStart()); } // ゲームを終了したかどうか public bool IsFinished() { return finished; } // ゲームのリスタート IEnumerator ReStart() { // 3秒後にリスタート yield return new WaitForSeconds(3f); // 現在のシーンを再読み込み SceneManager.LoadScene(SceneManager.GetActiveScene().name); } // ゲーム内メッセージを表示 public void InstantiateMessage(string message) { var ins = Instantiate<GameObject>(textPrefab); ins.GetComponentInChildren<Text>().text = message; } } |
textPrefabにはMessageTextプレハブを設定します。
timeManagerには後で作成するTimeManagerスクリプトを設定します。
finishedはゲームが終了したかどうかのフラグです。
StartメソッドではInstantiateMessageメソッドに「ゲームスタート」という文字列を渡して呼び出します。
Goalメソッドではfinishedをtrueにし、InstantiateMessageメソッドに「ゴール!!」という文字列を渡して呼び出します。
その後に最高タイムを更新したかどうかを判定し設定するTimeManagerスクリプトのUpdateFastestTimeメソッドを呼び出します。
その後コルーチンを使ってReStartメソッドを呼び出しリスタート処理を行います。
IsFinishedメソッドは単純にisFinishedを返すだけのメソッドです。
ReStartメソッドは3秒待機した後にSceneManagerのLoadSceneメソッドを使って現在のシーンを再読み込みします。
コルーチンに関しては以下を参照してください。
InstantiateMessageメソッドではtextPrefabをインスタンス化し、子要素のTextを引数で受け取った文字列に変更します。
こうすることでテキストのアニメーションはそのまま使い、ゲームスタート時とゴール時の文字列だけを変えることが出来ます。
タイム管理スクリプト
時間に関する管理を行うTimeManagerスクリプトを作成しTimerゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class TimeManager : MonoBehaviour { [SerializeField] private GameManager gameManager; private float currentTime; private Text currentTimeText; private Text fastestTimeText; private static float fastestTime = 999.999f; // Start is called before the first frame update void Start() { currentTimeText = transform.Find("CurrentTimeText").GetComponent<Text>(); // 最速タイムを表示 fastestTimeText = transform.Find("FastestTimeText").GetComponent<Text>(); fastestTimeText.text = fastestTime.ToString("0.000"); } // Update is called once per frame void Update() { // ゴールしていなければ時間を計測 if (!gameManager.IsFinished()) { currentTime += Time.deltaTime; currentTimeText.text = currentTime.ToString("0.000"); } } // 最高タイムの更新 public void UpdateFastestTime() { if (currentTime < fastestTime) { fastestTime = currentTime; } } } |
gameManagerにはMainCameraに取り付けたGameManagerスクリプトを設定します。
currentTimeは現在の時間を入れます。
currentTimeTextは現在の時間を入れておくテキストです。
fastestTimeTextは最高タイムを入れておくテキストです。
fastestTimeは最高タイムを入れておきますが、staticを付けて静的なフィールドとし、シーン間の移動をしても保持出来るようにしています。
Startメソッドではテキスト群の取得と最高タイムをテキストに表示する処理をしています。
1 2 3 | fastestTime.ToString("0.000"); |
0.000は時間の表示を小数点以下3桁まで表示する為の書式設定です。
UpdateFastestTimeメソッドでは現在の時間が最高タイムより小さかった時に最高タイムを更新します。
メッセージテキストの消去スクリプト
MessageTextはGameManagerスクリプトでインスタンス化していますが、それを消去する処理は記述していません。
そこでMessageTextプレハブ事態にDeleteMessageTextスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class DeleteMessageText : MonoBehaviour { private Text text; // Start is called before the first frame update void Start() { text = GetComponentInChildren<Text>(); } // Update is called once per frame void Update() { // テキストのアルファ値が0以下になったらルートのゲームオブジェクトを削除 if(text.color.a <= 0f) { Destroy(transform.root.gameObject); } } } |
StartメソッドでMessageTextの子要素のTextコンポーネントを取得します。
UpdateメソッドでtextのColorのAが0以下かどうか判定し、0以下であればDestroyを使って自身のルートのゲームオブジェクト(MessageText)を削除します。
障害物の移動・衝突判定スクリプト
障害物を移動、キャラクターとの衝突判定をするスクリプトObstacleScriptを作成し、各Obstacleゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class ObstacleScript : MonoBehaviour { [SerializeField] private float moveSpeedX = 1f; [SerializeField] private float moveDistanceX = 1f; [SerializeField] private float moveSpeedY = 0f; [SerializeField] private float moveDistanceY = 0f; [SerializeField] private float moveSpeedZ = 0f; [SerializeField] private float moveDistanceZ = 0f; private Vector3 initPos; // Start is called before the first frame update void Start() { initPos = transform.localPosition; } // Update is called once per frame void Update() { transform.localPosition = initPos + new Vector3( Mathf.Sin(moveSpeedX * Time.time) * moveDistanceX, Mathf.Sin(moveSpeedY * Time.time) * moveDistanceY, Mathf.Sin(moveSpeedZ * Time.time) * moveDistanceZ ); } // 自身(障害物)と接触したのがPlayerタグを持つコライダである private void OnTriggerEnter(Collider other) { if(other.tag == "Player") { // 現在のシーンを再読み込み SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } } |
XYZ方向それぞれに移動スピードと移動距離を設定出来るようにします。
initPosはゲームオブジェクトの最初の位置を入れます。
StartメソッドでinitPosにスクリプトを取り付けたゲームオブジェクトのローカル位置を入れます。
Updateメソッドではゲームオブジェクトのローカル位置に初期位置+Mathf.Sinで計算した位置を入れます。
Mathf.Sinは引数に値を入れると-1~1の値が返ってくるので、引数にTime.time(実行開始からの時間)に各moveSpeedを掛けて与えます。
各moveSpeedをTime.timeにかける事で引数に与える数値の値が大きくなるのでMathf.Sinで得られる値の変化率が大きくなります。
Mathf.Sinで得られた値-1から1の値に各moveDistanceを掛ける事で-3から3の値等、得られる値が大きくなります。
各moveDistanceを0にすることでその方向には移動しないようになります。
OnTriggerEnterメソッドはこのスクリプトを取り付けたゲームオブジェクトのコライダが他のコライダと接触した時に呼ばれるので、その時にPlayerタグを持つコライダであれば障害物(Obstacle)と衝突したとし、SceneManagerのLoadSceneを使って現在のシーンを再読み込みして最初からスタートさせます。
ゴール判定スクリプト
キャラクターがGoalAreaの領域に来たらゴールしたと判定する為、GoalAreaにGoalスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityStandardAssets.Characters.ThirdPerson; public class Goal : MonoBehaviour { [SerializeField] private GameManager gameManager; void OnTriggerEnter(Collider col) { if (col.tag == "Player") { // ゲームマネジャーにゴールしたことを知らせる gameManager.Goal(); // キャラクターの動作を止める col.GetComponent<ThirdPersonUserControl>().enabled = false; col.GetComponent<ThirdPersonCharacter>().enabled = false; col.GetComponent<Rigidbody>().velocity = Vector3.zero; col.GetComponent<Animator>().SetFloat("Forward", 0f); } } } |
gameManagerにはMainCameraに取り付けたGameManagerスクリプトを設定します。
OnTriggerEnterメソッドでGameAreaのコライダが他のコライダと接触した時の処理を行います。
Playerタグを持つコライダであればコライダのコンポーネントのThirdPersonUserControlのenabledをfalseにし操作出来ないようにします。
ThirdPersonCharacterのenabledをfalseにし移動処理を行わないようにします。
RigidbodyのvelocityをVector3.zeroでXYZを0にし、Rigidbodyの速度を0にします。
AnimatorのSetFloatでForwardアニメーションパラメータを0にしアニメーションを止めます。
カメラがキャラクターに追従するスクリプト
カメラがキャラクターを追従するようにMainCameraのインスペクタのAdd ComponentからScripts→UnityStandardAssets・Utility→Follow Targetを取り付け、TargetにThirdPersonControllerを設定します。
これでキャラクターの移動に合わせてカメラも移動します。
これで全ての機能が出来ました。
終わりに
今回はシンプルなタイムアタックゲームを作成してみました。
やりたかった事は単純にゲームオーバーになったら最初からやり直す機能を作成する。ということだけだったんですが、わたくしの悪い癖で他の要素も取り付けてしまいました・・・(´Д`)