sunagimoブログ

主にUnityに関する技術を取り上げます

【Unity】UniRxを覚える(3)

前回の記事。

sunagimo-app.hatenablog.com




コルーチン

UnityのコルーチンをIObservableに変換

Observable.FromCoroutine


コルーチンの終了タイミングをストリームで扱うことが可能

    void Start()
    {
        Observable.FromCoroutine(TestCoroutine)
            .Subscribe(
                e => {
                    Debug.Log("next");
                },
                () => {
                    Debug.Log("complete");
                }    
            ).AddTo(gameObject);
    }

    IEnumerator TestCoroutine()
    {
        Debug.Log("start");
        yield return new WaitForSeconds(4);
        Debug.Log("finish");
    }



start->finish->next->Complete


コルーチンから結果を返す

Observable.FromCoroutineValue

    void Start()
    {
        Observable.FromCoroutineValue<int>(ReturnValueCoroutine)
                    .Subscribe(e => Debug.Log(e));
    }

    IEnumerator ReturnValueCoroutine()
    {
        for(var idx = 0; idx < 10; ++idx)
        {
            yield return idx;
        }
    }


0から9まで出力


コルーチン内でOnNextを発行

    void Start()
    {
        Observable.FromCoroutine<int>(observer => CallOnNextCoroutine(observer))
                .Subscribe(
                    e => {
                        Debug.Log(e);
                    }
                ).AddTo(gameObject);
    }

    IEnumerator CallOnNextCoroutine(IObserver<int> observer)
    {
        observer.OnNext(1);
        yield break;
    }



まとめ

3記事に分けて学習していきました。
おそらく入門程度の知識を身についたと思います。

ほぼすべての処理を初期化で行うのでどのくらいの負荷がかかるのかとか、
チームで導入するには学習コストが高いなどの気になるところはあります。

ただその分、今まで何十行もかけて書いてた処理を数行でかけるようになったりと、
可読性もあがります。


私個人としてはとても便利だと思うので、
ここから更に理解を深めて積極的に使っていきたいです。



参考にさせていただいたサイト様

qiita.com

【Unity】UniRxを覚える(2)

前回の記事。

sunagimo-app.hatenablog.com



ストリーム

UniRxを覚える(1)で学んだ、
Subjectを定義して、OnNextとして、Subscribeに到達
という流れをストリームと呼びます。


Dispose

IObservableのSubscribeはIDisposableというインターフェースで実装されています。
これはリソースの開放を行うためのもので、
Dispose()のみ定義されています。

SubjectでDisposeを実行すると、ストリームの購読を終了することができます。

    /// <summary>
    /// UniRx Subject。
    /// </summary>
    public Subject<string> subject = new Subject<string>();

    void Awake()
    {
        // UniRx Subject登録。
        subject
        .Subscribe(
            message => {
                Debug.Log(message);
            },
            () => {
                Debug.Log("Completed");
            }
        );
    }

    void Start()
    {
        // UniRx Subject呼び出し。
        subject.OnNext("UniRx Hello!");
        subject.Dispose();
        subject.OnNext("UniRx Hello!");
        subject.OnCompleted();
    }


Disposeのあとの処理は呼び出されません。


ストリームのライフサイクル

Subjectが破棄されればストリームも全て破棄されます。
Subjectが残っていればストリームは動き続けます。

ストリームは使い終わったら必ずDisposeを呼ぶ
またはOnCompletedを発行するようにする。

AddTo

AddToを実装すれば、GameObjectが破棄されたタイミングで、
自動的にDisposeを行ってくれます。

        // UniRx Subject登録。
        subject
        .Subscribe(
            message => {
                Debug.Log(message);
            },
            () => {
                Debug.Log("Completed");
            }
        ).AddTo(gameObject);


ストリームのソース

Subject

自由にイベントを発行したいとき。

ReactiveProperty

変数にSubjectの機能をくっつけたもの。

        var reactiveProperty = new ReactiveProperty<string>("sample");
        reactiveProperty.Value = "Hello";
        reactiveProperty.Subscribe(e => Debug.Log(e));
        reactiveProperty.Value = "hello";


ReactiveCollection

状態の変化を通知する機能が内蔵されたList< T >

        var reactiveCollection = new ReactiveCollection<int>();
        reactiveCollection
            .ObserveAdd()
            .Subscribe(x => {
                Debug.Log("Add Index: " + x.Index + " Value: " + x.Value);
            });
        
        reactiveCollection
            .ObserveRemove()
            .Subscribe(x => {
                Debug.Log("Remove Index:" + x.Index + " Value: " + x.Value);
            });

        reactiveCollection.Add(1);
        reactiveCollection.Add(2);
        reactiveCollection.Remove(2);


Observable.Create

自由に値を発行するストリームを作ることができるファクトリメソッド。

        Observable.Create<int>(ob => {
            ob.OnNext(1);
            ob.OnCompleted();
            return Disposable.Create(() => {});
        }).Subscribe(e => Debug.Log(e));


Observable.Start

別スレッドで実行して結果を一つだけ発行するファクトリメソッド。
非同期で走らせ結果が出たら通知。

        Observable.Start(() => {
            var count = 0;
            for(var idx = 0; idx < 10000; ++idx)
            {
                count += idx;
            }
            return count;   
        })
        .ObserveOnMainThread()
        .Subscribe(e => Debug.Log(e));
        Debug.Log("mainThread");

mainThreadの文字列が先に発行される。

Observable.Timer/TimerFrame

一定時間後にメッセージを発行するファクトリメソッド。
実時間はTimer、フレーム数での指定はTimerFrame

        Observable.Timer(System.TimeSpan.FromSeconds(3))
        .Subscribe(e => Debug.Log("elapsed 3sec"));

        Observable.Timer(System.TimeSpan.FromSeconds(3), System.TimeSpan.FromSeconds(1))
        .Subscribe(e => Debug.Log("elapsed 3sec and repeat 1sec"))
        .AddTo(gameObject);

第一引数に発行されるまでのTimeSpan、第二引数に繰り返し発行するTimeSpanを指定できる。

Observable.EveryUpdate

Update時に通知してくれるストリームソース。
停止しない限りシーンをまたいでも動き続けます。

        Observable.EveryUpdate().Subscribe(e => Debug.Log("every update"));



UniRx.Triggers

UnityのコールバックイベントをUniRxのIObservableに変換してくれる。

        this.FixedUpdateAsObservable()
            .Subscribe(e => Debug.Log("fixed update"));



uGUIイベントのストリームソース

クリックイベント

        [SerializeField]
        Button button = null;

        button.OnClickAsObservable().Subscribe(e => Debug.Log("onclick"));



(3)に続きます。
sunagimo-app.hatenablog.com




参考にさせていただいたサイト様

qiita.com


qiita.com

【Unity】UniRxを覚える(1)

UniRxとは


C#のライブラリのReactive ExtensionsをUnity向けに作られたライブラリ
デザインパターンの一つのObserverパターンがベースになっています。


Subject


UniRxにおいて、重要なもの。
Subjectがこれまで使ってきた、eventの役割になる。


そしてSubjectの中でも重要な2つの処理が、
SubscribeとOnNext。

Subscribe

メッセージ受取時に実行する処理を登録

OnNext

Subscribeに登録された処理に渡して処理を実行


Subject、Subscribe、OnNextの処理を今までのイベント処理で書くとこんな感じになる。


UniRx実装前

    /// <summary>
    /// イベント。
    /// </summary>
    /// <param name="message">メッセージ。</param>
    public event Action<string> OnDisplayMessage = delegate(string message){};

    void Awake()
    {
        // 登録。
        OnDisplayMessage += (message) => {
            Debug.Log(message);
        };
    }

    void Start()
    {
        // 呼び出し。
        OnDisplayMessage("Hello!");
    }



UniRxを使うとこのように書き直せる。

UniRx実装後

    /// <summary>
    /// UniRx Subject。
    /// </summary>
    public Subject<string> subject = new Subject<string>();

    void Awake()
    {
        // UniRx Subjectへ登録。
        subject.Subscribe(message => {
            Debug.Log(message);
        });
    }

    void Start()
    {
        // UniRx Subject呼び出し。
        subject.OnNext("UniRx Hello!");
    }




IObserverとIObservable


SubjectはIObserverとIObservableの2つのインターフェースを実装しています。

IObserver

イベントメッセージの発行を担う

OnCompleted
OnError
OnNext

の3つのメソッドが定義されている。

IObservable

イベントメッセージを購読を担う

Subscribe
のメソッドが定義されている。


先程の処理に、OnNext、OnError、OnCompletedを追加するとこのように定義できる。

    /// <summary>
    /// UniRx Subject。
    /// </summary>
    public Subject<string> subject = new Subject<string>();

    void Awake()
    {
        // UniRx Subject登録。
        subject.Subscribe(
            message => {
                Debug.Log(message);
            },

            error => {
                Debug.LogError("Error");
            },

            () => {
                Debug.Log("Completed");
            }
        );
    }

    void Start()
    {
        // UniRx Subject呼び出し。
        subject.OnNext("UniRx Hello!");
    }



オペレータ


SubjectとSubscribeの間に挟み込んでメッセージを処理する部品のこと。
LINQのWhereオペレータみたいなもの。

    /// <summary>
    /// Linqサンプル。
    /// </summary>
    public List<int> linqSampleList = new List<int>();

    void Awake()
    {
        linqSampleList.Add(1);
        linqSampleList.Add(2);
        linqSampleList.Add(3);
        linqSampleList.Add(4);
    }

    void Start()
    {
        var resultList = linqSampleList.Where(e => e % 2 == 0).ToList();
        for(var idx = 0; idx < resultList.Count; ++idx)
        {
            Debug.Log(resultList[idx]);
        }
    }


UniRxの代表的なオペレータ

Where:フィルタリング
Select:メッセージを変換
Distinct:重複を排除
Buffer:一定個数をまとめるまで待つ
ThrottleFirst:短時間にまとめてきた場合に先頭のみを使う

LINQライクな書き方


Whereを使用する

    /// <summary>
    /// UniRx Subject。
    /// </summary>
    public Subject<string> subject = new Subject<string>();

    void Awake()
    {
        // UniRx Subject登録。
        subject
        .Where(
            e => e == "UniRx Hello!"
        )
        .Subscribe(
            message => {
                Debug.Log(message);
            },

            error => {
                Debug.LogError("Error");
            },

            () => {
                Debug.Log("Completed");
            }
        );
    }

    void Start()
    {
        // UniRx Subject呼び出し。
        subject.OnNext("UniRx Hello!");
        subject.OnNext("UniRx Hello1!");
        subject.OnNext("UniRx Hello2!");
    }



"UniRx Hello!"のみ出力される


AwakeやStartへの負担が気になる…


続き
sunagimo-app.hatenablog.com




参考にさせていただいたサイト様


qiita.com

【Unity】Addresable Asset Systemを使ってみる

本記事で扱うUnityのバージョンは「Unity2018.3.7f1」です。
Addresable Asset Systemのバージョンは「Version 0.6.7 Preview」です。



Addresable Asset Systemについて


今までの複雑だったAssetBundleのビルドやロードをよりシンプルにしたAsset管理システム

とにかくコードを書く量が少なく済み、環境によって書き換えたり、依存関係を気にすることがなくなる



使い方


導入編

まだPreview版なので、Unity2018.2以降のPackageManagerよりインストール可能です。

f:id:sunagimo_app:20190313024929p:plain


インストール後、UnityのWindow→Asset Management→Addresable Assetsを開きます。

f:id:sunagimo_app:20190313025051p:plain


はじめは設定が必要なので、
Create Addresables Settingを選択して、
設定ファイルを作成します。
f:id:sunagimo_app:20190313030709p:plain


作成後、このようなウインドウが開かれます。

f:id:sunagimo_app:20190313025138p:plain

ここで主にAssetを管理します。


Assetの登録

登録したいAssetをそのままAddresablesのウインドウに、ドラッグアンドドロップするか、
AssetのInspectorのAddresableにチェックを入れると登録されます。


Assetを選択してドラッグアンドドロップ
f:id:sunagimo_app:20190313025633p:plain


もしくはチェックマークを入れる
f:id:sunagimo_app:20190313025715p:plain


登録されるとこのようになります。
f:id:sunagimo_app:20190313025753p:plain



Addresableで使用するAsset名が長いのでシンプルにします。
f:id:sunagimo_app:20190313025828p:plain

対象のAssetを選択して、Simplify Entry Namesを選択。


Addresableで管理したいディレクトリを丸ごと持ってくるということは現状ではできないので、
今後追加されることを祈るか、自前で実装という感じですね。


一応これで準備は整いました。


Addresableのモード


Addresableの動作モードを説明します。
f:id:sunagimo_app:20190313030338p:plain


Play Mode Scriptを選択すると、各モードが選択できます。


・Fast
パッキングせずに、ファイルを直接ロード
・Virtual
パッキングせずに、実際にパッキングしたときの動作をシミュレート
・Packed
パッキングして、AssetBundleからロード


Assetの読み込み


実際にコードを書いていきます。

 Addressables.LoadAsset<Sprite>(assetName)
       .Completed += (op) => {
             img.sprite = op.Result;
       };


非同期で読み込みます。
渡す引数としては、「Asset名(assetName)」の他に、
「AssetReference」というのも渡せます。

こちらは、Addresableの対象となっているAssetを直接選択することで、
入力する手間やミスを防ぐことができます。


※ver0.6.6以降から
AssetReferenceTypeRestrictionのアトリビュートを指定して、
アセットのタイプを指定できたのですが、
いろいろ変更があったらしく、処理自体が消えました。

f:id:sunagimo_app:20190313032140p:plain


docs.unity3d.com



ver0.6のAPIReferenceも消えているので、今後どうなるのかわかりませんが、
おそらくそれぞれの指定できるTypeのAssetReferenceを呼ぶ必要があるのかと。

f:id:sunagimo_app:20190313032601p:plain



    [Header("Asset参照(Sprite)")]
    [SerializeField]
    private AssetReferenceSprite assetReference = null;

    /// <summary>
    /// 参照読み込み。
    /// </summary>
    void ReferenceLoad()
    {
        Addressables.LoadAsset<Sprite>(assetReference)
                    .Completed += (op) => {
                        img.sprite = op.Result;
                    };
    }


こんな感じでInspectorから指定できます。
f:id:sunagimo_app:20190313032944p:plain


Assetの解放

Addresables.ReleaseAssetで参照したAssetの解放が行えます。

    /// <summary>
    /// アセットリスト。
    /// </summary>
    List<Sprite> assetList = new List<Sprite>();

    [Header("Asset参照(Sprite)")]
    [SerializeField]
    private AssetReferenceSprite referenceSprite = null;
    
    /// <summary>
    /// 参照読み込み。
    /// </summary>
    void ReferenceLoad()
    {
        if(referenceSprite == null)
        {
            return;
        }

        Addressables.LoadAsset<Sprite>(referenceSprite)
                    .Completed += (op) => {
                        img.sprite = op.Result;
                        assetList.Add(op.Result);
                    };
    }

    /// <summary>
    /// 全ての参照を解放。
    /// </summary>
    void AllReleaseAsset()
    {
        for(var i = 0; i < assetList.Count; ++i)
        {
            Addressables.ReleaseAsset(assetList[i]);
        }
    }


参照しているAssetの情報などはAddresable Profilerより確認できます。

f:id:sunagimo_app:20190313033434p:plain

f:id:sunagimo_app:20190313033459p:plain


実際に読み込むとグラフとして表示され、下にログも残ります。
f:id:sunagimo_app:20190313033531p:plain


解放すると参照が消えて、色が薄くなります。
f:id:sunagimo_app:20190313033700p:plain



まとめ


今までのAssetBundleのシステムと比べるととてもシンプルになりました。
Profilerなども用意してあり、一々ログを仕込まなくてもよくなりました。

まだPreviewということなので、今後も変更があるとは思いますが、
よりよい変更になることを期待してます。


今回のプロジェクト


github.com





参考にさせていただいたサイト様


qiita.com
qiita.com
kan-kikuchi.hatenablog.com

【Unity】Sprite Atlasの実装(Assetbundleにも対応)

本記事で扱うUnityのバージョンは「Unity2018.3.7f1」です。



Sprite Atlasとは


docs.unity3d.com


Spriteをパッキングする機能。
複数枚の画像を一つの画像にする。



メリット


処理が軽くなります。
描画回数(Set Pass Calls)を節約できます。

こんな感じに


画像4枚をそのまま配置 → +4のSetPassCalls
画像4枚をSpriteAtlas化 → +1のSetPassCalls



作成方法


Project → Create → Sprite Atlasを選択

f:id:sunagimo_app:20190311004212p:plain



spriteAtlasファイルが作られます

f:id:sunagimo_app:20190311004246p:plain


Sprite Altasの設定

f:id:sunagimo_app:20190311004909p:plain


・Type
Masterのまま
・Include in Build
AssetBundleのときはチェックを外す 
・AllowRotation
uGUIで使用する際に表示崩れが起きるのでチェックは外す。本来はアトラスのサイズを抑えるためのもの。
・Tight Packing
uGUIで使用する際に表示崩れが起きるのでチェックは外す。本来はアトラスのサイズを抑えるためのもの。
・Padding
4のまま

パッキング

Objects for Packingという項目から、+ボタンでパッキングするSpriteを選択します。
f:id:sunagimo_app:20190311005143p:plain


選択できたらPack Previewを選択します。
f:id:sunagimo_app:20190311005238p:plain


こんな感じでパッキングされます。
f:id:sunagimo_app:20190311005300p:plain




使用方法


パッキングした元のSpriteを使用します。
f:id:sunagimo_app:20190311005829p:plain


SpriteRendererの場合はそのまま配置、
uGUIの場合はImageにつけたりと通常のSpriteと使用方法は同じです。
f:id:sunagimo_app:20190311010016p:plain


SetPass calls

【実行前】
Game ViewのStatsより確認できます。
実行前はまだAtlasの効果は出ていません。
f:id:sunagimo_app:20190311010220p:plain


【実行後】
実行するとSetPass calls減っているのがわかります。
・カメラ +1
・Atlas +1
f:id:sunagimo_app:20190311010231p:plain


uGUIだと実行前から効果がでています。
なぜか。

スクリプトから読み込む方法

もちろんScriptからも参照可能です。
ご参考に。
docs.unity3d.com




おまけ


AssetBundleでの使用方法

AssetBundleで使用する場合は、Sprite Atlasの設定のInclude in Buildのチェックを外します。

f:id:sunagimo_app:20190311010916p:plain


AssetBundleを読み込む際に、
SpriteAtlasManagerのatlasRequestedを使用します。
docs.unity3d.com


サンプルコード

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

public class SpriteAtlasLoad : MonoBehaviour
{
    /// <summary>
    /// アセットバンドル名。
    /// </summary>
    [Header("アセットバンドル名")]
    [SerializeField]
    string assetBundleName = null;

    /// <summary>
    /// アセット名。
    /// </summary>
    [Header("アセット名")]
    [SerializeField]
    string assetName =null;

    [Header("イメージ")]
    [SerializeField]
    Image img = null;
    
    /// <summary>
    /// AssetBundle。
    /// </summary>
    AssetBundle assetBundle;

    void Awake()
    {
        assetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + assetBundleName);
        img.enabled = false;
    }

    void OnEnable()
    {
        SpriteAtlasManager.atlasRequested += OnAtlasRequested;
    }

    void OnDisable()
    {
        SpriteAtlasManager.atlasRequested -= OnAtlasRequested;
    }

    void OnAtlasRequested(string tag, System.Action<SpriteAtlas> atlasCallback)
    {   
        var request = assetBundle.LoadAssetAsync<SpriteAtlas>(assetName);
        request.completed += (operation) => {
            atlasCallback.Invoke((SpriteAtlas)request.asset);
            img.enabled = true;
        };
    }
}




一応普通に読み込んだSpriteAtlasから参照して取れますが、
Warningが出ます。


サンプルコード

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

public class SpriteAtlasLoad : MonoBehaviour
{
    /// <summary>
    /// アセットバンドル名。
    /// </summary>
    [Header("アセットバンドル名")]
    [SerializeField]
    string assetBundleName = null;

    /// <summary>
    /// アセット名。
    /// </summary>
    [Header("アセット名")]
    [SerializeField]
    string assetName =null;

    /// <summary>
    /// スプライト名。
    /// </summary>
    [Header("スプライト名。")]
    [SerializeField]
    string spName = null;

    /// <summary>
    /// イメージ。
    /// </summary>
    [Header("イメージ")]
    [SerializeField]
    Image img = null;
    
    /// <summary>
    /// AssetBundle。
    /// </summary>
    AssetBundle assetBundle;

    void Awake()
    {
        assetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + assetBundleName);
    }

    void Start()
    {
        var request = assetBundle.LoadAssetAsync<SpriteAtlas>(assetName);
        request.completed += (operation) => {
            var atlas = (SpriteAtlas)request.asset;
            var atlasSp = atlas.GetSprite(spName);
            img.sprite = atlasSp;
        };
    }
}

f:id:sunagimo_app:20190311015439p:plain

atlasRequestedを使えと!



まとめ


モバイルで作る際にはどうしても必要な機能かと思います。


AssetBundleで使う際は少し癖はあるかと思いますが、
使い方さえ間違わなければとても強い味方。



使用した素材


yurudora.com


今回のプロジェクト


github.com




参考にさせていただいたサイト様


kan-kikuchi.hatenablog.com
tsubakit1.hateblo.jp
tsubakit1.hateblo.jp

【Unity】Webviewの実装(Android、iOS対応)

本記事で扱うUnityのバージョンは「Unity2018.3.7f1」です。


UnityでのWebviewのライブラリというと、
greeさんの「unity-webview」
github.com


もしくは、AssetStoreで配信されている「UniWebView」を使用されていると思います。
https://www.assetstore.unity3d.com/jp/?stay#!/content/92605



今回はgreeさんの「unity-webview」を使用して、
サンプルを作成したので使い方や解説を行っていきます。



実装例
github.com


基本はサンプル通りに処理を書いています。


ただ少し違うのが、WebView内で選択したリンクをそのままUnity側に送って、
Unity側で処理するようにしています。

Android

Android PluginのCWebViewPlugin.javaのshouldOverrideUrlLoading内の処理を一部修正しています。

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    canGoBack = webView.canGoBack();
    canGoForward = webView.canGoForward();
    // if (url.startsWith("http://") || url.startsWith("https://")
    //     || url.startsWith("file://") || url.startsWith("javascript:")) {
    //     // Let webview handle the URL
    //     return false;
    // } else if (url.startsWith("unity:")) {
    //     String message = url.substring(6);
    //     mWebViewPlugin.call("CallFromJS", message);
    //     return true;
    // }
    // すべてUnity側へ飛ばすように修正。
    if (url.startsWith("http://") || url.startsWith("https://")
    || url.startsWith("file://") || url.startsWith("javascript:") || url.startsWith("unity:")) {
        mWebViewPlugin.call("CallFromJS", url);
        return true;
    }
    
    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    PackageManager pm = a.getPackageManager();
    List<ResolveInfo> apps = pm.queryIntentActivities(intent, 0);
    if (apps.size() > 0) {
        view.getContext().startActivity(intent);
    }
    return true;
}





iOS

WebView.mmのdecidePolicyForNavigationActionの処理の一部を修正しています。

- (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    if (webView == nil) {
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    NSURL *nsurl = [navigationAction.request URL];
    NSString *url = [nsurl absoluteString];
    // URLの文字列が文字化けしてしまうためエンコード。
    NSString *sendUrl = [url stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]];
    if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) {
        [[UIApplication sharedApplication] openURL:nsurl];
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    } else if ([url hasPrefix:@"unity:"]) {
        // UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]);
        UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [sendUrl UTF8String]);
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    // } else if (![url hasPrefix:@"about:blank"]  // for loadHTML(), cf. #365
    //            && ![url hasPrefix:@"file:"]
    //            && ![url hasPrefix:@"http:"]
    //            && ![url hasPrefix:@"https:"]
    //            && ![url hasPrefix:@"mailto:"]
    //            && ![url hasPrefix:@"tel:"]
    //            && ![url hasPrefix:@"facetime:"]
    //            && ![url hasPrefix:@"sms:"]) {
    //     if([[UIApplication sharedApplication] canOpenURL:nsurl]) {
    //         [[UIApplication sharedApplication] openURL:nsurl];
    //     }
    //     decisionHandler(WKNavigationActionPolicyCancel);
    //     return;
    } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated
               && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) {
        // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c
        // [webView load:navigationAction.request];
        // 全てのメッセージを送ってあげるようにする。
        UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [sendUrl UTF8String]);
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    } else {
        if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) {
            // If the custom header is not attached, give it and make a request again.
            if (![self isSetupedCustomHeader:[navigationAction request]]) {
                NSLog(@"navi ... %@", navigationAction);
                [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]];
                decisionHandler(WKNavigationActionPolicyCancel);
                return;
            }
        }
    }
    UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]);
    decisionHandler(WKNavigationActionPolicyAllow);
}






他にもiOS側でもいくつか処理の追加を行っています。

bouncesの機能をOFFに変更

スクロールを最後まで引っ張ったときに、空白の部分まで見えてしまうやつ

画像をドラッグアンドドロップによる画像反転禁止
    // bouncesをOFFに変更。
    id subview = [[webView subviews] objectAtIndex:0];
    if([[subview class] isSubclassOfClass:[UIScrollView class]])
    {
        ((UIScrollView *)subview).bounces = NO;

        // iOS11からのドラッグアンドドロップによる画像反転禁止。
        if(@available(iOS 11.0, *))
        {
            ((UIScrollView *)subview).interactions = [[NSArray alloc] init];
        }
    }




3DTouchの機能を無効化
// iOS10以降で3DTouchを無効にする。
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo
{
    return false;
}
#endif





WebView内のテキスト選択を禁止

長押しによるメニューの表示禁止

// 読み込み完了時に呼び出される。
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    // webView内のテキスト選択禁止。
    [webView evaluateJavaScript:(@"document.documentElement.style.webkitUserSelect='none';") completionHandler:^(NSString *result, NSError *error) {}];
    // webView内の長押しによるメニュー表示禁止。
    [webView evaluateJavaScript:(@"document.documentElement.style.webkitTouchCallout='none';") completionHandler:^(NSString *result, NSError *error) {}];
}





ここらへんの処理も追加せずに、パラメータなどで切り替えられたらいいな
もういっそUnityでAPI化してほしい!

【Unity】Package Managerにモバイル通知の機能が追加されていた件(3)

本記事で扱うUnityのバージョンは「Unity2018.3.7f1」です。


前回
sunagimo-app.hatenablog.com




今回はサンプルを作成したので紹介します。
github.com




【機能一覧】
・通知の作成(AndroidiOS
・通知の削除(AndroidiOS
・通知の有無(AndroidiOS
・繰り返し通知の作成(AndroidiOS
・バックグラウンド通知(Android



Androidのバッググラウンド通知について


まず、モバイル通知ですが、
アプリがバックグラウンドにいる状態で届くのが基本です。

しかし今回使用した、モバイル通知API、および他のAssetにおいても、
iOSではバックグラウンド状態で通知が届くのですが、
Androidにおいてはフォアグラウンド状態でも通知が届いてしまいます。

今回のサンプルは、それを踏まえた上で
バッググラウンド状態のみ通知が届くように実装しております。


【バッググラウンド通知の実装の流れ】

1.作成した通知をリストに追加
2.Prefsにも保存する(電源を落としたあとにも保持するため)
3.アプリをバッググラウンド状態にする
4.リストにある通知を全て送信
5.アプリをフォアグラウンドにする
6.通知を全てキャンセルする



サンプルの紹介

f:id:sunagimo_app:20190305030218p:plain


左から、説明します。

・10秒後に通知(DateTime指定)
・10秒後に通知(TimeSpan指定)
・1分後に繰り返し通知
・スケジュールされている通知を全て削除

の4つのサンプル処理を用意しました。

NotificationController.SendNotification("通知テスト", "10秒後に通知(TimeSpan)", notificationTimeInterval, NotificationController.NotificationType.TYPE_1);
NotificationController.SendNotification("通知テスト", "10秒後に通知(DateTime)", notificationDateTime, NotificationController.NotificationType.TYPE_1);
NotificationController.SendNotification("通知テスト", "1分後に繰り返し通知", notificationDateTime, NotificationController.NotificationType.TYPE_1, true, repeatInterval);
NotificationController.AllCancelNotification();


NotificationControllerクラスから呼べるようにしてあります。
その中でAndroidiOSの処理を分けて呼んでいます。


また、パラメーターに通知タイプ、通知IDを指定するようにしています。
予め識別子を設けておくことで、管理が可能になります。



通知されるとこんな感じで表示されます。
なお、Androidの場合はOSのバージョンによって見た目が異なります。

f:id:sunagimo_app:20190305032944j:plain:w300:h533

f:id:sunagimo_app:20190305032948j:plain:w300:h533




通知アイコンの設定


Android)

f:id:sunagimo_app:20190305031509p:plain

Package ManagerからMobile Notificationsをインストールすると、
Editor/com.unity.mobile.notifications/NotificationSettingsという設定ファイルが作られます。

ここでAndroidの通知アイコンの設定ができます。
smallIconについては白色の透過アイコンでないと、
四角い真っ白になってしまうので注意が必要です。

Previewで正常に表示されていれば問題ないと思います。



ここらへんを参考に。
qiita.com



iOS

Player SettingsのNotification Iconsで設定します。

f:id:sunagimo_app:20190305032239p:plain



まとめ


まず、今まで外部ライブラリなどを用いて使ってきた機能が、
Unity内に含まれたことはとても素晴らしいことでした。


その上で今まで使ってきた、モバイル通知のライブラリと比較しても
代用できる十分な機能を備えていると思います。


ただ一つだけ、
この記事の最初でも書いていますが、
Androidのバックグラウンド通知をデフォルトとして設定できるようにしてほしいです。


いつか、Preview表示が消えることを祈っています。
そしてUnityさんに感謝しています。