504 lines
21 KiB
C#
504 lines
21 KiB
C#
using Invector.vCharacterController.AI;
|
|
using Invector.vEventSystems;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Invector
|
|
{
|
|
[vClassHeader("AI HEADTRACK", helpBoxText = "If the bone hips don't have the same orientation of the character,\n you can add a custom hips to override the original (Transforms)", useHelpBox = true)]
|
|
public class vAIHeadtrack : vMonoBehaviour
|
|
{
|
|
#region Public Variables
|
|
[vEditorToolbar("Settings")]
|
|
public bool canLook = true;
|
|
public bool _freezeLookPoint = false;
|
|
[vHelpBox("Check this option to continue to look and ignore the limitAngle. ex: something is pursuing you")]
|
|
public bool keepLookingOutAngle = true;
|
|
[Range(0, 1)]
|
|
public float strafeHeadWeight = 0.8f;
|
|
[Range(0, 1)]
|
|
public float strafeBodyWeight = 0.8f;
|
|
[Range(0, 1)]
|
|
public float freeHeadWeight = 1f;
|
|
[Range(0, 1)]
|
|
public float freeBodyWeight = 0.4f;
|
|
[vMinMax(minLimit = -180, maxLimit = 180)]
|
|
public Vector2 limitAngleX = new Vector2(-90, 90);
|
|
[vMinMax(minLimit = -90, maxLimit = 90)]
|
|
public Vector2 limitAngleY = new Vector2(-90, 90);
|
|
[Tooltip("Apply offset Y to look point")]
|
|
public float defaultOffSetLookHeight = 1.5f;
|
|
[SerializeField, vReadOnly] protected float currentOffSetLookHeight;
|
|
public float smooth = 12f;
|
|
[vHelpBox("Add a AnimatorTag here to ignore the Headtrack and play the animation instead")]
|
|
public List<string> animatorTags = new List<string>() { "Attack", "LockMovement", "CustomAction", "IgnoreHeadtrack" };
|
|
public Vector2 offsetSpine, offsetHead;
|
|
public Transform mainLookTarget;
|
|
public Transform eyes;
|
|
public float timeToExitLookPoint = 1;
|
|
public float timeToExitLookTarget = 1;
|
|
|
|
[vHelpBox("Use it with the FSM Action LookAround to simulate an look around animation")]
|
|
[SerializeField] protected float lookAroundAngle = 60f;
|
|
[SerializeField] protected AnimationCurve lookAroundCurve;
|
|
[SerializeField] protected float lookAroundSpeed = 0.1f;
|
|
|
|
[vEditorToolbar("Transforms")]
|
|
[Tooltip("If the bone hips don't have the same orientation of the character, you can add a custom hips to override the original (Transforms)")]
|
|
public Transform hips;
|
|
[Header("Just for Debug")]
|
|
public Transform head;
|
|
public List<Transform> spine;
|
|
public Vector3 currentLookPoint { get; set; }
|
|
public Vector3 currentLookDirection { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Private Variables
|
|
private vIControlAI character;
|
|
private Animator animator;
|
|
private Transform temporaryLookTarget;
|
|
private Vector3 temporaryLookPoint;
|
|
private Vector3 targetLookPoint;
|
|
private bool inLockPoint;
|
|
private bool inLockTarget;
|
|
private bool isInSmoothValues;
|
|
private bool updateIK;
|
|
private float targetOffsetHeight;
|
|
private float exitLookPointTime;
|
|
private float exitLookTargetTime;
|
|
private float headHeight;
|
|
private float yAngle, xAngle;
|
|
private float _yAngle, _xAngle;
|
|
private float yRotation, xRotation;
|
|
private float _currentHeadWeight, _currentbodyWeight;
|
|
private float lookAroundProgress;
|
|
private vIAnimatorStateInfoController animatorStateInfos;
|
|
#endregion
|
|
|
|
[vEditorToolbar("Events")]
|
|
public UnityEngine.Events.UnityEvent onPreUpdateSpineIK, onPosUpdateSpineIK;
|
|
|
|
#region PROTECTED VIRTUAL METHODS
|
|
|
|
#region UNITY METHODS
|
|
|
|
protected virtual void Start()
|
|
{
|
|
character = GetComponent<vIControlAI>();
|
|
animator = GetComponent<Animator>();
|
|
animatorStateInfos = GetComponent<vIAnimatorStateInfoController>();
|
|
if (animator.isHuman)
|
|
{
|
|
head = animator.GetBoneTransform(HumanBodyBones.Head);
|
|
var spine1 = animator.GetBoneTransform(HumanBodyBones.Spine);
|
|
var spine2 = animator.GetBoneTransform(HumanBodyBones.Chest);
|
|
spine = new List<Transform>();
|
|
if (spine1)
|
|
spine.Add(spine1);
|
|
if (spine2)
|
|
spine.Add(spine2);
|
|
var neck = animator.GetBoneTransform(HumanBodyBones.Neck);
|
|
if (!hips)
|
|
hips = animator.GetBoneTransform(HumanBodyBones.Hips);
|
|
if (neck && spine2 && neck.parent && neck.parent != spine2)
|
|
spine.Add(neck.parent);
|
|
}
|
|
|
|
if (head)
|
|
headHeight = Vector3.Distance(transform.position, head.position);
|
|
|
|
ResetOffseLookHeight();
|
|
currentLookPosition = GetLookPoint();
|
|
_lastLocalLookPosition = _currentLocalLookPosition;
|
|
lookAroundProgress = 0.5f;
|
|
}
|
|
|
|
protected virtual void FixedUpdate()
|
|
{
|
|
updateIK = true;
|
|
}
|
|
|
|
protected Vector3 _currentLocalLookPosition;
|
|
protected Vector3 _lastLocalLookPosition;
|
|
|
|
public virtual bool freezeLookPoint { get => _freezeLookPoint; set => _freezeLookPoint = value; }
|
|
|
|
public virtual Vector3 currentLookPosition
|
|
{
|
|
get => freezeLookPoint ? transform.TransformPoint(_lastLocalLookPosition) : transform.TransformPoint(_currentLocalLookPosition);
|
|
protected set
|
|
{
|
|
_currentLocalLookPosition = transform.InverseTransformPoint(value);
|
|
if (!freezeLookPoint) _lastLocalLookPosition = _currentLocalLookPosition;
|
|
}
|
|
}
|
|
|
|
protected virtual void LateUpdate()
|
|
{
|
|
if (animator == null || (character!=null && (character.currentHealth <= 0 || character.isDead || character.ragdolled)) || !animator.enabled || (!updateIK && animator.updateMode == AnimatorUpdateMode.Fixed)) return;
|
|
|
|
updateIK = false;
|
|
// call pre Update Event
|
|
if (onPreUpdateSpineIK != null) onPreUpdateSpineIK.Invoke();
|
|
// update SpineIK
|
|
if(!freezeLookPoint) currentLookPoint = GetLookPoint();
|
|
LookAtIK(currentLookPoint, _currentHeadWeight, _currentbodyWeight);
|
|
// call pos Update Event
|
|
if (onPosUpdateSpineIK != null && !IgnoreHeadTrackFromAnimator()) onPosUpdateSpineIK.Invoke();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SPINE IK BEHAVIOUR
|
|
|
|
protected virtual void LookAtIK(Vector3 point, float headWeight, float spineWeight)
|
|
{
|
|
var lookRotation = Quaternion.LookRotation(point);
|
|
var euler = lookRotation.eulerAngles - transform.rotation.eulerAngles;
|
|
|
|
var y = NormalizeAngle(euler.y);
|
|
var x = NormalizeAngle(euler.x);
|
|
|
|
xAngle = Mathf.Clamp(Mathf.Lerp(xAngle, (x), smooth * Time.deltaTime), limitAngleX.x, limitAngleX.y);
|
|
yAngle = Mathf.Clamp(Mathf.Lerp(yAngle, (y), smooth * Time.deltaTime), limitAngleY.x, limitAngleY.y);
|
|
for (int i = 0; i < spine.Count; i++)
|
|
{
|
|
var segment = spine[i];
|
|
var _y = NormalizeAngle(yAngle + Quaternion.Euler(offsetSpine).eulerAngles.y);
|
|
var _x = NormalizeAngle(xAngle + Quaternion.Euler(offsetSpine).eulerAngles.x);
|
|
|
|
var rotX = Quaternion.AngleAxis((_x * spineWeight) / spine.Count, segment.InverseTransformDirection(transform.right));
|
|
var rotY = Quaternion.AngleAxis((_y * spineWeight) / spine.Count, segment.InverseTransformDirection(transform.up));
|
|
segment.rotation *= rotX * rotY;
|
|
}
|
|
|
|
var eulerHeadOffset = Quaternion.Euler(offsetHead).eulerAngles.NormalizeAngle();
|
|
|
|
_yAngle = Mathf.Lerp(_yAngle, (yAngle - (yAngle * spineWeight)) + eulerHeadOffset.y, smooth * Time.deltaTime);
|
|
_xAngle = Mathf.Lerp(_xAngle, (xAngle - (xAngle * spineWeight)) + eulerHeadOffset.x, smooth * Time.deltaTime);
|
|
var _rotX = Quaternion.AngleAxis(_xAngle * headWeight, head.InverseTransformDirection(transform.right));
|
|
var _rotY = Quaternion.AngleAxis(_yAngle * headWeight, head.InverseTransformDirection(transform.up));
|
|
head.rotation *= _rotX * _rotY;
|
|
}
|
|
|
|
protected virtual void SmoothValues(float _headWeight = 0, float _bodyWeight = 0, float _x = 0, float _y = 0)
|
|
{
|
|
_currentHeadWeight = Mathf.Lerp(_currentHeadWeight, _headWeight, smooth * Time.deltaTime);
|
|
_currentbodyWeight = Mathf.Lerp(_currentbodyWeight, _bodyWeight, smooth * Time.deltaTime);
|
|
yRotation = Mathf.Lerp(yRotation, _y, smooth * Time.deltaTime);
|
|
xRotation = Mathf.Lerp(xRotation, _x, smooth * Time.deltaTime);
|
|
yRotation = Mathf.Clamp(yRotation, limitAngleY.x, limitAngleY.y);
|
|
xRotation = Mathf.Clamp(xRotation, limitAngleX.x, limitAngleX.y);
|
|
|
|
var completeY = Mathf.Abs(yRotation - Mathf.Clamp(_y, limitAngleY.x, limitAngleY.y)) < 0.01f;
|
|
var completeX = Mathf.Abs(yRotation - Mathf.Clamp(_x, limitAngleX.x, limitAngleX.y)) < 0.01f;
|
|
isInSmoothValues = !(completeY && completeX);
|
|
}
|
|
|
|
protected virtual Vector3 headPoint { get { return transform.position + (transform.up * headHeight); } }
|
|
|
|
protected virtual Vector3 GetLookPoint()
|
|
{
|
|
if (!IgnoreHeadTrackFromAnimator() && canLook)
|
|
{
|
|
// default look Point
|
|
var _defaultLookPoint = mainLookTarget ? mainLookTarget.position + Vector3.up * offsetHeightResult : defaultLookPoint;
|
|
targetLookPoint = _defaultLookPoint;
|
|
// temporary look Target
|
|
if (exitLookTargetTime > 0 || inLockTarget)
|
|
{
|
|
if (temporaryLookTarget) targetLookPoint = temporaryLookTarget.position + Vector3.up * offsetHeightResult;
|
|
else exitLookTargetTime = 0;
|
|
if (!inLockTarget) exitLookTargetTime -= Time.deltaTime;
|
|
}
|
|
// temporary look point
|
|
if (exitLookPointTime > 0 || inLockPoint)
|
|
{
|
|
targetLookPoint = temporaryLookPoint + Vector3.up * offsetHeightResult;
|
|
if (!inLockPoint) exitLookPointTime -= Time.deltaTime;
|
|
}
|
|
// calc look direction
|
|
var currentDir = defaultLookPoint - headPoint;
|
|
var desiredDir = targetLookPoint - headPoint;
|
|
|
|
currentDir = desiredDir;
|
|
// apply limit angles
|
|
var angle = GetTargetAngle(currentDir);
|
|
|
|
//check if is out angle
|
|
if (!keepLookingOutAngle)
|
|
{
|
|
if (LookDirectionIsOnRange(currentDir))
|
|
{
|
|
if (character != null && character.isStrafing) SmoothValues(strafeHeadWeight, strafeBodyWeight, angle.x, angle.y);
|
|
else SmoothValues(freeHeadWeight, freeBodyWeight, angle.x, angle.y);
|
|
}
|
|
else SmoothValues();
|
|
}
|
|
else
|
|
{
|
|
if (character != null && character.isStrafing) SmoothValues(strafeHeadWeight, strafeBodyWeight, angle.x, angle.y);
|
|
|
|
else SmoothValues(freeHeadWeight, freeBodyWeight, angle.x, angle.y);
|
|
}
|
|
}
|
|
else SmoothValues();
|
|
|
|
// finish look point calc
|
|
var rotA = Quaternion.AngleAxis(yRotation, transform.up);
|
|
var rotB = Quaternion.AngleAxis(xRotation, transform.right);
|
|
var finalRotation = (rotA * rotB);
|
|
var lookDirection = finalRotation * transform.forward;
|
|
currentLookPoint = headPoint + (lookDirection);
|
|
|
|
return lookDirection;
|
|
}
|
|
|
|
protected Vector3 defaultLookPoint
|
|
{
|
|
get { return headPoint + (transform.forward * 100); }
|
|
}
|
|
|
|
protected virtual Vector2 GetTargetAngle(Vector3 direction)
|
|
{
|
|
var lookRotation = Quaternion.LookRotation(direction, transform.up); //rotation from head to camera point
|
|
var angleResult = lookRotation.eulerAngles - transform.eulerAngles; // diference between transform rotation and desiredRotation
|
|
Quaternion desiredRotation = Quaternion.Euler(angleResult); // convert angleResult to Rotation
|
|
var x = (float)System.Math.Round(NormalizeAngle(desiredRotation.eulerAngles.x), 2);
|
|
var y = (float)System.Math.Round(NormalizeAngle(desiredRotation.eulerAngles.y), 2);
|
|
return new Vector2(x, y);
|
|
}
|
|
|
|
protected virtual bool IgnoreHeadTrackFromAnimator()
|
|
{
|
|
for (int i = 0; i < animatorTags.Count; i++)
|
|
{
|
|
if (IsAnimatorTag(animatorTags[i])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public virtual bool IsAnimatorTag(string tag)
|
|
{
|
|
if (animator == null) return false;
|
|
if (animatorStateInfos.isValid())
|
|
{
|
|
if (animatorStateInfos.animatorStateInfos.HasTag(tag))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected virtual float offsetHeightResult
|
|
{
|
|
|
|
get { return currentOffSetLookHeight = Mathf.Lerp(currentOffSetLookHeight, targetOffsetHeight, smooth * 2f); }
|
|
}
|
|
|
|
protected virtual float NormalizeAngle(float angle)
|
|
{
|
|
if (angle < -180)
|
|
return angle + 360;
|
|
else if (angle > 180)
|
|
return angle - 360;
|
|
else
|
|
return angle;
|
|
}
|
|
|
|
protected virtual bool LookDirectionIsOnRange(Vector3 direction)
|
|
{
|
|
var angle = GetTargetAngle(direction);
|
|
return (angle.x >= limitAngleX.x && angle.x <= limitAngleX.y && angle.y >= limitAngleY.x && angle.y <= limitAngleY.y);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region PUBLIC VIRTUAL METHODS. LOOK POINT AND TARGET BEHAVIOUR
|
|
|
|
/// <summary>
|
|
/// Set the definitive look Target
|
|
/// </summary>
|
|
/// <param name="target"></param>
|
|
public virtual void SetMainLookTarget(Transform target)
|
|
{
|
|
mainLookTarget = target;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the definitive look Target
|
|
/// </summary>
|
|
/// <param name="target"></param>
|
|
public virtual void RemoveMainLookTarget()
|
|
{
|
|
mainLookTarget = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulate a Look Around Animation using the Headtrack
|
|
/// </summary>
|
|
public virtual void LookAround()
|
|
{
|
|
lookAroundProgress += Time.deltaTime * lookAroundSpeed;
|
|
var pp = Mathf.PingPong(lookAroundProgress, 1f);
|
|
var l = Quaternion.AngleAxis(Mathf.Lerp(-lookAroundAngle, lookAroundAngle, lookAroundCurve.Evaluate(pp)), transform.up) * transform.forward;
|
|
var eyesPoint = eyes ? eyes.position : transform.position + Vector3.up * headHeight;
|
|
var lookp = eyesPoint + l * 100f;
|
|
LookAtPoint(lookp, 0.1f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look at point. Set a point to follow for default time (override lookTarget).<seealso cref="vAIHeadtrack.timeToExitLookPoint"/>
|
|
/// if you need to follow point, call this in update or use <seealso cref="vAIHeadtrack.LookAtTarget(Transform)"/>
|
|
/// </summary>
|
|
/// <param name="point">Point to look</param>
|
|
public virtual void LookAtPoint(Vector3 point, float offsetLookHeight = -1)
|
|
{
|
|
if (inLockPoint) return;
|
|
if (offsetLookHeight != -1) SetOffsetLookHeight(offsetLookHeight);
|
|
else ResetOffseLookHeight();
|
|
temporaryLookPoint = point;
|
|
exitLookPointTime = timeToExitLookPoint;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look at point. Set a point to follow for a especific time (override lookTarget).
|
|
/// if you need to follow point, call this in update or use <seealso cref="vAIHeadtrack.LookAtTarget(Transform, float)"/>
|
|
/// </summary>
|
|
/// <param name="point">Point to look</param>
|
|
/// <param name="timeToExitLookPoint">Time that will to be looking</param>
|
|
public virtual void LookAtPoint(Vector3 point, float timeToExitLookPoint, float offsetLookHeight = -1)
|
|
{
|
|
if (inLockPoint) return;
|
|
if (offsetLookHeight != -1) SetOffsetLookHeight(offsetLookHeight);
|
|
else ResetOffseLookHeight();
|
|
temporaryLookPoint = point;
|
|
exitLookPointTime = timeToExitLookPoint;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look at target.
|
|
/// Set a target to follow for default time <seealso cref="vAIHeadtrack.timeToExitLookTarget"/>
|
|
/// if you need to follow target always, call <seealso cref="vAIHeadtrack.LookAtPoint(Vector3)"/> in update
|
|
/// </summary>
|
|
/// <param name="target"> Target to look</param>
|
|
public virtual void LookAtTarget(Transform target, float offsetLookHeight = -1)
|
|
{
|
|
if (inLockTarget) return;
|
|
if (offsetLookHeight != -1) SetOffsetLookHeight(offsetLookHeight);
|
|
else ResetOffseLookHeight();
|
|
temporaryLookTarget = target;
|
|
exitLookTargetTime = timeToExitLookPoint;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look at target.
|
|
/// Set a target to follow for a especific time
|
|
/// if you need to follow target always, call <seealso cref="vAIHeadtrack.LookAtPoint(Vector3,float)"/> in update or Call <seealso cref="vAIHeadtrack.LockLookAt"/>
|
|
/// </summary>
|
|
/// <param name="target">Target to look</param>
|
|
/// <param name="timeToExitLookTarget"> Time that will to be looking</param>
|
|
public virtual void LookAtTarget(Transform target, float timeToExitLookTarget, float offsetLookHeight = -1)
|
|
{
|
|
if (inLockTarget) return;
|
|
if (offsetLookHeight != -1) SetOffsetLookHeight(offsetLookHeight);
|
|
else ResetOffseLookHeight();
|
|
|
|
temporaryLookTarget = target;
|
|
exitLookTargetTime = timeToExitLookTarget;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lock the current temporary look point
|
|
/// </summary>
|
|
public virtual void LockLookAtPoint()
|
|
{
|
|
inLockPoint = true;
|
|
inLockTarget = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlock the current temporary look point
|
|
/// </summary>
|
|
public virtual void UnlockLookAtPoint()
|
|
{
|
|
inLockPoint = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lock the current temporary look target
|
|
/// </summary>
|
|
public virtual void LockLookAtTarget()
|
|
{
|
|
inLockTarget = true;
|
|
inLockPoint = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlock the current temporary look target
|
|
/// </summary>
|
|
public virtual void UnlockLookAtTarget()
|
|
{
|
|
inLockTarget = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the temporary look point (Ignore timeToExit) <seealso cref="vAIHeadtrack.timeToExitLookPoint"/>
|
|
/// </summary>
|
|
public virtual void ResetLookPoint()
|
|
{
|
|
exitLookPointTime = 0;
|
|
inLockPoint = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the temporary look target (Ignore timeToExit) <seealso cref="vAIHeadtrack.timeToExitLookTarget"/>
|
|
/// </summary>
|
|
public virtual void ResetLookTarget()
|
|
{
|
|
exitLookTargetTime = 0;
|
|
temporaryLookTarget = null;
|
|
inLockTarget = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all temporary look (Ignore timeToExit)<seealso cref="vAIHeadtrack.timeToExitLookPoint"/>, <seealso cref="vAIHeadtrack.timeToExitLookTarget"/>
|
|
/// </summary>
|
|
public virtual void ResetLook()
|
|
{
|
|
ResetLookPoint();
|
|
ResetLookTarget();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if has a look point or look target
|
|
/// </summary>
|
|
public virtual bool isLookingForSomething
|
|
{
|
|
get { return ((defaultLookPoint != targetLookPoint && canLook) || isInSmoothValues); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set off set look height
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
public virtual void SetOffsetLookHeight(float value)
|
|
{
|
|
targetOffsetHeight = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set offset look Height to default <see cref="vAIHeadtrack.defaultOffSetLookHeight"/>
|
|
/// </summary>
|
|
public virtual void ResetOffseLookHeight()
|
|
{
|
|
if (targetOffsetHeight != defaultOffSetLookHeight)
|
|
targetOffsetHeight = defaultOffSetLookHeight;
|
|
}
|
|
#endregion
|
|
}
|
|
} |