/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Character.Abilities { using Opsive.Shared.Events; using Opsive.Shared.Game; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking; #endif using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Objects.CharacterAssist; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// Ability that uses the IDriveSource interface to drive a vehicle. /// [DefaultInputName("Action")] [DefaultState("Drive")] [DefaultStartType(AbilityStartType.ButtonDown)] [DefaultStopType(AbilityStopType.ButtonToggle)] [DefaultAllowPositionalInput(false)] [DefaultAllowRotationalInput(false)] [DefaultUseRootMotionPosition(AbilityBoolOverride.True)] [DefaultUseRootMotionRotation(AbilityBoolOverride.True)] [DefaultUseGravity(AbilityBoolOverride.False)] [DefaultDetectHorizontalCollisions(AbilityBoolOverride.False)] [DefaultDetectVerticalCollisions(AbilityBoolOverride.False)] [DefaultAbilityIndex(14)] [DefaultEquippedSlots(0)] public class Drive : DetectObjectAbilityBase { [Tooltip("Should the character teleport for the enter and exit animations?")] [SerializeField] protected bool m_TeleportEnterExit; [Tooltip("Can the Drive ability aim?")] [SerializeField] protected bool m_CanAim; [Tooltip("The speed at which the character moves towards the seat location.")] [SerializeField] protected float m_MoveSpeed = 0.2f; [Tooltip("The speed at which the character rotates towards the seat location.")] [SerializeField] protected float m_RotationSpeed = 2f; public bool TeleportEnterExit { get => m_TeleportEnterExit; set => m_TeleportEnterExit = value; } public bool CanAim { get => m_CanAim; set => m_CanAim = value; } public float MoveSpeed { get => m_MoveSpeed; set => m_MoveSpeed = value; } public float RotationSpeed { get => m_RotationSpeed; set => m_RotationSpeed = value; } /// /// Specifies the current status of the character. /// private enum DriveState { Enter, // The character is entering the vehicle. Drive, // The character is driving the vehicle. Exit, // The character is exiting the vehicle. ExitComplete // The character has exited the vehicle. } private IDriveSource m_DriveSource; private Transform m_OriginalParent; private Collider[] m_VehicleColliders; private DriveState m_DriveState; private Collider[] m_OverlapColliders; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER private INetworkInfo m_NetworkInfo; #endif private KinematicObjectManager.UpdateLocation m_StartUpdateLocation; private float m_Epsilon = 0.99999f; public override int AbilityIntData { get { return m_DriveSource.AnimatorID + (int)m_DriveState; } } public override float AbilityFloatData { get { return m_CharacterLocomotion.RawInputVector.x; } } /// /// Initializes the default values. /// public override void Awake() { base.Awake(); m_OverlapColliders = new Collider[1]; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER m_NetworkInfo = m_GameObject.GetCachedComponent(); #endif EventHandler.RegisterEvent(m_GameObject, "OnAnimatorEnteredVehicle", OnEnteredVehicle); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorExitedVehicle", OnExitedVehicle); } /// /// Validates the object to ensure it is valid for the current ability. /// /// The object being validated. /// The raycast hit of the detected object. Will be null for trigger detections. /// True if the object is valid. The object may not be valid if it doesn't have an ability-specific component attached. protected override bool ValidateObject(GameObject obj, RaycastHit? raycastHit) { if (!base.ValidateObject(obj, raycastHit)) { return false; } m_DriveSource = obj.GetCachedParentComponent(); if (m_DriveSource == null) { return false; } return true; } /// /// Called when the ablity is tried to be started. If false is returned then the ability will not be started. /// /// True if the ability can be started. public override bool CanStartAbility() { if (!base.CanStartAbility()) { return false; } return GetValidStartLocation(true) != null; } /// /// Returns a valid start location. /// /// Should the ground be checked at the start location? /// A valid start location (can be null). private MoveTowardsLocation GetValidStartLocation(bool groundCheck) { // At least one ability start location must be on the ground and not obstructed by any object. var startLocations = m_DriveSource.GameObject.GetComponentsInChildren(); for (int i = 0; i < startLocations.Length; ++i) { // The object must be on the ground. if (groundCheck && !Physics.Raycast(startLocations[i].transform.TransformPoint(0, 0.1f, 0), -startLocations[i].transform.up, 0.2f, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore)) { continue; } // If the start location has a collider then it should be clear of any other objects. var collider = startLocations[i].gameObject.GetCachedComponent(); if (collider == null || !ColliderOverlap(collider)) { return startLocations[i]; } } return null; } /// /// Is the collider overlapping with any other objects? /// /// The collider to determine if it is overlapping with another object. /// True if the collider is overlapping. private bool ColliderOverlap(Collider collider) { if (collider == null) { return true; } int hitCount; if (collider is CapsuleCollider) { Vector3 startEndCap, endEndCap; var capsuleCollider = collider as CapsuleCollider; MathUtility.CapsuleColliderEndCaps(capsuleCollider, collider.transform.TransformPoint(capsuleCollider.center), collider.transform.rotation, out startEndCap, out endEndCap); hitCount = Physics.OverlapCapsuleNonAlloc(startEndCap, endEndCap, capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider), m_OverlapColliders, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore); } else if (collider is BoxCollider) { var boxCollider = collider as BoxCollider; hitCount = Physics.OverlapBoxNonAlloc(collider.transform.TransformPoint(boxCollider.center), Vector3.Scale(boxCollider.size, boxCollider.transform.lossyScale) / 2, m_OverlapColliders, collider.transform.rotation, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore); } else { // SphereCollider. var sphereCollider = collider as SphereCollider; hitCount = Physics.OverlapSphereNonAlloc(collider.transform.TransformPoint(sphereCollider.center), sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider), m_OverlapColliders, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore); } // Any overlap occurs anytime there is more one collider intersecting the colliders. return hitCount > 0; } /// /// Returns the possible MoveTowardsLocations that the character can move towards. /// /// The possible MoveTowardsLocations that the character can move towards. public override MoveTowardsLocation[] GetMoveTowardsLocations() { if (m_TeleportEnterExit) { return null; } return m_DriveSource.GameObject.GetComponentsInChildren(); } /// /// The ability has started. /// protected override void AbilityStarted() { base.AbilityStarted(); m_OriginalParent = m_Transform.parent; m_VehicleColliders = m_DriveSource.GameObject.GetComponentsInChildren(); for (int i = 0; i < m_VehicleColliders.Length; ++i) { for (int j = 0; j < m_CharacterLocomotion.ColliderCount; ++j) { Physics.IgnoreCollision(m_VehicleColliders[i], m_CharacterLocomotion.Colliders[j], true); } } m_CharacterLocomotion.AddIgnoredColliders(m_VehicleColliders); m_CharacterLocomotion.AlignToGravity = true; m_StartUpdateLocation = m_CharacterLocomotion.UpdateLocation; // Used FixedUpdate so the root motion location is accurate when getting into the vehicle. m_CharacterLocomotion.UpdateLocation = KinematicObjectManager.UpdateLocation.FixedUpdate; m_CharacterLocomotion.SetPlatform(m_DriveSource.Transform); m_Transform.parent = m_DriveSource.Transform; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The IDriveSource is responsible for notifying the remote players for the changes. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_DriveState = DriveState.Enter; m_DriveSource.EnterVehicle(m_GameObject); // Teleport the character if there are no enter/exit animations. if (m_TeleportEnterExit) { OnEnteredVehicle(); m_CharacterLocomotion.InputVector = Vector2.zero; m_CharacterLocomotion.SetPositionAndRotation(m_DriveSource.DriverLocation.position, m_DriveSource.DriverLocation.rotation, true, false); } } /// /// Callback when the character has entered the vehicle. /// private void OnEnteredVehicle() { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The IDriveSource is responsible for notifying the remote players for the changes. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_DriveSource.EnteredVehicle(m_GameObject); m_DriveState = DriveState.Drive; m_CharacterLocomotion.ForceRootMotionRotation = false; m_CharacterLocomotion.ForceRootMotionPosition = false; m_CharacterLocomotion.AllowRootMotionRotation = false; m_CharacterLocomotion.AllowRootMotionPosition = false; m_CharacterLocomotion.UpdateLocation = m_StartUpdateLocation; m_CharacterLocomotion.UpdateAbilityAnimatorParameters(); } /// /// Called when another ability is attempting to start and the current ability is active. /// Returns true or false depending on if the new ability should be blocked from starting. /// /// The ability that is starting. /// True if the ability should be blocked. public override bool ShouldBlockAbilityStart(Ability startingAbility) { return m_AllowEquippedSlotsMask == 0 && startingAbility is Items.ItemAbility || (!m_CanAim && startingAbility is Items.Aim) || startingAbility is HeightChange; } /// /// Called when the current ability is attempting to start and another ability is active. /// Returns true or false depending on if the active ability should be stopped. /// /// The ability that is currently active. /// True if the ability should be stopped. public override bool ShouldStopActiveAbility(Ability activeAbility) { return m_AllowEquippedSlotsMask == 0 && activeAbility is Items.ItemAbility || (!m_CanAim && activeAbility is Items.Aim); } /// /// Updates the ability. /// public override void Update() { // Try to stop the ability after the character has exited. The ability won't be able to be stopped if the character isn't level with the gravity direction. if (m_DriveState == DriveState.ExitComplete && !m_TeleportEnterExit) { StopAbility(); } } /// /// Update the ability's Animator parameters. /// public override void UpdateAnimator() { // The horizontal input value can be used to animate the steering wheel. SetAbilityFloatDataParameter(m_CharacterLocomotion.RawInputVector.x, Time.deltaTime); } /// /// Update the controller's rotation values. /// public override void UpdateRotation() { var deltaRotation = Quaternion.identity; var rotation = m_Transform.rotation; if (m_DriveState != DriveState.Drive) { if (m_TeleportEnterExit) { return; } var upNormal = m_DriveState == DriveState.Enter ? m_DriveSource.Transform.up : -m_CharacterLocomotion.GravityDirection; // When the character is entering the vehicle they should rotate to face the same up direction as the car. This allows the character to enter while on slopes. // Similarly, when the character exits they should rotate to the gravity direction. var proj = (rotation * Vector3.forward) - Vector3.Dot(rotation * Vector3.forward, upNormal) * upNormal; if (proj.sqrMagnitude > 0.0001f) { var speed = m_RotationSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime * (m_DriveState == DriveState.ExitComplete ? 100 : 1); var targetRotation = Quaternion.Slerp(rotation, Quaternion.LookRotation(proj, upNormal), speed); deltaRotation = deltaRotation * (Quaternion.Inverse(rotation) * targetRotation); } } else if (m_DriveSource.DriverLocation != null) { // The character should fully rotate towards the target rotation after they have entered. deltaRotation = MathUtility.InverseTransformQuaternion(m_Transform.rotation, m_DriveSource.DriverLocation.rotation); } m_CharacterLocomotion.DeltaRotation = deltaRotation.eulerAngles; } /// /// Update the controller's position values. /// public override void UpdatePosition() { if (m_DriveState != DriveState.Drive || m_TeleportEnterExit || m_DriveSource.DriverLocation == null) { return; } m_CharacterLocomotion.MotorThrottle = Vector3.zero; var deltaPosition = Vector3.MoveTowards(m_Transform.position, m_DriveSource.DriverLocation.position, m_MoveSpeed) - m_Transform.position; m_CharacterLocomotion.AbilityMotor = deltaPosition / (m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * TimeUtility.FramerateDeltaTime); } /// /// Callback when the ability tries to be stopped. Start the dismount. /// public override void WillTryStopAbility() { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The IDriveSource is responsible for notifying the remote players for the changes. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif if (m_DriveState != DriveState.Drive) { return; } // The ability can't stop if there are no valid exit locations. MoveTowardsLocation startLocation; if ((startLocation = GetValidStartLocation(false)) == null) { return; } m_DriveSource.ExitVehicle(m_GameObject); m_DriveState = DriveState.Exit; m_CharacterLocomotion.AbilityMotor = Vector3.zero; m_CharacterLocomotion.ForceRootMotionRotation = true; m_CharacterLocomotion.ForceRootMotionPosition = true; m_CharacterLocomotion.AllowRootMotionRotation = true; m_CharacterLocomotion.AllowRootMotionPosition = true; m_CharacterLocomotion.UpdateAbilityAnimatorParameters(); // Teleport the character if there are no enter/exit animations. if (m_TeleportEnterExit) { OnExitedVehicle(); var forward = Vector3.ProjectOnPlane(startLocation.transform.forward, -m_CharacterLocomotion.GravityDirection); m_CharacterLocomotion.SetPositionAndRotation(startLocation.transform.position, Quaternion.LookRotation(forward, -m_CharacterLocomotion.GravityDirection), true, false); } } /// /// Callback when the character has exited the vehicle. /// private void OnExitedVehicle() { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The IDriveSource is responsible for notifying the remote players for the changes. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_DriveSource.ExitedVehicle(m_GameObject); m_DriveState = DriveState.ExitComplete; m_CharacterLocomotion.UpdateAbilityAnimatorParameters(); m_Transform.parent = m_OriginalParent; m_CharacterLocomotion.SetPlatform(null); m_CharacterLocomotion.AlignToGravity = false; m_CharacterLocomotion.ForceRootMotionRotation = false; } /// /// Can the ability be stopped? /// /// True if the ability can be stopped. public override bool CanStopAbility() { // The character has to be exited in order to stop. return m_DriveState == DriveState.ExitComplete && (m_TeleportEnterExit || Vector3.Dot(m_Transform.rotation * Vector3.up, -m_CharacterLocomotion.GravityDirection) >= m_Epsilon); } /// /// The ability has stopped running. /// /// Was the ability force stopped? protected override void AbilityStopped(bool force) { base.AbilityStopped(force); // If the drive state isn't exit complete then the ability was force stopped. if (m_DriveState != DriveState.ExitComplete) { m_DriveSource.ExitVehicle(m_GameObject); m_CharacterLocomotion.AbilityMotor = Vector3.zero; m_CharacterLocomotion.UpdateLocation = m_StartUpdateLocation; OnExitedVehicle(); } m_CharacterLocomotion.RemoveIgnoredColliders(m_VehicleColliders); for (int i = 0; i < m_VehicleColliders.Length; ++i) { for (int j = 0; j < m_CharacterLocomotion.ColliderCount; ++j) { Physics.IgnoreCollision(m_VehicleColliders[i], m_CharacterLocomotion.Colliders[j], false); } } } /// /// The character has been destroyed. /// public override void OnDestroy() { base.OnDestroy(); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorEnteredVehicle", OnEnteredVehicle); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorExitedVehicle", OnExitedVehicle); } } }