
이제 적에 대한 코드를 구현해보자. 우선 적이 어떤 상태를 가지게 될지 생각해보자.
간단한 쯔구르게임이기 때문에 가장 기본인 순찰, 추적, 공격상태가 있을 것이다. 우선은 순찰 및 추적까지 구현해보자.
이를 위해 상태 인터페이스를 구현해주자. 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이 잘 실행되는것을 볼 수 있다.

'게임공부 > Unity' 카테고리의 다른 글
[Unity][C#][비행기 슈팅 게임]1. 움직이기(new Input system) (0) | 2025.03.13 |
---|---|
[C#][Unity][팬 게임]카드 짝 맞추기 게임 만들기1 (0) | 2025.02.03 |
[C#][Unity][나만의 탑뷰 게임 만들기]8. 인벤토리 시스템 수정 (1) | 2025.01.13 |
[C#][디자인 패턴]State 패턴 (0) | 2025.01.09 |
[Unity]최적화 관련 팁 (0) | 2025.01.08 |