エイバースの中の人

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

ビルド を含む記事

Android8で権限が必要なアプリが起動しない

Unity 5.5.4f1でビルドしたアプリをAndroid8で起動した場合、Unityのパーミッションの設定を行うコードに問題があり、アプリが起動せず、ブラックスクリーンのままになります。

この問題は、設定->アプリ->権限、から手動で権限を設定すると解消します。また、Unity 5.5.5p1で解消されています。

(945338, 946061) - Android: Fixed black screen on startup on Android Oreo devices.
Game does not work on new Android Oreo
Android8.0 Oreoで起動ができなくなる

Android7以降でSocialConnectorが動作しない

SocialConnectorでは以下のようにuri.fromFileでファイル共有を行います。

var uri = new AndroidJavaClass ("android.net.Uri");
var file = new AndroidJavaObject ("java.io.File", textureUrl);
intent.Call<AndroidJavaObject> ("putExtra", "android.intent.extra.STREAM", uri.CallStatic<AndroidJavaObject> ("fromFile", file));


しかし、Android7から権限管理が強化されたため、画像共有時にAndroidJavaException: android.os.FileUriExposedExceptionが発生します。この問題を解決するには、FileProviderを使用する必要があります。

int FLAG_GRANT_READ_URI_PERMISSION = intent.GetStatic<int>("FLAG_GRANT_READ_URI_PERMISSION");
intent.Call<AndroidJavaObject>("addFlags", FLAG_GRANT_READ_URI_PERMISSION);

AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject unityContext = currentActivity.Call<AndroidJavaObject>("getApplicationContext");

string packageName = unityContext.Call<string>("getPackageName");
string authority = packageName + ".fileprovider";

AndroidJavaObject fileObj = new AndroidJavaObject("java.io.File", textureUrl);
AndroidJavaClass fileProvider = new AndroidJavaClass("android.support.v4.content.FileProvider");
AndroidJavaObject uri = fileProvider.CallStatic<AndroidJavaObject>("getUriForFile", unityContext, authority, fileObj);

intent.Call<AndroidJavaObject>("putExtra", "android.intent.extra.STREAM", uri);


AndroidManifestに権限を追加します。

 <!-- Add fileprovider for android n -->
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="バンドルID.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"></meta-data>
</provider>


Plugins/Android/res/xmlにfile_paths.xmlを作成し、以下のように共有フォルダ指定をします。

<?xml version="1.0" encoding="utf-8"?>
<paths>
      <external-cache-path name="external_files" path="."/>
      <external-path name="external_files" path="."/>
</paths>


しかし、UnityとAndroid SDK 26以降を合わせて使うと、Plugins/Android/resフォルダが存在するとREAD_PHONE_STATEパーミッションが追加されます。(READ_PHONE_STATE permission added when using SDK tools 26.0.2)Unity5.5.4f1からUnity5.5.5p2に上げても回避はできませんでした。

尚、ビルド済みのapkからManifestを抽出するにはapktoolを使用します。

apktool d input.apk

モナコインをマイニングしてみる

モナコインは2013年12月に誕生した日本初の暗号通貨です。

monacoin

仮想通貨は最初に正解を当てたクライアントのみが報酬を得るため、ソロマイニングをすると報酬が安定しません。そのため、複数のユーザでギルドのようなものを組み、報酬を分配することで、安定した報酬の獲得を目指します。

以下の手順でモナコインをマイニング可能です。

(1) Monappyでモナコインのウォレットを作成
(2) マイニングプール(ASICpoolやVIPpool等)のアカウントを作成
(3) cpuminerのインストール
(4) マイニングの実行

cpuminerは以下のリポジトリからCloneしてMakeします。lyra2rev2に対応している必要があるため、pooler/cpuminetではなく、tpruvot/cpuminer-multiが必要です。
https://github.com/tpruvot/cpuminer-multi

MacOSでビルドする場合、READMEに記載のCFLAGS="*-march=native*"を付けるとconfigureでgccが見つからないエラーが出るため、外します。シンボリックリンクを作成してOpenSSLにパスを通します。_neoscrypt_blkcpyでリンクエラーが発生するため、fix os x build with asm #14をマージします。

export PATH=/usr/local/Cellar/openssl/1.0.2m/bin:$PATH
cd /usr/local/include 
ln -s ../opt/openssl/include/openssl .


cpuminerは以下のように起動することができます。Webloginはユーザ名、WorkerNameはワーカー名、WorkerPasswordはワーカーパスワードが対応します。Stratumはマイニングのプロトコルです。lyra2rev2はハッシュアルゴリズムです。

./cpuminer -a lyra2rev2 -o stratum+tcp://stratum.asicpool.info:6969 -u Weblogin.WorkerName -p WorkerPassword


昔のモナコインはLiteCoinと同じscryptを使用していましたが、現在はハードフォークされており、lyra2rev2になっているため、-a scripyでは採掘できません。lyra2REv2は多くのメモリを必要とする構造になっているため、単純なSHA2を使用するビットコインに比べて、ASIC化が難しいと言われています。

モナコインの情報は、Ask Monaから取得可能です。マイニングプールはオープンソースのphp-mposを使用していることが多いようです。

[2017-12-09 20:53:28] Starting Stratum on stratum+tcp://stratum.asicpool.info:6969
[2017-12-09 20:53:28] 8 miner threads started, using 'lyra2rev2' algorithm.
[2017-12-09 20:53:32] Stratum difficulty set to 1 (0.00391)
[2017-12-09 20:53:32] lyra2rev2 block 1184360, diff 88512.700
[2017-12-09 20:53:33] CPU #0: 77.87 kH/s
[2017-12-09 20:53:33] CPU #5: 77.54 kH/s
[2017-12-09 20:53:33] CPU #7: 77.33 kH/s
[2017-12-09 20:53:33] CPU #2: 77.10 kH/s
[2017-12-09 20:53:33] CPU #1: 77.11 kH/s
[2017-12-09 20:53:33] CPU #6: 77.09 kH/s
[2017-12-09 20:53:33] CPU #3: 76.90 kH/s
[2017-12-09 20:53:33] CPU #4: 76.18 kH/s
[2017-12-09 20:53:36] accepted: 1/1 (diff 0.013), 616.59 kH/s yes!
[2017-12-09 20:53:42] CPU #1: 75.16 kH/s
[2017-12-09 20:53:42] accepted: 2/2 (diff 0.004), 615.18 kH/s yes!
[2017-12-09 20:53:53] CPU #1: 74.08 kH/s
[2017-12-09 20:53:53] accepted: 3/3 (diff 0.016), 614.10 kH/s yes!

yesが出れば掘れています。

Windows10にDarknetを入れる

Gigabyte Aorusゲームボックスを使うため、Windows10にDarknetを入れたのでメモです。PCはNUCです。



Thunderbolt Driverのインストール


NUCのサイトからtbt_win10_64_17.2.71.250.zipをダウンロードしてインストール。

VisualStudio 2015のインストール


MSDNからVisual Studio 2015をダウンロード。2017のダウンロードページに飛ばされるので、最下部の以前のバージョンからダウンロード。(以前のバージョンをお使いになりたいですか?

OpenCVのインストール


opencv/opencvからopencv-3.1.0.exeを展開。opencv/build/vc14/binにパスを通す。

CUDAのインストール


CUDA Toolkit Downloadからインストール。バージョンは9.0ではなく8.0が必要。cuDNNをダウンロード。登録が必要。cuda/binにパスを通す。尚、後述するようにDarknetのcuDNN対応がまだ微妙なため、Darknetだけを使用する場合はCUDA Toolkitだけをインストールするのでもよい。Tensorflowを使用する場合は必要。

Darknetのインストール


AlexeyAB/darknetをClone。build/darknet/darknet.slnを開き、opencv/build/vc14/libとopencv/build/includeとcuda/includeとcuda/libにパスを通す。CUDNNを有効にすると正しく認識が動作しないため、CUDNNのdefineを外し、GPUのみdefineを残す。ビルドするとx64フォルダにdarknet.exeができる。

パフォーマンス


WindowsのパフォーマンスモニタではCUDAのGPU稼働率を見ることはできないが、GPUが熱を持っているので稼働はわかる。TinyYoloで100Epocが、CPU(Xeon)だと12時間、GPU(1070)だと3分。240倍の性能。

Anacondaのインストール


Anacondaの公式サイトから3.6のバージョンを入手。Python 3.5の環境に変更。

conda install python=3.5
conda install pip


Kerasのインストール


以下のコマンドでKerasとTensorflowをインストール。

pip install keras
pip install tensorflow-gpu


Pytorchのインストール


以下のコマンドでtorchをインストール。

conda install -c peterjc123 pytorch 


Caffeのインストール


BVLC/caffeからビルド済みバイナリのVisual Studio 2015, CPU only, Python 3.5: Caffe Releaseをダウンロード。Pythonから使えるようにするためにcaffe/python/caffeをc:/users/user_bane/Anaconda/Python3.5/Lib/site-packagesにコピー。import caffeでDLL load failedが発生するため、caffe-windowsからthirdparty20170624.rarをダウンロードして./windows/thirdparty/binsにパスを通す。

Darknetを使用してYoloの係数を学習する

Darknetの概要


DarknetはYoloの開発者が開発しているディープラーニングのフレームワークです。C++で記述されており、簡単にビルドすることができます。Yoloの歴史についてはYOLO9000: Better, Faster, Strongerの資料が詳しいです。

Darknetを使用してYoloの係数を学習するチュートリアルは以下が詳しいです。
How to train YOLOv2 to detect custom objects

Darknetのインストール


環境構築はMacかUbuntuが簡単です。まず、リポジトリをCloneします。

git clone https://github.com/AlexeyAB/darknet.git

GNUmakeを使用してmakeします。darknetのバイナリが生成されます。

make

nVidiaのGPUであればMakefileを書き換えることでCUDAを有効にすることができます。今回はAMDのGPUを搭載したMacなため、CPUで動かします。

学習させるデータの準備


学習させるデータをダウンロードします。上記記事のサンプルデータがテストには最適です。
https://timebutt.github.io/content/other/NFPA_dataset.zip

ただし、pos-234.jpgの拡張子が大文字のJPGになっているため、小文字のjpgに変換する必要があります。また、train.txtとtest.txtの改行コードをWindows StyleからLinux Styleに変換する必要があります。

データセットには、入力画像のjpgと、バウンディングボックスのtxtが含まれています。バウンディングボックスのtxtのフォーマットは以下のようになります。画像のどの位置にどのカテゴリのオブジェクトが存在するかが記載されています。同様の形式で、jpgとtxtのペアを準備することで、任意の画像で学習させることができます。

[category number] [object center in X] [object center in Y] [object width in X] [object width in Y]

YoloV2での学習


次に、obj.data、obj.names、yolo-obj.cfgを準備します。

obj.dataには学習のための基本情報が含まれます。上から、識別クラス数、入力画像のファイルリスト、評価画像のファイルリスト、クラス名称、係数の出力ディレクトリです。trainやvalidはdarknetのバイナリからの相対パスです。

classes=1
train=data/nfpa/train.txt
valid=data/nfpa/test.txt
names=data/nfpa/obj.names
backup=backup/

obj.namesにはカテゴリ名称が含まれます。

NFPA

yolo-obj.cfgにはYoloのネットワーク構成が含まれます。yolo-voc.cfgをコピーして、batchを64に、subdivisionsを8に、classesを識別させたいクラス数に書き換えます。最終段のfiltersを(classes + 5)*5に書き換えます。

[net]
# Testing
batch=64 #書き換える
subdivisions=8 #書き換える
# Training
# batch=64
# subdivisions=8
height=416
width=416
channels=3

省略...

[convolutional]
size=1
stride=1
pad=1
filters=30 #書き換える
activation=linear

[region]
anchors =  1.3221, 1.73145, 3.19275, 4.00944, 5.05587, 8.09892, 9.47112, 4.84053, 11.2364, 10.0071
bias_match=1
classes=1 #書き換える
coords=4
num=5
softmax=1
jitter=.3
rescore=1

省略...

収束を速めるため、初期係数列をダウンロードしておきます。
darknet19_448.conv.23

最後に、以下のコマンドで学習を行います。

./darknet detector train data/nfpa/obj.data data/nfpa/yolo-obj.cfg darknet19_448.conv.23

学習にはハイエンドのGPUで1時間かかるということで、CPUだけだと厳しい感はあります。
学習結果は以下のコマンドで確認します。

./darknet detector test data/nfpa/obj.data data/nfpa/yolo-obj.cfg yolo-obj1000.weights testimage.jpg

学習済みのTiny-Yoloを動かしてみるには、ofxDarknetからtiny-yolo-voc.weightsをダウンロードした後、以下のコマンドで確認します。

./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg tiny-yolo-voc.weights data/person.jpg

上記ページのCOCO版のtiny-yolo.weightは学習時のcfgが異なるためか、正しく動作しないため、digitalbrain79/pyyoloのウエイトファイルを使用します。

./darknet detector test cfg/coco.data cfg/tiny-yolo.cfg tiny-yolo.weights data/person.jpg

YoloV1での学習


係数をCaffemodelに変換するには、pytorch-caffe-darknet-convertを使用します。変換にはCaffeのInstallが必要です。

python darknet2caffe.py tiny-yolo.cfg tiny-yolo.weights out.prototxt out.caffemodel

ただし、YoloV2にはCaffeではサポートされていないRegion Layerが含まれています。そのため、Caffeで動かす場合は、cfg/yolov1を使用する必要があります。Tiny-Cocoのweightsはyolov1のページからダウンロードできます。

動作テスト
./darknet coco test cfg/yolov1/tiny-coco.cfg tiny-coco.weights data/giraffe.jpg

Caffeモデルへの変換
python darknet2caffe.py tiny-coco.cfg tiny-coco.weights out.prototxt out.caffemodel

Yolov1で学習するには以下のコマンドを使用します。使用する画像の設定はsrc/yolo.cで行う必要があります。(You Only Look Once:Unified, 統合されたリアルタイムオブジェクトの検出

char *train_images = "/home/pjreddie/data/voc/test/train.txt";
char *backup_directory = "/home/pjreddie/backup/";

cfgのclassesとconnectedのoutputを書き換えます。outputはcfgのside*side*((coords+1)*n+classes)です。nfpaの1カテゴリでtiny-yoloを使用する場合は539になります。

VOCの学習
./darknet yolo train cfg/yolov1/yolo.train.cfg

yolo.cfgとyolo.train.cfgの違いはbatchとsubdivisionsだけです。

nfpaの学習
./darknet yolo train cfg/yolov1/tiny-yolo-nfpa.cfg

学習はMac Pro 2013だとシングルスレッドで24時間で200 Epoc、OpenMPで12時間で1400Epocでした。GTX1070を使えば、1時間で2000Epoc回すことができました。Darknetで2クラスの物体をトレーニングによると8000Epoc近く回す必要があるみたいなので、CPUで8000Epoc回すには3日程度必要です。GPUだと4時間程度で終わります。

クラス数1の学習結果のテストを行うにはyolo.cのdraw_detectionsの最後の引数の20をl.classesに書き換える必要があります。

学習結果のテスト
./darknet yolo test cfg/yolov1/tiny-yolo-nfpa.cfg backup/tiny-yolo-nfpa_200.weights data/nfpa/pos-1.jpg

500Epoc
500
1000Epoc
1000
2000Epoc
2000

2000 Epocまでいけばわりと認識できるようです。

(補足)Caffeのインストール


darknet2caffe.pyの実行にはCaffeが必要です。CaffeのインストールにはMakefile.configに以下の設定が必要でした。PYTHON_LIBのパスが合ってないと、import CaffeでSegmentation Faultが起きるようです。リンカフラグへのlibstdc++の設定は不要でした。

CPU_ONLY := 1
OPENCV_VERSION := 3
WITH_PYTHON_LAYER := 1
PYTHON_LIB :=  /usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/
INCLUDE_DIRS += /usr/local/Cellar/boost@1.59/1.59.0/include /usr/local/Cellar/boost-python@1.59/1.59.0/include
LIBRARY_DIRS += /usr/local/Cellar/boost@1.59/1.59.0/lib /usr/local/Cellar/boost-python@1.59/1.59.0/lib
INCLUDE_DIRS += /usr/local/lib/python2.7/site-packages/numpy/core/include

(補足)Darknetのマルチスレッド化


Darknetはデフォルトでは1スレッドで動作します。CPUでマルチスレッド化するには、OpenMPを有効化します。XcodeのデフォルトではOpenMPが使用できないため、最新のLLVMをインストールします。
brew install llvm

Makefileを書き換えます。
OPENMP=1
CC=/usr/local/opt/llvm/bin/clang
CPP=/usr/local/opt/llvm/bin/clang++
LDFLAGS+= -L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib

UnityアプリのiPhoneX対応

UnityアプリはXcode9でビルドした場合は互換モードでは動作せず、全画面モードで動作します。Unity 5.5など、古いバージョンのUnityでビルドした場合でも同様です。そのため、縦画面想定で、Canvas Scalerでmatch=1.0などと設定していると、UIの左右が見切れることになります。

この問題を解決するには、画面のアスペクト比を見て、match=0.0に設定する必要があります。具体的に、以下のようなスクリプトを全てのCanvas Scalerを含むGameObjectにアタッチします。尚、Canvas Scalerの初期化よりも前に走らせるために、Project SettingのScript Execution OrderでCanvasScreenAutoFixの実行順を早くしておきます。

//iPhoneXの縦長画面に対応
//Canvasにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CanvasScreenAutoFix : MonoBehaviour
{
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/16.0f){
			GetComponent<CanvasScaler>().matchWidthOrHeight=0.0f;
		}else{
			GetComponent<CanvasScaler>().matchWidthOrHeight=1.0f;
		}
	}
}

スクリプトのアタッチを手動で行うと大変なので、Editor Scriptで自動化します。

//iPhoneX対応のため、全てのCanvasにCanvasScreenAutoFixをアタッチ

using UnityEngine;
using UnityEditor;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public class iPhoneXSupport : MonoBehaviour {
    public static List<T> FindObjectsOfTypeAll<T>()
    {
        List<T> results = new List<T>();
        var s = SceneManager.GetActiveScene();
        if (s.isLoaded)
        {
            var allGameObjects = s.GetRootGameObjects();
            for (int j = 0; j < allGameObjects.Length; j++)
            {
                var go = allGameObjects[j];
                results.AddRange(go.GetComponentsInChildren<T>(true));
            }
        }
        return results;
    }

    [MenuItem ("iPhoneX/Add canvas scaler")]
    static void ChangeToProduction() {
        GameObject[] allObjects = (GameObject[])FindObjectsOfTypeAll( typeof(GameObject) );
        foreach ( GameObject obj in allObjects ){
            if(obj.GetComponent<Canvas>()!=null){
                obj.AddComponent<CanvasScreenAutoFix>();
                Debug.Log(obj.name);
                EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
            }
        }
    }
}

UIは互換表示のように黒帯を表示し、メインゲームは全画面で動作させる場合、UIのRectTransformのAnchorがCenter以外だとレイアウトが崩れる場合があるため、手動で全てCenterに書き換えます。

また、カメラの画角も変わり、大きく表示されるため、補正します。

//iPhoneXの縦長画面に対応
//Cameraにアタッチ

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

public class CameraScreenAutoFix : MonoBehaviour {

	// Use this for initialization
	void Awake () {
		float initial_field_of_view=GetComponent<Camera>().fieldOfView;
		float ratio=(9.0f/16.0f)/(1.0f*Screen.width/Screen.height);
		if(ratio<1.0f){
			ratio=1.0f;
		}
		GetComponent<Camera>().fieldOfView=initial_field_of_view*ratio;
	}
}

中央寄せではなく、上寄せしているAnchorを持つUIに対しては、セーフエリアの44pxを加算する必要があるため、以下のスクリプトを当てます。

//上寄せのUIのセーフエリアを適用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SafeArea : MonoBehaviour {
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/19.0f){
			Transform target=transform;
			while(target!=null){
				if(target.GetComponent<CanvasScaler>()!=null){
					break;
				}
				target=target.parent;
			}

			CanvasScaler canvas_scaler=target.GetComponent<CanvasScaler>();
			float safe_area_y=44.0f;
			safe_area_y=safe_area_y*canvas_scaler.referenceResolution.y/Screen.height;
			Vector3 pos=GetComponent<RectTransform>().anchoredPosition;
			pos.y=pos.y-safe_area_y;
			GetComponent<RectTransform>().anchoredPosition=pos;
		}
	}
}

新規開発のアプリからは、20:9でUIの背景を作成すると共に、端寄せでUIを配置する必要があるかなと思います。

Android NDK r8でビルドしたバイナリがAndroid6で動作しない

Android NDK r8でビルドした.soが、Android6でDllNotFoundExceptionになることがあります。logcatを見てみると、hoge.so: has text relocations、というエラーが発生しています。

調査したところ、Android 6.0 Marshmallow (API23)はテキスト再配置をサポートしておらず、この機能を持つライブラリは実行できないようです。

暫定対処としては、targetSDKVersionを23ではなく、22にするとよいようです。

抜本的には、Android NDK r10eでのリビルドを推奨なようです。

Unity Cloud BuildでUnable to merge android manifestsが出る

Unity 5.5.4f1にFirebase 4を入れようとした際、 Unable to merge android manifestsのエラーが発生しました。ローカルでビルドした場合と、Unity 5.6.3f1を使用した場合はエラーは発生しません。Unity 5.6からAndroid 4.4未満はサポートしなくなったため、Unity Cloud Buildで使用しているAndroid SDKのバージョンの問題かと考え、Androidのビルド設定のMinimum API Levelをデフォルトの2.3.1から4.4に上げたところ、問題は解消されました。

1820: [Unity] AndroidSDKToolsException: Unable to merge android manifests. See the Console for more details. 
1821: [Unity]   at UnityEditor.Android.AndroidSDKTools.DetectErrorsAndWarnings (System.String logMessages, System.String errorMsg) [0x00000] in :0 

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などに上げて、ランタイムでロードします。

CDNのデータは配信の過程で破損する可能性があります。アセットバンドルが破損していた場合、本来はstring.IsNullOrEmpty(www.error)でエラーを検出することができるはずです。しかし、実際に破損したファイルを生成して読み込ませたところ、Windows、Android、Macでは正常にエラーを検出できますが、iOSの場合、エラーが返ってきません。そのため、www.assetBundle.GetAllAssetNames()を呼び出すことで、アセットバンドルが正しく読み込めたかどうかの確認が必要です。

// iOSで破損ファイルがエラーチェックを抜けるのでアセットリストを取得して例外が起きないか確認
string[] asset_list;
try{
	asset_list=www.assetBundle.GetAllAssetNames();
	if(asset_list.Length>=1){
		Debug.Log("Asset Load Check Success : "+asset_list[0]);
	}
}catch(Exception e){
	is_error=true;
	Debug.Log("Asset Load Check Failed : "+e);
}

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

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

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"

UnityのAndroidビルドでx86のNative Pluginがapkに含まれない

UnityのAssets/Plugins/Android/x86に.soを置いてもUnityのInspectorが認識してくれず、apkに含まれないため、ランタイムエラーになります。

解決方法として、Assets/Plugins/Android/libs/armeabi-v7a、Assets/Plugins/Android/libs/x86に.soを置いた上で、ReimportしないとInspectorに表示されず、apkに含まれないようです。

一度、このパスに配置してmetaファイルが生成されると、自由に移動可能になります。

Firebase for UnityをUnity Cloud Buildで使用する

-------------------------------------------
2017/8/26追記
Firebase for Unity 4.1.0を使用することで、特別な設定をすることなくUnity Cloud Buildが使用できるようになりました。以下の記事はFirebase for Unity1.1の情報です。

2017/9/11追記
"-lz}"のリンカエラーが発生するようになったので、結局、同様の手順で手動で入れました。Unity Cloud Buildでエラーになった際の解決法 vol3もとても参考になります。
-------------------------------------------

Firebase Analytics + Push Notificationにおける、ローカル環境でのLinker Errorの解消方法とUnity Cloud Buildでの使用方法をまとめました。

Firebase for Unityのインポート

FirebaseのUnity PackageはDownload the SDKからダウンロードすることができます。今回は、Push Notificationを実装するため、FirebaseAnalyticsとFirebaseMessagingの2つのUnity Packageをプロジェクトにインポートします。

firebase_unity


いきなり本番のプロジェクトに導入するのはリスクがあるので、サンプルプログラムをダウンロードして、そこにUnity Packageをインポートするのがオススメです。

iOSでPush通知を使用するには、APNs の SSL 証明書のプロビジョニングが必要です。AppleのProvision Portalで取得した証明書をp12に変換し、Firebaseのサイトに登録します。

その後、Firebaseのサイトから、プロジェクトのgoogle-services.jsonとGoogleService-Info.plistをダウンロードし、UnityのAssetsの中の好きなフォルダに登録します。

これで、Push通知が使用できるようになります。

ローカル環境でのLinker Error

iOSビルドでLinker Errorが出る場合は、cocoapodsのバージョンを確認します。コマンドラインからpod --versionとすると、バージョンが表示されますが、これが1.0以降でない場合に、Firebaseがうまくリンクされません。

Unityは、Xcodeのプロジェクトファイルを生成する際、Podfileが存在する場合にpodコマンドを叩いてくれます。Firebaseはpodコマンドを使って依存ライブラリをダウンロードするのですが、podコマンドに失敗してエラーが発生した場合もプロジェクトファイルが生成されてしまうため、ビルドエラーが発生するようです。

podのバージョンが古い場合は、gem update cocoapodsでバージョンアップが可能です。

Unity Cloud BuildでのLinker Error

ローカルでビルドするにはこれだけでOKですが、Unity Cloud Buildではcocoapodsが使えないため、以下のエラーが出ます。

Error running cocoapods. Please ensure you have at least version 1.0.0.  You can install cocoapods with the Ruby gem package manager:

この場合、CocoaPods を使用せずに統合するからzipをダウンロードし、手動でFrameworksフォルダのFirebaseMessaging.frameworkなどをPlugin/iOSフォルダに置く必要があります。

copy


コピーする必要のあるファイルです。

## Analytics
  - FirebaseAnalytics.framework
  - FirebaseInstanceID.framework
  - GoogleInterchangeUtilities.framework
  - GoogleSymbolUtilities.framework
  - GoogleUtilities.framework
## AdMob (~> Analytics)
  - GoogleMobileAds.framework
## AppIndexing (~> Analytics)
  - FirebaseAppIndexing.framework
## Auth (~> Analytics)
  - FirebaseAuth.framework
  - GoogleNetworkingUtilities.framework
  - GoogleParsingUtilities.framework
## Crash (~> Analytics)
  - FirebaseCrash.framework
## Database (~> Analytics)
  - FirebaseDatabase.framework
## DynamicLinks (~> Analytics)
  - FirebaseDynamicLinks.framework
## Invites (~> Analytics)
  - FirebaseDynamicLinks.framework
  - FirebaseInvites.framework
  - GoogleAppUtilities.framework
  - GoogleAuthUtilities.framework
  - GoogleNetworkingUtilities.framework
  - GoogleParsingUtilities.framework
  - GooglePlusUtilities.framework
  - GoogleSignIn.framework

  You'll also need to add the resources in the
  Resources directory into your target's main
  bundle.
## Messaging (~> Analytics)
  - FirebaseMessaging.framework
  - GoogleIPhoneUtilities.framework
## RemoteConfig (~> Analytics)
  - FirebaseRemoteConfig.framework
  - GoogleIPhoneUtilities.framework
## Storage (~> Analytics)
  - FirebaseStorage.framework
  - GoogleNetworkingUtilities.framework

その上で、Firebase/Editor/AppDeps.csなど*Deps.csから以下のようにpodの呼び出しをコメントアウトします。

#elif UNITY_IOS
/*
        Type iosResolver = Google.VersionHandler.FindClass(
            "Google.IOSResolver", "Google.IOSResolver");
        if (iosResolver == null) {
            return;
        }
        Google.VersionHandler.InvokeStaticMethod(iosResolver, "AddPod", new object[] { "Firebase/Core" }, new Dictionary() { { "version", "3.10+" }, { "minTargetSdk", "7.0" } });
*/
#endif
また、Firebaseはsqlite3とAddressBook.frameworkを要求するのでEditorフォルダに以下のスクリプトを追加し、自動化します。-ObjCがないと実行時に例外が飛ぶので、これも追加します。
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
using System.Collections.Generic;
#endif
using System.IO;

public class PostBuildProcess {

    [PostProcessBuild]
    public static void OnPostProcessBuild (BuildTarget buildTarget, string path) {
#if UNITY_IOS
        string projPath = Path.Combine (path, "Unity-iPhone.xcodeproj/project.pbxproj");

        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));

        string target = proj.TargetGuidByName ("Unity-iPhone");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-lz");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-lsqlite3");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-ObjC");

        List frameworks = new List () {
            "CoreData.framework",
            "AddressBook.framework"
        };

        foreach (var framework in frameworks) {
            proj.AddFrameworkToProject (target, framework, false);
        }

        File.WriteAllText (projPath, proj.WriteToString ());
#endif
    }
}

この段階で、Firebase Analyticsは動作するのですが、Firebase Messagingは実行時に例外で落ちます。どうやら、Unity Pluginと公式サイトのzipのFrameworkのバージョンが異なることが原因のようです。そのため、Firebase Messagingを使用する場合は、ローカルでcocoapodsを使用してiOSビルドした後、プロジェクトのFrameworksフォルダに含まれるFirebaseのFrameworkで、zipからコピーしたFrameworkを置き換える必要があります。

copy


また、GTMDefines.h、GTMLogger.h、GTMLogger.m、GTMNSData+zlib.h、GTMNSData+zlib.mもPlugins/iOSにコピーした上で、InspectorでCompile Flagsに-fno-objc-arc -fobjc-exceptionsと記載する必要があります。

compile_flag


Androidでは、UnityでAndroid Buildに変更して、Assets -> Play Service Resolver -> Android Resolver -> Resolve Client Jarsを実行しておきます。

これでようやく、UnityでFirebase AnalyticsとNotificationを使用することができます。

firebase


XcodeのCapabilityにPush NotificationとBackground Modes -> Remote notificationを追加すると、Firebaseから発行した通知を受け取ることができます。ただし、Info.plistにFirebaseAppDelegateProxyEnabled=NOを設定しないと、アプリがバックグラウンドに回った後、フォアグラウンドに回ると、100%ハングします。

IMG_1873


XcodeのCapability設定とFirebaseAppDelegateProxyEnabledの設定をUnity Cloud Buildで自動化するには、Unityから自動でPush NotificationsをONにしたいを参考に、Editorスクリプトを作成する必要があります。

//Unity Cloud BuildでiOS Buildにfirebaseの依存ファイルを追加する
//加えて、CapabilityにPush Notificationを追加する

using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
#endif

public class PostBuildProcess {

    [PostProcessBuild]
    public static void OnPostProcessBuild (BuildTarget buildTarget, string path) {
#if UNITY_IOS
        string projPath = Path.Combine (path, "Unity-iPhone.xcodeproj/project.pbxproj");

        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));

        string target = proj.TargetGuidByName ("Unity-iPhone");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-lz");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-ObjC");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-lc++");
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-lsqlite3");

        List frameworks = new List () {
            "CoreData.framework",
            "AddressBook.framework"
        };

        foreach (var framework in frameworks) {
            proj.AddFrameworkToProject (target, framework, false);
        }

        //Add
        File.WriteAllText (projPath, proj.WriteToString ());

        //Set Push Notification Capability
        CreateEntitlements(path,YourAppName);
        SetCapabilities(path);
        SetBackgroundMode(path);
#endif
    }

#if UNITY_IOS
    private static void SetBackgroundMode(string path)
    {
        var plistPath = Path.Combine(path, "Info.plist");

        PlistDocument plist = new PlistDocument();
        plist.ReadFromFile(plistPath);

        plist.root.SetBoolean("FirebaseAppDelegateProxyEnabled",false);

        PlistElementArray bgModes = plist.root.CreateArray("UIBackgroundModes");
        bgModes.AddString("remote-notification");

        plist.WriteToFile(plistPath);
    }

    private static void CreateEntitlements(string path,string your_appname)
    {
        XmlDocument document = new XmlDocument ();
        XmlDocumentType doctype = document.CreateDocumentType ("plist", "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null);
        document.AppendChild (doctype);

        XmlElement plist = document.CreateElement ("plist");
        plist.SetAttribute ("version", "1.0");
        XmlElement dict = document.CreateElement ("dict");
        plist.AppendChild (dict);
        document.AppendChild (plist);

        XmlElement e = (XmlElement) document.SelectSingleNode ("/plist/dict");

        XmlElement key = document.CreateElement ("key");
        key.InnerText = "aps-environment";
        e.AppendChild (key);

        XmlElement value = document.CreateElement ("string");
        value.InnerText = "development";
        e.AppendChild (value);

        string entitlementsPath = path + "/Unity-iPhone/"+your_appname+".entitlements";
        document.Save(entitlementsPath);

        string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));
        string target = proj.TargetGuidByName ("Unity-iPhone");
        string guid = proj.AddFile (entitlementsPath, entitlementsPath);
        proj.SetBuildProperty(target, "CODE_SIGN_ENTITLEMENTS", "Unity-iPhone/"+your_appname+".entitlements");
        proj.AddFileToBuild(target, guid);
        proj.WriteToFile(projPath);
    }

    private static void SetCapabilities(string path)
    {
        string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));

        string[] lines = proj.WriteToString().Split ('\n');
        List newLines = new List ();
        bool editFinish = false;

        for (int i = 0; i < lines.Length; i++) {

            string line = lines [i];

            if (editFinish) {
                newLines.Add (line);
            } else if (line.IndexOf ("isa = PBXProject;") > -1) {
                do {
                    newLines.Add (line);
                    line = lines [++i];
                } while (line.IndexOf("TargetAttributes = {") == -1);

                // 以下の内容はxcodeprojの内容にあるproject.pbxprojを参照してください
                newLines.Add("TargetAttributes = {");
                newLines.Add("xxxxxxxx = {");
                newLines.Add("DevelopmentTeam = xxxxxxxx;");
                newLines.Add("SystemCapabilities = {");
                newLines.Add("com.apple.BackgroundModes = {");
                newLines.Add("enabled = 1;");
                newLines.Add("};");
                newLines.Add("com.apple.Push = {");
                newLines.Add("enabled = 1;");
                newLines.Add("};");
                newLines.Add("};");
                newLines.Add("};");
                editFinish = true;

            } else {
                newLines.Add (line);
            }
        }

        File.WriteAllText(projPath, string.Join ("\n", newLines.ToArray ()));
    }
#endif
}

FirebaseAppDelegateProxyEnabled=NOに設定したので、個別ユーザへのPushが必要な場合は、didRegisterForRemoteNotificationsWithDeviceTokenでFIRInstanceID.instanceID().setAPNSTokenを呼ぶ必要があります。iOSでプッシュ通知を実装する方法の超詳細まとめ(前編)の図のように、APNから取得したDeviceTokenをFirebaseに送らないと、個別ユーザへのPush通知ができないためです。Unity Cloud Buildで自動化したいので、UnityのiOSでAppDelegateに処理を追加するを参考に、iOSのPluginを書くのが順当ですが、ハングする問題は修正中とのことなので、とりあえずUnity SDK 1.2を待ってもよいかもしれません。(2017/1/19追記 Unity SDK 1.1.1がリリースされ、この問題は修正されました。SDK 1.1.1を使用する場合、上記、PostBuildScriptにUserNotifications.frameworkを追加し、plist.root.SetBoolean("FirebaseAppDelegateProxyEnabled",false)をコメントアウトします)

わりと大変なので、Unity Cloud Buildのcocoapods対応か、Firebase Pluginのzip版を期待したいです。

OneSignalを使ってUnityにPush通知を実装する

UnityでPush通知を実装する場合、OneSignalを使うと便利です。無料で任意のセグメントのユーザに通知を送ることができます。

iOSの設定

(1) アプリケーションフォルダにあるキーチェーンアクセスを開きます
(2) メニューの「証明書アシスタント」から「認証局に証明書を要求」してディスクに保存します
(3) iOS Dev CenterのProvision Portalへ行き、Certificatesの項目で、Apple Push Notification Authentication Key (Sandbox & Production)を作成します
(4) aps.cerをダウンロードします
(5) キーチェーンアクセスに取り込んで、右クリックし、p12で書き出します(左のタブをログインに合わせておかないとp12で書き出せません)
(6) OneSignalに登録します

証明書は毎年更新が必要です。

Androidの設定

(1) Firebaseのプロジェクトを作成してFCMのサーバーキーを取得します
(2) OneSignalに登録します

Unityの設定

(1) OneSignalのUnity SDKをインポートします
(2) 以下のコールバックを任意のスクリプトに登録します(ONE_SIGNAL_API_KEYは自分のものに書き換えます)
(3) OneSignalのサイトからPush通知を発行すると、アプリを起動している場合、ダイアログが表示されたあと、HandleNotificationOpenedが呼ばれます

using System.Collections.Generic;  
void Start () {
    // Enable line below to enable logging if you are having issues setting up OneSignal. (logLevel, visualLogLevel)
    // OneSignal.SetLogLevel(OneSignal.LOG_LEVEL.INFO, OneSignal.LOG_LEVEL.INFO);
  
  OneSignal.StartInit(ONE_SIGNAL_API_KEY)
    .HandleNotificationOpened(HandleNotificationOpened)
    .EndInit();
  
  // Sync hashed email if you have a login system or collect it.
  //   Will be used to reach the user at the most optimal time of day.
  // OneSignal.syncHashedEmail(userEmail);
}

// Gets called when the player opens the notification.
private static void HandleNotificationOpened(OSNotificationOpenedResult result) {
}

尚、Unityからアプリを書き出した後、XcodeのCapabilityでPushNotificationとBackground ModesのRemote notificationsを有効化する必要があります。従って、Unity Cloud Buildではビルドは通りますが、動作しません。

UnityのAndroid向けビルドにおいて、Google.JarResolver.ResolutionException: Cannot resolve com.google.android.gms:play-services-gcm:9+() というエラーが出る場合は、Android SDK managerでAndroid Support RepositoryとGoogle Play servicesとGoogle Repositoryをアップデートして下さい。 また、JAVA_HOMEにJREへのパスを設定する必要があります。

少人数でソーシャルゲームを作るツールチェイン

フェイスブックが買収したWhatsAppが社員数50人で4億5000万人に対してサービスを提供できたように、開発環境の進化とクラウド化によって、少人数でも大規模なアプリケーションが開発できるようになってきました。

今回は、エンジニア視点で、少人数で大規模なソーシャルゲームの開発を行うために最適なツールチェインを考えてみます。

Unity5


昔はゲームを作るときはゲームエンジンから作っていたものですが、Unityの登場によって、ゲームのコアだけを記述すればよくなりました。iOSとAndroidのマルチプラットフォーム化が必須な今、開発環境としてのWindowsとMacの対応も考えると、Unityを使わないという選択肢はない気がします。nGUIとIAPの対応で、外部プラグイン不要で完結するようになってきましたし、どうしても不足する機能は自分でプラグインを書けば良いという安心感もあります。また、最新のMAYAのfbxへの対応など、何もしなくてもゲームエンジンがメンテナンスされていくというのは、自社エンジンにはない魅力です。

Maya LT でローポリキャラクタモデリングに挑戦して Unity で動かしてみた

Unity Cloud Build


Unity Cloud Buildを使うと、リポジトリにpushするだけで、自動的にiOSとAndroidのアプリをビルドすることができます。これにより、物理的に離れた場所にいたとしても、チームメンバー全員がいつでも最新版で動作確認することができます。開発はPCだけで行えばよく、実機を有線で接続する必要もないので、単純な開発効率も上がります。iOSアプリをWindowsだけで開発できるというのは革命的です。

Unity Cloud Buildの使い方

Bitbucket


GitのPrivateリポジトリを無料で作ることができます。Unity Cloud Buildとの連携を考えると、リポジトリはクラウドに持った方が便利です。

Unity向け .gitignoreの設定について

Slack


メールだと定型句が必要ですが、チャットだと重要な点だけを書けるのでコミュニケーションの効率が上がります。コミュニケーションの手段としてのメールは今後、衰退していく気がします。ChatWork、HipChatとも比較しましたが、Slackが一番、アプリの出来がよくできていました。

Google Docs


仕様書やドキュメントなどは、複数人同時に編集できるGoogle Docsで管理すると便利です。日付付きのWordファイルやExcelファイルをメールで送る必要はありません。ゲームのリソースもGoogle Driveでやり取りするとスムーズです。

Google AppEngine


少人数で運営することを考えると、サーバ運営をしているリソースはありません。Google AppEngineはDataStoreなど、プログラム側にかなり強い制約がかかりますが、その制約によって、原理的にAppEngineで動けば必ずスケールすることが保証されます。また、マネージドサービスなため、脆弱性の発覚による依存ライブラリのバージョンアップなども不要です。すなわち、サーバの保守が不要になります。

唯一、国内での実績が乏しいのが採用のネックだったのですが、メルカリ アッテがAppEngineを採用したことで、その障壁もなくなりました。さらに、2016年9月には待望の東京リュージョンが開設され、遅延が減ります。証明書なしでSSLが使えるのも魅力です。ゲームサーバなら独自ドメインがいらないので手軽です。

ゲームサーバへのサーバレスアーキテクチャの適用は、今後のトレンドになるのではないでしょうか。

SSL の設定
サーバーレスアーキテクチャという技術分野についての簡単な調査

Xcode7におけるリンクエラーへの対策

Xcode7において、Xcode6で作成したStatic Link Libraryをリンクしようとすると、以下の二種類のエラーが発生します。

Enable Bitcodeに関するエラー


Xcode7では、-fembed-bitcodeがデフォルトで有効になりました。そのため、Xcode6でビルドしたStatic Link Libraryをリンクしようとすると、以下のようなリンクエラーが発生します。

ld: '*.a' does not contain bitcode. You must rebuild it with bitcode enabled
(Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor,
or disable bitcode for this target. for architecture arm64


この問題を解決するには、Static Link Libraryに-fembed-bitcodeオプションを付けてビルドする必要があります。サードパーティのライブラリを使用していてリビルドできない場合は、Build OptionsのEnable BitcodeをNOにすることで回避することができます。Unity5.1から生成されるXcodeプロジェクトも同様のリンクエラーが発生しますが、Enable BitcodeをNOに設定することで回避することができます。

ただし、watchOS向けにビルドする場合は、Enable Bitcodeは必須です。Enable Bitcodeを有効にすると、バイナリサイズは概ね3倍に膨らみます。

Enable Bitcodeを有効にすると、生成バイナリにLLVMのビットコードが含まれるようになります。将来的に、新しいCPUアーキテクチャが追加された場合に、App StoreがLLVMのビットコードから自動的にリビルドしてくれるようになります。

参照:Xcode7GMでビルドすると「does not contain bitcode.」とか言われる

Universal Binaryに関するエラー


Xcode7でUniversal Binaryを使用して、シミュレータ向けにビルドしようとすると、以下のようなリンクエラーが発生します。

ld: in *.a, building for iOS simulator,  
but linking in object file built for OSX, for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)


Xcode6のStatic Link Libraryでは、iOS simulator向けのビルドにおいて、-mmacosx-version-min=XX が設定されていますが、Xcode7では-miphoneos-version-min=XX が設定されるようになりました。そのため、Xcode6でビルドした場合は、ターゲットプラットフォームのミスマッチが発生します。

この問題を解決するには、Static Link Libraryに-miphoneos-version-min=XXオプションを付けてビルドする必要があります。サードパーティのライブラリを使用していて、リビルドできない場合の回避方法はありません。

この変更は、armバイナリとx86バイナリがOSX向けかiOS向けかを明確に区別する必要が出てきたためだと考えられます。

参照:Xcode 7's New Linker Rules

OpenH264をビルドしてみる

OpenH264はCiscoが公開したH.264のソフトウェアライブラリです。BSDライセンスで自由に使えるだけでなく、DLLのバイナリをCiscoのサーバからダウンロードする形にすると、MPEG-LAへのロイヤリティを支払わなくてもよくなります。

ソースコードはGitHubで公開されています。MSVCでビルドする場合は、MinGWのMsysとnasmをインストールした上で、MakefileのOSをmsvcに書き換え、gnumakeでmakeするだけでよいです。ビルドして生成されたopenh264.libがStatic Link Library、openh264.dllとopenh264-dll.libがDynamic Link Libraryになります。

OpenH264はもともと、WebRTC向けにオープン化されたため、以下の制約があります。

・High Profileには非対応(CABACは使える)
・Bフレームには非対応

DLLのインタフェースはcodec/api/svcにあります。サンプルコードはtest/apiがわかりやすいです。画像形式はYUV411です。

機能が限定されている分、フレームを入れるとフレームが出てくるシンプルな構成で、使いやすいです。ただし、High Profileに非対応の関係で、x264でエンコードしたデータをデコードしたりするのには不向きです。現状だと、機能が限定されていても問題の少ない、エンコーダをアプリに組み込むには便利かもしれません。

最近、8x8 Transformがサポートされたように、High Profile対応は進んでいるようですので、長期的にはデコーダも広く使えるようになるかもしれません。

リリースノートはopenh264.orgで読めます。

FFMPEGよりもコードがシンプルで読みやすいので、将来性に期待しています。

Unity5でNative Pluginが動作しない場合に確認すべきこと

Unity4で動作していたNative PluginがUnity5で動作しない場合、以下を確認するとよいです。

・Androidのライブラリ名の確認

Unity4までは、Native Pluginの名前がlibhoge.soだった場合、[DllImport("libhoge")]でも[DllImport("hoge")]でも動作しました。対して、Unity5では[DllImport("hoge")]でないと動作しなくなりました。

・iOSのStatic Link Libraryのビルドオプションの確認

Unity4までは、Static Link LibraryのビルドにGNUのlibstdc++を使う必要がありました。対して、Unity5ではLLVMのlibc++を使用する必要があり、libstdc++ではリンクエラーが発生します。Unity5向けにlibc++を使用するには、Static Link Libaryのビルド時に、clangに-stdlib=libc++を追加してビルドする必要があります。

Unity5自身もUnity製のStatic Link Libraryを使用しているため、Unity5の書き出したXcodeのプロジェクトファイルをlibstdc++を使用するように書き換えてもリンクエラーが発生します。

既存のライブラリのリビルドが必要で、外部ライブラリを使用している場合は、わりと大変かもしれません。ただ、libstdc++に比べて、libc++はC++11対応など近代化が進んでいるので、移行する価値はあるかと思います。

・アドレス空間のオーバフローの確認

Native Pluginへの引数にIntPtr.ToInt32()+hogeとしている場合、64bitアドレスにデータが配置された場合にオーバフローが発生します。IntPtr.ToInt32をIntPtr.ToInt64に変更することで対処します。

YosemiteにEmscriptenを導入する

Emscriptenを使うと、C++のコードをJavaScriptに変換してブラウザで実行することができます。また、OpenGLのコードも自動的にWebGLに変換され、ブラウザで実行することができます。

Emscriptenは、C++をLLVMバイト・コードに変換した後、JavaScriptに落とします。そのため、LLVMとnode.jsに依存しています。Emscriptenはgitから直接インストールすることもできますが、LLVM3.4を要求されたり、fastcompを自分でビルドをしないといけなかったりします。公式のSDKを使うと、依存するライブラリ全てを自動的にインストールしてくれるので楽です。

Emscriptenのページから、Emscripten SDKをダウンロードします。

SDKのDownload

ダウンロードしたemsdk-portable.tar.gzを解凍してできたemsdk_portableで、emsdk update、emsdk install latest、emsdk activate latestを実行します。

1. Download and unzip the portable SDK package to a directory of your choice. This directory will contain the Emscripten SDK.
2. Open a command prompt to the directory of the SDK.
3. Run `emsdk update`. This will fetch the latest registry of available tools.
4. Run `emsdk install latest`. This will download and install the latest SDK tools.
5. Run `emsdk activate latest`. This will set up ~/.emscripten to point to the SDK.

インストールが終わると、emsdk_portableのフォルダ構成は以下のようになります。

README.md		emsdk			node
clang			emsdk_env.sh		zips
emscripten		emsdk_manifest.json

emsdk_portable/emscripten/1.27.0に移動します。hello.cppを作成します。

#include 

int main()
{
    printf("hello world\n");
}

以下のコマンドでコンパイルします。

./em++ -o hello.html hello.cpp

hello.htmlとhello.jsが生成されるので、hello.htmlをブラウザで開くと実行されます。

emscripten

hello.jsは356KBもあります。Looking through Emscripten outputにあるように、-O2 --closure 1を付けると89KBになりました。stdio.hをincludeしてもしなくても、これぐらいの容量になります。

スタック管理などの固定で消費するjsの容量が大きいので、C++のファイルを1つずつjsにするのではなくて、個別にバイト・コードにコンパイルした後、まとめてjsにするのがよさそうです。

External Interface的に、C++のコードからJavaScriptライブラリのメソッドを呼び出す場合は、--js-libraryオプションを使うとよいようです。(EmscriptenでC言語をJavaScriptに変換する

ネイティブとHTML5の共通フレームワークを作る場合は、C++でインタフェースを切っておいて、ネイティブ向けにはC++のコード、HTML5向けには--js-libraryで外部のJavaScriptコードを呼ぶ感じでいけそうです。

Mac OSのバージョンとlibstdc++とlibc++

b2でビルドしたBoost 1.57のStatic Link Libraryのboost::unit_test_frameworkをYosemiteで実行するとSegfaultが起きたので、メインコードのMakefileの-mmacosx-version-min=10.4を-mmacosx-version-min=10.9にすると動作するようになりました。

どうやら、Mac OS 10.6までをターゲットにビルドするとlibstdc++が使用され、Mac OS 10.7以降をターゲットにビルドするとlibc++が使用されるようです。libc++でC++11対応が入ったようですね。

Does homebrew use the option '-c++11' to mean '-stdlib=libc++'?

Xcode6でBoostをビルドすると、デフォルトでlibc++が使用されますが、メインコードで-mmacosx-version-min=10.4を指定するとlibstdc++が使用され、C++の例外周りのABIの互換性の問題か何かで、Segfaultが起きているようです。

リンクまで問題なく通ってしまうので、原因の把握にわりと時間がかかってしまいました。以下のように、メインコードと合わせて、Boostを-mmacosx-version-min=10.4でビルドすると、Segfaultは解消しました。


./b2 clean link=static threading=multi address-model=32_64 --layout=tagged cflags="-mmacosx-version-min=10.4" cxxflags="-mmacosx-version-min=10.4"
./b2 install link=static threading=multi address-model=32_64 --layout=tagged cflags="-mmacosx-version-min=10.4" cxxflags="-mmacosx-version-min=10.4"


また、リンク時は、-mmacosx-version-min=10.4だけだと、libc++とlibstdc++の両方がリンクされてしまうため、-stdlib=libstdc++を追加する必要があるようです。


-mmacosx-version-min=10.4 -stdlib=libstdc++


dylibの依存関係はotool -l hoge.dylibで確認できます。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

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

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