This commit is contained in:
Jeff Ballard 2019-05-03 07:06:05 -07:00 коммит произвёл GitHub
Родитель e31845c969
Коммит a801c52db9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 5175 добавлений и 0 удалений

14
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,14 @@
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

Двоичные данные
Plugins/GameTelemetry.unitypackage Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,402 @@
//--------------------------------------------------------------------------------------
// AnimationController.cs
//
// Controller for animation state
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using System;
using System.Collections.Generic;
namespace GameTelemetry
{
//Manages animation state based on the time an animation was started
public class AnimationController
{
private Globals.TelemetryAnimationState state;
private int nextIndexDraw;
private bool needRefresh;
private DateTime localStartTime;
public float AnimSlider;
public float AnimSliderMax = 1;
public bool IsHeatmap = false;
private EventEditorContainer eventContainer;
public EventEditorContainer EventContainer
{
get
{
return eventContainer;
}
set
{
eventContainer = value;
}
}
private int playSpeed;
public int PlaySpeed
{
get
{
return playSpeed;
}
set
{
playSpeed = value;
}
}
public TimeSpan GetTimespan()
{
return eventContainer.GetTimespan();
}
public Color Color
{
get
{
return eventContainer.Color;
}
}
public PrimitiveType Type
{
get
{
return eventContainer.Type;
}
}
public bool ShouldAnimate
{
get
{
return eventContainer.ShouldAnimate;
}
}
public AnimationController()
{
state = Globals.TelemetryAnimationState.Stopped;
playSpeed = 0;
nextIndexDraw = 0;
needRefresh = true;
}
public AnimationController(EventEditorContainer newContainer)
{
state = Globals.TelemetryAnimationState.Stopped;
playSpeed = 0;
nextIndexDraw = 0;
needRefresh = true;
eventContainer = newContainer;
}
public bool IsReady()
{
return eventContainer != null;
}
public bool IsPlaying()
{
return state == Globals.TelemetryAnimationState.Playing;
}
public bool IsPaused()
{
return state == Globals.TelemetryAnimationState.Paused;
}
public bool IsStopped()
{
return state == Globals.TelemetryAnimationState.Stopped;
}
public bool NeedRefresh()
{
bool ret = needRefresh;
needRefresh = false;
return ret;
}
public void Play(int speed)
{
if (eventContainer == null) return;
if (state == Globals.TelemetryAnimationState.Playing && playSpeed == speed)
{
//Already playing at this speed, so pause
Pause();
}
else if (state == Globals.TelemetryAnimationState.Playing)
{
//Need to play at different speed, so reset the start time to when it would have started
//at the given speed
localStartTime = DateTime.UtcNow;
if (nextIndexDraw > 0)
{
localStartTime -= eventContainer.events[nextIndexDraw].Time - eventContainer.events[eventContainer.events.Count - 1].Time;
}
}
else if (state == Globals.TelemetryAnimationState.Stopped)
{
//Start playing from the beginning
needRefresh = true;
if (speed >= 0)
{
nextIndexDraw = eventContainer.events.Count - 1;
}
else
{
nextIndexDraw = 0;
}
localStartTime = DateTime.UtcNow;
state = Globals.TelemetryAnimationState.Playing;
eventContainer.ShouldAnimate = true;
}
else if (state == Globals.TelemetryAnimationState.Paused)
{
//Resume playing, so update start time to include the time this was paused
localStartTime = DateTime.UtcNow;
if (nextIndexDraw > 0)
{
localStartTime -= eventContainer.events[nextIndexDraw].Time - eventContainer.events[eventContainer.events.Count - 1].Time;
}
state = Globals.TelemetryAnimationState.Playing;
eventContainer.ShouldAnimate = true;
}
playSpeed = speed;
}
public void Pause()
{
state = Globals.TelemetryAnimationState.Paused;
eventContainer.ShouldAnimate = false;
}
public void Stop()
{
state = Globals.TelemetryAnimationState.Stopped;
playSpeed = 0;
nextIndexDraw = eventContainer.events.Count - 1;
eventContainer.ShouldAnimate = false;
}
public int GetNextIndex()
{
return nextIndexDraw;
}
public int GetPrevIndex()
{
if (nextIndexDraw <= 0)
{
return eventContainer.events.Count - 1;
}
return nextIndexDraw - 1;
}
//Provides an array of pointers to events that are ready to draw for the animation
public List<TelemetryEvent> GetNextEvents()
{
if (nextIndexDraw <= 0)
{
Stop();
eventContainer.ShouldDraw = true;
}
List<TelemetryEvent> newArray = new List<TelemetryEvent>();
if (playSpeed >= 0)
{
DateTime tempTime = eventContainer.events[eventContainer.events.Count - 1].Time + new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
while (nextIndexDraw >= 0 && eventContainer.events[nextIndexDraw].Time < tempTime)
{
newArray.Add(eventContainer.events[nextIndexDraw]);
nextIndexDraw--;
}
if (newArray.Count == 0 && nextIndexDraw >= 0)
{
//Skip ahead if the gap between events is too long
TimeSpan timeToNext = eventContainer.events[nextIndexDraw].Time - tempTime;
if (timeToNext > new TimeSpan(0, 0, 30))
{
localStartTime -= timeToNext - new TimeSpan(0, 0, 5);
}
}
}
return newArray;
}
//Provides the index of the next event to draw
public int GetNextEventCount()
{
if (nextIndexDraw <= 0)
{
Stop();
}
bool newEvents = false;
if (playSpeed >= 0)
{
DateTime tempTime = eventContainer.events[eventContainer.events.Count - 1].Time + new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
while (nextIndexDraw >= 0 && eventContainer.events[nextIndexDraw].Time < tempTime)
{
newEvents = true;
nextIndexDraw--;
}
if (!newEvents && nextIndexDraw >= 0)
{
//Skip ahead if the gap between events is too long
TimeSpan timeToNext = eventContainer.events[nextIndexDraw].Time - tempTime;
if (timeToNext > new TimeSpan(0, 0, 30))
{
localStartTime -= timeToNext - new TimeSpan(0, 0, 5);
}
}
if (nextIndexDraw < 0)
{
nextIndexDraw = 0;
}
}
return nextIndexDraw;
}
//Provides an array of pointers to events that are ready to draw for the animation (in reverse)
public List<TelemetryEvent> GetPrevEvents()
{
List<TelemetryEvent> newArray = new List<TelemetryEvent>();
if (playSpeed < 0)
{
if (nextIndexDraw >= eventContainer.events.Count - 1)
{
Stop();
eventContainer.ShouldDraw = false;
}
DateTime tempTime = eventContainer.events[0].Time + new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
while (nextIndexDraw < eventContainer.events.Count && eventContainer.events[nextIndexDraw].Time > tempTime)
{
newArray.Add(eventContainer.events[nextIndexDraw]);
nextIndexDraw++;
}
if (newArray.Count == 0 && nextIndexDraw < eventContainer.events.Count)
{
//Skip ahead if the gap between events is too long
TimeSpan timeToNext = eventContainer.events[nextIndexDraw].Time - tempTime;
if (timeToNext > new TimeSpan(0, 0, 30))
{
localStartTime += timeToNext - new TimeSpan(0, 0, 5);
}
}
}
return newArray;
}
//Provides the index of the next event to draw (in reverse)
public int GetPrevEventCount()
{
if (playSpeed < 0)
{
if (nextIndexDraw >= eventContainer.events.Count - 1)
{
Stop();
//eventContainer.SetShouldDraw(false);
}
DateTime tempTime = eventContainer.events[0].Time + new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
bool newEvents = false;
while (nextIndexDraw < eventContainer.events.Count && eventContainer.events[nextIndexDraw].Time > tempTime)
{
newEvents = true;
nextIndexDraw++;
}
if (!newEvents && nextIndexDraw < eventContainer.events.Count)
{
//Skip ahead if the gap between events is too long
TimeSpan timeToNext = eventContainer.events[nextIndexDraw].Time - tempTime;
if (timeToNext > new TimeSpan(0, 0, 30))
{
localStartTime += timeToNext - new TimeSpan(0, 0, 5);
}
}
if (nextIndexDraw >= eventContainer.events.Count)
{
nextIndexDraw = eventContainer.events.Count - 1;
}
}
return nextIndexDraw;
}
//Set a time for playback (0 to 1)
public void SetPlaybackTime(float scale)
{
int newIndexDraw = GetEventIndexForTimeScale(scale);
localStartTime = DateTime.UtcNow;
if (newIndexDraw > 0 && newIndexDraw < eventContainer.events.Count - 1)
{
localStartTime -= eventContainer.events[newIndexDraw].Time - eventContainer.events[eventContainer.events.Count - 1].Time;
}
needRefresh = true;
}
public TimeSpan GetCurrentPlayTime()
{
if (playSpeed >= 0)
{
return new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
}
else
{
return eventContainer.GetTimespan() + new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed);
}
}
public float GetTimeScaleFromTime()
{
return eventContainer.GetTimeScaleFromTime(new TimeSpan((DateTime.UtcNow - localStartTime).Ticks * playSpeed));
}
public float GetEventTimeScale(int index)
{
return eventContainer.GetEventTimeScale(index);
}
public int GetEventIndexForTimeScale(float scale)
{
return eventContainer.GetEventIndexForTimeScale(scale);
}
};
}

Просмотреть файл

@ -0,0 +1,291 @@
//--------------------------------------------------------------------------------------
// EventEditorContainer.cs
//
// Events and Containers for managing telemetry events within the UI
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using System;
using System.Collections.Generic;
namespace GameTelemetry
{
//Primary container for events managed by the UI
public class TelemetryEvent
{
public string User;
public string Build;
public string Name;
public string Category;
public string Session;
public Vector3 Point;
public Vector3 Orientation;
public DateTime Time;
public float Value;
private bool isPct;
public bool IsPercentage
{
get
{
return isPct;
}
}
public TelemetryEvent()
{
Name = "";
Category = "";
Session = "";
User = "";
Build = "";
Point = Vector3.zero;
Orientation = Vector3.zero;
Time = DateTime.Now;
Value = 0;
isPct = false;
}
public TelemetryEvent(string inName, string inCategory, string inSession, string inBuild, string inUser, Vector3 point, DateTime time, float value, bool isPct)
{
this.Point = point;
this.Time = time;
this.Value = value;
this.isPct = isPct;
Orientation = Vector3.zero;
Name = inName;
Category = inCategory;
Session = inSession;
User = inUser;
Build = inBuild;
}
public TelemetryEvent(string inName, string inCategory, string inSession, string inBuild, string inUser, Vector3 point, Vector3 orientation, DateTime time, float value, bool isPct)
{
this.Point = point;
this.Orientation = orientation;
this.Time = time;
this.Value = value;
this.isPct = isPct;
Name = inName;
Category = inCategory;
Session = inSession;
User = inUser;
Build = inBuild;
}
};
//Wrapper for the event container with draw information
public class EventEditorContainer
{
private Color color;
public Color Color
{
get
{
return color;
}
set
{
color = value;
}
}
private bool shouldDraw;
public bool ShouldDraw
{
get
{
return shouldDraw;
}
set
{
shouldDraw = value;
}
}
private bool shouldAnimate;
public bool ShouldAnimate
{
get
{
return shouldAnimate;
}
set
{
shouldAnimate = value;
}
}
private PrimitiveType type;
public PrimitiveType Type
{
get
{
return (PrimitiveType)type;
}
set
{
type = (PrimitiveType)value;
}
}
private bool isPct;
public bool IsPercentage
{
get
{
return isPct;
}
}
public DateTime TimeStart
{
get
{
return events[events.Count - 1].Time;
}
}
public DateTime TimeEnd
{
get
{
return events[0].Time;
}
}
public string Name;
public string Session;
public List<TelemetryEvent> events;
public EventEditorContainer()
{
shouldDraw = Globals.DefaultDrawSetting;
shouldAnimate = false;
isPct = false;
color = Color.red;
type = (int)PrimitiveType.Sphere;
events = new List<TelemetryEvent>();
}
public EventEditorContainer(string Name, int index) : this()
{
this.Name = Name;
color = Globals.DefaultColors[index % Globals.DefaultColors.Length];
}
public EventEditorContainer(List<QueryEvent> inEvents)
{
shouldDraw = Globals.DefaultDrawSetting;
shouldAnimate = false;
color = Color.red;
type = (int)PrimitiveType.Sphere;
if (inEvents.Count > 0)
{
Fill(inEvents);
Name = inEvents[0].Name;
isPct = events[0].IsPercentage;
}
}
public void AddEvent(QueryEvent newEvent)
{
string valueName = "";
double value = 0;
bool isPct = false;
//Check for a specified value the event wants to draw
if (newEvent.TryGetString("disp_val", out valueName))
{
value = newEvent.GetNumber(valueName);
isPct = valueName.StartsWith("pct_");
}
events.Add(new TelemetryEvent(
newEvent.Name,
newEvent.Category,
newEvent.SessionId,
$"{newEvent.BuildType} {newEvent.BuildId} {newEvent.Platform}",
newEvent.UserId,
newEvent.PlayerPosition,
newEvent.PlayerDirection,
newEvent.Time,
(float)value,
isPct));
}
//Add an array of query results
public void Fill(List<QueryEvent> inEvents)
{
foreach (var newEvent in inEvents)
{
AddEvent(newEvent);
}
}
//Provides the total timespan for all events
public TimeSpan GetTimespan()
{
return TimeEnd - TimeStart;
}
//Provides a percent location based on tick time
public float GetTimeScaleFromTime(TimeSpan inTime)
{
return (float)(inTime.TotalMilliseconds/(TimeEnd - TimeStart).TotalMilliseconds);
}
//Provides a percent location based on the element
public float GetEventTimeScale(int index)
{
return (float)((events[events.Count - 1 - index].Time - TimeStart).TotalMilliseconds / (TimeEnd - TimeStart).TotalMilliseconds);
}
//Provides an element based on the percent location
public int GetEventIndexForTimeScale(float scale)
{
int i;
DateTime targetValue = TimeStart.AddTicks((long)((TimeEnd - TimeStart).Ticks * scale));
for (i = events.Count - 1; i > 0; i--)
{
if (events[i].Time > targetValue) break;
}
return i;
}
//Gets the box for where events happen (for heatmap)
public Bounds GetPointRange()
{
return GetPointRange(0, events.Count - 1);
}
//Gets the box for where specified events happen (for heatmap animations)
public Bounds GetPointRange(int start, int end)
{
Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int i = start; i <= end; i++)
{
min.x = Math.Min(min.x, events[i].Point.x);
min.y = Math.Min(min.y, events[i].Point.y);
min.z = Math.Min(min.z, events[i].Point.z);
max.x = Math.Max(max.x, events[i].Point.x);
max.y = Math.Max(max.y, events[i].Point.y);
max.z = Math.Max(max.z, events[i].Point.z);
}
Bounds range = new Bounds();
range.SetMinMax(min, max);
return range;
}
};
}

Просмотреть файл

@ -0,0 +1,220 @@
//--------------------------------------------------------------------------------------
// TelemetryEventGameObject.cs
//
// Event structure used for rendering/managing each event in the game world
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using System;
namespace GameTelemetry
{
public class EventInfo : MonoBehaviour
{
//Client Time of the event
public DateTime Time;
//Value for an event
public float Value;
//Session of the event
public string Session;
//User ID of the event
public string User;
//Build string of the event
public string Build;
//Name of the event
public string Name;
//Category of the event
public string Category;
public void CopyFrom(EventInfo inEvent)
{
this.Time = inEvent.Time;
this.Value = inEvent.Value;
this.Session = inEvent.Session;
this.User = inEvent.User;
this.Build = inEvent.Build;
this.Category = inEvent.Category;
this.Name = inEvent.Name;
}
}
//Type of mesh to draw for an event
public class TelemetryEventGameObject
{
private Renderer eventRenderer;
private MaterialPropertyBlock properties = new MaterialPropertyBlock();
//Name and category of event
private string eventName;
//Location of an event
private Vector3 location;
//Orientation of an event
private Vector3 orientation;
//Client time of the event
private EventInfo eventInfo;
//Color of the event
private Color color;
public Color Color
{
get
{
return color;
}
set
{
if (color != value)
{
color = value;
ApplyColor();
}
}
}
//Shape of the event
private PrimitiveType type;
public PrimitiveType Type
{
get
{
return type;
}
set
{
if (type != value)
{
type = value;
ApplyShapeType();
}
}
}
//Host object
private GameObject gameObject;
public GameObject GameObject
{
get
{
return gameObject;
}
}
private Vector3 scale;
public float Scale
{
set
{
if (scale.x != value)
{
scale = new Vector3(value, value, value);
if (gameObject != null)
{
gameObject.transform.localScale = scale;
}
}
}
}
//Populates event values based on a telemetry event
public void SetEvent(TelemetryEvent inEvent, Color inColor, PrimitiveType inType)
{
SetEvent(inEvent, inColor, inType, -1);
}
public void SetEvent(TelemetryEvent inEvent, Color inColor, PrimitiveType inType, int index)
{
eventInfo = new EventInfo();
eventInfo.Time = inEvent.Time;
eventInfo.Value = inEvent.Value;
eventInfo.Session = inEvent.Session;
eventInfo.User = inEvent.User;
eventInfo.Build = inEvent.Build;
eventInfo.Name = inEvent.Name;
eventInfo.Category = inEvent.Category;
location = inEvent.Point;
orientation = inEvent.Orientation;
color = inColor;
type = inType;
scale = new Vector3(0.5f, 0.5f, 0.5f);
eventName = $"{eventInfo.Category} {eventInfo.Name}";
if (index >= 0)
{
eventName += index;
}
ApplyShapeType();
}
//Populates event values for heatmaps
public void SetHeatmapEvent(int index, Vector3 inPoint, Vector3 inOrientation, Color inColor, PrimitiveType inType, float inScale, float inValue)
{
SetHeatmapEvent(index, inPoint, inOrientation, inColor, inType, new Vector3(inScale, inScale, inScale), inValue);
}
public void SetHeatmapEvent(int index, Vector3 inPoint, Vector3 inOrientation, Color inColor, PrimitiveType inType, Vector3 inScale, float inValue)
{
eventInfo = new EventInfo();
eventInfo.Value = inValue;
location = inPoint;
orientation = inOrientation;
color = inColor;
type = inType;
scale = inScale;
eventName = $"Heatmap {index}";
ApplyShapeType();
}
private void ApplyColor()
{
if (gameObject != null)
{
eventRenderer.GetPropertyBlock(properties);
properties.SetColor("_BaseColor", color);
properties.SetColor("_Color", color);
properties.SetColor("_EmissionColor", color);
eventRenderer.SetPropertyBlock(properties);
}
}
private void ApplyShapeType()
{
gameObject = GameObject.CreatePrimitive(type);
gameObject.name = eventName;
gameObject.transform.position = location;
gameObject.transform.localScale = scale;
if (orientation != Vector3.zero)
{
gameObject.transform.rotation = Quaternion.LookRotation(orientation);
}
eventRenderer = gameObject.GetComponent<Renderer>();
EventInfo newInfo = gameObject.AddComponent<EventInfo>();
newInfo.CopyFrom(eventInfo);
ApplyColor();
}
};
}

Просмотреть файл

@ -0,0 +1,435 @@
//--------------------------------------------------------------------------------------
// TelemetryRenderer.cs
//
// Provides building for Telemetry events
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using System;
using System.Collections.Generic;
namespace GameTelemetry
{
public class GameTelemetryRenderer
{
private GameObject host;
public GameObject Host
{
get
{
if(host == null)
{
host = new GameObject("GameTelemetry");
}
return host;
}
}
public float HeatmapValueMin = -1;
public float HeatmapValueMax = -1;
private List<TelemetryEventGameObject> gameObjectCollection = new List<TelemetryEventGameObject>();
private bool needsTelemetryObjectUpdate = false;
public void TriggerTelemetryUpdate()
{
needsTelemetryObjectUpdate = true;
}
// Master draw call. Updates animation if running, otherwise checks if objects need to be updated
public void Tick(List<EventEditorContainer> filterCollection, float heatmapSize, HeatmapColors heatmapColor, int heatmapType, int heatmapShape, bool useOrientation, ref AnimationController animController)
{
if (!animController.IsStopped())
{
if (animController.ShouldAnimate)
{
if (animController.NeedRefresh())
{
DestroyTelemetryObjects();
//If we are playing in reverse, we need to add the remaining points to be removed later
if (animController.PlaySpeed < 0)
{
EventEditorContainer eventContainer = animController.EventContainer;
int j = 0;
if (eventContainer != null)
{
foreach (var currEvent in eventContainer.events)
{
CreateTelemetryObject(j, currEvent, animController.Color, animController.Type);
j++;
}
}
}
}
List<TelemetryEvent> tempArray;
if (animController.PlaySpeed >= 0)
{
if (animController.IsHeatmap)
{
//Regenerate the heatmap only using a subset of the events
int start = animController.GetNextIndex();
int next = animController.GetNextEventCount();
EventEditorContainer tempContainer = animController.EventContainer;
animController.AnimSlider = animController.GetTimeScaleFromTime() * animController.AnimSliderMax;
if (start != next)
{
GenerateHeatmap(tempContainer, heatmapSize, heatmapColor, heatmapType, heatmapShape, useOrientation, next, tempContainer.events.Count - 1);
}
}
else
{
//Update forward point animation by getting events that have occured over the last timespan and creating them
tempArray = animController.GetNextEvents();
int i = animController.GetNextIndex();
animController.AnimSlider = animController.GetTimeScaleFromTime() * animController.AnimSliderMax;
foreach (var currEvent in tempArray)
{
CreateTelemetryObject(i, currEvent, animController.Color, animController.Type);
i++;
}
}
}
else
{
if (animController.IsHeatmap)
{
//Regenerate the heatmap only using a subset of the events
int start = animController.GetPrevIndex();
int next = animController.GetPrevEventCount();
EventEditorContainer tempContainer = animController.EventContainer;
animController.AnimSlider = (1 - animController.GetTimeScaleFromTime()) * animController.AnimSliderMax;
if (start != next)
{
GenerateHeatmap(tempContainer, heatmapSize, heatmapColor, heatmapType, heatmapShape, useOrientation, next, tempContainer.events.Count - 1);
}
}
else
{
//Update reverse point animation by removing the last TelemetryObject for as many events occured over the last timespan
tempArray = animController.GetPrevEvents();
animController.AnimSlider = (1 - animController.GetTimeScaleFromTime()) * animController.AnimSliderMax;
foreach (var currEvent in tempArray)
{
DestroyLastTelemetryObject();
}
}
}
//Set the location of the slider
if (!animController.ShouldAnimate && animController.PlaySpeed >= 0)
{
animController.AnimSlider = animController.AnimSliderMax;
}
else if (!animController.ShouldAnimate && animController.PlaySpeed < 0)
{
animController.AnimSlider = 0;
}
}
}
else
{
if (needsTelemetryObjectUpdate)
{
//Draw data points
needsTelemetryObjectUpdate = false;
DestroyTelemetryObjects();
foreach (var events in filterCollection)
{
if (events.ShouldDraw)
{
int startIndex = 0;
int endIndex = events.events.Count;
CreateTelemetryObjects(events.events, startIndex, endIndex, events.Color, events.Type);
}
}
}
}
}
//Generate a heatmap for the range of events
public void GenerateHeatmap(EventEditorContainer collection, float heatmapSize, HeatmapColors heatmapColor, int heatmapType, int heatmapShape, bool useOrientation)
{
GenerateHeatmap(collection, heatmapSize, heatmapColor, heatmapType, heatmapShape, useOrientation, 0, collection.events.Count - 1);
}
public void GenerateHeatmap(EventEditorContainer collection, float heatmapSize, HeatmapColors heatmapColor, int heatmapType, int heatmapShape, bool useOrientation, int first, int last)
{
DestroyTelemetryObjects();
if (collection.events.Count != 0)
{
if ((first == 0 && first == last) || (first == collection.events.Count - 1 && first == last))
{
first = 0;
last = collection.events.Count - 1;
}
//Segment the world in to blocks of the specified size encompassing all points
Bounds range = collection.GetPointRange(first, last);
float extent = heatmapSize / 2;
Vector3 boxSize = new Vector3(heatmapSize, heatmapSize, heatmapSize);
List<Bounds> parts = new List<Bounds>();
range.max = new Vector3(range.max.x + heatmapSize, range.max.y + heatmapSize, range.max.z + heatmapSize);
for (float x = range.min.x + extent; x < range.max.x; x += heatmapSize)
{
for (float y = range.min.y + extent; y < range.max.y; y += heatmapSize)
{
for (float z = range.min.z + extent; z < range.max.z; z += heatmapSize)
{
parts.Add(new Bounds(new Vector3(x, y, z), boxSize));
}
}
}
if (parts.Count > 0)
{
//For each segment, collect all of the points inside and decide what data to watch based on the heatmap type
List<int> numValues = new List<int>();
List<float> values = new List<float>();
List<Vector3> orientation = new List<Vector3>();
TelemetryEventGameObject tempTelemetryObject;
numValues.AddRange(System.Linq.Enumerable.Repeat(0, parts.Count));
values.AddRange(System.Linq.Enumerable.Repeat<float>(0, parts.Count));
orientation.AddRange(System.Linq.Enumerable.Repeat<Vector3>(new Vector3(), parts.Count));
if ((HeatmapValueMin == -1 && HeatmapValueMax == -1) || HeatmapValueMin == HeatmapValueMax)
{
float largestValue = 0;
int largestNumValue = 0;
if (useOrientation)
{
for (int i = 0; i < parts.Count; i++)
{
for (int j = first; j <= last; j++)
{
if (parts[i].Contains(collection.events[j].Point))
{
numValues[i]++;
orientation[i] += collection.events[j].Orientation;
values[i] += collection.events[j].Value;
}
}
largestValue = Math.Max(largestValue, values[i]);
largestNumValue = Math.Max(largestNumValue, numValues[i]);
orientation[i] = orientation[i] / numValues[i];
}
}
else
{
for (int i = 0; i < parts.Count; i++)
{
for (int j = first; j <= last; j++)
{
if (parts[i].Contains(collection.events[j].Point))
{
numValues[i]++;
values[i] += collection.events[j].Value;
}
}
largestValue = Math.Max(largestValue, values[i]);
largestNumValue = Math.Max(largestNumValue, numValues[i]);
}
}
HeatmapValueMin = 0;
if (heatmapType == (int)Globals.HeatmapType.Value || heatmapType == (int)Globals.HeatmapType.Value_Bar)
{
if (collection.IsPercentage)
{
HeatmapValueMax = 100;
}
else
{
HeatmapValueMax = largestValue;
}
}
else
{
HeatmapValueMax = largestNumValue;
}
}
else
{
if (useOrientation)
{
for (int i = 0; i < parts.Count; i++)
{
for (int j = first; j <= last; j++)
{
if (parts[i].Contains(collection.events[j].Point))
{
numValues[i]++;
orientation[i] += collection.events[j].Orientation;
values[i] += collection.events[j].Value;
}
}
orientation[i] = orientation[i] / numValues[i];
}
}
else
{
for (int i = 0; i < parts.Count; i++)
{
for (int j = first; j <= last; j++)
{
if (parts[i].Contains(collection.events[j].Point))
{
numValues[i]++;
values[i] += collection.events[j].Value;
}
}
}
}
}
float tempColorValue;
if (heatmapType == (int)Globals.HeatmapType.Value)
{
for (int i = 0; i < parts.Count; i++)
{
if (values[i] > 0)
{
tempColorValue = (((float)values[i] / numValues[i]) - HeatmapValueMin) / (HeatmapValueMax - HeatmapValueMin);
tempColorValue = Math.Max(tempColorValue, 0);
tempColorValue = Math.Min(tempColorValue, (float)HeatmapValueMax);
tempTelemetryObject = new TelemetryEventGameObject();
tempTelemetryObject.SetHeatmapEvent(i, parts[i].center, orientation[i], heatmapColor.GetColorFromRange(tempColorValue), (PrimitiveType)heatmapShape, heatmapSize, values[i]);
CreateHeatmapObject(tempTelemetryObject);
}
}
}
else if (heatmapType == (int)Globals.HeatmapType.Population)
{
for (int i = 0; i < parts.Count; i++)
{
if (numValues[i] > 0)
{
tempColorValue = (float)(numValues[i] - HeatmapValueMin) / (HeatmapValueMax - HeatmapValueMin);
tempColorValue = Math.Max(tempColorValue, 0);
tempColorValue = Math.Min(tempColorValue, (float)HeatmapValueMax);
tempTelemetryObject = new TelemetryEventGameObject();
tempTelemetryObject.SetHeatmapEvent(i, parts[i].center, orientation[i], heatmapColor.GetColorFromRange(tempColorValue), (PrimitiveType)heatmapShape, heatmapSize, numValues[i]);
CreateHeatmapObject(tempTelemetryObject);
}
}
}
else if (heatmapType == (int)Globals.HeatmapType.Value_Bar)
{
float tempHeight;
for (int i = 0; i < parts.Count; i++)
{
if (values[i] > 0)
{
tempColorValue = (((float)values[i] / numValues[i]) - HeatmapValueMin) / (HeatmapValueMax - HeatmapValueMin);
tempColorValue = Math.Max(tempColorValue, 0);
tempColorValue = Math.Min(tempColorValue, (float)HeatmapValueMax);
tempHeight = (((float)values[i] / numValues[i]) / HeatmapValueMax) * heatmapSize;
tempTelemetryObject = new TelemetryEventGameObject();
tempTelemetryObject.SetHeatmapEvent(i, parts[i].center, Vector3.zero, heatmapColor.GetColorFromRange(tempColorValue), PrimitiveType.Cube, new Vector3(heatmapSize, heatmapSize, tempHeight), values[i]);
CreateHeatmapObject(tempTelemetryObject);
}
}
}
else if (heatmapType == (int)Globals.HeatmapType.Population_Bar)
{
float tempHeight;
for (int i = 0; i < parts.Count; i++)
{
if (numValues[i] > 0)
{
tempColorValue = (float)(numValues[i] - HeatmapValueMin) / (HeatmapValueMax - HeatmapValueMin);
tempColorValue = Math.Max(tempColorValue, 0);
tempColorValue = Math.Min(tempColorValue, (float)HeatmapValueMax);
tempHeight = ((float)numValues[i] / HeatmapValueMax) * heatmapSize;
tempTelemetryObject = new TelemetryEventGameObject();
tempTelemetryObject.SetHeatmapEvent(i, parts[i].center, Vector3.zero, heatmapColor.GetColorFromRange(tempColorValue), PrimitiveType.Cube, new Vector3(heatmapSize, heatmapSize, tempHeight), numValues[i]);
CreateHeatmapObject(tempTelemetryObject);
}
}
}
}
}
}
// Set up and create the gameobject for a heatmap component
public void CreateHeatmapObject(TelemetryEventGameObject inEvent)
{
gameObjectCollection.Add(inEvent);
inEvent.GameObject.transform.SetParent(host.transform);
UnityEngine.Object.DestroyImmediate(inEvent.GameObject.GetComponent<Collider>());
}
// Set up and create the gameobject for a telemetry event
public void CreateTelemetryObject(int index, TelemetryEvent data, Color color, PrimitiveType shape)
{
TelemetryEventGameObject tempTelemetryObject = new TelemetryEventGameObject();
tempTelemetryObject.SetEvent(data, color, shape, index);
gameObjectCollection.Add(tempTelemetryObject);
tempTelemetryObject.GameObject.transform.SetParent(host.transform);
UnityEngine.Object.DestroyImmediate(tempTelemetryObject.GameObject.GetComponent<Collider>());
}
// Create the gameobjects for a list of telemetry events
public void CreateTelemetryObjects(List<TelemetryEvent> data, Color color, PrimitiveType shape)
{
for(int i = 0; i < data.Count; i++)
{
CreateTelemetryObject(i, data[i], color, shape);
}
}
// Create the gameobjects for a list of telemetry events in a specific range
public void CreateTelemetryObjects(List<TelemetryEvent> data, int start, int end, Color color, PrimitiveType shape)
{
for (int i = start; i < data.Count && i < end; i++)
{
CreateTelemetryObject(i, data[i], color, shape);
}
}
// Destroys all known gameobjects
public void DestroyTelemetryObjects()
{
foreach(var gameObject in gameObjectCollection)
{
UnityEngine.Object.DestroyImmediate(gameObject.GameObject);
}
gameObjectCollection.Clear();
}
// Destroys the last known gameobject
public void DestroyLastTelemetryObject()
{
if (gameObjectCollection.Count > 0)
{
UnityEngine.Object.DestroyImmediate(gameObjectCollection[gameObjectCollection.Count - 1].GameObject);
gameObjectCollection.RemoveAt(gameObjectCollection.Count - 1);
}
}
}
}

Просмотреть файл

@ -0,0 +1,212 @@
//--------------------------------------------------------------------------------------
// TelemetryVisualizerTypes.cs
//
// Provides base types for the visualizer
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
namespace GameTelemetry
{
public static class Globals
{
//Setting whether to draw recieved events by default
public const bool DefaultDrawSetting = true;
//Set of default colors for drawing events
public static Color[] DefaultColors =
{
Color.red,
Color.green,
Color.blue,
Color.yellow,
Color.cyan,
Color.grey,
Color.magenta,
Color.black,
Color.white
};
//Types of heatmaps offered
public enum HeatmapType
{
Population,
Value,
Population_Bar,
Value_Bar
};
public static string[] HeatmapTypeString =
{
"Population",
"Value",
"Population - Bar",
"Value - Bar"
};
public enum TelemetryAnimationState
{
Stopped,
Playing,
Paused,
};
//Strings associated with different viz settings for UI
public static string[] ShapeStrings =
{
"Sphere",
"Capsule",
"Cylinder",
"Cube",
"Plane"
};
public static string[] AndOrStrings =
{
"Or",
"And"
};
public enum QueryField
{
Category,
BuildId,
BuildType,
ClientId,
Platform,
ProcessId,
SessionId,
UserId
};
public static string[] QueryFieldStrings =
{
"Category",
"Build ID",
"Build Type",
"Client ID",
"Platform",
"Process ID",
"Session ID",
"User ID"
};
public static string[] QueryExpectedStrings =
{
QueryIds.Category,
QueryIds.BuildId,
QueryIds.BuildType,
QueryIds.ClientId,
QueryIds.Platform,
QueryIds.ProcessId,
QueryIds.SessionId,
QueryIds.UserId,
};
public enum QueryOperator
{
Equal,
Not_Equal,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual
};
public static string[] QueryOperatorStrings =
{
"==",
"<>",
">",
"<",
">=",
"<="
};
}
//Wraps colors for heatmap settings
public class HeatmapColors
{
private Color lowColor;
public Color LowColor
{
get
{
return lowColor;
}
set
{
lowColor = value;
UpdateRange();
}
}
private Color highColor;
public Color HighColor
{
get
{
return highColor;
}
set
{
highColor = value;
UpdateRange();
}
}
private Color range;
private void UpdateRange()
{
range.a = highColor.a - lowColor.a;
range.b = highColor.b - lowColor.b;
range.g = highColor.g - lowColor.g;
range.r = highColor.r - lowColor.r;
}
public HeatmapColors()
{
lowColor = Color.green;
highColor = Color.red;
lowColor.a = .7f;
highColor.a = .7f;
UpdateRange();
}
public Color GetColorFromRange(float location)
{
Color retColor = lowColor;
retColor.a += range.a * location;
retColor.r += range.r * location;
retColor.g += range.g * location;
retColor.b += range.b * location;
return retColor;
}
};
//Container for each UI line of the query
public class QuerySetting
{
public bool isAnd;
public Globals.QueryField Field;
public Globals.QueryOperator Operator;
public string Value;
public QuerySetting()
{
isAnd = false;
Field = 0;
Operator = 0;
Value = "";
}
};
}

Просмотреть файл

@ -0,0 +1,661 @@
//--------------------------------------------------------------------------------------
// TelemetryVisualizerUI.cs
//
// Provides interactivity for Telemetry events
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using QueryNodeList = System.Collections.Generic.List<GameTelemetry.QueryNode>;
namespace GameTelemetry
{
public class GameTelemetryWindow : EditorWindow
{
private GameTelemetryRenderer renderer = new GameTelemetryRenderer();
private bool queryFolddown = true;
private bool searchFolddown = true;
private bool vizFolddown = true;
//Margin sizes
private static float marginSize = 15;
private static float textWidth = 130;
private static float postTextMargin = textWidth + 20;
private static float heatmapGap = 3;
private static Color lineColor = new Color(.08f, .08f, .08f);
//Query
private List<QuerySetting> queryCollection = new List<QuerySetting>();
private bool isWaiting = false;
private QueryExecutor executor;
private List<EventEditorContainer> queryEventCollection = new List<EventEditorContainer>();
private List<EventEditorContainer> filterCollection = new List<EventEditorContainer>();
//Search
private List<string> eventNames = new List<string>();
private string searchText = "";
private string eventCount = "Found 0 events";
//Animation
private int vizSelectedEvent;
private float heatmapSize = 2;
private string animMaxTime = "0:00";
private AnimationController animController = new AnimationController();
//Heatmap
private int heatmapType = 0;
private int heatmapShape = 3;
private HeatmapColors heatmapColor = new HeatmapColors();
private bool colorSelect = false;
private bool useOrientation = false;
//Set window defaults
private void OnEnable()
{
//Generate blank clause
if (queryCollection.Count == 0)
{
AddClause(-1);
}
}
[MenuItem("Window/Game Telemetry")]
public static void ShowTelemetryViewer()
{
//Show existing window instance. If one doesn't exist, make one.
EditorWindow.GetWindow(typeof(GameTelemetryWindow), false, "Game Telemetry");
}
private void Update()
{
renderer.Tick(filterCollection, heatmapSize, heatmapColor, heatmapType, heatmapShape, useOrientation, ref animController);
}
void OnGUI()
{
GUILayout.Space(10);
//Query section
EditorGUI.DrawRect(EditorGUILayout.GetControlRect(GUILayout.Height(1)), lineColor);
queryFolddown = EditorGUILayout.Foldout(queryFolddown, "Event Settings");
if(queryFolddown)
{
//GUILayout.Label("Create clauses to define what events are retrieved from the server. The local results are filtered in the search area", EditorStyles.wordWrappedLabel);
EditorGUILayout.HelpBox("Create clauses to define what events are retrieved from the server. The local results are filtered in the search area", MessageType.Info);
GenerateClauseList();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Add New Clause"))
{
AddClause(-1);
}
if (isWaiting)
{
GUILayout.Button("Running...");
}
else
{
if (GUILayout.Button("Submit"))
{
SubmitQuery();
}
}
EditorGUILayout.EndHorizontal();
}
//Search section
EditorGUI.DrawRect(EditorGUILayout.GetControlRect(GUILayout.Height(1)), lineColor);
searchFolddown = EditorGUILayout.Foldout(searchFolddown, "Event Search");
if (searchFolddown)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize * 2);
searchText = EditorGUILayout.TextField("", searchText);
if (GUILayout.Button("Search", GUILayout.Width(80)))
{
FilterEvents();
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize * 2);
GUILayout.Label(eventCount, EditorStyles.label);
EditorGUILayout.EndHorizontal();
GenerateEventGroup();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize * 2);
if (GUILayout.Button("Select All"))
{
for (int i = 0; i < filterCollection.Count; i++)
{
filterCollection[i].ShouldDraw = true;
}
}
if (GUILayout.Button("Select None"))
{
for (int i = 0; i < filterCollection.Count; i++)
{
filterCollection[i].ShouldDraw = false;
}
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
}
//Viz tools section
EditorGUI.DrawRect(EditorGUILayout.GetControlRect(GUILayout.Height(1)), lineColor);
vizFolddown = EditorGUILayout.Foldout(vizFolddown, "Visualization Tools");
if (vizFolddown)
{
GUILayout.Space(10);
//Event group selection
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Event Type", GUILayout.Width(textWidth));
int currSelectedEvent = EditorGUILayout.Popup("", vizSelectedEvent, eventNames.ToArray());
if(vizSelectedEvent != currSelectedEvent)
{
vizSelectedEvent = currSelectedEvent;
for(int i = 0; i < queryEventCollection.Count; i++)
{
if (queryEventCollection[i].Name == eventNames[vizSelectedEvent])
{
animController.EventContainer = queryEventCollection[i];
break;
}
}
TimeSpan tempSpan = animController.GetTimespan();
if(tempSpan.Seconds >= 10)
{
animMaxTime = $"{(int)tempSpan.TotalMinutes}:{tempSpan.Seconds}";
}
else
{
animMaxTime = $"{(int)tempSpan.TotalMinutes}:0{tempSpan.Seconds}";
}
animController.AnimSliderMax = (float)tempSpan.TotalSeconds;
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(10);
//Animation
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
float newSlider = GUILayout.HorizontalSlider(animController.AnimSlider, 0, animController.AnimSliderMax);
if(animController.AnimSlider != newSlider)
{
animController.AnimSlider = newSlider;
if(animController.IsReady())
{
animController.SetPlaybackTime(animController.AnimSlider / animController.AnimSliderMax);
}
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
EditorGUILayout.LabelField("0:00");
GUIStyle rightAlign = new GUIStyle(GUI.skin.label);
rightAlign.alignment = TextAnchor.MiddleRight;
EditorGUILayout.LabelField(animMaxTime, rightAlign);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
if (GUILayout.Button("<<<"))
{
if (animController.IsReady()) PlayAnimation(-4);
}
if (GUILayout.Button("<<"))
{
if (animController.IsReady()) PlayAnimation(-2);
}
if (GUILayout.Button("||"))
{
if (animController.IsReady()) PlayAnimation(0);
}
if (GUILayout.Button(">"))
{
PlayAnimation(1);
}
if (GUILayout.Button(">>"))
{
if (animController.IsReady()) PlayAnimation(2);
}
if (GUILayout.Button(">>>"))
{
if (animController.IsReady()) PlayAnimation(4);
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(10);
EditorGUI.DrawRect(EditorGUILayout.GetControlRect(GUILayout.Height(1)), lineColor);
//Heatmap
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
GUILayout.Label("Heatmap Settings");
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(heatmapGap);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Type", GUILayout.Width(textWidth));
heatmapType = EditorGUILayout.Popup("", heatmapType, Globals.HeatmapTypeString);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(heatmapGap);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Shape", GUILayout.Width(textWidth));
heatmapShape = EditorGUILayout.Popup("", heatmapShape, Globals.ShapeStrings);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(heatmapGap);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Shape Size", GUILayout.Width(textWidth));
heatmapSize = EditorGUILayout.FloatField(heatmapSize);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(heatmapGap);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Color Range", GUILayout.Width(textWidth));
heatmapColor.LowColor = EditorGUILayout.ColorField(new GUIContent(), heatmapColor.LowColor, false, true, false, GUILayout.Height(35));
GUILayout.Space(marginSize);
heatmapColor.HighColor = EditorGUILayout.ColorField(new GUIContent(), heatmapColor.HighColor, false, true, false, GUILayout.Height(35));
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
GUILayout.Label("Minimum", EditorStyles.label);
GUILayout.Space(marginSize);
GUILayout.Label("Maximum", EditorStyles.label);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(heatmapGap);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.LabelField("Value Range", GUILayout.Width(textWidth));
renderer.HeatmapValueMin = EditorGUILayout.FloatField(renderer.HeatmapValueMin);
GUILayout.Space(marginSize);
renderer.HeatmapValueMax = EditorGUILayout.FloatField(renderer.HeatmapValueMax);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
GUILayout.Label("Minimum", EditorStyles.label);
GUILayout.Space(marginSize);
GUILayout.Label("Maximum", EditorStyles.label);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
useOrientation = EditorGUILayout.ToggleLeft("Use Orientation", useOrientation);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
animController.IsHeatmap = EditorGUILayout.ToggleLeft("Apply to Animation", animController.IsHeatmap);
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
GUILayout.Space(marginSize);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(postTextMargin);
if (GUILayout.Button("Generate"))
{
if (queryEventCollection.Count == 0 || vizSelectedEvent < 0) return;
EventEditorContainer collection = new EventEditorContainer();
for (int i = 0; i < queryEventCollection.Count; i++)
{
queryEventCollection[i].ShouldAnimate = false;
queryEventCollection[i].ShouldDraw = false;
if (queryEventCollection[i].Name == eventNames[vizSelectedEvent])
{
collection = queryEventCollection[i];
}
}
renderer.GenerateHeatmap(collection, heatmapSize, heatmapColor, heatmapType, heatmapShape, useOrientation);
}
GUILayout.Space(marginSize);
EditorGUILayout.EndHorizontal();
}
GUILayout.Space(10);
}
// Creates list of query clauses
void GenerateClauseList()
{
GUIStyle buttonStyle = EditorStyles.miniButton;
buttonStyle.stretchWidth = false;
GUILayout.BeginScrollView(new Vector2(), false, true, GUILayout.Height(200));
for (int i = 0; i < queryCollection.Count; i++)
{
EditorGUILayout.BeginHorizontal();
// Plus
if (GUILayout.Button(new GUIContent("+", "Add New Clause"), buttonStyle))
{
AddClause(i);
}
if (i > 0)
{
// And/Or
queryCollection[i].isAnd = Convert.ToBoolean(EditorGUILayout.Popup("", Convert.ToInt32(queryCollection[i].isAnd), Globals.AndOrStrings, GUILayout.Width(90)));
}
else
{
GUILayout.Space(94);
}
// Category
queryCollection[i].Field = (Globals.QueryField)EditorGUILayout.Popup("", (int)queryCollection[i].Field, Globals.QueryFieldStrings, GUILayout.Width(100));
// ==
queryCollection[i].Operator = (Globals.QueryOperator)EditorGUILayout.Popup("", (int)queryCollection[i].Operator, Globals.QueryOperatorStrings, GUILayout.Width(100));
// Textbox
queryCollection[i].Value = EditorGUILayout.TextField("", queryCollection[i].Value);
// X
if (GUILayout.Button("X", buttonStyle))
{
RemoveClause(i);
}
EditorGUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
}
// Add a clause to the group
public void AddClause(int index)
{
if (index == -1)
{
queryCollection.Add(new QuerySetting());
}
else
{
queryCollection.Insert(index, new QuerySetting());
}
}
// Remove clause from group
public void RemoveClause(int index)
{
queryCollection.RemoveAt(index);
if (queryCollection.Count == 0)
{
AddClause(-1);
}
}
//Translates query UI in to query nodes for execution
public void SubmitQuery()
{
QueryNodeList andNodes = new QueryNodeList();
QueryNodeList orNodes = new QueryNodeList();
queryCollection[0].isAnd = true;
for (int i = 0; i < queryCollection.Count; i++)
{
QueryOp op = QueryOp.Eq;
switch ((Globals.QueryOperator)queryCollection[i].Operator)
{
case Globals.QueryOperator.Not_Equal:
op = QueryOp.Neq;
break;
case Globals.QueryOperator.GreaterThan:
op = QueryOp.Gt;
break;
case Globals.QueryOperator.LessThan:
op = QueryOp.Lt;
break;
case Globals.QueryOperator.GreaterThanOrEqual:
op = QueryOp.Gte;
break;
case Globals.QueryOperator.LessThanOrEqual:
op = QueryOp.Lte;
break;
}
if (queryCollection[i].isAnd)
{
andNodes.Add(new QueryNode(QueryNodeType.Comparison, Globals.QueryExpectedStrings[(int)queryCollection[i].Field], op, new JSONObj($"\"{queryCollection[i].Value}\"")));
}
else
{
orNodes.Add(new QueryNode(QueryNodeType.Comparison, Globals.QueryExpectedStrings[(int)queryCollection[i].Field], op, new JSONObj($"\"{queryCollection[i].Value}\"")));
}
}
if (queryCollection.Count == 1)
{
CollectEvents(andNodes[0]);
}
else
{
if (orNodes.Count == 1)
{
andNodes.Add(orNodes[0]);
CollectEvents(new QueryNode(QueryNodeType.Group, QueryOp.Or, andNodes));
}
else
{
if (orNodes.Count > 1)
{
andNodes.Add(new QueryNode(QueryNodeType.Group, QueryOp.Or, orNodes));
}
CollectEvents(new QueryNode(QueryNodeType.Group, QueryOp.And, andNodes));
}
}
}
//Call to execute query
public void CollectEvents(QueryNode query)
{
if (!isWaiting)
{
executor = renderer.Host.AddComponent<QueryExecutor>();
executor.ExecuteCustomQuery(QuerySerializer.Serialize(query).ToString(), QueryResults);
isWaiting = true;
}
}
//Called when the query request completes. Converts results to a local collection
public void QueryResults(QueryResult results)
{
queryEventCollection.Clear();
eventNames.Clear();
string currentName;
foreach (var newEvent in results.Events)
{
int currentIdx = -1;
currentName = newEvent.Name;
for (int i = 0; i < queryEventCollection.Count; i++)
{
if (queryEventCollection[i].Name == currentName)
{
currentIdx = i;
break;
}
}
if (currentIdx == -1)
{
queryEventCollection.Add(new EventEditorContainer(currentName, queryEventCollection.Count));
currentIdx = queryEventCollection.Count - 1;
}
queryEventCollection[currentIdx].AddEvent(newEvent);
}
foreach(var events in queryEventCollection)
{
eventNames.Add(events.Name);
}
vizSelectedEvent = -1;
searchFolddown = true;
FilterEvents();
if(eventNames.Count > 0)
{
vizFolddown = true;
}
isWaiting = false;
DestroyImmediate(executor);
}
// Creates list of event groups
void GenerateEventGroup()
{
GUILayout.BeginScrollView(Vector2.zero, false, true, GUILayout.MaxHeight(150));
for (int i = 0; i < filterCollection.Count; i++)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(marginSize * 2);
// Checkbox
bool tempDraw = EditorGUILayout.ToggleLeft(filterCollection[i].Name, filterCollection[i].ShouldDraw);
if (tempDraw != filterCollection[i].ShouldDraw)
{
filterCollection[i].ShouldDraw = tempDraw;
renderer.TriggerTelemetryUpdate();
}
GUILayout.Space(marginSize);
// Shape
PrimitiveType tempShape = (PrimitiveType)EditorGUILayout.Popup("", (int)filterCollection[i].Type, Globals.ShapeStrings);
if(tempShape != filterCollection[i].Type)
{
filterCollection[i].Type = tempShape;
renderer.TriggerTelemetryUpdate();
}
// Color
Color tempColor = EditorGUILayout.ColorField(new GUIContent(), filterCollection[i].Color, false, true, false);
if (tempColor != filterCollection[i].Color)
{
if (!colorSelect)
{
filterCollection[i].Color = tempColor;
renderer.TriggerTelemetryUpdate();
colorSelect = true;
}
else
{
colorSelect = false;
}
}
EditorGUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
}
//Trim the event group list based on search parameters
public void FilterEvents()
{
int count = 0;
filterCollection.Clear();
if (searchText == string.Empty)
{
for (int i = 0; i < queryEventCollection.Count; i++)
{
filterCollection.Add(queryEventCollection[i]);
count += queryEventCollection[i].events.Count;
}
}
else
{
for (int i = 0; i < queryEventCollection.Count; i++)
{
if (queryEventCollection[i].Name.Contains(searchText))
{
filterCollection.Add(queryEventCollection[i]);
count += queryEventCollection[i].events.Count;
}
}
}
eventCount = $"Found {count} events";
renderer.TriggerTelemetryUpdate();
}
// Starts animation
public void PlayAnimation(int speed)
{
for (int i = 0; i < queryEventCollection.Count; i++)
{
queryEventCollection[i].ShouldDraw = false;
}
animController.Play(speed);
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,19 @@
Copyright (c) 2010-2019 Matt Schoen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Просмотреть файл

@ -0,0 +1,211 @@
using UnityEngine;
/*
Copyright (c) 2010-2019 Matt Schoen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
public static partial class JSONTemplates {
/*
* Vector2
*/
public static Vector2 ToVector2(JSONObj obj) {
float x = obj["x"] ? obj["x"].f : 0;
float y = obj["y"] ? obj["y"].f : 0;
return new Vector2(x, y);
}
public static JSONObj FromVector2(Vector2 v) {
JSONObj vdata = JSONObj.obj;
if(v.x != 0) vdata.AddField("x", v.x);
if(v.y != 0) vdata.AddField("y", v.y);
return vdata;
}
/*
* Vector3
*/
public static JSONObj FromVector3(Vector3 v) {
JSONObj vdata = JSONObj.obj;
if(v.x != 0) vdata.AddField("x", v.x);
if(v.y != 0) vdata.AddField("y", v.y);
if(v.z != 0) vdata.AddField("z", v.z);
return vdata;
}
public static Vector3 ToVector3(JSONObj obj) {
float x = obj["x"] ? obj["x"].f : 0;
float y = obj["y"] ? obj["y"].f : 0;
float z = obj["z"] ? obj["z"].f : 0;
return new Vector3(x, y, z);
}
/*
* Vector4
*/
public static JSONObj FromVector4(Vector4 v) {
JSONObj vdata = JSONObj.obj;
if(v.x != 0) vdata.AddField("x", v.x);
if(v.y != 0) vdata.AddField("y", v.y);
if(v.z != 0) vdata.AddField("z", v.z);
if(v.w != 0) vdata.AddField("w", v.w);
return vdata;
}
public static Vector4 ToVector4(JSONObj obj) {
float x = obj["x"] ? obj["x"].f : 0;
float y = obj["y"] ? obj["y"].f : 0;
float z = obj["z"] ? obj["z"].f : 0;
float w = obj["w"] ? obj["w"].f : 0;
return new Vector4(x, y, z, w);
}
/*
* Matrix4x4
*/
public static JSONObj FromMatrix4x4(Matrix4x4 m) {
JSONObj mdata = JSONObj.obj;
if(m.m00 != 0) mdata.AddField("m00", m.m00);
if(m.m01 != 0) mdata.AddField("m01", m.m01);
if(m.m02 != 0) mdata.AddField("m02", m.m02);
if(m.m03 != 0) mdata.AddField("m03", m.m03);
if(m.m10 != 0) mdata.AddField("m10", m.m10);
if(m.m11 != 0) mdata.AddField("m11", m.m11);
if(m.m12 != 0) mdata.AddField("m12", m.m12);
if(m.m13 != 0) mdata.AddField("m13", m.m13);
if(m.m20 != 0) mdata.AddField("m20", m.m20);
if(m.m21 != 0) mdata.AddField("m21", m.m21);
if(m.m22 != 0) mdata.AddField("m22", m.m22);
if(m.m23 != 0) mdata.AddField("m23", m.m23);
if(m.m30 != 0) mdata.AddField("m30", m.m30);
if(m.m31 != 0) mdata.AddField("m31", m.m31);
if(m.m32 != 0) mdata.AddField("m32", m.m32);
if(m.m33 != 0) mdata.AddField("m33", m.m33);
return mdata;
}
public static Matrix4x4 ToMatrix4x4(JSONObj obj) {
Matrix4x4 result = new Matrix4x4();
if(obj["m00"]) result.m00 = obj["m00"].f;
if(obj["m01"]) result.m01 = obj["m01"].f;
if(obj["m02"]) result.m02 = obj["m02"].f;
if(obj["m03"]) result.m03 = obj["m03"].f;
if(obj["m10"]) result.m10 = obj["m10"].f;
if(obj["m11"]) result.m11 = obj["m11"].f;
if(obj["m12"]) result.m12 = obj["m12"].f;
if(obj["m13"]) result.m13 = obj["m13"].f;
if(obj["m20"]) result.m20 = obj["m20"].f;
if(obj["m21"]) result.m21 = obj["m21"].f;
if(obj["m22"]) result.m22 = obj["m22"].f;
if(obj["m23"]) result.m23 = obj["m23"].f;
if(obj["m30"]) result.m30 = obj["m30"].f;
if(obj["m31"]) result.m31 = obj["m31"].f;
if(obj["m32"]) result.m32 = obj["m32"].f;
if(obj["m33"]) result.m33 = obj["m33"].f;
return result;
}
/*
* Quaternion
*/
public static JSONObj FromQuaternion(Quaternion q) {
JSONObj qdata = JSONObj.obj;
if(q.w != 0) qdata.AddField("w", q.w);
if(q.x != 0) qdata.AddField("x", q.x);
if(q.y != 0) qdata.AddField("y", q.y);
if(q.z != 0) qdata.AddField("z", q.z);
return qdata;
}
public static Quaternion ToQuaternion(JSONObj obj) {
float x = obj["x"] ? obj["x"].f : 0;
float y = obj["y"] ? obj["y"].f : 0;
float z = obj["z"] ? obj["z"].f : 0;
float w = obj["w"] ? obj["w"].f : 0;
return new Quaternion(x, y, z, w);
}
/*
* Color
*/
public static JSONObj FromColor(Color c) {
JSONObj cdata = JSONObj.obj;
if(c.r != 0) cdata.AddField("r", c.r);
if(c.g != 0) cdata.AddField("g", c.g);
if(c.b != 0) cdata.AddField("b", c.b);
if(c.a != 0) cdata.AddField("a", c.a);
return cdata;
}
public static Color ToColor(JSONObj obj) {
Color c = new Color();
for(int i = 0; i < obj.Count; i++) {
switch(obj.keys[i]) {
case "r": c.r = obj[i].f; break;
case "g": c.g = obj[i].f; break;
case "b": c.b = obj[i].f; break;
case "a": c.a = obj[i].f; break;
}
}
return c;
}
/*
* Layer Mask
*/
public static JSONObj FromLayerMask(LayerMask l) {
JSONObj result = JSONObj.obj;
result.AddField("value", l.value);
return result;
}
public static LayerMask ToLayerMask(JSONObj obj) {
LayerMask l = new LayerMask {value = (int)obj["value"].n};
return l;
}
public static JSONObj FromRect(Rect r) {
JSONObj result = JSONObj.obj;
if(r.x != 0) result.AddField("x", r.x);
if(r.y != 0) result.AddField("y", r.y);
if(r.height != 0) result.AddField("height", r.height);
if(r.width != 0) result.AddField("width", r.width);
return result;
}
public static Rect ToRect(JSONObj obj) {
Rect r = new Rect();
for(int i = 0; i < obj.Count; i++) {
switch(obj.keys[i]) {
case "x": r.x = obj[i].f; break;
case "y": r.y = obj[i].f; break;
case "height": r.height = obj[i].f; break;
case "width": r.width = obj[i].f; break;
}
}
return r;
}
public static JSONObj FromRectOffset(RectOffset r) {
JSONObj result = JSONObj.obj;
if(r.bottom != 0) result.AddField("bottom", r.bottom);
if(r.left != 0) result.AddField("left", r.left);
if(r.right != 0) result.AddField("right", r.right);
if(r.top != 0) result.AddField("top", r.top);
return result;
}
public static RectOffset ToRectOffset(JSONObj obj) {
RectOffset r = new RectOffset();
for(int i = 0; i < obj.Count; i++) {
switch(obj.keys[i]) {
case "bottom": r.bottom = (int)obj[i].n; break;
case "left": r.left = (int)obj[i].n; break;
case "right": r.right = (int)obj[i].n; break;
case "top": r.top = (int)obj[i].n; break;
}
}
return r;
}
}

Просмотреть файл

@ -0,0 +1,300 @@
//--------------------------------------------------------------------------------------
// QueryEvent.cs
//
// Event structure returned from queries
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameTelemetry
{
public static class QueryIds
{
public const string PlayerPos = "pos";
public const string PlayerDir = "dir";
public const string CamPos = "cam_pos";
public const string CamDir = "cam_dir";
public const string Id = "id";
public const string ClientId = "client_id";
public const string SessionId = "session_id";
public const string Sequence = "seq";
public const string EventName = "name";
public const string EventAction = "action";
public const string ClientTimestamp = "client_ts";
public const string EventVersion = "e_ver";
public const string BuildType = "build_type";
public const string BuildId = "build_id";
public const string ProcessId = "process_id";
public const string Platform = "platform";
public const string UserId = "user_id";
public const string Category = "cat";
}
//Wrapper for each event
public class QueryEvent
{
Dictionary<string, JSONObj> Attributes = new Dictionary<string, JSONObj>();
public QueryEvent() { }
public QueryEvent(JSONObj jObj)
{
Debug.Assert(jObj.IsObject);
for (int i = 0; i < jObj.keys.Count; i++)
{
this.Attributes.Add(jObj.keys[i], jObj.list[i]);
}
}
public string Id
{
get
{
return GetString(QueryIds.Id);
}
}
public string ClientId
{
get
{
return GetString(QueryIds.ClientId);
}
}
public string SessionId
{
get
{
return GetString(QueryIds.SessionId);
}
}
public string Name
{
get
{
return GetString(QueryIds.EventName);
}
}
public string BuildType
{
get
{
return GetString(QueryIds.BuildType);
}
}
public string BuildId
{
get
{
return GetString(QueryIds.BuildId);
}
}
public string Platform
{
get
{
return GetString(QueryIds.Platform);
}
}
public string UserId
{
get
{
return GetString(QueryIds.UserId);
}
}
public string Category
{
get
{
return GetString(QueryIds.Category);
}
}
UInt32 Sequence
{
get
{
return (UInt32)GetNumber(QueryIds.Sequence);
}
}
public DateTime Time
{
get
{
return DateTime.Parse(GetString(QueryIds.ClientTimestamp));
}
}
public Vector3 PlayerPosition
{
get
{
return GetVector(QueryIds.PlayerPos);
}
}
public Vector3 PlayerDirection
{
get
{
return GetVector(QueryIds.PlayerDir);
}
}
public Vector3 CameraPosition
{
get
{
return GetVector(QueryIds.CamPos);
}
}
public Vector3 CameraDirection
{
get
{
return GetVector(QueryIds.CamDir);
}
}
public QueryEvent Parse(JSONObj jObj)
{
Debug.Assert(jObj.IsObject);
QueryEvent ev = new QueryEvent();
for (int i = 0; i < jObj.keys.Count; i++)
{
ev.Attributes.Add(jObj.keys[i], jObj.list[i]);
}
return ev;
}
public bool TryGetString(string name, out string outString)
{
if(Attributes.ContainsKey(name))
{
JSONObj Value = Attributes[name];
if (Value != null && Value.IsString)
{
string newString = Value.ToString();
outString = newString.Substring(1, newString.Length - 2);
return true;
}
}
outString = "";
return false;
}
public string GetString(string name)
{
string value;
TryGetString(name, out value);
return value;
}
public bool TryGetNumber(string name, out double number)
{
if (Attributes.ContainsKey(name))
{
JSONObj Value = Attributes[name];
if (Value != null && Value.IsNumber)
{
number = Value.n;
return true;
}
}
number = 0;
return false;
}
public double GetNumber(string name)
{
double Value;
TryGetNumber(name, out Value);
return Value;
}
public bool TryGetVector(string baseName, out Vector3 vector)
{
vector = new Vector3();
if (Attributes.ContainsKey(baseName))
{
JSONObj Value = Attributes[baseName];
if (Value != null && Value.IsObject)
{
for(int i = 0; i < Value.list.Count; i++)
{
if(Value.list[i].IsNumber)
{
if (Value.keys[i] == "x")
{
vector.x = Value.list[i].f;
}
else if (Value.keys[i] == "y")
{
vector.y = Value.list[i].f;
}
else if (Value.keys[i] == "z")
{
vector.z = Value.list[i].f;
}
}
}
return true;
}
}
return false;
}
public Vector3 GetVector(string baseName)
{
Vector3 vector;
TryGetVector(baseName, out vector);
return vector;
}
public bool TryGetBool(string Name, out bool outBool)
{
if(Attributes.ContainsKey(Name))
{
JSONObj Value = Attributes[Name];
if (Value != null && Value.IsBool)
{
outBool = Value.b;
return true;
}
}
outBool = false;
return false;
}
public bool GetBool(string Name)
{
bool value;
TryGetBool(Name, out value);
return value;
}
}
}

Просмотреть файл

@ -0,0 +1,154 @@
//--------------------------------------------------------------------------------------
// Telemetry.cs
//
// Telemetry definition common functions.
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
namespace GameTelemetry
{
//Common usage library for telemetry
public static class Telemetry
{
// Format a semantic version string given the provided values
public static string FormatVersion(int major, int minor, int patch)
{
return string.Format("{0}.{1}.{2}", major, minor, patch);
}
// Creates an empty telemetry builder
public static TelemetryBuilder Create() { return new TelemetryBuilder(); }
// Creates a telemetry builder based on provided properties
public static TelemetryBuilder Create(TelemetryProperties properties)
{
return new TelemetryBuilder(properties);
}
//Initializes the telemetry manager. Required before recording events
public static void Initialize(GameObject gameObject)
{
TelemetryManager.Instance.Initialize(gameObject);
}
// Records an event and places it in the buffer to be sent
public static void Record(string name, string category, string version, TelemetryBuilder properties)
{
TelemetryManager.Instance.Record(name, category, version, properties);
}
// Records an event and places it in the buffer to be sent
public static void Record(string name, string category, string version, TelemetryProperties properties)
{
TelemetryManager.Instance.Record(name, category, version, new TelemetryBuilder(properties));
}
// Helper functions for consistently formatting special properties
// name of the event
public static TelemetryProperty EventName(string name)
{
return new TelemetryProperty("name", name);
}
// Category for an event - Useful for grouping events of a similary type
public static TelemetryProperty Category(string value)
{
return new TelemetryProperty("cat", value);
}
// Position vector for an entity
public static TelemetryProperty Position(Vector3 value)
{
return new TelemetryProperty("pos", value);
}
// Orientation unit vector for an entity
public static TelemetryProperty Orientation(Vector3 vec)
{
return new TelemetryProperty("dir", vec);
}
// The name of the value the visualizer will use
public static TelemetryProperty DisplayValue(string value)
{
return new TelemetryProperty("disp_val", value);
}
// Timestamp of the event using the client's clock by default
public static TelemetryProperty ClientTimestamp() {
return new TelemetryProperty("client_ts", System.DateTime.UtcNow);
}
public static TelemetryProperty ClientTimestamp(System.DateTime value)
{
return new TelemetryProperty("client_ts", value);
}
// Unique client id for the device playing the game
// Typically set in the Common properties
public static TelemetryProperty ClientId(string value)
{
return new TelemetryProperty("ClientId", value);
}
// Unique user id for the user playing the game
// Typically set in the Common properties
public static TelemetryProperty UserId(string value)
{
return new TelemetryProperty("UserId", value);
}
// Unique session id for the current play session
public static TelemetryProperty SessionId(string value)
{
return new TelemetryProperty("SessionId", value);
}
// Semantic version of the telemetry event
// Use this to help data pipelines understand how to process the event after ingestion
public static TelemetryProperty Version(string value)
{
return new TelemetryProperty("tver", value);
}
// Semantic version of a component of the telemetry - Typically used by Telemetry Providers
// Use this to help data pipelines understand how to process the event after ingestion
public static TelemetryProperty Version(string subentity, string value)
{
return new TelemetryProperty("tver_" + subentity, value);
}
// A value that represents a percentage float between 0 and 100
public static TelemetryProperty Percentage(string subentity, float value)
{
return new TelemetryProperty("pct_" + subentity, Mathf.Clamp(value, 0, 100));
}
// A float value
public static TelemetryProperty Value(string subentity, float value)
{
return new TelemetryProperty("val_" + subentity, value);
}
// Generic telemetry property maker
public static TelemetryProperty Prop<T>(string name, T value)
{
return new TelemetryProperty(name, value);
}
// Debug utility function for outputting telemetry to a string for printing
public static string DumpJson(TelemetryProperties properties)
{
JSONObj jObject = new JSONObj(JSONObj.ObjectType.OBJECT);
foreach(var Prop in properties)
{
jObject.AddField(Prop.Key, Prop.Value);
}
return jObject.Print();
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
//--------------------------------------------------------------------------------------
// TelemetryBuilder.cs
//
// Useful wrapper for building telemetry events correctly
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
namespace GameTelemetry
{
public class TelemetryBuilder : TelemetryProperties
{
public TelemetryBuilder() : base() { }
public TelemetryBuilder(TelemetryProperties properties) : base(properties) { }
public void SetProperty<T>(string name, T value)
{
if (this.ContainsKey(name))
{
this[name] = value;
}
else
{
Add(name, value);
}
}
public void SetProperty(TelemetryProperty property)
{
if (this.ContainsKey(property.Item1))
{
this[property.Item1] = property.Item2;
}
else
{
Add(property.Item1, property.Item2);
}
}
public void SetProperties(TelemetryProperties otherProperties)
{
foreach(var prop in otherProperties)
{
if (this.ContainsKey(prop.Key))
{
this[prop.Key] = prop.Value;
}
else
{
Add(prop.Key, prop.Value);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,317 @@
//--------------------------------------------------------------------------------------
// TelemetryManager.cs
//
// System for submitting telemetry events
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
namespace GameTelemetry
{
public sealed class TelemetryManager
{
// Singleton instance
private static TelemetryManager instance = new TelemetryManager();
public static TelemetryManager Instance
{
get
{
return instance;
}
}
// Common properties which are sent with all events
private TelemetryBuilder commonProperties;
private TelemetryWorker worker;
private long seqNum = 0;
private bool hasInit = false;
private TelemetryManager()
{
this.commonProperties = new TelemetryBuilder();
}
// Singleton for common properties
// Intialization of the singleton fromm the provided configuration
public void Initialize(GameObject host)
{
if (hasInit)
{
instance.Shutdown();
}
else
{
//Build type
instance.commonProperties.SetProperty(QueryIds.BuildType, Application.buildGUID);
//Platform
instance.commonProperties.SetProperty(QueryIds.Platform, Application.platform.ToString());
//Client ID
instance.commonProperties.SetProperty(QueryIds.ClientId, SystemInfo.deviceUniqueIdentifier);
//Session ID
instance.commonProperties.SetProperty(QueryIds.SessionId, System.Guid.NewGuid().ToString());
//Build ID
instance.commonProperties.SetProperty(QueryIds.BuildId, Application.version);
//Process ID
instance.commonProperties.SetProperty(QueryIds.ProcessId, System.Diagnostics.Process.GetCurrentProcess().Id.ToString());
//User ID
instance.commonProperties.SetProperty(QueryIds.UserId, System.Environment.UserName);
}
worker = host.AddComponent<TelemetryWorker>();
worker.StartThread(TelemetrySettings.IngestUrl, TelemetrySettings.SendInterval, TelemetrySettings.TickInterval, TelemetrySettings.MaxBufferSize, TelemetrySettings.AuthenticationKey);
hasInit = true;
}
// Set a new client id - Usually set to the platform client id
// If setting this again after initialization, consider flushing existing buffered telemetry or they may be incorrectly associated with the new id
public void SetClientId(string inClientId)
{
instance.commonProperties.SetProperty(Telemetry.ClientId(inClientId));
}
// Set a new session id - Usually set when the game would like to differentiate between play sessions (such as changing users)
// If setting this more than once, consider flushing existing buffered telemetry or they may be incorrectly associated with the new id
public void SetSessionId(string inSessionId)
{
instance.commonProperties.SetProperty(Telemetry.SessionId(inSessionId));
}
// Set a common property which will be included in all telemetry sent
public void SetCommonProperty<T>(string name, T value)
{
instance.commonProperties.SetProperty(name, value);
}
// Set a common property which will be included in all telemetry sent
public void SetCommonProperty(TelemetryProperty property)
{
instance.commonProperties.SetProperty(property);
}
// Set a common property which will be included in all telemetry sent
public TelemetryProperties GetCommonProperties()
{
return instance.commonProperties;
}
// Get the currently set client id
public static string GetClientId()
{
return instance.commonProperties[QueryIds.ClientId].ToString();
}
// Get the currently set session id
public static string GetSessionId()
{
return instance.commonProperties[QueryIds.SessionId].ToString();
}
// Records an event and places it in the buffer to be sent
public void Record(string name, string category, string version, TelemetryBuilder propertiesBuilder)
{
if (hasInit)
{
TelemetryBuilder Evt = propertiesBuilder;
Evt.SetProperty(Telemetry.ClientTimestamp());
Evt.SetProperty(Telemetry.EventName(name));
Evt.SetProperty(Telemetry.Category(category));
Evt.SetProperty(Telemetry.Version(version));
Evt.SetProperty("seq", System.Threading.Interlocked.Increment(ref seqNum));
worker.Enqueue(Evt);
}
else
{
Debug.Log("Cannot record event because the telemetry subsystem has not been initialized.");
}
}
// Flushes any pending telemetry and shuts down the singleton
public void Shutdown()
{
if (hasInit)
{
worker.Exit();
}
}
}
// Payload used by worker thread
public class TelemetryBatchPayload
{
public string Payload;
private JSONObj headerObject;
private System.Collections.Generic.List<JSONObj> eventList;
public TelemetryBatchPayload(TelemetryProperties common)
{
headerObject = new JSONObj(JSONObj.ObjectType.OBJECT);
eventList = new System.Collections.Generic.List<JSONObj>();
foreach (var Prop in common)
{
headerObject.AddField(Prop.Key, Prop.Value);
}
}
public void AddTelemetry(TelemetryBuilder inEvent)
{
JSONObj eventObject = new JSONObj(JSONObj.ObjectType.OBJECT);
foreach (var Prop in inEvent)
{
eventObject.AddField(Prop.Key, Prop.Value);
}
eventList.Add(eventObject);
}
public string Finalize()
{
JSONObj finalObject = new JSONObj(JSONObj.ObjectType.OBJECT);
JSONObj eventListObject = new JSONObj(JSONObj.ObjectType.OBJECT);
foreach(var telEvent in eventList)
{
eventListObject.Add(telEvent);
}
finalObject.AddField("header", headerObject);
finalObject.AddField("events", eventListObject);
Payload = finalObject.Print();
return Payload;
}
}
// Worker thread for sending telemetry
public class TelemetryWorker : MonoBehaviour
{
private float sendInterval;
private float tickInterval;
private float currentTime;
private int pendingBufferSize;
private bool shouldRun;
private bool isComplete;
private string ingestUrl;
private string authenticationKey;
private System.Collections.Generic.Queue<TelemetryBuilder> pending;
public TelemetryWorker()
{
this.shouldRun = false;
this.isComplete = false;
}
public void StartThread(string ingestUrl, float sendInterval = 30, float tickInterval = 1, int pendingBufferSize = 127, string authKey = "")
{
this.ingestUrl = ingestUrl;
this.sendInterval = sendInterval;
this.tickInterval = tickInterval;
this.authenticationKey = authKey;
this.shouldRun = true;
this.pending = new System.Collections.Generic.Queue<TelemetryBuilder>();
this.pendingBufferSize = pendingBufferSize;
currentTime = 0;
StartCoroutine(Execute());
}
IEnumerator Execute()
{
while (shouldRun)
{
while(currentTime < sendInterval)
{
if (pending.Count >= pendingBufferSize) break;
yield return new WaitForSeconds(tickInterval);
currentTime += tickInterval;
}
if (pending.Count > 0)
{
TelemetryProperties commonProperties = TelemetryManager.Instance.GetCommonProperties();
yield return StartCoroutine(SendTelemetry(commonProperties));
}
currentTime = 0;
}
}
public void Stop()
{
shouldRun = false;
isComplete = true;
}
public void Exit()
{
if (!isComplete)
{
Stop();
}
}
public void Enqueue(TelemetryBuilder Properties)
{
if(pending != null)
{
pending.Enqueue(Properties);
}
}
IEnumerator SendTelemetry(TelemetryProperties commonProperties)
{
TelemetryBatchPayload BatchPayload = new TelemetryBatchPayload(commonProperties);
while (pending.Count > 0)
{
BatchPayload.AddTelemetry(pending.Dequeue());
}
string payload = BatchPayload.Finalize();
using (UnityWebRequest wr = new UnityWebRequest())
{
var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
wr.url = ingestUrl;
wr.method = UnityWebRequest.kHttpVerbPOST;
UploadHandler uploader = new UploadHandlerRaw(bytes);
wr.uploadHandler = uploader;
if (authenticationKey.Length > 0)
{
wr.SetRequestHeader("x-functions-key", authenticationKey);
}
wr.SetRequestHeader("Content-Type", "application/json");
wr.SetRequestHeader("x-ms-payload-type", "batch");
wr.timeout = 30;
yield return wr.SendWebRequest();
if (wr.isNetworkError || wr.isHttpError)
{
Debug.Log(wr.error);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
//--------------------------------------------------------------------------------------
// TelemetryProperties.cs
//
// Telemetry common types
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
namespace GameTelemetry
{
// A single telemetry property
public class TelemetryProperty : System.Tuple<string, object>
{
public TelemetryProperty(string key, object value) : base (key, value) {}
}
// A collection of telemetry properties
public class TelemetryProperties : System.Collections.Generic.Dictionary<string, object>
{
public TelemetryProperties() : base() { }
public TelemetryProperties(TelemetryProperties properties) : base(properties) { }
}
}

Просмотреть файл

@ -0,0 +1,345 @@
//--------------------------------------------------------------------------------------
// TelemetryQuery.cs
//
// Provides query interface for event lookup
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using QueryNodeList = System.Collections.Generic.List<GameTelemetry.QueryNode>;
namespace GameTelemetry
{
//Delegate called with query results
public delegate void QueryResultHandler(QueryResult result);
//Query identifier (single comparison or container of nodes)
public enum QueryNodeType
{
Comparison = 0,
Group = 1
};
//Query operator
public enum QueryOp
{
Eq = 0,
Gt = 1,
Gte = 2,
Lt = 3,
Lte = 4,
Neq = 5,
In = 6,
Btwn = 7,
And = 8,
Or = 9,
};
//Nodes for query conditions and containers
public class QueryNode
{
public QueryNodeType Type;
public QueryOp Operator;
public string Column;
public QueryNodeList Children;
public JSONObj Value;
// Standard single value node
public QueryNode(QueryNodeType type, string column, QueryOp op, JSONObj value)
{
this.Type = type;
this.Column = column;
this.Operator = op;
this.Value = value;
this.Children = new QueryNodeList();
}
// Overload for BETWEEN query nodes
public QueryNode(QueryNodeType type, string column, QueryOp op, JSONObj value1, JSONObj value2)
{
// Between and In operators are the only ones that know to look at the values array
Debug.Assert(op == QueryOp.Btwn || op == QueryOp.In);
this.Type = type;
this.Column = column;
this.Operator = op;
this.Children = new QueryNodeList();
this.Value = JSONObj.obj;
Value.Add(value1);
Value.Add(value2);
}
// Overload for grouping nodes (AND, OR)
public QueryNode(QueryNodeType type, QueryOp op, QueryNodeList childNodes)
{
this.Type = type;
this.Operator = op;
this.Column = "";
this.Value = JSONObj.nullJO;
this.Children = childNodes;
}
}
//Helper for building query nodes
class QBuilder
{
// Nodes for comparing a column to a single value (e.g. equals, greater than, etc.)
private QueryNode CreateComparisonNode<T>(string column, QueryOp op, T value)
{
return new QueryNode(QueryNodeType.Comparison, column, op, JSONObj.Create(value));
}
// Nodes for comparing a column to multiple values (e.g. between)
private QueryNode CreateComparisonNode<T>(string column, QueryOp op, T value1, T value2)
{
return new QueryNode(QueryNodeType.Comparison, column, op, JSONObj.Create(value1), JSONObj.Create(value2));
}
// Nodes for grouping multiple nodes together with a common query operator (e.g. and, or)
private QueryNode CreateGroupNode(QueryOp op, QueryNodeList childNodes)
{
return new QueryNode(QueryNodeType.Group, op, childNodes);
}
public QueryNode Eq<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Eq, value);
}
public QueryNode Gt<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Gt, value);
}
public QueryNode Gte<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Gte, value);
}
public QueryNode Lt<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Lt, value);
}
public QueryNode Lte<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Lte, value);
}
public QueryNode Neq<T>(string column, T value)
{
return CreateComparisonNode(column, QueryOp.Neq, value);
}
public QueryNode In<T>(string column, List<T> values)
{
return CreateComparisonNode(column, QueryOp.In, values);
}
public QueryNode Btwn<T>(string column, T lowerBound, T upperBound)
{
return CreateComparisonNode(column, QueryOp.Btwn, lowerBound, upperBound);
}
public QueryNode And(QueryNodeList childNodes)
{
return CreateGroupNode(QueryOp.And, childNodes);
}
public QueryNode Or(QueryNodeList childNodes)
{
return CreateGroupNode(QueryOp.Or, childNodes);
}
}
//Node to JSON serializer
public static class QuerySerializer
{
public static string SerializeToString(QueryNode node)
{
return Serialize(node).ToString();
}
public static JSONObj Serialize(QueryNode node)
{
JSONObj masterObj = new JSONObj(JSONObj.ObjectType.OBJECT);
masterObj.AddField("type", node.Type.ToString().ToLower());
masterObj.AddField("op", node.Operator.ToString().ToLower());
switch (node.Type)
{
case QueryNodeType.Group:
JSONObj innerObj = new JSONObj(JSONObj.ObjectType.ARRAY);
foreach (QueryNode Item in node.Children)
{
innerObj.Add(Serialize(Item));
}
masterObj.AddField("children", innerObj);
break;
case QueryNodeType.Comparison:
masterObj.AddField("column", node.Column);
masterObj.AddField("value", node.Value);
break;
}
string test = masterObj.ToString();
return masterObj;
}
}
//Result data structure for query request
public class QueryResult
{
public struct QueryResultHeader
{
public bool Success;
public int Count;
public long QueryTime;
}
public QueryResultHeader Header;
public List<QueryEvent> Events;
public QueryResult(string response)
{
Header = new QueryResultHeader();
Events = new List<QueryEvent>();
Parse(response);
}
public void Parse(string response)
{
JSONObj rootObject = new JSONObj(response);
JSONObj headerObject = rootObject.GetField("header");
JSONObj resultsObject = rootObject.GetField("results");
if (headerObject != null)
{
headerObject.GetField(ref this.Header.Success, "success");
headerObject.GetField(ref this.Header.Count, "count");
headerObject.GetField(ref this.Header.QueryTime, "queryTime");
}
if (resultsObject != null)
{
foreach (JSONObj Event in resultsObject.list)
{
this.Events.Add(new QueryEvent(Event));
}
}
}
}
//Query execution
public class QueryExecutor : MonoBehaviour
{
private int ConfiguredtakeLimit;
public QueryExecutor()
{
ConfiguredtakeLimit = TelemetrySettings.QueryTakeLimit;
}
public void ExecuteCustomQuery(string queryText, QueryResultHandler handlerFunc)
{
ExecuteCustomQuery(queryText, handlerFunc, -1);
}
public void ExecuteCustomQuery(string queryText, QueryResultHandler handlerFunc, int takeLimit)
{
StartCoroutine(RunCustomQuery(queryText, handlerFunc, takeLimit));
}
System.Collections.IEnumerator RunCustomQuery(string queryText, QueryResultHandler handlerFunc, int takeLimit)
{
if (takeLimit < 0)
{
takeLimit = ConfiguredtakeLimit;
}
using (UnityWebRequest Request = CreateRequest(handlerFunc, takeLimit))
{
Request.method = UnityWebRequest.kHttpVerbPOST;
var bytes = System.Text.Encoding.UTF8.GetBytes(queryText);
Request.uploadHandler = new UploadHandlerRaw(bytes);
Request.downloadHandler = new DownloadHandlerBuffer();
yield return Request.SendWebRequest();
if (Request.isNetworkError || Request.isHttpError)
{
Debug.Log(Request.error);
}
else if(handlerFunc != null)
{
QueryResult Result = new QueryResult(Request.downloadHandler.text);
handlerFunc(Result);
}
}
}
public void ExecuteCustomQuery(QueryResultHandler handlerFunc)
{
ExecuteDefaultQuery(handlerFunc, -1);
}
public void ExecuteDefaultQuery(QueryResultHandler handlerFunc, int takeLimit)
{
StartCoroutine(RunDefaultQuery(handlerFunc, takeLimit));
}
System.Collections.IEnumerator RunDefaultQuery(QueryResultHandler handlerFunc, int takeLimit)
{
if (takeLimit < 0)
{
takeLimit = ConfiguredtakeLimit;
}
using (UnityWebRequest request = CreateRequest(handlerFunc, takeLimit))
{
request.method = UnityWebRequest.kHttpVerbGET;
request.downloadHandler = new DownloadHandlerBuffer();
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.Log(request.error);
}
else if (handlerFunc != null)
{
QueryResult Result = new QueryResult(request.downloadHandler.text);
handlerFunc(Result);
}
}
}
private UnityWebRequest CreateRequest(QueryResultHandler handlerFunc, int takeLimit)
{
UnityWebRequest wr = new UnityWebRequest();
wr.url = TelemetrySettings.QueryUrl + "?take=" + takeLimit;
if (TelemetrySettings.AuthenticationKey.Length > 0)
{
wr.SetRequestHeader("x-functions-key", TelemetrySettings.AuthenticationKey);
}
wr.SetRequestHeader("Content-Type", "application/json");
wr.SetRequestHeader("x-ms-payload-type", "batch");
wr.SetRequestHeader("User-Agent", "X-UnityEngine-Agent");
wr.timeout = 30;
return wr;
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
//--------------------------------------------------------------------------------------
// TelemetrySettings.cs
//
// Telemetry settings
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------
namespace GameTelemetry
{
public static class TelemetrySettings
{
public static string QueryUrl = "<PLACE QUERY URL HERE>";
public static string IngestUrl = "<PLACE INGEST URL HERE>";
public static float SendInterval = 30;
public static float TickInterval = 1;
public static int MaxBufferSize = 128;
public static int QueryTakeLimit = 10000;
public static string AuthenticationKey = "<PLACE KEY HERE>";
}
}

48
README.md Normal file
Просмотреть файл

@ -0,0 +1,48 @@
# Unity Telemetry Visualizer
![View Telemetry](docs/images/unity/points.png)
This is a plugin for adding telemetry to your Unity game and visualizing that telemetry in the Unity editor. It is featured in the [Azure Gaming Reference Architectures](https://docs.microsoft.com/en-us/gaming/azure/reference-architectures/analytics-in-editor-debugging). There is also an [Unreal Engine 4 version of this plugin](https://github.com/Microsoft/UE4TelemetryVisualizer).
## Summary
Game telemetry itself is nothing new, but the number of "out of the box" solutions for games to leverage while ___in development___ are next to none. Historically, developers would use log files to debug issues, which worked fine for local debugging. Today's games more closely resemble distributed systems, interacting with other clients, servers, and various cloud services regularly.
This project was inspired by developers needing to answer questions for which their own development systems could not provide all the answers, and who were tired of asking people to email them log files!
## Goals
* Make it as simple as possible for __anyone__ on a development team to add and view telemetry - No dedicated engineers required!
* Iterative development model - You won't know what you'll need up front, so make it as easy as possible to add things along the way!
* Low cost of operation
## Features
* Telemetry generation API and asynchronous upload with configurable buffers and send intervals,
* Optimized for a large volume of telemetry (10's to 100's per second) from a small number of clients (10-30)
* Batching and compression out of the box, common fields denormalized server side to reduce bandwidth requirements
* Simple UI to build queries in editor, wraps a JSON defined query language usable by external tools
* Customizable colors and shapes, as well as heatmaps
## Example use cases for telemetry during development
### Programmers
* Code Correctness: Asserts, non-critical errors, unlikely code paths taken
* Profiling: Tracking system counters over thresholds (RAM, CPU, frame render time)
### Artists and Game Designers
* Game asset tracking (missing assets, over-budget assets, hitches)
* Playtesting analytics in realtime (focus groups, friends and family alpha builds)
* Game mechanics validation (AI, balancing)
### Testing and Quality
* Testing test coverage per build
* Bug correlation, reproduction, regression
## Setup Instructions
This plugin requires a server component to send telemetry to, which can be found here: https://github.com/Azure-Samples/gaming-in-editor-telemetry.
Plugin setup instructions can be found [here](docs/Unity_Instructions.md).
If you find issues, please add them to the [issue tracker](https://github.com/Microsoft/UnityTelemetryVisualizer/issues).
## Contributing
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct)
We welcome contributions to this project. This project Please read the [contributing](/CONTRIBUTING.md) information before submitting a pull request. If you need inspiration, please check the [issue tracker](https://github.com/Microsoft/UnityTelemetryVisualizer/issues). We are using the [GitHub Flow](https://guides.github.com/introduction/flow/) development model.

151
docs/Unity_Instructions.md Normal file
Просмотреть файл

@ -0,0 +1,151 @@
# Adding Game Telemetry to your project
## Setup
1. Create a directory called **GameTelemetryPlugins** in your project's **\Assets\Scripts** directory and copy the **Game Telemetry** folder into that directory.
-- or --
1. Open the GameTelemetry Unity package
2. Open **TelemetrySettings.cs** and set your injest URL, query URL, and auth key
Your project is now ready for game telemetry.
---
## Adding events
1. You can access the recording interface by using the **GameTelemetry** namespace
2. To start, initialize the system with a game object that will be consistantly in the game, such as a player.
```cs
using GameTelemetry;
Telemetry.Initialize(myGameObject);
```
2. While any event attribute can be overwritten, the following are populated automatically in the plugin based on information available to the engine:
+ Build Type
+ Platform
+ Client ID
+ Client Timestamp
+ Session ID
+ Build ID
+ Process ID
+ User ID
+ Sequence ID
3. Use TelemetryBuilder to assign properties to your event
Example:
```cs
TelemetryBuilder Builder = new TelemetryBuilder();
Builder.SetProperty(Telemetry.Position(settings.position));
Builder.SetProperty(Telemetry.Orientation(settings.rotation.eulerAngles));
```
4. Record the event, providing the required name, category, event version, and your properties
Example:
```cs
Telemetry.Record(”Health”, ”Gameplay”, ”1.3”, Builder);
```
With that, your event will be sent with the next batch send (set by SendInterval during setup)
---
## Making your events visualizer friendly
While you can record any event you want, the ability to view it using the GameTelemetry plugin requires a couple of settings:
1. The position of the event is required in order to know where the event can be drawn. Orientation is also supported
Example:
```cs
Telemetry.Position(settings.position);
Telemetry.Orientation(settings.rotation.eulerAngles);
```
2. If your event has related data that can be used by the heatmap generator (or that you would like attached to each event drawn), a “disp_val” property is needed to direct what event value is needed:
Example:
```cs
Telemetry.DisplayValue(”val_health”);
```
3. The value that “disp_val” uses will also need to be populated
+ For percentages, either prefix your property with “pct_” or use the construction shortcut Percentage. This will let the visualizer know that the value is a float between 0 and 100.
+ For all other values, prefix your property with “val_” or use the construction shortcut Value.
Example:
```cs
Telemetry.Percentage(”health”, MyHealth)
```
Example:
```cs
Telemetry.Value(”health”, MyHealth)
```
A final, visualizer friendly event might look something like this:
```cs
TelemetryBuilder Builder = new TelemetryBuilder();
Builder.SetProperty(Telemetry.Position(settings.position);
Builder.SetProperty(Telemetry.Orientation(settings.rotation.eulerAngles);
Builder.SetProperty(Telemetry.Value("health", MyHealth));
Builder.SetProperty(Telemetry.DisplayValue(”val_health”));
Telemetry.Record(”Health”, ”Gameplay”, ”1.3”, Builder);
```
---
## Using the visualizer
Once you have data uploaded, you are ready to start visualizing it!
1. In the Unity editor, open your project.
2. Open the **Window** menu and select **Game Telemetry**
![alt text](\images\unity\window_menu.png)
3. A new window will open. Note that these can be docked anywhere in the editor or combined within windows.
### Game Telemetry Window
4. First, we will use the **Event Settings** sections to get our first dataset. Build a query for what general events you would like to recieve. Once you are ready, press **Submit** and wait for the results. If the query found events, you will see them populate in the **Event Search** section.
![alt text](\images\unity\data_viewer.png)
5. By default, all data received by the query will be enabled. In the **Event Search** section, you can uncheck any event groups you do not wish to see and use the search bar above to look for different event names. In addition, each event group has a changeable color and shape for how each event is drawn.
6. You should now have events being drawn directly in your game world. Use the different shapes and colors to customize your view to see the most relivant information. Also note that using shapes such as the capsule will provide orientation detail as well.
![alt text](\images\unity\points.png)
7. All of the events are actually interactive elements in the game. They can be clicked to see further details, zoomed, saved, and more. In the **Hierarchy** window, just look for game objects under the **GameTelemetry** section.
![alt text](\images\unity\world_outliner.png)
8. Now we will use the **Visualization Tools** section to get unique views of our data. Expand the drop down to see a list of event types, the same from the *Event Search* section above. Select one of those event groups.
9. You will notice that underneith, a time will now be at the end of our animation bar. This is the elapsed time of the selected data selected. Simply press any of the play/rewind buttons or drag the bar to any location in the timeline to see how your data has changed over time. Points will appear or disappear as if they were happening in realtime.
Note: If there is a gap in time greater than 30 seconds, the animation will skip ahead to keep the visualization more fluid
10. Now lets look at the *Heatmap Settings* area. Here, you can select a variety of options to combine your event data in to a 3D map and even watch that collection of data animate over time.
![alt text](\images\unity\heatmap.png)
11. Type represents the type of heatmap you would like to generate.
+ *Population* combines events into physical groups and displays a heatmap of the number of each event within a given group.
+ *Population - Bar* does the same, but generates a 3D bar graph over the XY plane.
+ *Value* also combines events into physical groups, but displays a heatmap of the average of the value of each event within a given group.
+ *Value - Bar* provides a 3D bar graph of value information.
12. Shape and shape size represent the shape of the heatmap elements along with their radius. The smaller the radius, the more detailed the information (though also the more complex the heatmap is to generate).
13. The color range provides the color for the minimum and maximum values in the map. Colors in between are a fade between the two colors chosen.
14. Type range will populate when you generate the heatmap, but can be used to adjust the colors. This can be helpful when your data has outlying values that can cause the heatmap to lack variability. Adjust the range to remove extreme highs and lows to get more interesting information.
15. Use Orientation will rotate the heatmap shapes to the average orientation of all events within each element.
16. Apply to Animation will allow the animation controls above control animating the entire heatmap
---
## Troubleshooting / FAQ
+ I am getting an error that System.Tuple is undefined
+ This issue can occur if you are attempting to build using a version of the .NET framework prior to 4.0. Under File->Build Settings->PlayerSettings->OtherSettings->Configuration, change the target framework to use 4.0 or later.

Двоичные данные
docs/images/unity/data_viewer.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 42 KiB

Двоичные данные
docs/images/unity/heatmap.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 514 KiB

Двоичные данные
docs/images/unity/points.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 505 KiB

Двоичные данные
docs/images/unity/window_menu.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 19 KiB

Двоичные данные
docs/images/unity/world_outliner.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 24 KiB