エイバースの中の人

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

Unity

Mac環境でlibtiffをビルドする

libtiffのダウンロードページからtiff-4.0.9.tar.gzをダウンロードします。
https://download.osgeo.org/libtiff/

./configureしたあとでmakeします。

libtiffフォルダに.libsフォルダが生成され、libtiff.5.dylibが生成されます。

デフォルトではlibtiff.5.dylibを/usr/local/libにインストールしようとするため、install_name_toolでrpath経由のパスに変更します。

install_name_tool -id @rpath/libtiff.dylib libtiff.5.dylib 
otool -L libtiff.dylib
libtiff.dylib:
@rpath/libtiff.dylib (compatibility version 9.0.0, current version 9.0.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.8)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.60.2)

MacのUnity Pluginからサードパーティのdylibを読み込む

MacのUnity Pluginはbundle形式になっています。bundleをビルドする際、bundleからサードパーティのdylibを読み込みたい場合があります。

通常の手順でdylibをリンクし、bundleをビルドした場合、otool -Lでbundleの依存関係を表示すると以下のようになります。

otool -L mybundle
mybundle:
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1454.90.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
@rpath/libthirdparty.dylib (compatibility version 0.0.0, current version 0.0.0)

このとき、@rpathからlibthirdparty.dylibが読み込まれるように設定されていますが、@rpathは$PATHなため、Unityから読み込めません。

そこで、install_name_toolで@rpathを@loader_pathに書き換えます。

install_name_tool -change @rpath/libthirdparty.dylib @loader_path/../Frameworks/libthirdparty.dylib mybundle

その上で、以下のようにContents/Frameworlsにdylibを配置すれば、dylibへのパスが通ります。

mybundle.bundle/Contents/MacOS/mybundle
mybundle.bundle/Contents/Frameworks/libthirdparty.dylib 

Unity+WindowsMRでメニューキーの入力を取得する

UnityでWindowsMRの開発を行う場合、通常はUWPでの開発が必要ですが、SteamVRをインストールしていると通常のStandaloneモードで開発することができます。その際、ProjectSettingsのXR SettingのVirtualRealitySupportのチェックボックスを入れるだけで、Main Cameraが自動的にステレオカメラになり、位置と方向が反映されます。

virtual_reality


コントローラの入力を取得する場合、スティックはInputManager、ボタンはInput.GetKeyを使用します。スティックの入力を取得するには、InputManagerで名前を登録した後、Input.GetAxis ("Horizontal2")のようにします。トリガーの入力を取得するには、Input.GetKey(KeyCode.JoystickButton14) のようにします。

input_manager


ここまで、開発は順調に進んでいたかのように見えたのですが、WindowsMRでメニューキーの入力を取得しようとしてトラブルが起きました。以下のサイトのキーコードに従って、Input.GetKey(KeyCode.JoystickButton6)としても、反応しないのです。

Input for Windows Mixed Reality

調査を進めたところ、WindowsMRでもStreamVRを使用している場合は、OpenVRのキーコードに従う必要があることがわかりました。具体的に、Input.GetKey(KeyCode.JoystickButton0)とするとキー入力が取得できました。

Input for OpenVR controllers

ヘッドマウントディスプレイは、Acer、Samsung Odyssey、Dell Visorの三種類を試しましたが、眼鏡に一番やさしいのはDell Visorでした。Oculus Goよりも余裕があり、セルフレームでも締め付けられません。



Dell Visorの箱はとても大きいので、以下のケースがおすすめです。

Unityのシェーダで右ビットシフトを行うとAndroidで不定となる

Unityのピクセルシェーダで右ビットシフト演算を行うと、floatでエミュレートされて実行されるため、Android(OpenGL ES3)における結果が不定になります。

vec = (vec >> 3);


この問題を回避するには、以下のように記述します。

vec = int(floor(vec/8.0));


また、右ビットシフト以外のビット演算は正常に動作します。

vec = (vec & 0x7);


Windows、Mac、iOSでは右ビットシフト演算も問題なく正確な値が取得可能です。

jslibで引数のポインタに値を書き込む

UnityでNativePluginと同様のインタフェースを持つjslibを作成することを考えます。

C#側で以下のCreate APIがあった時、JS側からhogeに値を書き込みたいとします。

 [DllImport("__Internal")]
 public static extern int Create(ref IntPtr hoge);

JS側はEmscriptenのsetValue APIで値を書き込むことができます。

Create : function(hoge)
{
  var some_value=1;
  setValue(hoge,some_value,"i32*");
}

同様に構造体へも書き込むことが可能です。

C#側の定義は以下です。

[DllImport("__Internal")]
public static extern int getInfo([In,Out] InfoStructinfo);

[StructLayout(LayoutKind.Sequential)]
public class InfoStruct
{
	public UInt32 width;
	public UInt32 height;
}

JS側の実装は以下です。

getInfo: function(info)
{
	setValue(info+0,i.width,"i32");
	setValue(info+4,i.height,"i32");
	return 0;
}

参考:Emscripten : preamble.js

Unity Collaborateでファイルが更新されない

PC1でCollaborateにアップロードした後、PC2でダウンロードした際、PC1ではアップロードに成功しているものの、PC2ではダウンロードできない場合があります。

その場合は、PC1で、Library/Collab/CollabSnapshot_*を削除した後、アップロードし直すことで、問題を解消することができます。

File is Missing bug

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

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を配置する必要があるかなと思います。

UnityWebRequest.Postで32KB以上のデータが送信できない

UnityWebRequest.Postに32KB以上のデータを与えると、UriFormatException: Uri is longer than the maximum {0} characters.が発生します。これは、UnityWebRequestのPostの内部でUri.EscapeDataStringを使用しており、本来は制約がないはずのPostのデータについても、Uriの32768文字制約が適用されるためです。具体的に、UnityのソースコードをデコンパイルしたSerializeSimpleFormにおいて、Uri.EscapeDataString(current.Value)を呼んでいることがわかります。

この問題を回避するには、Uri.EscapeDataStringを細かく呼ぶ以下のようなクラスを自作して、UnityWebRequest.Postを置き換える必要があります。

尚、検証に使用したUnityは5.5.4f1です。

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

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine.Scripting;
using UnityEngineInternal;

public class UnityWebRequestUnlimited : MonoBehaviour {
	private static string EscapeLongDataString(string stringToEscape)
	{
		var sb = new StringBuilder();
		var length = stringToEscape.Length;

		// 32767以上だとエラーになるため分割する
		var limit = 32767 - 1;
		for (int i = 0; i < length; i += limit)
		{
			sb.Append(Uri.EscapeDataString(stringToEscape.Substring(i, Math.Min(limit, length - i))));
		}

		return sb.ToString();
	}

	public static byte[] SerializeSimpleForm(Dictionary formFields)
	{
		string text = "";
		foreach (KeyValuePair current in formFields)
		{
			if (text.Length > 0)
			{
				text += "&";
			}
			text = text + Uri.EscapeDataString(current.Key) + "=" + EscapeLongDataString(current.Value);
		}
		return Encoding.UTF8.GetBytes(text);
	}

	public static UnityWebRequest Post(string uri, Dictionary formFields)
	{
		UnityWebRequest unityWebRequest = new UnityWebRequest(uri, "POST");
		byte[] data = null;
		if (formFields != null && formFields.Count != 0)
		{
			data = UnityWebRequestUnlimited.SerializeSimpleForm(formFields);
		}
		unityWebRequest.uploadHandler = new UploadHandlerRaw(data)
		{
			contentType = "application/x-www-form-urlencoded"
		};
		unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
		return unityWebRequest;
	}
}

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における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がソフトデコードになるので使いすぎない

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 によるモバイル アプリのカスタム分析

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の縦長ディスプレイはアプリごとに表示を変えられるので安心

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;
    }
Recent Comments
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