-
게임 프로그래밍 패턴 Part 2 디자인 패턴 다시 보기 - 관찰자보관함 2020. 2. 1. 15:37
이 시리즈는 [게임 프로그래밍 패턴]에 등장하는 팁을 정리하고 패턴을 직접 구현하거나 구현되어 있는 패턴을 확인하는 것으로 해당 패턴에 대해 이해하는 것을 목표로 한다.
이번 포스팅에서는 Part 2의 세 번째 패턴인 관찰자 패턴을 살펴보고 유니티로 관찰자 패턴을 이용한 업적 시스템을 만드는 것을 목표로 한다.
관찰자 패턴이란?)
[GoF디자인 패턴]에서는 관찰자 패턴을 다음과 같이 소개했다.
객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 업데이트될 수 있게 만듭니다.
이것을 다시 말하면 어떤 기능을 담당하는 시스템을 객체가 구독(Register) 한 상태에서 객체에 변화가 생길 때 시스템에 알리고(Notify) 시스템에 의미 있는 변화인 경우 특정 기능을 수행하도록 하는 것이다.
정리하면, 관찰자 패턴은 어떤 코드에서 흥미로운 일이 생겼을 때 누가 받든 상관없이 알림을 보낼 수 있다.
구현 준비 및 목표)
유티니 2019를 사용하여 튜토리얼 프로젝트 중 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를 추가한다.
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로 만들어 다시 사용할 때 찾아 쓰도록했다.
실행 결과는 다음과 같다.
추가정보)
- 관찰자 패턴은 동기적이므로 멀티스레드를 많이 사용하는 경우 이벤트 큐를 이용하는 것이 더 좋을 수 있다.
- 만약 관찰자간 순서 때문에 문제가 생긴다면 이는 관찰자 사이에 커플링이 존재한다는 의미이므로 나중에 문제가 될 소지가 크다.
- 객체가 제거될 때에는 구독하고 있던 관찰자를 반드시 해제해줘야 한다.
- 관찰자를 구현하는 최신 방식은 메서드나 함수 레퍼런스만으로 만드는 것이다.