이제 적에 대한 코드를 구현해보자. 우선 적이 어떤 상태를 가지게 될지 생각해보자.

간단한 쯔구르게임이기 때문에 가장 기본인 순찰, 추적, 공격상태가 있을 것이다. 우선은 순찰 및 추적까지 구현해보자.

이를 위해 상태 인터페이스를 구현해주자. 2개의 PatrolPoint에서 하나씩 목적지로 선정하여 가까워지면 다른 포인트로 이동하게 구현하였다.

IMobState.cs

using UnityEngine;

public interface IMobState
{
    void Enter(BaseMobController mob);      // 상태 진입
    void Execute();                 // 상태 실행
    void Exit();                    // 상태 종료
}

public class PatrolState : IMobState
{
    private BaseMobController _mob;
    private Vector2 currentTargetPoint;
    private Vector2 patrolPointA;
    private Vector2 patrolPointB;

    public PatrolState(Vector2 pointA, Vector2 pointB)
    {
        patrolPointA = pointA;
        patrolPointB = pointB;
        currentTargetPoint = patrolPointA;
    }

    public void Enter(BaseMobController mob)
    {
        this._mob = mob;
        _mob.SetDestination(currentTargetPoint);
    }

    public void Execute()
    {
        if (_mob.IsPlayerDetected())
        {
            _mob.ChangeState(new ChaseState());
            return;
        }

        if (Vector2.Distance(_mob.transform.position, currentTargetPoint) < 1f)
        {
            currentTargetPoint = (currentTargetPoint == patrolPointA) ? patrolPointB : patrolPointA;
            _mob.SetDestination(currentTargetPoint);
        }

        _mob.Move(currentTargetPoint);
    }

    public void Exit()
    {

    }
}

public class ChaseState : IMobState
{
    private BaseMobController _mob;
    private Transform playerTransform;

    public void Enter(BaseMobController mob)
    {
        this._mob = mob;
        playerTransform = Managers.Game.GetPlayer().transform;
    }

    public void Execute()
    {
        float distanceToPlayer = Vector2.Distance(_mob.transform.position,playerTransform.position);
        if(distanceToPlayer <= _mob.GetAttackRange())
        {
            _mob.ChangeState(new AttackState());
        }else if(distanceToPlayer > _mob.GetAttackRange())
        {
            Vector2 pointA = _mob.GetPatrolPointA();
            Vector2 pointB = _mob.GetPatrolPointB();
            _mob.ChangeState(new PatrolState(pointA, pointB));
        }else
        {
            _mob.Move(playerTransform.position);
        }
    }

    public void Exit()
    {
        
    }
}

 

그리고 다양한 Mob이 있을 수 있기때문에 Mob이 가지고 있어야  할 함수와 변수를 남은 abstract 객체를 선언해주자. 그리고 기본적인 정보는 FlyWeight 패턴을 사용하여 Scriptable Object를 참조하도록  했다.

BaseMobController.cs

using System.Collections.Generic;
using UnityEngine;

public abstract class BaseMobController : MonoBehaviour
{
    protected IMobState currentState;
    public MobData mobData;

    public IMobState GetCurrentState()
    {
        return currentState;
    }

    public abstract void ChangeState(IMobState state);
    public abstract float GetDetectionRange();
    public abstract float GetAttackRange();
    public abstract float GetChasableRange();
    public abstract Vector2 GetPatrolPointA();
    public abstract Vector2 GetPatrolPointB();
    public abstract void SetDestination(Vector2 destination);
    public abstract void SetPatrolPoints(Vector2 pointA, Vector2 pointB);
    public abstract bool IsPlayerDetected();
    public abstract void Move(Vector2 target);
}

 

MobData.cs

using UnityEngine;

[CreateAssetMenu(fileName = "MobBaseStat", menuName = "Game Data/Stats")]
public class MobData : ScriptableObject
{
    public float currentHealth;
    public float deffensePower;
}

 

그리고 일단 하나의 Mob을 움직여보기 위해 위의 abstract 클래스를 상속받는 MobController를 만들어주자.

using UnityEditor;
using UnityEngine;

public class MobController : BaseMobController
{
    [SerializeField] private float speed = 10f;
    [SerializeField] private float detectionRange = 5f;
    [SerializeField] private float chasableRange = 5f;
    [SerializeField] private float attackRange = 5f;

    [SerializeField] private Vector2 patrolPointA;
    [SerializeField] private Vector2 patrolPointB;

    private Rigidbody2D Rigidbody2D;
    private Animator animator;

    public override float GetDetectionRange() => detectionRange;
    public override float GetAttackRange() => attackRange;
    public override float GetChasableRange() => chasableRange;

    public override Vector2 GetPatrolPointA() => patrolPointA;
    public override Vector2 GetPatrolPointB() => patrolPointB;

    private void Awake()
    {
        Rigidbody2D = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();

        // 초기 상태를 순찰 상태로 설정
        ChangeState(new PatrolState(patrolPointA, patrolPointB));
    }

    private void FixedUpdate()
    {
        currentState?.Execute();
    }

    public override void ChangeState(IMobState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter(this);
    }


    public override void SetDestination(Vector2 destination)
    {
        Move(destination);
    }

    public override void SetPatrolPoints(Vector2 pointA, Vector2 pointB)
    {
        patrolPointA = pointA;
        patrolPointB = pointB;
#if UNITY_EDITOR
        EditorUtility.SetDirty(this); // 에디터에서 변경사항을 감지하도록 설정
#endif
    }

    public override bool IsPlayerDetected()
    {
        return Vector2.Distance(transform.position, Managers.Game.GetPlayer().transform.position) <= detectionRange;
    }

    public override void Move(Vector2 target)
    {
        //이동
        Vector2 currentPosition = transform.position;
        //방향벡터
        Vector2 direction = (target - currentPosition).normalized;
        //방향*속도 => 해당방향으로의 속도
        Rigidbody2D.velocity = direction * speed;

        animator.SetFloat("MoveX", direction.x);
        animator.SetFloat("MoveY", direction.y);
        animator.SetFloat("Speed",Rigidbody2D.velocity.magnitude);
    }
}

 

이때 움직이는 방향에 따라 다른 애니메이션이 재생되도록 몹의 애니메이션에 Blend Tree를 적용시켜 주었다.

그리고 이 PatrolPoint를 에디터에서 보고 직접 조정할 수 있으며 초기화할 수도 있게하기위해 Editor 수정 코드를 추가해주었다.

MobController.cs

using UnityEditor;
using UnityEngine;

public class MobController : BaseMobController
{
    [SerializeField] private float speed = 10f;
    [SerializeField] private float detectionRange = 5f;
    [SerializeField] private float chasableRange = 5f;
    [SerializeField] private float attackRange = 5f;

    [SerializeField] private Vector2 patrolPointA;
    [SerializeField] private Vector2 patrolPointB;

    private Rigidbody2D Rigidbody2D;
    private Animator animator;

    public override float GetDetectionRange() => detectionRange;
    public override float GetAttackRange() => attackRange;
    public override float GetChasableRange() => chasableRange;

    public override Vector2 GetPatrolPointA() => patrolPointA;
    public override Vector2 GetPatrolPointB() => patrolPointB;

    private void Awake()
    {
        Rigidbody2D = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();

        // 초기 상태를 순찰 상태로 설정
        ChangeState(new PatrolState(patrolPointA, patrolPointB));
    }

    private void FixedUpdate()
    {
        currentState?.Execute();
    }

    public override void ChangeState(IMobState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter(this);
    }


    public override void SetDestination(Vector2 destination)
    {
        Move(destination);
    }

    public override void SetPatrolPoints(Vector2 pointA, Vector2 pointB)
    {
        patrolPointA = pointA;
        patrolPointB = pointB;
#if UNITY_EDITOR
        EditorUtility.SetDirty(this); // 에디터에서 변경사항을 감지하도록 설정
#endif
    }

    public override bool IsPlayerDetected()
    {
        return Vector2.Distance(transform.position, Managers.Game.GetPlayer().transform.position) <= detectionRange;
    }

    public override void Move(Vector2 target)
    {
        //이동
        Vector2 currentPosition = transform.position;
        //방향벡터
        Vector2 direction = (target - currentPosition).normalized;
        //방향*속도 => 해당방향으로의 속도
        Rigidbody2D.velocity = direction * speed;

        animator.SetFloat("MoveX", direction.x);
        animator.SetFloat("MoveY", direction.y);
        animator.SetFloat("Speed",Rigidbody2D.velocity.magnitude);
    }
}

 

 

에디터를 수정해주려면 [CustomEditor(typeof(BaseMobController), true)] 이러한 코드를 클래스전에 선언해주어야한다. 또한 클래스에서 Editor 클래스를  상속받아 코드를 구현해야한다. setDirty를 통해 진행상황이 바로 저장되도록 구현하였다.

MobControllerEditor.cs

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(BaseMobController), true)] // 이제 모든 MobController 파생 클래스에 대해 이 에디터를 사용할 수 있습니다.
public class MobControllerEditor : Editor
{
    private float _handleSize = 5f; // Scene view에서의 핸들 크기를 더 작게 조정

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI(); // 기존 인스펙터 GUI 요소를 그린다.

        BaseMobController mob = (BaseMobController)target;
        if (GUILayout.Button("Initialize Patrol Points"))
        {
            Vector2 center = mob.transform.position;
            Vector2 pointA = center + new Vector2(2f, 0f);
            Vector2 pointB = center + new Vector2(-2f, 0f);

            mob.SetPatrolPoints(pointA, pointB);

            //변경사항 적용
            EditorUtility.SetDirty(mob);
        }
    }

    protected void OnSceneGUI()
    {
        BaseMobController mob = (BaseMobController)target;
        EditorGUI.BeginChangeCheck();

        Vector3 pointAWorld = mob.GetPatrolPointA();
        Vector3 pointBWorld = mob.GetPatrolPointB();

        // 🔹 Scene 뷰 줌 레벨에 따라 핸들 크기 자동 조정
        float handleSize = HandleUtility.GetHandleSize(mob.transform.position) * 0.2f;

        // 현재 상태에 따라 범위를 다르게 그리기
        IMobState currentState = mob.GetCurrentState();
        if (currentState is PatrolState)
        {
            DrawDetectionRange(mob);
            DrawAttackRange(mob);
        }
        else if (currentState is ChaseState)
        {
            DrawChasableRange(mob);
            DrawAttackRange(mob);
        }
        else if (currentState is AttackState)
        {
            DrawAttackRange(mob);
        }

        Handles.color = Color.red;
        pointAWorld = Handles.FreeMoveHandle(pointAWorld, handleSize, Vector3.zero, Handles.SphereHandleCap);
        Handles.Label(pointAWorld, "Patrol Point A");

        Handles.color = Color.blue;
        pointBWorld = Handles.FreeMoveHandle(pointBWorld, handleSize, Vector3.zero, Handles.SphereHandleCap);
        Handles.Label(pointBWorld, "Patrol Point B");

        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(mob, "Change Patrol Points");
            mob.SetPatrolPoints(pointAWorld, pointBWorld);
            EditorUtility.SetDirty(mob);
        }

        Handles.DrawLine(pointAWorld, pointBWorld);
    }


    private void DrawDetectionRange(BaseMobController mob)
    {
        Handles.color = Color.yellow;
        Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetDetectionRange());
    }

    private void DrawChasableRange(BaseMobController mob)
    {
        Handles.color = Color.yellow;
        Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetChasableRange());
    }

    private void DrawAttackRange(BaseMobController mob)
    {
        Handles.color = Color.red;
        Handles.DrawWireArc(mob.transform.position, Vector3.forward, Vector3.up, 360, mob.GetAttackRange());
    }
}

 

 

이렇게 해주면 Scene창에서 캐릭터의 Patorl Point를 눈으로 보고 직접 수정해줄 수 있으며 초기화해줄 수 있다.

 

이렇게 해주고 실행해주면 Patrol이 잘 실행되는것을 볼 수 있다.

+ Recent posts