// Rollercoaster1.cs using IHXR.Simulation; using System.Collections; public class Rollercoaster1 : ScriptBase { public SceneObject AnimatedObject; public SceneObject ButtonRandomFrame; public SceneObject ButtonPlay; public SceneObject ButtonStop; public SceneObject ButtonReset; // Cart sounds: objects with an Audio Source (a looping ride sound) placed inside the carts. Each is pitched by how // fast its own cart moves: almost silent and very low when the ride stands still, higher the faster it goes. // Leave a slot empty if you don't need it. public SceneObject CartSound1; public SceneObject CartSound2; public float CartSoundMinPitch = 0.05f; // pitch when stopped public float CartSoundMaxPitch = 1.3f; // pitch at full speed public float CartSoundFullSpeed = 14f; // cart speed (metres per second) that reaches the full pitch public float CartSoundVolumeAtRest = 0f; // 0 = silent when stopped public string ClipName = ""; public float PlaybackSpeed = 1.0f; public bool Loop = true; public float SyncInterval = 1.0f; public float BootupDelay = 12.0f; private Animation anim; private AnimationState state; private Coroutine syncRoutine; private bool syncingPlayback; public override void Start() { SetupAnim(); MakeInteractable(ButtonRandomFrame, OnRandomFrame, true, null, true, 10f, "Random Frame"); MakeInteractable(ButtonPlay, OnPlay, true, null, true, 10f, "Play Anim"); MakeInteractable(ButtonStop, OnStop, true, null, true, 10f, "Stop Anim"); MakeInteractable(ButtonReset, OnReset, true, null, true, 10f, "Reset Anim"); SetupCartSounds(); Log("[LegacyAnimSyncTester] Ready."); StartCoroutine(DelayedPlayStartup()); } // The speed of a cart is only known to the viewers (the carts are moved by the model's animation, not by physics), // so every viewer measures it and pitches the sound itself: everyone hears the same thing. private void SetupCartSounds() { foreach (SceneObject sound in new[] { CartSound1, CartSound2 }) { if (sound == null || !sound.Exists) continue; SetAudioPitchByMotion(sound, CartSoundMinPitch, CartSoundMaxPitch, CartSoundFullSpeed, CartSoundVolumeAtRest); } } private IEnumerator DelayedPlayStartup() { yield return new WaitForSeconds(BootupDelay); PlayAnim(); } private void SetupAnim() { if (AnimatedObject == null || !AnimatedObject.Exists) { LogError("AnimatedObject is not assigned."); return; } anim = AnimatedObject.GetComponentInChildren(true); if (anim == null) { LogError("No legacy Animation found."); return; } if (string.IsNullOrEmpty(ClipName)) { foreach (AnimationState s in anim) { ClipName = s.name; break; } } state = anim[ClipName]; if (state == null) { LogError("Clip not found: " + ClipName); return; } anim.playAutomatically = true; state.wrapMode = Loop ? WrapMode.Loop : WrapMode.Default; FreezeAtTime(0f); } private void OnRandomFrame(InteractionInfo info) { if (!EnsureAnim()) return; int maxFrame = Mathf.Max(1, Mathf.RoundToInt(state.clip.frameRate * state.clip.length)); int frame = Random.Range(0, maxFrame); float time = frame / state.clip.frameRate; FreezeAtTime(time); Log("Random synced frame: " + frame); } private void OnPlay(InteractionInfo info) { PlayAnim(); } private void PlayAnim() { if (!EnsureAnim()) return; syncingPlayback = true; state.enabled = true; state.weight = 1f; state.speed = PlaybackSpeed; state.wrapMode = Loop ? WrapMode.Loop : WrapMode.Default; anim.Play(state.name); SyncLegacyAnimationState(AnimatedObject); if (syncRoutine != null) StopCoroutine(syncRoutine); syncRoutine = StartCoroutine(SyncPlaybackLoop()); Log("Synced play at speed " + PlaybackSpeed); } private void OnStop(InteractionInfo info) { if (!EnsureAnim()) return; syncingPlayback = false; if (syncRoutine != null) { StopCoroutine(syncRoutine); syncRoutine = null; } FreezeAtTime(state.time); Log("Synced stop/freeze at time " + state.time); } // Stops playback and puts the animation back on its first frame (frame 0), for everyone. private void OnReset(InteractionInfo info) { if (!EnsureAnim()) return; syncingPlayback = false; if (syncRoutine != null) { StopCoroutine(syncRoutine); syncRoutine = null; } FreezeAtTime(0f); Log("Synced reset to frame 0"); } private void FreezeAtTime(float time) { state.enabled = true; state.weight = 1f; state.speed = 0f; state.time = Mathf.Repeat(time, state.clip.length); anim.Play(state.name); anim.Sample(); // Important: sync before Stop(). speed=0 tells clients to sample/freeze. SyncLegacyAnimationState(AnimatedObject); } private IEnumerator SyncPlaybackLoop() { while (syncingPlayback && anim != null && state != null && anim.IsPlaying(state.name)) { SyncLegacyAnimationState(AnimatedObject); yield return new WaitForSeconds(SyncInterval); } syncRoutine = null; } private bool EnsureAnim() { if (anim == null || state == null) SetupAnim(); return anim != null && state != null; } }