【バイオハザード風のゲームを作ってみよう。その12】

今回も下記動画をやっていきます。
youtu.be

今回はゾンビのアタック動作を実装していきます。

・ゾンビは、ラン後に攻撃モーションをします。mixamoから該当するアニメーションをダウンロードし、Zombieアニメーターコントローラーのoverrideレイヤーに下記のように設定します。

・ScriptableObject を作成します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//CreateAssetMenu()を記述することで、Projectビュー右クリック時のメニューに、指定したクラスのScriptableObjectを作成する項目を手っ取り早く追加できる。
[CreateAssetMenu(menuName = "A.I/Actions/Zombie Attack Action")]
public class ZombieAttackAction : ScriptableObject //EP12追加
{
    [Header("Attack Animation")]
    public string attackanimation;

    [Header("Attack Cooldown")]
    public float attackCooldown = 5f; //The time before the zombie can perform another attack (After performing this attack)

    [Header("Attack Angles & Distances")]
    public float maxmumAttackAngle=20f;     //The maximum angle of sight needed to perform THIS attack
    public float minimumAttackAngle=-20f;    //The minimum angle of sight needed to perform THIS attack
    public float minimumAttackDistance=1f; //The minimum distance from the current target needed to perform THIS attack
    public float maxmumAttackDistance=3.5f; //The maxmum distance from the current target needed to perform THIS attack
}

スクリプトを追加します。

public class AttackState : State //EP10追加 ゾンビ直下のstatesオブジェクトにアタッチ
{
    PurseTargetState purseTargetState; //EP12追加

    [Header("Zombie Attacks")]
    public ZombieAttackAction[] zombieAttackActions; //EP12追加

    [Header("Potential Attacks Performed Right Now")]
    public List<ZombieAttackAction> potentialAttacks; //EP12追加

    [Header("Current Attack Being Performed")]
    public ZombieAttackAction currentAttack; //EP12追加

    [Header("Stage Flags")]
    public bool hasPerformAttack; //EP12追加

    private void Awake()
    {
        purseTargetState = GetComponent<PurseTargetState>(); //EP12追加
    }

    public override State Tick(ZombieManager zombieManager)
    {
        zombieManager.animator.SetFloat("Vertical", 0f, 0.2f, Time.deltaTime);

        //if the zombie is being hurt, or is in some action, pause the state
        if (zombieManager.isPerformingAction) //EP11追加
        {
            zombieManager.animator.SetFloat("Vertical", 0f, 0.2f, Time.deltaTime);
            return this;
        }

        if(!hasPerformAttack && zombieManager.attackCoolDownTimer <= 0) //EP12追加
        {
            if(currentAttack == null)
            {
                //GET A NEW ATTACK BASED ON DISTANCE AND ANGLE FROM CURRENT TARGET
                GetNewAttack(zombieManager);
            }
            else
            {
                //ATTACK THE CURRENT TARGET
                AttackTarget(zombieManager);
            }
        }

        if (hasPerformAttack) //EP12追加
        {
            ResetStateFlags();
            return purseTargetState;
        }
        else
        {
            return this;
        }

    }

    //EP12追加
    private void GetNewAttack(ZombieManager zombieManager)
    {
        for(int i = 0; i < zombieAttackActions.Length; i++)
        {
            ZombieAttackAction zombieAttack = zombieAttackActions[i];

            //Check for attackdistanced needed to perform the potential attack
            if(zombieManager.distanceFromCurrentTarget <= zombieAttack.maxmumAttackDistance &&
                zombieManager.distanceFromCurrentTarget >= zombieAttack.minimumAttackDistance)
            {
                //Check for attack angles, needed to perform the potential attack
                if(zombieManager.viewAngleFromCurrentTarget<=zombieAttack.maxmumAttackAngle &&
                    zombieManager.viewAngleFromCurrentTarget >= zombieAttack.minimumAttackAngle)
                {
                    //if the attack passes the distance and angle, add it to the list of attacks we MAY preform right now
                    potentialAttacks.Add(zombieAttack);
                }
            }
        }

        int randomValue = Random.Range(0, potentialAttacks.Count); //EP12追加

        if (potentialAttacks.Count > 0) //EP12追加
        {
            currentAttack = potentialAttacks[randomValue];
            potentialAttacks.Clear();
        }   
    }

    //EP12追加
    private void AttackTarget(ZombieManager zombieManager)
    {
        if (currentAttack != null)
        {
            hasPerformAttack = true;
            zombieManager.attackCoolDownTimer = currentAttack.attackCooldown;
            zombieManager.ZombieAnimationManager.PlayTargetAttackAnimation(currentAttack.attackanimation);
        }
        else
        {
            //Debug.Log の派生で警告メッセージをコンソール出力
            Debug.LogWarning("WARNING: ZoMbie is attempting to perform an attack,but has no attack");
        }
    }

    //EP12追加
    private void ResetStateFlags()
    {
        hasPerformAttack = false;
    }
}
public class ZombieManager : MonoBehaviour //EP8追加 ゾンビオブジェクトにアタッチ
{
    public ZombieAnimationManager ZombieAnimationManager; //EP12追加

    //The state this character begins on
    public IdleState startingState;

    [Header("Flags")]
    public bool isPerformingAction; //EP11追加

    [Header("Current State")]
    //The state this character is currently on
    [SerializeField] private State currentState;

    [Header("Current Target")]
    public PlayerManager currentTarget; //EP9追加
    public Vector3 targetDirection; //EP12追加
    public float distanceFromCurrentTarget; //EP10追加
    public float viewAngleFromCurrentTarget; //EP12追加

    [Header("Animator")] //EP10追加
    public Animator animator;

    [Header("Navmesh Agent")]
    public NavMeshAgent zombieNavmeshAgent; //EP10追加

    [Header("RigidBody")]
    public Rigidbody zombieRigidbody; //EP10追加

    [Header("Locomotion")]
    public float rotationSpeed = 5f; //EP10追加

    [Header("Attack")]
    public float attackCoolDownTimer; //EP12追加
    public float minimumAttackDistance = 1f; //EP10追加 SET THIS TO THE NUMBER OF YOUR MINIMIM ATTACK DISTANCE,OF THE SHOREST RANGE ATTACK
    public float maximumAttackDistance = 3.5f; //EP12追加 SET THIS TO THE NUMBER OF YOUR MAXMIM ATTACK DISTANCE,OF THE LONGEST RANGE ATTACK

    private void Awake()
    {
        currentState = startingState;
        zombieNavmeshAgent = GetComponentInChildren<NavMeshAgent>(); //EP10追加
        animator = GetComponent<Animator>(); //EP10追加
        zombieRigidbody = GetComponent<Rigidbody>(); //EP10追加
        ZombieAnimationManager = GetComponent<ZombieAnimationManager>(); //EP12追加
    }

    private void FixedUpdate()
    {
        HandleStateMachine();
    }

    private void Update() //EP10追加
    {
        zombieNavmeshAgent.transform.localPosition = Vector3.zero;

        if (attackCoolDownTimer > 0) //EP12追加
        {
            attackCoolDownTimer = attackCoolDownTimer - Time.deltaTime;
        }

        if (currentTarget != null) //EP10追加
        {
            targetDirection = currentTarget.transform.position - transform.position; //EP12追加
            viewAngleFromCurrentTarget = Vector3.SignedAngle(targetDirection, transform.forward, Vector3.up); //EP12追加

            //ターゲットと自身の距離を計算
            distanceFromCurrentTarget = Vector3.Distance(currentTarget.transform.position, transform.position);
        }
    }

    private void HandleStateMachine()
    {
        State nextState;

        if (currentState != null)
        {
            nextState = currentState.Tick(this);

            if (nextState != null)
            {
                currentState = nextState;
            }
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ZombieAnimationManager : MonoBehaviour //EP12追加 ゾンビにアタッチ
{
    ZombieManager zombieManager;

    private void Awake()
    {
        zombieManager = GetComponent<ZombieManager>();
    }

    public void PlayTargetAttackAnimation(string attackAnimation)
    {
        //appapplyRootMotion = trueで座標移動が含まれるアニメーションをそのまま再生する
        zombieManager.animator.applyRootMotion = true;
        zombieManager.isPerformingAction = true;
        zombieManager.animator.CrossFade(attackAnimation, 0.2f);
    }
}
public class PurseTargetState : State //EP8追加 ゾンビ直下のstatesオブジェクトにアタッチ
{
    AttackState attackState; //EP10追加

    private void Awake()
    {
        attackState = GetComponent<AttackState>(); //EP10追加
    }

    public override State Tick(ZombieManager zombieManager)
    {
        //if the zombie is being hurt, or is in some action, pause the state
        if (zombieManager.isPerformingAction) //EP11追加
        {
            zombieManager.animator.SetFloat("Vertical", 0f, 0.2f, Time.deltaTime);
            return this;
        }
           

        Debug.Log("Running pursue target state");
        MoveTowardsCurrentTarget(zombieManager); //EP10追加
        RotateTowardsTarget(zombieManager); //EP10追加

        if (zombieManager.distanceFromCurrentTarget <= zombieManager.maximumAttackDistance) //EP12追加
        {
            zombieManager.zombieNavmeshAgent.enabled = false; //EP12追加
            return attackState;
        }
        else
        {
            return this;
        }
        
    }

    private void MoveTowardsCurrentTarget(ZombieManager zombieManager) //EP10追加
    {
        //walkアニメーションに切り替え
        zombieManager.animator.SetFloat("Vertical", 1f, 0.2f, Time.deltaTime);
    }

    private void RotateTowardsTarget(ZombieManager zombieManager) //EP10追加
    {
        zombieManager.zombieNavmeshAgent.enabled = true;
        //移動先に移動開始
        zombieManager.zombieNavmeshAgent.SetDestination(zombieManager.currentTarget.transform.position);
        //ターゲット方向に回転
        zombieManager.transform.rotation = Quaternion.Slerp(zombieManager.transform.rotation,
            zombieManager.zombieNavmeshAgent.transform.rotation, zombieManager.rotationSpeed / Time.deltaTime);
            
    
    }
}

プレイ動画はこんな感じです。
youtu.be

今回は以上です。