HoloLens(1세대) 및 Azure 309: Application Insights
참고 항목
Mixed Reality 아카데미 자습서는 HoloLens(1세대) 및 Mixed Reality 몰입형 헤드셋을 염두에 두고 설계되었습니다. 따라서 이러한 디바이스 개발에 대한 지침을 계속 찾고 있는 개발자를 위해 이러한 자습서를 그대로 두는 것이 중요합니다. 이러한 자습서는 HoloLens 2에 사용되는 최신 도구 집합 또는 상호 작용으로 업데이트되지 않습니다. 대신 지원되는 디바이스에서 계속 작동하도록 유지 관리됩니다. HoloLens 2용으로 개발하는 방법을 보여 주는 새로운 자습서 시리즈가 향후 게시될 예정입니다. 이 알림은 해당 자습서가 게시될 때 해당 자습서에 대한 링크로 업데이트됩니다.
이 과정에서는 Azure 애플리케이션 Insights API를 사용하여 사용자 동작에 대한 분석을 수집하여 Application Insights 기능을 혼합 현실 애플리케이션에 추가하는 방법을 알아봅니다.
Application Insights는 개발자가 애플리케이션에서 분석을 수집하고 사용하기 쉬운 포털에서 관리할 수 있는 Microsoft 서비스입니다. 분석은 성능에서 수집하려는 사용자 지정 정보에 이르기까지 무엇이든 될 수 있습니다. 자세한 내용은 Application Insights 페이지를 참조 하세요.
이 과정을 완료하면 다음을 수행할 수 있는 혼합 현실 몰입형 헤드셋 애플리케이션이 있습니다.
- 사용자가 장면을 응시하고 이동할 수 있도록 허용합니다.
- 시선 및 근접을 사용하여 장면 내 개체에 대한 분석을 Application Insights Service로 보내도록 트리거합니다.
- 또한 앱은 서비스를 호출하여 지난 24시간 이내에 사용자가 가장 접근한 개체에 대한 정보를 가져옵니다. 해당 개체의 색이 녹색으로 변경됩니다.
이 과정에서는 Application Insights Service에서 Unity 기반 샘플 애플리케이션으로 결과를 가져오는 방법을 설명합니다. 빌드할 수 있는 사용자 지정 애플리케이션에 이러한 개념을 적용하는 것은 사용자에게 달려 있습니다.
디바이스 지원
과정 | HoloLens | 몰입형 헤드셋 |
---|---|---|
MR 및 Azure 309: Application Insights | ✔️ | ✔️ |
참고 항목
이 과정은 주로 Windows Mixed Reality 몰입형(VR) 헤드셋에 중점을 두고 있지만 이 과정에서 배운 내용을 Microsoft HoloLens에 적용할 수도 있습니다. 과정을 따라가면 HoloLens를 지원하기 위해 사용해야 할 수 있는 변경 내용에 대한 메모가 표시됩니다. HoloLens를 사용하는 경우 음성 캡처 중에 일부 에코를 확인할 수 있습니다.
필수 조건
참고 항목
이 자습서는 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 데이터 검색을 위한 인터넷 액세스
시작하기 전에
이 프로젝트를 빌드할 때 문제를 방지하려면 루트 또는 루트에 가까운 폴더에 이 자습서의 프로젝트를 만드는 것이 좋습니다(긴 폴더 경로는 빌드 시 문제를 일으킬 수 있음).
Warning
Application Insights로 가는 데이터에는 시간이 걸리므로 인내심을 가져야 합니다. 서비스에서 데이터를 받았는지 확인하려면 14장을 확인하여 포털을 탐색하는 방법을 보여 줍니다.
1장 - Azure Portal
Application Insights를 사용하려면 Azure Portal에서 Application Insights 서비스를 만들고 구성해야 합니다.
Azure Portal에 로그인합니다.
참고 항목
Azure 계정이 아직 없는 경우 계정을 만들어야 합니다. 교실 또는 랩 상황에서 이 자습서를 따르는 경우 강사 또는 프록터 중 한 명에게 새 계정 설정에 대한 도움을 요청하세요.
로그인한 후 왼쪽 위 모서리에서 새로 만들기를 클릭하고 Application Insights를 검색하고 Enter 키를 클릭합니다.
참고 항목
새 단어는 최신 포털에서 리소스 만들기로 대체되었을 수 있습니다.
오른쪽의 새 페이지에서는 Azure 애플리케이션 Insights Service에 대한 설명을 제공합니다. 이 페이지의 왼쪽 아래에서 만들기 단추를 선택하여 이 서비스와의 연결을 만듭니다.
만들기를 클릭한 후에는 다음을 수행합니다.
이 서비스 인스턴스에 원하는 이름을 삽입합니다.
애플리케이션 유형으로 일반을 선택합니다.
적절한 구독을 선택합니다.
리소스 그룹을 선택하거나 새 리소스 그룹을 만듭니다. 리소스 그룹은 Azure 자산 컬렉션에 대한 청구를 모니터링, 제어, 프로비전 및 관리하는 방법을 제공합니다. 단일 프로젝트(예: 이러한 과정)와 연결된 모든 Azure 서비스를 공통 리소스 그룹 아래에 유지하는 것이 좋습니다.
Azure 리소스 그룹에 대해 자세히 알아보려면 리소스 그룹 문서를 방문하세요.
위치를 선택합니다.
또한 이 서비스에 적용된 사용 약관을 이해했는지 확인해야 합니다.
만들기를 실행합니다.
만들기를 클릭하면 서비스가 생성될 때까지 기다려야 합니다. 이 작업은 1분 정도 걸릴 수 있습니다.
서비스 인스턴스가 만들어지면 포털에 알림이 표시됩니다.
알림을 선택하여 새 서비스 인스턴스를 탐색합니다.
알림에서 리소스 로 이동 단추를 클릭하여 새 서비스 인스턴스를 탐색합니다. 새 Application Insights Service 인스턴스로 이동합니다.
참고 항목
이 웹 페이지를 열고 액세스하기 쉽게 유지하면 수집된 데이터를 보기 위해 자주 여기로 돌아옵니다.
Important
Application Insights를 구현하려면 계측 키, 애플리케이션 ID 및 API 키의 세 가지 특정 값을 사용해야 합니다. 아래에서 서비스에서 이러한 값을 검색하는 방법을 확인할 수 있습니다. 코드에서 곧 사용할 수 있으므로 빈 메모장 페이지에서 이러한 값을 기록해 두세요.
계측 키를 찾으려면 서비스 함수 목록을 아래로 스크롤하고 속성을 선택해야 합니다. 표시된 탭에 서비스 키가 표시됩니다.
속성 아래에서 클릭해야 하는 API Access를 찾을 수 있습니다. 오른쪽 패널에서 앱의 애플리케이션 ID를 제공합니다.
애플리케이션 ID 패널이 계속 열려 있는 상태에서 API 키 만들기를 클릭하면 API 키 만들기 패널이 열립니다.
이제 열려 있는 API 키 만들기 패널 내에서 설명을 입력하고 세 개의 상자를 선택합니다.
키 생성을 클릭합니다. API 키가 만들어지고 표시됩니다.
Warning
서비스 키가 표시되는 유일한 시간이므로 지금 복사본을 만들어야 합니다.
2장 - Unity 프로젝트 설정
다음은 혼합 현실로 개발하기 위한 일반적인 설정이며, 따라서 다른 프로젝트에 적합한 템플릿입니다.
Unity를 열고 새로 만들기를 클릭합니다.
이제 Unity 프로젝트 이름을 제공하고 MR_Azure_Application_Insights 삽입해야 합니다. 템플릿이 3D로 설정되어 있는지 확인합니다. 위치를 적절한 위치로 설정합니다(루트 디렉터리에 더 가깝습니다.). 그런 다음 프로젝트 만들기를 클릭합니다.
Unity를 열면 기본 스크립트 편집 기가 Visual Studio로 설정되어 있는지 확인할 필요가 있습니다. 기본 설정 편집 > 으로 이동한 다음 새 창에서 외부 도구로 이동합니다. 외부 스크립트 편집기를 Visual Studio 2017로 변경합니다. 기본 설정 창을 닫습니다.
다음으로 파일 빌드 설정으로 이동하여 플랫폼 전환 단추를 클릭하여 플랫폼을 유니버설 Windows 플랫폼 전환합니다.>
파일 > 빌드 설정으로 이동하여 다음을 확인합니다.
대상 디바이스 가 모든 디바이스로 설정됩니다.
Microsoft HoloLens의 경우 대상 디바이스를 HoloLens로 설정합니다.
빌드 유형 이 D3D로 설정됨
SDK 가 최신 설치됨으로 설정됨
빌드 및 실행 이 로컬 컴퓨터로 설정됩니다.
장면을 저장하고 빌드에 추가합니다.
열린 장면 추가를 선택하여 이 작업을 수행합니다. 저장 창이 나타납니다.
이에 대한 새 폴더 및 이후 장면을 만든 다음 새 폴더 단추를 클릭하여 새 폴더 를 만들고 이름을 Scenes로 지정합니다.
새로 만든 Scenes 폴더를 연 다음 파일 이름: 텍스트 필드에 ApplicationInsightsScene를 입력한 다음 저장을 클릭합니다.
빌드 설정의 나머지 설정은 현재 기본값으로 남아 있어야 합니다.
빌드 설정 창에서 플레이어 설정을 선택하면 Inspector가 있는 공간에서 관련 패널이 열립니다.
이 패널에서 몇 가지 설정을 확인해야 합니다.
기타 설정 탭에서 다음을 수행 합니다.
런타임 버전 스크립팅은 편집기를 다시 시작해야 하는 실험적 버전(.NET 4.6 등가)이어야 합니다.
백 엔드 스크립팅은 .NET이어야 합니다.
API 호환성 수준은 .NET 4.6이어야 합니다.
게시 설정 탭의 기능 아래에서 다음을 확인합니다.
InternetClient
패널 아래에서 XR 설정(게시 설정 아래에 있음)에서 지원되는 Virtual Reality를 확인하여 Windows Mixed Reality SDK가 추가되었는지 확인합니다.
빌드 설정으로 돌아가면 Unity C# 프로젝트가 더 이상 회색으로 표시되지 않습니다. 이 옆에 있는 확인란을 선택합니다.
빌드 설정 창을 닫습니다.
장면 및 프로젝트 저장(FILE>SAVE SCENE / FILE>SAVE PROJECT).
3장 - Unity 패키지 가져오기
Important
이 과정의 Unity 설정 구성 요소를 건너뛰고 코드를 계속 진행하려면 이 Azure-MR-309.unitypackage를 자유롭게 다운로드하여 사용자 지정 패키지로 프로젝트로 가져옵니다. 여기에는 다음 챕터의 DLL도 포함됩니다. 가져온 후 6장에서 계속 합니다.
Important
Unity 내에서 Application Insights를 사용하려면 Newtonsoft DLL과 함께 DLL을 가져와야 합니다. 현재 Unity에는 가져온 후 플러그 인을 다시 구성해야 하는 알려진 문제가 있습니다. 버그가 해결된 후에는 이러한 단계(이 섹션의 4-7)가 더 이상 필요하지 않습니다.
Application Insights를 사용자 고유의 프로젝트로 가져오려면 플러그 인이 포함된 '.unitypackage'를 다운로드했는지 확인합니다. 그런 후 다음을 수행합니다.
자산 패키지 사용자 지정 패키지 > 가져오기 메뉴 옵션을 사용하여 Unity에 .unitypackage**를 추가합니다>.
팝업되는 Unity 패키지 가져오기 상자에서 플러그 인(및 포함) 아래의 모든 항목이 선택되어 있는지 확인합니다.
가져오기 단추를 클릭하여 프로젝트에 항목을 추가합니다.
프로젝트 보기의 플러그 인 아래에 있는 Insights 폴더로 이동하여 다음 플러그 인만 선택합니다.
- Microsoft.ApplicationInsights
이 플러그 인을 선택한 상태에서 모든 플랫폼이 선택 취소되었는지 확인하고 WSAPlayer도 선택 취소되었는지 확인하고 적용을 클릭합니다. 이렇게 하는 것은 파일이 올바르게 구성되었는지 확인하는 것입니다.
참고 항목
이와 같이 플러그 인을 표시하면 Unity 편집기에서만 사용하도록 구성합니다. WSA 폴더에는 프로젝트를 Unity에서 내보낸 후 사용할 다른 DLL 집합이 있습니다.
다음으로 Insights 폴더 내에서 WSA 폴더를 열어야 합니다. 구성한 것과 동일한 파일의 복사본이 표시됩니다. 이 파일을 선택한 다음 검사기에서 모든 플랫폼의 선택을 취소했는지 확인한 다음 WSAPlayer 만 확인합니다. 적용을 클릭합니다.
이제 4-6단계를 수행해야 하지만 대신 Newtonsoft 플러그 인에 대해 수행해야 합니다. 결과가 어떻게 표시되어야 하는지에 대한 아래 스크린샷을 참조하세요.
4장 - 카메라 및 사용자 컨트롤 설정
이 챕터에서는 사용자가 장면을 보고 이동할 수 있도록 카메라와 컨트롤을 설정합니다.
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음 비어 있음 만들기>를 클릭합니다.
비어 있는 새 GameObject 이름을 카메라 부모로 바꿉니다.
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음 3D 개체, Sphere를 차례로 클릭합니다.
구의 이름을 오른손으로 바꿉니다.
오른손의 변환 배율을 0.1, 0.1, 0.1로 설정합니다.
Sphere Collider 구성 요소의 기어를 클릭하여 오른쪽에서 Sphere Collider 구성 요소를 제거한 다음 구성 요소를 제거합니다.
계층 구조 패널에서 주 카메라 및 오른손 개체를 카메라 부모 개체로 끕니다.
주 카메라와 오른손 개체의 변환 위치를 0, 0, 0으로 설정합니다.
5장 - Unity 장면에서 개체 설정
이제 사용자가 상호 작용할 수 있는 장면에 대한 몇 가지 기본 셰이프를 만듭니다.
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음 3D 개체에서 평면을 선택합니다.
평면 변환 위치를 0, -1, 0으로 설정합니다.
평면 변환 배율을 5, 1, 5로 설정합니다.
다른 셰이프를 더 쉽게 볼 수 있도록 Plane 개체와 함께 사용할 기본 재질을 만듭니다. 프로젝트 패널로 이동하여 마우스 오른쪽 단추를 클릭한 다음 만들기, 폴더 차례로 새 폴더를 만듭니다. 이름을 재질로 지정합니다.
Material 폴더를 연 다음 마우스 오른쪽 단추를 클릭하고 [만들기], [재질]을 차례로 클릭하여 새 재질을 만듭니다. 이름을 파란색으로 지정합니다.
새 파란색 재질을 선택한 상태에서 검사기를 살펴보고 Albedo와 함께 사각형 창을 클릭합니다. 파란색을 선택합니다(아래 그림은 16진수 색: #3592FFFF). 선택한 후 닫기 단추를 클릭합니다.
재질 폴더의 새 재질을 장면 내의 새로 만든 평면으로 끌어다 놓습니다(또는 계층 구조 내의 Plane 개체에 놓습니다).
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음 3D 개체인 캡슐을 클릭합니다.
- 캡슐을 선택한 상태에서 변환 위치를 -10, 1, 0으로 변경합니다.
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음, 3D 개체인 큐브를 클릭합니다.
- 큐브를 선택한 상태에서 변환 위치를 0, 0, 10으로 변경합니다.
계층 패널의 빈 영역을 마우스 오른쪽 단추로 클릭한 다음 3D 개체인 Sphere를 클릭합니다.
- Sphere를 선택한 상태에서 변환 위치를 10, 0, 0으로 변경합니다.
참고 항목
이러한 위치 값은 제안 사항입니다. 개체의 위치를 원하는 대로 자유롭게 설정할 수 있지만, 개체 거리가 카메라에서 그리 멀지 않은 경우 애플리케이션 사용자가 더 쉽습니다.
애플리케이션이 실행 중이면 장면 내의 개체를 식별할 수 있어야 합니다. 이를 위해서는 태그를 지정해야 합니다. 개체 중 하나를 선택하고 검사기 패널에서 [태그 추가]를 클릭합니다. 그러면 [검사기]를 [태그] 및 [레이어] 패널로 바꿉니다.
+ (더하기) 기호를 클릭한 다음 태그 이름을 ObjectInScene로 입력합니다.
Warning
태그에 다른 이름을 사용하는 경우 장면 내에서 개체를 찾고 검색할 수 있도록 나중에 DataFromAnalytics, ObjectTrigger 및 Gaze, 스크립트도 변경되었는지 확인해야 합니다.
태그를 만들면 이제 세 개체 모두에 적용해야 합니다. 계층 구조에서 Shift 키를 누른 다음 캡슐, 큐브 및 구체 개체를 클릭한 다음 검사기에서 태그와 함께 드롭다운 메뉴를 클릭한 다음 만든 ObjectInScene 태그를 클릭합니다.
6장 - ApplicationInsightsTracker 클래스 만들기
만들어야 하는 첫 번째 스크립트는 다음을 담당하는 ApplicationInsightsTracker입니다.
Azure 애플리케이션 Insights에 제출할 사용자 상호 작용을 기반으로 이벤트를 만듭니다.
사용자 상호 작용에 따라 적절한 이벤트 이름을 만듭니다.
Application Insights Service 인스턴스에 이벤트 제출
이 클래스를 만들려면 다음을 수행합니다.
프로젝트 패널을 마우스 오른쪽 단추로 클릭한 다음 폴더를 만듭니>다. 폴더 이름을 스크립트로 지정합니다.
스크립트 폴더를 만든 상태에서 두 번 클릭하여 엽니다. 그런 다음 해당 폴더 내에서 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;
참고 항목
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 스크립트입니다. 이 스크립트는 사용자가 보고 있는 개체를 감지하기 위해 주 카메라에서 앞으로 프로젝싱되는 Raycast를 만듭니다. 이 경우 Raycast는 사용자가 ObjectInScene 태그가 있는 개체를 보고 있는지 확인한 다음 사용자가 해당 개체를 응시하는 기간을 계산해야 합니다.
스크립트 폴더를 두 번 클릭하여 엽니다.
스크립트 폴더 내부를 마우스 오른쪽 단추로 클릭하고 C# 스크립트 만들기>를 클릭합니다. 스크립트 응시 이름을 지정합니다.
스크립트를 두 번 클릭하여 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() 메서드에 다음 코드를 추가하여 Raycast를 프로젝션하고 대상 적중을 검색합니다.
/// <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; } } }
이제 응시 스크립트를 완료했습니다. Unity로 돌아가기 전에 Visual Studio에서 변경 내용을 저장합니다.
8장 - ObjectTrigger 클래스 만들기
만들어야 하는 다음 스크립트는 다음을 담당하는 ObjectTrigger입니다.
- 주 카메라에 충돌하는 데 필요한 구성 요소를 추가합니다.
- 카메라가 ObjectInScene으로 태그가 지정된 개체 근처에 있는지 감지합니다.
스크립트를 만들려면 다음을 수행합니다.
스크립트 폴더를 두 번 클릭하여 엽니다.
스크립트 폴더 내부를 마우스 오른쪽 단추로 클릭하고 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 애플리케이션 Insights Service 인스턴스와의 통신을 허용하는 서비스 키를 사용합니다.
- 이벤트 수가 가장 높은 장면의 개체를 정렬합니다.
- 가장 접근한 개체의 재질 색을 녹색으로 변경합니다.
스크립트를 만들려면 다음을 수행합니다.
스크립트 폴더를 두 번 클릭하여 엽니다.
스크립트 폴더 내부를 마우스 오른쪽 단추로 클릭하고 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 호출의 쿼리 구조는 쿼리 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() 메서드 바로 아래에 IEnumerator를 반환하는 GetWebRequest()라는 메서드를 추가합니다. 이 메서드는 Application Insights 내에서 특정 GameObject에 해당하는 이벤트가 호출된 횟수를 요청합니다. 보낸 모든 쿼리가 반환 되면 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(); } } } }
다음 방법은 가장 높은 이벤트 수에 따라 GameObject 및 Int 쌍 목록을 정렬하는 DetermineWinner()입니다. 그런 다음 해당 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장 - 이동 클래스 만들기
이동 스크립트는 만들어야 하는 다음 스크립트입니다. 다음의 역할을 담당합니다.
- 카메라가 바라보는 방향에 따라 주 카메라를 이동합니다.
- 다른 모든 스크립트를 장면 개체에 추가합니다.
스크립트를 만들려면 다음을 수행합니다.
스크립트 폴더를 두 번 클릭하여 엽니다.
스크립트 폴더 내부를 마우스 오른쪽 단추로 클릭하고 C# 스크립트 만들기>를 클릭합니다. 스크립트 이동 이름을 지정합니다.
스크립트를 두 번 클릭하여 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장 - 스크립트 참조 설정
이 챕터에서는 이동 스크립트를 카메라 부모에 배치하고 참조 대상을 설정해야 합니다. 그런 다음, 해당 스크립트는 필요한 위치에 다른 스크립트를 배치하는 작업을 처리합니다.
프로젝트 패널의 스크립트 폴더에서 [계층 구조] 패널에 있는 [카메라 부모] 개체로 이동 스크립트를 끕니다.
카메라 부모를 클릭합니다. 계층 패널의 [계층 구조] 패널에서 [검사기] 패널의 참조 대상 컨트롤러로 오른쪽 개체를 끕니다. 아래 이미지와 같이 사용자 속도를 5로 설정합니다.
12장 - Unity 프로젝트 빌드
이제 이 프로젝트의 Unity 섹션에 필요한 모든 작업이 완료되었으므로 Unity에서 빌드해야 합니다.
빌드 설정(파일>빌드 설정)으로 이동합니다.
빌드 설정 창에서 빌드를 클릭합니다.
파일 탐색기 창이 표시되어 빌드 위치를 묻는 메시지가 표시됩니다. 왼쪽 위 모서리에서 새 폴더를 클릭하여 새 폴더 를 만들고 이름을 BUILDS로 지정합니다.
새 BUILDS 폴더를 열고 다른 폴더(새 폴더 사용)를 만들고 이름을 MR_Azure_Application_Insights.
MR_Azure_Application_Insights 폴더를 선택한 상태에서 폴더 선택을 클릭합니다. 프로젝트를 빌드하는 데 1분 정도 소요됩니다.
빌드 후 새 프로젝트의 위치를 보여 주는 파일 탐색기 표시됩니다.
13장 - 컴퓨터에 MR_Azure_Application_Insights 앱 배포
로컬 머신에 MR_Azure_Application_Insights 앱을 배포하려면 다음을 수행합니다.
Visual Studio에서 MR_Azure_Application_Insights 앱의 솔루션 파일을 엽니다.
솔루션 플랫폼에서 x86, 로컬 머신을 선택합니다.
솔루션 구성에서 디버그를 선택합니다.
빌드 메뉴로 이동하고 솔루션 배포를 클릭하여 애플리케이션을 컴퓨터에 테스트용으로 로드합니다.
이제 앱이 설치된 앱 목록에 표시되고 시작할 준비가 되었습니다.
혼합 현실 애플리케이션을 시작합니다.
장면 주위를 이동하고, 개체에 접근하고, 보고, Azure Insight Service 가 충분한 이벤트 데이터를 수집하면 가장 녹색으로 접근한 개체를 설정합니다.
Important
서비스에서 이벤트 및 메트릭을 수집하기 위한 평균 대기 시간은 약 15분이지만 경우에 따라 최대 1시간이 걸릴 수 있습니다.
14장 - Application Insights Service 포털
장면을 돌아다니며 여러 개체를 살펴보면 Application Insights Service 포털에서 수집된 데이터를 볼 수 있습니다.
Application Insights Service 포털로 돌아갑니다.
메트릭 탐색기를 선택합니다.
애플리케이션과 관련된 이벤트 및 메트릭을 나타내는 그래프가 포함된 탭에서 열립니다. 위에서 설명한 대로 데이터가 그래프에 표시되는 데 시간이 걸릴 수 있습니다(최대 1시간).
애플리케이션 버전별 이벤트 합계에서 이벤트 표시줄을 선택하여 해당 이름으로 이벤트에 대한 자세한 분석을 확인합니다.
완료된 Application Insights Service 애플리케이션
축하합니다. Application Insights 서비스를 활용하여 앱 내에서 사용자의 활동을 모니터링하는 혼합 현실 앱을 빌드했습니다.
보너스 연습
연습 1
ObjectInScene 개체를 수동으로 만드는 대신 생성을 시도하고 스크립트 내 평면에서 해당 좌표를 설정합니다. 이러한 방식으로 가장 인기 있는 개체가 무엇인지 Azure에 물어보고(응시 또는 근접 결과에서) 해당 개체 중 하나를 추가로 생성할 수 있습니다.
연습 2
Application Insights 결과를 시간별로 정렬하여 가장 관련성이 큰 데이터를 얻고 애플리케이션에서 해당 시간을 중요한 데이터를 구현합니다.