// CharacterPhysicsModes.cs // Ragdoll physics for players: the Drop button (or a fall trigger) turns the player's body into a ragdoll that falls // onto the ground, terrain, furniture or moving physics objects; the Stand button (or the recover timers) gets them up // again. The Ragdoll settings below are sent to each player before they fall. using System.Collections; using System.Collections.Generic; using IHXR.Simulation; public class CharacterPhysicsModes : ScriptBase { 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 fallStopRecoverSeconds = 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; [Header("Ragdoll")] [Tooltip("Muscles: 0 = completely limp, 1 = stiff as a statue. Around 0.1 to 0.2 looks like a person falling.")] [Range(0f, 1f)] public float ragdollStrength = 0.12f; [Tooltip("1 = normal gravity, 0.5 = floaty, 2 = heavy.")] [Range(-2f, 3f)] public float ragdollGravityScale = 1f; [Tooltip("Air drag: 0 = none, higher = the body slows down sooner.")] [Range(0f, 0.5f)] public float ragdollDamping = 0.02f; [Tooltip("0 = the body slides along surfaces, 1 = it stops where it lands.")] [Range(0f, 1f)] public float ragdollFriction = 0.6f; [Tooltip("0 = no bounce, 1 = bounces right back.")] [Range(0f, 1f)] public float ragdollBounciness = 0.08f; [Tooltip("How hard the Drop button pushes the player (in the direction below) as they fall. 0 = they just collapse.")] public float dropPushPower = 0f; public Vector3 dropPushDirection = new Vector3(0f, 0.3f, 1f); Dictionary activeFallTokens = new Dictionary(); Dictionary landedFallTokens = new Dictionary(); int nextFallToken; public override void Start() { RegisterButtons(); RegisterFallTriggers(); } public override void OnUserJoin(UserInfo user) { string username = user != null ? user.Username : null; if (resetUsersOnJoin && !string.IsNullOrWhiteSpace(username)) { ClearFallState(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(InteractionInfo info) { if (info == null || string.IsNullOrWhiteSpace(info.Username)) return; SendRagdollSettings(info.Username); ApplyCharacterImpact( info.Username, dropPushPower > 0f ? dropPushDirection : Vector3.zero, dropPushPower, 0.15f, true, dropRecoverSeconds, CharacterImpactPart.All); PlayOptionalAudio(DropAudioObject); } void OnStandClicked(InteractionInfo info) { if (info == null || string.IsNullOrWhiteSpace(info.Username)) return; ClearFallState(info.Username); StopCharacterPhysicsMode(info.Username, standTransitionSeconds); } void OnFallStartEnter(TriggerInfo info) { if (info == null || !info.IsPlayer || string.IsNullOrWhiteSpace(info.Username)) return; string username = info.Username.Trim(); int token = StartNewFallCycle(username); StopCharacterPhysicsMode(username, 0f); PlayOptionalAudio(FallStartAudioObject); StartCoroutine(SetFallingNextFrame(username, token)); } IEnumerator SetFallingNextFrame(string username, int token) { yield return null; if (!IsActiveFall(username, token)) yield break; SendRagdollSettings(username); SetCharacterPhysicsMode( username, CharacterPhysicsMode.Falling, 0.15f, 0f); ScheduleFallAutoRecover(username, token); } void OnFallStopEnter(TriggerInfo info) { if (info == null || !info.IsPlayer || string.IsNullOrWhiteSpace(info.Username)) return; string username = info.Username.Trim(); int token; if (!activeFallTokens.TryGetValue(username, out token)) return; int landedToken; if (landedFallTokens.TryGetValue(username, out landedToken) && landedToken == token) return; landedFallTokens[username] = token; PlayOptionalAudio(FallStopAudioObject); ScheduleFallStop(username, token); } int StartNewFallCycle(string username) { nextFallToken++; if (nextFallToken == int.MaxValue) nextFallToken = 1; activeFallTokens[username] = nextFallToken; landedFallTokens.Remove(username); return nextFallToken; } bool IsActiveFall(string username, int token) { int activeToken; return activeFallTokens.TryGetValue(username, out activeToken) && activeToken == token; } void ScheduleFallAutoRecover(string username, int token) { float delaySeconds = Mathf.Max(0f, fallRecoverSeconds); if (delaySeconds > 0f) StartCoroutine(AutoRecoverFallAfterDelay(username, token, delaySeconds)); } IEnumerator AutoRecoverFallAfterDelay(string username, int token, float delaySeconds) { yield return new WaitForSeconds(delaySeconds); int landedToken; if (IsActiveFall(username, token) && !landedFallTokens.TryGetValue(username, out landedToken)) CompleteFallCycle(username, token); } void ScheduleFallStop(string username, int token) { float delaySeconds = GetFallStopRecoverSeconds(); if (delaySeconds <= 0f) { CompleteFallCycle(username, token); return; } StartCoroutine(StopFallingAfterDelay(username, token, delaySeconds)); } float GetFallStopRecoverSeconds() { if (fallStopRecoverSeconds > 0f) return fallStopRecoverSeconds; return Mathf.Max(0f, fallRecoverSeconds); } IEnumerator StopFallingAfterDelay(string username, int token, float delaySeconds) { yield return new WaitForSeconds(delaySeconds); CompleteFallCycle(username, token); } void CompleteFallCycle(string username, int token) { if (!IsActiveFall(username, token)) return; activeFallTokens.Remove(username); landedFallTokens.Remove(username); StopCharacterPhysicsMode(username, standTransitionSeconds); } void ClearFallState(string username) { activeFallTokens.Remove(username); landedFallTokens.Remove(username); } void SendRagdollSettings(string username) { CharacterRagdollSettings settings = new CharacterRagdollSettings(); settings.Strength = ragdollStrength; settings.GravityScale = ragdollGravityScale; settings.Damping = ragdollDamping; settings.Friction = ragdollFriction; settings.Bounciness = ragdollBounciness; SetCharacterRagdollSettings(username, settings); } void PlayOptionalAudio(SceneObject audioObject) { if (audioObject == null || audioObject.IsNull) return; if (stopAudioBeforePlay) StopAudio(audioObject, null, audioSourceIndex, includeAudioChildren); PlayAudio(audioObject, null, audioSourceIndex, includeAudioChildren); } }