エイバースの中の人

株、不動産、機械学習など。

カテゴリ: Unity

自動検証でランダムに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;
    }

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

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を同時にビルドして、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 5.5.3f1で、インデックスPNGを使用すると、アルファ値が正しく取得できず、不透過のPNGとして扱われるようです。インデックスPNGは使用しない方がよさそうです。

Unity Cloud Buildでは、テクスチャのキャッシュが使用されるため、Unity 5.5.3fに切り替わった直後には気が付かず、キャッシュクリアされたReimportされた後に、問題が発生するため、Unity 5.5.3fが原因かの切り分けが難しくなっています。

尚、Unity 5.5.3p1で治っているようです。

(891365) - TextureImporter: Fixed a bug with some types of PNG files caused the texture importer to not detect the alpha channel properly. (Please re-import the affected assets to fix them.)

https://unity3d.com/jp/unity/qa/patch-releases

ただし、Unity 5.5.3p1はまだUnity Cloud Buildで使えないようです。

Unity 5.5.2のAndroidでアプリケーションからApplication.Quitを呼ぶと、Galaxyなど特定の機種において、libcで例外が起き、アプリケーションが停止されましたと表示されます。Unity 5.6では起きません。

対策としては、Application.Quitではなく、moveTaskToBackを呼ぶとよいようです。

#if UNITY_ANDROID
if (Input.GetKeyDown(KeyCode.Escape)){
	AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").
	GetStatic("currentActivity");
	activity.Call("moveTaskToBack" , true);
	return;
}
#endif

海外のフォーラムでは、StartCoroutineで描画終了を待てば改善するとありましたが、試してみても改善しませんでした。

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ファイルが生成されると、自由に移動可能になります。

-------------------------------------------
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版を期待したいです。

iPhone5Sと6は、メモリが1GBしかないため、UIの巨大画像がメモリを圧迫し、シーンの遷移で落ちることがあります。

SceneManager.LoadSceneの第二引数にLoadSceneMode.Singleを設定したとしても、シーン移行のタイミングでは、遷移前のシーンと、遷移後のシーンの両方のメモリを専有するようです。

最初は、テクスチャの参照カウントを0にすれば、LoadScene内でも解放されるはずだと期待して、循環参照になりそうなGameObjectにOnDestroyでnullを代入したりしていたのですが、一向に参照カウントが減少せず、仕様だと諦めました。

2シーンの合計となると、半分のメモリしか使用できないため、ゲームのクオリティが落ちてしまいます。この問題を解消するには、空のシーン、俗に言うローディング画面を間に挟むとよいようです。

遷移開始時は以下のようにします。

SceneManager.LoadScene ("loading");
Resources.UnloadUnusedAssets();
System.GC.Collect();

Updateの中で、loadingに遷移したタイミングで本当に行きたかったシーンに遷移します。

if(SceneManager.GetActiveScene().name=="loading"){
SceneManager.LoadScene("next_scene");
Resources.UnloadUnusedAssets();
System.GC.Collect();
}

これにより、iPhone5Sでも落ちなくなりました。

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へのパスを設定する必要があります。

UnityでuGUIを使用する場合、複数の解像度に対応するため、Canvas Scalerを割り当てます。

canvas_scaler

しかし、Canvas Scalerを与えたCanvasにおいて、子要素の座標をスクリプトから制御しようとした場合、transform.positionにCanvas Scalerの拡大率が考慮されないため、うまく制御できません。これは、Unity Editor上では、Canvas Scalerで指定した座標系で設定できるのが、スクリプトでは実座標系で設定することになるのが原因です。

スクリプトからも、Canvas Scalerで指定した座標系で値を設定するには、anchoredPosition3Dを使用します。

具体的に、
 gameObject.transform.position=v;
ではなく、
 gameObject.GetComponent<RectTransform>().anchoredPosition3D=v;
と記述することで、Canvas Scalerを適用しても、適切な位置に子要素を設定することができます。

2016年10月31日にリリースされたUnity IAP Store package 1.9から、コードを書かずにアプリ内課金を行えるようになりました。



- [Beta] Codeless IAP tools. Implement IAP by adding IAP Buttons to your project (Window > Unity IAP > Create IAP Button) and configure your catalog of IAP products without writing a line of code (Window > Unity IAP > IAP Catalog). Preliminary documentation is available [here](https://docs.google.com/document/d/1597oxEI1UkZ1164j1lR7s-2YIrJyidbrfNwTfSI1Ksc/edit).

Unity IAP Store package 1.9 is now available

ドキュメントはCodeless IAP Getting Started Guideにあります。

ServicesからUnity IAPを1.9に更新すると、ウィンドウメニューに、Window -> Unity IAP -> Create IAP Buttonが追加されます。選択すると、IAP Button(Script)の追加された、uGUIのボタンが配置されます。

shop

次に、IAP Catalogに、iTunes Connectで設定した課金アイテムのIDを設定します。エディターで実行すると、Fake Storeの購入ダイアログが出ます。

editor

課金が成功すると、On Purchase Completeが、課金がキャンセルされると、On Purchase Failedが呼ばれます。

次に、実機で実行してみたのですが、ボタンを押しても反応がありません。Unity Cloud Buildでは詳細なエラーが分からないので、Xcodeプロジェクトを書き出してみた所、Add the In-App Purchase feature to your App IDというエラーが出ていました。


unity_iap


どうやら、iTunes ConnectのConrtacts、Tax、Bankingを登録しないと、課金APIのテストはできないようです。


You will not be able to test any StoreKit functionality until you have an iOS Paid Applications contract – StoreKit calls in your code will fail until Apple has processed your Contracts, Tax, and Banking information.

Part 1 - In-App Purchase Basics and Configuration


登録が終わったら、実機テストをします。iTunesConnectでの審査前でもテストは可能です。ただし、iOSでの実機テストでは、SandBoxテスターのアカウントでないと、以下のようなエラーで失敗します。



2017-01-06 16:46:29.171848 appid[261:19234] UnityIAP:Requesting product data...
2017-01-06 16:46:30.103910 appid[261:19234] UnityIAP:Received 1 products
2017-01-06 16:46:30.144066 appid[261:19234] UnityIAP:No App Receipt found
2017-01-06 16:46:30.150693 appid[261:19234] UnityIAP:addTransactionObserver
IAPButton.PurchaseProduct() with product ID: gacha1
UnityEngine.Purchasing.IAPButton:PurchaseProduct()
UnityEngine.Events.InvokableCallList:Invoke(Object[])
UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()
UnityEngine.EventSystems.StandaloneInputModule:Process()

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

2017-01-06 16:46:30.310534 appid[261:19234] UnityIAP:PurchaseProduct: gacha1
2017-01-06 16:46:30.331487 appid[261:19234] UnityIAP:UpdatedTransactions
purchase({0}): gacha1
UnityEngine.Events.InvokableCallList:Invoke(Object[])
UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()
UnityEngine.EventSystems.StandaloneInputModule:Process()

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

2017-01-06 16:46:31.806437 appid[261:19234] UnityIAP:UpdatedTransactions
2017-01-06 16:46:31.806652 appid[261:19234] UnityIAP:PurchaseFailed: 0
onPurchaseFailedEvent({0}): gacha1
UnityEngine.Purchasing.PurchasingManager:OnPurchaseFailed(PurchaseFailureDescription)
UnityEngine.Purchasing.AppleStoreImpl:ProcessMessage(String, String, String, String)
UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

IAPButton.OnPurchaseFailed(Product UnityEngine.Purchasing.Product, PurchaseFailureReason Unknown)
UnityEngine.Purchasing.IAPButton:OnPurchaseFailed(Product, PurchaseFailureReason)
UnityEngine.Purchasing.IAPButtonStoreManager:OnPurchaseFailed(Product, PurchaseFailureReason)
UnityEngine.Purchasing.AppleStoreImpl:ProcessMessage(String, String, String, String)
UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)


iTunes ConnectでSandBoxテスターを作成する際は、gmailのエイリアスを使うと便利です。iPhoneの設定のiTunesからApple IDをログアウトした後、テストアプリを起動、IAP Buttonをタップするとログインを要求されるので、SandBoxテスターとしてログインします。これで、課金のテストができます。

参考サイト:Unity IAPを使ってて思ったこと審査前にテスト課金をする

GPUを使用してフィルタリングを実装する場合、ピクセルシェーダもしくはコンピュートシェーダを使用することができます。

ピクセルシェーダは最も基本的なシェーダであり、テクスチャとパラメータを入力して演算した後、1画素を出力することができます。例えば、ピクセルシェーダで5x5タップのフィルタを実装する場合、25画素を取得して、1画素を返すことになります。テクスチャはランダムリードはできますが、ランダムライトはできません。また、出力は1画素に限定されます。

コンピュートシェーダでは、ピクセルシェーダの機能に加えて、テクスチャへのランダムライトと、共有メモリが使用可能です。また、出力は1画素に限定されません。コンピュートシェーダで5x5タップのフィルタを実装する場合、複数画素の処理をまとめて行うことで、テクスチャのフェッチ回数を削減し、ピクセルシェーダよりも高速化が可能です。例えば、16x16画素をまとめて共有メモリに格納した後、スレッドIDに応じて、5x5タップのフィルタを並列で実行し、12x12画素を書き込むことができます。これにより、ピクセルシェーダでは25in:1outだったテクスチャフェッチ比率を、256in:144out=1.7in:1outまで落とすことができます。

compute

コンピュートシェーダの処理の単位はスレッドで、複数のスレッドが集まってスレッドグループを構成します。共有メモリはスレッドグループ内でのみアクセス可能です。シェーダコードには、3次元で何個のスレッドでグループを構成するかを記述します。共有メモリのアクセス管理はメモリバリアで、GroupMemoryBarrierWithGroupSync();を読んだタイミングで全てのスレッドが完了するまで待ちます。テクスチャのアクセス座標は、スレッドに割り当てられたIDから計算します。

コンピュートシェーダは、ピクセルシェーダよりも初期設定が面倒ですが、Unityを使うと簡単に実験することができます。テクスチャへのランダムアクセスもRenderTextureにrandomAccessフラグを付けるだけでよく、簡単です。ただし、Unityでコンパイル済みのシェーダを使用する方法が存在しないため、シェーダプログラムを秘匿したい場合は、Render PluginとしてDLLを呼び出して、中でfxcを読み込むしかなさそうです。

Render Pluginなど、Unityを使用せず、直接、Direct Computeを使用する場合、ランダムライトが発生するテクスチャには、ShaderResourceViewではなく、UnalignedResourceViewが必要です。UnalignedResourceViewはピクセルシェーダでは読めないので、1つのテクスチャに対して両方のビューを作っておくことで、コンピュートシェーダで書き込み、ピクセルシェーダで読み込むことが可能です。UnityのNativeTexturePtrからもUnalignedResourceViewは作成可能です。

フェイスブックが買収した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 の設定
サーバーレスアーキテクチャという技術分野についての簡単な調査

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に変更することで対処します。

ZigfuはUnityでKinectを扱えるようにするアセットです。オープンソースのセンシングAPIであるOpen NIを使っているため、Macでも動作します。また、DLLを使用していないため、Unity Freeでも動作します。商用利用しない場合は、Xbox360版のKinectを使うと、中古で7000円程度で手に入るのでリーズナブルです。ZigfuのMacへの導入は、[Unity] Unity+Zigfu導入 | CHO DESIGN LABが詳しいです。

miku
(モデルはTda式初音ミク・アペンドをお借りしました)

ZigfuにはサンプルでDanaというモデルが使われていますが、MMD4MecanimでMMDモデルを読み込むことができます。ただし、Zigfuはデフォルトのモデルで手を左右に真横に開いていることを想定しています。そのため、MMDのモデルを使う場合、モデルのRightShoulderとLeftShoulderのZ方向に40度程度の回転を行い、手が真横を向くように設定しておく必要があります。

shoulder

その後、スケルトンを以下のように設定すると、概ね、うまく動くようになります。

zigfu_skelton

Kinect でお手軽に頭の位置とカメラ位置を連動させて Oculus Rift をもっと楽しむのようにカメラを置くと、主観視点にすることもできます。

UnityでGoogle Cardboardに対応したアプリを開発する場合、描画部分はDurovis DiveのSDKを使えばよいのですが、Durovis Diveにはマグネットボタンが存在しないため、マグネットボタンへの対応は自分で行う必要があります。

マグネットボタンの判定には、Input.compass.rawvectorを使います。rawvectorにどのような値が入るかは、Androidアプリの内蔵センサーで計測することができます。

CardboardにNexus5を入れた状態で、マグネットボタンを押していない場合、左側面が受ける磁気強度は100マイクロテスラ程度ですが、マグネットボタンを押すと、500マイクロテスラ程度の大きな値になります。従って、以下のように、磁気強度が大きくなった点を検出すれば、ボタンが押されたかを検出することができます。

Vector3 now=Input.compass.rawVector;
int thre=250;
if(Mathf.Abs(prev.x)<thre && Mathf.Abs(now.x)>=thre){
Debug.Log("Hit");
}
prev=now;


磁気強度は磁石によると思うので、しきい値には、ある程度のマージンを設定しておく必要があります。

Google CardboardのAPIはUnityに対応していません。しかし、Durovis DiveがGoogle Cardboardと同じ原理で動作するため、Durovis DiveのSDKを使うと、簡単にGoogle Cardboard用のアプリを開発することができます。

まず、Durovis Diveの”Download the Plugin Package: Dive Unity Plugin Package 1.6 for Android/ iOS.”からプラグインをダウンロードします。Oculusと違って、PCではネイティブプラグインを使用していないため、無料版のUnityだけでコンテンツを開発することができます。

Dive Unity Pluginにはジャイロセンサーに対応したカメラが含まれているため、既存のカメラを置き換えるだけでVR対応が可能です。

dive_camera

試しに、Unity Chanのアセットにカメラ(Assets/Dive/Dive_Camera)を置いてみました。これをAndroid向けにビルドするだけでUnity Chanを眺めることができます。Build SettingのDefault OrientationはLandscape Leftに設定して下さい。

IMG_1136

デフォルトだと、ソフトキーが表示されて中央寄せにならないので、ソフトキーを非表示にするために、HOW TO ENABLE “IMMERSIVE MODE” FOR ANDROID APPS IN UNITYの一番下にある、DOWNLOAD UNITY PACKAGE FOR EASY IMPORT INTO PROJECTからUnity Packageをインストールするとよいようです。

MMD向けのコンテンツをVRで手軽に楽しめる日は近いと思います。

---
2015/6/30追記

Google Cardboard SDKに、公式のUnity Pluginが追加されたので、今からVRコンテンツを作成するならCardboard SDKがおすすめです。Dive SDKはlibc++に対応していないため、Unity5のiOSビルドでリンクエラーが発生します。Cardboard SDKの場合、libc++に対応しているので、Unity5でも使用できます。テクスチャが点滅する場合は、Development Buildのチェックを有効にする必要があるようです。

Oculus Rift SDKにはUnity用のPrefabが含まれているため、簡単にUnityアプリをOculus対応にすることができます。

まず、Oculus Riftのデベロッパーサイトでデベロッパー登録を行い、Unity 4 Pro Integrationをダウンロードします。

oculus


中に含まれているOVRフォルダをUnityのプロジェクトにドロップします。Prefabsフォルダに含まれているOVRPlayerController.prefabをHierarchyにドロップします。これで終わりです。

ovr


OVRPlayerController.prefabは、OVRCameraController.prefabを内包しています。カメラだけを使いたい場合はOVRCameraController.prefabを、カメラの移動まで行いたい場合はOVRPlayerController.prefabを使用します。既存アプリを置き換える場合、現在使用しているカメラをOVRPlayerController.prefabで置き換えるのが簡単です。

test


以上の操作で、自動的にOculus用のレンダリングが行われます。Oculusを接続している場合は湾曲してレンダリングされ、接続していない場合は普通にレンダリングされます。

尚、HDMI分配器を使用した場合、Oculusを認識できず、正常にレンダリングできないので注意して下さい。Oculusのライブラリ内部で、HMDが接続されているかどうかのチェックが行われています。

また、Oculusのライブラリは、内部で、WindowsのDLLと、Mac OSXのbundleを呼んでいるため、Unity Proのライセンスが必要です。

↑このページのトップヘ