- 주제
>> 상태 인터페이스 (IState)
>> 상태머신 추상 클래스 (StateMachine)
>> 플레이어 상태머신 클래스 (PlayerStateMachine)
>> 이동 상태 클래스 (PlayerMovingState)
>> 공격 상태 클래스 (PlayerAttackingState)
>> 추적 상태 클래스 (PlayerChasingState)
- 공부내용
1. 상태 인터페이스(IState)
상태 인터페이스는 상태들이 구현해야 하는 메서드들을 간략하게 정의한다.(공통 메서드 정의)
Enter는 상태에 진입할 때, Exit는 상태에서 벗어날 때 호출된다.
// 상태 인터페이스
public interface IState
{
void Enter(); // 상태 진입
void Exit(); // 상태 종료
}
2. 상태머신 추상 클래스 (StateMachine)
상태머신 추상 클래스는 현재 상태를 추적하고 상태를 변경하는 기능을 제공한다.
ChangeState 메서드는 현재 상태를 종료하고(Exit), 새로운 상태를 시작한다.(Enter).
특정 조건이 만족되면, ChangeState 메서드를 호출하여 상태를 전환할 수 있다.
예를 들어, PlayerMovingState에서 몬스터를 감지하면, PlayerChasingState로 전환된다.
// 상태머신 추상클래스
public abstract class StateMachine
{
protected IState currentState; // 현재 상태
public void ChangeState(IState state)
{
currentState?.Exit(); // 현재 상태 종료
currentState = state; // 새로운 상태 설정
currentState?.Enter(); // 새로운 상태 진입
}
}
3. 플레이어 상태머신 클래스 (PlayerStateMachine)
플레이어 상태머신 클래스는 플레이어의 상태와 애니메이터, 이동 속도, 공격 간격 등을 관리한다.
각 상태 객체 (MovingState, AttackingState, ChasingState)를 초기화한다.
// 플레이어 상태머신 클래스
public class PlayerStateMachine : StateMachine
{
public Player Player { get; } // 플레이어
public Animator Animator { get; } // 애니메이터
// 상태들
public PlayerMovingState MovingState { get; } // 이동 상태
public PlayerAttackingState AttackingState { get; } // 공격 상태
public PlayerChasingState ChasingState { get; } // 추적 상태
public float MoveSpeed { get; } // 이동 속도
public float AttackInterval { get; } // 공격 간격
// 생성자
public PlayerStateMachine(Player player, Animator animator, float moveSpeed, float attackInterval)
{
this.Player = player; // 플레이어 설정
this.Animator = animator; // 애니메이터 설정
MoveSpeed = moveSpeed; // 이동 속도 설정
AttackInterval = attackInterval; // 공격 간격 설정
MovingState = new PlayerMovingState(this); // 이동 상태 초기화
AttackingState = new PlayerAttackingState(this, animator); // 공격 상태 초기화
ChasingState = new PlayerChasingState(this); // 추적 상태 초기화
}
}
4. 이동 상태 클래스 (PlayerMovingState)
- Enter: 이동 코루틴을 시작(개별 메서드 작성)
- Exit: 이동 코루틴을 종료(개별 메서드 작성)
StopCoroutine 메서드 사용법
1. StopCoroutine(string methodName) – 메서드 이름을 문자열로 제공
2. StopCoroutine(IEnumerator routine) – 코루틴의 인스턴스를 제공
기존의 방식: Exit 메서드에서 StopCoroutine(Move())를 호출
-> 버그 발생(이동이 멈추지 않음)
-> 원인: StopCoroutine(Move())를 호출할 때, 새로운 IEnumerator 인스턴스가 생성됨. 이 인스턴스는 StartCoroutine으로 시작된 실제 코루틴 인스턴스와 다르기 때문에 StopCoroutine이 해당 코루틴을 멈출 수 없음.
현재 방식: 특정 코루틴의 실행을 추적해서, 필요 시 정확히 해당 코루틴을 중지(코루틴 인스턴스 추적)
1. 코루틴 인스턴스 저장
StartCoroutine을 호출할 때 반환되는 Coroutine 객체를 변수에 저장(현재 실행 중인 코루틴 추적)
ex) moveCoroutine = stateMachine.Player.StartCoroutine(Move());
2. 코루틴 종료
StopCoroutine을 호출할 때, 저장된 Coroutine 객체를 사용하여 정확히 해당 코루틴을 중지
ex) stateMachine.Player.StopCoroutine(moveCoroutine);
3. 코루틴 변수 초기화
코루틴이 중지된 후, 해당 변수 (moveCoroutine)를 null로 설정하여 더 이상 코루틴이 실행 중이지 않음을 나타냄
- Move: 매 프레임마다 플레이어를 앞으로 이동시키고, 몬스터를 감지하면 추적 상태로 전환
- DetectMonster: 몬스터를 감지하여 추적 상태로 전환할지 여부를 결정
// 이동 상태 클래스
public class PlayerMovingState : IState
{
private PlayerStateMachine stateMachine; // 상태머신
private Coroutine moveCoroutine; // 이동 코루틴
// 생성자
public PlayerMovingState(PlayerStateMachine stateMachine)
{
this.stateMachine = stateMachine; // 상태머신 설정
}
public void Enter()
{
StartMoveCoroutine(); // 이동 코루틴 시작
}
public void Exit()
{
StopMoveCoroutine(); // 이동 코루틴 종료
}
// 이동 코루틴
private IEnumerator Move()
{
while (true)
{
stateMachine.Player.transform.Translate(Vector3.forward * stateMachine.MoveSpeed * Time.deltaTime); // 플레이어 이동
if (DetectMonster()) // 몬스터 감지
{
stateMachine.ChangeState(stateMachine.ChasingState); // 추적 상태로 전환
yield break;
}
yield return null;
}
}
// 몬스터 감지
private bool DetectMonster()
{
Collider[] hitColliders = Physics.OverlapSphere(stateMachine.Player.transform.position, stateMachine.Player.detectionRadius, LayerMask.GetMask("Monster"));
foreach (var hitCollider in hitColliders)
{
Vector3 directionToMonster = hitCollider.transform.position - stateMachine.Player.transform.position;
float angle = Vector3.Angle(stateMachine.Player.transform.forward, directionToMonster);
if (angle < stateMachine.Player.detectionAngle / 2)
{
return true; // 몬스터 발견
}
}
return false; // 몬스터 미발견
}
// 이동 코루틴 시작
public void StartMoveCoroutine()
{
if (moveCoroutine == null)
{
moveCoroutine = stateMachine.Player.StartCoroutine(Move());
}
}
// 이동 코루틴 종료
public void StopMoveCoroutine()
{
if (moveCoroutine != null)
{
stateMachine.Player.StopCoroutine(moveCoroutine);
moveCoroutine = null;
}
}
}
5. 공격 상태 클래스 (PlayerAttackingState)
- Enter: 공격 코루틴을 시작
- Exit: 공격 코루틴을 종료
- Attack: 공격 애니메이션을 트리거하고 일정 시간 동안 대기
// 공격 상태 클래스
public class PlayerAttackingState : IState
{
private PlayerStateMachine stateMachine; // 상태머신
private Animator animator; // 애니메이터
// 생성자
public PlayerAttackingState(PlayerStateMachine stateMachine, Animator animator)
{
this.stateMachine = stateMachine; // 상태머신 설정
this.animator = animator; // 애니메이터 설정
}
public void Enter()
{
stateMachine.Player.StartCoroutine(Attack()); // 공격 코루틴 시작
}
public void Exit()
{
stateMachine.Player.StopCoroutine(Attack()); // 공격 코루틴 종료
}
// 공격 코루틴
private IEnumerator Attack()
{
animator.SetTrigger("Slash"); // Slash 애니메이션 트리거 설정
yield return new WaitForSeconds(stateMachine.AttackInterval); // 공격 간격 대기
}
}
6. 추적 상태 클래스 (PlayerChasingState)
- Enter: 추적 코루틴을 시작
- Exit: 추적 코루틴을 종료
- Chase: 매 프레임마다 플레이어를 몬스터 방향으로 이동시키고, 가장 가까운 몬스터 수색
- FindTargetMonster: 탐지 반경 내에서 가장 가까운 몬스터를 수색
// 추적 상태 클래스
public class PlayerChasingState : IState
{
private PlayerStateMachine stateMachine; // 상태머신
private Coroutine chaseCoroutine; // 추적 코루틴
private Transform targetMonster; // 목표 몬스터
// 생성자
public PlayerChasingState(PlayerStateMachine stateMachine)
{
this.stateMachine = stateMachine; // 상태머신 설정
}
public void Enter()
{
StartChaseCoroutine(); // 추적 코루틴 시작
}
public void Exit()
{
StopChaseCoroutine(); // 추적 코루틴 종료
}
// 추적 코루틴
private IEnumerator Chase()
{
while (true)
{
FindTargetMonster(); // 목표 몬스터 찾기
if (targetMonster != null)
{
Vector3 direction = (targetMonster.position - stateMachine.Player.transform.position).normalized;
stateMachine.Player.transform.Translate(direction * stateMachine.MoveSpeed * Time.deltaTime); // 몬스터를 향해 이동
}
yield return null;
}
}
// 목표 몬스터 찾기
private void FindTargetMonster()
{
Collider[] hitColliders = Physics.OverlapSphere(stateMachine.Player.transform.position, stateMachine.Player.detectionRadius, LayerMask.GetMask("Monster"));
float closestDistance = float.MaxValue;
Transform closestMonster = null;
foreach (var hitCollider in hitColliders)
{
Vector3 directionToMonster = hitCollider.transform.position - stateMachine.Player.transform.position;
float angle = Vector3.Angle(stateMachine.Player.transform.forward, directionToMonster);
if (angle < stateMachine.Player.detectionAngle / 2)
{
float distance = directionToMonster.magnitude;
if (distance < closestDistance)
{
closestDistance = distance;
closestMonster = hitCollider.transform;
}
}
}
targetMonster = closestMonster; // 가장 가까운 몬스터 설정
}
// 추적 코루틴 시작
public void StartChaseCoroutine()
{
if (chaseCoroutine == null)
{
chaseCoroutine = stateMachine.Player.StartCoroutine(Chase());
}
}
// 추적 코루틴 종료
public void StopChaseCoroutine()
{
if (chaseCoroutine != null)
{
stateMachine.Player.StopCoroutine(chaseCoroutine);
chaseCoroutine = null;
}
}
}
'유니티' 카테고리의 다른 글
Day 42 - Object Pooling을 활용한 특정 위치 아이템 드랍 (2) | 2024.12.06 |
---|---|
Day 41 - WebGL로 웹 새로고침 시 게임 상태 저장하기 (0) | 2024.12.05 |
Day 39 - SoundManager(오브젝트 풀에서 사운드 소스를 가져와 재생) (0) | 2024.06.12 |
Day 38 - SaveManager.cs 생성(정보 저장) (2) | 2024.06.11 |
Day 37 - 3D 건축 시스템 (0) | 2024.06.04 |