using UnityEngine; //using Physics = RotaryHeart.Lib.PhysicsExtension.Physics; using MalbersAnimations.Scriptables; using MalbersAnimations.Events; using System.Collections.Generic; using System.Linq; using System; #if UNITY_EDITOR using UnityEditor; #endif namespace MalbersAnimations.Utilities { [DefaultExecutionOrder(10000)] [AddComponentMenu("Malbers/Utilities/Aiming/Aim")] public class Aim : MonoBehaviour, IAim, IAnimatorListener { #region Vars and Props #region Public Variables [SerializeField, Tooltip("Is the Aim Active")] private BoolReference m_active = new BoolReference(true); [SerializeField, Tooltip("Aim Origin Reference"), ContextMenuItem("Head as AimOrigin", "HeadAimOrigin")] private Transform m_aimOrigin; [SerializeField, Tooltip("Smoothness Lerp value to change from Active to Disable")] private float m_Smoothness = 10f; [SerializeField, Tooltip("Layers inlcuded on the Aiming Logic")] private LayerReference m_aimLayer = new LayerReference(-1); [SerializeField, Tooltip("Does the Aiming Logic ignore Colliders??")] private QueryTriggerInteraction m_Triggers = QueryTriggerInteraction.Ignore; [SerializeField, Tooltip("Forced a Target on the Aiming Logic. Calculate the Aim from the Aim Origin to a Target")] private TransformReference m_AimTarget = new TransformReference(); [Tooltip("Transform Helper that stores the position of the Hit")] public TransformReference m_AimPosition = new TransformReference(); [SerializeField, Tooltip("Set a Transform Hierarchy to Ignore on the Aim Ray")] private TransformReference m_Ignore = new TransformReference(); [SerializeField, Tooltip("Camera Reference used for calculatin the Aim logic from the Camera Center. By Default will use the Camera.Main Transform")] private TransformReference m_camera = new TransformReference(); private Camera cam; [SerializeField, Tooltip("Do the raycast every X Cycles to increase performance")] [Min(1)] private int m_cycles = 1; private int CurrentCycles; [SerializeField, Tooltip("Default screen center")] private Vector2Reference m_screenCenter = new Vector2Reference(0.5f, 0.5f); [SerializeField] [Tooltip("This Parameter is used to Change the Current Camera to the Side of which the Character is relative to Camera or the Target")] private AimSide m_AimSide = 0; [Tooltip("Update mode for the Aim Logic")] public UpdateType updateMode = UpdateType.LateUpdate; /// Maximun Distance for the Camera Ray [Tooltip("Maximun Distance from the Origin to the Possible Target")] public float MaxDistance = 100f; [Tooltip("If you set a new Target usually the Pivot is on the base of the Transform. Enable this to find the Center of the Mesh Renderer")] public BoolReference m_UseTargetCenter = new BoolReference(false); public Renderer TargetRenderer { get; private set; } /// Find the Target Center public Vector3 TargetCenter => TargetRenderer != null ? TargetRenderer.bounds.center : AimTarget.position; [SerializeField, Tooltip("Use Raycasting for finding the Hit Point. Disable this if you don't need to know which was the object hitted.")] private BoolReference m_UseRaycasting = new BoolReference(true); /// Radius for the Sphere Casting, if this is set to Zero they I will use a Ray Casting [Tooltip("Radius for the Sphere Casting, if this is set to Zero they I will use a Ray Casting")] public FloatReference rayRadius = new FloatReference(0.0f); /// Ray Counts for the Ray casting [Tooltip("Maximum Ray Hits for the Ray casting")] public int RayHits = 5; public TransformEvent OnAimRayTarget = new TransformEvent(); public Vector3Event OnScreenCenter = new Vector3Event(); public IntEvent OnAimSide = new IntEvent(); public BoolEvent OnAiming = new BoolEvent(); public BoolEvent OnUsingTarget = new BoolEvent(); public bool debug; private string hitName; private int hitcount; #endregion #region Properties /// Main Camera public Transform MainCamera { get => m_camera.Value; set => m_camera.Value = value; } /// Use Raycasting for finding the Hit Point public bool UseRaycasting { get => m_UseRaycasting.Value; set => m_UseRaycasting.Value = value; } /// Sets the Aim Origin Transform public Transform AimOrigin { get => m_aimOrigin; set { if (value) m_aimOrigin = value; else m_aimOrigin = defaultOrigin; } } private Transform defaultOrigin; /// Set a Extra Transform to Ignore it (Used in case of the Mount for the Rider) public Transform IgnoreTransform { get => m_Ignore.Value; set => m_Ignore.Value = value; } /// Direction the GameObject is Aiming (Smoothed) public Vector3 AimDirection => AimOrigin.DirectionTo(AimPoint); /// Raw Direction the GameObject is Aiming public Vector3 RawAimDirection { get; private set; } /// is the Current AimTarget a Target Assist? public bool IsTargetAssist { get; private set; } /// Smooth Aim Point the ray is Aiming public Vector3 AimPoint { get; private set; } /// RAW Aim Point the ray is Aiming public Vector3 RawPoint { get; private set; } public float HorizontalAngle { get; private set; } public float VerticalAngle { get; private set; } /// Default Screen Center public Vector3 ScreenCenter { get; private set; } public IAimTarget LastAimTarget; /// Is the Aiming Logic Active? public bool Active { get => m_active; set { m_active.Value = enabled = value; if (value) EnterAim(); else ExitAim(); } } public void EnterAim() { if (AimSide == AimSide.Left) OnAimSide.Invoke(-1); else if (AimSide == AimSide.Right) OnAimSide.Invoke(1); CalculateAiming(); OnAiming.Invoke(true); if (AimPosition) AimPosition.gameObject.SetActive(true); //Hide the Helper } public void ExitAim() { OnScreenCenter.Invoke(ScreenCenter); OnAimRayTarget.Invoke(null); AimSide = AimSide.None; OnAimSide.Invoke(0); OnAiming.Invoke(false); if (AimPosition) AimPosition.gameObject.SetActive(false); //Hide the Helper } /// Limit the Aiming via Angle limit Which means the Aiming is Active but should not be used public bool Limited { get; set; } /// Last Raycast stored for calculating the Aim private RaycastHit aimHit; /// Last Raycast stored for calculating the Aim public RaycastHit AimHit => aimHit; private Transform m_AimTargetAssist; /// Target Transform Stored from the AimRay public Transform AimRayTargetAssist { get => m_AimTargetAssist; set { if (m_AimTargetAssist != value) { m_AimTargetAssist = value; OnAimRayTarget.Invoke(value); } } } /// Check if the camera is in the right:true or Left: False side of the Character public bool AimingSide { get; private set; } /// Forced Target on the Aiming Logic public Transform AimTarget { get => m_AimTarget.Value; set { if (m_AimTarget.Value != value) //Only execute the logic when the values are different { m_AimTarget.Value = value; if (value && m_UseTargetCenter.Value) { TargetRenderer = AimTarget.GetComponentInChildren(); //Find the Center of the First SkinMeshRenderer if (TargetRenderer == null) TargetRenderer = AimTarget.GetComponentInChildren();//Find the Center of the First MeshRenderer if the first one fails } OnUsingTarget.Invoke(value != null); OnAimRayTarget.Invoke(value); } } } /// Tranform Helper use to Ping the Hit Point public Transform AimPosition { get => m_AimPosition.Value; set => m_AimPosition.Value = value; } /// Layer to Aim and Hit public LayerMask Layer { get => m_aimLayer.Value; set => m_aimLayer.Value = value; } public QueryTriggerInteraction TriggerInteraction { get => m_Triggers; set => m_Triggers = value; } public AimSide AimSide { get => m_AimSide; set => m_AimSide = value; } public RaycastHit[] ArrayHits { get; private set; } #endregion #endregion public int EditorTab1 = 1; void Awake() { if (MainCamera == null) { cam = MTools.FindMainCamera(); if (cam) MainCamera = cam.transform; } else { cam = MainCamera.GetComponent(); } if (AimOrigin) defaultOrigin = AimOrigin; else AimOrigin = defaultOrigin = transform; GetCenterScreen(); CurrentCycles = UnityEngine.Random.Range(0, 999999); } void OnEnable() { CalculateAiming(); AimTarget = m_AimTarget.Value; //Call the Events on the Aim Target } private void FixedUpdate() { if (updateMode == UpdateType.FixedUpdate) { CurrentCycles++; var UseRay = UseRaycasting && (CurrentCycles % m_cycles == 0); if (UseRay) CurrentCycles = 0; AimLogic(UseRay); SmoothValues(Time.fixedDeltaTime); } } private void LateUpdate() { if (updateMode == UpdateType.LateUpdate) { CurrentCycles++; var UseRay = UseRaycasting && (CurrentCycles % m_cycles == 0); if (UseRay) CurrentCycles = 0; AimLogic(UseRay); SmoothValues(Time.deltaTime); } } public virtual void AimLogic(bool useRaycasting) { if (AimTarget) { aimHit = DirectionFromTarget(useRaycasting); RawPoint = UseRaycasting ? aimHit.point : TargetCenter; } else { if (MainCamera) { aimHit = DirectionFromCamera(useRaycasting); RawPoint = aimHit.point; } } CalculateAngles(); } /// Calculate the Aiming Direction with smoothing public void CalculateAiming() { if (Active) { AimLogic(UseRaycasting); SmoothValues(0); } } public void Active_Set(bool value) => Active = value; public void Active_Toggle() => Active ^= true; public void SetTarget(Transform target) => AimTarget = target; public void SetTarget(Component target) => SetTarget(target.transform); public void SetTarget(GameObject target) => SetTarget(target.transform); public void ClearTarget() => AimTarget = null; /// Calculates the Camera/Target Horizontal Angle Normalized public void CalculateAngles() { AimingSide = Vector3.Dot((AimOrigin.position - AimPoint), transform.right) > 0; //Calculate the side of the Origin according to the Aiming Position Vector3 HorizontalDir = Vector3.ProjectOnPlane(AimDirection, Vector3.up); Vector3 ForwardDir = Vector3.ProjectOnPlane(transform.forward, Vector3.up); HorizontalAngle = Vector3.Angle(HorizontalDir, ForwardDir) * (AimingSide ? 1 : -1); //Get the Normalized value for the look direction VerticalAngle = (Vector3.Angle(transform.up, AimDirection) - 90); //Get the Normalized value for the look direction } void SmoothValues(float time) { float Smoothlerp = time * m_Smoothness; Smoothlerp = Mathf.Sin(Smoothlerp * Mathf.PI * 0.5f); //don't remember why Smooth In Out the Time var isRaw = m_Smoothness == 0 || time == 0; AimPoint = isRaw ? RawPoint : Vector3.Lerp(AimPoint, RawPoint, Smoothlerp); if (AimPosition != null) //Helper for the Aim Position { AimPosition.position = AimPoint; AimPosition.up = isRaw ? aimHit.normal : Vector3.Lerp(AimPosition.up, aimHit.normal, Smoothlerp); } } private void GetCenterScreen() { var SC = new Vector3(Screen.width * m_screenCenter.Value.x, Screen.height * m_screenCenter.Value.y); //Gets the Center of the Aim Dot Transform if (SC != ScreenCenter) { ScreenCenter = SC; OnScreenCenter.Invoke(ScreenCenter); } } public RaycastHit DirectionFromCamera(bool useray) { GetCenterScreen(); RawAimDirection = cam.transform.forward; Ray CamRay = cam.ScreenPointToRay(ScreenCenter); var hit = new RaycastHit(); if (this.UseRaycasting && !useray) { hit.distance = AimHit.distance; hit.point = CamRay.GetPoint(AimHit.distance); } else { hit.distance = MaxDistance; hit.point = CamRay.GetPoint(MaxDistance); } if (useray) { ArrayHits = new RaycastHit[RayHits]; if (rayRadius > 0) hitcount = Physics.SphereCastNonAlloc(CamRay, rayRadius, ArrayHits, MaxDistance, Layer, m_Triggers); else hitcount = Physics.RaycastNonAlloc(CamRay, ArrayHits, MaxDistance, Layer, m_Triggers); if (hitcount > 0) { foreach (RaycastHit rHit in ArrayHits) { if (rHit.transform == null) break; //Means nothing was found if (rHit.transform.root == IgnoreTransform) continue; //Dont Hit anything the Ignore if (rHit.transform.root == AimOrigin.root) continue; //Dont Hit anything in this hierarchy // if (Vector3.Distance(MainCamera.position, rHit.point) < Vector3.Distance(MainCamera.position, AimOrigin.position)) continue; //If I hit something behind the camera skip if (hit.distance > rHit.distance) hit = rHit; } } return GetAimAssist(hit); } return (hit); } public RaycastHit DirectionFromTarget(bool UseRaycasting) { var TargetCenter = this.TargetCenter; //Cache the Center RawAimDirection = AimOrigin.DirectionTo(TargetCenter); Ray ray = new Ray(AimOrigin.position, RawAimDirection); var hit = new RaycastHit() { distance = MaxDistance, point = TargetCenter, }; if (UseRaycasting) { ArrayHits = new RaycastHit[RayHits]; if (rayRadius > 0) hitcount = Physics.SphereCastNonAlloc(ray, rayRadius, ArrayHits, MaxDistance, Layer, m_Triggers); else hitcount = Physics.RaycastNonAlloc(ray, ArrayHits, MaxDistance, Layer, m_Triggers); if (hitcount > 0) { foreach (RaycastHit rHit in ArrayHits) { if (rHit.transform == null) break; //Means nothing was found if (rHit.transform.root == IgnoreTransform) continue; //Dont Hit anything the Ignore if (rHit.transform.root == AimOrigin.root) continue; //Dont Hit anything in this hierarchy if (hit.distance > rHit.distance) hit = rHit; } } return GetAimAssist(hit); } return hit; } /// Find if the Transform Hit with the RayCast is an AimAssist /// /// private RaycastHit GetAimAssist(RaycastHit hit) { #if UNITY_EDITOR hitName = hit.collider ? hit.collider.name : string.Empty; //For debbuging purposes #endif IAimTarget IAimTarg = hit.collider != null ? hit.collider.GetComponent() : null; IsTargetAssist = false; if (IAimTarg != null) { if (IAimTarg.AimAssist) { IsTargetAssist = true; AimRayTargetAssist = IAimTarg.AimPoint; hit.point = IAimTarg.AimPoint.position; } if (IAimTarg != LastAimTarget) { LastAimTarget = IAimTarg; LastAimTarget.IsBeenAimed(true, gameObject); } } else { LastAimTarget?.IsBeenAimed(false, gameObject); LastAimTarget = null; AimRayTargetAssist = null; } return hit; } /// This is used to listen the Animator asociated to this gameObject public virtual bool OnAnimatorBehaviourMessage(string message, object value) => this.InvokeWithParams(message, value); private void HeadAimOrigin() { var anim = transform.root.GetComponentInChildren(); if (anim) { if (anim.isHuman) { var head = anim.GetBoneTransform(HumanBodyBones.Head); if (head) AimOrigin = head; } else { AimOrigin = anim.transform.FindGrandChild("Head"); } } MTools.SetDirty(this); } #if UNITY_EDITOR void Reset() { if (MainCamera == null) { cam = MTools.FindMainCamera(); MainCamera = cam.transform; } else { cam = MainCamera.GetComponent(); } AimOrigin = defaultOrigin = transform; } void OnDrawGizmos() { if (debug && Application.isPlaying && enabled) { if (!Limited && AimOrigin) { float radius = 0.05f; Gizmos.color = Color.green; Gizmos.DrawWireSphere(AimPoint, radius); Gizmos.DrawSphere(AimPoint, radius); Gizmos.DrawLine(AimOrigin.position, AimPoint); Gizmos.color = Color.black; Gizmos.DrawLine(AimOrigin.position, RawPoint); if (UseRaycasting) { GUIStyle style = new GUIStyle(UnityEditor.EditorStyles.label) { fontStyle = FontStyle.Bold, fontSize = 10 }; style.normal.textColor = Color.green; UnityEditor.Handles.Label(AimPoint, hitName, style); } } } } #endif } #region Inspector #if UNITY_EDITOR [CanEditMultipleObjects, CustomEditor(typeof(Aim))] public class AimEditor : Editor { Aim m; SerializedProperty m_active, m_aimOrigin, m_Smoothness, m_aimLayer, m_Triggers, m_AimTarget, m_AimPosition, m_AimSide, debug, m_UpdateMode, OnAiming, m_cycles, m_Ignore, m_camera, m_UseTargetCenter, m_screenCenter, rayRadius, RayHits, OnAimRayTarget, OnUsingTarget, OnScreenCenter, OnAimSide, EditorTab1, MaxDistance, m_UseRaycasting; private void OnEnable() { m = (Aim)target; m_active = serializedObject.FindProperty("m_active"); m_Smoothness = serializedObject.FindProperty("m_Smoothness"); m_aimOrigin = serializedObject.FindProperty("m_aimOrigin"); m_UseTargetCenter = serializedObject.FindProperty("m_UseTargetCenter"); m_aimLayer = serializedObject.FindProperty("m_aimLayer"); m_Triggers = serializedObject.FindProperty("m_Triggers"); m_AimTarget = serializedObject.FindProperty("m_AimTarget"); m_AimPosition = serializedObject.FindProperty("m_AimPosition"); m_Ignore = serializedObject.FindProperty("m_Ignore"); m_camera = serializedObject.FindProperty("m_camera"); m_screenCenter = serializedObject.FindProperty("m_screenCenter"); m_AimSide = serializedObject.FindProperty("m_AimSide"); rayRadius = serializedObject.FindProperty("rayRadius"); MaxDistance = serializedObject.FindProperty("MaxDistance"); RayHits = serializedObject.FindProperty("RayHits"); EditorTab1 = serializedObject.FindProperty("EditorTab1"); debug = serializedObject.FindProperty("debug"); m_UpdateMode = serializedObject.FindProperty("updateMode"); OnAimRayTarget = serializedObject.FindProperty("OnAimRayTarget"); OnUsingTarget = serializedObject.FindProperty("OnUsingTarget"); OnScreenCenter = serializedObject.FindProperty("OnScreenCenter"); OnAimSide = serializedObject.FindProperty("OnAimSide"); OnAiming = serializedObject.FindProperty("OnAiming"); m_cycles = serializedObject.FindProperty("m_cycles"); m_UseRaycasting = serializedObject.FindProperty("m_UseRaycasting"); } public override void OnInspectorGUI() { serializedObject.Update(); MalbersEditor.DrawDescription("Aim Logic. Uses raycasting to find object when the character is aiming with the camera or towards a target"); EditorTab1.intValue = GUILayout.Toolbar(EditorTab1.intValue, new string[] { "General", "References", "Events" }); if (Application.isPlaying) { EditorGUILayout.BeginVertical(m.AimTarget != null ? MTools.StyleOrange : MTools.StyleGreen); EditorGUILayout.HelpBox("Using " + (m.AimTarget != null ? "a Target" : "the Camera"), MessageType.Info); EditorGUILayout.EndVertical(); } //First Tabs int Selection = EditorTab1.intValue; if (Selection == 0) ShowGeneral(); else if (Selection == 1) ShowReferences(); else if (Selection == 2) ShowEvents(); serializedObject.ApplyModifiedProperties(); } private void ShowGeneral() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(m_active); if (EditorGUI.EndChangeCheck()) { m.enabled = m.Active; EditorUtility.SetDirty(target); } MalbersEditor.DrawDebugIcon(debug); //var currentGUIColor = GUI.color; //GUI.color = debug.boolValue ? Color.red : currentGUIColor; //debug.boolValue = GUILayout.Toggle(debug.boolValue, "Debug", EditorStyles.miniButton, GUILayout.Width(50)); //GUI.color = currentGUIColor; EditorGUILayout.EndHorizontal(); EditorGUILayout.PropertyField(m_UpdateMode); EditorGUILayout.PropertyField(m_AimSide); EditorGUILayout.PropertyField(MaxDistance); EditorGUILayout.PropertyField(m_Smoothness); EditorGUILayout.PropertyField(m_aimOrigin); if (m_aimOrigin.objectReferenceValue == null) EditorGUILayout.HelpBox("Please Select an Aim Origin Reference", MessageType.Error); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // EditorGUILayout.LabelField("Ray Casting", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_UseRaycasting); if (m.UseRaycasting) { EditorGUILayout.PropertyField(rayRadius); EditorGUILayout.PropertyField(RayHits); EditorGUILayout.PropertyField(m_cycles); } EditorGUILayout.EndVertical(); if (m.UseRaycasting) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Layer Interaction", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_aimLayer, new GUIContent("Layer")); EditorGUILayout.PropertyField(m_Triggers); EditorGUILayout.EndVertical(); } if (Application.isPlaying && debug.boolValue) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.LabelField("Debug", EditorStyles.boldLabel); EditorGUILayout.LabelField("Camera Side: " + (m.AimingSide ? "Right" : "Left")); EditorGUILayout.FloatField("Vertical Angle", m.VerticalAngle); EditorGUILayout.FloatField("Horizontal Angle", m.HorizontalAngle); EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); } } private void ShowReferences() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); // EditorGUILayout.LabelField("Target", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_AimTarget, new GUIContent("Target")); EditorGUILayout.PropertyField(m_UseTargetCenter); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // EditorGUILayout.LabelField("Camera", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_camera); EditorGUILayout.PropertyField(m_screenCenter); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Extras", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_Ignore); EditorGUILayout.PropertyField(m_AimPosition); EditorGUILayout.EndVertical(); } private void ShowEvents() { EditorGUILayout.PropertyField(OnAiming); EditorGUILayout.PropertyField(OnAimRayTarget); EditorGUILayout.PropertyField(OnUsingTarget); EditorGUILayout.PropertyField(OnScreenCenter); EditorGUILayout.PropertyField(OnAimSide); } } #endif #endregion }