Connect Unreal Engine to a Sticklytics relay session for real-time 3D lacrosse stick visualization.
BLE sensor → Sticklytics app (processing + physics) → backend relay → Unreal Engine (visualization only).
Sticklytics performs all sensor processing. Unreal polls the relay for processed packets and renders them.
In the Sticklytics Devices page, click Start Unreal Visualization. The relay returns a session response:
{
"sessionId": "session-uuid",
"status": "active",
"latestPacketUrl": "/sessions/session-uuid/latest",
"eventsUrl": "/sessions/session-uuid/events",
"recommendedPollingIntervalMs": 50,
"accessToken": "temporary-session-token",
"expiresAt": "2026-07-29T16:54:21Z"
}Copy the Session ID and Access Token from the panel into your Unreal project.
All Unreal requests use the temporary session token:
Authorization: Bearer {accessToken}Tokens are session-scoped, expire automatically (default 60s of inactivity), and are never the admin key. Do not log or persist the token beyond the session lifetime.
Unreal polls at a 50ms interval (recommended). Use afterSequence to fetch only newer data:
GET https://api.base44.com/v1/functions/unrealRelay/sessions/{sessionId}/latest?afterSequence=1841
Authorization: Bearer {accessToken}New data response:
{
"status": "active",
"hasNewData": true,
"latestSequence": 1842,
"packetAgeMs": 25,
"serverTimestamp": "ISO timestamp",
"packet": { ... }
}No newer data:
{
"status": "active",
"hasNewData": false,
"latestSequence": 1842,
"packet": null
}When hasNewData is false, do not re-apply the previous packet — skip the frame.
The packet contains orientation as a quaternion (preferred) and euler degrees (debug):
"orientation": {
"quaternion": { "w": 0.91, "x": 0.12, "y": -0.18, "z": 0.35 },
"eulerDegrees": { "pitch": 12.4, "roll": -4.8, "yaw": 87.2 }
}Unreal uses a left-handed coordinate system (X=forward, Y=right, Z=up). Sticklytics already converts sensor axes to Unreal space via the coordinate profile. Apply the quaternion directly:
// Unreal C++ (pseudocode) FQuat StickQuat; StickQuat.W = Packet.orientation.quaternion.w; StickQuat.X = Packet.orientation.quaternion.x; StickQuat.Y = Packet.orientation.quaternion.y; StickQuat.Z = Packet.orientation.quaternion.z; StickMesh->SetRelativeRotation(StickQuat);
For euler fallback: FRotator(Packet.eulerDegrees.pitch, Packet.eulerDegrees.yaw, Packet.eulerDegrees.roll).
Fetch events newer than a sequence:
GET https://api.base44.com/v1/functions/unrealRelay/sessions/{sessionId}/events?afterSequence=1840
Authorization: Bearer {accessToken}Event types: session_started, calibration_completed, cradle_started, cradle_stopped, pass_detected, shot_windup, shot_released, peak_velocity, impact_detected, sensor_disconnected, sensor_reconnected, session_paused, session_resumed, session_ended.
Trigger Unreal effects (particles, sounds, UI) based on eventType and metrics.
packetAgeMs exceeds 500ms, treat the stick as stale (lerp toward idle).status is "paused", keep the last pose frozen.Sessions expire after sessionTimeoutSeconds of inactivity (default 60s). The expiresAt field gives the exact expiry. On expiry, all endpoints return 410 with SESSION_EXPIRED.
https://api.base44.com/v1/functions/unrealRelay/sessions/{sessionId}/latest?afterSequence={LastSeq}.Authorization: Bearer {accessToken} header.hasNewData.packet.orientation.quaternion.FQuat and call SetRelativeRotation on your stick mesh.LastSeq = latestSequence./events?afterSequence= at 200ms to trigger effects.void AStickVisualizer::Tick(float DeltaTime)
{
if (bIsPolling) return;
bIsPolling = true;
FString Url = FString::Printf(TEXT("%s/sessions/%s/latest?afterSequence=%d"),
*EndpointBase, *SessionId, LastSequence);
FHttpRequest* Req = FHttpModule::CreateRequest();
Req->SetURL(Url);
Req->SetHeader(TEXT("Authorization"),
FString::Printf(TEXT("Bearer %s"), *AccessToken));
Req->OnProcessRequestComplete().BindUObject(this,
&AStickVisualizer::OnLatestResponse);
Req->ProcessRequest();
}
void AStickVisualizer::OnLatestResponse(FHttpRequest* Req,
FHttpResponse* Resp)
{
bIsPolling = false;
if (Resp->GetResponseCode() != 200) return;
const FString Json = Resp->GetContentAsString();
FPacket Packet;
if (!FJsonObjectConverter::JsonObjectStringToUStruct(
Json, &Packet, nullptr, 0))
{
return;
}
if (!Packet.bHasNewData || !Packet.Packet.IsValid())
{
return; // no new data — keep current pose
}
LastSequence = Packet.LatestSequence;
const FQuat StickQuat(
Packet.Packet.Orientation.Quaternion.X,
Packet.Packet.Orientation.Quaternion.Y,
Packet.Packet.Orientation.Quaternion.Z,
Packet.Packet.Orientation.Quaternion.W);
StickMesh->SetRelativeRotation(StickQuat);
// Derived metrics for HUD / particle intensity
const float SpeedMph =
Packet.Packet.DerivedMetrics.StickSpeedMph;
}Structured errors:
{
"error": {
"code": "SESSION_NOT_FOUND",
"message": "The requested Unreal relay session does not exist.",
"retryable": false
}
}Codes: SESSION_NOT_FOUND (404), SESSION_EXPIRED (410), SESSION_ENDED (410), STALE_SEQUENCE (409), UNAUTHORIZED (401), FORBIDDEN (403), REPLAY_NOT_ENABLED (400), VALIDATION_ERROR (400).
When Record for Replay is enabled, packets and events are stored. Retrieve them:
GET https://api.base44.com/v1/functions/unrealRelay/sessions/{sessionId}/replay?startMs=0&endMs=10000
Authorization: Bearer {accessToken}Returns an ordered array of packets and events with original timestamps and sequence numbers.