【Unity】Unity2019におけるImageのSprite変更で生じる深刻なバグとその回避方法

#この記事を読む前に
<font color="Red">**Unity2019.1.5でこの記事で紹介するバグが修正されました。**</font>
<font color="Red">**以下で紹介するバグはUnity2019.1.0～Unity2019.1.4で発生するバグです。**</font>

#はじめに
UnityでUI.Imageを使う時はスクリプトからSprite、つまり画像を変更させる場面が多々ある。その時は以下のようなコードで画像を変更させていただろう。

```C#
public Image Image1;
public Sprite Sprite1;

private void Hoge ()
{
	Image1.sprite = Sprite1;
}
```

上記のコードは何の捻りも無いコードであり、ImageのSpriteを問題無く変更可能である。
**Unity2018までは。**
残念な事にUnity2019では上記のコードでは画像の表示でバグが生じる。

本記事では`sprite`にSpriteを代入した際に生じるバグとその回避方法について紹介する。
なお、紛らわしさ回避のためにImageの変数である`sprite`は背景をこのように灰色にして表示し、グラフィックスオブジェクトを示すSpriteは先頭を大文字にして背景無し、または単に画像と示す。

#`sprite`に画像を代入すると発生するバグについて
##バグの検証環境
Unity2019で`sprite`を使用して画像を変えるとどんなバグが発生するのか説明する。筆者のUnityバージョンは**Unity2019.1.4**である。
まず、ゲーム画面を以下のように設定する。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/a1370384-fde2-886f-3cd8-8b860b1bef3a.png)
ご覧の通り、~~圧倒的巨乳の~~女の子のImageが4つ、Buttonが1つある。Imageは左から順に1,2,3,4である。Imageはそれぞれ別のSpriteが割り当てられており、RectTransformはX座標以外全て同一である。また、以下に示すスクリプトを用意し、ButtonPush関数をButtonを押した時に呼ぶように設定する。

```C#:ImageTest
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ImageTest : MonoBehaviour
{
	public Image [] Images;
	public Sprite [] Sprites;

	private int ButtonPushCount;//ボタンが押された回数

	public void ButtonPush ()
	{
		ButtonPushCount++;
		for (int i = 0; i < Images.Length; i++)
		{
			//Sprite変更
			Images [i].sprite = Sprites [(ButtonPushCount + i) % Sprites.Length];
		}
	}
}

```

Imagesには4つのImageを左から順に代入し、Spritesは4つのImageの`sprite`に最初からアタッチされているSpriteを左から順に代入する。各Spriteのサイズなどを以下の表に示す。

|Sprite|上の画像での位置|サイズ(横px*縦px)|画像名|
|---|---|---|---|
|Sprites[0]|一番左|368*600|Yuyuko|
|Sprites[1]|左から二番目|388*702|Suwako_1|
|Sprites[2]|右から二番目|614*598|Mima|
|Sprites[3]|一番右|292*700|Keine_1|

ButtonPushが呼ばれた時、つまりボタンを押した時はImageの`sprite`が左に送られる動作をする。
##画像のアスペクト比がバグる
Playを開始してButtonを押したら以下の通りになる。
![gif1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/f80c97c2-9db6-ef65-78d3-cc87efac5afc.gif)
ボタン押下1回目及び4n+1回目(nは自然数)は綺麗に表示されているが、それ以外はアスペクト比が明らかにおかしい。常にImages[0]とImages[2]が縦に大きく、Images[1]が横に大きい。
よく観察すると分かるが、ContentSizeFitterコンポーネントがあるにも関わらず**画像サイズがボタン押下1回目の画像サイズで固定されてしまっているのだ。**

今回のスクリプトに限らず、(筆者が確認した限りでは)**どのような状況においてもこのバグが発生した。**

##色もバグってる
`sprite`では画像のサイズだけでなく色も固定されてしまう。それも確認するため、上記のスクリプトのButtonPush関数に以下に示すように色を変化させる機能も追加する。

```C#
public void ButtonPush ()
{
	ButtonPushCount++;
	for (int i = 0; i < Images.Length; i++)
	{
		//Sprite変更
		Images [i].sprite = Sprites [(ButtonPushCount + i) % Sprites.Length];

		//色変更
		switch (ButtonPushCount % Sprites.Length)
		{
		case 0:
		default:
			Images [i].color = new Color (0.5f, 0.5f, 0.5f, 0.5f);
			break;
		case 1:
			Images [i].color = Color.red;
			break;
		case 2:
			Images [i].color = Color.green;
			break;
		case 3:
			Images [i].color = Color.blue;
			break;
		}
	}
}
```

Play画面はこちら。今回はインスペクターも見えるようにした。
![gif2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/670cec13-a2b9-8f40-b6f2-e477925189b5.gif)
インスペクターを見る限りではサイズと色がボタンを押すたびに変更されている。しかしゲーム画面ではサイズはさっき示した通り、色は見れば分かる通りButton押下1回目の赤色で固定されてしまっている。

以上の通り、画像のサイズと色で盛大にバグる。
画像の表示というのはどのゲーム(特にソシャゲやギャルゲーなどのキャラゲー)において最も重要な要素の1つだと考えられるので、このバグは**極めて深刻なバグ**であると言えよう。

#バグの回避策
このバグの回避策を探した所、以下の処理のどれかを行う事でバグの回避が可能であると分かった。

 1. `sprite`に画像を代入する直前に`sprite`にnullを代入する
 2. `sprite`にではなく`overrideSprite`に画像を代入する
 3. Imageの`type`をFilledにする

##nullを代入する方法
nullを代入してから画像を代入する事でバグを回避可能である。

```C#
Images [i].sprite = null;
Images [i].sprite = Sprites [(ButtonPushCount + i) % Sprites.Length];
```

他の方法と異なりバグの温床にもならないので、原則としてこれを使うべきだろう。

##overrideSpriteを使う方法
[overrideSprite](https://docs.unity3d.com/ja/2019.1/ScriptReference/UI.Image-overrideSprite.html)にSpriteを代入した際はこのバグは発生しない。この変数は読んで字の如く、`sprite`をオーバーライドして使用するSpriteを示す変数である。聞き慣れない人も多いだろうし実際にこれに触れた記事は非常に少なかったが、[Unity5](https://docs.unity3d.com/ja/530/ScriptReference/UI.Image-overrideSprite.html)の時代からある結構古い変数である。

```C#
Images [i].overrideSprite = Sprites [(ButtonPushCount + i) % Sprites.Length];
```

`sprite`の記述方法まんまである。
注意として、`overrideSprite`はインスペクター上から変更できず、**`overrideSprite`にnullを代入した場合は`sprite`の画像が表示される。**

##typeをFilledにする方法
typeをFilledにすることでもバグが回避可能である。インスペクター上では以下の画像のように設定する。
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/3135401f-4806-0eab-388e-76692066259d.png)
fillMethodなどの値は何でも良い。
スクリプトからは以下のようにして設定可能である。

```C#
Images [i].type = Image.Type.Filled;
```

##バグの回避処理を行った結果
上記どれかのバグ回避処理を行い、Playを開始してButtonを押したら以下の通りになる。
![gif3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/cbe2152b-e7cd-3474-9a44-c604a2b048ae.gif)
**おお、バグってない!**
色も変更させ、インスペクターも表示した場合は以下のようになる。なお、以下では`overrideSprite`を使ったバグ回避をしている。
![gif4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/33367720-7581-9057-d9d1-418ed4e16ab4.gif)

#では既存のコードを書き換えてバグ回避処理を行うべきか?
バグ回避処理が分かったのは良いが、全ての`sprite`への画像代入の場面でバグ回避処理を行うべきではない。理由として

- バグ回避処理追加の作業自体が非常に面倒
- Unity公式がこのバグを修正した時にバグ回避処理を削除すると、誤って別のコードも削除して別のバグを発生させかねない
- だからと言って残したままにするとバグ回避処理が無駄な処理として実行されるだけである

なのでコード中の`sprite`への画像代入処理を全て書き換えるのではない、別の方法によりバグ回避をした方が良いだろう。
その方法として、ゲーム中の全てのImageオブジェクトを取得し、バグ回避処理を行うコードを書く事が考えられる。具体的には以下に示すようなコードである。

```C#:ImageBugFixer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ImageBugFixer : MonoBehaviour
{
#if UNITY_2019_1
	private static ImageBugFixer instance = null;//シングルトン
	private Sprite sprite;//一時キャッシュ用

	private void Awake ()
	{
		if (instance == null)
		{
			instance = this;
		}
		else if (instance != this)
		{
			Destroy (gameObject);
		}
		DontDestroyOnLoad (gameObject);
	}

	//Update、FixedUpdate、OnRenderObjectだと修正前の画像が一瞬見えてしまう(特にコルーチン)
	private void LateUpdate ()
	{
		foreach (Image img in FindObjectsOfType<Image>())
		{
			//シーン上に存在し、spriteが割り当てられているか
			if (img.gameObject.activeInHierarchy && img.sprite != null)
			{
				sprite = img.sprite;
				img.sprite = null;
				img.sprite = sprite;
			}
		}
	}
#else
	private void Awake ()
	{
		Debug.Log ("ImageBugFixerはUnity2019.1のみ動作します");
		Debug.Log ("現在のバージョンでも動作させたい場合はImageBugFixer.csの8行目を「#if UNITY_2019_2」などに変更してください");
		Destroy (gameObject);
	}
#endif
}

```

使い方として、ゲーム中に最初に読み込まれるシーンに空のゲームオブジェクトを作り、それに上記のスクリプトをアタッチするだけである。
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318712/422acf0f-c60d-81d4-f550-9aa4b93e1325.png)
こうする事でゲーム中の全てのImageについてバグ回避処理が行われる。Unity公式がバグを修正した時はこのゲームオブジェクトを削除してあげることで万事解決する。ただ、注意点として**LateUpdate関数内でFind関数を呼び出す、極めて重い処理を行っている。**毎フレーム重い処理を行っているので、ゲームによってはパフォーマンスが大きく悪化してしまう。


#まとめ
Unity2019でImageの`sprite`に画像を代入すると、画像のサイズと色がバグる。
そのバグを回避するために上の`ImageBugFixer`スクリプトをゲームで最初に読み込まれるシーンに空のオブジェクトを作り、それにアタッチすれば良い。

……ただ、このバグ修正のスクリプトは非常に重いのでUnityをまだ2019にアップデートしていない人はアップデートを見送り、もう2019にアップデートしてしまった人はUnity公式がバグを修正してくれるように報告したり祈ったりすべきだろう。
<追記>
記事の最初に書いた通り、**Unity2019.1.5**でバグが修正された。
UnityバージョンがUnity2019の人はUnity2019.1.5にアップデートすべきだろう。~~バグ修正のアプデの度に新しいバグが出るのはUnityではよくある事なので心配な人は時間を置いてからアプデしよう~~