3DCG覚書とかとか

参考書のすみっこ

~普段メモとかしても忘れちゃうのでブログにしました~

目指せUnityマスター【UI基本編】

Q.何この記事?
A.新卒Webエンジニアが、Unityマスターになるまでの過程を描いていく記事。
1カ月続きそう( ´_ゝ`)

今回の目玉は、UnityでのUI(ユーザーインターフェース)全般かな。

今週やったこと

Create with Code

learn.unity.com

先週と同じ、今回はUnit5だけ。

Unit5 - User Interface

名前の通り、UIについて学習していく。

ゴール

・タイトル画面
・難易度選択
・スコア表示
・リスタート
大きく分けてこの4つが実装できるようになること。そのほかには、マウスクリックとかまだ使ってない標準メソッドを使ったりする。

Lesson5.1 Clicky Mouse

ここでは今までに学習したことから、基本的なゲームプレイ用の機能を実装していく。
あとメソッドのリファクタリングとかもするから参考になるかも。

この辺で気になったんだけど、変数名をpublicで指定してInspectorに直で値を入力したり、オブジェクトを追加したりするのってどうなのかなと思った。だから結構privateとGameObject.Findとかの組み合わせで初期化してたんだよね。
どうなのかっていうのは、C#とかオブジェクト指向において変数って基本的にprivateで表すもののはずだし、外部から取得する必要があったとしてもゲッターとかセッター(C#だとプロパティ)で表すもんでしょって思ってるから。
じゃないと、「外部からの不正アクセス」とか「共同開発時のヒューマンエラー」につながってしまう。
それにちゃんとブラックボックスにしておかないと、あとから処理の修正とかする時に絶対ゴチャゴチャになっちゃうしね。

それで調べてみると、

unity-michi.com

ここの記事が参考になりそうだった。
まず原則として、
・変数は関数やプロパティで公開する。
・処理はできるだけクラス内に収める。

これを守っておく必要がある。
そのうえでInspectorから変数をいじりたい時は、どうやら[SerializeField]なるものがあるらしく、

[SerializeField]
private int score;

こんな感じに書けばprivateなフィールド変数をInspectorに表示させることができるらしい。
まだ実装中でどのパーティクル使おうかとか、どのオブジェクト使おうかとか更新が多いGameObjectとか配置するときは、これを使ってみるといいのかもしれない。

まぁ原則privateでフィールドを宣言するってことがわかってれば良さそう。あとは基本設計の段階でモデルをちゃんと設計しておけば、ファイルがゴチャゴチャしたり管理しづらくなったりはしなそうだから、そっちの勉強もしていこう。

Lesson5.2 Keeping Score

ここではUIオブジェクトを使って、画面上にScore用のテキストを表示させる。

UIを使うにはまずヒエラルキーでCreate > UI > TextMeshPro textを選ぶとポップアップが出るから、”Import TMP Essentials"をクリックする。すると自動的にCanvasとその子オブジェクトにText(TMPオブジェクト)が生成される。

あとは今まで通りスクリプトでこれを取得しながら使っていけばいい。
具体的には、

using TMPro;

...

private TextMeshPro scoreText;

private int score;
public int Score{
    get{return score;}
    set{
        score = value;
        scoreText.text = "Score: "+score;
    }
}

void Start(){
    //最初は非アクティブになってることが多いので、GameObject.FInd()で直接探そうとすると取得できない。
    scoreText = GameObject.Find("Canvas").transform.Find("ScoreText").gameObject.GetComponent<TextMeshPro>();
}

public void UpdateScore(int score){
    Score += score;
}

こんな感じにして、UpdateScoreを公開して外部から呼ぶ形になるかな。

Lesson5.3 Game Over

ここでは、
1.ゲームの遷移によって表示するUIやisGameActiveなどの変数を変える。
2.現在のシーンをリスタートする。
これらの方法を学習した。

1.ゲームの遷移によって表示するUIなどを変える。

まず今回のゲームの流れは、

①タイトル画面表示 -> ②難易度選択 -> ③ゲームプレイ -> ④ゲームオーバー画面表示 -> ⑤リスタート選択 ->①へ

というかんじになっている。これらのタイミングで何を行う必要があるかというと、

⓪全てのオブジェクトが初期化される

①タイトル用テキストを表示(TitleText,DifficultyButton[]等)

②ゲームがアクティブになったことを知らせる(isGameActive=true)
難易度選択用ボタンに設定されたイベントハンドラを呼び出す
(button.onClick.AddListener(デリゲート形式のメソッド))
※ここでゲームプレイ用の変数が初期化され、タイトルテキストが非アクティブになる

スコアやHPなどが変動

ゲームオーバー用テキストを表示、ゲームが非アクティブになったことを知らせる(isGameActive=false)

リスタート用ボタンに設定されたイベントハンドラを呼び出して、シーンをリスタート

簡単なゲームだが、これだけ変数の中身が移り変わっていくことがわかる。
これに沿うようにタイムライン上で変数や処理が移り変わっていき、その変数が更新されたタイミングでUIも更新することになる。

2.現在のシーンをリスタートする

これは、

using UnityEngine.SceneManagement;

...

void RestartGame(){
    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

という感じに、リスタート用の関数を定義しておいて、

using UnityEngine.UI;

...

private Button restartButton;

void Start(){
    restartButton = GameObject.Find("Canvas").transform.Find("RestartButton").gameObject.GetComponent<Button>();
    restartButton.onClick.AddListener(RestartGame);
}

こんな感じでボタンのクリックイベントに関数を追加しておくと良い。

Lesson5.4 What's the Difficulty?

ここでは、タイトル画面にボタンを表示させて難易度選択をする機能を実装する。

といっても処理は単純で、それぞれのボタンをヒエラルキーで用意して、それにスクリプトを適用するだけ。具体的にはボタン用スクリプトに下記のメソッドを用意しておけば良い。

    private Button button;
    private GameManager gameManager;

    void Start()
    {
        button = GetComponent<Button>();
        button.onClick.AddListener(SetDifficulty);

        gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
    }

    void SetDifficulty()
    {
        //button.nameは["EasyButton","NormalButton","HardButton"]でヒエラルキーに用意してある。
        //それを文字列変換で難易度に対応付けている。
        gameManager.StartGame(button.name.Replace("Button",""));
        Debug.Log(button.gameObject.name + " was clicked");
    }

ちなみにここで呼ばれてるgameManager.StartGame()は、

public void StartGame(string difficulty)
{
    isGameActive = true;
    score = 0;
    UpdateScore(0);
    spawnRate /= GetDifficulty(difficulty);
    StartCoroutine(SpawnCoroutine());
    titleScene.SetActive(false);
}

private int GetDifficulty(string difficulty)
{
    Dictionary<string, int> difficultyDictionary = new Dictionary<string, int>()
    {
        {"Easy",1 },
        {"Normal",2 },
        {"Hard",3 }
    };
    return difficultyDictionary[difficulty];
}

こんな感じで、連想配列のキーに難易度の文字列を渡している。こっちのほうがわかりやすいと思う。

Challenge5 Whack-a-Food

ここでは今までの学習の総復習をする。
特記事項は無いけど...。しいて言えば最後のチャレンジ問題かな。

チャレンジ問題は”時間制限が無いから実装しよう”っていう感じで、

void Start(){
    StartCoroutine(TimeLimit());
}

// ゲームの制限時間を設ける。
IEnumerator TimeLimit()
{
    for(var i = 60; i > 0; i--)
    {
        UpdateTimeLimit(timeLimit);
        yield return new WaitForSeconds(1);
    }
    GameOver();
}

//タイムリミット用変数とテキストを更新する
public void UpdateTimeLimit(int timeLimit)
{
    this.timeLimit = timeLimit;
    this.timeLimitText.text = "Time: " + this.timeLimit;
}

自分はこんな感じに実装した。当時はプロパティとか使おうと思ってなかったからこんな感じだけど、使えばもう少し読みやすくなって、管理がしやすいコードになるかなと思う。

振り返り

今回は連休もあってちょっと早めに記事を書いておいた。
ひとまずLab5やったらチュートリアルがおしまいなので、自分なりのミニゲームを作っていこうかなと思ってる。4連休のうちにどんなゲーム作るかぐらいは考えておこっかな~。

みなさん今週もお疲れ様です( ´_ゝ`)