/// <summary> /// Rotates the camera to face the character. /// </summary> /// <param name="horizontalMovement">-1 to 1 value specifying the amount of horizontal movement.</param> /// <param name="verticalMovement">-1 to 1 value specifying the amount of vertical movement.</param> /// <param name="immediateUpdate">Should the camera be updated immediately?</param> /// <returns>The updated rotation.</returns> public override Quaternion Rotate(float horizontalMovement, float verticalMovement, bool immediateUpdate) { #if ULTIMATE_CHARACTER_CONTROLLER_VR if (m_VREnabled && immediateUpdate) { EventHandler.ExecuteEvent("OnTryRecenterTracking"); } #endif var up = m_CharacterLocomotion.AlignToGravity ? m_CharacterLocomotion.Up : m_UpAxis; var rotation = Quaternion.LookRotation(-m_Ray.direction, up); return(immediateUpdate ? rotation : Quaternion.Slerp(m_Transform.rotation, rotation, m_RotationSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime)); }
/// <summary> /// Adds amount to health and then to the shield if there is still an amount remaining. Will not go over the maximum health or shield value. /// </summary> /// <param name="amount">The amount of health or shield to add.</param> /// <returns>True if the object was healed.</returns> public virtual bool Heal(float amount) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkHealthMonitor.Heal(amount); } #endif var healAmount = 0f; // Contribute the amount of the health first. if (m_HealthAttribute != null && m_HealthAttribute.Value < m_HealthAttribute.MaxValue) { var healthAmount = Mathf.Min(amount, m_HealthAttribute.MaxValue - m_HealthAttribute.Value); amount -= healthAmount; m_HealthAttribute.Value += healthAmount; healAmount += healthAmount; } // Add any remaining amount to the shield. if (m_ShieldAttribute != null && amount > 0 && m_ShieldAttribute.Value < m_ShieldAttribute.MaxValue) { var shieldAmount = Mathf.Min(amount, m_ShieldAttribute.MaxValue - m_ShieldAttribute.Value); m_ShieldAttribute.Value += shieldAmount; healAmount += shieldAmount; } // Don't play any effects if the object wasn't healed. if (healAmount == 0) { return(false); } // Play any heal audio. m_HealAudioClipSet.PlayAudioClip(m_GameObject); EventHandler.ExecuteEvent <float>(m_GameObject, "OnHealthHeal", healAmount); if (m_OnHealEvent != null) { m_OnHealEvent.Invoke(healAmount); } if (m_DamagePopupManager != null) { m_DamagePopupManager.OpenHealPopup(m_Transform.position, amount); } return(true); }
/// <summary> /// Rotates the camera to face the character. /// </summary> /// <param name="horizontalMovement">-1 to 1 value specifying the amount of horizontal movement.</param> /// <param name="verticalMovement">-1 to 1 value specifying the amount of vertical movement.</param> /// <param name="immediateUpdate">Should the camera be updated immediately?</param> /// <returns>The updated rotation.</returns> public override Quaternion Rotate(float horizontalMovement, float verticalMovement, bool immediateUpdate) { #if ULTIMATE_CHARACTER_CONTROLLER_VR if (m_VREnabled) { EventHandler.ExecuteEvent("OnTryRecenterTracking"); } #endif Quaternion targetRotation; var activeMovementType = m_CharacterLocomotion.ActiveMovementType as Character.MovementTypes.Pseudo3D; if (activeMovementType != null && activeMovementType.Path != null) { targetRotation = Quaternion.LookRotation(Vector3.Cross(activeMovementType.Path.GetTangent(GetAnchorPosition(), ref m_PathIndex), m_CharacterLocomotion.Up)) * Quaternion.LookRotation(m_ForwardAxis); } else { targetRotation = Quaternion.LookRotation(m_ForwardAxis); } return((immediateUpdate ? targetRotation : Quaternion.Slerp(m_Transform.rotation, targetRotation, m_RotationSmoothing)) * Quaternion.Euler(m_SecondaryRotationSpring.Value)); }
/// <summary> /// Do the explosion. /// </summary> /// <param name="damageAmount">The amount of damage to apply to the hit objects.</param> /// <param name="impactForce">The amount of force to apply to the hit object.</param> /// <param name="impactForceFrames">The number of frames to add the force to.</param> /// <param name="originator">The originator of the object.</param> public void Explode(float damageAmount, float impactForce, int impactForceFrames, GameObject originator) { Health health = null; Rigidbody colliderRigidbody = null; IForceObject forceObject = null; var hitCount = Physics.OverlapSphereNonAlloc(m_Transform.position, m_Radius, m_CollidersHit, m_ImpactLayers, QueryTriggerInteraction.Ignore); #if UNITY_EDITOR if (hitCount == m_MaxCollisionCount) { Debug.LogWarning("Warning: The maximum number of colliders have been hit by " + m_GameObject.name + ". Consider increasing the Max Collision Count value."); } #endif for (int i = 0; i < hitCount; ++i) { // A GameObject can contain multiple colliders. Prevent the explosion from occurring on the same GameObject multiple times. if (m_ObjectExplosions.Contains(m_CollidersHit[i].gameObject)) { continue; } m_ObjectExplosions.Add(m_CollidersHit[i].gameObject); // The base character GameObject should only be checked once. if ((forceObject = m_CollidersHit[i].gameObject.GetCachedParentComponent <IForceObject>()) != null) { if (m_ObjectExplosions.Contains(forceObject)) { continue; } m_ObjectExplosions.Add(forceObject); } // OverlapSphere can return objects that are in a different room. Perform a cast to ensure the object is within the explosion range. if (m_LineOfSight) { // Add a slight vertical offset to prevent a floor collider from getting in the way of the cast. var position = m_Transform.TransformPoint(0, 0.1f, 0); var direction = m_CollidersHit[i].transform.position - position; if (Physics.Raycast(position - direction.normalized * 0.1f, direction, out m_RaycastHit, direction.magnitude, m_ImpactLayers, QueryTriggerInteraction.Ignore) && !(m_RaycastHit.transform.IsChildOf(m_CollidersHit[i].transform) #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. || m_RaycastHit.transform.gameObject.GetCachedParentComponent <FirstPersonController.Character.FirstPersonObjects>() != null #endif )) { // If the collider is part of a character then ensure the head can't be hit. var parentAnimator = m_CollidersHit[i].transform.gameObject.GetCachedParentComponent <Animator>(); if (parentAnimator != null && parentAnimator.isHuman) { var head = parentAnimator.GetBoneTransform(HumanBodyBones.Head); direction = head.position - position; if (Physics.Raycast(position, direction, out m_RaycastHit, direction.magnitude, m_ImpactLayers, QueryTriggerInteraction.Ignore) && !m_RaycastHit.transform.IsChildOf(m_CollidersHit[i].transform) && !m_CollidersHit[i].transform.IsChildOf(m_RaycastHit.transform) && m_RaycastHit.transform.IsChildOf(m_Transform) #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. && m_RaycastHit.transform.gameObject.GetCachedParentComponent <FirstPersonController.Character.FirstPersonObjects>() == null #endif ) { continue; } } else { continue; } } } // The shield can absorb some (or none) of the damage from the explosion. var hitDamageAmount = damageAmount; #if ULTIMATE_CHARACTER_CONTROLLER_MELEE ShieldCollider shieldCollider; if ((shieldCollider = m_CollidersHit[i].transform.gameObject.GetCachedComponent <ShieldCollider>()) != null) { hitDamageAmount = shieldCollider.Shield.Damage(this, hitDamageAmount); } #endif // ClosestPoint only works with a subset of collider types. Vector3 closestPoint; if (m_CollidersHit[i] is BoxCollider || m_CollidersHit[i] is SphereCollider || m_CollidersHit[i] is CapsuleCollider || (m_CollidersHit[i] is MeshCollider && (m_CollidersHit[i] as MeshCollider).convex)) { closestPoint = m_CollidersHit[i].ClosestPoint(m_Transform.position); } else { closestPoint = m_CollidersHit[i].ClosestPointOnBounds(m_Transform.position); } var hitDirection = closestPoint - m_Transform.position; // Allow a custom event to be received. EventHandler.ExecuteEvent <float, Vector3, Vector3, GameObject, object, Collider>(m_CollidersHit[i].transform.gameObject, "OnObjectImpact", hitDamageAmount, closestPoint, hitDirection * m_ImpactForce, originator, this, m_CollidersHit[i]); if (m_OnImpactEvent != null) { m_OnImpactEvent.Invoke(hitDamageAmount, closestPoint, hitDirection * m_ImpactForce, originator); } // If the shield didn't absorb all of the damage then it should be applied to the character. if (hitDamageAmount > 0) { // If the Health component exists it will apply an explosive force to the character/character in addition to deducting the health. // Otherwise just apply the force to the character/rigidbody. if ((health = m_CollidersHit[i].gameObject.GetCachedParentComponent <Health>()) != null) { // The further out the collider is, the less it is damaged. var damageModifier = Mathf.Max(1 - (hitDirection.magnitude / m_Radius), 0.01f); health.Damage(hitDamageAmount * damageModifier, m_Transform.position, hitDirection.normalized, impactForce * damageModifier, impactForceFrames, m_Radius, originator, this, null); } else if (forceObject != null) { var damageModifier = Mathf.Max(1 - (hitDirection.magnitude / m_Radius), 0.01f); forceObject.AddForce(hitDirection.normalized * impactForce * damageModifier); } else if ((colliderRigidbody = m_CollidersHit[i].gameObject.GetCachedComponent <Rigidbody>()) != null) { colliderRigidbody.AddExplosionForce(impactForce * MathUtility.RigidbodyForceMultiplier, m_Transform.position, m_Radius); } } } m_ObjectExplosions.Clear(); // An audio clip can play when the object explodes. m_ExplosionAudioClipSet.PlayAudioClip(m_GameObject); Scheduler.Schedule(m_Lifespan, Destroy); }
/// <summary> /// The object is no longer alive. /// </summary> /// <param name="position">The position of the damage.</param> /// <param name="force">The amount of force applied to the object while taking the damage.</param> /// <param name="attacker">The GameObject that killed the character.</param> public virtual void Die(Vector3 position, Vector3 force, GameObject attacker) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkHealthMonitor.Die(position, force, attacker); } #endif // Spawn any objects on death, such as an explosion if the object is an explosive barrel. if (m_SpawnedObjectsOnDeath != null) { for (int i = 0; i < m_SpawnedObjectsOnDeath.Length; ++i) { var spawnedObject = ObjectPoolBase.Instantiate(m_SpawnedObjectsOnDeath[i], m_Transform.position, m_Transform.rotation); Explosion explosion; if ((explosion = spawnedObject.GetCachedComponent <Explosion>()) != null) { explosion.Explode(gameObject); } var rigidbodies = spawnedObject.GetComponentsInChildren <Rigidbody>(); for (int j = 0; j < rigidbodies.Length; ++j) { rigidbodies[j].AddForceAtPosition(force, position); } } } // Destroy any objects on death. The objects will be placed back in the object pool if they were created within it otherwise the object will be destroyed. if (m_DestroyedObjectsOnDeath != null) { for (int i = 0; i < m_DestroyedObjectsOnDeath.Length; ++i) { if (ObjectPoolBase.InstantiatedWithPool(m_DestroyedObjectsOnDeath[i])) { ObjectPoolBase.Destroy(m_DestroyedObjectsOnDeath[i]); } else { Object.Destroy(m_DestroyedObjectsOnDeath[i]); } } } // Change the layer to a death layer. if (m_DeathLayer.value != 0) { m_AliveLayer = m_GameObject.layer; m_GameObject.layer = m_DeathLayer; } // Play any take death audio. Use PlayAtPosition because the audio won't play if the GameObject is inactive. m_DeathAudioClipSet.PlayAtPosition(m_Transform.position); // Deactivate the object if requested. if (m_DeactivateOnDeath) { SchedulerBase.Schedule(m_DeactivateOnDeathDelay, Deactivate); } // The attributes shouldn't regenerate. if (m_ShieldAttribute != null) { m_ShieldAttribute.CancelAutoUpdate(); } if (m_HealthAttribute != null) { m_HealthAttribute.CancelAutoUpdate(); } // Notify those interested. EventHandler.ExecuteEvent <Vector3, Vector3, GameObject>(m_GameObject, "OnDeath", position, force, attacker); if (m_OnDeathEvent != null) { m_OnDeathEvent.Invoke(position, force, attacker); } }
/// <summary> /// The object has taken been damaged. /// </summary> /// <param name="damageData">The data associated with the damage.</param> public virtual void OnDamage(DamageData damageData) { if (damageData == null) { return; } // Add a multiplier if a particular collider was hit. Do not apply a multiplier if the damage is applied through a radius because multiple // collider are hit. if (damageData.Radius == 0 && damageData.Direction != Vector3.zero && damageData.HitCollider != null) { if (m_ColliderHitboxMap != null && m_ColliderHitboxMap.Count > 0) { Hitbox hitbox; if (m_ColliderHitboxMap.TryGetValue(damageData.HitCollider, out hitbox)) { damageData.Amount *= hitbox.DamageMultiplier; } else { // The main collider may be overlapping child hitbox colliders. Perform one more raycast to ensure a hitbox collider shouldn't be hit. float distance = 0.2f; if (damageData.HitCollider is CapsuleCollider capsuleCollider) { distance = capsuleCollider.radius; } else if (damageData.HitCollider is SphereCollider sphereCollider) { distance = sphereCollider.radius; } // The hitbox collider may be underneath the base collider. Fire a raycast to detemine if there are any colliders underneath the hit collider // that should apply a multiplier. var hitCount = Physics.RaycastNonAlloc(damageData.Position, damageData.Direction, m_RaycastHits, distance, ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.Overlay | 1 << LayerManager.VisualEffect), QueryTriggerInteraction.Ignore); for (int i = 0; i < hitCount; ++i) { var closestRaycastHit = QuickSelect.SmallestK(m_RaycastHits, hitCount, i, m_RaycastHitComparer); if (closestRaycastHit.collider == damageData.HitCollider) { continue; } // A new collider has been found - stop iterating if the hitbox map exists and use the hitbox multiplier. if (m_ColliderHitboxMap.TryGetValue(closestRaycastHit.collider, out hitbox)) { damageData.Amount *= hitbox.DamageMultiplier; damageData.HitCollider = hitbox.Collider; break; } } } } } // Apply the damage to the shield first because the shield can regenrate. if (m_ShieldAttribute != null && m_ShieldAttribute.Value > m_ShieldAttribute.MinValue) { var shieldAmount = Mathf.Min(damageData.Amount, m_ShieldAttribute.Value - m_ShieldAttribute.MinValue); damageData.Amount -= shieldAmount; m_ShieldAttribute.Value -= shieldAmount; } // Decrement the health by remaining amount after the shield has taken damage. if (m_HealthAttribute != null && m_HealthAttribute.Value > m_HealthAttribute.MinValue) { m_HealthAttribute.Value -= Mathf.Min(damageData.Amount, m_HealthAttribute.Value - m_HealthAttribute.MinValue); } var force = damageData.Direction * damageData.ForceMagnitude; if (damageData.ForceMagnitude > 0) { // Apply a force to the object. if (m_ForceObject != null) { m_ForceObject.AddForce(force, damageData.Frames); } else { // Apply a force to the rigidbody if the object isn't a character. if (m_Rigidbody != null && !m_Rigidbody.isKinematic) { if (damageData.Radius == 0) { m_Rigidbody.AddForceAtPosition(force * MathUtility.RigidbodyForceMultiplier, damageData.Position); } else { m_Rigidbody.AddExplosionForce(force.magnitude * MathUtility.RigidbodyForceMultiplier, damageData.Position, damageData.Radius); } } } } var attacker = damageData.DamageOriginator?.Owner; // Let other interested objects know that the object took damage. EventHandler.ExecuteEvent <float, Vector3, Vector3, GameObject, Collider>(m_GameObject, "OnHealthDamage", damageData.Amount, damageData.Position, force, attacker, damageData.HitCollider); EventHandler.ExecuteEvent <DamageData>(m_GameObject, "OnHealthDamageWithData", damageData); if (m_OnDamageEvent != null) { m_OnDamageEvent.Invoke(damageData.Amount, damageData.Position, force, attacker); } if (m_DamagePopupManager != null) { m_DamagePopupManager.OpenDamagePopup(damageData); } // The object is dead when there is no more health or shield. if (!IsAlive()) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { #endif Die(damageData.Position, force, attacker); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER } #endif } else { // Play any take damage audio if the object did not die. If the object died then the death audio will play. m_TakeDamageAudioClipSet.PlayAudioClip(m_GameObject); } }