Help us understand the problem. What is going on with this article?

【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 デバッグメニュー編

More than 5 years have passed since last update.

2年目のゲームプログラマがゲーム会社でUnityを使用して
スマートフォン向けのゲームを開発して感じたことをまとめておきます

デバッグメニュー

デバッグメニューを作成することでゲームのデバッグが容易になります
今回は実際にどんな感じでデバッグメニューを作成したのかを紹介していきます

デバッグメニュー用のシーンの枠組み作成

デバッグメニュー用のシーンファイルとスクリプトを作成します

デバッグメニュー用のシーンに「DebugMenu」オブジェクトを作成します

「DebugMenu」オブジェクトに「DebugMenu.cs」をアタッチします

「DebugMenu.cs」を下記のように編集します

using UnityEngine;

public class DebugMenu : MonoBehaviour
{
     private void Awake()
     {
          DontDestroyOnLoad( gameObject );
     }
}

Object.DontDestroyOnLoad関数にデバッグメニューのルートのオブジェクトを指定することで
シーンを遷移してもデバッグメニューが消えないようにしています

あとはゲーム起動時に呼び出されるコンポーネントのAwake関数で
Application.LoadLevelAdditive関数を使用して読み込みます

using UnityEngine;

public class Global : MonoBehaviour
{
     private void Awake()
     {
          // RELEASE シンボルが定義されている場合は
          // デバッグメニューのシーンを読み込みません
#if RELEASE
#else
          Application.LoadLevelAdditive( "DebugMenu" );
#endif
     }
}

この時に特定のシンボルが定義されている場合はデバッグメニューを読み込まないようにすることで
リリース版のアプリではデバッグメニューが表示されないようにします

これでデバッグメニューを作成する準備が完了です
あとは作成したデバッグメニューのシーンにOnGUIやuGUI、NGUIを使用してUIを配置していきます

注意:可能ならOnGUIで作成しない

デバッグメニューのUIを作成する時はuGUIやNGUIを使用することをオススメします
OnGUIは下記のような問題があるためです

  • ドローコールが多くて重い
    • デバッグメニューがアプリの処理速度に影響してしまいます
  • 実機で動作確認する時に解像度の調整が必要
    • 特にタブレットの場合は文字がほぼ見えない状態になってしまいます

(この記事では便宜上OnGUIでコードサンプルを記述しています)

デバッグメニューのシーンをビルド対象外にする

前述の方法だと、リリース版のアプリでデバッグメニューを表示しないようにできますが
デバッグメニューのシーンファイル自体はアプリに含まれてしまいます

JenkinsなどのCIツールを使用してアプリをビルドしている場合は
下記のようなスクリプトでアプリをビルドすることで
デバッグメニューのシーンファイルをビルドの対象から外すことができます

using System.Linq;
using UnityEditor;

public static class BuildBatch
{
     public static void ReleaseBuildAndroid()
     {
          var levels = EditorBuildSettings.scenes
               .Select( c => c.path )
               .ToList();

          levels.Remove( "DebugMenu" );

          BuildPipeline.BuildPlayer(
               levels.ToArray(),
               "app.apk",
               BuildTarget.Android,
               BuildOptions.None
          );
     }
}

見える化

Unityエディタ上であればInspectorやMonoDevelopのデバッグ機能を使用することで素早くパラメータを確認できますが
実機で動作確認するときは基本的にはログ出力でしかパラメータを確認できないので
デバッグメニューで各種パラメータを見れるようにしておくと実機でデバッグする時に役に立ちます

Unityのバージョン

Application.unityVersion

多くのプラグインを使用している場合、想定していないバージョンのUnityでアプリをビルドしてしまうと
挙動がおかしくなる可能性があるので、デバッグメニューで見れるようにしておくといざというときに役に立ちます

GUILayout.Label( Application.unityVersion );

タイムスケール

Time.timeScale

Time.timeScaleを使用してゲームの一時停止を実装している場合Time.timeScaleに0を代入したままになってしまい
ゲームがフリーズしてしまったように見えることがあったのでこれも表示しておくと良いかもしれません

GUILayout.Label( Time.timeScale.ToString() );

ゲーム独自の情報

  • プレイヤーの情報
  • 所持ユニットの一覧
  • 所持アイテムの一覧
  • フレンドの一覧
  • マスタの情報
  • etc.

このようなゲーム独自の情報もデバッグメニューで見れるようにしておくと
いざ不具合が発生した時にパラメータが正常かどうかをすぐに確認できます
クラスのパラメータを手早く表示したい場合はこちらで紹介している方法でJSON形式で表示するのがオススメです

var obj = new PlayerData();
var json = JsonManager.ToJson( obj );
GUILayout.Label( json );
{
    "Id" : 25,
    "Name" : "ピカチュウ"
}

Unity APIの情報

Unity APIのクラスの情報もJSON形式で表示しておくと良いかなと思います
特にApplicationクラスとSystemInfoクラスの情報は表示しておいて損はないです

Applicationクラス

Applicationクラスを参照することで現在のシーン名やビルドしたシーンの数を確認することができます

var obj = new Application();
var json = JsonManager.ToJson( obj );
GUILayout.Label( json );
{
    "loadedLevel" : 0,
    "loadedLevelName" : "DebugMenu",
    "isLoadingLevel"  : true,
    "levelCount"      : 1,
    "streamedBytes"   : 0,
    ...

SystemInfoクラス

SystemInfoクラスを参照すると端末のOSバージョンやメモリ情報を確認できます
UnityでAndroidやiOSのアプリを開発していると端末によって不具合が発生したりしなかったりするため
SystemInfoの情報もデバッグメニューで見れるようにしておくと役に立ちます

例)
NGUIのUIInputクラスをAndroid4.2.2で使用すると不具合が発生することがある
https://www.facebook.com/groups/unityuserj/permalink/727691427290800/

var obj = new SystemInfo();
var json = JsonManager.ToJson( obj );
GUILayout.Label( json );
{
    "operatingSystem" : "Windows 7 Service Pack 1 (6.1.7601) 64bit",
    "processorType"   : "Intel(R) Core(TM) i5 CPU M 480 @ 2.67GHz",
    "processorCount"  : 4,
    "systemMemorySize" : 3893,
    "graphicsMemorySize" : 128,
    ...

シーン遷移の情報

現在のシーンはApplication.loadedLevelNameで取得可能です

現在のシーン名をデバッグメニューに表示するのはあまり役に立たないかもしれませんが
シーン遷移時にシーン名を配列やリストに記憶しておき
シーン遷移の履歴をデバッグメニューで見れるようにしておくと不具合が発生した時に再現しやすくなります

using System.Collections.Generic;
using UnityEngine;

public static class LevelLoader
{
    private static readonly List<string> mHistory = new List<string>();

    public static IList<string> History
    {
         get { return mHistory; }
     }

    public static void Load( string name )
    {
        Application.LoadLevel( name );
        mHistory.Insert( 0, name );
    }
}

このようなクラスを作成しておき、実際にシーンを遷移するときはLevelLoader.Load関数を呼び出すようにすることで
LevelLoader.Historyプロパティにシーン遷移の履歴が溜まっていきます
あとはLevelLoader.Historyプロパティの内容をデバッグメニューで表示します

foreach ( var n in LevelLoader.History )
{
     GUILayout.Label( n );
}

FPS

ゲームを作成していると実装方法によってはゲームの処理速度が低下してしまうことがあるので
下記のようなスクリプトを作成していつでもFPSを見られるようにしておくことをオススメします

using UnityEngine;

public class FpsMeasure : MonoBehaviour
{
     public float Interval = 0.5f;

     private int mFrame;
     private float mOldTime;
     private float mFrameRate;

     public float FrameRate { get; private set; }

     private void Awake()
     {
          mOldTime = Time.realtimeSinceStartup;
     }

     private void Update()
     {
          mFrame++;
          var time = Time.realtimeSinceStartup - mOldTime;
          if ( time < Interval )
          {
               return;
          }
          mFrameRate = mFrame / time;
          mOldTime = Time.realtimeSinceStartup;
          mFrame = 0;
     }
}
var fps = GetComponent<FpsMeasure>();
GUILayout.Label( fps.FrameRate.ToString( "0.00" ) );

ゲームオブジェクトの一覧

現在、シーン上に存在するゲームオブジェクトの一覧をデバッグメニューで見られるようにしておくと
実機でも不要なゲームオブジェクトが生成されていないかどうかを確認できるようになります
また、一部のオブジェクトが表示されていない場合に

  • activeSelffalseになっているのか
  • 前後関係がおかしくなっているのか
  • そもそもオブジェクトが存在しないのか

といったこともわかるようになります

var list = Object.FindObjectsOfType<GameObject>();
foreach ( var n in list )
{
     GUILayout.Label( n.name );
}

Object.FindObjectsOfType関数を使用することで
Hierarchyから全てのゲームオブジェクトを取得可能ですが
この関数は非アクティブのゲームオブジェクトを取得できません

GameObject.GetComponentsInChildren関数を使用すると
非アクティブなゲームオブジェクトを取得可能なので
次のようにObject.FindObjectsOfType関数と
GameObject.GetComponentsInChildren関数を併用することで
概ねほぼ全てのゲームオブジェクトを取得可能です

var list = Object
     .FindObjectsOfType<GameObject>()
     .Where( c => c.transform.parent == null )
     .SelectMany( c => c.GetComponentsInChildren<Transform>( true ) )
     .Select( c => c.gameObject )
     .ToArray();

あとは表示したいパラメータをデバッグメニューで表示するだけです
例えば親子関係の一覧を表示するだけでも、どのオブジェクトがシーン上に存在するのかが確認できるようになります

using System.Collections.Generic;
using UnityEngine;

public static class GameObjectExtensions
{
     public static string GetPath( this GameObject self )
     {
          var list = new List<GameObject>();
          for ( var n = self.transform.parent; n != null; n = n.parent )
          {
               list.Add( n.gameObject );
          }
          list.Reverse();
          list.Add( self );
          var names = list.Select( c => c.name ).ToArray();
          return string.Join( "/", names );
     }
}

このような拡張メソッドを用意してループ構文で表示するだけで親子関係の一覧が表示できます

foreach ( var n in list )
{
     GUILayout.Label( n.GetPath() );
}

Main Camera
UIRoot
UIRoot/Base
UIRoot/Base/Button
UIRoot/Base/Button/Label

デバッグログの表示

実機でもAndroidであればlogcatを使用すれば、iOSであればXcodeを使用すればログを確認できますが
ログを確認したい時に毎回PCと実機をつなぐのは手間になります

Unity APIのApplication.RegisterLogCallbackを使用することで
出力されるすべてのログを検知することができます

class LogData
{
    public string Condition;
    public string StackTrace;
    public LogType Type;

    public LogData( string condition, string stackTrace, LogType type )
    {
        Condition = condition;
        StackTrace= stackTrace;
        Type = type;
    }
}

List<LogData> mList = new List<LogData>();

void Awake()
{
    Application.RegisterLogCallback( HandleLog );
}

void HandleLog( string condition, string stackTrace, LogType type )
{
    mList.Add( new LogData( condition, stackTrace, type ) );
}

void OnGUI()
{
    foreach ( var n in mList )
    {
        GUILayout.Label( n.Condition );
    }
}
baba_s
株式会社ハ・ン・ドの7年目リードプログラマです。5本リリース経験あり
http://baba-s.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away