エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

UnityにおけるiOS版のアプリサイズの削減

AppStoreでは100MBを超えるとLTEでアプリをダウンロードできなくなります。ダウンロード数を増やすには、アプリサイズ(圧縮後)を100MB以下に抑える必要があります。

しかし、Unityを使用して開発していると、iOSのアプリサイズがAndroidよりも大幅に大きくなります。その原因の多くは、テクスチャの容量です。

Androidでアルファ付き画像を使用すると、デフォルトでETC2+ALPHAが選択され、1画素8bitになります。また、非正方形の画像も4x4画素の倍数であれば圧縮可能です。対して、iOSの場合はデフォルトでPVRTC 4bitが選択されますが、PVRTCは正方形かつ2の乗数しか扱えないため、往々にしてRGBA無圧縮が選択され、1画素32bitになります。その結果、iOSのアプリサイズがAndroidの4倍になります。

この問題を解決するには、UnityのSpritePackerを使用して、2の乗数の解像度に複数の画像をまとめる必要があります。具体的に、テクスチャのSprite ModeにPolygonに指定した上で、Packing Tagを指定すると、自動的にパッキングすることができます。

Packingによって、テクスチャの解像度は正方形になります。その結果、PVRTCでの圧縮が有効になります。アルファ値が存在しない場合は、PVRTC 4bitでも十分な画質が確保できるため、これだけで問題は解消です。しかし、UI画像など、アルファ値が存在する場合は、PVRTC 4bitが非常に汚いため、実用的ではありません。

アルファ値が存在する画像を使用する場合は、iOSのTexture CompressionでASTC 4x4を指定します。これを使用すると、1画素8bitになり、とても綺麗です。しかし、ASTC 4x4はiPhone6以降にのみ対応しており、iPhone5SではランタイムでRGBA無圧縮に展開されるため、1枚につき数百ミリ秒程度の展開コストがかかります。そのため、全ての画像にASTC 4x4を使用すると非常に重いです。そのため、全てのUI画像に適用するのではなく、Sprite Packingする画像にのみ適用することを推奨します。

推奨設定:
・画像は4x4の倍数で作成する (ETC2およびASTCの制約)
・テクスチャはSpritePackerでまとめる (PVRTCを使用できるようにする)
・アルファなしの場合はPVRTCを使用する (iPhone5Sのパフォーマンス向上のため)
・アルファ付きの場合はASTC 4x4を使用する (画質を上げるため)

注意:
・iPhone5SではASTCがソフトデコードになるので使いすぎない

YOLOの出力ベクトルからバウンディングボックスを計算

YOLOは、シンプルなCNNでオブジェクトのカテゴリとバウンディングボックスを検出することができるネットワークです。概要はChainerでYOLOからどうぞ。

YOLOの入力は448x448ピクセルの画像を、RRR...GGG...BBB...で並べて与えます。その際、入力画素のレンジは+-1.0に正規化する必要があるので、RGBが8bitの場合は、normalizedRGB=RGB/255.0f*2-1.0fとします。また、Bitmapの上下のスキャン順にも注意します。

対して、YOLOの出力は1470次元のベクトルであり、バウンディングボックスを計算するには、やや複雑な計算が必要です。

バウンディングボックスの計算方法はYOLOtiny_chainer/predict.pyのコードを参考にさせて頂きました。

YOLOの出力は、順に以下の項目で構成されます。

  probs=ans[0:980].reshape((7,7,20))     # class probabilities
  confs=ans[980:1078].reshape((7,7,2))   # confidence score for Bounding Boxes
  boxes = ans[1078:].reshape((7,7,2,4))  # Bounding Boxes positions (x,y,w,h)


probsは64x64ピクセルを1グリッドとして、画像を7x7個のグリッドに区切ったそれぞれで、以下の20カテゴリの推定確率が入っています。

  classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
             "bus", "car", "cat", "chair", "cow",
             "diningtable", "dog", "horse", "motorbike", "person",
             "pottedplant", "sheep", "sofa", "train","tvmonitor"]


confsには7x7個のグリッドにつき、2つのバウンディングボックスの信頼度が入っています。

boxesには7x7個のグリッドの2つのバウンディングボックスの座標が、(x,y,w,h)の順に入っています。wとhはsqrtがかかっています。そのため、有効なバウンディングボックスの座標は以下で計算することができます。

for(int by=0;by<7;by++){
  for(int bx=0;bx<7;bx++){
    for(int box=0;box<2;box++){
       for(int category=0;category<20;category++){
         if(confs[by][bx][box]*probs[by][bx][category]>0.1f){
           x= (bx+boxes[by][bx][box][0])*(448/7);
           y = (by+boxes[by][bx][box][1])*(448/7);
           w = pow(box[by][bx][box][2],2)*448;
           h = pow(box[by][bx][box][3],2)*448;
           x1 = x - w/2;
           x2 = x + w/2;
           y1 = y - h/2;
           y2 = y + h/2;
           class = classes[category];
        }
     }
  }
}


尚、このままでは、1つのオブジェクトに対して複数のバウンディングボックスが描画されてしまいます。この問題を解決するには、You Only Look Once:Unified, 統合されたリアルタイムオブジェクトの検出を参考に、計算したバウンディングボックスに対してNon-Maximum suppressionを適用する必要があります。簡易的には、バウンディングボックスを確率順でソートして、重なっているバウンディングボックスを削除してもよいかと思います。

BigQueryのStreamingAPIで日付分割する

AppEngineからBigQueryにinsertする際、templateSuffixに日付を指定すると、TABLE_IDのSchemaを使用して、自動的にテーブルを日付分割することができます。

    SUFFIX_ID = "_"+datetime.datetime.now().strftime("%Y%m%d")
    body = { 'rows':[{'json':data_hash}] ,'ignoreUnknownValues':True ,'templateSuffix':SUFFIX_ID}
...
    response = bigquery.tabledata().insertAll(projectId=PROJECT_ID,
                                              datasetId=DATASET_ID,
                                              tableId=TABLE_ID,
                                              body=body).execute()

ディープラーニングの重みを圧縮するDeep Compression

DEEP COMPRESSION: COMPRESSING DEEP NEURAL NETWORKS WITH PRUNING, TRAINED QUANTIZATION AND HUFFMAN CODING

ディープラーニングの重みの圧縮に関する論文です。Network pruning(刈り込み)と非線形量子化によってAlexNetの係数を240MBから6.9MBまで圧縮します。

以下、論文からアーキテクチャを意訳してみました。

architecture


Network Pruning

Network pruningはCNNモデルの圧縮において広く利用されています。1989年にLeCunによって、ネットワークの複雑さの削減とオーバーフィッティングのために有効であると証明されました。また、2015年にHanによって、近代のState-of-the-artなCNN modelsに対して適用されました。

Pruningでは、まず、connectivityを通常のネットワークトレーニングによって学習します。次に、小さい重みのconnectionを刈り込みます。全てのしきい値以下の重みがネットワークから取り除かれます。最後に、残っているスパースなconnectionに対して、再学習を行います。Pruningによって、AlexNetのパラメータは1/9〜1/13まで削減されます。

Pruningによってスパースになった重みは、圧縮行格納方式 CSR(compressed sparse row)もしくは圧縮列格納方式 CSC(compressed sparse column)フォーマットで符号化します。その結果、2a+n+1個の値が残ります。aは非0の要素数、nはrowかcolumnの数です。

さらに圧縮するため、非0重みの出現位置を、絶対位置ではなく相対位置インデックスで符号化します。畳み込み層に対しては8bit、全結合層に対しては5bitで符号化します。8bitもしくは5bitを超えた値が出現した場合、0を符号化することで、残差が8bitもしくは3bitを超えないようにします。

padding


Trained Quantization and Weight Sharing

ネットワークの量子化と重みの共有化によって、刈り込まれたネットワークの重みをさらに圧縮します。保存すべき複数のconnectionで共有される効果的な重みを見つけ、共有された重みをfine-tuneします。

quantization


Weight Sharingの概念を図3に示します。4入力、4出力のニューロンがあり、重みは4x4行列となっています。左上が係数の行列、左下が勾配の行列です。重みは量子化され、4つのbinsに区分けされます。各binを色分けして示しています。同じbinに格納された重みは、共通の重み(セントロイド)に量子化されます。その結果、重みは共有された重みテーブルのインデックスとなります。updateによって、勾配はグループごとに平均化され、学習率を乗じ、セントロイドを更新します。

刈り込みされたAlexNetにおいて、1つの畳込み層につき、重みは256 shared weightsまで非線形量子化され、各重みは8bitとなります。また、1つの全結合層につき、重みは32 shared weightsまで非線形量子化され、各重みは5bitになります。

quantized_result


量子化によって、4x4行列が、4セントロイドと2bitインデックスになるため、データサイズは1/3まで削減されます。

ハフマン符号化

量子化された重みと、疎行列のインデックスをハフマン符号化することで、データサイズは20%〜30%削減されます。

LoadSceneMode.AdditiveによるuGUIのメモリ削減

UnityではPrefabのネストが使用できないため、複雑なUIのシーンでは、シーン内に多くのuGUI要素が含まれることになります。uGUIのテクスチャは、クオリティの問題から、高解像度かつRGBA32で格納されることが多いため、すぐにメモリ不足になります。

この問題は、UIごとに個別のシーンを作成し、LoadSceneMode.Additiveを使用することで解決することができます。

//シーンの追加読み込み
UnityEngine.SceneManagement.SceneManager.LoadScene( additive_scene, UnityEngine.SceneManagement.LoadSceneMode.Additive );
before_scene=additive_scene;

//シーンの破棄
if(before_scene!=""){
	SceneManager.UnloadSceneAsync(before_scene);
	before_scene="";
}

シーンを追加読み込みした場合も、各シーンからGameObject.Findを行い、シーン間のオブジェクトを参照することができます。また、シーンの追加読み込みは、iOS、Android共に数100ms程度で行うことができるため、通常のシーン読み込みが数秒かかることに比べると、かなり高速です。

ただし、UnityEngine.SceneManagement.LoadSceneMode.AdditiveはブロッキングAPIにもかかわらず、シーンの読み込みが1フレーム遅延するという問題があります。そのため、LoadSceneを実行した直後にGameObject.Findすると失敗するので、注意する必要があります。

UnityのIAPButtonの異常系の対策

UnityのIAPButtonには以下の2つの問題があります。

(1) たまにボタンを押しても反応しない
(2) 連打すると複数の決済が走る

(1)が起きる場合、IAPButton.IAPButtonStoreManager.InitiatePurchaseにおいて、controllerがnullになっており、Null Pointer Exceptionが発生しています。これは、IAPButtonがActiveになったタイミングでストアへの問い合わせが発生しており、問い合わせが完了するまでIAPButtonが有効でないことに起因しています。しかし、このエラーを通知するコールバックは存在しないため、IAPButton.csを書き換えず、クライアントからcontroller==nullを検出するには、GetProductの戻り値をnullチェックするのが良さそうです。

(2)は、Editorでは即時にストアが立ち上がるのですが、iOSではストアの立ち上がりが遅延するため、その間にもう一回同じボタンが押せてしまうのが問題です。IAPButtonにはonPurchaseCompleteもしくはonPurchaseFailedしかなく、onPurchaseStartが存在しません。IAPButton.csを書き換えず、クライアントから購入の開始を検出するには、Button側にAddListenerすると良さそうです。ButtonのonClickでis_purchasingフラグを設定して、他のUIを無効化するアプローチです。尚、その際、(1)の対策をしていないと、異常系で全くUIが押せなくなり、悲惨なことになるので注意です。

以上を踏まえて、以下のような実装が良さそうです。

button.GetComponent<Button>().onClick.AddListener(
	() => {
		if(UnityEngine.Purchasing.IAPButton.IAPButtonStoreManager.Instance.GetProduct(cash.GetComponent<UnityEngine.Purchasing.IAPButton>().productId)==null){
			//ストア情報の取得に失敗するなど、IAPの初期化に失敗した場合、
			//PurchaseFailureEventが呼ばれないまま、NullPointerExceptionになるので、
			//自前でケアする必要がある
			invalid_state(info.StoreID);
		}else{
			//ボタンの連打を防ぐ
			button.GetComponent<Button>().interactable=false;
			is_purchasing=true;
		}
	}
);

button.gameObject.GetComponent<UnityEngine.Purchasing.IAPButton>().onPurchaseComplete.AddListener(
(UnityEngine.Purchasing.Product product) => {
	success(product);
}
);

button.gameObject.GetComponent<UnityEngine.Purchasing.IAPButton>().onPurchaseFailed.AddListener(
(UnityEngine.Purchasing.Product product,UnityEngine.Purchasing.PurchaseFailureReason reason) => {
	button.GetComponent<Button>().interactable=true;	//購入キャンセルした場合はボタンを有効化
	is_purchasing=false;
	
	failed(product,reason);
}
);


尚、アプリ決済直後にアプリを終了した場合、onPurchaseCompleteが呼ばれませんが、次回のアプリ起動時、IAPButtonがActiveになった際に、onPurchaseCompleteが呼ばれます。ただし、IAPButton.csのPurchaseProcessingResultはデフォルトで以下のような実装になっています。TODOに記載されているように、このままの実装では、決済に失敗したIAPButtonがInactiveの際に、別のIAPButtonを表示した場合に、復帰処理が正しく行われず、IAPButtonに設定したPurchaseCompleteコールバックが呼ばれないまま、IAPがCOMPLETEしてしまいます。

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
	foreach (var button in activeButtons) {
		if (button.productId == e.purchasedProduct.definition.id) {
			return button.ProcessPurchase(e);
		}
	}
	return PurchaseProcessingResult.Complete; // TODO: Maybe this shouldn't return complete
}

この問題を回避するために、PurchaseProcessingResult.Pendingを返すことで、決済の復帰シーケンスで対象のIAPButtonが有効になる前にCompleteが呼ばれるのを防止した方がよいです。Pendingを返しておくと、次回のアプリ起動時に、再度、決済の終了処理が走ります。

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
	foreach (var button in activeButtons) {
		if (button.productId == e.purchasedProduct.definition.id) {
			return button.ProcessPurchase(e);
		}
	}
	return PurchaseProcessingResult.Pending;
}

シーンロード直後のタップイベントを無効化する

UnityのEventSystemはシーンロード中のタップが、シーンロード直後に発火する問題があります。そのため、ロード中にタップを連打していると、次のシーンに遷移した直後に大量のタップイベントが発生します。この問題を解消するには、EventSystemに以下のようにスクリプトを当て、シーン開始直後、一定期間はEventSystemを無効化する必要があります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class EventSystemUpdater : MonoBehaviour {
	private float t;

	private const float INVALIDATE_T=0.33f;

	void Awake () {
		t=0.0f;
		GetComponent().enabled=false;
	}

	// Use this for initialization
	void Start () {
	}
	
	// Update is called once per frame
	void Update () {
		t=t+Time.deltaTime;
		if(t>=INVALIDATE_T){
			if(GetComponent().enabled==false){
				GetComponent().enabled=true;
			}
		}
	}
}

Firebaseに格納した進行度をBigQueryで集計する

Firebaseのカスタムイベントに対して、カスタムキーでイベントを複数回送信すると、1ユーザに対して複数のイベントがレコードに登録されます。このデータを、FirebaseからBigQueryに転送した後、Redashで解析することを考えます。

このイベントが進行度の場合で、カスタムキーに対してint値を保つ場合に、各進行度に対して、何人のユーザがいるかを計測したい場合があります。その際、そのままカウントしてしまうと、1ユーザが複数回イベントを発行してしまっているため、ユーザ数が実際のユーザ数よりも多くなってしまいます。1ユーザが複数のイベントを発行していた場合、ユーザごとに最大の進行度を計算してから、カウントしたいです。

この目的を達成するには、以下のようなクエリを実行します。GROUP BYで user_dim.app_info.app_instance_idを指定して、MAX(event_dim.params.value.int_value) でまとめた後、COUNTします。

SELECT 
  some_int_value,
  EXACT_COUNT_DISTINCT( user_dim.app_info.app_instance_id ) as users
FROM
(
SELECT user_dim.app_info.app_instance_id,
       MAX(event_dim.params.value.int_value) AS some_int_value
FROM [xxx-xxx:xxx_ANDROID.app_events_xxx]
WHERE event_dim.name = "some_event_name"
  AND event_dim.params.key = "some_event_key"
GROUP BY user_dim.app_info.app_instance_id
)
GROUP BY
  1
ORDER BY 
  some_int_value

また、複数の日付のテーブルを結合して集計する場合は、FROMを以下のように書き換えます。

FROM 
(TABLE_DATE_RANGE([xxx-xxx:xxx_ANDROID.app_events_], 
                    TIMESTAMP('2017-07-04'), 
                    TIMESTAMP('2017-07-09')))

参考:BigQuery と Firebase Analytics によるモバイル アプリのカスタム分析

Gear VR 2016年版と2017年版の比較

Gear VR 2016年版(SM-R323)と2017年版(SM-R324)を比較してみました。

IMG_2323


こちらが2016年版です。アタッチメントが黒です。Galaxy S8は指で押すと、若干ですが、上下に動きます。

IMG_2322


こちらが2017年版です。アタッチメントが灰色です。Galaxy S8は、かっちりと固定され、指で押しても上下に動きません。

IMG_2321


当初は、USB-Cアタッチメントの差だけかと思っていたのですが、2017年版のUSB-Cアタッチメントを2016年版に装着してみても、上下のゆらぎは改善しませんでした。どうやら、スマートフォンを上下から挟み込む3連の出っ張りの高さも違うようです。

結論として、2016年版と2017年版は、筐体もアタッチメントも差分があるため、2017年版を購入した方がよいようです。

Galaxy S8でGear VRの熱問題は解決したか

Gear VRの最大の問題は熱です。Galaxy S6で動画を見ていると、15分程度で熱の警告が表示され、フレームレートが大きく低下します。

この問題は、Galaxy S6をファンで冷却することで改善することができます。具体的には、扇風機の前でプレイするか、サンワダイレクトのスマートフォンクーラーで冷却することで、改善します。

IMG_2300


しかし、サンワダイレクトのスマートフォンクーラーは、ホールド力が弱く、うまくGear VRに固定できず、ストラップを通して無理やり固定したため、視力調整が難しくなってしまいました。また、クーラー自体もMicro USBで充電しないといけないのが不便です。その結果、ほとんど使わず、扇風機で対処していました。

Galaxy S8では、SOCのプロセスの進化の恩恵で、劇的に熱問題が改善しています。以下の記事では、GearVRをサーマルカメラで計測したところ、同条件で、Galaxy S7が50度、Galaxy S8が39度まで改善することが示されています。

Galaxy S8 vs. Galaxy S7: Which is best for VR?

実際、Galaxy S8を使い始めた以降、動画・ゲーム・インターネットのいずれでも、一度も熱警告は発生していません。

Galaxy S6では負荷の低いアプリでも熱警告が頻発するため、Gear VRを使用する場合、Galaxy S6ではなく、Galaxy S8を選択するのがよさそうです。

Gear VRの2016年版をGalaxy S8で使えるか

Gear VRには2016年版と2017年版があります。2016年版の型番はSM-R323、2017年版の型番はSM-R324です。2017年版にはコントローラが付属します。2016年版のGear VRはGalaxy S8でも使用できるのでしょうか。

そもそも、Galaxy S8はUSB-Cです。Gear VRは、USB端子がアタッチメント方式になっており、Micro USBとUSB-Cを切り替えられるようになっているため、USB-Cアタッチメントがあれば接続できそうです。

2016年版には、USB-Cアタッチメントが付属するものと、付属しないものがあります。もともと、USB-CはGalaxy Note 7用のものであり、Galaxy Note 7のバッテリー問題以後、USB-Cのアタッチメントは同梱されなくなりました。そのため、Amazonやメルカリには、Gear VRの2016年版が安価に出品されていますが、USB-Cのアタッチメントが付属するかどうかは、事前に確認が必要です。

USB-Cのアタッチメントがあれば、接続できそうです。ということで、実際に接続してみました。

IMG_2314


接続したところ、Galaxy S8はGear VRを認識し、問題なく動作しました。視野的にも問題は感じず、Galaxy S6と同等の見栄えです。ただ、Gear VR本体とGalaxy S8のホルダーに若干、遊びがあるためか、Gear VRを強く降ると、若干、がたつきがあるように感じます。

海外のフォーラムで調査してみると、2016年版と2017年版では、数ミリcm程度、アタッチメントのサイズが異なるという情報がありました。

比較画像

若干のがたつきは、この数ミリが影響しているのかもしれません。

公式には、Galaxy S8で使用できるのはSM-R324だけとなっているため、基本的には2017年版の新型を使った方が良さそうです。

Galaxy S8の縦長画面におけるuGUIの動作

Galaxy S8では16:9を超える縦長になっています。縦長アプリの場合、uGUIのMatch Width Or Heightに1を設定し、横長になった場合に左右に余白を追加する方向に調整することが多いため、Galaxy S8では逆にマイナスの余白が発生し、見切れてしまうのではないかと心配していました。

しかし、その心配は杞憂で、実際にGalaxy S8でUnityで開発したアプリを動作させたところ、自動的にアプリは16:9の解像度で起動し、上下に黒の余白が入るようになっていました。Galaxy S8側で、アプリの互換性を保つよう、ホワイトリストで16:9表示にフォールバックするようです。

Galaxy S8:18.5:9の縦長ディスプレイはアプリごとに表示を変えられるので安心

Kindle Fire7(2017)とFire HD 8(2017)の比較

Amazonの格安タブレット、Kindle Fire 7とKindle Fire HD 8を両方買ってみました。

IMG_2303

スペック差は以下のようになります。

項目Fire 7Fire HD 8
価格(割引後)4980円7980円
CPUARM Cortex-A7 (quad-core, 1.3 GHz)ARM Cortex-A53 (64-bit quad-core, 1.3 GHz)
メモリ1GB1.5GB
容量8GB16GB
解像度1024 x 600 (171 ppi)1280 x 800 (189ppi)
スピーカーモノラルステレオ
重量295g369g

両方使用してみた印象としては、

・Fire7よりもFire HD8の方が動きが格段に滑らか
・Fire HD8の方がスピーカーの音質がよい
・Fire7の方がコンパクトで軽量

パフォーマンスはHD8の方が格段に快適でした。ただ、Fire 7もブラウザなど使わず、Prime Videoを見る専用機としてはとても優秀です。

PNGイメージ-CA80C2B20578-1

Kindleでコミックを読む場合、縦置きだとFire 7もHD8も十分な解像感で読むことができます。

IMG_2304

横置きだと、Fire 7だと解像度が不足して厳しいです。HD8だと、普通に読めます。

IMG_2305

Fire 7だと、横置きで文字が潰れます。

IMG_2307

Fire HD8だと、横置きでも文字が潰れません。

IMG_2306

写真だとFire HD8の方が黄色っぽく見えますが、実機だと特に違和感のある色ではありません。きちんと白に見えます。iPhone 7のカメラとの相性かもしれません。

IMG_2308

Fire OSはAndroid 5ベースです。初めてのFire OSですが、Kindle、Prime Videoなど、Amazonのサービスをアプリを起動せずに統合されたインタフェースでサクサク使えるのは、とても満足感があります。iOSだとブラウザからしかKindleの書籍が買えないのが、Kindleアプリの中でワンクリックで買えるのはとても快適です。

アプリは、Yahoo、Twitter、TDnetViewを入れたぐらいで、他はAmazonサービスに依存する感じです。iPad Pro 9.7も保有しているのですが、雑誌を読む以外の用途であれば、Fire HD8でも十分そうです。これが7980円で買えるのはすごいですね。

Amazon Primeは従来、年間3900円のプランしか選択できませんでしたが、先月から月額400円プランが出来ました。月額400円プランでも、Kindle Fireの4000円 OFFのクーポンは有効ですので、まずは月額400円プランでもよいかと思います。1ヶ月で退会しようと思っていたのですが、Amazon Prime Videoがあまりに便利なので、しばらく加入してしまいそうです。

個人的な理想は、Fire HD8のスペックのFire7ではあるので、HD7の登場を期待してはいます。それでも、現行世代でFire7とFire HD8を比べると、Fire HD8の方が格段にパフォーマンスがよいため、タブレット1台持ちならFire HD8がベストではないでしょうか。

Fire7は、iPad Proなどを保有していて、軽量なKindle + Prime Video専用端末を求めている場合や、普段と違うガジェットを触ってみたい場合に購入すると良さそうです。iPadを保有している場合、Fire HD8とサイズ感や用途が重複するため、Fire7の方がガジェット的な満足感は高いかと思います。




クーポンコード『PRIMEFIRE7』で4,000円OFF



クーポンコード『PRIMEFIREHD8』で4,000円OFF

以下、Prime Videoのオススメです。クーポンはシンゴジラに使いました。



充電は以下のマグネットケーブルを使っています。


gree/unity-webviewでバウンスを止める

Assets/Plugins/iOS/WebView.mmのinitWithGameObjectNameにおいて、以下のように、bounces = NOを代入すると、iOSのバウンスを止めることができます。

- (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent enableWKWebView:(BOOL)enableWKWebView
{
    self = [super init];

   ...
   //追加

    id subview = [[webView subviews] objectAtIndex:0];
    if([[subview class] isSubclassOfClass:[UIScrollView class]]){
        ((UIScrollView *)subview).bounces = NO; 
    }

    return self;
}

uGUIの上にパーティクルを表示する

UnityのuGUIのデフォルトでは、CanvasのRender ModeがScreen Space - Overlayに設定されています。Overlayの場合、UI要素は常に最前面に表示されるため、3DのオブジェクトはuGUIに隠れることになります。パーティクルも3Dでレンダリングされるため、uGUIの後ろに表示されます。

uGUIの上にパーティクルを表示したい場合、CanvasのRender ModeにScreen Space - Cameraを設定することになります。Screen Space - CameraではuGUIのレンダリング先プレーンのZ値を指定できるため、uGUIよりも大きなZ値でレンダリングすれば、パーティクルが前面に来ます。

しかし、3Dのゲームを作っており、3Dの背景の上にuGUIを表示する場合、Screen Space - Cameraを設定すると、Z値の大きな3D要素もuGUIの上に表示されてしまいます。パーティクルのみを前面に持ってくることができません。

この問題を解決するには、複数のカメラを使用します。まず、シーンに2つのカメラを配置します。1つめのカメラのCulling MaskからUIを外します。2つめのカメラのCulling MaskをUIのみにします。2つめのカメラのClear FlagsをDepth onlyにします。パーティクルのLayerをUIにします。

こうすると、背景の3Dのシーンを1つ目のカメラでレンダリングした後、Z値をクリアして、2つ目のカメラでUIとパーティクルがレンダリングされます。中間でZ値がクリアされるため、UI要素は常に3Dの上に表示することができます。

カメラを分けることで、好きなImage Effectをかけることもできるので便利です。

uGUIのボタンが押せるかどうか判定する

自動検証でランダムにuGUIのボタンを押したいことがあります。シーン内のボタンは、
 Button [] list=GameObject.FindObjectsOfType<Button> ();
で取得できるのですが、Panelなど、他のオブジェクトの下にあるボタンも検出してしまうため、そのままでは使えません。

特定のボタンが押せるかどうか判定するには、ボタンからスクリーン座標を求め、EventSystemからRaycastし、自分自身かどうかで判定します。ボタンからスクリーン座標を求めるには、親キャンバスを取得した上で、RectTransformUtility.WorldToScreenPointを使用する必要があります。

private static bool IsButtonClickable(Button item){
        //親キャンバスを取得
        Transform target=item.transform;
        while(target.GetComponent<Canvas>()==null){
            target=target.parent;
            if(target==null){
                return false;
            }
        }

        //親キャンバスからスクリーン座標を求める
        Vector2 position=RectTransformUtility.WorldToScreenPoint(target.GetComponent<Canvas>().worldCamera,item.gameObject.GetComponent<RectTransform>().position);

        //スクリーン座標からRayを飛ばす
        PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current);
        eventDataCurrentPosition.position = position;
        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventDataCurrentPosition, results);

        //自分自身でない場合は他のUIの下なのでタップできない
        if(results.Count >= 1){
            if(results[0].gameObject.name!=item.gameObject.name){
                return false;
            }
        }else{
            return false;
        }

        //タップできる
        return true;
    }

Unity 5.5で使うAssetBundle

AssetBundleは、UnityのAssetを別ファイルに格納できる仕組みです。GooglePlayで配信できるapkファイルには100MBの制約があるため、アセットをAssetBundleに分離する必要があります。

まず、AssetBundleに格納するアセットをエディタで指定します。指定は、アセット単位でも、フォルダ単位でも行うことができます。フォルダに指定した場合、フォルダ内の全てのアセットが格納されます。尚、AssetBundleには階層構造が存在せず、最終階層のファイル名で識別されます。

assetbundle


AssetBundleをビルドするには、Editorスクリプトで実行する必要があります。AssetBundleにはコンパイル済みのシェーダが含まれるため、iOS向け、Android向けで異なるファイルが生成されます。Unity Cloud Buildでビルドした際のパスと合わせるため、StreamingAssets/AssetBundles/iOS or Androidに格納するのがオススメです。

using UnityEditor;
using UnityEngine;

using System.IO;

public class CreateAssetBundles
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        var platform = "Standalone";
        #if UNITY_ANDROID
            platform="Android";
        #endif
        #if UNITY_IOS
            platform="iOS";
        #endif

        if (!Directory.Exists(Application.streamingAssetsPath+"/AssetBundles"))
        {
	        Directory.CreateDirectory(Application.streamingAssetsPath+"/AssetBundles");
	    }

        if (!Directory.Exists(Application.streamingAssetsPath+"/AssetBundles/"+platform))
        {
            Directory.CreateDirectory(Application.streamingAssetsPath+"/AssetBundles/"+platform);
        }

        Debug.Log("Build asset bundles for "+EditorUserBuildSettings.activeBuildTarget);
        BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath+"/AssetBundles/"+platform, BuildAssetBundleOptions.None, EditorUserBuildSettings.activeBuildTarget);
    }
}


生成したAssetBundleは、 WWW.LoadFromCacheOrDownloadで読み込むことができます。 WWW.LoadFromCacheOrDownloadを使用した際、キャッシュに存在すればキャッシュから、存在しなければ指定したパスから読み込みます。キャッシュに存在するかどうかは、Caching.IsVersionCached(url,BUNDLE_VERSION)で取得することができ、キャッシュのクリアはCaching.CleanCache();で行うことができます。BUNDLE_VERSIONは固定値を入れることが推奨されています。BUNDLE_VERSIONを上げた場合、旧バージョンのアセットが消去されないため、キャッシュ容量が増加し続けるためです。そのため、crc値を書き換えることで、キャッシュクリアする方法が推奨されています。crc値は、生成したAssetBundleと同じフォルダにあるmanifestファイルに記載されています。

			// ダウンロード処理
			WWW www=null;
			if(CHECK_ASSET_BUNDLE_CRC){
				www = WWW.LoadFromCacheOrDownload(url, BUNDLE_VERSION, crc);
			}else{
				www = WWW.LoadFromCacheOrDownload(url, BUNDLE_VERSION);
			}
			while (!www.isDone)
			{
				progress_cnt=www.progress;
				yield return null;
			}

			// エラー処理
			if(!string.IsNullOrEmpty(www.error))
			{
				load_failed=true;
				errror_detail=url;
				Debug.Log(www.error);
				yield break;
			}

			// Asset Bundleをキャッシュ
			assetBundleCache[bundlename] = www.assetBundle;

			// リクエストは開放
			www.Dispose();


AssetBundleには、高圧縮低速のLZMA形式と、低圧縮高速のLZ4形式があります。WWW.LoadFromCacheOrDownloadでLZMA形式をロードすると、自動的にLZ4形式に変換してキャッシュします。そのため、AssetBundleはLZMA形式で生成して問題ありません。

AssetBundleには依存関係があります。とあるPrefabをAssetBundleに格納した場合、そのPrefabに紐付いたAssetが自動的に検索され、AssetBundleに格納されます。しかし、このままだと、複数のPrefabから参照されるオブジェクトが、複数のAssetBundleに格納されることになり、ファイル容量が増大してしまいます。この問題は、複数のPrefabから参照されるオブジェクトを、別のAssetBundleに格納することで回避できます。その場合、とあるPrefabのAssetBundle.LoadAssetを呼ぶまでに、複数のPrefabから参照されるオブジェクトを格納したAssetBundleが読み込まれている必要があり、その依存関係がDependenciesに記載されています。尚、AssetBundleを読み込む順番は、Dependencies順でなくてもかまいません。あくまで、AssetBundle.LoadAssetを呼ぶまでに依存関係が解決されていればよいです。そのため、WWW.LoadFromCacheOrDownloadをCoroutineで並列化して、高速化することができます。

ManifestFileVersion: 0
CRC: 903982090
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: 378b24330402254e95eb1568de8f4e56
  TypeTreeHash:
    serializedVersion: 2
    Hash: a3429469e80eaecda247646a582aa377
HashAppended: 0
ClassTypes:
- Class: 1
  Script: {instanceID: 0}
Assets:
- Assets/AssersBundleResources/***/***.prefab
Dependencies:
- AssetBundles/iOS/models_***


AssetBundleからGameObjectを取得するには、Resources.Loadと同様に、assetBundle.LoadAssetで取得可能です。その際、階層構造の指定はできず、最終的なファイル名でロードします。

	// Asset BundleからGameObjectを取得
	public GameObject GetObject(string assetbundle_id,string assetName){
		try
		{
			GameObject obj=assetBundleCache[assetbundle_id].LoadAsset(string.Format("{0}", assetName));
			if(obj==null){
				Debug.Log(""+assetbundle_id+" "+assetName+" not found");
			}

	#if UNITY_EDITOR
			if(obj!=null){
				if(obj.GetComponent()==null){
					obj.AddComponent();
				}
			}
	#endif
			return obj;
		}
		catch (NullReferenceException e)
		{
			Debug.Log(e.ToString());
			return null;
		}


作成したAssetBundleは、manifestファイルと一緒に、CDNなどに上げて、ランタイムでロードします。

AssetBundleでしか参照しないコードが存在すると例外が起きる

AssetBundleでしか参照しないコードが存在し、iOSビルドの設定のStrip Engine CodeがONの場合、AssetBundleからGameObjectを取得しようとした際に、PersistentManager.cppでEXC_BAD_ACCESSの例外が飛びます。対策としては、Strip Engine CodeをOFFにするとよいようです。

AssetBundleにPrefabを入れるとShaderがMissingになる

AssetBundleに敵のモデルのPrefabを入れてロードした場合、iOSの実機では動きますが、EditorではShaderがMissingになり、黒くなったり、ピンクになったりします。

本質的には、iOS向けに書き出したAssetBundleに含まれるコンパイル済みシェーダが、EDITORに対応していないのが問題なのですが、AssetBundleをEDITOR向けに書き出すのはコストが高いです。

そこで、We're looking for feedback on the artist features in the 2017.1 beta, help us out by filling outを参考に、以下のスクリプトでシェーダを当て直すとよいようです。GetComponentsInChildrenにtrueを入れることで、非アクティブのオブジェクトにも適用するように修正しています。

using UnityEngine;
using System.Collections;
 
 
public class ReApplyShaders : MonoBehaviour
{
    public Renderer[] renderers;
    public Material[] materials;
    public string[] shaders;
 
    void Awake()
    {
        renderers = GetComponentsInChildren(true);
    }
 
    void Start ()
    {
        foreach(var rend in renderers)
        {
            materials = rend.sharedMaterials;
            shaders =  new string[materials.Length];
 
            for( int i = 0; i < materials.Length; i++)
            {
                shaders[i] = materials[i].shader.name;
            }        
 
            for( int i = 0; i < materials.Length; i++)
            {
                materials[i].shader = Shader.Find(shaders[i]);
            }
        }
    }
}

Unity Cloud BuildのAsset Bundleの格納先

Unity Cloud Buildを使用してビルドする際、Asset Bundleを同時にビルドして、Streaming Assetsに格納することができます。

Unity Cloud Buildのビルドタイプのアセットバンドルの設定から、Build Asset BundlesとCopy to Streaming Assetsを有効にします。

asset_bundle_oath


ビルド済のapkファイルとipaファイルの拡張子をzipに変更して展開すると、以下のフォルダにアセットバンドルが格納されます。

iOS : Payload/app/Data/Raw/AssetBundles/iOS
Android : assets/AssetBundles/Android

従って、以下のパスでアクセスすることができます。

iOS : Application.streamingAssetsPath + "/AssetBundles/iOS"
Android : Application.streamingAssetsPath + "/AssetBundles/Android"
Search
Profile
Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING