티스토리 뷰

💻 Unity 디자인 패턴

Singleton

public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    public static GameManager Instance
    { 
        get 
        { 
            if (_instance == null)
            {
                _instance = FindFirstObjectByType<GameManager>();

                if (_instance == null)
                {
                    GameObject gameObject = new GameObject("GameManager");
                    _instance = gameObject.AddComponent<GameManager>();
                }
            }

            return _instance;
        } 
    }

    private int _score = 0;
    public int Score
    {
        get { return _score; }
    }

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }

        _instance = this;

        DontDestroyOnLoad(gameObject);
    }

    public void AddScore(int point)
    {
        _score += point;
        Debug.Log($"Current Score : {Score}");
    }
}

GameManager 오브젝트를 생성하지도 않아도 됨

Observer

public class EventManager : MonoBehaviour
{
    private static EventManager _instance;
    public static EventManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindFirstObjectByType<EventManager>();

                if (_instance == null)
                {
                    GameObject gameObject = new GameObject("EventManager");
                    _instance = gameObject.AddComponent<EventManager>();
                }
            }

            return _instance;
        }
    }

    private Dictionary<string, Action<object>> _eventDictionary = new Dictionary<string, Action<object>>();

    public void AddListsner(string eventName, Action<object> listener)
    {
        if (_eventDictionary.TryGetValue(eventName, out Action<object> thisEvent))
        {
            thisEvent += listener;
            _eventDictionary[eventName] = thisEvent;
        }
        else
        {
            _eventDictionary.Add(eventName, listener);
        }
    }

    public void RemoveListener(string eventName, Action<object> listener)
    {
        if (_eventDictionary.TryGetValue(eventName, out Action<object> thisEvent))
        {
            thisEvent -= listener;
            _eventDictionary[eventName] = thisEvent;
        }
    }

    public void TriggerEvent(string eventName, object data = null)
    {
        if (_eventDictionary.TryGetValue(eventName, out Action<object> thisEvent))
        {
            thisEvent?.Invoke(data);
        }
    }
}

public class UIHealthDisplay : MonoBehaviour
{
    void Start()
    {
        EventManager.Instance.AddListsner("PlayerHealthChanged", OnPlayHealthChanged);
        EventManager.Instance.AddListsner("PlayerDied", OnPlayerDied);
    }

    private void OnPlayHealthChanged(object data)
    {
        int health = (int)data;
        Debug.Log($"UI Update : Player hp is changed to {health}.");

        // UI 업데이트
    }

    private void OnPlayerDied(object data)
    {
        Debug.Log("UI Update : Player is died!");

        // 게임 오버 화면 표시
    }

    private void OnDestroy()
    {
        EventManager.Instance.RemoveListener("PlayerHealthChanged", OnPlayHealthChanged);
        EventManager.Instance.RemoveListener("PlayerDied", OnPlayerDied);
    }
}

public class Player : MonoBehaviour
{
    private int _health = 100;
    public int Health
    {
        get { return _health; }
        set
        {
            _health = value;
            EventManager.Instance.TriggerEvent("PlayerHealthChanged", _health);

            if (_health <= 0)
            {
                EventManager.Instance.TriggerEvent("PlayerDied");
            }
        }
    }

    private void TakeDamage(int damage)
    {
        Health -= damage;
    }

    void Update()
    {
        //테스트용: 스페이스 키를 누르면 데미지 받기
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (Health > 0)
            {
                TakeDamage(10);
            }
        }
    }
}

Factory

// EnemyFactory.cs

public class EnemyFactory : MonoBehaviour
{
    private static EnemyFactory _instance;
    public static EnemyFactory Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindFirstObjectByType<EnemyFactory>();

                if (_instance == null)
                {
                    GameObject gameObject = new GameObject("EnemyFactory");
                    _instance = gameObject.AddComponent<EnemyFactory>();
                }
            }

            return _instance;
        }
    }

    public GameObject GruntPrefab;
    public GameObject RunnerPrefab;
    public GameObject TankPrefab;

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public IEnemy CreateEnemy(EnemyType type, Vector3 position)
    {
        GameObject enemyObject = null;

        switch (type)
        {
            case EnemyType.Grunt:
                enemyObject = Instantiate(GruntPrefab);
                break;
            case EnemyType.Runner:
                enemyObject = Instantiate(RunnerPrefab);
                break;
            case EnemyType.Tank:
                enemyObject = Instantiate(TankPrefab);
                break;
            default:
                Debug.LogError($"Unknown enemy type : {type}");
                return null;
        }

        IEnemy enemy = enemyObject.GetComponent<IEnemy>();
        enemy.Initialize(position);
        return enemy;
    }
}

// EnemyBase.cs

public enum EnemyType
{
    Grunt,
    Runner,
    Tank,
    Boss
}

public interface IEnemy
{
    void Initialize(Vector3 position);
    void Attack();
    void TakeDamage(float damage);
}

public abstract class EnemyBase : MonoBehaviour, IEnemy
{
    public float Health;
    public float Speed;
    public float Damage;

    public virtual void Initialize(Vector3 position)
    {
        transform.position = position;
    }

    public abstract void Attack();

    public virtual void TakeDamage(float damage)
    {
        Health -= damage;
        if (Health <= 0)
        {
            Die();
        }
    }

    protected virtual void Die()
    {
        Destroy(gameObject);
    }
}

// EnemySpawner.cs

public class EnemySpawner : MonoBehaviour
{
    [SerializeField]
    private float spawnInterval = 5f;
    private float _timer;

    void Update()
    {
        _timer += Time.deltaTime;
        if (_timer >= spawnInterval)
        {
            SpawnRandomEnemy();
            _timer = 0;
        }
    }

    private void SpawnRandomEnemy()
    {
        Vector3 spawnPosition = new Vector3(Random.Range(-10f, 10f), 0, Random.Range(-10f, 10f));
        EnemyType randomgType = (EnemyType)Random.Range(0, 3);
        IEnemy enemy = EnemyFactory.Instance.CreateEnemy(randomgType, spawnPosition);
        Debug.Log($"{randomgType}{spawnPosition}에 생성되었습니다.");
    }
}


🕹️ 실습 (2D 횡스크롤)

슬로우모션

// EnemyMissile.cs

void Update()
{
    float timeScale = TimeController.Instance.GetTimeScale();
    transform.Translate(direction * speed * Time.deltaTime * timeScale);
}
// Player.cs

void Update()
{
    // 시간조절 입력 체크 (왼쪽 시프트키를 누르면 슬로우 모션 시작)
    if (Input.GetKeyDown(KeyCode.LeftShift))
    {
        // 포스트프로세싱 화면효과
        TimeController.Instance.SetSlowMotion(true);
    }

}

적 사망

// ShootingEnemy.cs

public void PlayDeathAnimation()
{
    animator.SetBool("Death", true);
    Destroy(gameObject, animator.GetCurrentAnimatorStateInfo(0).length * 2); // 애니메이션 종료 후 오브젝트 제거
}
// Slash.cs

private void OnTriggerEnter2D(Collider2D collision)
{
    // 적 처리
    if (collision.CompareTag("Enemy"))
    {
        // 적 죽음 애니메이션 실행
        ShootingEnemy enemy = collision.GetComponent<ShootingEnemy>();
        if (enemy != null)
        {
            enemy.PlayDeathAnimation();
        }
    }
}
// EnemyMissile.cs

private void OnTriggerEnter2D(Collider2D other)
{
    if(other.CompareTag("Player"))
    {
		    // ...
    }
    else if (other.CompareTag("Enemy"))
    {
        ShootingEnemy enemy = other.GetComponent<ShootingEnemy>();
        if (enemy != null)
        {
            enemy.PlayDeathAnimation();
        }
        Destroy(gameObject);
    }
}

슬로우모션시 화면 효과 (Post Processing)

  • Main Camera에 Post-process Layer 추가
  • PostProcessVolume 오브젝트에 Post-process Volye 추가 하고 vignette, colo rgrading, bloom 추가 후 모두 체크
// TimeController.cs

[Header("Post Processing")]
public PostProcessVolume postProcessVolume;
private Vignette vignette;
private ColorGrading colorGrading;

public void SetSlowMotion(bool slow)
{
    isSlowMotion = slow;
    if (slow)
    {
        // 슬로우 모션 시작 시 효과 설정
        slowMotionTimer = 0f;
        vignette.intensity.value = 0.8f;         // 비네트 강도 대폭 증가
        colorGrading = postProcessVolume.profile.GetSetting<ColorGrading>();
        colorGrading.saturation.value = -40f;    // 채도 더욱 낮게
        colorGrading.temperature.value = -25f;    // 매우 차가운 색감
        colorGrading.contrast.value = 20f;        // 대비 더 강하게
        colorGrading.postExposure.value = -1.0f;  // 전체적으로 더 어둡게
        colorGrading.tint.value = 10f;           // 약간의 초록빛 추가
    }
    else
    {
        // 슬로우 모션 종료 시 효과 초기화
        vignette.intensity.value = 0f;
        colorGrading.saturation.value = 0f;
        colorGrading.temperature.value = 0f;
        colorGrading.contrast.value = 0f;
        colorGrading.postExposure.value = 0f;
        colorGrading.tint.value = 0f;
    }
}

씬 전환

// Player.cs

private void OnTriggerEnter2D(Collider2D collision)
{
    // 보스 씬 진입 포탈과 충돌 체크
    if (collision.CompareTag("BossSceneTransition"))
    {
        // 보스 씬으로 전환
        SceneManager.LoadScene("BossScene");
    }
}

📝 과제

공부한 내용을 바탕으로 예제 만들어 보기

https://github.com/yh97yhyh/likelion-unity-study/tree/main/DesignPatternAssignment

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함