상세 컨텐츠

본문 제목

유니티 유한상태기계 - FSM(Finite State Machine) 2

카테고리 없음

by MJ_119 2025. 1. 25. 23:16

본문

유니티 유한상태기계 - FSM(Finite State Machine) 2

 

FSM 1의 기본틀에다가 이것저것 기능들 추가함

 

 

Enemy 스크립트 :

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

public class Enemy : MonoBehaviour
{
    [Header("Idle data")]
    public float idleTime;
    public float aggressionRange;


    [Header("Move data")]
    public float moveSpeed;
    public float chaseSpeed;
    private bool manualMovement;
    private bool manualRotation;

    public Transform[] patrolPoints;
    int currentPatrolIndex;
    public int trunSpeed;


    public Transform player { get; private set; }
    public NavMeshAgent agent { get; private set; }
    public EnemyStateMachine enemyStateMachine { get; private set; }

    public Animator anim { get; private set; }

    protected virtual void Awake()
    {
        enemyStateMachine = new EnemyStateMachine();

        agent = GetComponent<NavMeshAgent>();
        anim = GetComponentInChildren<Animator>();
        //player = GameObject.Find("Player_Character").GetComponent<Transform>();
        player = GameObject.FindWithTag("Player").transform;
    }

    protected virtual void Start()
    {
        foreach (var p in patrolPoints)
        {
            p.parent = null;
        }
    }

    protected virtual void Update()
    {
        
    }

    public bool PlayerInAggressionRange()
    {
        return Vector3.Distance(player.position, transform.position) < aggressionRange;
    }


    public void ActivateManualMovement(bool manualMovement)
    {
        this.manualMovement = manualMovement;
    }

    public bool ManualMovementActive()
    {
        return manualMovement;
    }

    public void ActivateManualRotation(bool manualRotation)
    {
        this.manualRotation = manualRotation;
    }

    public bool ManualRotationActive()
    {
        return manualRotation;
    }

    public Vector3 GetPatrolDestination()
    {
        Vector3 destination = patrolPoints[currentPatrolIndex].transform.position;

        currentPatrolIndex++;

        if (currentPatrolIndex >= patrolPoints.Length)
            currentPatrolIndex = 0;

        return destination;
    }

    protected virtual void OnDrawGizmos()
    {
        Gizmos.DrawWireSphere(transform.position, aggressionRange);

    }

    public Quaternion FaceTarget(Vector3 target)
    {
        // 객체가 target을 바라보도록 하는 회전값
        Quaternion targetRotation = Quaternion.LookRotation(target - transform.position);

        // 현재 회전 각도 가져오기
        Vector3 currentEulerAngels = transform.rotation.eulerAngles;

        // y축 회전 보간
        float yRotation = Mathf.LerpAngle(currentEulerAngels.y, targetRotation.eulerAngles.y, trunSpeed * Time.deltaTime);

        return Quaternion.Euler(currentEulerAngels.x, yRotation, currentEulerAngels.z);
    }

    public void AnimationTrigger()
    {
        enemyStateMachine.currentState.AnimationTrigger();
    }
}

 

 

Enemy_Melee 스크립트 :

 

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

[System.Serializable]
public struct AttackData
{
    public string attackName;
    public float attackRange;
    public float moveSpeed;
    public float attackIndex;
    [Range(1, 2)]
    public float animationSpeed;
}

public enum EnemyType
{
    Normal,
    Patrol
}


public class Enemy_Melee : Enemy
{
    [Header("Attack data")]
    public AttackData attackData;

    public EnemyType enemyType;
    
    public IdleState_Melee idleState { get; private set; }
    public MoveState_Melee moveState { get; private set; }
    public RecoveryState_Melee recoveryState { get; private set; }

    public ChaseState_Melee chaseState { get; private set; }

    public AttackState_Melee attackState { get; private set; }



    protected override void Awake()
    {
        base.Awake();

        idleState = new IdleState_Melee(this, enemyStateMachine, "Idle");
        moveState = new MoveState_Melee(this, enemyStateMachine, "Move");
        recoveryState = new RecoveryState_Melee(this, enemyStateMachine, "Recovery");
        chaseState = new ChaseState_Melee(this, enemyStateMachine, "Chase");
        attackState = new AttackState_Melee(this, enemyStateMachine, "Attack");
    }

    protected override void Start()
    {
        base.Start();

        enemyStateMachine.Initialize(idleState);
    }

    protected override void Update()
    {
        base.Update();

        enemyStateMachine.currentState.Update();
    }



    protected override void OnDrawGizmos()
    {
        base.OnDrawGizmos();

        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, attackData.attackRange);
    }


    public bool PlayerInAttackRange()
    {
        return Vector3.Distance(player.position, transform.position) < attackData.attackRange;
    }
}

 

 

EnemyState 스크립트 : 

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

public class EnemyState
{
    protected Enemy enemyBase;
    protected EnemyStateMachine enemyStateMachine;

    protected string animBoolName;
    protected float stateTimer;

    protected bool triggerCalled;

    public EnemyState(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName)
    {
        this.enemyBase = enemyBase;
        this.enemyStateMachine = enemyStateMachine;
        this.animBoolName = animBoolName;
    }
        
    public virtual void Enter()
    {
        enemyBase.anim.SetBool(animBoolName, true);

        triggerCalled = false;
    }

    public virtual void Update()
    {
        stateTimer -= Time.deltaTime;
    }

    public virtual void Exit()
    {
        enemyBase.anim.SetBool(animBoolName, false);
    }

    public bool AnimationTrigger()
    {
        return triggerCalled = true;
    }
}

 

 

EnemyStateMachine 스크립트 :

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

public class EnemyStateMachine
{
    public EnemyState currentState { get; private set; }

    // 첫번째 상태를 초기화함
    public void Initialize(EnemyState startState)
    {
        currentState = startState;
        currentState.Enter();
    }

    // 이전상태를 종료하고 바로 다른 상태로 전환
    public void ChangeState(EnemyState newState)
    {
        Debug.Log($"Changing state from {currentState} to {newState}");
        currentState.Exit(); // 이전 상태를 종료하고
        currentState = newState; // 새로운 상태를 가져와서
        currentState.Enter(); // 새로운 상태로 들어감
    }
}

 

 

EnemyAnimationEvents 스크립트 : 애니메이션 클립에 이벤트 추가 용도.

ex. 애니메이션 동작이 끝나는 부분에 AnimationTrigger() 이벤트를 호출해서 다음 동작을 하게 만듦.

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

public class EnemyAnimationEvents : MonoBehaviour
{
    Enemy enemy;

    private void Awake()
    {
        enemy = GetComponentInParent<Enemy>();
    }

    public void AnimationTrigger()
    {
        enemy.AnimationTrigger();
    }

    public void StartManualMovement()
    {
        enemy.ActivateManualMovement(true);
    }
    public void StopManualMovement()
    {
        enemy.ActivateManualMovement(false);
    }

    public void StartManualRotation()
    {
        enemy.ActivateManualRotation(true);
    }
    public void StopManualRotation()
    {
        enemy.ActivateManualRotation(false);
    }
}

 

 

IdleState_Melee 스크립트 :

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

public class IdleState_Melee : EnemyState
{
    private Enemy_Melee enemy;

    public IdleState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
    {
        // Enemy_Melee에 있는 다른 상태로 접근 및 전환하기 위해 선언.
        // ex) enemyStateMachine.ChangeState(enemy.moveState);
        enemy = enemyBase as Enemy_Melee; // Enemy_Melee 타입으로 형변환
    }

    public override void Enter()
    {
        base.Enter();

        stateTimer = enemyBase.idleTime;
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();

        if (enemy.PlayerInAggressionRange())
        {
            enemyStateMachine.ChangeState(enemy.recoveryState);
            return;
        }

        if (stateTimer < 0 && enemy.enemyType == EnemyType.Patrol)
        {
            enemyStateMachine.ChangeState(enemy.moveState);
        }
    }

}

 

 

MoveState_Melee 스크립트 :

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.FullSerializer;
using UnityEngine;

public class MoveState_Melee : EnemyState
{
    Enemy_Melee enemy;
    Vector3 destination;

    public MoveState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
    {
        enemy = enemyBase as Enemy_Melee;
    }

    public override void Enter()
    {
        base.Enter();

        enemy.agent.speed = enemy.moveSpeed;
        
        destination = enemy.GetPatrolDestination();
        enemy.agent.SetDestination(destination);
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();


        if(enemy.PlayerInAggressionRange())
        {
            enemyStateMachine.ChangeState(enemy.recoveryState);
            return;
        }

        if(enemy.agent.remainingDistance <= enemy.agent.stoppingDistance )
        {
            enemyStateMachine.ChangeState(enemy.idleState);
        }

    }
}

 

 

 

RecoveryState_Melee 스크립트 :

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

public class RecoveryState_Melee : EnemyState
{
    Enemy_Melee enemy;

    public RecoveryState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
    {
        enemy = enemyBase as Enemy_Melee;
    }

    public override void Enter()
    {
        base.Enter();

        enemy.agent.isStopped = true;
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();

        enemy.transform.rotation = enemy.FaceTarget(enemy.player.position);

        if (triggerCalled)
        {
            if (enemy.PlayerInAttackRange())
            {
                enemyStateMachine.ChangeState(enemy.attackState);
            }
            else
            {
                enemyStateMachine.ChangeState(enemy.chaseState);
            }
        }
    }
}

 

 

AttackState_Melee 스크립트 : 

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

public class AttackState_Melee : EnemyState
{
    Enemy_Melee enemy;

    public Vector3 attackDirection;
    private float attackMoveSpeed;

    private const float MAX_ATTACK_DISTANCE = 50;

    public AttackState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
    {
        enemy = enemyBase as Enemy_Melee;
    }

    public override void Enter()
    {
        base.Enter();

        attackMoveSpeed = enemy.attackData.moveSpeed;
        enemy.anim.SetFloat("AttackAnimationSpeed", enemy.attackData.animationSpeed);
        enemy.anim.SetFloat("AttackIndex", enemy.attackData.attackIndex);

        enemy.agent.isStopped = true;
        enemy.agent.velocity = Vector3.zero; 

        attackDirection = enemy.transform.position + (enemy.transform.forward * MAX_ATTACK_DISTANCE);
    }

    public override void Exit()
    {
        base.Exit();

        if (enemy.PlayerInAttackRange())
        {
            enemy.anim.SetFloat("RecoveryIndex", 1);
        }
        else
        {
            enemy.anim.SetFloat("RecoveryIndex", 0);
        }

    }

    public override void Update()
    {
        base.Update();

        if (enemy.ManualRotationActive())
        {
            enemy.transform.rotation = enemy.FaceTarget(enemy.player.position);
        }

        if (enemy.ManualMovementActive())
        {
            enemy.transform.position = Vector3.MoveTowards(enemy.transform.position, attackDirection, attackMoveSpeed * Time.deltaTime);
        }

        if (triggerCalled)
        {
            if (enemy.PlayerInAttackRange())
            {
                enemyStateMachine.ChangeState(enemy.recoveryState);
            }
            else
            {
                enemyStateMachine.ChangeState(enemy.chaseState);
            }
        }
    }
}