// CharacterPhysicsModes.cs using System.Collections; using System.Collections.Generic; using Simuspaces.Simulation; public class CharacterPhysicsModes : SimuScriptBase { public SceneObject DropButton; public SceneObject StandButton; public SceneObject FallStartTrigger; public SceneObject FallStopTrigger; public SceneObject FallStartAudioObject; public SceneObject FallStopAudioObject; public SceneObject DropAudioObject; public float dropRecoverSeconds = 3.0f; public float fallRecoverSeconds = 0.0f; public float standTransitionSeconds = 0.8f; public bool resetUsersOnJoin = true; public bool highlightButtons = true; public float interactDistance = 10f; public int audioSourceIndex = 0; public bool includeAudioChildren = true; public bool stopAudioBeforePlay = true; HashSet fallingUsers = new HashSet(); Dictionary fallingExpiresAt = new Dictionary(); public override void Start() { RegisterButtons(); RegisterFallTriggers(); } public override void OnUserJoin(SVUserInfo user) { string username = user != null ? user.Username : null; if (resetUsersOnJoin && !string.IsNullOrWhiteSpace(username)) { ClearFallingUser(username); StopCharacterPhysicsMode(username, standTransitionSeconds); } RegisterButtons(username); } void RegisterButtons(string targetUsername = null) { if (DropButton != null && !DropButton.IsNull) { MakeInteractable( DropButton, OnDropClicked, highlightButtons, targetUsername, true, interactDistance, "Drop"); } if (StandButton != null && !StandButton.IsNull) { MakeInteractable( StandButton, OnStandClicked, highlightButtons, targetUsername, true, interactDistance, "Stand"); } } void RegisterFallTriggers() { if (FallStartTrigger != null && !FallStartTrigger.IsNull) RegisterTrigger(FallStartTrigger, OnFallStartEnter); if (FallStopTrigger != null && !FallStopTrigger.IsNull) RegisterTrigger(FallStopTrigger, OnFallStopEnter); } void OnDropClicked(SVInteractionInfo info) { if (info == null || string.IsNullOrWhiteSpace(info.Username)) return; ApplyCharacterImpact( info.Username, Vector3.zero, 0f, 0f, true, dropRecoverSeconds, SVCharacterImpactPart.All); PlayOptionalAudio(DropAudioObject); } void OnStandClicked(SVInteractionInfo info) { if (info == null || string.IsNullOrWhiteSpace(info.Username)) return; ClearFallingUser(info.Username); StopCharacterPhysicsMode(info.Username, standTransitionSeconds); } void OnFallStartEnter(SVTriggerInfo info) { if (info == null || !info.IsPlayer || string.IsNullOrWhiteSpace(info.Username)) return; SetCharacterPhysicsMode( info.Username, SVCharacterPhysicsMode.Falling, 0.15f, fallRecoverSeconds); if (fallingUsers.Add(info.Username)) PlayOptionalAudio(FallStartAudioObject); ScheduleFallingExpiry(info.Username); } void OnFallStopEnter(SVTriggerInfo info) { if (info == null || !info.IsPlayer || string.IsNullOrWhiteSpace(info.Username)) return; bool wasFalling = fallingUsers.Contains(info.Username); ClearFallingUser(info.Username); StopCharacterPhysicsMode(info.Username, standTransitionSeconds); if (wasFalling) PlayOptionalAudio(FallStopAudioObject); } void ScheduleFallingExpiry(string username) { if (fallRecoverSeconds <= 0f) { fallingExpiresAt.Remove(username); return; } float expiresAt = Time.time + fallRecoverSeconds; fallingExpiresAt[username] = expiresAt; StartCoroutine(ClearFallingAfterDelay(username, fallRecoverSeconds, expiresAt)); } IEnumerator ClearFallingAfterDelay(string username, float delaySeconds, float expiresAt) { yield return new WaitForSeconds(delaySeconds); float activeExpiresAt; if (fallingExpiresAt.TryGetValue(username, out activeExpiresAt) && Mathf.Approximately(activeExpiresAt, expiresAt)) ClearFallingUser(username); } void ClearFallingUser(string username) { fallingUsers.Remove(username); fallingExpiresAt.Remove(username); } void PlayOptionalAudio(SceneObject audioObject) { if (audioObject == null || audioObject.IsNull) return; if (stopAudioBeforePlay) StopAudio(audioObject, null, audioSourceIndex, includeAudioChildren); PlayAudio(audioObject, null, audioSourceIndex, includeAudioChildren); } }