HoloLens (第 1 世代) と Azure 309: アプリケーションの分析情報
Note
Mixed Reality Academy のチュートリアルは、HoloLens (第 1 世代) と Mixed Reality イマーシブ ヘッドセットを念頭に置いて編成されています。 そのため、それらのデバイスの開発に関するガイダンスを引き続き探している開発者のために、これらのチュートリアルをそのまま残しておくことが重要だと考えています。 これらのチュートリアルが、HoloLens 2 に使用されている最新のツールセットや操作に更新されることは "ありません"。 これらは、サポートされているデバイス上で継続して動作するように、保守されます。 今後、HoloLens 2 の開発方法を説明する新しいチュートリアルが公開される予定です。 この通知は、それらのチュートリアルが投稿されたときにリンクと共に更新されます。
このコースでは、Azure アプリlication Insights API を使用してユーザーの動作に関する分析を収集して、Application Insights 機能を Mixed Reality アプリケーションに追加する方法について説明します。
Application Insights は、開発者がアプリケーションから分析情報を収集し、使いやすいポータルで管理できるようにする Microsoft サービスです。 分析情報は、パフォーマンスからカスタムの情報まで、収集を行うあらゆる情報が対象となります。 詳細については、Application Insights に関するページを参照してください。
このコースを完了すると、Mixed Reality イマーシブ ヘッドセット アプリケーションが用意され、次のことが可能になります。
- ユーザーが視線入力し、シーン内を移動できるようにします。
- 視線入力とシーン内オブジェクトへの近接性を使用して、 Application Insights Service への分析の送信をトリガーします。
- また、アプリはこのサービスを呼び出して、過去 24 時間以内にユーザーが最も接近したオブジェクトに関する情報を取得します。 そのオブジェクトの色は緑に変わります。
このコースでは、Application Insights サービスから結果を取得して、Unity ベースのサンプル アプリケーションに取り込む方法について説明します。 作成中のカスタム アプリケーションがある場合に、これらの概念をそのアプリケーションで採用するかどうかはご自身でご判断ください。
デバイス サポート
コース | HoloLens | イマーシブ ヘッドセット |
---|---|---|
MR と Azure 309: Application Insights | ✔️ | ✔️ |
Note
このコースは主に Windows Mixed Reality イマーシブ(VR)ヘッドセットに焦点を当てていますが、このコースで学んだことを Microsoft HoloLens にも応用できます。 このコースに取り組む過程で、HoloLens をサポートするために採用する必要のある変更に関するノートが表示されます。 HoloLens を使用すると、音声キャプチャ中に反響音が生じることがあります。
前提条件
Note
このチュートリアルは、Unity と C# の基本的な使用経験がある開発者を対象としています。 また、このドキュメント内の前提条件や文章による説明は、執筆時 (2018 年 7 月) にテストおよび検証された内容であることをご了承ください。 ツールのインストールの記事に記載されているように、お客様は最新のソフトウェアを自由に使用できます。ただし、このコースの情報は、以下に記載されているものよりも新しいソフトウェアでの設定や結果と完全に一致するとは限りません。
このコースでは、次のハードウェアとソフトウェアをお勧めします。
- イマーシブ (VR) ヘッドセットの開発に必要な Windows Mixed Reality と互換性のある開発用 PC
- 開発者モードが有効になっている Windows 10 Fall Creators Update (またはそれ以降)
- 最新の Windows 10 SDK
- Unity 2017.4
- Visual Studio 2017
- 開発者モードが有効になっている Windows Mixed Reality イマーシブ (VR) ヘッドセットまたは Microsoft HoloLens
- 内蔵マイク付きヘッドホンのセット (ヘッドセットにマイクとスピーカーが組み込まれていない場合)
- Azure のセットアップと Application Insights データの取得のためのインターネット アクセス
開始する前に
このプロジェクトをビルドするときの問題を回避するには、このチュートリアルでプロジェクトをルート フォルダーまたはほぼルート フォルダーに作成することを強くお勧めします (長いフォルダー パスがビルド時に問題を引き起こす可能性があります)。
警告
Application Insights へのデータの送信には時間がかかるのでご注意ください。 サービスがデータを受信したかどうか確認する場合は、第 14 章を参照してください。この章では、ポータルをナビゲートする方法を説明しています。
第 1 章 - Azure Portal
Application Insights を使用するには、Azure portal で Application Insights Service を作成して構成する必要があります。
Azure portal にサインインします。
Note
まだ Azure アカウントをお持ちでない方は、作成する必要があります。 このチュートリアルを教室やラボで受講している場合は、インストラクターや監督者に新しいアカウントの設定方法を質問してください。
ログインしたら、左上隅の New をクリックし、 Application Insights を検索して、 Enter をクリックします。
Note
新しいポータルでは、[新規作成] という文字列が [リソースの作成] に置き換えられることがあります。
右側の新しいページに、[Azure Application Insights] サービスの説明が表示されます。 このページの左下にある [作成] ボタンを選択すると、このサービスとの関連付けが作成されます。
Create をクリックすると:
このサービス インスタンスに任意の名前を入力します。
[アプリケーションの種類] は [全般] を選択します。
適切な [サブスクリプション] を選択します。
[リソース グループ] を選択するか、新規に作成します。 リソース グループは、Azure アセットのコレクションの監視、アクセス制御、プロビジョニング、課金管理を行う方法を提供します。 すべての Azure サービスを 1 つのプロジェクト (たとえば、これらのコースなど) に関連付け、共通のリソース グループの下に保持することをお勧めします。
Azure リソース グループの詳細については、リソース グループに関する記事を参照してください。
[場所] を選択します。
また、このサービスに適用される使用条件を理解していることも確認する必要があります。
[作成] を選択します
Createをクリックすると、サービスが作成されるまで待機する必要があります。これには時間がかかる場合があります。
サービス インスタンスが作成されると、ポータルに通知が表示されます。
通知を選択して、新しいサービス インスタンスを探索します。
通知の[リソースに移動]ボタンをクリックして、新しいサービスインスタンスを探します。 新しい Application Insights Service インスタンスが表示されます。
Note
この Web ページを開いたままにして、簡単にアクセスできるようにしておきます。収集されたデータを確認するために頻繁にこのページに戻るためです。
重要
Application Insights を実装するには、インストルメンテーション キー、アプリケーション ID、および API キーの 3 つの特定の値を使用する必要があります。 以下で、サービスからこれらの値を取得する方法について説明します。 これらの値を [メモ帳] の空白のページに書き留めてください。この後すぐにコードで使用するためです。
Instrumentation Keyを見つけるには、サービス関数の一覧を下にスクロールし、Propertiesを選択する必要があります。表示されるタブには、Service キーが表示されます。
Propertiesの少し下に、クリックする必要API Accessがあります。 右側のパネルに、アプリの [アプリケーション ID] が表示されます。
[アプリケーション ID] パネルを開いたままにして、[API キーの作成] をクリックすると、"[API キーの作成]" パネルが開きます。
開かれた "[API キーの作成]" パネルで、説明を入力し、3 つのボックスをオンにします。
[キーの生成] をクリックします。 API キーが作成されて、表示されます。
警告
サービス キーが表示されるのはこのときだけのため、必ずここでコピーを作成してください。
第 2 章 - Unity プロジェクトの設定
Mixed Reality を使用して開発するための一般的なセットアップを次に示します。そのため、他のプロジェクトに適したテンプレートです。
Unity を開き、[新規] をクリックします。
次に、Unity プロジェクト名を指定し、 MR_Azure_Application_Insightsを挿入する必要があります。 [テンプレート] が [3D] に設定されていることを確認します。 [場所] を適切な場所に設定します (ルート ディレクトリに近い方が適しています)。 [Create project]\(プロジェクトの作成\) をクリックします。
Unity を開いた状態で、既定の Script エディター が Visual Studio に設定されていることを確認する価値があります。 [編集] >[Preferences] (環境設定) に移動し、新しいウィンドウで [外部ツール] に移動します。 [外部スクリプト エディター] を [Visual Studio 2017] に変更します。 [環境設定] ウィンドウを閉じます。
次に、[ファイル] > [ビルド設定] で、[プラットフォームの切り替え] ボタンをクリックして、プラットフォームを [ユニバーサル Windows プラットフォーム] に切り替えます。
[ファイル] > [ビルド設定] に移動して、次を確認します。
[ターゲット デバイス] が [すべてのデバイス] に設定されている。
Microsoft HoloLens の場合は、[ターゲット デバイス] を [HoloLens] に設定します。
[Build Type] (ビルドの種類) が [D3D] に設定されている
[SDK] が [最新のインストール] に設定されている。
[Build and Run] (ビルドと実行) が [Local Machine] (ローカル マシン) に設定されている
シーンを保存し、ビルドに追加します。
これを行うには、[Add Open Scenes] (開いているシーンを追加) を選択します。 保存ウィンドウが表示されます。
これと、今後のシーン用の新しいフォルダーを作成し、[New folder]\(新しいフォルダー\) ボタンをクリックして、新しいフォルダーを作成し、「Scenes」という名前を付けます。
新しく作成した Scenes フォルダーを開き、[File name:]\(ファイル名\) テキスト フィールドに「ApplicationInsightsScene」と入力して [Save]\(保存\) をクリックします。
[ビルド設定] の残りの設定は、ここでは既定値のままにしておきます。
Build の設定 ウィンドウで、Player Settings を選択すると、Inspector が配置されている領域に関連するパネルが開きます。
このパネルでは、いくつかの設定を確認する必要があります。
[Other Settings] (その他の設定) タブで、次の内容を確認します。
Scripting Runtime Version はexperimental (.NET 4.6 Equivalent)する必要があり、エディターを再起動する必要があります。
[スクリプト バックエンド] が [.NET] である。
[API Compatibility Level]\(API 互換性レベル\) が [.NET 4.6] である。
[Publishing Settings]\(公開設定\) タブ内の [Capabilities]\(機能\) で、次の内容を確認します。
InternetClient
パネルのさらに下にある [XR Settings]\(XR 設定\) ([Publishing Settings]\(公開設定\) の下) で、[Virtual Reality Supported]\(Virtual Reality サポート\) をオンにし、Windows Mixed Reality SDK が追加されていることを確認します。
[ビルド設定] に戻ると、"Unity C# プロジェクト" に適用されていた灰色表示が解除されています。その横にあるチェック ボックスをオンにします。
[ビルド設定] ウィンドウを閉じます。
シーンとプロジェクトを保存します ([FILE]\(ファイル\)>[SAVE SCENE / FILE]\(シーン/ファイルの保存\)>[SAVE PROJECT]\(プロジェクトの保存\))。
第 3 章 - Unity パッケージのインポート
重要
このコースの Unity のセットアップ コンポーネントをスキップして、そのままコードに進みたい場合は、この Azure-MR-309.unitypackage をダウンロードして、カスタム パッケージとしてプロジェクトにインポートしてください。 これには、次の章の DLL も含まれます。 インポート後、第 6 章 から手順を進めてださい。
重要
Unity 内で Application Insights を使用するには、その DLL を Newtonsoft DLL とともにインポートする必要があります。 現在、Unity には、インポート後にプラグインを再構成する必要がある既知の問題があります。 バグが解決された後は、これらの手順 (このセクションの 4 - 7) は不要になります。
Application Insights を独自のプロジェクトにインポートするには、プラグインを含む '.unitypackage' をダウンロード していることを確認。 次に、以下を実行します。
Assets > Import Package > Custom Package メニュー オプションを使用して、Unity に .unitypackage** を追加します。
ポップアップ表示される [Unity パッケージのインポート] ボックスで、[プラグイン] およびその下にあるすべての項目が選択されていることを確認します。
[Import]\(インポート\) ボタンをクリックして、各項目をプロジェクトに追加します。
[プロジェクト] ビューの [プラグイン] の下の Insights フォルダーに移動して、以下のプラグインのみ選択します。
- Microsoft.ApplicationInsights
この "プラグイン" を選択した状態で、[任意のプラットフォーム] がオフになっていることを確認し、[WSAPlayer] もオフになっていることを確認してから、[適用] をクリックします。 この操作は単に、ファイルが正しく構成されていることを確認するためのものです。
Note
プラグインは、このようにマークされると、Unity エディターでのみ使用されるように構成されます。 プロジェクトが Unity からエクスポートされた後に使用される異なるセットの DLL が WSA フォルダー内にあります。
次に、Insights フォルダー内にある WSA フォルダーを開く必要があります。 構成したのと同じファイルのコピーが表示されます。 このファイルを選択し、インスペクターで、 Any Platform が unchecked であることを確認してから、 only WSAPlayer が チェックされていることを確認。 [適用] をクリックします。
ステップ4-6に従う必要がありますが、代わりにNewtonsoftプラグインに従う必要があります。 次のスクリーンショットを参照し、結果がどのようになるか確認してください。
第 4 章 - カメラとユーザー コントロールの設定
この章では、ユーザーがシーン内を見て移動できるようにカメラとコントロールを設定します。
[Hierarchy]\(階層\) パネルで空の領域を右クリックして、[Create]\(作成\)>[Empty]\(空\) と進みます。
新しい空の GameObject の名前を [カメラの親] に変更します。
[Hierarchy]\(階層\) パネルで空の領域を右クリックして、[3D Object]\(3D オブジェクト\)、[Sphere]\(スフィア\) と進みます。
スフィアの名前を [右手] に変更します。
[Right Hand]\(右手\) の[Transform]\(変換\) の [Scale]\(スケール\)を 0.1、0.1、0.1 に設定します。
[スフィア コライダー] コンポーネントの歯車をクリックし、次に[コンポーネントの削除] をクリックして、[右手] から [スフィア コライダー] コンポーネントを削除します。
階層パネルで、 Main Camera オブジェクトと Right Hand オブジェクトを Camera Parent オブジェクトにドラッグします。
[Main Camera]\(メイン カメラ\) と [Right Hand]\(右手\) の両方のオブジェクトの [Transform]\(変換\) の [Position]\(位置\) を 0、0、0 に設定します。
第 5 章 - Unity シーン内のオブジェクトの設定
ここでは、ユーザーが操作できるシーンの基本的な図形をいくつか作成します。
[階層] パネルで空の領域を右クリックして、[3D オブジェクト] で [プレーン] を選択します。
[プレーン] の [変換] の [位置] を 0、-1、0 に設定します。
[Plane]\(プレーン\) の [Transform]\(変換\) の [Scale]\(スケール\) を 5、1、5 に設定します。
他のシェイプが見やすくなるように、[Plane]\(プレーン\) オブジェクトで使用する基本的な素材を作成します。 [プロジェクト パネル] に移動して右クリックし、[作成]、[フォルダー] と進んで、新しいフォルダーを作成します。 それに [素材] という名前を付けます。
[素材] フォルダーを開いて右クリックし、[作成]、[素材]と進んで、新しい素材を作成します。 それに [青] という名前を付けます。
新しい [Blue]\(青) の素材を選択した状態で、"[Inspector]\(インスペクター)" を表示して、[Albedo] の横にある長方形のウィンドウをクリックします。 青の色 (以下の図の色は Hex カラー: #3592FFFF です) を選択します。 選択したら、閉じるボタンをクリックします。
[Materials]\(素材) フォルダーにある新しい素材を、シーン内の新しく作成した [Plane]\(プレーン\) にドラッグします (または [Hierarchy]\(階層\) 内の [Plane]\(プレーン\) オブジェクトにドロップします)。
[階層] パネルで空の領域を右クリックして、[3D オブジェクト]、[カプセル] と進みます。
- Capsuleを選択した状態で、Transform Position を -10, 1, 0 に変更します。
[階層] パネルで空の領域を右クリックして、[3D オブジェクト]、[キューブ] と進みます。
- Cubeを選択した状態で、Transform Position を 0, 0, 10 に変更します。
[階層] パネルで空の領域を右クリックして、[3D オブジェクト]、[スフィア] と進みます。
- Sphereを選択した状態で、Transform Position を 10, 0, 0 に変更します。
Note
こらの [位置] の値は推奨値です。 オブジェクトの位置は任意の値に設定できます。ただし、オブジェクトの距離がカメラから離れすぎていない場合の方が、アプリケーションのユーザーにとって操作が簡単です。
アプリケーションが実行されているときに、シーン内のオブジェクトを識別できる必要があります。これを行うには、タグ付けが必要になります。 オブジェクトの 1 つを選択し、Inspector パネルで [タグの追加]<
をクリックします。Inspectorを Tags および Layers パネルにスワップします。 + (プラス) 記号をクリックし、タグ名として ObjectInScene を入力します。
警告
タグに別の名前を使用する場合は、シーン内でオブジェクトが検索および検出されるように、後で DataFromAnalytics、ObjectTrigger、Gaze スクリプトでもこの変更を確実に行う必要があります。
タグを作成したら、3 つのオブジェクトすべてにそれを適用する必要があります。 [階層] で、Shift キーを押しながら [カプセル]、[キューブ]、[スフィア] オブジェクトをクリックし、[インスペクター] で、[タグ] の横にあるドロップダウン メニューをクリックし、作成した [ObjectInScene] タグをクリックします。
第 6 章 - ApplicationInsightsTracker クラスの作成
最初に作成する必要があるスクリプトは ApplicationInsightsTracker です。これは、以下を担当します。
ユーザーとのやり取りに基づいてイベントを作成し、Azure Application Insights に送信する。
ユーザー操作に基づいて、適切なイベント名を作成する。
Application Insights Service インスタンスにイベントを送信する。
このクラスを作成するには、次の手順を実行します。
[プロジェクト] パネルを右クリックし、[作成]>[フォルダー] を選択します。 フォルダーに「Scripts」という名前を付けます。
作成した Scripts フォルダーをダブルクリックして開きます。 次に、そのフォルダー内で右クリックし、[作成]>[C# スクリプト] と選択します。 そのスクリプトに ApplicationInsightsTracker という名前を付けます。
新しい ApplicationInsightsTracker スクリプトをダブルクリックして、 Visual Studio で開きます。
スクリプトの上部にある名前空間を次のように更新します。
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using UnityEngine;
クラス内に次の変数を挿入します。
/// <summary> /// Allows this class to behavior like a singleton /// </summary> public static ApplicationInsightsTracker Instance; /// <summary> /// Insert your Instrumentation Key here /// </summary> internal string instrumentationKey = "Insert Instrumentation Key here"; /// <summary> /// Insert your Application Id here /// </summary> internal string applicationId = "Insert Application Id here"; /// <summary> /// Insert your API Key here /// </summary> internal string API_Key = "Insert API Key here"; /// <summary> /// Represent the Analytic Custom Event object /// </summary> private TelemetryClient telemetryClient; /// <summary> /// Represent the Analytic object able to host gaze duration /// </summary> private MetricTelemetry metric;
Note
第 1 章の手順 9 以降で説明したように、Azure Portal のサービス キーを使用して、instrumentationKey、applicationId、API_Key に適切な値を設定します。
次に、Start() と Awake() メソッドを追加します。これらは、次のように、クラスが初期化されたときに呼び出されます。
/// <summary> /// Sets this class instance as a singleton /// </summary> void Awake() { Instance = this; } /// <summary> /// Use this for initialization /// </summary> void Start() { // Instantiate telemetry and metric telemetryClient = new TelemetryClient(); metric = new MetricTelemetry(); // Assign the Instrumentation Key to the Event and Metric objects TelemetryConfiguration.Active.InstrumentationKey = instrumentationKey; telemetryClient.InstrumentationKey = instrumentationKey; }
アプリケーションによって登録されたイベントとメトリックの送信を担当するメソッドを追加します。
/// <summary> /// Submit the Event to Azure Analytics using the event trigger object /// </summary> public void RecordProximityEvent(string objectName) { telemetryClient.TrackEvent(CreateEventName(objectName)); } /// <summary> /// Uses the name of the object involved in the event to create /// and return an Event Name convention /// </summary> public string CreateEventName(string name) { string eventName = $"User near {name}"; return eventName; } /// <summary> /// Submit a Metric to Azure Analytics using the metric gazed object /// and the time count of the gaze /// </summary> public void RecordGazeMetrics(string objectName, int time) { // Output Console information about gaze. Debug.Log($"Finished gazing at {objectName}, which went for <b>{time}</b> second{(time != 1 ? "s" : "")}"); metric.Name = $"Gazed {objectName}"; metric.Value = time; telemetryClient.TrackMetric(metric); }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 7 章 - Gaze スクリプトの作成
次に作成するスクリプトは、Gaze スクリプトです。 このスクリプトは、レイキャストの作成を担当します。これは、ユーザーがどのオブジェクトを見ているかを検出するために、メイン カメラから前方に投影されます。 この場合、レイキャストは、ユーザーが ObjectInScene タグを持つオブジェクトを見ているかどうか識別し、ユーザーがそのオブジェクトを注視している期間を測定する必要があります。
Scripts フォルダーをダブルクリックして開きます。
Scripts フォルダー内で右クリックし、[作成]>[C# スクリプト] をクリックします。 スクリプトに Gaze という名前を付けます。
スクリプトをダブルクリックして Visual Studio で開きます。
既存のコードを次のコードに置き換えます。
using UnityEngine; public class Gaze : MonoBehaviour { /// <summary> /// Provides Singleton-like behavior to this class. /// </summary> public static Gaze Instance; /// <summary> /// Provides a reference to the object the user is currently looking at. /// </summary> public GameObject FocusedGameObject { get; private set; } /// <summary> /// Provides whether an object has been successfully hit by the raycast. /// </summary> public bool Hit { get; private set; } /// <summary> /// Provides a reference to compare whether the user is still looking at /// the same object (and has not looked away). /// </summary> private GameObject _oldFocusedObject = null; /// <summary> /// Max Ray Distance /// </summary> private float _gazeMaxDistance = 300; /// <summary> /// Max Ray Distance /// </summary> private float _gazeTimeCounter = 0; /// <summary> /// The cursor object will be created when the app is running, /// this will store its values. /// </summary> private GameObject _cursor; }
Awake() メソッドと Start() メソッドのコードを追加する必要があります。
private void Awake() { // Set this class to behave similar to singleton Instance = this; _cursor = CreateCursor(); } void Start() { FocusedGameObject = null; } /// <summary> /// Create a cursor object, to provide what the user /// is looking at. /// </summary> /// <returns></returns> private GameObject CreateCursor() { GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere); // Remove the collider, so it does not block raycast. Destroy(newCursor.GetComponent<SphereCollider>()); newCursor.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); newCursor.GetComponent<MeshRenderer>().material.color = Color.HSVToRGB(0.0223f, 0.7922f, 1.000f); newCursor.SetActive(false); return newCursor; }
Gaze クラス内で、Update() メソッドに次のコードを追加して、レイキャストを投影し、ターゲット ヒットを検出します。
/// <summary> /// Called every frame /// </summary> void Update() { // Set the old focused gameobject. _oldFocusedObject = FocusedGameObject; RaycastHit hitInfo; // Initialize Raycasting. Hit = Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, _gazeMaxDistance); // Check whether raycast has hit. if (Hit == true) { // Check whether the hit has a collider. if (hitInfo.collider != null) { // Set the focused object with what the user just looked at. FocusedGameObject = hitInfo.collider.gameObject; // Lerp the cursor to the hit point, which helps to stabilize the gaze. _cursor.transform.position = Vector3.Lerp(_cursor.transform.position, hitInfo.point, 0.6f); _cursor.SetActive(true); } else { // Object looked on is not valid, set focused gameobject to null. FocusedGameObject = null; _cursor.SetActive(false); } } else { // No object looked upon, set focused gameobject to null. FocusedGameObject = null; _cursor.SetActive(false); } // Check whether the previous focused object is this same object. If so, reset the focused object. if (FocusedGameObject != _oldFocusedObject) { ResetFocusedObject(); } // If they are the same, but are null, reset the counter. else if (FocusedGameObject == null && _oldFocusedObject == null) { _gazeTimeCounter = 0; } // Count whilst the user continues looking at the same object. else { _gazeTimeCounter += Time.deltaTime; } }
ResetFocusedObject() メソッドを追加して、ユーザーがオブジェクトを見たときに、データを Application Insights に送信します。
/// <summary> /// Reset the old focused object, stop the gaze timer, and send data if it /// is greater than one. /// </summary> public void ResetFocusedObject() { // Ensure the old focused object is not null. if (_oldFocusedObject != null) { // Only looking for objects with the correct tag. if (_oldFocusedObject.CompareTag("ObjectInScene")) { // Turn the timer into an int, and ensure that more than zero time has passed. int gazeAsInt = (int)_gazeTimeCounter; if (gazeAsInt > 0) { //Record the object gazed and duration of gaze for Analytics ApplicationInsightsTracker.Instance.RecordGazeMetrics(_oldFocusedObject.name, gazeAsInt); } //Reset timer _gazeTimeCounter = 0; } } }
これで、 Gaze スクリプトが完了しました。 Unity に戻る前に、Visual Studio で変更を保存します。
第 8 章 - ObjectTrigger クラスの作成
次に作成する必要があるスクリプトは ObjectTrigger です。これは、以下を担当します。
- メイン カメラへの衝突に必要なコンポーネントを追加します。
- ObjectInScene としてタグ付けされたオブジェクトの近くにカメラがあるかどうかを検出します。
スクリプトを作成するには:
Scripts フォルダーをダブルクリックして開きます。
Scripts フォルダー内で右クリックし、[作成]>[C# スクリプト] をクリックします。 スクリプトに ObjectTrigger という名前を付けます。
スクリプトをダブルクリックして Visual Studio で開きます。 既存のコードを次のコードに置き換えます。
using UnityEngine; public class ObjectTrigger : MonoBehaviour { private void Start() { // Add the Collider and Rigidbody components, // and set their respective settings. This allows for collision. gameObject.AddComponent<SphereCollider>().radius = 1.5f; gameObject.AddComponent<Rigidbody>().useGravity = false; } /// <summary> /// Triggered when an object with a collider enters this objects trigger collider. /// </summary> /// <param name="collision">Collided object</param> private void OnCollisionEnter(Collision collision) { CompareTriggerEvent(collision, true); } /// <summary> /// Triggered when an object with a collider exits this objects trigger collider. /// </summary> /// <param name="collision">Collided object</param> private void OnCollisionExit(Collision collision) { CompareTriggerEvent(collision, false); } /// <summary> /// Method for providing debug message, and sending event information to InsightsTracker. /// </summary> /// <param name="other">Collided object</param> /// <param name="enter">Enter = true, Exit = False</param> private void CompareTriggerEvent(Collision other, bool enter) { if (other.collider.CompareTag("ObjectInScene")) { string message = $"User is{(enter == true ? " " : " no longer ")}near <b>{other.gameObject.name}</b>"; if (enter == true) { ApplicationInsightsTracker.Instance.RecordProximityEvent(other.gameObject.name); } Debug.Log(message); } } }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 9 章 - DataFromAnalytics クラスの作成
次に、次を担当する DataFromAnalytics スクリプトを作成する必要があります。
- カメラが最も接近したオブジェクトに関する分析データを取得します。
- サービス キーを使用して、Azure Application Insights サービス インスタンスとのやり取りを可能にします。
- イベント数が最も多いものの順に、シーン内のオブジェクトを並べ替えます。
- 最も接近したオブジェクトの素材の色を緑色に変更します。
スクリプトを作成するには:
Scripts フォルダーをダブルクリックして開きます。
Scripts フォルダー内で右クリックし、[作成]>[C# スクリプト] をクリックします。 そのスクリプトに DataFromAnalytics という名前を付けます。
スクリプトをダブルクリックして Visual Studio で開きます。
次の名前空間を挿入します。
using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Networking;
スクリプト内に、以下を挿入します。
/// <summary> /// Number of most recent events to be queried /// </summary> private int _quantityOfEventsQueried = 10; /// <summary> /// The timespan with which to query. Needs to be in hours. /// </summary> private int _timepspanAsHours = 24; /// <summary> /// A list of the objects in the scene /// </summary> private List<GameObject> _listOfGameObjectsInScene; /// <summary> /// Number of queries which have returned, after being sent. /// </summary> private int _queriesReturned = 0; /// <summary> /// List of GameObjects, as the Key, with their event count, as the Value. /// </summary> private List<KeyValuePair<GameObject, int>> _pairedObjectsWithEventCount = new List<KeyValuePair<GameObject, int>>(); // Use this for initialization void Start() { // Find all objects in scene which have the ObjectInScene tag (as there may be other GameObjects in the scene which you do not want). _listOfGameObjectsInScene = GameObject.FindGameObjectsWithTag("ObjectInScene").ToList(); FetchAnalytics(); }
DataFromAnalytics クラス内の Start() メソッドのすぐ後に、FetchAnalytics() という名前の以下のメソッドを追加します。 このメソッドは、GameObject とプレースホルダーのイベント カウント数により、キーと値のペアのリストにデータを設定する役割を担っています。 次に、これは GetWebRequest() コルーチンを初期化します。 Application Insights への呼び出しのクエリ構造も、Query URL エンドポイントとしてこのメソッド内に存在します。
private void FetchAnalytics() { // Iterate through the objects in the list for (int i = 0; i < _listOfGameObjectsInScene.Count; i++) { // The current event number is not known, so set it to zero. int eventCount = 0; // Add new pair to list, as placeholder, until eventCount is known. _pairedObjectsWithEventCount.Add(new KeyValuePair<GameObject, int>(_listOfGameObjectsInScene[i], eventCount)); // Set the renderer of the object to the default color, white _listOfGameObjectsInScene[i].GetComponent<Renderer>().material.color = Color.white; // Create the appropriate object name using Insights structure string objectName = _listOfGameObjectsInScene[i].name; // Build the queryUrl for this object. string queryUrl = Uri.EscapeUriString(string.Format( "https://api.applicationinsights.io/v1/apps/{0}/events/$all?timespan=PT{1}H&$search={2}&$select=customMetric/name&$top={3}&$count=true", ApplicationInsightsTracker.Instance.applicationId, _timepspanAsHours, "Gazed " + objectName, _quantityOfEventsQueried)); // Send this object away within the WebRequest Coroutine, to determine it is event count. StartCoroutine("GetWebRequest", new KeyValuePair<string, int>(queryUrl, i)); } }
FetchAnalytics() メソッドのすぐ下に、GetWebRequest() という名前のメソッドを追加します。これは、IEnumerator を返します。 このメソッドは、特定の GameObject に対応するイベントが Application Insights 内で呼び出された回数を要求する役割を持っています。 送信されたすべてのクエリが返されると、DetermineWinner() メソッドが呼び出されます。
/// <summary> /// Requests the data count for number of events, according to the /// input query URL. /// </summary> /// <param name="webQueryPair">Query URL and the list number count.</param> /// <returns></returns> private IEnumerator GetWebRequest(KeyValuePair<string, int> webQueryPair) { // Set the URL and count as their own variables (for readability). string url = webQueryPair.Key; int currentCount = webQueryPair.Value; using (UnityWebRequest unityWebRequest = UnityWebRequest.Get(url)) { DownloadHandlerBuffer handlerBuffer = new DownloadHandlerBuffer(); unityWebRequest.downloadHandler = handlerBuffer; unityWebRequest.SetRequestHeader("host", "api.applicationinsights.io"); unityWebRequest.SetRequestHeader("x-api-key", ApplicationInsightsTracker.Instance.API_Key); yield return unityWebRequest.SendWebRequest(); if (unityWebRequest.isNetworkError) { // Failure with web request. Debug.Log("<color=red>Error Sending:</color> " + unityWebRequest.error); } else { // This query has returned, so add to the current count. _queriesReturned++; // Initialize event count integer. int eventCount = 0; // Deserialize the response with the custom Analytics class. Analytics welcome = JsonConvert.DeserializeObject<Analytics>(unityWebRequest.downloadHandler.text); // Get and return the count for the Event if (int.TryParse(welcome.OdataCount, out eventCount) == false) { // Parsing failed. Can sometimes mean that the Query URL was incorrect. Debug.Log("<color=red>Failure to Parse Data Results. Check Query URL for issues.</color>"); } else { // Overwrite the current pair, with its actual values, now that the event count is known. _pairedObjectsWithEventCount[currentCount] = new KeyValuePair<GameObject, int>(_pairedObjectsWithEventCount[currentCount].Key, eventCount); } // If all queries (compared with the number which was sent away) have // returned, then run the determine winner method. if (_queriesReturned == _pairedObjectsWithEventCount.Count) { DetermineWinner(); } } } }
次のメソッドは、DetermineWinner() です。これは、GameObject と Int のペアのリストを、イベント数が最も多い順に並べ替えます。 次に、その GameObject の素材の色を緑に変更します (最大カウントを持っていることのフィードバックとして)。 これにより、分析結果を含むメッセージが表示されます。
/// <summary> /// Call to determine the keyValue pair, within the objects list, /// with the highest event count. /// </summary> private void DetermineWinner() { // Sort the values within the list of pairs. _pairedObjectsWithEventCount.Sort((x, y) => y.Value.CompareTo(x.Value)); // Change its colour to green _pairedObjectsWithEventCount.First().Key.GetComponent<Renderer>().material.color = Color.green; // Provide the winner, and other results, within the console window. string message = $"<b>Analytics Results:</b>\n " + $"<i>{_pairedObjectsWithEventCount.First().Key.name}</i> has the highest event count, " + $"with <i>{_pairedObjectsWithEventCount.First().Value.ToString()}</i>.\nFollowed by: "; for (int i = 1; i < _pairedObjectsWithEventCount.Count; i++) { message += $"{_pairedObjectsWithEventCount[i].Key.name}, " + $"with {_pairedObjectsWithEventCount[i].Value.ToString()} events.\n"; } Debug.Log(message); }
Application Insights から受け取った JSON オブジェクトを逆シリアル化するために使用するクラス構造を追加します。 これらのクラスは、クラス定義の外部にある、DataFromAnalytics クラス ファイルの一番下に追加します。
/// <summary> /// These classes represent the structure of the JSON response from Azure Insight /// </summary> [Serializable] public class Analytics { [JsonProperty("@odata.context")] public string OdataContext { get; set; } [JsonProperty("@odata.count")] public string OdataCount { get; set; } [JsonProperty("value")] public Value[] Value { get; set; } } [Serializable] public class Value { [JsonProperty("customMetric")] public CustomMetric CustomMetric { get; set; } } [Serializable] public class CustomMetric { [JsonProperty("name")] public string Name { get; set; } }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 10 章 - Movement クラスの作成
Movement スクリプトは、次に作成する必要があるスクリプトです。 以下の処理を担います。
- カメラが向いている方向に合わせて、メイン カメラを移動します。
- 他のすべてのスクリプトをシーン オブジェクトに追加します。
スクリプトを作成するには:
Scripts フォルダーをダブルクリックして開きます。
Scripts フォルダー内で右クリックし、[作成]>[C# スクリプト] をクリックします。 スクリプトに Movement という名前を付けます。
スクリプトをダブルクリックして、 Visual Studio で開きます。
既存のコードを次のコードに置き換えます。
using UnityEngine; using UnityEngine.XR.WSA.Input; public class Movement : MonoBehaviour { /// <summary> /// The rendered object representing the right controller. /// </summary> public GameObject Controller; /// <summary> /// The movement speed of the user. /// </summary> public float UserSpeed; /// <summary> /// Provides whether source updates have been registered. /// </summary> private bool _isAttached = false; /// <summary> /// The chosen controller hand to use. /// </summary> private InteractionSourceHandedness _handness = InteractionSourceHandedness.Right; /// <summary> /// Used to calculate and proposes movement translation. /// </summary> private Vector3 _playerMovementTranslation; private void Start() { // You are now adding components dynamically // to ensure they are existing on the correct object // Add all camera related scripts to the camera. Camera.main.gameObject.AddComponent<Gaze>(); Camera.main.gameObject.AddComponent<ObjectTrigger>(); // Add all other scripts to this object. gameObject.AddComponent<ApplicationInsightsTracker>(); gameObject.AddComponent<DataFromAnalytics>(); } // Update is called once per frame void Update() { } }
Movement クラス内で、空の Update() メソッドの下に、ユーザーがハンド コントローラーを使用して、仮想空間内を移動できるようにする次のメソッドを挿入します。
/// <summary> /// Used for tracking the current position and rotation of the controller. /// </summary> private void UpdateControllerState() { #if UNITY_WSA && UNITY_2017_2_OR_NEWER // Check for current connected controllers, only if WSA. string message = string.Empty; if (InteractionManager.GetCurrentReading().Length > 0) { foreach (var sourceState in InteractionManager.GetCurrentReading()) { if (sourceState.source.kind == InteractionSourceKind.Controller && sourceState.source.handedness == _handness) { // If a controller source is found, which matches the selected handness, // check whether interaction source updated events have been registered. if (_isAttached == false) { // Register events, as not yet registered. message = "<color=green>Source Found: Registering Controller Source Events</color>"; _isAttached = true; InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated; } // Update the position and rotation information for the controller. Vector3 newPosition; if (sourceState.sourcePose.TryGetPosition(out newPosition, InteractionSourceNode.Pointer) && ValidPosition(newPosition)) { Controller.transform.localPosition = newPosition; } Quaternion newRotation; if (sourceState.sourcePose.TryGetRotation(out newRotation, InteractionSourceNode.Pointer) && ValidRotation(newRotation)) { Controller.transform.localRotation = newRotation; } } } } else { // Controller source not detected. message = "<color=blue>Trying to detect controller source</color>"; if (_isAttached == true) { // A source was previously connected, however, has been lost. Disconnected // all registered events. _isAttached = false; InteractionManager.InteractionSourceUpdated -= InteractionManager_InteractionSourceUpdated; message = "<color=red>Source Lost: Detaching Controller Source Events</color>"; } } if(message != string.Empty) { Debug.Log(message); } #endif }
/// <summary> /// This registered event is triggered when a source state has been updated. /// </summary> /// <param name="obj"></param> private void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs obj) { if (obj.state.source.handedness == _handness) { if(obj.state.thumbstickPosition.magnitude > 0.2f) { float thumbstickY = obj.state.thumbstickPosition.y; // Vertical Input. if (thumbstickY > 0.3f || thumbstickY < -0.3f) { _playerMovementTranslation = Camera.main.transform.forward; _playerMovementTranslation.y = 0; transform.Translate(_playerMovementTranslation * UserSpeed * Time.deltaTime * thumbstickY, Space.World); } } } }
/// <summary> /// Check that controller position is valid. /// </summary> /// <param name="inputVector3">The Vector3 to check</param> /// <returns>The position is valid</returns> private bool ValidPosition(Vector3 inputVector3) { return !float.IsNaN(inputVector3.x) && !float.IsNaN(inputVector3.y) && !float.IsNaN(inputVector3.z) && !float.IsInfinity(inputVector3.x) && !float.IsInfinity(inputVector3.y) && !float.IsInfinity(inputVector3.z); } /// <summary> /// Check that controller rotation is valid. /// </summary> /// <param name="inputQuaternion">The Quaternion to check</param> /// <returns>The rotation is valid</returns> private bool ValidRotation(Quaternion inputQuaternion) { return !float.IsNaN(inputQuaternion.x) && !float.IsNaN(inputQuaternion.y) && !float.IsNaN(inputQuaternion.z) && !float.IsNaN(inputQuaternion.w) && !float.IsInfinity(inputQuaternion.x) && !float.IsInfinity(inputQuaternion.y) && !float.IsInfinity(inputQuaternion.z) && !float.IsInfinity(inputQuaternion.w); }
最後に、Update() メソッド内にメソッド呼び出しを追加します。
// Update is called once per frame void Update() { UpdateControllerState(); }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 11 章 - スクリプト参照の設定
この章では、Movement スクリプトを [カメラの親] に配置し、その参照ターゲットを設定する必要があります。 それにより、このスクリプトによって、他のスクリプトを必要な場所に配置する処理が行われます。
[プロジェクト] パネルの Scripts フォルダーにある Movement スクリプトを、[階層] パネル にある [カメラの親] オブジェクトにドラッグします。
Camera Parent をクリックします。 [階層] パネルで、[右手] オブジェクトを [階層] パネルから、[インスペクター] パネル内の参照ターゲットの [コントローラー] にドラッグします。 次の図に示すように、[User Speed]\(ユーザーの速度\) を 5 に設定します。
第12章 - Unity プロジェクトのビルド
このプロジェクトの Unity セクションに必要なものがすべて完了したので、Unity からビルドします。
[ビルド設定] ([ファイル]>[ビルド設定]) に移動します。
[ビルド設定] ウィンドウで、[ビルド] をクリックします。
エクスプローラー ウィンドウがポップアップ表示され、ビルドの場所を確認するメッセージが表示されます。 新しいフォルダーを作成し (左上隅の [New Folder]\(新規フォルダー\) をクリックします)、それに BUILDS という名前を付けます。
新しい BUILDS フォルダーを開き、フォルダーをもう 1 つ作成し ([New Folder]\(新規フォルダー\) をもう一度使用します)、MR_Azure_Application_Insights という名前を付けます。
MR_Azure_Application_Insights フォルダーを選択した状態で、[フォルダーの選択] をクリックします。 プロジェクトがビルドされるまで 1 分ほどかかります。
ビルドの後、[ファイル エクスプローラー] に、新しいプロジェクトの場所が表示されます。
第 13 章 - MR_Azure_Application_Insights アプリのコンピューターへのデプロイ
MR_Azure_Application_Insights アプリをローカル コンピューターにデプロイするには、次のようにします。
MR_Azure_Application_Insights アプリのソリューション ファイルを Visual Studio で開きます。
ソリューションプラットフォームで、x86、ローカルマシンを選択します。
[ソリューション構成] で、[デバッグ] を選択します。
Build メニューに移動しソリューションのデプロイをクリックして、アプリケーションをコンピューターにサイドロードします。
インストールされたアプリの一覧にお客様のアプリが表示され、起動できる状態になります。
Mixed Reality アプリケーションを起動します。
シーン内を移動し、オブジェクトに近づいてそれらを確認すると、Azure Insights サービスが十分なイベント データを収集したときに、最も接近したオブジェクトが緑色に設定されます。
重要
サービスによるイベントとメトリックの収集の平均待機時間は約 15 分ですが、場合によっては最大 1 時間かかることがあります。
第 14 章 - Application Insights サービスのポータル
シーン内をローミングして複数のオブジェクトを監視すると、 Application Insights Service ポータルで収集されたデータを確認できます。
Application Insights サービスのポータルに戻ります。
[メトリックス エクスプローラー] を選びます。
アプリケーションに関連する Events と Metrics を表すグラフを含むタブで開きます。 前述のように、データがグラフに表示されるまでに時間がかかる場合があります (最大 1 時間)。
Total of Events by Application Version でEvents バーを選択すると、イベントとその名前の詳細な内訳が表示されます。
Application Insights サービス アプリケーションの完成
これで、Application Insights サービスを活用して、アプリ内でユーザーのアクティビティを監視する Mixed Reality アプリが作成されました。
ボーナスの演習
演習 1
ObjectInScene オブジェクトを手動で作成するのではなく、スポーンしてみます。また、スクリプト内のプレーン上に座標を設定します。 このようにして、最も人気のあるオブジェクトを Azure に尋ね (視線入力や近接の結果から)、それらのオブジェクトの 1 つを追加でスポーンすることができます。
演習 2
Application Insights の結果を時間で並べ替えて、最も関連性の高いデータを取得し、その時間別データをアプリケーションに実装します。