流畅的智能打斗程序【转载搬运】
简介
- 《 Batman:Arkham Series(蝙蝠侠:阿卡姆系列)》中行云流水的战斗如图
步骤
- 第一步:构建负责监测敌人和激活动画的基础战斗功能
- 第二步:创建可以攻击和移动的敌人,并为玩家添加反击系统
- 第三步:建立管理敌人状态的AI,方便创建一个简单的战斗循环
项目创建
首先在HDRP下创建一个新项目
利用示例场景,放置一个人物,满足简单的人物移动和镜头跟随功能。再加入几个不同待机角色当做敌人。
角色控制器组件MovementInput源代码加注解
// 使用Unity引擎
using UnityEngine;
// 使用Unity输入系统
using UnityEngine.InputSystem;
// 需要角色控制器组件
[RequireComponent(typeof(CharacterController))]
// 定义一个名为MovementInput的公共类,继承自MonoBehaviour
public class MovementInput : MonoBehaviour
{
// 定义私有变量
private Animator anim; // 动画
private Camera cam; // 摄像机
private CharacterController controller; // 角色控制器
private Vector3 desiredMoveDirection; // 期望的移动方向
private Vector3 moveVector; // 移动向量
public Vector2 moveAxis; // 移动轴
private float verticalVel; // 垂直速度
// 设置
[Header("Settings")]
[SerializeField] float movementSpeed; // 移动速度
[SerializeField] float rotationSpeed = 0.1f; // 旋转速度
[SerializeField] float fallSpeed = .2f; // 下落速度
public float acceleration = 1; // 加速度
// 布尔值
[Header("Booleans")]
[SerializeField] bool blockRotationPlayer; // 是否阻止玩家旋转
private bool isGrounded; // 是否在地面上
// 在游戏开始时执行
void Start()
{
anim = this.GetComponent<Animator>(); // 获取动画组件
cam = Camera.main; // 获取主摄像机
controller = this.GetComponent<CharacterController>(); // 获取角色控制器组件
}
// 每帧更新时执行
void Update()
{
InputMagnitude(); // 计算输入大小
isGrounded = controller.isGrounded; // 判断是否在地面上
if (isGrounded)
verticalVel -= 0; // 如果在地面上,垂直速度减0
else
verticalVel -= 1; // 如果在空中,垂直速度减1
moveVector = new Vector3(0, verticalVel * fallSpeed * Time.deltaTime, 0); // 计算移动向量
controller.Move(moveVector); // 控制器移动
}
// 玩家移动和旋转
void PlayerMoveAndRotation()
{
var camera = Camera.main; // 获取主摄像机
var forward = cam.transform.forward; // 获取摄像机前方向
var right = cam.transform.right; // 获取摄像机右方向
forward.y = 0f; // 设置前方向的y值为0
right.y = 0f; // 设置右方向的y值为0
forward.Normalize(); // 前方向归一化
right.Normalize(); // 右方向归一化
desiredMoveDirection = forward * moveAxis.y + right * moveAxis.x; // 计算期望的移动方向
if (blockRotationPlayer == false) // 如果不阻止玩家旋转
{
// 摄像机
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(desiredMoveDirection), rotationSpeed * acceleration); // 旋转
controller.Move(desiredMoveDirection * Time.deltaTime * (movementSpeed * acceleration)); // 移动
}
else // 如果阻止玩家旋转
{
// Strafe
controller.Move((transform.forward * moveAxis.y + transform.right * moveAxis.y) * Time.deltaTime * (movementSpeed * acceleration)); // 移动
}
}
// 看向某个位置
public void LookAt(Vector3 pos)
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(pos), rotationSpeed); // 旋转
}
// 旋转到摄像机
public void RotateToCamera(Transform t)
{
var forward = cam.transform.forward; // 获取摄像机前方向
desiredMoveDirection = forward; // 设置期望的移动方向
Quaternion lookAtRotation = Quaternion.LookRotation(desiredMoveDirection); // 计算看向的旋转
Quaternion lookAtRotationOnly_Y = Quaternion.Euler(transform.rotation.eulerAngles.x, lookAtRotation.eulerAngles.y, transform.rotation.eulerAngles.z); // 计算只在Y轴旋转的旋转
t.rotation = Quaternion.Slerp(transform.rotation, lookAtRotationOnly_Y, rotationSpeed); // 旋转
}
// 计算输入大小
void InputMagnitude()
{
// 计算输入大小
float inputMagnitude = new Vector2(moveAxis.x, moveAxis.y).sqrMagnitude;
// 物理移动玩家
if (inputMagnitude > 0.1f) // 如果输入大小大于0.1
{
anim.SetFloat("InputMagnitude", inputMagnitude * acceleration, .1f, Time.deltaTime); // 设置动画的输入大小
PlayerMoveAndRotation(); // 玩家移动和旋转
}
else // 如果输入大小小于等于0.1
{
anim.SetFloat("InputMagnitude", inputMagnitude * acceleration, .1f,Time.deltaTime); // 设置动画的输入大小
}
}
// 输入
#region Input
// 移动
public void OnMove(InputValue value)
{
moveAxis.x = value.Get<Vector2>().x; // 设置移动轴的x值
moveAxis.y = value.Get<Vector2>().y; // 设置移动轴的y值
}
#endregion
// 当禁用时
private void OnDisable()
{
anim.SetFloat("InputMagnitude", 0); // 设置动画的输入大小为0
}
}
敌方监测
- 《 Batman:Arkham Series(蝙蝠侠:阿卡姆系列)》中首要特征是允许玩家通过移动手柄摇杆来选择要进攻的目标敌人。
导入Input System
- 这样可以获取角色的移动量来识别角色相对于相机的方向。
inputDirection = forward * movementInput.moveAxis.y + right * movementInput.moveAxis.x;
inputDirection = inputDirection.normalized;
- 使用该值进行球形射线监测该路径上是否有敌人
if (Physics.SphereCast(transform.position, 3f, inputDirection, out info, 10,layerMask))
{
if(info.collider.transform.GetComponent<EnemyScript>().IsAttackable())
currentTarget = info.collider.transform.GetComponent<EnemyScript>();
}
- 并将监测到的敌人作为变量:"currentTarget "存储在脚本中。
Input System:
EnemyDetection的完整的代码
using System.Collections.Generic;
using UnityEngine;
public class EnemyDetection : MonoBehaviour
{
[SerializeField] private EnemyManager enemyManager;
private MovementInput movementInput;
private CombatScript combatScript;
public LayerMask layerMask;
[SerializeField] Vector3 inputDirection;
[SerializeField] private EnemyScript currentTarget;
public GameObject cam;
private void Start()
{
movementInput = GetComponentInParent<MovementInput>();
combatScript = GetComponentInParent<CombatScript>();
}
private void Update()
{
var camera = Camera.main;
var forward = camera.transform.forward;
var right = camera.transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
inputDirection = forward * movementInput.moveAxis.y + right * movementInput.moveAxis.x;
inputDirection = inputDirection.normalized;
RaycastHit info;
if (Physics.SphereCast(transform.position, 3f, inputDirection, out info, 10,layerMask))
{
if(info.collider.transform.GetComponent<EnemyScript>().IsAttackable())
currentTarget = info.collider.transform.GetComponent<EnemyScript>();
}
}
public EnemyScript CurrentTarget()
{
return currentTarget;
}
public void SetCurrentTarget(EnemyScript target)
{
currentTarget = target;
}
public float InputMagnitude()
{
return inputDirection.magnitude;
}
private void OnDrawGizmos()
{
Gizmos.color = Color.black;
Gizmos.DrawRay(transform.position, inputDirection);
Gizmos.DrawWireSphere(transform.position, 1);
if(CurrentTarget() != null)
Gizmos.DrawSphere(CurrentTarget().transform.position, .5f);
}
}
玩家移动(DOTTween)
- 有了这个监测机制后,还需要实现在玩家选择攻击后向敌人移动。这实现起来方法有很多。
- 这里选择插件DOTTween,去简化实现流程。
- 然后调用功能能够在特定持续时间内看向目标并同时向目标移动。
void MoveTorwardsTarget(EnemyScript target, float duration)
{
OnTrajectory.Invoke(target);
transform.DOLookAt(target.transform.position, .2f);
transform.DOMove(TargetOffset(target.transform), duration);
}
同时在这个功能中
- 允许玩家多次攻击并创建一个协程做为攻击冷却。
- 同时具备禁用玩家的移动并激活
- “public bool isAttackingEnemy = false;”一小段时间。
Mixamo动画
添加攻击动画:
mixamo链接 好用!!!
- 每个动画,都需要去检测关键帧并且在动作表示接触攻击敌人的那一帧设定一个特定的动画事件。
- 这个事件会调用所有敌人都会监听的一个函数。
playerCombat.OnHit.AddListener((x) => OnPlayerHit(x));
冲击粒子
-
加入使用HDRP Unlit材质的例子系统来产生空间扭曲,实现击打时的冲击特效。
-
当战斗脚本监测的敌人目标和当前打击目标相同才会启用其中的受击功能。
下面是核心功能代码:
void OnPlayerHit(EnemyScript target)
{
if (target == this)
{
animator.SetTrigger("Hit");
}
}
完整结构代码:
void OnPlayerHit(EnemyScript target)
{
if (target == this)
{
StopEnemyCoroutines();
DamageCoroutine = StartCoroutine(HitCoroutine());
enemyDetection.SetCurrentTarget(null);
isLockedTarget = false;
OnDamage.Invoke(this);
health--;
if (health <= 0)
{
Death();
return;
}
animator.SetTrigger("Hit");
transform.DOMove(transform.position - (transform.forward / 2), .3f).SetDelay(.1f);
StopMoving();
}
IEnumerator HitCoroutine()
{
isStunned = true;
yield return new WaitForSeconds(.5f);
isStunned = false;
}
}
敌人行为树
- 下面给敌人添加移动功能,面向和围绕玩家移动,而不是简单的左右移动。敌人需要始终面对玩家, 垂直方向或者朝着玩家进行移动。
敌人的攻击
- 敌人可以靠近玩家,也会攻击玩家,当敌人靠近玩家一定的距离,敌人会激活攻击动画和特效。
if(Vector3.Distance(transform.position, playerCombat.transform.position) < 2)
{
StopMoving();
if (!playerCombat.isCountering && !playerCombat.isAttackingEnemy)
Attack();
else
PrepareAttack(false);
}
- 和设置玩家动画一样,在攻击的那一帧调用动画事件,并且还向玩家的脚本发送攻击信息。
计数器系统
- 玩家:这时候,给玩家添加反击功能,在之前的攻击冷却协程当中已经激活了朝向敌人移动的功能,
public void SetAttack()
{
isWaiting = false;
PrepareAttackCoroutine = StartCoroutine(PrepAttack());
IEnumerator PrepAttack()
{
PrepareAttack(true);
yield return new WaitForSeconds(.2f);
moveDirection = Vector3.forward;
isMoving = true;
}
}
- 调用一个函数来激活==IEnumerator PrepAttack()==变量函数,这意味着在敌人攻击的任何时还可玩家都可以反击。
创建粒子系统
- 为了更加清晰的提醒玩家敌人可被攻击,用一些自定义脚本来创建粒子系统。
在CombatScript脚本中
- 创建一个会在反击键被按下时调用的函数“void CounterCheck()”。
- 这个 lockedTarget = ClosestCounterEnemy() 用来监测敌人是否在准备攻击
- 如果是的,则玩家会锁定该敌人为反击对象:“Attack(lockedTarget, TargetDistance(lockedTarget));”
//在反击键被按下时调用的函数
void CounterCheck()
{
//Initial check
if (isCountering || isAttackingEnemy || !enemyManager.AnEnemyIsPreparingAttack())
return;
//用来监测敌人是否在准备攻击
lockedTarget = ClosestCounterEnemy();
OnCounterAttack.Invoke(lockedTarget);
if (TargetDistance(lockedTarget) > 2)
{
//如果是的,则玩家会锁定该敌人为反击对象
Attack(lockedTarget, TargetDistance(lockedTarget));
return;
}
float duration = .2f;
animator.SetTrigger("Dodge");
transform.DOLookAt(lockedTarget.transform.position, .2f);
transform.DOMove(transform.position + lockedTarget.transform.forward, duration);
if (counterCoroutine != null)
StopCoroutine(counterCoroutine);
counterCoroutine = StartCoroutine(CounterCoroutine(duration));
IEnumerator CounterCoroutine(float duration)
{
isCountering = true;
movementInput.enabled = false;
yield return new WaitForSeconds(duration);
Attack(lockedTarget, TargetDistance(lockedTarget));
isCountering = false;
}
}
敌人伤害和最后一击逻辑
- EnemyScript 添加一个简单的血条量,每被攻击一次减少一定的数值。
- 一旦Health为零的时候,敌人会触发死亡动画,不在作为敌人目标被检测。
当玩家给敌人最后一击的时候,镜头特写功能实现
- 摄像机会聚焦于最后一击,并且放慢时间速度。于是创建一个新的虚拟摄像机来复刻这个效果。
- 新建一个摄像机节点,命名“TargetGroup”,为其i添加一个“CinemaChineTargetGroup”组件,其中包括了玩家和特定敌人目标。
- 当玩家击倒最后的一个玩家后,创建了一个协程减少TimeScale并激活第二个虚拟相机去聚焦攻击动作。
在CombatScript代码中
IEnumerator FinalBlowCoroutine()
{
Time.timeScale = .5f;
lastHitCamera.SetActive(true);
lastHitFocusObject.position = lockedTarget.transform.position;
yield return new WaitForSecondsRealtime(2);
lastHitCamera.SetActive(false);
Time.timeScale = 1f;
}
基础AI
- 准备好上述的准备工作后,创建一个简易的敌人AI。
- 敌人一共总结出四种状态。
- 待机状态、进攻、后退和包围玩家
- 脚本会设定随机数量敌人处于进攻状态,或者设置其他敌人处于休眠或者包围状态。
- 在状态间做切换,等处于进攻状态的敌人完成行动后,将状态设置为后退,然后循环。
- 需要确保循环不破坏其他的逻辑。
EnemyScript 的完整代码:
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
public class EnemyScript : MonoBehaviour
{
//Declarations
private Animator animator;
private CombatScript playerCombat;
private EnemyManager enemyManager;
private EnemyDetection enemyDetection;
private CharacterController characterController;
[Header("Stats")]
public int health = 3;
private float moveSpeed = 1;
private Vector3 moveDirection;
[Header("States")]
[SerializeField] private bool isPreparingAttack;
[SerializeField] private bool isMoving;
[SerializeField] private bool isRetreating;
[SerializeField] private bool isLockedTarget;
[SerializeField] private bool isStunned;
[SerializeField] private bool isWaiting = true;
[Header("Polish")]
[SerializeField] private ParticleSystem counterParticle;
private Coroutine PrepareAttackCoroutine;
private Coroutine RetreatCoroutine;
private Coroutine DamageCoroutine;
private Coroutine MovementCoroutine;
//Events
public UnityEvent<EnemyScript> OnDamage;
public UnityEvent<EnemyScript> OnStopMoving;
public UnityEvent<EnemyScript> OnRetreat;
void Start()
{
enemyManager = GetComponentInParent<EnemyManager>();
animator = GetComponent<Animator>();
characterController = GetComponent<CharacterController>();
playerCombat = FindObjectOfType<CombatScript>();
enemyDetection = playerCombat.GetComponentInChildren<EnemyDetection>();
playerCombat.OnHit.AddListener((x) => OnPlayerHit(x));
playerCombat.OnCounterAttack.AddListener((x) => OnPlayerCounter(x));
playerCombat.OnTrajectory.AddListener((x) => OnPlayerTrajectory(x));
MovementCoroutine = StartCoroutine(EnemyMovement());
}
IEnumerator EnemyMovement()
{
//Waits until the enemy is not assigned to no action like attacking or retreating
yield return new WaitUntil(() => isWaiting == true);
int randomChance = Random.Range(0, 2);
if (randomChance == 1)
{
int randomDir = Random.Range(0, 2);
moveDirection = randomDir == 1 ? Vector3.right : Vector3.left;
isMoving = true;
}
else
{
StopMoving();
}
yield return new WaitForSeconds(1);
MovementCoroutine = StartCoroutine(EnemyMovement());
}
void Update()
{
//Constantly look at player
transform.LookAt(new Vector3(playerCombat.transform.position.x, transform.position.y, playerCombat.transform.position.z));
//Only moves if the direction is set
MoveEnemy(moveDirection);
}
//Listened event from Player Animation
void OnPlayerHit(EnemyScript target)
{
if (target == this)
{
StopEnemyCoroutines();
DamageCoroutine = StartCoroutine(HitCoroutine());
enemyDetection.SetCurrentTarget(null);
isLockedTarget = false;
OnDamage.Invoke(this);
health--;
if (health <= 0)
{
Death();
return;
}
animator.SetTrigger("Hit");
transform.DOMove(transform.position - (transform.forward / 2), .3f).SetDelay(.1f);
StopMoving();
}
IEnumerator HitCoroutine()
{
isStunned = true;
yield return new WaitForSeconds(.5f);
isStunned = false;
}
}
void OnPlayerCounter(EnemyScript target)
{
if (target == this)
{
PrepareAttack(false);
}
}
void OnPlayerTrajectory(EnemyScript target)
{
if (target == this)
{
StopEnemyCoroutines();
isLockedTarget = true;
PrepareAttack(false);
StopMoving();
}
}
void Death()
{
StopEnemyCoroutines();
this.enabled = false;
characterController.enabled = false;
animator.SetTrigger("Death");
enemyManager.SetEnemyAvailiability(this, false);
}
public void SetRetreat()
{
StopEnemyCoroutines();
RetreatCoroutine = StartCoroutine(PrepRetreat());
IEnumerator PrepRetreat()
{
yield return new WaitForSeconds(1.4f);
OnRetreat.Invoke(this);
isRetreating = true;
moveDirection = -Vector3.forward;
isMoving = true;
yield return new WaitUntil(() => Vector3.Distance(transform.position, playerCombat.transform.position) > 4);
isRetreating = false;
StopMoving();
//Free
isWaiting = true;
MovementCoroutine = StartCoroutine(EnemyMovement());
}
}
public void SetAttack()
{
isWaiting = false;
PrepareAttackCoroutine = StartCoroutine(PrepAttack());
IEnumerator PrepAttack()
{
PrepareAttack(true);
yield return new WaitForSeconds(.2f);
moveDirection = Vector3.forward;
isMoving = true;
}
}
void PrepareAttack(bool active)
{
isPreparingAttack = active;
if (active)
{
counterParticle.Play();
}
else
{
StopMoving();
counterParticle.Clear();
counterParticle.Stop();
}
}
void MoveEnemy(Vector3 direction)
{
//Set movespeed based on direction
moveSpeed = 1;
if (direction == Vector3.forward)
moveSpeed = 5;
if (direction == -Vector3.forward)
moveSpeed = 2;
//Set Animator values
animator.SetFloat("InputMagnitude", (characterController.velocity.normalized.magnitude * direction.z) / (5 / moveSpeed), .2f, Time.deltaTime);
animator.SetBool("Strafe", (direction == Vector3.right || direction == Vector3.left));
animator.SetFloat("StrafeDirection", direction.normalized.x, .2f, Time.deltaTime);
//Don't do anything if isMoving is false
if (!isMoving)
return;
Vector3 dir = (playerCombat.transform.position - transform.position).normalized;
Vector3 pDir = Quaternion.AngleAxis(90, Vector3.up) * dir; //Vector perpendicular to direction
Vector3 movedir = Vector3.zero;
Vector3 finalDirection = Vector3.zero;
if (direction == Vector3.forward)
finalDirection = dir;
if (direction == Vector3.right || direction == Vector3.left)
finalDirection = (pDir * direction.normalized.x);
if (direction == -Vector3.forward)
finalDirection = -transform.forward;
if (direction == Vector3.right || direction == Vector3.left)
moveSpeed /= 1.5f;
movedir += finalDirection * moveSpeed * Time.deltaTime;
characterController.Move(movedir);
if (!isPreparingAttack)
return;
if(Vector3.Distance(transform.position, playerCombat.transform.position) < 2)
{
StopMoving();
if (!playerCombat.isCountering && !playerCombat.isAttackingEnemy)
Attack();
else
PrepareAttack(false);
}
}
private void Attack()
{
transform.DOMove(transform.position + (transform.forward / 1), .5f);
animator.SetTrigger("AirPunch");
}
public void HitEvent()
{
if(!playerCombat.isCountering && !playerCombat.isAttackingEnemy)
playerCombat.DamageEvent();
PrepareAttack(false);
}
public void StopMoving()
{
isMoving = false;
moveDirection = Vector3.zero;
if(characterController.enabled)
characterController.Move(moveDirection);
}
void StopEnemyCoroutines()
{
PrepareAttack(false);
if (isRetreating)
{
if (RetreatCoroutine != null)
StopCoroutine(RetreatCoroutine);
}
if (PrepareAttackCoroutine != null)
StopCoroutine(PrepareAttackCoroutine);
if(DamageCoroutine != null)
StopCoroutine(DamageCoroutine);
if (MovementCoroutine != null)
StopCoroutine(MovementCoroutine);
}
#region Public Booleans
public bool IsAttackable()
{
return health > 0;
}
public bool IsPreparingAttack()
{
return isPreparingAttack;
}
public bool IsRetreating()
{
return isRetreating;
}
public bool IsLockedTarget()
{
return isLockedTarget;
}
public bool IsStunned()
{
return isStunned;
}
#endregion
}
CombatScript的完整代码:
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
using Cinemachine;
public class CombatScript : MonoBehaviour
{
private EnemyManager enemyManager;
private EnemyDetection enemyDetection;
private MovementInput movementInput;
private Animator animator;
private CinemachineImpulseSource impulseSource;
[Header("Target")]
private EnemyScript lockedTarget;
[Header("Combat Settings")]
[SerializeField] private float attackCooldown;
[Header("States")]
public bool isAttackingEnemy = false;
public bool isCountering = false;
[Header("Public References")]
[SerializeField] private Transform punchPosition;
[SerializeField] private ParticleSystemScript punchParticle;
[SerializeField] private GameObject lastHitCamera;
[SerializeField] private Transform lastHitFocusObject;
//Coroutines
private Coroutine counterCoroutine;
private Coroutine attackCoroutine;
private Coroutine damageCoroutine;
[Space]
//Events
public UnityEvent<EnemyScript> OnTrajectory;
public UnityEvent<EnemyScript> OnHit;
public UnityEvent<EnemyScript> OnCounterAttack;
int animationCount = 0;
string[] attacks;
void Start()
{
enemyManager = FindObjectOfType<EnemyManager>();
animator = GetComponent<Animator>();
enemyDetection = GetComponentInChildren<EnemyDetection>();
movementInput = GetComponent<MovementInput>();
impulseSource = GetComponentInChildren<CinemachineImpulseSource>();
}
//This function gets called whenever the player inputs the punch action
void AttackCheck()
{
if (isAttackingEnemy)
return;
//Check to see if the detection behavior has an enemy set
if (enemyDetection.CurrentTarget() == null)
{
if (enemyManager.AliveEnemyCount() == 0)
{
Attack(null, 0);
return;
}
else
{
lockedTarget = enemyManager.RandomEnemy();
}
}
//If the player is moving the movement input, use the "directional" detection to determine the enemy
if (enemyDetection.InputMagnitude() > .2f)
lockedTarget = enemyDetection.CurrentTarget();
//Extra check to see if the locked target was set
if(lockedTarget == null)
lockedTarget = enemyManager.RandomEnemy();
//AttackTarget
Attack(lockedTarget, TargetDistance(lockedTarget));
}
public void Attack(EnemyScript target, float distance)
{
//Types of attack animation
attacks = new string[] { "AirKick", "AirKick2", "AirPunch", "AirKick3" };
//Attack nothing in case target is null
if (target == null)
{
AttackType("GroundPunch", .2f, null, 0);
return;
}
if (distance < 15)
{
animationCount = (int)Mathf.Repeat((float)animationCount + 1, (float)attacks.Length);
string attackString = isLastHit() ? attacks[Random.Range(0, attacks.Length)] : attacks[animationCount];
AttackType(attackString, attackCooldown, target, .65f);
}
else
{
lockedTarget = null;
AttackType("GroundPunch", .2f, null, 0);
}
//Change impulse
impulseSource.m_ImpulseDefinition.m_AmplitudeGain = Mathf.Max(3, 1 * distance);
}
void AttackType(string attackTrigger, float cooldown, EnemyScript target, float movementDuration)
{
animator.SetTrigger(attackTrigger);
if (attackCoroutine != null)
StopCoroutine(attackCoroutine);
attackCoroutine = StartCoroutine(AttackCoroutine(isLastHit() ? 1.5f : cooldown));
//Check if last enemy
if (isLastHit())
StartCoroutine(FinalBlowCoroutine());
if (target == null)
return;
target.StopMoving();
MoveTorwardsTarget(target, movementDuration);
IEnumerator AttackCoroutine(float duration)
{
movementInput.acceleration = 0;
isAttackingEnemy = true;
movementInput.enabled = false;
yield return new WaitForSeconds(duration);
isAttackingEnemy = false;
yield return new WaitForSeconds(.2f);
movementInput.enabled = true;
LerpCharacterAcceleration();
}
IEnumerator FinalBlowCoroutine()
{
Time.timeScale = .5f;
lastHitCamera.SetActive(true);
lastHitFocusObject.position = lockedTarget.transform.position;
yield return new WaitForSecondsRealtime(2);
lastHitCamera.SetActive(false);
Time.timeScale = 1f;
}
}
void MoveTorwardsTarget(EnemyScript target, float duration)
{
OnTrajectory.Invoke(target);
transform.DOLookAt(target.transform.position, .2f);
transform.DOMove(TargetOffset(target.transform), duration);
}
void CounterCheck()
{
//Initial check
if (isCountering || isAttackingEnemy || !enemyManager.AnEnemyIsPreparingAttack())
return;
lockedTarget = ClosestCounterEnemy();
OnCounterAttack.Invoke(lockedTarget);
if (TargetDistance(lockedTarget) > 2)
{
Attack(lockedTarget, TargetDistance(lockedTarget));
return;
}
float duration = .2f;
animator.SetTrigger("Dodge");
transform.DOLookAt(lockedTarget.transform.position, .2f);
transform.DOMove(transform.position + lockedTarget.transform.forward, duration);
if (counterCoroutine != null)
StopCoroutine(counterCoroutine);
counterCoroutine = StartCoroutine(CounterCoroutine(duration));
IEnumerator CounterCoroutine(float duration)
{
isCountering = true;
movementInput.enabled = false;
yield return new WaitForSeconds(duration);
Attack(lockedTarget, TargetDistance(lockedTarget));
isCountering = false;
}
}
float TargetDistance(EnemyScript target)
{
return Vector3.Distance(transform.position, target.transform.position);
}
public Vector3 TargetOffset(Transform target)
{
Vector3 position;
position = target.position;
return Vector3.MoveTowards(position, transform.position, .95f);
}
public void HitEvent()
{
if (lockedTarget == null || enemyManager.AliveEnemyCount() == 0)
return;
OnHit.Invoke(lockedTarget);
//Polish
punchParticle.PlayParticleAtPosition(punchPosition.position);
}
public void DamageEvent()
{
animator.SetTrigger("Hit");
if (damageCoroutine != null)
StopCoroutine(damageCoroutine);
damageCoroutine = StartCoroutine(DamageCoroutine());
IEnumerator DamageCoroutine()
{
movementInput.enabled = false;
yield return new WaitForSeconds(.5f);
movementInput.enabled = true;
LerpCharacterAcceleration();
}
}
EnemyScript ClosestCounterEnemy()
{
float minDistance = 100;
int finalIndex = 0;
for (int i = 0; i < enemyManager.allEnemies.Length; i++)
{
EnemyScript enemy = enemyManager.allEnemies[i].enemyScript;
if (enemy.IsPreparingAttack())
{
if (Vector3.Distance(transform.position, enemy.transform.position) < minDistance)
{
minDistance = Vector3.Distance(transform.position, enemy.transform.position);
finalIndex = i;
}
}
}
return enemyManager.allEnemies[finalIndex].enemyScript;
}
void LerpCharacterAcceleration()
{
movementInput.acceleration = 0;
DOVirtual.Float(0, 1, .6f, ((acceleration)=> movementInput.acceleration = acceleration));
}
bool isLastHit()
{
if (lockedTarget == null)
return false;
return enemyManager.AliveEnemyCount() == 1 && lockedTarget.health <= 1;
}
#region Input
private void OnCounter()
{
CounterCheck();
}
private void OnAttack()
{
AttackCheck();
}
#endregion
}