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

【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 コーディング編

More than 5 years have passed since last update.

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

nullチェックの回避方法

nullチェックは大変でバグの原因にもなりやすいので
nullチェックを回避する方法を書き残しておきます

変数初期化子で初期化しておく

変数初期化子で空のパラメータを代入したりインスタンス生成したりすることで
メンバ変数がnullかどうかを意識することなくコーディングできます

private string str = string.Empty;
private List<int> list = new List<int>();
private Dictionary<int, int> dict = new Dictionary<int, int>();
private Action act = delegate{};

List<T>Dictionary<TKey, TValue>のメンバ変数は
readonlyにすることで後からnullを代入できなくなるのでさらに安全に扱えます

private readonly List<int> list = new List<int>();
private readonly Dictionary<int, int> dict = new Dictionary<int, int>();

nullもしくは空かどうか」を判断する拡張メソッドを用意する

// 文字列がnullもしくは空かどうか
if ( str == null || str == "" ){}

// 配列がnullもしくは空かどうか
if ( array == null || array.Length == 0 ){}

// リストがnullもしくは空かどうか
if ( list == null || list.Count == 0 ){}

引数やコールバック関数で受け取った文字列やリストが
正常なものかどうかを確認するために上記のようなチェックを毎回書くのは大変ですが
nullもしくは空かどうか」を判断する拡張メソッドを用意しておくと
コーディングしやすくなります

stringの場合

public static bool IsNullOrEmpty( this string self ){
     return string.IsNullOrEmpty( self );
}

上記のような拡張メソッドを用意することで次のように記述できます

if ( str.IsNullOrEmpty() ){}

空白文字も対象外にしたい場合は下記のような拡張メソッドを用意することで

public static bool IsNullOrWhiteSpace( this string self ){
     return self == null || self.Trim() == "";
}

次のように記述できます

if ( str.IsNullOrWhiteSpace() ){}

配列やリストの場合

public static bool IsNullOrEmpty<T>( this IList<T> self ){
     return self == null || self.Count == 0;
}

上記のような拡張メソッドを用意することで次のように記述できます

if ( array.IsNullOrEmpty() ){}
if ( list.IsNullOrEmpty() ){}

ActionデリゲートやFuncデリゲートの場合

Action act = null;
if ( act != null ){
    act();
}
Func<bool> func = null;
if ( func != null ){
    func();
}

ActionデリゲートやFuncデリゲートでイベントを定義したり引数を受け取ったりする場合
上記のように毎回nullチェックを行う必要があります
このようなnullチェックは下記のような拡張メソッドを用意することで省略できます

public static void Call(this Action action){
    if (action != null){
        action();
    }
}

public static void Call<T>(this Action<T> action, T arg){
    if (action != null){
        action(arg);
    }
}

public static void Call<T1, T2>(this Action<T1, T2> action, T1 arg1, T2 arg2){
    if (action != null){
        action(arg1, arg2);
    }
}

public static void Call<T1, T2, T3>(this Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3){
    if (action != null){
        action(arg1, arg2, arg3);
    }
}

public static TResult Call<TResult>(this Func<TResult> func, TResult result = default(TResult)){
    return func != null ? func() : result;
}

public static TResult Call<T, TResult>(this Func<T, TResult> func, T arg, TResult result = default(TResult)){
    return func != null ? func(arg) : result;
}

public static TResult Call<T1, T2, TResult>(this Func<T1, T2, TResult> func, T1 arg1, T2 arg2, TResult result = default(TResult)){
    return func != null ? func(arg1, arg2) : result;
}

public static TResult Call<T1, T2, T3, TResult>(this Func<T1, T2, T3, TResult> func, T1 arg1, T2 arg2, T3 arg3, TResult result = default(TResult)){
    return func != null ? func(arg1, arg2, arg3) : result;
}
Action act = null;
act.Call();
Func<bool> func = null;
func.Call();

関数呼び出しの可読性向上

名前付き引数を使う

引数をたくさん取る関数を呼び出す場合
後から見た時にどの引数が何を示しているのかがわかりづらいです

MessageBox.OpenOkCancel(
    "タイトル",
    "テキスト",
    () => Debug.Log( "OKボタンが押された" ),
    () => Debug.Log( "キャンセルボタンが押された" )
);

名前付き引数を使うことで
後から見た時でもどの引数が何を示しているのかがわかりやすくなります

MessageBox.OpenOkCancel(
    title : "タイトル",
    text : "テキスト",
    onReleasedOkButton : () => Debug.Log( "OKボタンが押された" ),
    onReleasedCancelButton : () => Debug.Log( "キャンセルボタンが押された" )
);

ifディレクティブではなくConditional属性を使う

public static void Log( object message ){
#if DEBUG
     Debug.Log( message );
#endif
}

ifディレクティブを使うと特定のシンボルが定義されている時に
処理を切り替えることができますが、多用するとソースコードの可読性が落ちます

[Conditional( "DEBUG" )]
public static void Log( object message ){
     Debug.Log( message );
}

Conditional属性を使うとソースコードの可読性を保持できます

[Conditional( "DEBUG_A" ), Conditional( "DEBUG_B" )]
public static void Log( object message ){
     Debug.Log( message );
}

いずれかのシンボルが定義されている場合に関数を有効にしたい場合は
上記のようにConditional属性を複数個適用します

#if DEBUG_A && DEBUG_B
#define DEBUG
#endif

...

[Conditional( "DEBUG" )]
public static void Log( object message ){
     Debug.Log( message );
}

すべてのシンボルが定義されている場合に関数を有効にしたい場合は
上記のようにソースコードの先頭でdefineを使用します

デバッグ出力を楽にする

JSONを使う

クラスの情報をログに出力したいことはよくあります

  • プレイヤーの情報
  • 所持ユニットの一覧
  • 所持アイテムの一覧
  • フレンドの一覧
  • マスタの情報
  • etc.
public class PlayerData
{
     public int Id;
     public string Name;

     public override string ToString()
     {
          var builder = new StringBuilder();
          builder.AppendFormat( "Id:{0},", Id );
          builder.AppendFormat( "Name:{0}", Name );
          return builder.ToString();
     }
}

しかしすべてのクラスでこのようなToString関数を定義するのは大変です
こんな時はJSONを使用することをオススメします

LitJSON

  1. http://lbv.github.io/litjson/ にアクセスする
  2. 「Download>File releases>Latest release>dll」を選択してDLLをダウンロードする
  3. ダウンロードした「LitJson.dll」をUnityプロジェクトのPluginsフォルダに追加する
  4. 次のようなクラスを作成する
using LitJson;

public static class JsonManager
{
    static JsonManager()
    {
        JsonMapper.RegisterExporter<float>( (obj, writer) => writer.Write( Convert.ToDouble( obj ) ) );
        JsonMapper.RegisterImporter<double,float>( input => Convert.ToSingle( input ) );
        JsonMapper.RegisterImporter<int,long>( input => Convert.ToInt64( input ) );
    }
    public static string ToJson<T>( T obj, int indentLevel = 4 )
    {
        var builder = new StringBuilder();
        var writer = new JsonWriter( builder );
        writer.PrettyPrint = true;
        writer.IndentValue = indentLevel;
        JsonMapper.ToJson( obj, writer );
        return builder.ToString();
    }
}

後は文字列としてデバッグメニューに出力したいクラスのインスタンスを
JsonManager.ToJsonでJSON形式の文字列に変換します

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

Unity APIのクラスのインスタンスをJSON形式の文字列に変換することも可能です

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

独自に定義したクラスの場合はToString関数をオーバーライドしておくと便利です

public class PlayerData
{
     public int Id;
     public string Name;

     public override string ToString()
     {
          JsonManager.ToJson( this );
     }
}
var obj = new PlayerData();
Debug.Log( obj );

処理速度の向上

StringBuilderクラスで文字列連結する

var str = "";
for ( int i = 0; i < 1000; i++ ){
     str += i.ToString();
}
Debug.Log( str );

通常のstringクラスの文字列連結は処理が遅く、さらにGCが発生してしまいます
StringBuilderクラスを使用することで処理速度を速くし、GCの発生を抑えられます

var builder = new StringBuilder();
for ( int i = 0; i < 1000; i++ ){
     builder.Append( i.ToString() );
}
Debug.Log( builder.ToString() );

何もしないDebugクラスを定義する

UnityのDebug.Logはリリース版でもログを出力してしまいます
そのため、例えばAndroidでlogcatすると製品版でもログの出力が見れてしまいます
また、ログ出力は重たい処理なのでゲームの処理速度も遅くなってしまいます

リリース版でログを出力しないようにしたい場合は
次のように、特定のシンボルが定義されている場合にのみ
何もしないDebugクラスをグローバルな名前空間に定義されるようにします

#if RELEASE

using System.Diagnostics;
using UnityEngine;

public static class Debug
{
     [Conditional("RELEASE")] public static void Break(){}
     [Conditional("RELEASE")] public static void ClearDeveloperConsole(){}
     [Conditional("RELEASE")] public static void DebugBreak(){}
     [Conditional("RELEASE")] public static void DrawLine(Vector3 start, Vector3 end){}
     [Conditional("RELEASE")] public static void DrawLine(Vector3 start, Vector3 end, Color color){}
     [Conditional("RELEASE")] public static void DrawLine(Vector3 start, Vector3 end, Color color, float duration){}
     [Conditional("RELEASE")] public static void DrawLine(Vector3 start, Vector3 end, Color color, float duration, bool depthTest){}
     [Conditional("RELEASE")] public static void DrawRay(Vector3 start, Vector3 dir){}
     [Conditional("RELEASE")] public static void DrawRay(Vector3 start, Vector3 dir, Color color){}
     [Conditional("RELEASE")] public static void DrawRay(Vector3 start, Vector3 dir, Color color, float duration){}
     [Conditional("RELEASE")] public static void DrawRay(Vector3 start, Vector3 dir, Color color, float duration, bool depthTest){}

     [Conditional("RELEASE")] public static void Log(object message){}
     [Conditional("RELEASE")] public static void Log(object message, UnityEngine.Object context){}
     [Conditional("RELEASE")] public static void LogError(object message){}
     [Conditional("RELEASE")] public static void LogError(object message, UnityEngine.Object context){}
     [Conditional("RELEASE")] public static void LogException(System.Exception exception){}
     [Conditional("RELEASE")] public static void LogException(System.Exception exception, UnityEngine.Object context){}
     [Conditional("RELEASE")] public static void LogWarning(object message){}
     [Conditional("RELEASE")] public static void LogWarning(object message, UnityEngine.Object context){}
}
#endif

このようなDebugクラスを定義しておき
リリース時は特定のシンボルを定義してからビルドすることで
リリース版でログ出力を無効化できます

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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした