パート⑦:ステージを切り替える@2Dローグライク公式tutorial解説【Unity2018】

このパートで行うこと

今回のパートでは、プレイヤーが扉に入ったら次のステージに切り替わるという流れを実装する。
また、プレイヤーのHP(Food)が0以下になることによって、ゲームオーバーにもさせる。

これでこのゲームも開始から終了までの一通りのゲームが流れることになる。

f:id:snoopopo:20180727130006g:plain


前回の記事はこちら。
パート⑥:アイテムを入手してHPを回復できるようにする@2Dローグライク公式tutorial解説【Unity2018】


※この記事は、unity公式チュートリアル「2D Roguelike tutorial」を解説した連載記事です。
この連載記事共通の事項(実施している環境等)は以下の記事を参照ください。
表紙:2Dローグライク公式tutorial解説【Unity2018】


扉プレハブの設定

前回のパートのアイテムと同じように、扉に当たり判定をつけていこう。

f:id:snoopopo:20180727094620p:plain

タグの設定を「Exit」に、
Box Collider 2Dコンポーネントをアタッチして、忘れずにIsTriggerにもチェックをいれよう。

ステージの切り替え

このゲームはプレイヤーが扉に入る(当たる)と、ステージが切り替える。
切り替わる際に、「Day 1」「Day 2」「Day 3」… 
と切り替わるたびにステージのレベルが1つずつ上がっていく。

ステージレベルを表示する

最初に、ステージレベルを表示するための準備を整えよう。
前回のパートで登場したuGUIのコンポーネントを利用する。

まずは背景用の画面全体のべた塗り画像のゲームオブジェクトを作成する。

(ヒエラルキーの)「Canvas」ゲームオブジェクトを右クリック -> UI -> Image

Imageコンポーネントをもった「Image」というゲームオブジェクトが作成されるので「LevelImage」に名前を変えておこう。

Imageコンポーネントは、名前の通りUI上の画像を表示する機能をもっている。
Image#SourceImageには、スプライト画像を指定することができるが、指定しない場合は、矩形の画像としてが表示される。

f:id:snoopopo:20180727104903p:plain

ただの四角の画像。

今回描画したいのは背景用の画面全体のべた塗り画像である。
前回のパートで説明した通り、RectTransformは矩形範囲を指定できるものなので、
画面全体を矩形範囲として指定することもできる。

以下のように水平・垂直方向ともstretch 状態にしておく。
Altキーを押すと自然に画面全体に画像が伸びるのでやってみてほしい。

f:id:snoopopo:20180727105308g:plain

矢印が伸びたタイミングでAltキーを押している。


次は、現在のレベルを文字で表示しよう。これは前回のパートでやっているので思い出そう。

作成したゲームオブジェクトは、こちらもわかりやすいように「LevelText」と名前を変えておく。
また、先程作成した「LavelImage」と同時のタイミングで表示/非表示を切り替えるものとなるので、 「LavelImage」の子オブジェクトとして作成しよう。

このような感じになる。

f:id:snoopopo:20180727110310p:plain

インスペクターで「LavelImage」のTextコンポーネントの設定を以下の通りに設定しておこう。

設定項目 設定値
Text Day 1
Font PressStart2P-Regular
FontSize 32
Alignment 水平・垂直方向とも中央
Horizonal Overflow Overflow
Vertical Overflow Overflow
Color #FFFFFF(白)

ステージの切り替え

次のステージに切り替えるには今まで使用しているシーンを再読み込みすることで行う。

まずはPlayerスクリプトに機能を追加していこう。

Player.cs
protected override void Start(){
    base.Start();
    this.food = GameManager.instance.playerFoodPoints; //←追加
    this.foodText.text = "Food:" + this.food;
}

private void OnDisable(){
    GameManager.instance.playerFoodPoints = this.food;
}

private void OnTriggerEnter2D(Collider2D other){
    if(other.tag == "Food"){
        food += pointPerFood;
        other.gameObject.SetActive(false);
        this.foodText.text = "+" + pointPerFood + " Food:" + this.food;
        
    } else if(other.tag == "Soda"){
        food += pointPerSoda;
        other.gameObject.SetActive(false);
        this.foodText.text = "+" + pointPerSoda + " Food:" + this.food;

    } else if(other.tag == "Exit") { //追加!!
        Invoke("Restart", this.restartLevelDelay);
        this.enabled = false;
    } 
}

private void Restart(){
    //再読み込み
    SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex, LoadSceneMode.Single);
}

わかりやすいところから説明していく。

OnTriggerEnter2D()に扉プレハブのif文を追加している。
OnTriggerEnter2D()はコライダーと当たった時に呼ばれるメソッドだった。
扉とプレイヤーがぶつかったときにInvoke()restartLevelDelay秒待ったあとRestart()を実行する。

Restart()では現在実行しているシーンの再読み込みを行っている。

また、OnTriggerEnter2D()では、Playerスクリプトを無効にしており、
そのタイミングでOnDisable()が呼ばれる。

OnDisable()では、現在のHP(food)の値を次のステージに引き継ぎたいため、
シーンの再読み込みを行っても、値を保持しつづけられるGameManager#playerFoodPointsに退避させている。
GameManagerがシーンの再読み込みを行っても、値を保持しつづけられるのは、
以前のパートでGameManagerDontDestroyOnLoadにして常に消えないオブジェクトとなっているためである。

パート②:静的な物体(床・外壁・出口)を配置する@2Dローグライク公式tutorial解説【Unity2018】

最後にシーンが読み込まれた後に呼ばれるStart()GameManager#playerFoodPointsに一時退避させていた値を、
自身のfoodに代入している。

という流れである。


次にGameManagerにも変更を加える。

GameManager.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour {

    public int playerFoodPoints = 100; //プレイヤーの初期HP

    private int level = 1; //ステージレベル
    private bool doingSetup; //trueならセットアップ中
    private GameObject levelImage;
    private Text levelText;
    private float levelStartDelay = 2f;
 //その他のフィールドは変更なし

    void InitGame()
    {
        this.doingSetup = true;

        this.levelImage = GameObject.Find("LevelImage");
        this.levelText = GameObject.Find("LevelText").GetComponent<Text>();
        this.levelText.text = "Day " + level;
        this.levelImage.SetActive(true);

        Invoke("HideLevelImage", this.levelStartDelay);
        boardScript.SetupScene();
    }

    private void HideLevelImage()
    {
        this.levelImage.SetActive(false);
        this.doingSetup = false;
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    static public void CallbackInitialization()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    static private void OnSceneLoaded(Scene arg0, LoadSceneMode arg1)
    {
        instance.level++; //ステージレベルを上げる
        instance.InitGame();
    }

    void Update()
    {
        if (playersTurn || wait || this.doingSetup) //doingSetupを追加。セットアップ中は処理しないように。
        {
            return;
        }
        StartCoroutine(WaitTurn());
    }
    //その他のメソッドは変更なし
}

InitGame()で作成した「LevelImage」「LevelText」ゲームオブジェクトを取得し、
「LevelText」の文字を現在の ”Day ” + ステージレベルに更新し、levelStartDelay 秒表示してから、
HideLevelImage() でステージレベルの描画をやめて、ステージの描画に戻す。

CallbackInitialization()についている、[RuntimeInitializeOnLoadMethod] はゲーム起動時に1度だけ実行するようにするAttributeだ。
つまり、CallbackInitialization()はゲーム起動時に一度だけ実行される。

CallbackInitialization()で行っている、

SceneManager.sceneLoaded += OnSceneLoaded;

上記は、シーンが読み込まれた際に行う処理を設定している。
つまり、ステージが切り替わるたびにシーンの再読み込みをしているので、OnSceneLoaded()はステージが切り替わるたびに呼ばれることになる。

OnSceneLoaded()では、ステージレベルをひとつ上げ、InitGame()を呼んでステージのセットアップを行う。


この状態でゲームを起動してみよう。

NullReferenceException: Object reference not set to an instance of an object
Completed.GameManager.OnSceneLoaded (Scene arg0, LoadSceneMode arg1) (at Assets/_Complete-Game/Scripts/GameManager.cs:69)
UnityEngine.SceneManagement.SceneManager.Internal_SceneLoaded (Scene scene, LoadSceneMode mode) (at C:/buildslave/unity/build/artifacts/generated/bindings_old/common/Core/SceneManagerBindings.gen.cs:245)

上記のエラーが出てしまう場合は、
完成版である「_Complete-Game」のスクリプトが動いてしまっている。


これは意図した動きではないため、
エラーが出ている箇所の「_Complete-Game」配下のGameManager#CallbackInitialization()についている、
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] をコメントアウトして動かないようにしておこう。*1

「_Complete-Game」配下のGameManager.cs
//コメントアウトしておく。
//[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static public void CallbackInitialization(){
        //register the callback to be called everytime the scene is loaded
        SceneManager.sceneLoaded += OnSceneLoaded;
}

ゲームが正常に動いただろうか。

  • プレイヤーが扉に当たるとステージが切り替わること。
  • ステージのレベルが上がっていること
  • HP(Food)の値が引き継がれていること

を確認しよう。

f:id:snoopopo:20180727121551g:plain

ゲームオーバー

最後にゲームオーバーを実装しよう。

Playerにゲームオーバーになっているかチェックを行うメソッドCheckIfGameOver()を追加し、
歩く都度呼ばれるAttemptMove()でHPが減るので、その都度チェックを行うようにする。

Player.cs
protected override void AttemptMove<T>(int xDir, int yDir) {    
    food --;
    foodText.text = "Food: " + food;

    base.AttemptMove<T>(xDir, yDir);
    CheckIfGameOver();
    GameManager.instance.playersTurn = false;
}

private void CheckIfGameOver(){
    if(this.food <= 0) {
        GameManager.instance.GameOver();
    }
}

GameManagerでは、ゲームオーバーになったときの、ステージレベルをゲームの結果として表示する。

GameManager.cs
public void GameOver(){
    this.levelText.text = "After " + this.level + " days, you starved";
    this.levelImage.SetActive(true);

    this.enabled = false;
}

これでゲームが開始から終了まで一通り流れができたことになる。

f:id:snoopopo:20180727130006g:plain HP(Food)が0になるとゲームオーバーになる。

次回予告

今回のパートでゲームの流れが一通り完成した。
これで残る登場人物は1つとなった。そう、残るは 敵 のみである。

次回のパートで敵プレハブを作成し、プレイヤーを追うように移動したり、プレイヤーへ攻撃したりさせる。

⇒ 次パートはこちら。

www.snoopopo.com

*1:もちろん完成版を動かす場合は元に戻す必要がある