카테고리 없음

디자인 패턴 - 커맨드 패턴

informsec 2026. 9. 15. 13:16
커맨드 패턴을 그림으로 간단하게 표현을 해보았다.  (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();
        }
    }
}

 

커맨드 패턴GIF.mp4
1.46MB