ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 게임 프로그래밍 패턴 Part 2 디자인 패턴 다시 보기 - 관찰자
    보관함 2020. 2. 1. 15:37

    게임 프로그래밍 패턴로버트 나이스트롬 (Robert Nystrom)
    상세보기

    이 시리즈는 [게임 프로그래밍 패턴]에 등장하는 팁을 정리하고 패턴을 직접 구현하거나 구현되어 있는 패턴을 확인하는 것으로 해당 패턴에 대해 이해하는 것을 목표로 한다.

     

    이번 포스팅에서는 Part 2의 세 번째 패턴인 관찰자 패턴을 살펴보고 유니티로 관찰자 패턴을 이용한 업적 시스템을 만드는 것을 목표로 한다.


    관찰자 패턴이란?)

    [GoF디자인 패턴]에서는 관찰자 패턴을 다음과 같이 소개했다.

    객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 업데이트될 수 있게 만듭니다.

     

    이것을 다시 말하면 어떤 기능을 담당하는 시스템을 객체가 구독(Register) 한 상태에서 객체에 변화가 생길 때 시스템에 알리고(Notify) 시스템에 의미 있는 변화인 경우 특정 기능을 수행하도록 하는 것이다.

     

    정리하면, 관찰자 패턴은 어떤 코드에서 흥미로운 일이 생겼을 때 누가 받든 상관없이 알림을 보낼 수 있다.

     

    구현 준비 및 목표)

    유티니 2019를 사용하여 튜토리얼 프로젝트 중 2D Roguelike를 구현한다.

    Unity Learn - 2D Roguelike

    구현 목표는 다음과 같다.

        1. 관찰자 패턴을 적용한 업적 매니저를 제작한다.

        2. 업적 달성 시 화면에 알림을 표시하는 기능을 추가한다.

     

    구현 과정)

    | 업적 매니저 제작

    업적을 달성하기 위한 조건을 업적매니저에 꾸준히 보내주고 받은 정보를 바탕으로 해결 가능한 부분을 확인해 업적을 해금하도록 스크립트를 추가한다.

    using System.Collections;
    using System.Collections.Generic;
    using System;
    using UnityEngine;
    
    public class AcheivementManager : MonoBehaviour
    {
        public enum Achievements
        {
            Walk50,
            ReachDay5,
        }
    
        class AchievementsComparer : IEqualityComparer<Achievements>
        {
            public bool Equals(Achievements a, Achievements b)
            {
                return a == b;
            }
            public int GetHashCode(Achievements obj)
            {
                return ((int)obj).GetHashCode();
            }
        }
    
        static AcheivementManager _instance;
    
        public static AcheivementManager Instance
        {
            get
            {
                if(_instance == null)
                {
                    GameObject obj = new GameObject("AcheivementManager");
                    _instance = obj.AddComponent<AcheivementManager>();
                    DontDestroyOnLoad(obj);
                }
                return _instance;
            }
        }
    
        Dictionary<Achievements, bool> _dicAchievementUnlock =
            new Dictionary<Achievements, bool>(new AchievementsComparer());
    
        public void OnNotify(Achievements achv, int level = 0, int totalWalk = 0)
        {
            switch(achv)
            {
                case Achievements.ReachDay5:
                    UnlockReachDay5(level);
                    break;
                case Achievements.Walk50:
                    UnlockWalk50(totalWalk);
                    break;
            }
        }
    
        public AcheivementManager()
        {
            foreach(Achievements achv in Enum.GetValues(typeof(Achievements)))
            {
                _dicAchievementUnlock[achv] = false;
            }
        }
    
        void UnlockWalk50(int totalWalk)
        {
            if (_dicAchievementUnlock[Achievements.Walk50])
                return;
    
            if(totalWalk >= 50)
            {
                Debug.Log("50걸음 걷기 달성!");
                _dicAchievementUnlock[Achievements.Walk50] = true;
            }
        }
    
        void UnlockReachDay5(int level)
        {
            if (_dicAchievementUnlock[Achievements.ReachDay5])
                return;
    
            if (level >= 5)
            {
                Debug.Log("5일 버티기 성공!");
                _dicAchievementUnlock[Achievements.ReachDay5] = true;
            }
        }
    }

     

    그리고 여기에서 추가한 조건을 만족시킬 수 있도록 다음과 같이 누적 걸음 수를 기록하는 변수를 추가하고 이동할 때 해당 값을 늘리고 체크하는 콜백을 추가했다.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class InputManager : MonoBehaviour
    {
    // ... 중략
    
        private int _totalWalk = 0;
    
        private void Start()
        {
            if (instance == null)
            {
                instance = this;
            }
            else if (instance != this)
            {
                Destroy(gameObject);
                return;
            }
    
            DontDestroyOnLoad(gameObject);
    
            LeftButton += AddTotalWalk;
            RightButton += AddTotalWalk;
            UpButton += AddTotalWalk;
            DownButton += AddTotalWalk;
        }
    
    // ... 중략
    
        void AddTotalWalk()
        {
            _totalWalk++;
    
            AcheivementManager.Instance.OnNotify(
                AcheivementManager.Achievements.Walk100,
                totalWalk: _totalWalk
                );
        }
    }

     

    또, 마찬가지로 게임의 레벨(Day)가 증가시켜주는 부분에 업적 매니저에 정보를 넘기는 코드를 추가한다.

    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine.UI;
    
    public class GameManager : MonoBehaviour
    {
    // ... 중략
    
        private void OnLevelWasLoaded(int index)
        {
            level++;
    
            AcheivementManager.Instance.OnNotify(
                AcheivementManager.Achievements.ReachDay10,
                level
                );
    
            InitGame();
            enemiesMoving = false;
        }
    
    // ... 후략
    }

     

    추가적으로 Player는 명령 패턴에 넣는 콜백을 대입에서 추가로 수정하고 OnDestroy에서 다시 해당 콜백을 제거하도록 했다.

    using UnityEngine;
    using System.Collections;
    using UnityEngine.SceneManagement;
    using UnityEngine.UI;
    
    public class Player : MovingObject
    {
    // ... 중략
        protected override void Start()
        {
            animator = GetComponent<Animator>();
    
            food = GameManager.instance.playerFoodPoints;
    
            foodText.text = $"Food : {food}";
    
            InputManager.instance.LeftButton += MoveLeft;
            InputManager.instance.RightButton += MoveRight;
            InputManager.instance.UpButton += MoveUp;
            InputManager.instance.DownButton += MoveDown;
    
            base.Start();
        }
    
        private void OnDestroy()
        {
            InputManager.instance.LeftButton -= MoveLeft;
            InputManager.instance.RightButton -= MoveRight;
            InputManager.instance.UpButton -= MoveUp;
            InputManager.instance.DownButton -= MoveDown;
        }
    // ... 후략
    }

    위 코드는 기존 2D Roguelike에 명령 패턴을 적용한 것으로 이전 포스팅(게임 프로그래밍 패턴 Part 2 디자인 패턴 다시 보기 - 명령)에서 확인할 수 있다.


    처리 결과는 다음과 같다.

     

    | 업적 달성 알림 기능 추가

    달성 알림을 보여주기 위해 Text UI를 추가한다.

    ahievementText라는 이름으로 Text추가, 사이즈 28, 색 흰색(255,255,255)

     

    AchievementManager엔 다음 코드를 추가하여 도전과제 달성 시 UI로 표시되도록 한다.

    using System.Collections;
    using System.Collections.Generic;
    using System;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.SceneManagement;
    
    public class AcheivementManager : MonoBehaviour
    {
    // ... 중략
        Text _achievementText;
        Text achievementText
        {
            get
            {
                if(_achievementText == null)
                {
                    GameObject obj = GameObject.Find("Canvas/achievementText");
                    if(obj != null)
                    {
                        _achievementText = obj.GetComponent<Text>();
                    }
                }
                return _achievementText;
            }
        }
    
    // ... 중략
        private void Start()
        {
            SceneManager.sceneLoaded += OnSceneLoad;
        }
    
        void OnSceneLoad(Scene scene, LoadSceneMode loadSceneMode)
        {
            _achievementText = null;
        }
    
        public void OnNotify(Achievements achv, int level = 0, int totalWalk = 0)
        {
            switch(achv)
            {
                case Achievements.ReachDay5:
                    UnlockReachDay5(level);
                    break;
                case Achievements.Walk50:
                    UnlockWalk50(totalWalk);
                    break;
            }
        }
    
        void UnlockWalk50(int totalWalk)
        {
            if (_dicAchievementUnlock[Achievements.Walk50])
                return;
    
            if(totalWalk >= 50)
            {
                _dicAchievementUnlock[Achievements.Walk50] = true;
                StartCoroutine(Cor_ShowText5Sec("50걸음 걷기 달성!"));
            }
        }
    
        void UnlockReachDay5(int level)
        {
            if (_dicAchievementUnlock[Achievements.ReachDay5])
                return;
    
            if (level >= 5)
            {
                _dicAchievementUnlock[Achievements.ReachDay5] = true;
                StartCoroutine(Cor_ShowText5Sec("5일 버티기 성공!"));
            }
        }
    
        IEnumerator Cor_ShowText5Sec(string text)
        {
            achievementText.text = text;
            yield return new WaitForSeconds(5);
            achievementText.text = string.Empty;
        }
    }

     

    씬을 재 로드할때 기존에 찾아둔 UI가 제거되어 쓰레기 값을 가리키는 일이 발생하지 않도록 씬일 로딩됐을 때 해당 값을 null로 만들어 다시 사용할 때 찾아 쓰도록했다.

     

    실행 결과는 다음과 같다.

     

    추가정보)

    • 관찰자 패턴은 동기적이므로 멀티스레드를 많이 사용하는 경우 이벤트 큐를 이용하는 것이 더 좋을 수 있다.
    • 만약 관찰자간 순서 때문에 문제가 생긴다면 이는 관찰자 사이에 커플링이 존재한다는 의미이므로 나중에 문제가 될 소지가 크다.
    • 객체가 제거될 때에는 구독하고 있던 관찰자를 반드시 해제해줘야 한다.
    • 관찰자를 구현하는 최신 방식은 메서드나 함수 레퍼런스만으로 만드는 것이다.

     

    댓글

Designed by Tistory.