UIを作ろう のバックアップ(No.8)


はじめに

アイテムを設置しようの続きになります。
今回のテーマはUI(User Interface)の追加です。

 

UIとは、&color(,#FBB){プレイヤー(=User)とゲームとをつなぐ箇所(=Interface)という意味}で、
ここでは、その仕組みのことを指します。
マップやガイド、スコア表示、体力ゲージ、ボタン、メッセージなどがこれに当たります。

 

このゲームでは、

・ゲームシーンでの現在のタイムスコア(経過時間)のテキスト
・スコアシーンでのリスタートボタン
・スコアシーンでの最終的なタイムスコアのテキスト

を作っていきます。
一つずつ実装していきましょう。

 

タイムスコア(ゲームシーン)の設置

まずは、ゲームシーンでのタイムスコアのテキストを表示してみましょう。

 

UIの設置は、ほかのGameObjectと同様にヒエラルキーを右クリックしてできます。
UI>Textを選択して名前を「Timer」とします。
これで、ヒエラルキーに CanvasEventSytem が追加されて、
Canvas の中に「Timer」という名前でテキストUIができます。
画面のどこかに(おそらく画面中央)に「NewText」と文字が出てきてるかと思います。

 

Canvas とは、UI を描画するための(抽象的な)領域です。
一般的なオブジェクトと、UI オブジェクトは別々の領域で描画されます。

 

EventSystem とは、

 

次は、テキストUIの大きさや位置を調整します。
現在、このゲームのカメラは解像度が未固定で、ゲームのウィンドウの大きさに自動で調整されるようになっています。
しかし、キャンバスの初期状態では、UIはpixel固定であるためゲーム画面の大きさで表記に乱れが出てしまいます。
そこで、 Canvas のインスペクタから CanvasScaler > UIScaleMode を ScaleWithScreensize に設定を変更してあげることで、
カメラと同じようにウィンドウサイズに合わせてUIの大きさが変わるようにできます。
そして、「Timer」のインスペクタのRestTranformから( posX 、posY 、Width 、height )、Textから( FontSize 、Paragraph > Aligment )を変更します。
それぞれ「キャンパスに対する相対位置(x座標、y座標)」「UIの幅、高さ」「文字の大きさ、UI内の位置」に当たります。
見やすい位置、大きさになるように調整してみましょう。

次に、経過時間を出力する機能をスクリプトで書いていきます。
「Text」のインスペクタから AddCompornent > NewScript を押して「Timer」という名前でスクリプトを追加します。

 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI; // UI関連の名前空間
  5.  
  6. public class Timer : MonoBehaviour
  7. {
  8.     private Text Text = null; // 「Text」コンポーネントを定義
  9.  
  10.     // Start is called before the first frame update
  11.     void Start()
  12.     {
  13.         Text = this.GetComponent<Text>(); // 「Text」コンポーネントを取得
  14.     }
  15.  
  16.     // Update is called once per frame
  17.     void Update()
  18.     {
  19.         float totalTime = Time.time; // ゲーム開始からの経過時間
  20.         Text.text = totalTime.ToString() // 「Text」コンポーネントの「text」を変更
  21.     }
  22. }
 

名前空間として、UnityEngine.UI を追加しています。
UnityEngine.UI はUI関連を扱う名前空間で、UI関連の処理を行うときに使われます
Update関数の中で出てきた Time.time というのは、ゲーム(シーン)開始からの時間を扱う変数です。
これを定義・取得した Text コンポーネントの text 変数に代入することで、現在の経過時間を出力できます。(Text コンポーネントは UnityEngine.UIの名前空間を用いることで使えるようになります)
ただ、totalTime は float 型なのに対して、Text.text は string 型しか指定できません。
そこで、toString() メゾットという、数値型を string 型に変更するメゾットを用いて、totalTime を string 型に変えます。

 

次に、出力のフォーマットを「○○:○○(分:秒)」形になるように改良してみます。

 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI; 
  5.  
  6. public class Timer : MonoBehaviour
  7. {
  8. //略
  9.     void Update()
  10.     {
  11.         int minutes = (int)(time.time / 60); //「分」を取得
  12.         int seconds = (int)(Time.time % 60); //「秒」を取得
  13.         Text.text = minutes.ToString("D2") + ":" + seconds.ToString("D2"); // それぞれ10進数2桁にして出力
  14.     }
  15. }
 

まず経過時間をまとめて「totalTime」で取得していたものを分と秒でそれぞれ分けて取得します。
分は総経過時間を60で割ったもの、秒は60で割った余りになるので、それを代入しましょう。
最後に、書式設定をします。
toString() メゾットは、引数に表記のフォーマットを指定することで簡単に書式設定が可能です。
「D2(D=>10進数表記、2=>2桁、足りない部分は0埋め)」に設定すれば完成です。

 

リスタートボタンの設置

次は、リスタートボタンの設置をします。
まず「Score」シーンに移動します。(アセット>Scene>Scoreを押すとシーンが変わります)
真っ青な画面になって、ヒエラルキーを見てみるとカメラしかない状態になったと思います。
オブジェクトはシーンごとに管理されるので、別のシーンに移るとこれまで作成したオブジェクトはなくなります。
カメラはどのシーンにも作成時にデフォルトでできるのでカメラだけのシーンになったわけです。
そのため、これまで指定してきたカメラ、アスペクト比の設定等がこのシーンでは設定されていないことになってしまいます。
ということで、最初にシーンの各設定を行います。
それぞれ以下のようにします。

シーンウィンドウの解像度設定を「16:9Aspect」にする
カメラのインスペクタからCamera>Projection>orthographicにする(このシーンはUIしか置かないのでしなくてもよい)
 

そうしたら、ボタンUIを追加します。
テキストUIと同様にヒエラルキーを右クリックして、UI > Button を押して名前を「Restart」とします。
忘れずに「Canvas」のインスペクタから CanvasScaler > UIScaleMode を ScaleWithScreenSize に設定しましょう。
次に、ボタンとその中の文字の位置、大きさを調節します。
ボタンの位置、大きさは、ボタンのインスペクタ、 RestComponet の position、 width、 height から、
文字の位置、大きさは、ボタンの子要素にあるテキストオブジェクトのインスペクタ、 Text の FontSize 、 Paragraph から調節します。
文字の内容は、同じくテキストオブジェクトのインスペクタ、 Text の Text 要素を書き換えれば指定できます。

 

最後に、ボタンが押された時の動作を作ります。
まず、「任意のタイミングでシーンの移動をしてくれる」オブジェクトを作ります。
ヒエラルキーを右クリックして CreateEmpty から「MoveScene」という空のオブジェクトを作り、
このオブジェクトにインスペクタから AddComponent > NewScript から「MoveScene.cs」という名前でスクリプトを作ってください。

 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.SceneManagement; // シーン関連の名前空間を追加
  5.  
  6. public class MoveScene : MonoBehaviour
  7. {
  8.     //「OnClick」という新しい関数を定義 ボタンが取得できるように「Public」を指定
  9.     public void OnClick()
  10.     {
  11.         SceneManager.LoadScene("SampleScene");
  12.     }
  13. }
 

このオブジェクトは任意のタイミングでシーンの移動を行いたいため、Start関数やUpdate関数は使いません。
代わりに新しく「OnClick」という名前の関数を定義します。これで「OnClick() 関数を呼び出したとき」シーンの移動が行われるオブジェクトが出来上がります。

 

では最後に、先ほど作ったボタンが押されたら、この「MoveScene」オブジェクトのOnClick関数を呼び出すようにしましょう。
ボタンのインスペクタから Button > OnClick()(Noneとある所)に、「MoveScene」オブジェクトをドラッグ&ドロップして、
NoFunction > MoveScene > OnClick() を押します。
これで、ボタンを押したら「MoveScene」のOnClick関数が呼び出さる。つまり、シーンが移動する、という機能が出来上がりました。

 

タイムスコア(スコアシーン)の設置

最後に、スコアシーンにゴールした時の時間、タイムスコアを表示でいるようにします。
一見簡単そうですが、実は意外と複雑なことをする必要があります。
というのも、少し前にも言いましたがゲームオブジェクトはシーンごとに別々に管理されています。
時間を集計するスクリプトもゲームオブジェクトのコンポーネントとして動作しているため、
シーンを移動するときにその記録がオブジェクトと一緒に消されてしまいます。
そこで、シーンからシーンにデータを引き継ぐ際は、

1.引き継ぎたいデータをもつオブジェクトをシーンの移動の際に壊さないように設定する
2.引き継ぎたいデータをオブジェクトに依存しない形に保存して、移動後に読み取る

などがあります。(他にもいくつか方法はありますがこの2がスタンダード?)
1.は直感的にできて作りやすいですが、シーンの移動が多くなるとオブジェクトが溜まっていき処理が重くなっていってしまいます。
それに対して2.は難しさを感じますが、覚えれば非常に簡潔にかつ汎用的に使うことができます。
もちろん2.にもデメリットはありますが、基本的にこの方法を使っていくといいと思います。
ということで、今回も2.を使っていきます。

 

実装の流れは、

・ゴール直後、シーン移動直前にスコアの保存を行う
・シーン移動後スコアを受け取る
・スコアをテキストUIとして出力する

となります。

 

スコアの計測は「Timer」オブジェクト、ゴール判定は「Goal」オブジェクトと、それぞれ別オブジェクトで処理しているため、
まず、スコアを保存する前準備として、「Goal」オブジェクトから「Timer」オブジェクトのコンポーネントを取得できるように編集します。

 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI; 
  5.  
  6. public class Timer : MonoBehaviour
  7. {
  8.     //タイム(○○:○○表記)の文字列を入れる変数を定義
  9.     //「Goal」オブジェクトで受け取れるように「public」指定
  10.     public String timeScore = null; 
  11.  
  12. // 略
  13.  
  14.     void Update()
  15.     {
  16.         int minutes = (int)(time.time / 60);
  17.         int seconds = (int)(Time.time % 60);
  18.  
  19.         timeScore = minutes.ToString("D2") + ":" + seconds.ToString("D2"); // 毎フレームでスコア更新
  20.         Text.text = timeScore;
  21.     }
  22. }
 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.SceneManagement; 
  5.  
  6. public class Goal : MonoBehaviour
  7. {
  8.     private Timer timer = null; //「Timer」コンポーネントの変数を定義
  9.  
  10.     void Start()
  11.     {
  12.         timer = GameObject.Find("Timer").GetComponent<Timer>(); //「Timer」コンポーネントの変数を取得
  13.     }
  14. // 略
  15. }
 

「Timer.cs」でタイムスコアを変数として毎フレーム保存し、「Goal.cs」で「Timer」コンポーネントを取得して、
「Goal」オブジェクトでタイムスコアを取得できるようになりました。

 

次は、そのスコアをゲームオブジェクトに依存しない値として保存するスクリプトを作っていきます。
これまではゲームオブジェクトにコンポーネントとしてスクリプトを書いてきましたが、「ゲームオブジェクトに依存しない」ということで、
独立したスクリプトを書いていきます。
アセットを右クリックして、Create > C#Script を押し、「Data」としてスクリプトを作ります。

 
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. public static class Data // 「:MonoBehaver」を消して、「static」を追加
  6. {
  7. public static string score = null;
  8. }
 

今まで気にすることのなかった MonoBehaver ですが、
ここではクラスの継承といわれることをしていました。
クラスの継承とは、あるクラスから性質を受け継いだ新しいクラスを作ることです。
Start() 関数の呼び出しをする必要がなかったり、スクリプトをオブジェクトにコンポーネントとして付与できていたのは、実はこの MonoBehaver''クラスのおかげでした。
しかし、前述の通り今回のコードはゲームオブジェクトに依存しない独立したものになるので不要というわけです。
代わりにつけた ''static" ですが、