【在Unity实现《 Batman:Arkham Series(蝙蝠侠:阿卡姆系列)》中行云流水的打斗程序_内附源工程】【转载搬运】

本文详细介绍了如何在Unity的HDRP环境下,实现类似《Batman: Arkham Series》中流畅的战斗系统,包括角色控制器、敌人监测、玩家移动、Mixamo动画、冲击粒子效果、敌人行为树、计数器系统以及敌人伤害和最后一击的逻辑。通过源代码和步骤解析,帮助读者理解游戏中的战斗机制实现。
摘要由CSDN通过智能技术生成

简介

  • 《 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

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

}

最终效果

相关信息

博主的YouTube链接
项目工程Github链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暴走约伯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值