Unityで簡単な2D脱出ゲームを作ってウェブサイトで公開してみよう 〜ヒント画面の表示〜

ヒントの表示

この記事を三行にまとめると

メインのゲーム画面の上にもう一つの画面を出すみたいなことです
アクティブ状態を操作して表示する画面を切り替えます
僕も最初はforeachで子オブジェクトを毎回全て非アクティブにしていました
今回はヒント画面を出す処理をスクリプトで書いてみます。やっていることはメインのゲーム画面の上にもう一つの画面を出すみたいなことです。ポップアップ画面とかモーダル画面とか言えば良いのかな……? なのでヒント画面に限らず、メイン画面の上にワイプみたいな画面を出したいような時はこの方法が役立つかもしれません。

考え方としては室内の移動と同じで、アクティブ状態を操作して表示する画面を切り替えます。

さっそくやってみましょう。動画はこちらです。





ヒント画面の構成

ヒント画面はRoom1〜Room4とは別のCanvasオブジェクトで管理しています。

Dialogオブジェクト

Dialogというのがヒント画面用の親Canvasです。このDialogはゲーム中は常にアクティブな状態です。なのでヒント画面を表示する時に操作するのはその子供であるHintオブジェクトと、さらにその子供の子供であるCalendarやFrameなどです。

Hintオブジェクトは空オブジェクトと呼ばれるオブジェクトです。動画では#4のテキストオブジェクトの回で一度紹介しているのですが、オブジェクトを作る際に「Create Empty」でオブジェクトを作ると、位置と大きさだけを持った空のオブジェクトができます。こいつ自身は画像やイベントなどは何もついていなくて、単に複数のオブジェクトをまとめて管理したい場合の親オブジェクトとして使うことができます。こいつを親にしておくことで、Hintのアクティブ状態を操作してヒント画面の表示、非表示を切り替えられるようになります。



ヒント画面を開く

ヒント画面を開くスクリプトはこんな感じです。

public class Dialog : MonoBehaviour {
  public GameObject dialog;

  public void OpenDialog(GameObject detail) {
    dialog.SetActive(true);
    detail.SetActive(true);
  }
}

dialog変数にはHintオブジェクトが入ります。これがアクティブになればHint画面が開いた状態になります。

それからOpenDialog関数のところにdetailという引数を作っていますが、これはヒント画面の中に表示するオブジェクトです。

室内移動の時はRoom1やRoom2のように画面を丸ごと別の親オブジェクトで管理していたので、それらのアクティブ状態を切り替えるだけで表示画面を切り替えることができました。ヒント画面も同じように画面ごとにHint1、Hint2……と分けて作れば、Roomと同じやり方で表示の切り替えができます。図にするとこんな感じ。

Dialog
  ├ Hint1
  │  ├ Overlay
  │  ├ Background
  │  ├ Calendar
  │  └ Close
  │ 
  ├ Hint2
  │  ├ Overlay
  │  ├ Background
  │  ├ Frame
  │  └ Close
  │ 
  └ Hint3
     ├ Overlay
     ├ Background
     ├ Picture
     └ Close

これならHint1やHint2の表示を切り替えればそれぞれのヒント画面を表示できます。でも上の画像を見れば分かる通り、本作ではこのような作りにはなっていません。全てのヒント画面は共通のHintという親オブジェクトの中にあります。だからヒント画面を開く時にどのヒントをアクティブにするかを指定する必要があります。指定する方法はいろいろ考えられると思いますが、今回は引数で指定する方法を採用しました。これならOpenDialog関数を呼び出す時にアクティブにしたいオブジェクトを自由に選択できます。



ヒント画面を閉じる

続いてヒント画面を閉じるスクリプトを追加してみましょう。「CloseDialog」という関数を作成して画面を閉じる処理を書いてみます。

public class Dialog : MonoBehaviour {
  public GameObject dialog;

  public void OpenDialog(GameObject detail) {
    dialog.SetActive(true);
    detail.SetActive(true);
  }

  public void CloseDialog() {
    dialog.SetActive(false);
  }
}

dialog変数を非アクティブにする処理を書きました。これでヒント画面を閉じることができます。

一見するとこれで完成のようにも見えますが、実はこれだけだと不具合が発生します。

親オブジェクトを非アクティブにすると子供たちも画面からは見えなくなりますが、非アクティブになるわけではありません。なのでdialog変数を非アクティブにしても、その子供であるdetail変数にセットしたオブジェクトはアクティブなままです。つまり一度表示したヒント画面は次に別のヒントを表示した時に一緒に画面に出てきてしまいます。

ヒントとヒントがかぶってしまった

これを回避するためにはCloseDialog関数の中でdetail変数のオブジェクトも一緒に非アクティブにするのが望ましいのですが、残念ながら引数で指定した変数は他の関数で使うことはできません。今回の場合だとdetail変数はOpenDialogの中でしか使えません。じゃあCloseDialog関数にも引数で非アクティブにするオブジェクトを渡せば良いじゃんって言いたいところなんですけど、本作ではCloseDialog関数を呼び出すボタンも共通のオブジェクトなので、クリック時にどのヒントを非アクティブにしたら良いかっていう判断ができません。ヒントごとに閉じるボタンを用意すればその方法でもいけるんですけど……。

というわけで、今回はもう一つ変数を用意して不具合を回避します。

public class Dialog : MonoBehaviour {
  public GameObject dialog;
  private GameObject current;

  public void OpenDialog(GameObject detail) {
    current = detail;

    dialog.SetActive(true);
    current.SetActive(true);
  }

  public void CloseDialog() {
    dialog.SetActive(false);
    current.SetActive(false);
  }
}

currentという変数を用意しました。この関数はOpenDialogでもCloseDialogでも使えるので、detail変数の中身をcurrent変数に入れ直すことでCloseDialog関数でヒント画面の中身の部分を非アクティブにできます。



子オブジェクトをまとめて操作する

上記のやり方以外でも問題はないですが、他にも「子アクティブをまとめて非アクティブにする」という方法で先ほどの不具合を回避することもできます。

とりあえずコードを書いてみましょう。

public class Dialog : MonoBehaviour {
  public GameObject dialog;
  public GameObject parent;

  public void CloseDialog() {
    dialog.SetActive(false);

    foreach(Transform child in parent.transform) {
      child.gameObject.SetActive(false);
    }
  }
}

foreachというのは配列のそれぞれの要素に同じ処理を行うことができる構文です。繰り返し処理とかループ処理などと言います。

室内の移動を実装する際にroomsという配列用の変数を作成しましたが、あの配列に対してforeachを使うと全部のオブジェクトをまとめて非アクティブにできたりします。

public class Room : MonoBehaviour {
  public GameObject[] rooms;

  public void Move() {
    foreach(GameObject room in rooms) {
      room.SetActive(false);
    }
    
    room[0].SetActive(true);
  }
}

例えばこんな感じで、いったん全部のオブジェクトを非アクティブにしてから表示したいオブジェクトをアクティブにして表示する画面を切り替える、なんて処理も可能です。この方法でも視点移動は実現できます。

同じ要領で全ての子オブジェクトをまとめて非アクティブにする処理を入れておけば、毎回全部のヒント画面が非アクティブになるので、アクティブなヒントが残ったままの状態は回避できます。

子オブジェクトは「parent.transform」の中に配列で入っています。parentは親オブジェクト用の変数です。

ちなみに子オブジェクトというのは一段階下のオブジェクトのみを指します。階層が何重にもなっていたとしても、それら全てのオブジェクトがtransformの中に入っているわけではありません。

Dialog
  └ Hint
      ├ Overlay
      ├ Background
      │   ├ Calendar
      │   ├ Frame
      │   └ Picture
      └ Close

本作のヒエラルキーはこんな感じになっているわけですが、この場合、Dialogオブジェクトの子供はHintだけです。さらにその下のOverlayとかCalendarは子オブジェクトには含まれません。同じくHintの子供もOverlay、Background、Closeの3つだけで、CalendarやFrameはBackgorundの子供ということになります。

つまり上記のforeach文でCalendarやFrame、Pictureを非アクティブにしたい場合、この構造ならparent変数にはBackgroundを入れることになりますね。

foreach文を使う場合はどのオブジェクトを非アクティブにするか判定する必要がなくなるので、先ほどのcurrent変数に渡すという処理はいらなくなります。どっちの方が良いかは……うーん、子オブジェクトの数があまりにも多い場合はforeach文を使わない方が良さそうな気もしますが、本作くらいの数なら使いやすい方で良いんじゃないかなあ。

実は僕も最初はこのforeachで子オブジェクトを毎回全て非アクティブにするという方法を採用していました。Closeオブジェクトが外にあるのはその名残です。画面の構成的にはCloseもBackgroundの中にある方が良い気もしたんですが、そうするとforeachの処理でCloseオブジェクトも非アクティブになってしまうため、次にヒント画面を開いた時に閉じるボタンが消えているという現象が発生してしまいます。だからCloseはBackgroundの子オブジェクトにならないところに置いていました。でも別に変数をもういっこ用意すれば良いかあと途中で思いまして……今に至る。






ヒエラルキーの構造とかによっても多少は変わってくると思いますけど、基本的な考え方はそんなに差がないはずなので、だいたいどんなポップアップ画面もこんな感じのやり方で実装できると思います。本作はHintやOverlay、BackgroundやCloseを共通化して一つにしていますが、場合によってはまとめすぎるとごちゃごちゃしてしまうこともあると思うので、先の例のようにHint1、Hint2……とHintごとに同じものを複製するやり方の方が管理がしやすければそっちの構成にするのも良いと思います。プログラミングでも共通の処理をまとめすぎちゃった結果、逆に汎用性とかコードの読みやすさ、修正のしやすさが下がることもたびたびありますからね。僕もだいぶ長いことエンジニアやってますが、ここはまとめた方が良い、ここはあえてまとめない方が良いという判断は未だに上手くできないんだよなぁ……。

次回はオブジェクトの大きさや画像などをインスペクターではなくスクリプトから変更する方法を紹介します。本作ではあまり使ってるところがないんですけど、テキストの変更とかは覚えておくと便利だと思います。

それじゃあ次回もよろしくお願いしるえいてぃ。
 もしかしたら何か関連しているかも? 
 みんなからのコメント 
まだコメントはいただけてないみたい……