using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Invector.vCharacterController.AI { using UnityEngine.AI; using vMelee; public class vSimpleMeleeAI_Motor : vCharacter { #region Variables #region Layers [vEditorToolbar("Layers")] [Tooltip("Layers that the character can walk on")] public LayerMask groundLayer = 1 << 0; [Tooltip("Distance to became not grounded")] [SerializeField] protected float groundCheckDistance = 0.5f; [Tooltip("What objects can make the character auto crouch")] public LayerMask autoCrouchLayer = 1 << 0; [Tooltip("[SPHERECAST] ADJUST IN PLAY MODE - White Spherecast put just above the head, this will make the character Auto-Crouch if something hit the sphere.")] public float headDetect = 0.95f; #endregion #region AI variables [vEditorToolbar("Locomotion")] [Tooltip("Use to limit your locomotion animation, if you want to patrol walking set this value to 0.5f")] [Range(0f, 1.5f)] public float patrolSpeed = 0.5f; [Tooltip("Use to limit your locomotion animation, if you want to wander walking set this value to 0.5f")] [Range(0f, 1.5f)] public float wanderSpeed = 0.5f; [Tooltip("Use to limit your locomotion animation, if you want to chase the target walking set this value to 0.5f")] [Range(0f, 1.5f)] public float chaseSpeed = 1f; [Tooltip("Use to limit your locomotion animation, if you want to strafe the target walking set this value to 0.5f")] [Range(0f, 1.5f)] public float strafeSpeed = 1f; [Header("--- Strafe ---")] [Tooltip("Strafe around the target")] public bool strafeSideways = true; [Tooltip("Strafe a few steps backwards")] public bool strafeBackward = true; [Tooltip("Distance to switch to the strafe locomotion, leave with 0 if you don't want your character to strafe")] public float strafeDistance = 3f; [Tooltip("Min time to change the strafe direction")] public float minStrafeSwape = 2f; [Tooltip("Max time to change the strafe direction")] public float maxStrafeSwape = 5f; [Tooltip("Velocity to rotate the character while strafing")] public float strafeRotationSpeed = 5f; [vEditorToolbar("Detection")] protected AIStates _currentState = AIStates.PatrolWaypoints; public virtual AIStates currentState { get { return _currentState; } set { _currentState = value; } } [Header("Who is your Target?")] public vSimpleMeleeAI_SphereSensor sphereSensor; public vTagMask tagsToDetect = new vTagMask { "Player" }; public LayerMask layersToDetect = 1 << 0; public LayerMask obstaclesLayer; public bool sortTargetFromDistance = false; [Range(0f, 360f)] public float fieldOfView = 95f; [Tooltip("Max Distance to detect the Target with FOV")] public float maxDetectDistance = 5f; [Tooltip("Min Distance to noticed the Target without FOV")] public float minDetectDistance = 2f; [Tooltip("Distance to lost the Target")] public float distanceToLostTarget = 5f; public float lostTargetDistance { get { return maxDetectDistance + distanceToLostTarget; } } [Tooltip("Distance to stop when chasing the Player")] public float chaseStopDistance = 1f; public bool drawAgentPath = false; public bool displayGizmos; [vEditorToolbar("Combat")] [Tooltip("Check if you want the Enemy to be passive even if you attack him")] public bool passiveToDamage = false; [Tooltip("Check if you want the Enemy to chase the Target at first sight")] public bool agressiveAtFirstSight = true; [Tooltip("Velocity to rotate the character while attacking")] public float attackRotationSpeed = 0.5f; [Tooltip("Delay to trigger the first attack when close to the target")] public float firstAttackDelay = 0f; [Tooltip("Min frequency to attack")] public float minTimeToAttack = 4f; [Tooltip("Max frequency to attack")] public float maxTimeToAttack = 6f; [Tooltip("How many attacks the AI will make on a combo")] public int maxAttackCount = 3; [Tooltip("Randomly attacks based on the maxAttackCount")] public bool randomAttackCount = true; [Range(0f, 1f)] public float chanceToRoll = .1f; [Range(0f, 1f)] public float chanceToBlockInStrafe = .1f; [Range(0f, 1f)] public float chanceToBlockAttack = 0f; [Tooltip("How much time the character will stand up the shield")] public float raiseShield = 4f; [Tooltip("How much time the character will lower the shield")] public float lowerShield = 2f; [vEditorToolbar("Waypoint")] [Tooltip("Max Distance to change waypoint")] [Range(0.5f, 100f)] public float distanceToChangeWaypoint = 1f; [Tooltip("Min Distance to stop when Patrolling through waypoints")] [Range(0.5f, 100f)] public float patrollingStopDistance = 0.5f; public AIPatrolWithOutAreaStyle patrolWithoutAreaStyle = AIPatrolWithOutAreaStyle.GoToStartPoint; public vWaypointArea pathArea; public bool randomWaypoints; public vFisherYatesRandom randomWaypoint = new vFisherYatesRandom(); public vFisherYatesRandom randomPatrolPoint = new vFisherYatesRandom(); [HideInInspector] public CapsuleCollider _capsuleCollider; // there is a prefab of health hud example that you can drag and drop into the head bone of your character [HideInInspector] public v_SpriteHealth healthSlider; // attach a meleeManager component to create new hitboxs and set up different weapons [HideInInspector] public vMeleeManager meleeManager; // check your MeleeWeapon Inspector, each weapon can set up different distances to attack public OnSetAgressiveEvent onSetAgressive = new OnSetAgressiveEvent(); public class OnSetAgressiveEvent : UnityEngine.Events.UnityEvent { } [HideInInspector] public bool lockMovement; public enum AIPatrolWithOutAreaStyle { GoToStartPoint, Wander, Idle } public enum AIStates { Idle, PatrolSubPoints, PatrolWaypoints, Wander, Chase } #endregion #region Protected Variables public CharacterTarget currentTarget;// { protected set; get; } [System.Serializable] public struct CharacterTarget { public Transform transform; public vIHealthController character; public Collider colliderTarget; } protected Vector3 _targetPos; protected virtual Vector3 targetPos { get { return _targetPos; } set { _targetPos = value; } } [SerializeField, vReadOnly] protected bool _canSeeTarget; protected virtual bool canSeeTarget { get { return _canSeeTarget; } set { _canSeeTarget = value; } } protected Vector3 _destination; protected virtual Vector3 destination { get { return _destination; } set { _destination = value; } } protected Vector3 _fwd; protected virtual Vector3 fwd { get { return _fwd; } set { _fwd = value; } } protected virtual bool _isGrounded { get; set; } protected virtual bool isGrounded { get { return _isGrounded; } set { _isGrounded = value; } } protected bool _isStrafing; protected virtual bool isStrafing { get { return _isStrafing; } set { _isStrafing = value; } } protected virtual bool inResetAttack { get; set; } protected virtual bool firstAttack { get; set; } protected virtual int attackCount { get; set; } protected virtual int _currentWaypoint { get; set; } protected virtual int currentWaypoint { get { return _currentWaypoint; } set { _currentWaypoint = value; } } protected virtual int _currentPatrolPoint { get; set; } protected virtual int currentPatrolPoint { get { return _currentPatrolPoint; } set { _currentPatrolPoint = value; } } protected float _direction; protected virtual float direction { get { return _direction; } set { _direction = value; } } protected float _timer; protected virtual float timer { get { return _timer; } set { _timer = value; } } protected float _wait; protected virtual float wait { get { return _wait; } set { _wait = value; } } protected float _fovAngle; protected virtual float fovAngle { get { return _fovAngle; } set { _fovAngle = value; } } protected virtual float sideMovement { get; set; } protected virtual float fwdMovement { get; set; } protected virtual float strafeSwapeFrequency { get; set; } protected virtual float groundDistance { get; set; } protected Vector3 _startPosition; protected virtual Vector3 startPosition { get { return _startPosition; } set { _startPosition = value; } } protected RaycastHit groundHit; protected NavMeshAgent agent; protected NavMeshPath agentPath; protected Quaternion freeRotation; protected Quaternion desiredRotation; protected Vector3 oldPosition; protected Vector3 combatMovement; protected Vector3 rollDirection; protected Rigidbody _rigidbody; protected PhysicsMaterial frictionPhysics; protected Transform head; protected Collider colliderTarget; protected vWaypoint targetWaypoint; protected vPoint targetPatrolPoint; protected List visitedPatrolPoint = new List(); protected List visitedWaypoint = new List(); #endregion #region Actions protected bool _isCrouched; protected bool _canAttack; protected bool _tryingBlock; protected bool _isRolling; protected virtual bool isCrouched { get { return _isCrouched; } set { _isCrouched = value; } } protected virtual bool canAttack { get { return _canAttack; } set { _canAttack = value; } } protected virtual bool tryingBlock { get { return _tryingBlock; } set { _tryingBlock = value; } } protected virtual bool isRolling { get { return _isRolling; } set { _isRolling = value; } } protected bool _isBlocking; public virtual bool isBlocking { get { return _isBlocking; } protected set { _isBlocking = value; } } protected bool _isAttacking; public bool isAttacking { get { return _isAttacking; } protected set { _isAttacking = value; } } public bool isArmed { get { return meleeManager != null && (meleeManager.rightWeapon != null || (meleeManager.leftWeapon != null && meleeManager.leftWeapon.meleeType != vMeleeType.OnlyDefense)); } } protected bool _actions; public virtual bool actions { get { return _actions; } set { _actions = value; } } #endregion #endregion public override void Init() { base.Init(); fwd = transform.forward; destination = transform.position; agent = GetComponent(); agentPath = new UnityEngine.AI.NavMeshPath(); sphereSensor = GetComponentInChildren(); if (sphereSensor) { sphereSensor.root = transform; } meleeManager = GetComponent(); canAttack = true; attackCount = 0; sideMovement = GetRandonSide(); destination = transform.position; _rigidbody = GetComponent(); _rigidbody.useGravity = true; _rigidbody.constraints = RigidbodyConstraints.None | RigidbodyConstraints.FreezeRotation; agent.updatePosition = false; agent.updateRotation = false; agent.enabled = false; _capsuleCollider = GetComponent(); // avoid collision detection with inside colliders Collider[] AllColliders = this.GetComponentsInChildren(); Collider thisCollider = GetComponent(); for (int i = 0; i < AllColliders.Length; i++) { Physics.IgnoreCollision(thisCollider, AllColliders[i]); } healthSlider = GetComponentInChildren(); head = animator.GetBoneTransform(HumanBodyBones.Head); oldPosition = transform.position; if (!fillHealthOnStart) currentHealth = _currentHealth; startPosition = transform.position; } #region AI Locomotion public float distanceToAttack { get { if (meleeManager) { return meleeManager.GetAttackDistance(); } return 1f; } } public bool OnCombatArea { get { if (currentTarget.transform == null) { return false; } var inFloor = Vector3.Distance(new Vector3(0, transform.position.y, 0), new Vector3(0, currentTarget.transform.position.y, 0)) < distanceToAttack; return (inFloor && agressiveAtFirstSight && TargetDistance <= strafeDistance && !agent.isOnOffMeshLink); } } public bool OnStrafeArea { get { if (!canSeeTarget) { isStrafing = false; return false; } if (currentTarget.transform == null || !agressiveAtFirstSight) { return false; } var inFloor = Vector3.Distance(new Vector3(0, transform.position.y, 0), new Vector3(0, currentTarget.transform.position.y, 0)) < 1.5f; // exit strafe if (isStrafing) { isStrafing = TargetDistance < (strafeDistance + 2f); } // enter strafe else { isStrafing = OnCombatArea; } return inFloor ? isStrafing : false; } } public bool AgentDone() { if (!agent.enabled && agent.updatePosition) { return true; } return !agent.pathPending && AgentStopping() && agent.updatePosition; } public bool AgentStopping() { if (!agent.enabled || !agent.isOnNavMesh) { return true; } return agent.remainingDistance <= agent.stoppingDistance; } public int GetRandonSide() { var side = UnityEngine.Random.Range(-1, 1); if (side < 0) { side = -1; } else { side = 1; } return side; } protected void CheckGroundDistance() { if (_capsuleCollider != null) { var dist = 10f; Ray ray1 = new Ray(transform.position + new Vector3(0, _capsuleCollider.height / 2, 0), Vector3.down); if (Physics.Raycast(ray1, out groundHit, _capsuleCollider.height * 0.75f, groundLayer)) { dist = transform.position.y - groundHit.point.y; } groundDistance = dist; if (!actions && !isRolling && groundDistance < 0.3f) { if (currentHealth > 0) { UnityEngine.AI.NavMeshHit navHit; if (UnityEngine.AI.NavMesh.SamplePosition(transform.position, out navHit, 5f, UnityEngine.AI.NavMesh.AllAreas)) { _rigidbody.constraints = RigidbodyConstraints.None | RigidbodyConstraints.FreezeRotation | RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ; _rigidbody.useGravity = false; agent.updatePosition = false; agent.enabled = true; } } if (agent.enabled && agent.isOnNavMesh && Vector3.Distance(agent.nextPosition, transform.position) <= 0.1f) { agent.updatePosition = true; } else if (agent.enabled && agent.isOnNavMesh && !agent.updatePosition) { agent.nextPosition = transform.position; } } if (!agent.isOnNavMesh && groundDistance > 0.3f && !ragdolled) { _rigidbody.useGravity = true; _rigidbody.constraints = RigidbodyConstraints.None | RigidbodyConstraints.FreezeRotation; agent.enabled = false; agent.updatePosition = false; } } } public void CheckAutoCrouch() { // radius of SphereCast float radius = _capsuleCollider.radius * 0.9f; // Position of SphereCast origin stating in base of capsule Vector3 pos = transform.position + Vector3.up * ((_capsuleCollider.height * 0.5f) - _capsuleCollider.radius); // ray for SphereCast Ray ray2 = new Ray(pos, Vector3.up); RaycastHit groundHit; // sphere cast around the base of capsule for check ground distance //if (Physics.SphereCast(ray2, radius, out groundHit, _capsuleCollider.bounds.max.y - (_capsuleCollider.radius * 0.1f), groundLayer)) if (Physics.SphereCast(ray2, radius, out groundHit, headDetect - (_capsuleCollider.radius * 0.1f), autoCrouchLayer)) { isCrouched = true; } else { isCrouched = false; } } #endregion #region Check Target /// /// Calculate Fov Angle /// /// public bool onFovAngle() { if (currentTarget.transform == null) { return false; } var freeRotation = Quaternion.LookRotation(currentTarget.transform.position - transform.position, Vector3.up); var newAngle = freeRotation.eulerAngles - transform.eulerAngles; fovAngle = newAngle.NormalizeAngle().y; if (fovAngle < fieldOfView && fovAngle > -fieldOfView) { return true; } return false; } int canSeeTargetIteration; /// /// Target Detection /// /// /// public void CheckTarget() { if (currentTarget.transform == null || !agressiveAtFirstSight) { canSeeTarget = false; canSeeTargetIteration = 0; return; } if (TargetDistance > maxDetectDistance) { canSeeTarget = false; canSeeTargetIteration = 0; return; } if (currentTarget.colliderTarget == null || currentTarget.colliderTarget.transform != currentTarget.transform) { currentTarget.colliderTarget = currentTarget.transform.GetComponent(); } if (currentTarget.colliderTarget == null) { canSeeTarget = false; canSeeTargetIteration = 0; return; } var top = new Vector3(currentTarget.colliderTarget.bounds.center.x, currentTarget.colliderTarget.bounds.max.y, currentTarget.colliderTarget.bounds.center.z); var bottom = new Vector3(currentTarget.colliderTarget.bounds.center.x, currentTarget.colliderTarget.bounds.min.y, currentTarget.colliderTarget.bounds.center.z); var offset = Vector3.Distance(top, bottom) * 0.15f; top.y -= offset; bottom.y += offset; if (!onFovAngle() && TargetDistance > minDetectDistance) { canSeeTarget = false; canSeeTargetIteration = 0; return; } RaycastHit hit; if (canSeeTargetIteration == 0 && !Physics.Linecast(head.position, top, out hit, obstaclesLayer)) { canSeeTarget = true; canSeeTargetIteration = 0; return; } else if (canSeeTargetIteration == 1 && !Physics.Linecast(head.position, bottom, out hit, obstaclesLayer)) { canSeeTarget = true; canSeeTargetIteration = 0; return; } else if (canSeeTargetIteration == 2 && !Physics.Linecast(head.position, currentTarget.colliderTarget.bounds.center, out hit, obstaclesLayer)) { canSeeTarget = true; canSeeTargetIteration = 0; return; } else { canSeeTargetIteration++; } if (canSeeTargetIteration > 1) { canSeeTargetIteration = 0; } canSeeTarget = false; } public float TargetDistance { get { if (currentTarget.transform != null) { return currentTarget.colliderTarget ? Vector3.Distance(_capsuleCollider.bounds.center, currentTarget.colliderTarget.bounds.center) : Vector3.Distance(transform.position, currentTarget.transform.position); } return maxDetectDistance + 1f; } } public Transform headTarget { get { if (currentTarget.transform != null && currentTarget.transform.GetComponent() != null) { return currentTarget.transform.GetComponent().GetBoneTransform(HumanBodyBones.Head); } else { return null; } } } #endregion #region AI Health protected void RemoveComponents() { if (!removeComponentsAfterDie) { return; } if (_capsuleCollider != null) { Destroy(_capsuleCollider); } if (_rigidbody != null) { Destroy(_rigidbody); } if (animator != null) { Destroy(animator); } if (agent != null) { Destroy(agent); } var comps = GetComponents(); for (int i = 0; i < comps.Length; i++) { Destroy(comps[i]); } } /// /// TAKE DAMAGE - you can override the take damage method from the vCharacter and add your own calls /// /// damage to apply public override void TakeDamage(vDamage damage) { // ignore damage if the character is rolling, dead or the animator is disable if (isRolling || currentHealth <= 0 || !animator.enabled) { return; } if (!damage.ignoreDefense && !actions && CheckChanceToRoll()) { return; } base.TakeDamage(damage); } protected override void TriggerDamageReaction(vDamage damage) { if (!isRolling) { base.TriggerDamageReaction(damage); } } #endregion #region AI Melee Combat Methods protected bool CheckChanceToRoll() { if (isAttacking || actions) { return false; } float randomRoll = UnityEngine.Random.value; if (randomRoll < chanceToRoll && randomRoll > 0 && currentTarget.transform != null) { animator.SetTrigger("ResetState"); sideMovement = GetRandonSide(); Ray ray = new Ray(currentTarget.transform.position, currentTarget.transform.right * sideMovement); rollDirection = ray.direction; animator.CrossFadeInFixedTime("Roll", 0.1f); return true; } return false; } protected IEnumerator CheckChanceToBlock(float chance, float timeToEnter) { tryingBlock = true; float randomBlock = UnityEngine.Random.value; if (randomBlock < chance && randomBlock > 0 && !isBlocking) { if (timeToEnter > 0) { yield return new WaitForSeconds(timeToEnter); } isBlocking = currentTarget.transform == null || (actions) || isAttacking ? false : true; StartCoroutine(ResetBlock()); tryingBlock = false; } else { tryingBlock = false; } } protected IEnumerator ResetBlock() { yield return new WaitForSeconds(currentTarget.transform == null ? 0 : raiseShield); isBlocking = false; } protected virtual void SetAggressive(bool value) { agressiveAtFirstSight = value;// onSetAgressive.Invoke(value); } int GetDamageResult(int damage, float defenseRate) { int result = (int)(damage - ((damage * defenseRate) / 100)); return result; } #endregion #region Ragdoll public override void ResetRagdoll() { oldPosition = transform.position; ragdolled = false; _capsuleCollider.isTrigger = false; _rigidbody.isKinematic = false; agent.updatePosition = false; agent.enabled = true; } public override void EnableRagdoll() { agent.enabled = false; agent.updatePosition = false; ragdolled = true; _rigidbody.isKinematic = true; _capsuleCollider.isTrigger = true; } #endregion } }