@uptimizr/unity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,259 @@
1
+ // UptimizrUnityBridge.cs — example engine-side bridge MonoBehaviour (copy-in asset).
2
+ //
3
+ // Part of the `@uptimizr/unity` connector (ADR 0045). This component is the **engine
4
+ // side** of the bridged capture tier: it samples Unity's own Camera pose, raycast
5
+ // picks, and frame timing each interval and pushes them across the WASM<->JS interop
6
+ // boundary (via the companion `Uptimizr.jslib` plugin) to the browser-side connector.
7
+ //
8
+ // It carries **no** analytics logic, IDs, or schema knowledge. Every world-space value
9
+ // is pushed in Unity's **native** frame (left-handed, y-up, meters); the connector —
10
+ // not this component — owns coordinate normalization and schema mapping (ADR 0018).
11
+ //
12
+ // Privacy (ADR 0003): only poses, FPS, and developer-named objects are sent. This
13
+ // component MUST NOT invent identifiers or forward raw input text. The pick channel
14
+ // sends only the hit GameObject's name (which you author) and the world hit point.
15
+ //
16
+ // Install:
17
+ // 1. Copy `Uptimizr.jslib` to `Assets/Plugins/WebGL/Uptimizr.jslib`.
18
+ // 2. Copy this file anywhere under `Assets/` and add the component to a GameObject.
19
+ // 3. On the host page, register the connector: `trackUnity({ ... })` (or
20
+ // `client.use(unityCollector())`) so `window.__uptimizr_unity__` exists.
21
+ //
22
+ // Uses the legacy `UnityEngine.Input` API for broad compatibility; adapt to the new
23
+ // Input System if your project has disabled the legacy one.
24
+ using System.Globalization;
25
+ using System.Runtime.InteropServices;
26
+ using System.Text;
27
+ using UnityEngine;
28
+
29
+ [DisallowMultipleComponent]
30
+ public class UptimizrUnityBridge : MonoBehaviour
31
+ {
32
+ // Must match BRIDGE_PROTOCOL_VERSION exported by `@uptimizr/web-export`.
33
+ const int ExpectedProtocolVersion = 1;
34
+
35
+ [Header("Camera pose")]
36
+ [Tooltip("Camera sampled for view-direction pose. Defaults to Camera.main.")]
37
+ public Camera targetCamera;
38
+
39
+ [Tooltip("Seconds between camera-pose samples.")]
40
+ public float poseSampleInterval = 0.25f;
41
+
42
+ [Header("Performance")]
43
+ [Tooltip("Seconds between performance (FPS / long-frame) reports.")]
44
+ public float perfReportInterval = 2f;
45
+
46
+ [Tooltip("Frame time (ms) above which a frame counts as a long / janky frame.")]
47
+ public float jankFrameMs = 50f;
48
+
49
+ [Header("Picks")]
50
+ [Tooltip("Raycast on primary pointer-down and push the named object that was hit.")]
51
+ public bool capturePicks = true;
52
+
53
+ [Tooltip("Layers eligible for pick raycasts.")]
54
+ public LayerMask pickMask = ~0;
55
+
56
+ #if UNITY_WEBGL && !UNITY_EDITOR
57
+ [DllImport("__Internal")]
58
+ static extern int UptimizrUnityGetProtocolVersion();
59
+
60
+ [DllImport("__Internal")]
61
+ static extern void UptimizrUnityPushPose(
62
+ float px, float py, float pz,
63
+ float fx, float fy, float fz,
64
+ float ux, float uy, float uz,
65
+ float fov);
66
+
67
+ [DllImport("__Internal")]
68
+ static extern void UptimizrUnityPushPick(string objectName, float hx, float hy, float hz);
69
+
70
+ [DllImport("__Internal")]
71
+ static extern void UptimizrUnityPushPerf(float fps, int longFrames);
72
+
73
+ [DllImport("__Internal")]
74
+ static extern void UptimizrUnitySetSceneProxy(string json);
75
+ #else
76
+ // Editor / non-WebGL stubs so the component compiles and runs in the Editor and in
77
+ // native builds (where there is no JS bridge to push to).
78
+ static int UptimizrUnityGetProtocolVersion() => ExpectedProtocolVersion;
79
+
80
+ static void UptimizrUnityPushPose(
81
+ float px, float py, float pz,
82
+ float fx, float fy, float fz,
83
+ float ux, float uy, float uz,
84
+ float fov) { }
85
+
86
+ static void UptimizrUnityPushPick(string objectName, float hx, float hy, float hz) { }
87
+
88
+ static void UptimizrUnityPushPerf(float fps, int longFrames) { }
89
+
90
+ static void UptimizrUnitySetSceneProxy(string json) { }
91
+ #endif
92
+
93
+ float poseTimer;
94
+ float perfTimer;
95
+ int frameCount;
96
+ int longFrameCount;
97
+
98
+ void Start()
99
+ {
100
+ int version = UptimizrUnityGetProtocolVersion();
101
+ if (version != ExpectedProtocolVersion)
102
+ {
103
+ Debug.LogWarning(
104
+ "[Uptimizr] bridge protocol mismatch: this shim expects v" +
105
+ ExpectedProtocolVersion + " but the connector reports v" + version +
106
+ ". Make sure trackUnity()/unityCollector() is registered on the host " +
107
+ "page before the export loads. Disabling the bridge.");
108
+ enabled = false;
109
+ return;
110
+ }
111
+
112
+ if (targetCamera == null)
113
+ {
114
+ targetCamera = Camera.main;
115
+ }
116
+ }
117
+
118
+ void Update()
119
+ {
120
+ float dt = Time.unscaledDeltaTime;
121
+
122
+ // Performance: accumulate over a window, then report measured FPS + long frames.
123
+ frameCount++;
124
+ if (dt * 1000f > jankFrameMs)
125
+ {
126
+ longFrameCount++;
127
+ }
128
+ perfTimer += dt;
129
+ if (perfTimer >= perfReportInterval && perfTimer > 0f)
130
+ {
131
+ float fps = frameCount / perfTimer;
132
+ UptimizrUnityPushPerf(fps, longFrameCount);
133
+ perfTimer = 0f;
134
+ frameCount = 0;
135
+ longFrameCount = 0;
136
+ }
137
+
138
+ Camera cam = targetCamera != null ? targetCamera : Camera.main;
139
+ if (cam == null)
140
+ {
141
+ return;
142
+ }
143
+
144
+ // Camera pose: position / forward / up in Unity's native frame; FOV in radians.
145
+ poseTimer += dt;
146
+ if (poseTimer >= poseSampleInterval)
147
+ {
148
+ poseTimer = 0f;
149
+ Transform t = cam.transform;
150
+ Vector3 p = t.position;
151
+ Vector3 f = t.forward;
152
+ Vector3 u = t.up;
153
+ float fovRad = cam.fieldOfView * Mathf.Deg2Rad;
154
+ UptimizrUnityPushPose(p.x, p.y, p.z, f.x, f.y, f.z, u.x, u.y, u.z, fovRad);
155
+ }
156
+
157
+ // Picks: on primary pointer-down, raycast and push the named object + hit point.
158
+ if (capturePicks && Input.GetMouseButtonDown(0))
159
+ {
160
+ Ray ray = cam.ScreenPointToRay(Input.mousePosition);
161
+ if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, pickMask))
162
+ {
163
+ Vector3 hp = hit.point;
164
+ UptimizrUnityPushPick(hit.collider.gameObject.name, hp.x, hp.y, hp.z);
165
+ }
166
+ }
167
+ }
168
+
169
+ /// <summary>
170
+ /// Optional one-shot: push the world-space AABBs of the given renderers as the
171
+ /// scene proxy (for spatial context + replay). Call once after your scene is
172
+ /// loaded. Object names come from your GameObjects — keep them non-PII (ADR 0003).
173
+ /// </summary>
174
+ public void PushSceneProxy(Renderer[] renderers)
175
+ {
176
+ if (renderers == null)
177
+ {
178
+ return;
179
+ }
180
+
181
+ var sb = new StringBuilder();
182
+ sb.Append('[');
183
+ bool first = true;
184
+ foreach (Renderer r in renderers)
185
+ {
186
+ if (r == null)
187
+ {
188
+ continue;
189
+ }
190
+ Bounds b = r.bounds;
191
+ Vector3 min = b.min;
192
+ Vector3 max = b.max;
193
+ if (!first)
194
+ {
195
+ sb.Append(',');
196
+ }
197
+ first = false;
198
+ sb.Append("{\"name\":");
199
+ AppendJsonString(sb, r.gameObject.name);
200
+ sb.Append(",\"aabb\":[");
201
+ AppendFloat(sb, min.x);
202
+ sb.Append(',');
203
+ AppendFloat(sb, min.y);
204
+ sb.Append(',');
205
+ AppendFloat(sb, min.z);
206
+ sb.Append(',');
207
+ AppendFloat(sb, max.x);
208
+ sb.Append(',');
209
+ AppendFloat(sb, max.y);
210
+ sb.Append(',');
211
+ AppendFloat(sb, max.z);
212
+ sb.Append("]}");
213
+ }
214
+ sb.Append(']');
215
+ UptimizrUnitySetSceneProxy(sb.ToString());
216
+ }
217
+
218
+ static void AppendFloat(StringBuilder sb, float v)
219
+ {
220
+ sb.Append(v.ToString("R", CultureInfo.InvariantCulture));
221
+ }
222
+
223
+ static void AppendJsonString(StringBuilder sb, string s)
224
+ {
225
+ sb.Append('"');
226
+ foreach (char c in s)
227
+ {
228
+ switch (c)
229
+ {
230
+ case '"':
231
+ sb.Append("\\\"");
232
+ break;
233
+ case '\\':
234
+ sb.Append("\\\\");
235
+ break;
236
+ case '\n':
237
+ sb.Append("\\n");
238
+ break;
239
+ case '\r':
240
+ sb.Append("\\r");
241
+ break;
242
+ case '\t':
243
+ sb.Append("\\t");
244
+ break;
245
+ default:
246
+ if (c < ' ')
247
+ {
248
+ sb.Append("\\u").Append(((int)c).ToString("x4"));
249
+ }
250
+ else
251
+ {
252
+ sb.Append(c);
253
+ }
254
+ break;
255
+ }
256
+ }
257
+ sb.Append('"');
258
+ }
259
+ }
@@ -0,0 +1,22 @@
1
+ import type { EngineBridge, NativeFrame, TrackWebExportOptions, WebExportCollectorOptions, WebExportSession } from "@uptimizr/web-export";
2
+ import type { Collector } from "@uptimizr/sdk-core";
3
+ /** Unity's native world coordinate frame: left-handed, y-up, meters (canonical). */
4
+ export declare const UNITY_FRAME: NativeFrame;
5
+ /** The engine id used for connector provenance and the collector name. */
6
+ export declare const UNITY_CONNECTOR_NAME = "unity";
7
+ export type UnityCollectorOptions = Omit<WebExportCollectorOptions, "name" | "frame">;
8
+ export type TrackUnityOptions = Omit<TrackWebExportOptions, "name" | "frame">;
9
+ /**
10
+ * The Unity collector — register it with an sdk-core client via `client.use(...)`.
11
+ * Wires the JS-only tier and exposes the engine bridge (default global
12
+ * `window.__uptimizr_unity__`) for the engine-side shim.
13
+ */
14
+ export declare function unityCollector(options?: UnityCollectorOptions): Collector;
15
+ /**
16
+ * One-call Unity integration: create a client, register {@link unityCollector}, and
17
+ * start the session with Unity's connector provenance (ADR 0018). Returns the client
18
+ * and the {@link EngineBridge} the Unity shim pushes through.
19
+ */
20
+ export declare function trackUnity(options: TrackUnityOptions): WebExportSession;
21
+ export type { EngineBridge };
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,oFAAoF;AACpF,eAAO,MAAM,WAAW,EAAE,WAA+D,CAAC;AAE1F,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,yBAAyB,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AACtF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,SAAS,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `@uptimizr/unity` — the Unity (WebGL export) connector for Uptimizr (ADR 0045).
3
+ *
4
+ * Unity compiles to WebAssembly and renders into a `<canvas>`, so there is no live
5
+ * JS scene to read. This connector is **two-part**:
6
+ *
7
+ * - a **JS-only tier** (this package, no engine code) — pointer heatmaps, rAF FPS,
8
+ * and error capture straight from the canvas DOM; and
9
+ * - a **bridged tier** — a thin engine-side shim (a `.jslib` plugin + a small
10
+ * `MonoBehaviour`, see `bridge/`) pushes camera pose / picks / perf over the
11
+ * versioned {@link EngineBridge} for view-direction heatmaps, world-space gaze,
12
+ * and replay.
13
+ *
14
+ * Unity's native world frame is **left-handed, y-up, meters** — already Uptimizr's
15
+ * canonical wire frame, so world-space payloads need no axis conversion (the
16
+ * normalization is the identity for Unity).
17
+ */
18
+ import { trackWebExport, webExportCollector } from "@uptimizr/web-export";
19
+ /** Unity's native world coordinate frame: left-handed, y-up, meters (canonical). */
20
+ export const UNITY_FRAME = { handedness: "left", upAxis: "y", unitScale: 1 };
21
+ /** The engine id used for connector provenance and the collector name. */
22
+ export const UNITY_CONNECTOR_NAME = "unity";
23
+ /**
24
+ * The Unity collector — register it with an sdk-core client via `client.use(...)`.
25
+ * Wires the JS-only tier and exposes the engine bridge (default global
26
+ * `window.__uptimizr_unity__`) for the engine-side shim.
27
+ */
28
+ export function unityCollector(options = {}) {
29
+ return webExportCollector({ ...options, name: UNITY_CONNECTOR_NAME, frame: UNITY_FRAME });
30
+ }
31
+ /**
32
+ * One-call Unity integration: create a client, register {@link unityCollector}, and
33
+ * start the session with Unity's connector provenance (ADR 0018). Returns the client
34
+ * and the {@link EngineBridge} the Unity shim pushes through.
35
+ */
36
+ export function trackUnity(options) {
37
+ return trackWebExport({ ...options, name: UNITY_CONNECTOR_NAME, frame: UNITY_FRAME });
38
+ }
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAU1E,oFAAoF;AACpF,MAAM,CAAC,MAAM,WAAW,GAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AAE1F,0EAA0E;AAC1E,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAK5C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,UAAiC,EAAE;IAChE,OAAO,kBAAkB,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;AAC5F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,OAAO,cAAc,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;AACxF,CAAC"}