Edited at

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


この記事を読む前に

Unity2019.1.5でこの記事で紹介するバグが修正されました。

以下で紹介するバグはUnity2019.1.0~Unity2019.1.4で発生するバグです。


はじめに

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

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

ご覧の通り、圧倒的巨乳の女の子のImageが4つ、Buttonが1つある。Imageは左から順に1,2,3,4である。Imageはそれぞれ別のSpriteが割り当てられており、RectTransformはX座標以外全て同一である。また、以下に示すスクリプトを用意し、ButtonPush関数をButtonを押した時に呼ぶように設定する。


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

ボタン押下1回目及び4n+1回目(nは自然数)は綺麗に表示されているが、それ以外はアスペクト比が明らかにおかしい。常にImages[0]とImages[2]が縦に大きく、Images[1]が横に大きい。

よく観察すると分かるが、ContentSizeFitterコンポーネントがあるにも関わらず画像サイズがボタン押下1回目の画像サイズで固定されてしまっているのだ。

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


色もバグってる

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

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

インスペクターを見る限りではサイズと色がボタンを押すたびに変更されている。しかしゲーム画面ではサイズはさっき示した通り、色は見れば分かる通りButton押下1回目の赤色で固定されてしまっている。

以上の通り、画像のサイズと色で盛大にバグる。

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


バグの回避策

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



  1. spriteに画像を代入する直前にspriteにnullを代入する


  2. spriteにではなくoverrideSpriteに画像を代入する

  3. ImageのtypeをFilledにする


nullを代入する方法

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

Images [i].sprite = null;

Images [i].sprite = Sprites [(ButtonPushCount + i) % Sprites.Length];

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


overrideSpriteを使う方法

overrideSpriteにSpriteを代入した際はこのバグは発生しない。この変数は読んで字の如く、spriteをオーバーライドして使用するSpriteを示す変数である。聞き慣れない人も多いだろうし実際にこれに触れた記事は非常に少なかったが、Unity5の時代からある結構古い変数である。

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

spriteの記述方法まんまである。

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


typeをFilledにする方法

typeをFilledにすることでもバグが回避可能である。インスペクター上では以下の画像のように設定する。

image3.png

fillMethodなどの値は何でも良い。

スクリプトからは以下のようにして設定可能である。

Images [i].type = Image.Type.Filled;


バグの回避処理を行った結果

上記どれかのバグ回避処理を行い、Playを開始してButtonを押したら以下の通りになる。

gif3.gif

おお、バグってない!

色も変更させ、インスペクターも表示した場合は以下のようになる。なお、以下ではoverrideSpriteを使ったバグ回避をしている。

gif4.gif


では既存のコードを書き換えてバグ回避処理を行うべきか?

バグ回避処理が分かったのは良いが、全てのspriteへの画像代入の場面でバグ回避処理を行うべきではない。理由として


  • バグ回避処理追加の作業自体が非常に面倒

  • Unity公式がこのバグを修正した時にバグ回避処理を削除すると、誤って別のコードも削除して別のバグを発生させかねない

  • だからと言って残したままにするとバグ回避処理が無駄な処理として実行されるだけである

なのでコード中のspriteへの画像代入処理を全て書き換えるのではない、別の方法によりバグ回避をした方が良いだろう。

その方法として、ゲーム中の全てのImageオブジェクトを取得し、バグ回避処理を行うコードを書く事が考えられる。具体的には以下に示すようなコードである。


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

こうする事でゲーム中の全てのImageについてバグ回避処理が行われる。Unity公式がバグを修正した時はこのゲームオブジェクトを削除してあげることで万事解決する。ただ、注意点としてLateUpdate関数内でFind関数を呼び出す、極めて重い処理を行っている。毎フレーム重い処理を行っているので、ゲームによってはパフォーマンスが大きく悪化してしまう。


まとめ

Unity2019でImageのspriteに画像を代入すると、画像のサイズと色がバグる。

そのバグを回避するために上のImageBugFixerスクリプトをゲームで最初に読み込まれるシーンに空のオブジェクトを作り、それにアタッチすれば良い。

……ただ、このバグ修正のスクリプトは非常に重いのでUnityをまだ2019にアップデートしていない人はアップデートを見送り、もう2019にアップデートしてしまった人はUnity公式がバグを修正してくれるように報告したり祈ったりすべきだろう。

<追記>

記事の最初に書いた通り、Unity2019.1.5でバグが修正された。

UnityバージョンがUnity2019の人はUnity2019.1.5にアップデートすべきだろう。バグ修正のアプデの度に新しいバグが出るのはUnityではよくある事なので心配な人は時間を置いてからアプデしよう