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

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

* [【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 コーディング編](http://qiita.com/baba_s/items/f2ad850dd7fc84165e96)
* [【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 拡張メソッド編](http://qiita.com/baba_s/items/a0490ea36f6a5bcf8b07)
* [【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 デバッグメニュー編](http://qiita.com/baba_s/items/08cecb26e656900c3bb3)
* [【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 UI編](http://qiita.com/baba_s/items/c8d25456c0ee4ef52f94)
* [【Unity】ゲーム会社でスマホ向けゲームを開発して得た知識 アセットバンドル編](http://qiita.com/baba_s/items/a64b97de1b3cd41460f9)

# UI

今回開発したゲームではNGUIを使用してUIを作成しました
その時に行った開発の効率化や遭遇した問題について書き残しておきます
（今回開発したゲームではNGUI 3.6.8を使用しました）

## 開発の効率化

### TexturePackerのバッチを作成する

NGUIではアトラスで1枚のテクスチャを切り分けて使用することが可能です
また、TexturePackerを使用することで簡単にアトラスを作成できます

ただし、テクスチャが追加されたり変更されたりするたびに
デザイナーさんに毎回TexturePackerを開いてPublishして頂くのは大変だったので
アトラスの作成からUnityプロジェクトへの反映までを自動化するために下記のようなバッチファイルを作成しました

```sh
set folder_path=【アトラス化するテクスチャが格納されたフォルダのパス】
set out_filename=%1

texturepacker %folder_path% --format unity --disable-rotation --trim-mode None --sheet %out_filename%.png --data %out_filename%.txt
pause
```

```sh
texture_packer_publish.bat 【作成したアトラスの出力先】
```

### テクスチャの設定をインポート時に自動で設定する

Unityでは`AssetPostprocessor`クラスを使用することで
テクスチャやオーディオがインポートされた時にアセットの設定を変更することができます

テクスチャの設定をインポート時に自動で設定したい場合は
`AssetPostprocessor`を継承したクラスを作成して`OnPreprocessTexture`関数を定義することで実現可能です

```cs
using UnityEditor;
using UnityEngine;

public class TexturePostprocessor : AssetPostprocessor
{
	private static readonly string[] ASSET_PATH_LIST = 
	{
		"Assets/Resources/2D", 
		"Assets/Resources/LocalData/2D", 
	};

	private void OnPreprocessTexture()
	{
		if ( !ASSET_PATH_LIST.Any( c => assetImporter.assetPath.Contains( c ) ) )
		{
			return;
		}
			
		var importer = assetImporter as TextureImporter;

		importer.filterMode				= FilterMode.Bilinear;
		importer.mipmapEnabled			= false;
		importer.maxTextureSize			= 1024;
		importer.textureFormat			= TextureImporterFormat.ARGB16;
		importer.textureType			= TextureImporterType.Advanced;
		importer.wrapMode				= TextureWrapMode.Clamp;
		importer.npotScale				= TextureImporterNPOTScale.None;
		importer.alphaIsTransparency	= true; // http://d.hatena.ne.jp/nakamura001/20130722/1374499269
	}
}
```

上記の様なスクリプトをUnityプロジェクトの「Editor」フォルダに追加しておくことで
UI用のテクスチャがUnityプロジェクトに追加された時に自動で設定を変更するようにしました

### SceneビューでUIを配置しやすいようにする

![](https://dl.dropboxusercontent.com/u/52568393/ui/0011.png)

Sceneビューの「2D」をオンにするとUIの配置がしやすくなります
また、「Gizmos」から「Camera」のアイコンを選択してオフにすることで
Sceneビューのカメラのアイコンを非表示にできます

### デバッグ用のUI配置

アプリ開発中は所持金や体力などのパラメータを最大値にして動作確認したいことがあったので
デバッグメニューとは別にUI上にチートコマンド実行用のボタンを配置していました

そういったUIがリリース版のアプリで表示されてはいけないので
下記のようなコンポーネントを作成してアタッチしておくことでリリース版のアプリで表示されないようにしました

```cs
using UnityEngine;

public class DebugOnly : MonoBehaviour
{
	private void Awake()
	{
#if RELEASE
		Destroy( gameObject );
#endif
	}
}
```

ただし、このやり方だとリリース版のアプリで表示されなくなるだけでUIの情報自体はアプリに含まれてしまうので
完全にリリース版のアプリに含まれないようにしたい場合は、アプリをビルドする前にすべてのシーンファイルから
特定のコンポーネントがアタッチされているオブジェクトを検索して削除する
下記のようなスクリプトを作成すると良いかなと思います

```cs
﻿using System.Linq;
using UnityEditor;
using UnityEngine;

public static class DebugOnlyObjectDestroyer
{
	[MenuItem( "Tools/Destroy/Debug Only" )]
	private static void DestroyDebugOnly()
	{
		var pathList = EditorBuildSettings.scenes
			.Select( c => c.path )
			.ToArray();

		foreach ( var path in pathList )
		{
			EditorApplication.OpenScene( path );

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

			foreach ( var obj in objectList )
			{
				Object.Destroy( obj );
			}

			EditorApplication.SaveScene();
		}
	}
}
```

### エディタ上でのみ表示したいUI

![](https://dl.dropboxusercontent.com/u/52568393/ui/0004.png)

エディタ上でのみ表示したいUIに関しては
Inspectorでそのオブジェクトの`Tag`を`EditorOnly`にしておくことで
実機に含まれないようにすることが可能です

例えば、UIをUnityエディタ上で配置していく時に
UIのレイアウトの一枚絵をUITextureで表示しながら作業することがあったので
そういった作業用のUIは`Tag`を`EditorOnly`にすることでリリース版のアプリに含まれないようにしました

### 禁則処理

![](https://dl.dropboxusercontent.com/u/52568393/ui/0003.png)

NGUIのUILabelには日本語の禁則処理の機能が備わっていないので
行の先頭に「。」「、」「っ」などの文字が来てしまうことがありました

[unity_ngui_label - FreeStyleWiki](http://ft-lab.ne.jp/cgi-bin-unity/wiki.cgi?page=unity_ngui_label)

こちらのサイト様で公開されているスクリプトを使用することで
「。」「、」「っ」などの文字が行の先頭に来ないように補正をかけることができます

## 改善点

### アトラスのテクスチャサイズ

今回開発したアプリでは古いAndroid端末でも画像が正常に表示されるように
1024x1024を最大サイズとしてアトラスのテクスチャをデザイナーさんに作成していただいたのですが
最近のスマートフォンであれば2048x2048の画像も問題なく表示されるようなので
2048x2048のテクスチャでアトラスを作成することでドローコールが削減できるかなと考えています

参考サイト：[【Unity】モバイルにおけるテクスチャの最大サイズ](http://baba-s.hatenablog.com/entry/2015/02/04/003646)

### UIの配置

今回はUIの配置をUnityエディタ上で行ったのですが
デザイナーさんにUnityやNGUIの使い方を覚えていただくのが少し大変だったので
Photoshopでレイアウトを設定して、それをNGUIに変換できるようなツールを作成したほうが良かったかなと思っています

有料ですがAsset Storeには既に「[PSD Layers to NGUI](https://www.assetstore.unity3d.com/jp/#!/content/11220)」というアセットが公開されているので
これを各プロジェクト用にカスタマイズしてみたいなと考えています
「[ウチの姫さまがいちばんカワイイ](http://www.amebaspgame.com/uchihime/lp/)」ではこのアセットを使用しているそうです
また、Cocos2d-xで開発されたアプリですが、「[消滅都市](http://shoumetsu.wrightflyer.net/)」でもPhotoshopでUIを配置できるようにしたようです

参考サイト様

* [非エンジニアが知ってると得するUnityの知識｜1 pixel｜サイバーエージェント公式クリエイターズブログ](http://ameblo.jp/ca-1pixel/entry-11718229785.html)
* [[CEDEC2014] 消滅都市のつくりかた 半年で素敵なゲームをリリースするには](http://www.slideshare.net/greetech/cedec2014-38854442)

### ボタンの同時押し

Unityエディタ上だと同時押しができないため
ボタンを同時押しても問題ないかどうかを確認するときはいつもアプリを実機に書き出して動作確認していました

「[Unity Remote 4](https://play.google.com/store/apps/details?id=com.unity3d.genericremote&hl=ja)」を使用すればそういった手間も省くことができたかなと思います

## 遭遇した問題

### 表示されないUI

![](https://dl.dropboxusercontent.com/u/52568393/ui/0010.png)

`UIPanel`の`Clipping`が`Soft Clip`で、かつオブジェクトのスケールが均一の値になっていない場合、UIが表示されなくなるので
もし表示されないUIがあるときはスケールの値を確認すると良いかもしれません

### スケール(0, 0, 0)のUIのドローコール

![](https://dl.dropboxusercontent.com/u/52568393/ui/0001.png)

![](https://dl.dropboxusercontent.com/u/52568393/ui/0002.png)

開閉するウィンドウのUIを作成した時に、スケールを(0, 0, 0)にすることで非表示にしていましたが
NGUIではスケール(0, 0, 0)のオブジェクトもドローコールに加算されてしまっていたので
ウィンドウが開く時に`SetActive( true )`を呼び出し
ウィンドウが閉じきった時に`SetActive( flase )`を呼び出すようにしてドローコールを抑えました

### 入力欄の制限

ユーザー名やコメントの入力欄を実装した際に次のような問題に遭遇しました

* 入力できる文字数を制限していなかった
* 改行可能かどうかを制限できていなかった
* モバイルで表示されるソフトウェアキーボードの種類を設定していなかった
* 空文字列が入力された時に代わりに表示される文字列を設定していなかった

#### 入力できる文字数を制限していなかった

![](https://dl.dropboxusercontent.com/u/52568393/ui/0006.png)

`UIInput`の`Character Limit`を設定することで入力できる文字数を制限できます

ただ、今回関わったプロジェクトでは
「全角の場合は最大10文字まで入力できるようにする」
「半角の場合は最大20文字まで入力できるようにする」
という仕様で開発していたので、下記のようなスクリプトを作成して入力できる文字数を制限するようにしました

```cs
public static class StringExtensions
{
	/// <summary>
	/// 文字が全角なら true を、半角なら false を返します
	/// </summary>
	public static bool IsChar2Byte( char c )
	{
		return !( ( c >= 0x0 && c < 0x81 ) || ( c == 0xf8f0 ) || ( c >= 0xff61 && c < 0xffa0 ) || ( c >= 0xf8f1 && c < 0xf8f4 ) );
	}
	
	/// <summary>
	/// バイト数を計算して返します
	/// </summary>
	public static int GetByteCount( this string self )
	{
		int count = 0;
		for ( int i = 0; i < self.Length; i++ )
		{
			if ( IsChar2Byte( self[ i ] ) )
			{
				count++;
			}
			count++;
		}
		return count;
	}
	
	/// <summary>
	/// インスタンスからバイト単位で部分文字列を取得します
	/// </summary>
	public static string SubstringInByte( this string self, int byteCount )
	{
		string tmp = "";
		int count = 0;
		for ( int i = 0; i < self.Length; i++ )
		{
			char c = self[ i ];
			if ( IsChar2Byte( c ) )
			{
				count++;
			}
			count++;
			if ( count > byteCount )
			{
				break;
			}
			tmp += c;
		}
		return tmp;
	}
	
	/// <summary>
	/// <para>インスタンスからバイト単位で部分文字列を取得します</para>
	/// <para>文字数が指定されたバイト数以内の場合はインスタンスをそのまま返します</para>
	/// </summary>
	public static string SafeSubstringInByte( this string self, int byteCount )
	{
		return byteCount < self.GetByteCount() ? self.SubstringInByte( byteCount ) : self;
	}
}
```

```cs
private UIInput mUIInput;

private void Awake()
{
	mUIInput = GetComponent<UIInput>();
	EventDelegate.Add( mUIInput.onSubmit, OnSubmit );
}

private void OnSubmit()
{
	mUIInput.value = mUIInput.value.SafeSubstringInByte( mMaxByteCount );
	mUIInput.label.text = mUIInput.value;
}
```

#### 改行可能かどうかを制限できていなかった

![](https://dl.dropboxusercontent.com/u/52568393/ui/0007.png)

`UIInput`の`Label`に設定されている`UILabel`の`Max Lines`に`1`を設定することで改行できないように制限できます

ただし、これだけだと改行を含む文字列をコピペした場合に改行文字を入力することが可能でした
それを防ぐために改行できない入力欄では下記のようなスクリプトを適用しました

```cs
private UIInput mUIInput;

private void Awake()
{
	mUIInput = GetComponent<UIInput>();
	EventDelegate.Add( mUIInput.onChange, OnChange );
}

private void OnChange()
{
	mUIInput.value = mUIInput.value
		.Replace( "\n", "" )
		.Replace( "\r", "" );
	mUIInput.label.text = mUIInput.value;
}
```

#### モバイルで表示されるソフトウェアキーボードの種類を設定していなかった

![](https://dl.dropboxusercontent.com/u/52568393/ui/0008.png)

`UIInput`の`Mobile Keyboard`を設定することで
実機で表示されるソフトウェアキーボードの種類を変更できます
なので、例えば数値しか入力できない場面では`NumberPad`を設定しておくことで
ユーザーの方がストレスなく数値を入力できるようになるかなと思います

#### 空文字列が入力された時に代わりに表示される文字列を設定していなかった

![](https://dl.dropboxusercontent.com/u/52568393/ui/0009.png)

「ここにあなたの名前を入力してください」のような文言を入力欄の中に表示したい場合
`UIInput`の`Label`に設定されている`UILabel`の`Text`にあらかじめその文字列を設定しておくことで
空文字列が入力された時に代わりに表示できるようになります

### 表示するテキストが動的に変わる`UILabel`

キャラクターやアイテムの説明文のように、マスタで設定する文言を`UILabel`表示する時に
デザイナーさんが想定していた文字数を超えて文言が設定されており、
UIの枠を超えて文言が表示されてしまうことがありました

これを防ぐために、マスタに入力された文言が
想定文字数を超えていないかどうかを確認するチェッカーツールを作成すると共に
表示するテキストが動的に変わる`UILabel`では`Overflow`を`ShrinkContent`にするようにしました

![](https://dl.dropboxusercontent.com/u/52568393/ui/0005.png)

## その他

### 他のスマホゲームでどのようにUIが作られているのかを確認する

[ゲームUIブログ | ゲームUI画像の収集と考察](http://gameui.matme.info/blog/)

こちらのサイト様で多くのスマホゲームのUIを見ることができるので、ゲーム開発中は参考にさせていただきました