/// --------------------------------------------- /// 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.Traits; using Opsive.UltimateCharacterController.Objects.CharacterAssist; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// Interacts with another object within the scene. The object that the ability interacts with must have the Interact component added to it. /// [DefaultStartType(AbilityStartType.ButtonDown)] [DefaultInputName("Action")] [DefaultAbilityIndex(9)] [DefaultAllowPositionalInput(false)] [DefaultAllowRotationalInput(false)] [AllowDuplicateTypes] public class Interact : DetectObjectAbilityBase { [Tooltip("The ID of the Interactable. A value of -1 indicates no ID.")] [SerializeField] protected int m_InteractableID = -1; [Tooltip("Can the Height Change ability stay active while interacting?")] [SerializeField] protected bool m_AllowActiveHeightChange; [Tooltip("The value of the AbilityIntData animator parameter.")] [SerializeField] protected int m_AbilityIntDataValue; [Tooltip("Specifies if the ability should wait for the OnAnimatorInteract animation event or wait for the specified duration before interacting with the item.")] [SerializeField] protected AnimationEventTrigger m_InteractEvent = new AnimationEventTrigger(false, 0.2f); [Tooltip("Specifies if the ability should wait for the OnAnimatorInteractComplete animation event or wait for the specified duration before stopping the ability.")] [SerializeField] protected AnimationEventTrigger m_InteractCompleteEvent = new AnimationEventTrigger(false, 0.2f); public int InteractableID { get { return m_InteractableID; } set { m_InteractableID = value; } } public bool AllowActiveHeightChange { get { return m_AllowActiveHeightChange; } set { m_AllowActiveHeightChange = value; } } public int AbilityIntDataValue { get { return m_AbilityIntDataValue; } set { m_AbilityIntDataValue = value; } } public AnimationEventTrigger InteractEvent { get { return m_InteractEvent; } set { m_InteractEvent = value; } } public AnimationEventTrigger InteractCompleteEvent { get { return m_InteractCompleteEvent; } set { m_InteractCompleteEvent = value; } } private CharacterIKBase m_CharacterIK; protected Interactable m_Interactable; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER private INetworkInfo m_NetworkInfo; #endif private ScheduledEventBase[] m_DisableIKInteractionEvents; private bool m_HasInteracted; private bool m_ExitedTrigger; public override int AbilityIntData { get { return m_AbilityIntDataValue; } } public override string AbilityMessageText { get { var message = m_AbilityMessageText; if (m_Interactable != null) { message = string.Format(message, m_Interactable.AbilityMessage()); } return message; } set { base.AbilityMessageText = value; } } #if UNITY_EDITOR public override string AbilityDescription { get { if (m_InteractableID != -1) { return "Interactable " + m_InteractableID; } return string.Empty; } } #endif /// /// Initialize the default values. /// public override void Awake() { base.Awake(); m_CharacterIK = m_GameObject.GetCachedComponent(); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER m_NetworkInfo = m_GameObject.GetCachedComponent(); #endif EventHandler.RegisterEvent(m_GameObject, "OnAnimatorInteract", DoInteract); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorInteractComplete", InteractComplete); } /// /// 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() { // The base class may prevent the ability from starting. if (!base.CanStartAbility()) { m_Interactable = null; return false; } // The ability can't start if the Interactable isn't ready. if (!m_Interactable.CanInteract(m_GameObject)) { return false; } return true; } /// /// Returns the possible MoveTowardsLocations that the character can move towards. /// /// The possible MoveTowardsLocations that the character can move towards. public override MoveTowardsLocation[] GetMoveTowardsLocations() { return m_Interactable.gameObject.GetCachedComponents(); } /// /// 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; } if (m_Interactable != null && raycastHit.HasValue) { return obj == m_Interactable.gameObject || obj.transform.IsChildOf(m_Interactable.transform); } // The object must have the Interactable component. var interactable = obj.GetCachedParentComponent(); if (interactable != null) { // If the ID is used then the IDs must match. if (m_InteractableID != -1 && interactable.ID != m_InteractableID) { return false; } // Interactable will not be null if coming from a trigger. if (m_Interactable == null) { m_Interactable = interactable; } return true; } return false; } /// /// 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 (startingAbility is Items.ItemAbility) || startingAbility.Index > Index || startingAbility is StoredInputAbilityBase; } /// /// 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_AllowActiveHeightChange && activeAbility is HeightChange) || activeAbility is StoredInputAbilityBase; } /// /// The ability has started. /// protected override void AbilityStarted() { base.AbilityStarted(); m_HasInteracted = false; m_ExitedTrigger = false; if (!m_InteractEvent.WaitForAnimationEvent) { Scheduler.ScheduleFixed(m_InteractEvent.Duration, DoInteract); } // The interactable can move the limbs to a specific location. if (m_CharacterIK != null) { for (int i = 0; i < m_Interactable.IKTargets.Length; ++i) { var ikTarget = m_Interactable.IKTargets[i]; if (ikTarget.Goal != CharacterIKBase.IKGoal.Last) { Scheduler.ScheduleFixed(ikTarget.Delay, SetIKTarget, ikTarget, ikTarget.Transform); } } } } /// /// Sets the IK target. /// /// The IK target that should be set. /// The transform target that should be set. private void SetIKTarget(AbilityIKTarget ikTarget, Transform targetTransform) { m_CharacterIK.SetAbilityIKTarget(targetTransform, ikTarget.Goal, ikTarget.InterpolationDuration); // If the transform is not null then the end should be scheduled so it can be set to null. if (targetTransform != null) { if (m_DisableIKInteractionEvents == null) { m_DisableIKInteractionEvents = new ScheduledEventBase[m_Interactable.IKTargets.Length]; } else if (m_DisableIKInteractionEvents.Length < m_Interactable.IKTargets.Length) { System.Array.Resize(ref m_DisableIKInteractionEvents, m_Interactable.IKTargets.Length); } for (int i = 0; i < m_DisableIKInteractionEvents.Length; ++i) { if (m_DisableIKInteractionEvents[i] == null) { m_DisableIKInteractionEvents[i] = Scheduler.ScheduleFixed(ikTarget.Duration, (AbilityIKTarget abilityIKTarget, Transform target) => { SetIKTarget(ikTarget, target); m_DisableIKInteractionEvents[i] = null; }, ikTarget, null); break; } } } } /// /// Interacts with the object. /// private void DoInteract() { if (!IsActive || m_HasInteracted) { return; } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The Interact event will be sent through a message. The ability does not need to call the interaction. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_Interactable.Interact(m_GameObject); m_HasInteracted = true; if (!m_InteractCompleteEvent.WaitForAnimationEvent) { Scheduler.ScheduleFixed(m_InteractCompleteEvent.Duration, InteractComplete); } } /// /// Completes the ability. /// private void InteractComplete() { if (!IsActive) { return; } StopAbility(); } /// /// The character has exited a trigger. /// /// The GameObject that the character exited. /// Returns true if the entered object leaves the trigger. protected override bool TriggerExit(GameObject other) { if (IsActive) { m_ExitedTrigger = true; return false; } if (base.TriggerExit(other)) { // The character may have been in multiple triggers. if (m_DetectedObject == null) { m_Interactable = null; } else { m_Interactable = m_DetectedObject.GetCachedParentComponent(); } return true; } return false; } /// /// The ability has stopped running. /// /// Was the ability force stopped? protected override void AbilityStopped(bool force) { base.AbilityStopped(force); if (m_ExitedTrigger) { m_Interactable = null; m_DetectedTriggerObjectsCount = 0; m_DetectedObject = null; m_ExitedTrigger = false; } // The ability may end before the interaction duration has elapsed. if (m_DisableIKInteractionEvents != null) { for (int i = 0; i < m_DisableIKInteractionEvents.Length; ++i) { if (m_DisableIKInteractionEvents[i] == null) { continue; } m_DisableIKInteractionEvents[i].Invoke(); Scheduler.Cancel(m_DisableIKInteractionEvents[i]); m_DisableIKInteractionEvents[i] = null; } } } /// /// The object has been destroyed. /// public override void OnDestroy() { base.OnDestroy(); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorInteract", DoInteract); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorInteractComplete", InteractComplete); } } }