2年目のゲームプログラマがゲーム会社でUnityを使用して
スマートフォン向けのゲームを開発して感じたことをまとめておきます
- 【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 コーディング編
- 【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 拡張メソッド編
- 【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 デバッグメニュー編
- 【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 UI編
- 【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
- http://lbv.github.io/litjson/ にアクセスする
- 「Download>File releases>Latest release>dll」を選択してDLLをダウンロードする
- ダウンロードした「LitJson.dll」をUnityプロジェクトのPluginsフォルダに追加する
- 次のようなクラスを作成する
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
クラスを定義しておき
リリース時は特定のシンボルを定義してからビルドすることで
リリース版でログ出力を無効化できます