Skip to content

Commit 02a9dc0

Browse files
author
Miguel Cartier
committed
feat: add analytics system and editor tools for UI service
- Add IUiAnalytics interface with performance tracking (load/open/close durations, counts, lifetime) - Add UiAnalytics implementation with custom callback support and UnityEvents - Integrate analytics tracking throughout UiService lifecycle operations - Add editor windows: Analytics, Layer Visualizer, Hierarchy, and UiPresenter inspector - Add AssemblyInfo for internal API access to editor tools - Update README with analytics documentation and editor tools guide
1 parent e9d8719 commit 02a9dc0

16 files changed

+1732
-342
lines changed

Editor/UiAnalyticsWindow.cs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
using System.Linq;
2+
using UnityEditor;
3+
using UnityEngine;
4+
using GameLovers.UiService;
5+
6+
// ReSharper disable once CheckNamespace
7+
8+
namespace GameLoversEditor.UiService
9+
{
10+
/// <summary>
11+
/// Editor window for viewing UI analytics and performance metrics.
12+
/// This window uses UiService.CurrentAnalytics (internal) to access the analytics instance
13+
/// from the currently active UiService in play mode.
14+
/// Note: CurrentAnalytics is internal and only accessible to editor code within this package.
15+
/// </summary>
16+
public class UiAnalyticsWindow : EditorWindow
17+
{
18+
private Vector2 _scrollPosition;
19+
private bool _autoRefresh = true;
20+
private double _lastRefreshTime;
21+
private const double RefreshInterval = 1.0; // seconds
22+
23+
[MenuItem("Tools/UI Service/Analytics")]
24+
public static void ShowWindow()
25+
{
26+
var window = GetWindow<UiAnalyticsWindow>("UI Analytics");
27+
window.minSize = new Vector2(500, 300);
28+
window.Show();
29+
}
30+
31+
// ReSharper disable once UnusedMember.Local
32+
private void OnEnable()
33+
{
34+
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
35+
}
36+
37+
// ReSharper disable once UnusedMember.Local
38+
private void OnDisable()
39+
{
40+
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
41+
}
42+
43+
private void OnPlayModeStateChanged(PlayModeStateChange state)
44+
{
45+
Repaint();
46+
}
47+
48+
// ReSharper disable once UnusedMember.Local
49+
private void OnGUI()
50+
{
51+
DrawHeader();
52+
53+
if (!Application.isPlaying)
54+
{
55+
DrawNotPlayingMessage();
56+
return;
57+
}
58+
59+
if (_autoRefresh && EditorApplication.timeSinceStartup - _lastRefreshTime > RefreshInterval)
60+
{
61+
_lastRefreshTime = EditorApplication.timeSinceStartup;
62+
Repaint();
63+
}
64+
65+
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
66+
67+
DrawAnalyticsData();
68+
69+
EditorGUILayout.EndScrollView();
70+
71+
DrawFooter();
72+
}
73+
74+
private void DrawHeader()
75+
{
76+
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
77+
EditorGUILayout.LabelField("UI Service Analytics", EditorStyles.boldLabel);
78+
79+
GUILayout.FlexibleSpace();
80+
81+
_autoRefresh = GUILayout.Toggle(_autoRefresh, "Auto Refresh", EditorStyles.toolbarButton, GUILayout.Width(100));
82+
83+
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
84+
{
85+
Repaint();
86+
}
87+
88+
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(50)))
89+
{
90+
GetCurrentAnalytics()?.Clear();
91+
}
92+
93+
EditorGUILayout.EndHorizontal();
94+
EditorGUILayout.Space(5);
95+
}
96+
97+
private void DrawNotPlayingMessage()
98+
{
99+
EditorGUILayout.HelpBox(
100+
"UI Analytics is only available in Play Mode.\n\n" +
101+
"Enter Play Mode to see performance metrics and event tracking.",
102+
MessageType.Info);
103+
}
104+
105+
private void DrawAnalyticsData()
106+
{
107+
var analytics = GetCurrentAnalytics();
108+
109+
if (analytics == null)
110+
{
111+
EditorGUILayout.HelpBox("No UiService instance found. Create a UiService to enable analytics tracking.", MessageType.Warning);
112+
return;
113+
}
114+
115+
var metrics = analytics.PerformanceMetrics;
116+
117+
if (metrics.Count == 0)
118+
{
119+
EditorGUILayout.HelpBox("No analytics data collected yet. Use the UI Service to generate data.", MessageType.Info);
120+
return;
121+
}
122+
123+
EditorGUILayout.LabelField($"Tracked UIs: {metrics.Count}", EditorStyles.boldLabel);
124+
EditorGUILayout.Space(5);
125+
126+
// Sort by total lifetime
127+
var sortedMetrics = metrics.Values.OrderByDescending(m => m.TotalLifetime).ToList();
128+
129+
foreach (var metric in sortedMetrics)
130+
{
131+
DrawMetricCard(metric);
132+
}
133+
}
134+
135+
private void DrawMetricCard(UiPerformanceMetrics metric)
136+
{
137+
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
138+
139+
// Header
140+
EditorGUILayout.LabelField(metric.UiName, EditorStyles.boldLabel);
141+
EditorGUILayout.Space(5);
142+
143+
// Performance metrics
144+
EditorGUI.indentLevel++;
145+
146+
DrawMetricRow("Load Duration:", $"{metric.LoadDuration:F3}s", GetLoadColor(metric.LoadDuration));
147+
DrawMetricRow("Open Duration:", $"{metric.OpenDuration:F3}s", GetOpenColor(metric.OpenDuration));
148+
DrawMetricRow("Close Duration:", $"{metric.CloseDuration:F3}s", GetCloseColor(metric.CloseDuration));
149+
150+
EditorGUILayout.Space(5);
151+
152+
DrawMetricRow("Open Count:", metric.OpenCount.ToString(), Color.white);
153+
DrawMetricRow("Close Count:", metric.CloseCount.ToString(), Color.white);
154+
DrawMetricRow("Total Lifetime:", $"{metric.TotalLifetime:F1}s", Color.cyan);
155+
156+
if (metric.FirstOpened != System.DateTime.MinValue)
157+
{
158+
EditorGUILayout.Space(5);
159+
DrawMetricRow("First Opened:", metric.FirstOpened.ToString("HH:mm:ss"), Color.white);
160+
}
161+
162+
if (metric.LastClosed != System.DateTime.MinValue)
163+
{
164+
DrawMetricRow("Last Closed:", metric.LastClosed.ToString("HH:mm:ss"), Color.white);
165+
}
166+
167+
EditorGUI.indentLevel--;
168+
169+
EditorGUILayout.EndVertical();
170+
EditorGUILayout.Space(5);
171+
}
172+
173+
private void DrawMetricRow(string label, string value, Color color)
174+
{
175+
EditorGUILayout.BeginHorizontal();
176+
EditorGUILayout.LabelField(label, GUILayout.Width(150));
177+
178+
var originalColor = GUI.contentColor;
179+
GUI.contentColor = color;
180+
EditorGUILayout.LabelField(value, EditorStyles.boldLabel);
181+
GUI.contentColor = originalColor;
182+
183+
EditorGUILayout.EndHorizontal();
184+
}
185+
186+
private void DrawFooter()
187+
{
188+
EditorGUILayout.Space(5);
189+
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
190+
191+
var analytics = GetCurrentAnalytics();
192+
if (analytics != null)
193+
{
194+
var metrics = analytics.PerformanceMetrics;
195+
if (metrics.Count > 0)
196+
{
197+
var totalOpens = metrics.Values.Sum(m => m.OpenCount);
198+
var totalCloses = metrics.Values.Sum(m => m.CloseCount);
199+
EditorGUILayout.LabelField($"Total Opens: {totalOpens} | Total Closes: {totalCloses}");
200+
}
201+
}
202+
203+
GUILayout.FlexibleSpace();
204+
205+
if (GUILayout.Button("Log Summary", EditorStyles.toolbarButton))
206+
{
207+
GetCurrentAnalytics()?.LogPerformanceSummary();
208+
}
209+
210+
EditorGUILayout.EndHorizontal();
211+
}
212+
213+
/// <summary>
214+
/// Helper method to get the current analytics instance from the active UiService.
215+
/// Note: This accesses an internal property only available to editor code within this package.
216+
/// </summary>
217+
private IUiAnalytics GetCurrentAnalytics()
218+
{
219+
return GameLovers.UiService.UiService.CurrentAnalytics;
220+
}
221+
222+
private Color GetLoadColor(float duration)
223+
{
224+
if (duration < 0.1f) return Color.green;
225+
if (duration < 0.5f) return Color.yellow;
226+
return Color.red;
227+
}
228+
229+
private Color GetOpenColor(float duration)
230+
{
231+
if (duration < 0.05f) return Color.green;
232+
if (duration < 0.2f) return Color.yellow;
233+
return Color.red;
234+
}
235+
236+
private Color GetCloseColor(float duration)
237+
{
238+
if (duration < 0.05f) return Color.green;
239+
if (duration < 0.2f) return Color.yellow;
240+
return Color.red;
241+
}
242+
}
243+
}
244+

Editor/UiAnalyticsWindow.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/UiConfigsEditor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace GameLoversEditor.UiService
1818
public static class UiConfigsSelect
1919
{
2020

21-
[MenuItem("Tools/Select UiConfigs.asset")]
21+
[MenuItem("Tools/UI Service/Select UiConfigs")]
2222
private static void SelectUiConfigs()
2323
{
2424
var assets = AssetDatabase.FindAssets($"t:{nameof(UiConfigs)}");

0 commit comments

Comments
 (0)