Files
RoomAwareVR/Assets/_Scripts/GhostSystem/GhostRecorderBatch.cs
T
2026-01-16 11:19:54 +01:00

205 lines
8.6 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using System;
using Oculus.Interaction;
using System.IO;
// Dieses Script ist für die Aufnahme des Replays während des Trainingsdurchlaufs zuständig.
namespace GhostSystem
{
public class GhostRecorderBatch : MonoBehaviour
{
public string SceneId; // SceneID zum Übergeben des Raumnamens an die Datenbank.
public float SampleRate = 0.05f; // Entspricht einer Framerate von 20 FPS.
public int BatchSize = 10; // Anzahl der Frames nach denen ein neuer Batch beginnt. Wird nicht mehr genutzt, da manueller Flush.
public float FlushInterval = 2f; // Länge des Intervalls bis zum nächsten Flush. Wird nicht mehr genutzt, da manueller Flush.
public List<Transform> TrackedObjects;
private int doorCount;
private float _sampleTimer;
private float _flushTimer;
private bool _isRecording;
private bool supabaseReady = false;
private bool sceneIdReady = false;
private readonly List<FrameModel> _frameBuffer = new();
private string _offlinePath;
public static event Action OnRecord;
public static event Action OnStopRecord;
public static event Action OnUploadFinished;
/*private void Awake()
{
DontDestroyOnLoad(this);
}*/
private void OnEnable()
{
SupabaseManager.OnSuperbaseReady += HandleSupabaseReady; // Das Skript horcht nach den beiden Events, da eine Aufnahme in die Datenbank nur möglich sien soll,
RoomManager.OnSceneIdReady += HandleSceneIdReady; // wenn die Szene komplett geladen und die Verbindung zu Supabase aufgebaut ist.
}
private void OnDisable()
{
SupabaseManager.OnSuperbaseReady -= HandleSupabaseReady;
RoomManager.OnSceneIdReady -= HandleSceneIdReady;
}
private void Start()
{
string now = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string dateiname = now + ".jsonl";
_offlinePath = Path.Combine(Application.persistentDataPath, dateiname);
}
private void HandleSupabaseReady()
{
supabaseReady = true;
}
private void HandleSceneIdReady(string id, List<GameObject> doors, List<RecieveHeat> targets) // Die Szene muss komplett geladen sein, da mit dem Event die SceneID
{ // übergeben wird.
SceneId = id;
sceneIdReady = true;
if (SceneId == "Base")
{
//_flushTimer = 0f;
//_ = FlushBatchAsync(); // Fire and forget
}
else
{
foreach (GameObject door in doors) // neben den Tracked Objects, die in jeder Szene gleich sind (Player-Root, Kopf, Hände, Augen), müssen die Türen jedes Raums
{ // zu den Tracked Objects hinzugefügt werden. Nur so wird die Bewegung der Tür auch im Replay wiedergegeben.
var rotator = door.GetComponentInChildren<OneGrabRotateTransformer>();
if (rotator != null)
{
TrackedObjects.Add(rotator.transform);
doorCount++;
}
}
}
}
private void Update()
{
if (!_isRecording) return; // Code im Update wird nur ausgeführt, während die Aufnahme läuft.
_sampleTimer += Time.deltaTime;
_flushTimer += Time.deltaTime;
if (_sampleTimer >= SampleRate) // Es wird immer die Dauer bis zum nächsten Record-Frame runter gezählt und ein Sample aufgenommen.
{
_sampleTimer = 0f;
RecordSample();
}
// Würde genutzt werden, um live in die Datenbank hochzuladen.
/*if (_frameBuffer.Count >= BatchSize || _flushTimer >= FlushInterval)
{
_flushTimer = 0f;
_ = FlushBatchAsync(); // Fire and forget
}*/
}
private void RecordSample()
{
foreach (var obj in TrackedObjects) // Ein Sample besteht aus einer Liste von FrameModels, die für den Zustand jedes getrackten Objekts zum aktuellen Zeitpunkt
{ // erstellt werden.
var frame = new GhostFrame(SceneId, obj.name, obj.position, obj.rotation);
_frameBuffer.Add(FrameModel.FromGhostFrame(frame)); // Die Frames werden an den Buffer angehängt, um noch während der Aufnahme noch nciht so viele System-
} // Ressourcen zu verbrauchen.
}
private async UniTaskVoid FlushBatchAsync() // Die eigentliche Methode zum Hochladen eines Batches/leeren des Buffers.
{
if (_frameBuffer.Count == 0) return;
var batch = new List<FrameModel>(_frameBuffer);
_frameBuffer.Clear();
Debug.Log("Flushing Frame Buffer");
await FallbackToOfflineAsync(batch);
OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst.
/*try
{
var response = await SupabaseManager.instance.supabase.From<FrameModel>().Insert(batch); // Versucht, den Batch an Superbase hochzuladen
if (!response.ResponseMessage.IsSuccessStatusCode)
{
Debug.LogWarning($"Batch insert failed: {response.ResponseMessage.StatusCode}, fallback to offline.");
await FallbackToOfflineAsync(batch);
OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst.
Debug.Log("Upload Finished");
}
if (response.ResponseMessage.IsSuccessStatusCode && sceneIdReady) // Löst Action aus, wenn der Upload geklappt hat und die neue Szene geladen ist.
{
OnUploadFinished?.Invoke();
Debug.Log("Upload Finished");
}
}
catch (Exception ex)
{
Debug.LogWarning($"Insert failed, fallback to offline: {ex.Message}");
await FallbackToOfflineAsync(batch); // Bei nicht erfolgtem Upload wird der Batch lokal gescpeichert.
OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst.
Debug.Log("Upload Finished");
}*/
}
private async UniTask FallbackToOfflineAsync(List<FrameModel> batch) // Schreibt den Buffer in eine Datei, falls Upload nicht möglich. Kann zu Rucklern führen.
{
try
{
using var writer = new StreamWriter(_offlinePath, append: true);
foreach (var model in batch)
{
string jsonLine = JsonConvert.SerializeObject(model);
await writer.WriteLineAsync(jsonLine);
}
}
catch (Exception ex)
{
Debug.LogError($"Offline write failed: {ex.Message}");
}
}
public void StartRecording() // Wenn die Bedingungen erfüllt sind, startet diese Methode die Aufnahme. Sie kann von anderen Skripten aufgerufen werden.
{
if (!supabaseReady || !sceneIdReady) return;
_isRecording = true;
OnRecord?.Invoke();
Debug.Log($"Recording {SceneId}");
}
public void StopRecording() // Diese Methode stoppt die Aufnahme, löst ein entsprechendes Event aus, das abonniert werden kann und entfernt die Türen des aktuellen
{ // Raums wieder aus der Liste der Tracked Objects.
_isRecording = false;
sceneIdReady = false;
OnStopRecord?.Invoke();
Debug.Log($"Stop recording: {SceneId}");
if (doorCount > 0 && TrackedObjects.Count >= doorCount)
{
TrackedObjects.RemoveRange(TrackedObjects.Count - doorCount, doorCount);
}
doorCount = 0;
}
public void Flush() // Manueller Flush
{
_flushTimer = 0f;
_ = FlushBatchAsync(); // Fire and forget
}
}
}