using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Analytics;
namespace UnityEditor.Experimental.TerrainAPI
{
///
/// Analytics class for collecting and sending aggregated user data
///
public static class TerrainToolsAnalytics
{
//Event Data
static bool s_EventRegistered = false;
const int k_MaxEventsPerHour = 1000;
const int k_MaxNumberOfElements = 1000;
const string k_VendorKey = "unity.terraintools";
const string k_EventName = "analytics.uTerrainTools";
//Brush Analytics Data
const float k_SignficantThreshold = .01f;
static BrushAnalyticsData m_Data;
static List s_ModifiedBrushParameters = new List();
static List s_UsedBrushShortcut = new List();
static float s_PaintingDuration;
static bool s_ParameterChanged;
[Serializable]
struct BrushParameterData
{
public string name;
public string value;
}
struct BrushAnalyticsData
{
public string brush_name;
public List shortcuts;
public string[] mask_filters;
public float strength;
public float size;
public float rotation;
public float spacing;
public float scatter;
public float duration;
public List brush_parameters;
}
///
/// Array of BrushParameters used to compare against the most recent brush parameters to check if there
/// has been a change.
///
internal static IBrushParameter[] m_OriginalParameters;
///
/// Interface for iterating over brush parameters of an ambiguous type
///
public interface IBrushParameter
{
System.Type ParameterType();
string Name { get; set; }
}
///
/// Struct containing the name and value associated to an individual brush parameter.
///
/// The variable type of the brush parameter
internal struct BrushParameter : IBrushParameter
{
public System.Type ParameterType()
{
return Value.GetType();
}
public T Value { get; set; }
public string Name { get; set; }
}
///
/// Register the AnalyticsEvent for sending data to BigQuery
///
/// EventRegisterd boolean identifying if the event was registered correctly
static bool EnableAnalytics()
{
//Early out if the event has already been registered, returning bool determining
//if Editor Analytics are enabled
if (s_EventRegistered)
return EditorAnalytics.enabled;
AnalyticsResult result = EditorAnalytics.RegisterEventWithLimit(k_EventName, k_MaxEventsPerHour,
k_MaxNumberOfElements, k_VendorKey);
if (result == AnalyticsResult.Ok)
s_EventRegistered = true;
return s_EventRegistered && EditorAnalytics.enabled;
}
///
/// Update the analytics data to be sent when a user starts painting with new parameters/settings
/// The users time is being tracked while painting
/// Once the user changes any brush parameters original data is sent and the new data is cached to be compared later
///
/// Brush Base class containing common brush parameters
/// Function returning brush specific parameters
internal static void UpdateAnalytics(BaseBrushUIGroup baseBrushSettings, Func brushParamFunc)
{
if (!EnableAnalytics())
return;
s_PaintingDuration += Time.deltaTime;
if (!s_ParameterChanged)
return;
SendAnalytics();
CompareBrushSettings(brushParamFunc?.Invoke());
CacheAnalyticsData(baseBrushSettings);
}
///
/// Send the EditorAnalytics event and clear data for reuse
///
static void SendAnalytics()
{
if(m_Data.Equals(default(BrushAnalyticsData)))
return;
m_Data.duration = s_PaintingDuration;
EditorAnalytics.SendEventWithLimit(k_EventName, m_Data);
//Clear data
s_ModifiedBrushParameters.Clear();
s_UsedBrushShortcut.Clear();
s_ParameterChanged = false;
s_PaintingDuration = 0;
}
///
/// Checks whether the brush parameters have changed between
/// the original and current state of a brush.
///
/// Array of brushparameter structs which identify the name and value of the brushes
/// parameters.
static void CompareBrushSettings(IBrushParameter[] parameters)
{
if (parameters == null)
return;
for (int i = 0; (i < parameters.Length && i < m_OriginalParameters.Length); i++)
{
if (parameters[i].Equals(m_OriginalParameters[i]))
continue;
System.Type type = parameters[i].ParameterType();
TypeCode typecode = Type.GetTypeCode(type);
switch (typecode)
{
case TypeCode.Int32:
case TypeCode.Int64:
{
int currentValue = ((BrushParameter)parameters[i]).Value;
int originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
break;
}
case TypeCode.Single:
{
float currentValue = ((BrushParameter)parameters[i]).Value;
float originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
//Check if the user made a significant enough change to the parameter
if (CompareSignificance(currentValue, originalValue) == originalValue)
{
break;
}
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
break;
}
case TypeCode.Boolean:
{
bool currentValue = ((BrushParameter)parameters[i]).Value;
bool originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
break;
}
case TypeCode.String:
{
string currentValue = ((BrushParameter)parameters[i]).Value;
string originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
break;
}
case TypeCode.Object:
{
if (type == typeof(Vector3))
{
Vector3 currentValue = ((BrushParameter)parameters[i]).Value;
Vector3 originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
}
else if (type == typeof(Vector4))
{
Vector4 currentValue = ((BrushParameter)parameters[i]).Value;
Vector4 originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
CacheChangedParamter(parameters[i].Name, currentValue, originalValue);
}
else if (type == typeof(Keyframe[]))
{
Keyframe[] currentValue = ((BrushParameter)parameters[i]).Value;
Keyframe[] originalValue = ((BrushParameter)m_OriginalParameters[i]).Value;
for (int k = 0; k < currentValue.Length; k++)
{
//Check if there's a change between the original and current keyframe values
//Cache the change if there's a difference
if(!currentValue[k].Equals(originalValue[k]))
{
CacheChangedParamter($"{parameters[i].Name}", currentValue[k], originalValue[k]);
break;
}
}
}
break;
}
default:
Debug.LogWarning($"The parameter of type {type} isn't able to be tracked by Analytics");
break;
}
}
m_OriginalParameters = parameters;
}
static void CacheAnalyticsData(BaseBrushUIGroup brush)
{
m_Data.brush_name = brush.brushName;
m_Data.shortcuts = s_UsedBrushShortcut;
m_Data.mask_filters = brush.brushMaskFilterStack.filters?.
Where(x => x.enabled).
Select(x => x.GetType().Name).ToArray();
m_Data.strength = CompareSignificance(brush.brushStrength, m_Data.strength);
m_Data.size = CompareSignificance(brush.brushSize, m_Data.size);
m_Data.rotation = CompareSignificance(brush.brushRotation, m_Data.rotation);
m_Data.spacing = CompareSignificance(brush.brushSpacing, m_Data.spacing);
m_Data.scatter = CompareSignificance(brush.brushScatter, m_Data.scatter);
m_Data.brush_parameters = s_ModifiedBrushParameters;
}
#region Helper Methods
///
/// Caches the shortcutId on keyRelease to be sent as analytics data if the
/// shortcut key hasn't been cached already
///
/// ID of the shortcut
internal static void OnShortcutKeyRelease(string shortcutId)
{
if(!s_UsedBrushShortcut.Contains(shortcutId))
s_UsedBrushShortcut.Add(shortcutId);
}
///
/// Flags the parameter changed boolean notifying that the user has changed brush parameters
/// and the old data needs to be sent while the new data needs to be cached.
///
internal static void OnParameterChange() => s_ParameterChanged = true;
///
/// Cache the parameter that has been changed to be sent as analytics data
///
/// The name of the changed setting Ex: "Brush Strength"
/// Setting of the brush before the user starts painting
/// Setting of the brush which the user starts painting with
/// Returns a boolean indicating if the parameter was changed
static void CacheChangedParamter(string name, T currentSetting, T originalSetting)
{
s_ModifiedBrushParameters.Add(new BrushParameterData
{
name = name,
value = currentSetting.ToString()
});
}
///
/// Check whether the difference between the initial and current brush parameters
/// is significant enough to send the data for analyzing.
///
/// The latest brush parameter
/// The starting brush parameter
///
static float CompareSignificance(float currentValue, float originalValue)
{
//Determine if value A has significantly changed from value B
if (Mathf.Abs(currentValue - originalValue) >= k_SignficantThreshold)
return currentValue;
else
return originalValue;
}
#endregion
}
}