using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class EnemyActions : MonoBehaviour { [Space(10)] [Header ("Linked components")] public GameObject target; //current target public UnitAnimator animator; //animator component public GameObject GFX; //GFX of this unit public Rigidbody rb; //rigidbody component public CapsuleCollider capsule; //capsule collider [Header("Attack Data")] public DamageObject[] AttackList; //a list of attacks public bool PickRandomAttack; //choose a random attack from the list public float hitZRange = 2; //the z range of all attacks public float defendChance = 0; //the chance that an incoming attack is defended % public float hitRecoveryTime = .4f; //the timeout after a hit before the enemy can do an action public float standUpTime = 1.1f; //the time it takes for this enemy to stand up public bool canDefendDuringAttack; //true if the enemy is able to defend an incoming attack while he is doing his own attack public bool AttackPlayerAirborne; //attack a player while he is in the air private DamageObject lastAttack; //data from the last attack that has taken place private int AttackCounter = 0; //current attack number public bool canHitEnemies; //true is this enemy can hit other enemies public bool canHitDestroyableObjects; //true is this enemy can hit destroyable objects like crates, barrels. [HideInInspector] public float lastAttackTime; //time of the last attack [Header ("Settings")] public bool pickARandomName; //assign a random name public TextAsset enemyNamesList; //the list of enemy names public string enemyName = ""; //the name of this enemy public float attackRangeDistance = 1.4f; //the distance from the target where the enemy is able to attack public float closeRangeDistance = 2f; //the distance from the target at close range public float midRangeDistance = 3f; //the distance from the target at mid range public float farRangeDistance = 4.5f; //the distance from the target at far range public float RangeMarging = 1f; //the amount of space that is allowed between the player and enemy before we reposition ourselves public float walkSpeed = 1.95f; //the speed of a walk public float walkBackwardSpeed = 1.2f; //the speed of walking backwards public float sightDistance = 10f; //the distance when we can see the target public float attackInterval = 1.2f; //the time inbetween attacking public float rotationSpeed = 15f; //the rotation speed when switching directions public float lookaheadDistance; //the distance at which we check for obstacles in from of us public bool ignoreCliffs; //ignore cliff detection public float KnockdownTimeout = 0f; //the time before we stand up after a knockdown public float KnockdownUpForce = 5f; //the up force of a knockDown public float KnockbackForce = 4; //the horizontal force of a knockDown private LayerMask HitLayerMask; //the layermask for damagable objects public LayerMask CollisionLayer; //the layers we check collisions with public bool randomizeValues = true; //randomize values to avoid enemy synchronization [HideInInspector] public float zSpreadMultiplier = 2f; //multiplyer for the z distance between enemies float chargeTime = 0f; [Header ("Stats")] public RANGE range; public ENEMYTACTIC enemyTactic; public UNITSTATE enemyState; public DIRECTION currentDirection; public bool targetSpotted; public bool cliffSpotted; public bool wallspotted; public bool isGrounded; public bool isDead; private Vector3 moveDirection; public float distance; private Vector3 fixedVelocity; private bool updateVelocity; //list of states where the enemy cannot move private List NoMovementStates = new List { UNITSTATE.DEATH, UNITSTATE.ATTACK, UNITSTATE.DEFEND, UNITSTATE.GROUNDHIT, UNITSTATE.HIT, UNITSTATE.IDLE, UNITSTATE.KNOCKDOWNGROUNDED, UNITSTATE.STANDUP, }; //list of states where the player can be hit private List HitableStates = new List { UNITSTATE.ATTACK, UNITSTATE.DEFEND, UNITSTATE.HIT, UNITSTATE.IDLE, UNITSTATE.KICK, UNITSTATE.PUNCH, UNITSTATE.STANDUP, UNITSTATE.WALK, UNITSTATE.KNOCKDOWNGROUNDED, }; [HideInInspector] public float ZSpread; //the distance between enemies on the z-axis //[HideInInspector] public Vector3 distanceToTarget; private List defendableStates = new List { UNITSTATE.IDLE, UNITSTATE.WALK, UNITSTATE.DEFEND }; //a list of states where the enemy is able to defend an incoming attack //global event handler for enemies public delegate void UnitEventHandler(GameObject Unit); //global event Handler for destroying units public static event UnitEventHandler OnUnitDestroy; //--- public void OnStart(){ //assign a name to this enemy if(pickARandomName) enemyName = GetRandomName(); //set random player as target GameObject[] players = GameObject.FindGameObjectsWithTag("Player"); if (target == null) target = players[Random.Range(0, players.Length)]; //tell enemymanager to update the list of active enemies EnemyManager.getActiveEnemies(); //enable defending during an attack if (canDefendDuringAttack) defendableStates.Add (UNITSTATE.ATTACK); //set up HitLayerMask HitLayerMask = 1 << LayerMask.NameToLayer("Player"); if(canHitEnemies)HitLayerMask |= (1 << LayerMask.NameToLayer("Enemy")); if(canHitDestroyableObjects)HitLayerMask |= (1 << LayerMask.NameToLayer("DestroyableObject")); } #region Update //late Update public void OnLateUpdate(){ //apply any root motion offsets to parent if (animator && animator.GetComponent().applyRootMotion && animator.transform.localPosition != Vector3.zero) { Vector3 offset = animator.transform.localPosition; animator.transform.localPosition = Vector3.zero; transform.position += offset * (int)currentDirection; } } //physics update public void OnFixedUpdate() { if(updateVelocity) { rb.velocity = fixedVelocity; updateVelocity = false; } } //set velocity on next fixed update void SetVelocity(Vector3 velocity) { fixedVelocity = velocity; updateVelocity = true; } #endregion #region Attack //Attack public void ATTACK() { //don't attack when player is jumping var playerMovement = target.GetComponent(); if (!AttackPlayerAirborne && playerMovement != null && playerMovement.jumpInProgress) { return; } else { //init enemyState = UNITSTATE.ATTACK; Move(Vector3.zero, 0f); LookAtTarget(target.transform); TurnToDir(currentDirection); //pick random attack if (PickRandomAttack) AttackCounter = Random.Range (0, AttackList.Length); //play animation animator.SetAnimatorTrigger (AttackList[AttackCounter].animTrigger); //go to the next attack in the list if (!PickRandomAttack) { AttackCounter += 1; if (AttackCounter >= AttackList.Length) AttackCounter = 0; } lastAttackTime = Time.time; lastAttack = AttackList [AttackCounter]; lastAttack.inflictor = gameObject; //resume Invoke ("Ready", AttackList [AttackCounter].duration); } } #endregion #region We are Hit //Unit was hit public void Hit(DamageObject d){ if(HitableStates.Contains(enemyState)) { //only allow ground attacks to hit us when we are knocked down if(enemyState == UNITSTATE.KNOCKDOWNGROUNDED && !d.isGroundAttack) return; CancelInvoke(); StopAllCoroutines(); animator.StopAllCoroutines(); Move(Vector3.zero, 0f); //add attack time out so this enemy cannot attack instantly after a hit lastAttackTime = Time.time; //don't hit this unit when it's allready down if((enemyState == UNITSTATE.KNOCKDOWNGROUNDED || enemyState == UNITSTATE.GROUNDHIT) && !d.isGroundAttack) return; //defend an incoming attack if(!d.DefenceOverride && defendableStates.Contains(enemyState)) { int rand = Random.Range(0, 100); if(rand < defendChance) { Defend(); return; } } //hit sfx GlobalAudioPlayer.PlaySFXAtPosition(d.hitSFX, transform.position); //hit particle effect ShowHitEffectAtPosition(new Vector3(transform.position.x, d.inflictor.transform.position.y + d.collHeight, transform.position.z)); //camera Shake CamShake camShake = Camera.main.GetComponent(); if(camShake != null) camShake.Shake(.1f); //activate slow motion camera if(d.slowMotionEffect) { CamSlowMotionDelay cmd = Camera.main.GetComponent(); if(cmd != null) cmd.StartSlowMotionDelay(.2f); } //substract health HealthSystem healthSystem = GetComponent(); if(healthSystem != null) { healthSystem.SubstractHealth(d.damage); if(healthSystem.CurrentHp == 0) return; } //ground attack if(enemyState == UNITSTATE.KNOCKDOWNGROUNDED) { StopAllCoroutines(); enemyState = UNITSTATE.GROUNDHIT; StartCoroutine(GroundHit()); return; } //turn towards the direction of the incoming attack int dir = d.inflictor.transform.position.x > transform.position.x? 1 : -1; TurnToDir((DIRECTION)dir); //check for a knockdown if(d.knockDown) { StartCoroutine(KnockDownSequence(d.inflictor)); return; } else { //default hit int rand = Random.Range(1, 3); animator.SetAnimatorTrigger("Hit" + rand); enemyState = UNITSTATE.HIT; //add small force from the impact LookAtTarget(d.inflictor.transform); animator.AddForce(-KnockbackForce); //switch enemy state from passive to aggressive when attacked if(enemyTactic != ENEMYTACTIC.ENGAGE) { EnemyManager.setAgressive(gameObject); } Invoke("Ready", hitRecoveryTime); return; } } } //Defend void Defend(){ enemyState = UNITSTATE.DEFEND; animator.ShowDefendEffect(); animator.SetAnimatorTrigger ("Defend"); GlobalAudioPlayer.PlaySFX ("DefendHit"); animator.SetDirection (currentDirection); } #endregion #region Check for hit //checks if we have hit something (Animation Event) public void CheckForHit() { //draws a hitbox in front of the character to see which objects are overlapping it Vector3 boxPosition = transform.position + (Vector3.up * lastAttack.collHeight) + Vector3.right * ((int)currentDirection * lastAttack.collDistance); Vector3 boxSize = new Vector3 (lastAttack.CollSize/2, lastAttack.CollSize/2, hitZRange/2); Collider[] hitColliders = Physics.OverlapBox(boxPosition, boxSize, Quaternion.identity, HitLayerMask); int i=0; while (i < hitColliders.Length) { //hit a damagable object IDamagable damagableObject = hitColliders[i].GetComponent(typeof(IDamagable)) as IDamagable; if (damagableObject != null && damagableObject != (IDamagable)this) { damagableObject.Hit(lastAttack); } i++; } } //Display hit box + lookahead sphere in Unity Editor (Debug) #if UNITY_EDITOR void OnDrawGizmos(){ //visualize hitbox if (lastAttack != null && (Time.time - lastAttackTime) < lastAttack.duration) { Gizmos.color = Color.red; Vector3 boxPosition = transform.position + (Vector3.up * lastAttack.collHeight) + Vector3.right * ((int)currentDirection * lastAttack.collDistance); Vector3 boxSize = new Vector3 (lastAttack.CollSize, lastAttack.CollSize, hitZRange); Gizmos.DrawWireCube (boxPosition, boxSize); } //visualize lookahead sphere Gizmos.color = Color.yellow; Vector3 offset = -moveDirection.normalized * lookaheadDistance; Gizmos.DrawWireSphere (transform.position + capsule.center - offset, capsule.radius); } #endif #endregion #region KnockDown Sequence //knockDown sequence IEnumerator KnockDownSequence(GameObject inflictor) { enemyState = UNITSTATE.KNOCKDOWN; yield return new WaitForFixedUpdate(); //look towards the direction of the incoming attack int dir = 1; if(inflictor != null) dir = inflictor.transform.position.x > transform.position.x? 1 : -1; currentDirection = (DIRECTION)dir; animator.SetDirection(currentDirection); TurnToDir(currentDirection); //add knockback force animator.SetAnimatorTrigger("KnockDown_Up"); while(IsGrounded()){ SetVelocity(new Vector3(KnockbackForce * -dir, KnockdownUpForce, 0)); yield return new WaitForFixedUpdate(); } //going up... while(rb.velocity.y >= 0) yield return new WaitForFixedUpdate(); //going down animator.SetAnimatorTrigger ("KnockDown_Down"); while(!IsGrounded()) yield return new WaitForFixedUpdate(); //hit ground animator.SetAnimatorTrigger ("KnockDown_End"); GlobalAudioPlayer.PlaySFXAtPosition("Drop", transform.position); animator.SetAnimatorFloat ("MovementSpeed", 0f); animator.ShowDustEffectLand(); enemyState = UNITSTATE.KNOCKDOWNGROUNDED; Move(Vector3.zero, 0f); //cam shake CamShake camShake = Camera.main.GetComponent(); if (camShake != null) camShake.Shake(.3f); //dust effect animator.ShowDustEffectLand(); //stop sliding float t = 0; float speed = 2; Vector3 fromVelocity = rb.velocity; while (t<1){ SetVelocity(Vector3.Lerp (new Vector3(fromVelocity.x, rb.velocity.y + Physics.gravity.y * Time.fixedDeltaTime, fromVelocity.z), new Vector3(0, rb.velocity.y, 0), t)); t += Time.deltaTime * speed; yield return new WaitForFixedUpdate(); } //knockDown Timeout Move(Vector3.zero, 0f); yield return new WaitForSeconds(KnockdownTimeout); //stand up enemyState = UNITSTATE.STANDUP; animator.SetAnimatorTrigger ("StandUp"); Invoke("Ready", standUpTime); } //ground hit public IEnumerator GroundHit(){ CancelInvoke(); GlobalAudioPlayer.PlaySFXAtPosition ("EnemyGroundPunchHit", transform.position); animator.SetAnimatorTrigger ("GroundHit"); yield return new WaitForSeconds(KnockdownTimeout); if(!isDead) animator.SetAnimatorTrigger ("StandUp"); Invoke("Ready", standUpTime); } #endregion #region Movement public void Charge() { Move(moveDirection.normalized, walkSpeed); } //walk to target public void WalkTo(float proximityRange, float movementMargin){ Vector3 dirToTarget; LookAtTarget(target.transform); enemyState = UNITSTATE.WALK; //clamp zspread to attackDistance when ENGAGED, otherwise we might not be able to reach the player at all if (enemyTactic == ENEMYTACTIC.ENGAGE) { dirToTarget = target.transform.position - (transform.position + new Vector3 (0, 0, Mathf.Clamp(ZSpread, 0, attackRangeDistance))); } else { dirToTarget = target.transform.position - (transform.position + new Vector3 (0, 0, ZSpread)); } //we are too far away, move closer if (distance >= proximityRange ) { moveDirection = new Vector3(dirToTarget.x,0,dirToTarget.z); if (IsGrounded() && !WallSpotted() && !PitfallSpotted()) { Move(moveDirection.normalized, walkSpeed); animator.SetAnimatorFloat ("MovementSpeed", rb.velocity.sqrMagnitude); return; } } //we are too close, move away if (distance <= proximityRange - movementMargin) { moveDirection = new Vector3(-dirToTarget.x,0,0); if (IsGrounded() && !WallSpotted() && !PitfallSpotted()) { Move(moveDirection.normalized, walkBackwardSpeed); animator.SetAnimatorFloat ("MovementSpeed", -rb.velocity.sqrMagnitude); return; } } //otherwise do nothing Move(Vector3.zero, 0f); animator.SetAnimatorFloat ("MovementSpeed", 0); } //move towards a vector public void Move(Vector3 vector, float speed){ if(!NoMovementStates.Contains(enemyState)) { SetVelocity(new Vector3(vector.x * speed, rb.velocity.y + Physics.gravity.y * Time.fixedDeltaTime, vector.z * speed)); } else { SetVelocity(Vector3.zero); } } //returns true if there is an environment collider in front of us bool WallSpotted(){ Vector3 Offset = moveDirection.normalized * lookaheadDistance; Collider[] hitColliders = Physics.OverlapSphere (transform.position + capsule.center + Offset, capsule.radius, CollisionLayer); int i = 0; bool hasHitwall = false; while (i < hitColliders.Length) { if(CollisionLayer == (CollisionLayer | 1 << hitColliders[i].gameObject.layer)) { hasHitwall = true; } i++; } wallspotted = hasHitwall; return hasHitwall; } //returns true if there is a cliff in front of us bool PitfallSpotted(){ if (!ignoreCliffs) { float lookDownDistance = 1f; Vector3 StartPoint = transform.position + (Vector3.up * .3f) + (Vector3.right * (capsule.radius + lookaheadDistance) * moveDirection.normalized.x); RaycastHit hit; #if UNITY_EDITOR Debug.DrawRay (StartPoint, Vector3.down * lookDownDistance, Color.red); #endif if (!Physics.Raycast (StartPoint, Vector3.down, out hit, lookDownDistance, CollisionLayer)) { cliffSpotted = true; return true; } } cliffSpotted = false; return false; } //returns true if this unit is grounded public bool IsGrounded(){ float colliderSize = capsule.bounds.extents.y - .1f; if (Physics.CheckCapsule (capsule.bounds.center, capsule.bounds.center + Vector3.down*colliderSize, capsule.radius, CollisionLayer)) { isGrounded = true; return true; } else { isGrounded = false; return false; } } //turn towards a direction public void TurnToDir(DIRECTION dir) { transform.rotation = Quaternion.LookRotation(Vector3.forward * (int)dir); } #endregion //show hit effect public void ShowHitEffectAtPosition(Vector3 pos) { GameObject.Instantiate (Resources.Load ("HitEffect"), pos, Quaternion.identity); } //unit is ready for new actions public void Ready() { enemyState = UNITSTATE.IDLE; animator.SetAnimatorTrigger("Idle"); animator.SetAnimatorFloat ("MovementSpeed", 0f); Move(Vector3.zero, 0f); } //look at the current target public void LookAtTarget(Transform _target){ if(_target != null){ Vector3 newDir = Vector3.zero; int dir = _target.transform.position.x >= transform.position.x ? 1 : -1; currentDirection = (DIRECTION)dir; if (animator != null) animator.currentDirection = currentDirection; newDir = Vector3.RotateTowards(transform.forward, Vector3.forward * dir, rotationSpeed * Time.deltaTime, 0.0f); transform.rotation = Quaternion.LookRotation(newDir); } } //randomizes values public void SetRandomValues(){ walkSpeed *= Random.Range(.8f, 1.2f); walkBackwardSpeed *= Random.Range(.8f, 1.2f); attackInterval *= Random.Range(.7f, 1.5f); KnockdownTimeout *= Random.Range(.7f, 1.5f); KnockdownUpForce *= Random.Range(.8f, 1.2f); KnockbackForce *= Random.Range(.7f, 1.5f); } //destroy event public void DestroyUnit(){ if(OnUnitDestroy != null) OnUnitDestroy(gameObject); } //returns a random name string GetRandomName(){ if(enemyNamesList == null) { Debug.Log("no list of names was found, please create 'EnemyNames.txt' that contains a list of enemy names and put it in the 'Resources' folder."); return ""; } //convert the lines of the txt file to an array string data = enemyNamesList.ToString(); string cReturns = System.Environment.NewLine + "\n" + "\r"; string[] lines = data.Split(cReturns.ToCharArray()); //pick a random name from the list string name = ""; int cnt = 0; while(name.Length == 0 && cnt < 100) { int rand = Random.Range(0, lines.Length); name = lines[rand]; cnt += 1; } return name; } }