[Unity] 新UGUIでゲーム内にモーダルダイアログ(ポーズメニュー)を表示する

Unityでゲーム内モーダルダイアログ(ポーズメニュー)の表示に挑戦しようと思います。

発端は「Escapeキーが押されたらダイアログを出したい!」と思い調べてみたら、Unityにはモーダルダイアログ/モーダルウィンドウの類は存在しないこと知りました。無いものはしょうがないので自作しよう、というわけで実装してみた内容をまとめました。今回も実装方法が正しいかどうかわかりません。

 

2015/1/27 追記

この方法だとポーズを解いた時にアニメーションが停止位置から再生されません。やはりTime.timeScale=0の方法が良さそうですね。

 

 

 

環境

Unity 4.6 + UGUI

 

参考にしたサイト

【Unity Action】 ポーズメニュー怖い(Time.timeScale = 0 関連の話)
http://hideapp.cocolog-nifty.com/blog/2013/06/unity-action-ti.html

[Unity] ポーズ動作をTime.timeScale=0を使わずに実現する
http://ftvoid.com/blog/post/660

[Unity] ポーズ動作をTime.timeScale=0を使わずに実現する(その2)
http://ftvoid.com/blog/post/662

モーダルダイアログではなくポーズメニューが一般的だったね。まぁモーダルダイアログでも通じると思うから・・・。

 

目標

  • モーダルダイアログ表示中は他の操作を無効にしたい
  • ゲーム内時間を止めたい

 

定義

2つの組み合わせで4つのパターンが出来ます・・・

  1. 他の操作無効 + ゲーム内時間の停止 = システムモーダルダイアログ
  2. 他の操作無効 + ゲーム内は停止しない = モーダルダイアログ
  3. 他の操作も可能 + ゲーム内は停止しない = モーダレスダイアログ
  4. 他の操作も可能 + ゲーム内時間の停止 = 一時停止機能

※ゲーム内時間の停止にはアニメーションの停止を含みたい場合と、アニメーションは停止させたくない場合がありそうです。例えば攻撃モーション中にポーズをした時、ゲーム内の状態が変わりうる攻撃モーションは停止するが、ゲーム内の状態が変わらない草木の揺らぎなどは停止する必要がない、みたいなの。でもとりあえず考えない!

 

結果

注意、音が出ます!

画面上に3つボタンがあり、ダイアログの出方が異なります。ただすべての状態でTime.deltaTimeは一定のままです。

3Dモデル : Animated Knight and Slime Monster

音楽 : 魔王魂

 

 

時間停止を再現する方法

こちらの考えをいただきました。ありがたやー。

[Unity] ポーズ動作をTime.timeScale=0を使わずに実現する
http://ftvoid.com/blog/post/660

[Unity] ポーズ動作をTime.timeScale=0を使わずに実現する(その2)
http://ftvoid.com/blog/post/662

Behaviour.enabled = falseでUpdateメソッドが呼ばれなくなるなんて!

 

入力無効にする方法

Canvasの下にあるGameObjectを対象にし、 UnityEngine.UI.Selectableコンポーネントを検索し interactable = false とすることで入力系を無効にしました。

 

考え方

参考にした記事では時間停止/入力無効にしたいGameObjectに対してPauserスクリプトを追加することで対応していますが、開発が進んでいくと面倒になりそう、デバッグが大変になりそう、などが考えられるのでPauserスクリプトを追加することなく実装していきます。

つまり開発しているプロジェクトの途中でも追加できる!

 

実装方法

単純にGameObjectを列挙し「Canvasの子GameObjectはすべてUI関係」、「Canvas以外のGameObjectはすべてGame関係」と判断します。

列挙したものに対して参考サイトのPauserスクリプトの内容を実行してあげます。これで完成です。

 

時間停止/入力無効クラス(TimePauserクラス)

参考サイトのPauserスクリプトを元に改造したコードです。

TimePauser.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// ポーズ機能を提供します
/// </summary>
public class TimePauser 
{
  /// <summary>
  /// ポーズ状態にしたUIオブジェクト配列
  /// </summary>
  private List<UnityEngine.UI.Selectable> _pause_selectables = new List<UnityEngine.UI.Selectable> ();

  /// <summary>
  /// ポーズ状態にしたオブジェクト配列
  /// </summary>
  private List<Behaviour> _pause_objects = new List<Behaviour> ();

  /// <summary>
  /// ポーズ状態にしたRigidbody配列
  /// </summary>
  private List<Rigidbody> _RigidBodies = new List<Rigidbody>();
  private List<Vector3> _RigidBodyVelocities = new List<Vector3>();
  private List<Vector3> _RigidBodyAngularVelocities = new List<Vector3>();
  
  /// <summary>
  /// ポーズ状態にしたRigidbody2D配列
  /// </summary>
  private List<Rigidbody2D> _RigidBodies2D = new List<Rigidbody2D>();
  private List<Vector2> _RigidBodyVelocities2D = new List<Vector2>();
  private List<float> _RigidBodyAngularVelocities2D = new List<float>();

  /// <summary>
  /// 無視するオブジェクト
  /// </summary>
  private GameObject _excludeObject = null;

  /// <summary>
  /// 
  /// </summary>
  public TimePauser()
  {
  }

  /// <summary>
  /// コンストラクタ
  /// </summary>
  /// <param name="excludeObject">無視するオブジェクト</param>
  public TimePauser( GameObject excludeObject )
  {
    this._excludeObject = excludeObject;
  }

  /// <summary>
  /// UI関係のオブジェクトを無効にします
  /// </summary>
  public void PauseUI()
  {
    Pause (UIObject ());
  }

  /// <summary>
  /// Game関係のオブジェクトを無効にします
  /// </summary>
  public void PauseGame()
  {
    Pause (GmObject ());
  }

  /// <summary>
  /// ポーズにしたオブジェクトを元に戻します
  /// </summary>
  public void Resume()
  {
    // UnityEngine.UI.Selectableを有効
    _pause_selectables.ForEach (o => o.interactable = true);

    // Behaviourを有効
    _pause_objects.ForEach (o => o.enabled = true);

    // Rigidbodyを有効
    for( var i=0; i<_RigidBodies.Count; i++ ) {
      _RigidBodies[i].WakeUp();
      _RigidBodies[i].velocity = _RigidBodyVelocities[i];
      _RigidBodies[i].angularVelocity = _RigidBodyAngularVelocities[i];
    }
    
    // Rigidbody2Dを有効
    for( var i=0; i<_RigidBodies2D.Count; i++ ) {
      _RigidBodies2D[i].WakeUp();
      _RigidBodies2D[i].velocity = _RigidBodyVelocities2D[i];
      _RigidBodies2D[i].angularVelocity = _RigidBodyAngularVelocities2D[i];
    }
  }

  /// <summary>
  /// 指定オブジェクトを無効化します
  /// </summary>
  /// <param name="objs">Objects.</param>
  private void Pause( GameObject[] objs )
  {
    foreach( var obj in objs ) {
      // 無効にする対象かどうか
      if (IsExclude (obj)) {
        continue;
      }

      // UnityEngine.UI.Selectableを無効
      var pauseSelectable = Array.FindAll(obj.GetComponentsInChildren<UnityEngine.UI.Selectable>(), (cmp) => { return cmp.interactable; });
      _pause_selectables.AddRange( pauseSelectable );

      // Behaviourを無効
      var pauseBehavs = Array.FindAll(obj.GetComponentsInChildren<Behaviour>(), (cmp) => { return !(cmp is UnityEngine.EventSystems.UIBehaviour) && cmp.enabled; });
      _pause_objects.AddRange( pauseBehavs );

      // Rigidbodyを無効
      _RigidBodies.AddRange( Array.FindAll(obj.GetComponentsInChildren<Rigidbody>(), (cmp) => { return !cmp.IsSleeping(); }) );
      _RigidBodyVelocities = new List<Vector3>( _RigidBodies.Count );
      _RigidBodyAngularVelocities = new List<Vector3>( _RigidBodies.Count );
      for ( var i = 0 ; i < _RigidBodies.Count ; ++i ) {
        _RigidBodyVelocities[i] = _RigidBodies[i].velocity;
        _RigidBodyAngularVelocities[i] = _RigidBodies[i].angularVelocity;
        _RigidBodies[i].Sleep();
      }

      // Rigidbody2Dを無効
      _RigidBodies2D.AddRange( Array.FindAll(obj.GetComponentsInChildren<Rigidbody2D>(), (cmp) => { return !cmp.IsSleeping(); }) );
      _RigidBodyVelocities2D = new List<Vector2>( _RigidBodies2D.Count );
      _RigidBodyAngularVelocities2D = new List<float>( _RigidBodies2D.Count );
      for ( var i = 0 ; i < _RigidBodies2D.Count ; ++i ) {
        _RigidBodyVelocities2D[i] = _RigidBodies2D[i].velocity;
        _RigidBodyAngularVelocities2D[i] = _RigidBodies2D[i].angularVelocity;
        _RigidBodies2D[i].Sleep();
      }
    }

    // 無効
    _pause_selectables.ForEach( o => o.interactable = false );
    _pause_objects.ForEach (o => o.enabled = false);
  }

  /// <summary>
  /// 例外的に処理をしないオブジェクトかどうか取得します
  /// </summary>
  /// <returns>無視する場合はtureを返します</returns>
  /// <param name="obj"></param>
  private bool IsExclude( GameObject obj )
  {
    // 外部指定の無視オブジェクト
    if (this._excludeObject == obj) {
      return true;
    }
    // カメラ
    if (obj.GetComponent<Camera> () != null) {
      return true;
    }
    // ライト
    if (obj.GetComponent<Light> () != null) {
      return true;
    }
    // イベントシステム
    if (obj.GetComponent<UnityEngine.EventSystems.EventSystem> () != null) {
      return true;
    }

    // MonoBehaviourのみで構成されたGameObject
    // どうやって判定するの?

    return false;
  }

  /// <summary>
  /// ルートオブジェクトを取得します
  /// </summary>
  private static GameObject[] Root()
  {
    return Array.FindAll( GameObject.FindObjectsOfType<GameObject> (), (item) => item.transform.parent == null);
  }

  /// <summary>
  /// UI関係のGameObjectを取得します
  /// </summary>
  /// <returns>The object.</returns>
  private static GameObject[] UIObject()
  {
    var roots = Root ();
    var canvas = Array.FindAll (roots, obj => obj.GetComponent<Canvas> () != null);

    // Canvas自体は無効にしたくないので、Canvasの子オブジェクトを対象にする
    var uiObjs = new List<GameObject> ();
    for (int i=0; i<canvas.Length; i++) {
      for( int n=0; n<canvas[i].transform.childCount; n++ ) {
        uiObjs.Add ( canvas[i].transform.GetChild( n ).gameObject );
      }
    }
    return uiObjs.ToArray();
  }

  /// <summary>
  /// Game関係のGameObjectを取得します
  /// </summary>
  /// <returns>The object.</returns>
  private static GameObject[] GmObject()
  {
    var roots = Root ();
    return Array.FindAll (roots, obj => obj.GetComponent<Canvas> () == null);
  }
}

 

2種類のダイアログクラス

実際に制御が必要なのはシステムモーダルダイアログ、モーダルダイアログの2種類なのでクラス化しておきます。

作成したクラスはダイアログ化したいGameObjectに追加すればOK。

Dialog.cs
using UnityEngine;
using System.Collections;

/// <summary>
/// モーダル/モーダレスダイアログ、ゲーム内時間Pause状態の設定が行えるダイアログクラス
/// </summary>
public abstract class Dialog : MonoBehaviour
{
  /// <summary>
  /// ポーズ状態
  /// </summary>
  TimePauser _pauser = null;

  /// <summary>
  /// UI状態
  /// </summary>
  private bool _isUIEnable = true;

  /// <summary>
  /// ゲーム内時間Pause状態
  /// </summary>
	private bool _isGameEnable = true;

  /// <summary>
  /// モーダルダイアログ状態の取得または設定します
  /// </summary>
  /// <value></value>
  public bool IsUIEnable
  {
		get { return _isUIEnable; }
		set { _isUIEnable = value; }
  }

  /// <summary>
  /// 起動時のゲーム内時間Pause状態を取得または設定します
  /// </summary>
  /// <value></value>
	public bool IsGameEnable 
  {
    get { return _isGameEnable; }
    set { _isGameEnable = value; }
  }

  /// <summary>
  /// Start
  /// </summary>
  void Start()
  {
    // ダイアログ開始
    _pauser = new TimePauser (gameObject);
    if (IsUIEnable != true) {
      _pauser.PauseUI();
    }
    if (IsGameEnable != true) {
      _pauser.PauseGame();
    }
  }
  
  /// <summary>
  /// OnDestory
  /// </summary>
  void OnDisable()
  {
    // ダイアログ終了
    _pauser.Resume ();
  }

  /// <summary>
  /// 閉じるイベント
  /// </summary>
  public void OnCloseButton()
  {
    Destroy (gameObject);
  }
}
NormalModalDialog.cs
using UnityEngine;
using System.Collections;

/// <summary>
/// モーダルダイアログ
/// 自分自身以外のダイアログ入力禁止を行います
/// </summary>
public class NormalModalDialog : Dialog 
{
  /// <summary>
  /// 
  /// </summary>
  void Awake () 
  {
    IsUIEnable = false;
    IsGameEnable = true;
  }
}
SystemModalDialog.cs
using UnityEngine;
using System.Collections;

/// <summary>
/// システムモーダルダイアログ
/// 自分自身以外のダイアログ入力禁止 + ゲーム内時間の停止を行います
/// </summary>
public class SystemModalDialog : Dialog 
{
  /// <summary>
  /// 
  /// </summary>
  void Awake () 
  {
    IsUIEnable = false;
    IsGameEnable = false;
  }
}

 

時間停止/入力無効をしたくない例外的なオブジェクト

UIを無効にし、ゲーム内時間を止めたとしても無効にされなくないオブジェクトが存在します。そういうオブジェクトに何かスクリプトを付けて・・・と思ったのですがこれも面倒、忘れそうだったので、TimePauserクラス内で判断するようにしました。

例外オブジェクトは

  1. カメラ
  2. ライト
  3. イベントシステム
  4. GameObject.Nameに”(non_timescale_object)”が含まれるもの

最後の「GameObject.Nameに”(non_timescale_object)”が含まれるもの」が必要な理由は、空のGameObject + 音楽制御用のスクリプト等だった場合、無効にすると音楽が停止します。(うわーローカルルールが増えたー。)

この例外的オブジェクトチェックはプロジェクトによって変化すると思います。あとカメラやライトが一番上の階層にいる前提なので注意ですね。

 

制限

  • Canvasを利用しているのでUGUI 4.6でないとUIとGameの2つに分離できない。つまり旧GUI(?)やOnGUIを使用している場合は対応できない。
  • カメラ、ライト、イベントシステムが一番上の階層にいることが前提。
  • 時間停止をしたくないオブジェクトは、一番上の階層に配置し、かつ名前の中に「(non_timescale_object)」を含めなくてはいけない。
  • なぜかパーティクルは停止しない、これは意図していません。

 

まとめ

依存関係を作ることなくモーダルダイアログ機能を実装できました。気に入らなければTimePauser、Dialog、SystemModalDialog、ModalDialogの4つのファイルを消せば元通り。

さらにTime.timeScaleを0にしているわけではないのでTime.deltaTimeなどはそのまま使用できますし、ゲーム内の時間が停止しているなのでアニメーションも動きます。

 

ただTime.timeScale = 0で止める方が正解かも。

【Unity Action】 ポーズメニュー怖い(Time.timeScale = 0 関連の話)
http://hideapp.cocolog-nifty.com/blog/2013/06/unity-action-ti.html

【Unity】時間を止めてもアニメーションさせる
http://bribser.co.jp/blog/timescale-animation/

 

こちらのサイトでもTime.timeScale = 0を使用してポーズを再現しています

CREATE A SLIDING PAUSE MENU – UNITY 4.6 GUI
http://www.thegamecontriver.com/2014/10/create-sliding-pause-menu-unity-46-gui.html

シンプルでわかりやすい。

 

ただ今回の記事で他のボタン等の入力を不可に出来たので、その点だけは使えるのかなと・・・


希木小鳥

Diablo1でハクスラの世界に。今はBorderlands2をプレイ中。ぬるゲーマー。

あわせて読みたい