20200124のUnityに関する記事は6件です。

Oculus Quest ハンドトラッキングの実装

◆ はじめに

今回はOculus Questのハンドトラッキング機能の実装の説明です。
Unityプロジェクトの作成、ビルド方法などは
【UnityでOculus Quest向けのアプリを作る】APPをビルドして、動作確認する
【UnityでOculus Quest向けのアプリを作る】物を掴む為には
参照してください。

◆ 開発環境

macOS Mojave バージョン 10.14.6
Unity 2018.4.12f1
Android SDK

ハンドトラッキング機能を使用する為、
Oculusのバージョンは(Ver12)にアップデートする必要があります。

◆ 手順

  • 準備すべき物
  • ハンドトラッキング作成
  • 手を表示するためには
  • 補充説明
  • 動作確認

準備すべき物

まず、メニューバーの「Window」→「Asset Store」
Oculus Integrationを検索し、(最新バージョンを)Importします。
Download が終わるとImportボタン出るので、Importボタンをチェックします。01のコピー.png

ハンドトラッキング作成

ここでは、前回作成した「前回のリンク」SCENEを使います。

前回「Hierarchy」に残した「LocalAvatarWithGrab」を削除し、
先程、Importした中にある「OVRCameraRig」を「Hierarchy」に入れます。
03のコピー.png
追加した「OVRCameraRig」を選択し、「Inspector」にある、
「Hand Tracking Support」を「Hands Only」に変更すれば。
準備完了です。
04.png

手を表示するためには

先程追加した「OVRCameraRig」>「TrackingSpace」下階層の
「LeftHandAnchor」と「RightHandAnchor」中に「OVRHandPrefab」を追加する必要があります。

「OVRHandPrefab」は「Oculus Integration」の中にあります。
05.png
ドラッグして追加する。
06.png
「RightHandAnchor」の中にある「OVRHandPrefab」を選択し、
「OVR Skeleton」→「Skeleton Type」を「Hand Right」設定します。

「OVR Mesh」→「Mesh Type」も同じく「Hand Right」に設定します。
(初期値は「Hand Left」なので、左手は設定する必要がないです。)
スクリーンショット 2020-01-24 20.26.35.png

補充説明

手のスケルトンを表示したい場合
「OVR Skeleton Renderer」にチェックを入れればOKです。
(今回は分かりやすいように、右手にチェックを入れます。Materialsの削除も忘れに)スクリーンショット 2020-01-24 20.28.14.png
手カスタマイズしたい場合は「Skinned Mesh Renderer」で調整可能です。
左手を調整しました。
スクリーンショット 2020-01-24 20.29.46.png
最後に
「OVR Skeleton」中の「Enable Physics Capsules」にチェックを入れると、手に当たり判定が追加出来ます。
今回は右手に追加します。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3534313338302f32313935643366642d626630342d353366622d643835382d3839386331383565653839362e706e67.png

動作確認

最後はビルドして、動作確認します。
com.oculus.vrshell-20200124-204654_1.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでマルチスレッドなHttpサーバー

Unity と Webアプリ間で データ連携したかったので、Unity で Httpサーバーを作ってみました。後々、Unityで簡易な WebAPI を作りたいので、再利用しやすい方法をまとめています。

やりたいこと

・Unityで Httpサーバーを作成
・利用するライブラリは System.Net.HttpListener
・マルチスレッド処理で描画負荷の影響を抑える
・ Get / Post リクエストを処理
・再利用したいので、サーバー処理とリクエスト処理のコンポーネントを分離
・UnityEvent を使って、インスペクタでイベントを管理
・通信テストは、Postman で行う

System.Net.HttpListenerとは

HTTP 要求に応答する単純な HTTP プロトコルリスナーを作成できます。
.NET 標準クラスなので、Unityでも標準で使うことができます。
MS-DOC HttpListener クラス概要

マルチスレッド処理の準備

Unity は描画負荷が高いので、安定させるために System.Net.HttpListener を別スレッドで実行します。

ただし、リクエスト内容によってはメインスレッドで描画を行う必要があります。Unity ではスレッドをまたいだ関数の実行はできないので、UnityMainThreadDispatcher を使って、メインスレッドのアクションを呼び出せるようにします。

下記からダウンロードし、Assetsに配置します。
UnityMainThreadDispatcher - GitHub

HTTPサーバーのコンポーネントを作る

以下のようなスクリプトを書きます。

HTTPサーバーのコード

UnityHttpListener.cs
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;

public class UnityHttpListener : MonoBehaviour
{
    private HttpListener listener;
    private Thread listenerThread;

    public string domain = "localhost";
    public int port = 8080;

    [System.Serializable]
    public class OnGetRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnGetRequestEvent OnGetRequest;

    [System.Serializable]
    public class OnPostGetRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnPostGetRequestEvent OnPostRequest;

    void Start()
    {
        listener = new HttpListener();
        listener.Prefixes.Add("http://" + domain + ":" + port + "/");
        listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
        listener.Start();

        listenerThread = new Thread(startListener);
        listenerThread.Start();
        Debug.Log("Server Started");
    }

    private void OnDestroy()
    {
        listener.Stop();
        listenerThread.Join();
    }

    private void startListener()
    {
        while (listener.IsListening)
        {
            var result = listener.BeginGetContext(ListenerCallback, listener);
            result.AsyncWaitHandle.WaitOne();
        }
    }

    private void ListenerCallback(IAsyncResult result)
    {
        if (!listener.IsListening) return;
        HttpListenerContext context = listener.EndGetContext(result);
        Debug.Log("Method: " + context.Request.HttpMethod);
        Debug.Log("LocalUrl: " + context.Request.Url.LocalPath);

        try
        {
            if (ProcessGetRequest(context)) return;
            if (ProcessPostRequest(context)) return;
        }
        catch (Exception e)
        {
            ReturnInternalError(context.Response, e);
        }
    }

    private bool CanAccept(HttpMethod expected, string requested)
    {
        return string.Equals(expected.Method, requested, StringComparison.CurrentCultureIgnoreCase);
    }

    private bool ProcessGetRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Get, context.Request.HttpMethod) || context.Request.IsWebSocketRequest)
            return false;
        //メインスレッドでGetリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnGetRequest.Invoke(context));
        return true;
    }

    private bool ProcessPostRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Post, context.Request.HttpMethod))
            return false;
        //メインスレッドでPostリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnPostRequest.Invoke(context));
        return true;
    }

    private void ReturnInternalError(HttpListenerResponse response, Exception cause)
    {
        Console.Error.WriteLine(cause);
        response.StatusCode = (int) HttpStatusCode.InternalServerError;
        response.ContentType = "text/plain";
        try
        {
            using(var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
            writer.Write(cause.ToString());
            response.Close();
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e);
            response.Abort();
        }
    }
}

Get / Post のリクエストがあれば、UnityMainThreadDispatcher を使って、メインスレッドでそれぞれのUnityEventを呼び出しています。

リクエスト処理のコード

RequestHandler.cs
using System;
using System.Net;
using UnityEngine;

public class RequestHandler : MonoBehaviour
{
    private MyData data;

    private void Start()
    {
        data = new MyData();
    }

    public void OnGetRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int) HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key) [0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }

    public void OnPostRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int) HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key) [0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }
}

[System.Serializable]
class MyData
{
    public bool success = false;
}

ここでは、UnityEvent を受けて、リクエストに対応したレスポンスを返しています。

  • リクエストキー が GetData の場合: Unity 側のデータを返信
  • リクエストキー が SetData の場合: Unity 側のデータをリクエスト値に変更 -> 変更後のデータを返信

レスポンスの ContentType はひとまず Json にしました。
データを保持する MyData クラスを用意して、JsonUtilityでJsonに変換しています。
通信テストができればいいので、 Get / Post どちらも同じ内容です。

コンポーネントをアタッチする

空の GameObject を作り、準備していた UnityMainThreadDispatcher と 先ほど作った UnityHttpListener、RequestHandlerをアタッチします。
スクリーンショット 2020-01-24 17.33.23.png

次にイベントをアタッチしていきます。
インスペクタの OnGetRequestOnPostRequest 下部の + ボタンからイベントを追加し、それぞれにRequest Handler (Script)コンポーネントを貼り付けます。
スクリーンショット 2020-01-24 17.57.26.png

プルダウンメニューから OnGetRequestOnPostRequest それぞれの呼び出す関数を設定します。
スクリーンショット 2020-01-24 17.57.59.png

こんな感じになれば、完成
スクリーンショット 2020-01-24 18.03.43.png

通信テストのために、Unity を再生しておきます。

通信テストしてみる

Postman を使って、通信テストをします。
Postman Download

インストールの手順や使い方はこちら
Postmanを使ったAPIテストのやり方 - IT業務で使えるプログラミングテクニック

Postman で Get テスト

メソッドをGetにして、http://localhost:8080/?GetData=を送ってみます。
スクリーンショット 2020-01-24 18.47.20.png

Unity で作った MyData が Json で返ってきてました。
スクリーンショット 2020-01-24 18.45.14.png

次は、http://localhost:8080/?SetData=trueを送ってみます。
スクリーンショット 2020-01-24 18.47.41.png

ちゃんと successtrue に変わりました。

メソッドを Post にすれば、Postのテストが出来ます。

まとめ

Unityだけで Post/Get リクエストを処理出来るのは、とても楽ですね。プロトタイプでは充分使えます。

今回はローカルでテストしましたが、ngroklocaltunnel を使って外部公開すれば、Unity でも 簡易な WebAPI が作れそうです。

参考にしたサイト

UnityでHTTPリクエストを処理してみる -Qiita
C#でHTTPSサーバ(Ver. HttpListener) -Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでマルチスレッドなHTTPサーバー

Unity と Webアプリ間で データ連携したかったので、Unity で HTTPサーバーを作ってみました。後々、Unityで簡易な WebAPI を作りたいので、再利用しやすい方法をまとめています。

やりたいこと

・Unityで HTTPサーバーを作成
・利用するライブラリは System.Net.HttpListener
・マルチスレッド処理で描画負荷の影響を抑える
・ Get / Post リクエストを処理
・再利用したいので、サーバー処理とリクエスト処理のコンポーネントを分離
・UnityEvent を使って、インスペクタでイベントを管理
・通信テストは、Postman で行う

System.Net.HttpListenerとは

HTTP 要求に応答する単純な HTTP プロトコルリスナーを作成できます。
.NET 標準クラスなので、Unityでも標準で使うことができます。
MS-DOC HttpListener クラス概要

マルチスレッド処理の準備

Unity は描画負荷が高いので、安定させるために System.Net.HttpListener を別スレッドで実行します。

ただし、リクエスト内容によってはメインスレッドで描画を行う必要があります。Unity ではスレッドをまたいだ関数の実行はできないので、UnityMainThreadDispatcher を使って、メインスレッドのアクションを呼び出せるようにします。

下記からダウンロードし、Assetsに配置します。
UnityMainThreadDispatcher - GitHub

HTTPサーバーのコンポーネントを作る

以下のようなスクリプトを書きます。

HTTPサーバーのコード

UnityHttpListener.cs
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;

public class UnityHttpListener : MonoBehaviour
{
    private HttpListener listener;
    private Thread listenerThread;

    public string domain = "localhost";
    public int port = 8080;

    [System.Serializable]
    public class OnGetRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnGetRequestEvent OnGetRequest;

    [System.Serializable]
    public class OnPostGetRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnPostGetRequestEvent OnPostRequest;

    void Start()
    {
        listener = new HttpListener();
        listener.Prefixes.Add("http://" + domain + ":" + port + "/");
        listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
        listener.Start();

        listenerThread = new Thread(startListener);
        listenerThread.Start();
        Debug.Log("Server Started");
    }

    private void OnDestroy()
    {
        listener.Stop();
        listenerThread.Join();
    }

    private void startListener()
    {
        while (listener.IsListening)
        {
            var result = listener.BeginGetContext(ListenerCallback, listener);
            result.AsyncWaitHandle.WaitOne();
        }
    }

    private void ListenerCallback(IAsyncResult result)
    {
        if (!listener.IsListening) return;
        HttpListenerContext context = listener.EndGetContext(result);
        Debug.Log("Method: " + context.Request.HttpMethod);
        Debug.Log("LocalUrl: " + context.Request.Url.LocalPath);

        try
        {
            if (ProcessGetRequest(context)) return;
            if (ProcessPostRequest(context)) return;
        }
        catch (Exception e)
        {
            ReturnInternalError(context.Response, e);
        }
    }

    private bool CanAccept(HttpMethod expected, string requested)
    {
        return string.Equals(expected.Method, requested, StringComparison.CurrentCultureIgnoreCase);
    }

    private bool ProcessGetRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Get, context.Request.HttpMethod) || context.Request.IsWebSocketRequest)
            return false;
        //メインスレッドでGetリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnGetRequest.Invoke(context));
        return true;
    }

    private bool ProcessPostRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Post, context.Request.HttpMethod))
            return false;
        //メインスレッドでPostリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnPostRequest.Invoke(context));
        return true;
    }

    private void ReturnInternalError(HttpListenerResponse response, Exception cause)
    {
        Console.Error.WriteLine(cause);
        response.StatusCode = (int) HttpStatusCode.InternalServerError;
        response.ContentType = "text/plain";
        try
        {
            using(var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
            writer.Write(cause.ToString());
            response.Close();
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e);
            response.Abort();
        }
    }
}

Get / Post のリクエストがあれば、UnityMainThreadDispatcher を使って、メインスレッドでそれぞれのUnityEventを呼び出しています。

リクエスト処理のコード

RequestHandler.cs
using System;
using System.Net;
using UnityEngine;

public class RequestHandler : MonoBehaviour
{
    private MyData data;

    private void Start()
    {
        data = new MyData();
    }

    public void OnGetRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int) HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key) [0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }

    public void OnPostRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int) HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key) [0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }
}

[System.Serializable]
class MyData
{
    public bool success = false;
}

ここでは、UnityEvent を受けて、リクエストに対応したレスポンスを返しています。

  • リクエストキー が GetData の場合: Unity 側のデータを返信
  • リクエストキー が SetData の場合: Unity 側のデータをリクエスト値に変更 -> 変更後のデータを返信

レスポンスの ContentType はひとまず Json にしました。
データを保持する MyData クラスを用意して、JsonUtilityでJsonに変換しています。
通信テストができればいいので、 Get / Post どちらも同じ内容です。

コンポーネントをアタッチする

空の GameObject を作り、準備していた UnityMainThreadDispatcher と 先ほど作った UnityHttpListener、RequestHandlerをアタッチします。
スクリーンショット 2020-01-24 17.33.23.png

次にイベントをアタッチしていきます。
インスペクタの OnGetRequestOnPostRequest 下部の + ボタンからイベントを追加し、それぞれにRequest Handler (Script)コンポーネントを貼り付けます。
スクリーンショット 2020-01-24 17.57.26.png

プルダウンメニューから OnGetRequestOnPostRequest それぞれの呼び出す関数を設定します。
スクリーンショット 2020-01-24 17.57.59.png

こんな感じになれば、完成
スクリーンショット 2020-01-24 18.03.43.png

通信テストのために、Unity を再生しておきます。

通信テストしてみる

Postman を使って、通信テストをします。
Postman Download

インストールの手順や使い方はこちら
Postmanを使ったAPIテストのやり方 - IT業務で使えるプログラミングテクニック

Postman で Get テスト

メソッドをGetにして、http://localhost:8080/?GetData=を送ってみます。
スクリーンショット 2020-01-24 18.47.20.png

Unity で作った MyData が Json で返ってきてました。
スクリーンショット 2020-01-24 18.45.14.png

次は、http://localhost:8080/?SetData=trueを送ってみます。
スクリーンショット 2020-01-24 18.47.41.png

ちゃんと successtrue に変わりました。

メソッドを Post にすれば、Postのテストが出来ます。

まとめ

Unityだけで Post/Get リクエストを処理出来るのは、とても楽ですね。プロトタイプでは充分使えます。

今回はローカルでテストしましたが、ngroklocaltunnel を使って外部公開すれば、Unity でも 簡易な WebAPI が作れそうです。

参考にしたサイト

UnityでHTTPリクエストを処理してみる -Qiita
C#でHTTPSサーバ(Ver. HttpListener) -Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MotionLibraryで購入したアニメーションが動かない件の解決策の一例

はじめに

Unity内のモデルを動かすためのモーションを専用に扱ったモーションサイト、MotionLibraryというものがあります。多くは1つのモーションにつき3ドル~6ドルで、量は少ないですが無料のモーションもあります。
個人的感覚ですが、基本的にモーションの購入はAssetStoreで買うよりもこっちで買う方が安く済みますし効率が良いです。

問題

MotionLibraryでモーションを購入すると、FBXファイルがインポートされます。
そのFBXファイルの中のアニメーションをanimatorにアタッチしてPlayしても、モデルがいい感じに動いてくれない時があります。

僕の場合、本来なら購入したモーションの動き通りにモデルが動くはずですが、モデルがずっと座ったような状態が続きました。
image.png
画像は、想定通りに動かなかった時のスクショです。

解決策

私の場合、以下のようにして解決できました。

問題のFBXファイルを選択

インポートしたFBXはMotionLibraryディレクトリの中にあるはずです。その中で、想定通りに動かないFBXを選択してください。
image.png

Inspectorの確認

選択したFBXのInspectorの中のRigを見てください。
Animation TypeがHumanoid以外になってませんか。
僕の場合、このAnimetionTypeがHumanoid以外だと上手く動きませんでした。
image.png

Animation Typeの編集

先ほどのAnimationTypeを、Humanoidに変更して、右下のApplyを押してください。これでいい感じに動くようになったはずです。
image.png

さいごに

別のインターネット上の記事では他の解決方法が紹介されていたりします。
もしもこの記事の事をしても上手くいかない場合、他の記事も参考にしてください。以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UniRxでダブルクリック判定を3種類の方法で書いてみる

UnityのUniRxは便利ですし、ネットの記事を読むことで基本的な事は理解できるでしょう
しかし、いざ応用するとなるとなかなか感覚を掴めません

そこで、ダブルクリック判定は、UniRxの扱いを入門するのにちょうどいいものだと思い、
UniRxによるシングル、ダブルクリック判定法を3つ書いてみました

今回実装するダブルクリック判定の仕様は次のようです
・シングルクリックとダブルクリックをどちらも判定できる
・ダブルクリック時に、シングルクリックは判定しない
・連打時の事も考える

準備

まずはクリック判定だけ取っておきましょう

var click = this.UpdateAsObservable()
    .Where(_ => Input.GetMouseButtonDown(0));

先に言っておきます。シングルとダブルの判定は同じObservable内でbool型で判定します
シングルクリックならfalseを流し
ダブルクリックならtrueを流すことにします

1つ目

まずは、ネットでUniRx ダブルクリックなど検索するといくらか目にする例です

var dClick1 = click
    .TimeInterval() // 前のイベントとの時間差
    .Select(v => v.Interval <= TimeSpan.FromSeconds(0.2f))
        // ↑ シングルならfalse、ダブルならtrue
    .Buffer(2, 1)
    .Where(b => !(b[0] && b[1]));
        // ↑ 連打時の判定 (3連打以上は無視)

// 出力
dClick1.Subscribe(b => Debug.Log(b ? "シングルクリック" : "ダブルクリック");
    // ↑ 3項演算子によるシングル、ダブルクリック出力

クリックをした時間差で判定する方法ですね
余計に前の状態をBufferで取ってこなくてはいけませんが、UniRx初心者が理解しやすい内容と言えます。
連打時の挙動は、
最初にシングルクリック、次にダブルクリックが流れ、
後はまったく流れないです

2つ目

今度はストリームの合成の概念を使います

var dClick2 = click
    .ThrottleFirst(TimeSpan.FromSeconds(0.2f))
        // シングルクリック判定の間隔。0.2秒以内に2つ以上流さない
    .SelectMany(Observable.Return(false) // シングルクリックはfalseを流す
        .Merge(click // ダブルクリック判定につなげる
        .Select(_ => true) // ダブルクリックはtrueを流す
        .Take(TimeSpan.FromSeconds(0.2f)).Take(1)));
            // ダブルクリック判定。trueを流す

今度は、シングルクリック判定をしたら、シングルクリック判定をやめて
ダブルクリック判定を一定時間するという方法です
Returnにより、一旦シングルクリック判定を流し、
Mergeにより、ダブルクリック判定につなげます
SelectMany
 元々のObservbleにイベントが流れた時に、
 引数に渡したObservableからイベントを取り始めます

ThrottleFirstによって、シングルクリック判定を一定時間やめておき、
そのうちに、ダブルクリック判定をしています
判定時間を2回指定しなければいけないので少々気持ち悪いです

連打時の挙動は
シングルクリックとダブルクリックの判定が交互に流れます
素直な挙動です

3つ目

var dClick3 = click
    .Select(_ => false).Take(1) // シングルならfalse。一つだけ通す
    .Concat(click.Select(_ => true) // ダブルクリックならtrue
        .Take(TimeSpan.FromSeconds(0.2f)).Take(1)) // ダブルクリック判定
    .RepeatSafe(); // 判定が終わったら繰り返し

理解の難易度が高いオペレータを使いますが、一番シンプルな実装方法です
僕はこの方法が一番好きです

Concatを使っていますがこれは
 直前のObservableがOnCompletedしたら、引数に渡したObservableに切り替える
というものです
また、引数を渡さないこともでき、その場合は
 直前にSelectで指定したObservableを使う
  (この場合は一時的にIObservable<IObservable<T>>という型になる)
というようになります

シングルクリック判定が起こってから、
ダブルクリック判定をはじめて、
すべて終了すれば RepeatSafe()により最初から判定を行う
という感じです
RepeatSafe()はOnCompletedが連続で流れれば繰り返しを終了します
Repeat()なんて使うな!無限ループが怖いぞ!

連打時には
シングルクリックとダブルクリックの判定が交互に行われるので
シングルクリックとダブルクリックが交互に出力されます

出力

最後にDebug.Logの出力だけ書きます(1つ目のところでもちらっと書きました)

var doubleClick = dClick3;
doubleClick.Subscribe(b =>
    Debug.Log(b ? "ダブルクリック" : "シングルクリック");

doubleClickにdClick1や2をいれれば、そちらの方の実装を試せます

最後

ダブルクリックというのは単純なようですが、
実際に判定を取る事を考えると奥が深い処理です

この記事を書くにあたって、Concatを覚えられて良かったです

UniRxはまだまだ学習コストが高いですね

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】SoundManagerクラスを作ってみた。

初めに

このクラスは自分なりに欲しい機能を追加しただけなので足りないと感じた方はここからさらに拡張してみてください。

使用されているメソッド拡張&Managerクラス

  • SKTool
  • SaveLoadManager
    これらは私が過去投稿したものを使用しております。これらのクラスを導入しないと動かない場合があります。

ダウンロード先

コード

using System.Collections.Generic;
using UnityEngine;
using SKTool.Unity;
using System;
using System.Collections;

public class SoundManager : MonoBehaviour
{
    const string SAVE_FILE_NAME = "SoundInformation";       //セーブデータのファイル名
    const float BGM_FADE_TIME = 1f;             //フェードにかかる秒数

    [Serializable]
    public class SoundInfo
    {
        public float MainVolume;
        public float BackMusicVolume;
        public float SfxVolume;
    }
    SoundInfo soundInfo = new SoundInfo();

    [SerializeField, Header("BGM")]
    AudioClip backMusic;

    public float MainVolume { set { SoundVolumeApplication(); MainVolume = value; } get { return MainVolume; } }

    public float BackMusicVolume { set { SoundVolumeApplication(); BackMusicVolume = value; } get { return BackMusicVolume; } }

    public float SfxVolume { set { SoundVolumeApplication(); SfxVolume = value; } get { return SfxVolume; } }

    GameObject backMusicObject;

    public List<AudioSource> sfxList = new List<AudioSource>();

    public AudioClip clip;


    private void Start()
    {
        Initialize();

    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GetComponent<AudioSource>().Play();
            Debug.Log(Mathf.Clamp(5, 0, 2));
        }
    }

    /// <summary>
    /// 初期化
    /// </summary>
    private void Initialize()
    {
        //サウンドの設定データがなければ作成
        if (SaveLoadManager.Load(SAVE_FILE_NAME) == null)
        {
            Debug.Log("セーブデータを作成");
            soundInfo.BackMusicVolume = 1;
            soundInfo.MainVolume = 1;
            soundInfo.SfxVolume = 1;
            VolumeAttach();
        }
        //サウンドの設定データを呼び出す
        else
        {
            soundInfo = (SoundInfo)SaveLoadManager.Load(SAVE_FILE_NAME);
            VolumeAttach();
        }

        //BGMが最初からアタッチしてあればそちらを流す
        if (backMusic != null)
        {
            RequestBackMusic(backMusic);
        }
    }


    /// <summary>
    /// バックミュージックをリクエストする
    /// </summary>
    /// <param name="_backMusic">流したいBGM</param>
    /// <param name="_overwrite">今流れているBGMを上書きするかどうか</param>
    /// <returns>バックミュージックを流しているAudioSource</returns>
    public AudioSource RequestBackMusic(AudioClip _backMusic, bool _overwrite = false, float _pitch = 1f)
    {
        AudioSource audioSource = null;
        //何も流れていなければ再生
        if (backMusicObject == null)
        {
            backMusicObject = new GameObject("BackMusicObject");
            audioSource = backMusicObject.AddComponent<AudioSource>();
            StartCoroutine(BackGroundMusicFade(_backMusic,audioSource,FadeMode.FadeIn));
            audioSource.loop = true;
            audioSource.pitch = Mathf.Clamp(_pitch, -3, 3);
        }
        //バックミュージックを上書き
        else if (backMusicObject != null && _overwrite == true)
        {
            audioSource.Stop();
            audioSource.loop = true;
            audioSource.time = 0;
            audioSource.pitch = Mathf.Clamp(_pitch, -3, 3);
            StartCoroutine(BackGroundMusicFade(_backMusic, audioSource, FadeMode.FadeOut));
        }

        return audioSource;
    }

    /// <summary>
    /// バックミュージックを止める
    /// </summary>
    /// <returns>バックミュージックを止められたかの結果 true or false</returns>
    public bool StopBackMusic()
    {
        bool result = false;
        if (backMusicObject != null)
        {
            backMusicObject.GetComponent<AudioSource>().Stop();
            result = true;
        }
        return result;
    }

    /// <summary>
    /// サウンドをリクエスとする
    /// </summary>
    /// <param name="_sound">流したいサウンド</param>
    /// <param name="_localPos">再生させたい位置</param>
    /// <param name="_loop">ループ</param>
    /// <param name="_pitch">ピッチ(-3~3)</param>
    /// <param name="_spatial">立体音響(0~1)</param>
    /// <returns>再生させているサウンドのAudioSource</returns>
    public AudioSource RequestSound(AudioClip _sound, Vector3 _localPos = default, bool _loop = false, float _pitch = 1f, float _spatial = 0f)
    {

        //Soundを鳴らす用のオブジェクトを生成
        GameObject tempAudio = new GameObject("TempAudio");
        //鳴らす位置を決める
        tempAudio.transform.position = _localPos;
        //AudioSourceをAddする
        AudioSource audioSource = tempAudio.AddComponent<AudioSource>() as AudioSource;
        //リストに格納
        sfxList.Allocation(audioSource, true);

        //ループさせるかどうか
        audioSource.loop = _loop;
        //音声を追加
        audioSource.clip = _sound;
        //音声を再生
        audioSource.Play();
        //ピッチ調整
        audioSource.pitch = Mathf.Clamp(_pitch, -3, 3);
        //立体音響
        audioSource.spatialBlend = Mathf.Clamp(_spatial, 0, 1);

        //ループでなければ
        if (_loop == false)
        {
            //音声が再生され終えたら削除
            Destroy(tempAudio, _sound.length);
        }

        //AudioSourceを返す
        return audioSource;
    }

    /// <summary>
    /// 現在なっている音に変更後の音量を適用させる
    /// </summary>
    public void SoundVolumeApplication()
    {

        soundInfo.MainVolume = MainVolume;
        soundInfo.BackMusicVolume = BackMusicVolume;
        soundInfo.SfxVolume = SfxVolume;

        SaveLoadManager.Save(soundInfo, SAVE_FILE_NAME);

        //バックミュージックがなければ処理しない
        if (backMusicObject != null)
        {
            //音量再適用
            backMusicObject.GetComponent<AudioSource>().volume = MainVolume * BackMusicVolume;
        }

        //音量再適用
        for (int i = 0; i < sfxList.Count; i++)
        {
            if (sfxList[i] == null)
            {
                continue;
            }
            else
            {
                sfxList[i].volume = MainVolume * BackMusicVolume;
            }
        }
    }

    /// <summary>
    /// セーブしてあった値を読み込む
    /// </summary>
    public void VolumeAttach()
    {
        MainVolume = soundInfo.MainVolume;
        BackMusicVolume = soundInfo.BackMusicVolume;
        SfxVolume = soundInfo.SfxVolume;
    }

    public enum FadeMode
    {
        FadeIn,
        FadeOut
    }

    IEnumerator BackGroundMusicFade(AudioClip _clip,AudioSource _audioSource,FadeMode _fadeMode)
    {
        float volume;
        switch (_fadeMode)
        {
            case FadeMode.FadeIn:
                volume = MainVolume*BackMusicVolume;
                _audioSource.volume = 0;
                _audioSource.clip = _clip;
                _audioSource.Play();
                while (_audioSource.volume >= volume)
                {
                    _audioSource.volume += volume / Time.deltaTime / BGM_FADE_TIME;
                    yield return null;
                }
                _audioSource.volume = volume;
                break;
            case FadeMode.FadeOut:
                volume = _audioSource.volume;
                while (_audioSource.volume <= 0)
                {
                    volume -= Time.deltaTime / BGM_FADE_TIME;
                    _audioSource.volume = volume;
                    yield return null;
                }
                if (_clip != null)
                {
                    StartCoroutine(BackGroundMusicFade(_clip, _audioSource, FadeMode.FadeIn));
                }
                break;
            default:
                break;
        }
        yield break;
    }
}

まとめ

まだデバックが完全にすんではいませんし、機能の追加もまだしたいですがとりあえずできているとこまで載せます。
不具合などありましたらご連絡ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む