커맨드 패턴을 그림으로 간단하게 표현을 해보았다. (Made by CHATGPT)



이 중에서 3번과 5번을 실습에 적용해보도록 하겠다.
보스 AI 패턴 만들기
목표는
- 위치로 이동 - 3초 대기 - 플레이어 조준 사격 - 총알 난사
단계 별로 적용을 시켜보겠다.
1단계: ICommand (명령서의 기본 뼈대 만들기)
// 커맨드 패턴의 핵심: 모든 행동(명령서)이 반드시 지켜야 하는 3가지 규칙을 정의합니다.
public interface ICommand
{
// 1. 명령이 처음 시작될 때 딱 한 번 호출됩니다. (초기화 역할)
void Enter();
// 2. 명령이 실행되는 동안 매 프레임(Update)마다 호출됩니다. (실제 행동 역할)
void Execute();
// 3. 이 명령이 다 끝났는지 보스(실행자)에게 알려주는 신호등 역할입니다.
bool IsFinished { get; }
}
2단계: Concrete Command (구체적인 행동이 적힌 명령서 만들기) - BossCommands.cs
- 지정된 위치로 이동 [명령서]
// [패턴 1] 지정된 위치로 부드럽게 이동하는 명령서
public class MoveCommand : ICommand
{
private Transform _transform;
private Vector2 _targetPos;
private float _speed;
public bool IsFinished { get; private set; }
// 생성자: '누가', '어디로', '얼마나 빠르게' 갈지 명령서를 쓸 때 미리 적어둡니다(상태 저장).
public MoveCommand(Transform transform, Vector2 targetPos, float speed)
{
_transform = transform;
_targetPos = targetPos;
_speed = speed;
}
public void Enter() { IsFinished = false; }
public void Execute()
{
// Vector2.MoveTowards를 사용해 목표 위치를 향해 이동합니다.
_transform.position = Vector2.MoveTowards(_transform.position, _targetPos, _speed * Time.deltaTime);
// 내 현재 위치와 목표 위치의 거리가 0.1 이하로 가까워지면 "도착(완료)!" 신호를 보냅니다.
if (Vector2.Distance(_transform.position, _targetPos) < 0.1f) IsFinished = true;
}
}
- 3초 대기 [명령서]
// [패턴 2] 정해진 시간 동안 가만히 대기하는 명령서
public class WaitCommand : ICommand
{
private float _duration; // 대기할 목표 시간
private float _timer; // 현재 흘러간 시간
// 타이머가 목표 시간에 도달하면 자동으로 IsFinished가 true가 됩니다.
public bool IsFinished => _timer >= _duration;
public WaitCommand(float duration) { _duration = duration; }
public void Enter() { _timer = 0f; } // 시작할 때 타이머를 0으로 초기화
public void Execute() { _timer += Time.deltaTime; } // 매 프레임마다 시간 증가
}
- 플레이어 조준 사격 [명령서]
// [패턴 3] 플레이어를 향해 한 발을 조준 사격하는 명령서
public class AimedFireCommand : ICommand
{
private GameObject _bulletPrefab;
private Transform _firePoint;
private GameObject _player;
public bool IsFinished { get; private set; }
public AimedFireCommand(GameObject bulletPrefab, Transform firePoint, GameObject player)
{
_bulletPrefab = bulletPrefab;
_firePoint = firePoint;
_player = player;
}
public void Enter()
{
if (_player != null && _bulletPrefab != null)
{
// 1. 총알 생성
GameObject bullet = Object.Instantiate(_bulletPrefab, _firePoint.position, Quaternion.identity);
// 2. 플레이어를 향하는 방향 벡터와 각도를 계산하여 총알의 회전값(방향)을 비틀어줍니다.
Vector2 dir = _player.transform.position - _firePoint.position;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f;
bullet.transform.rotation = Quaternion.Euler(0, 0, angle);
}
// 발사는 1프레임 만에 즉시 끝나므로 바로 완료 신호를 보냅니다.
IsFinished = true;
}
public void Execute() { /* 대기열로 넘어가므로 비워둠 */ }
}
- 총알 난사 [명령서]
// [패턴 4] 3방향으로 탄막을 난사하는 명령서
public class SpreadFireCommand : ICommand
{
private GameObject _bulletPrefab;
private Transform _firePoint;
public bool IsFinished { get; private set; }
public SpreadFireCommand(GameObject bulletPrefab, Transform firePoint)
{
_bulletPrefab = bulletPrefab;
_firePoint = firePoint;
}
public void Enter()
{
if (_bulletPrefab != null)
{
// -30도, 0도, 30도 각도로 총 3발의 총알을 동시에 생성합니다.
float[] angles = { -30f, 0f, 30f };
foreach (float angle in angles)
{
Object.Instantiate(_bulletPrefab, _firePoint.position, Quaternion.Euler(0, 0, angle));
}
}
IsFinished = true;
}
public void Execute() { }
}
3단계: 보스 클래스 (명령을 조립하고 실행하는 주체) - BossEnemy.cs
using System.Collections.Generic;
using UnityEngine;
public class BossEnemy : Enemy
{
[Header("보스 전용 설정")]
public GameObject BulletPrefab; // 쏠 총알
public Transform FirePoint; // 총구 위치
private GameObject _player;
// ★ 커맨드 패턴 핵심 변수: 명령서들을 차곡차곡 쌓아둘 콤보 리스트
private List<ICommand> _patternList = new List<ICommand>();
private int _currentIndex = 0; // 현재 몇 번째 명령을 실행 중인지 추적
private ICommand _currentCommand; // 지금 손에 쥐고 있는 명령서
private void Start()
{
// 1. 플레이어 찾기 (현재 리팩터링 이전 구조에 맞춤)
_player = GameObject.FindWithTag("Player");
// ========================================================
// 2. [행동 계획 조립] 기획자가 원하는 순서대로 큐에 명령을 밀어 넣습니다.
// ========================================================
// 인덱스 0: 화면 상단 중앙(0, 3.5f)으로 속도 2로 등장!
_patternList.Add(new MoveCommand(transform, new Vector2(0, 3.5f), 2f));
// 인덱스 1: 3초 동안 가만히 위압감 주기 (반복 루프 시작점)
_patternList.Add(new WaitCommand(3f));
// 인덱스 2: 플레이어 얄밉게 저격하기
_patternList.Add(new AimedFireCommand(BulletPrefab, FirePoint, _player));
// 인덱스 3: 사격 후 0.5초 짧은 대기 (총알 겹침 방지)
_patternList.Add(new WaitCommand(0.5f));
// 인덱스 4: 양옆으로 거대한 탄막 난사하기
_patternList.Add(new SpreadFireCommand(BulletPrefab, FirePoint));
// 3. 첫 번째 명령서(등장 이동)를 꺼내서 실행을 시작합니다.
if (_patternList.Count > 0)
{
_currentCommand = _patternList[0];
_currentCommand.Enter();
}
}
// 부모(Enemy.cs)가 하던 멍청한 단순 직진 이동은 무시합니다. (커맨드가 통제해야 하므로)
protected override void Move() { }
protected override void Update()
{
// 명령서가 없으면 아무것도 안 함
if (_currentCommand == null) return;
// 현재 손에 쥔 명령서의 로직을 매 프레임 실행시킴 (이동 혹은 타이머 증가)
_currentCommand.Execute();
// 손에 쥔 명령서가 "나 끝났어!"라고 신호를 보내면?
if (_currentCommand.IsFinished)
{
_currentIndex++; // 다음 명령서로 인덱스 이동
// 만약 패턴 끝까지 다 돌았다면? -> 인덱스 1번(Wait)으로 돌아가서 무한 콤보 반복!
if (_currentIndex >= _patternList.Count)
{
_currentIndex = 1;
}
// 새로운 명령서를 꺼내서 초기화(Enter) 실행
_currentCommand = _patternList[_currentIndex];
_currentCommand.Enter();
}
}
}