11
11

 

📢 학습목표

  • 하위 뷰 모델을 뷰 그룹에 생성할 수 있다.
  • 생성된 하위 뷰 모델을 활용하여 다양한 작업을 수행할 수 있다.
  • 뷰 모델싱글톤 매니저의 데이터를 연동할 수 있다.
  • 이벤트 또는 액션에 구독하여 뷰 모델의 데이터를 갱신할 수 있다.

지난 시간 복습

[Binding] 키워드로 바인딩할 대상(클래스 / 함수)를 지정할 수 있습니다.

또한 단방향, 양방향, 이벤트 바인딩을 통해 변수들이 변할 때 마다 추적을 했습니다.

이어서 뷰 그룹을 다루겠습니다.

 

뷰 그룹이란

하위 요소 뷰 모델을 자신의 아래에 원하는 갯수만큼 생성하는 뷰모델입니다.

우선 저번 시간에 다뤘던 DemoViewModel.cs를 다시 보겠습니다.

UnityWeldDemo 씬에 있는 DemoViewModel 오브젝트에서 보시면 컴포넌트 인스펙터에 Plus Minus Item Collection이 있습니다.

이게 바로 상위 뷰 모델에서 소환할 수 있는 하위 뷰모델입니다.

이때 하위 뷰모델은 프리팹과 비슷한 방식으로 복제 기준이 되는 뷰모델입니다.

이제 코드를 뜯어서 이 하위 뷰모델을 어떻게 생성하는지 보겠습니다.

 

DemoViewModel.cs를 열어봅시다.

    [Binding]
    public class DemoViewModel : ViewModel
    {
        [SerializeField] ViewModelCollection _plusMinusItemCollection;

        private void SetCount(int count)
        {
            Count = count;
            IsMinus = Count < 0;
            _plusMinusItemCollection.PrepareViewModels(Math.Abs(Count));
            foreach (var item in _plusMinusItemCollection.GetViewModels())
            {
                var plusMinusItem = item as PlusMinusItem;
                if (plusMinusItem != null)
                {
                    plusMinusItem.IsPlus = Count > 0;
                }
            }
        }

보다시피 ViewModelCollection을 할당 가능하고 이걸 PrepareViewModels(갯수) 메서드를 호출하여 해당 갯수만큼 뷰모델을 생성할 수 있습니다.

 

 

PrepareViewModels 함수 내부

        public void PrepareViewModels(int count)
        {
            if (_pool.Count < count)
            {
                for (int i = _pool.Count; i < count; i++)
                {
                    var viewModel = Instantiate(baseViewModel, transform);
                    _pool.Add(viewModel);
                }
            }
            for (int i = 0; i < count; i++)
            {
                _pool[i].gameObject.SetActive(true);
            }
            for(int i = count; i < _pool.Count; i++)
            {
                _pool[i].gameObject.SetActive(false);
            }
            _count = count;
        }

해당 함수에서 하위 뷰모델을 Instantiate로 복제가 되는걸 볼 수 있습니다.

이렇게 복제한 뷰모델은 아래 이미지와 같이 LayoutGroup에서 위치를 조정가능합니다.

 

 

하위 뷰모델을 원하는 위치에 생성하는 법

예시를 스크롤 뷰 그룹으로 들겠습니다.

스크롤 안에 들어갈 요소들은 스크롤 뷰 아래에 생성해야겠죠?

그러기 위해서 DemoViewModel.cs와 비슷하지만 원하는 위치를 할당 할 수 있는 코드로 개선해본 GroupView.cs를 보여드리겠습니다.

 

 

GroupView.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UI;
using UnityEngine;
using UnityWeld.Binding;

namespace UnityWeld
{
    [Binding]
    public class GroupView : ViewModel
    {
        private bool _isActive;

        [Binding]
        public bool IsActive
        {
            get => _isActive;
            set
            {
                _isActive = value;
                OnPropertyChanged(nameof(IsActive));
            }
        }
        private int _count = 0;
        public int Count => _count;
        [SerializeField] private ViewModel baseViewModel;

        [SerializeField] private Transform scrollParent; // 생성될 뷰모델의 부모
        private List<ViewModel> _pool = new();

        protected override void Awake()
        {
            base.Awake();
            if (scrollParent == null && baseViewModel != null) scrollParent = baseViewModel.transform.parent;
            if(baseViewModel != null) baseViewModel.gameObject.SetActive(false);
        }

        public void PrepareViewModels(int count)
        {
            if (_pool.Count < count)
            {
                for (int i = _pool.Count; i < count; i++)
                {
                    var viewModel = Instantiate(baseViewModel, scrollParent);
                    _pool.Add(viewModel);
                }
            }
            for (int i = 0; i < count; i++)
            {
                _pool[i].gameObject.SetActive(true);
            }
            for(int i = count; i < _pool.Count; i++)
            {
                _pool[i].gameObject.SetActive(false);
            }
            _count = count;
        }
        
        public ViewModel GetViewModel(int index)
        {
            if(index < 0 || index >= Count)
                throw new IndexOutOfRangeException();
            return _pool[index];
        }
        public List<ViewModel> GetViewModels()
        {
            return _pool.GetRange(0, Count);
        }
    }
}

코드를 보시면 위에 [SerializeField] private Transform scrollParent; 부분이 있습니다.

해당 필드를 통해 인스펙터에서 뷰모델의 부모를 할당가능합니다.

위에서 소개해드렸던 DemoViewModel.cs와의 차이점은 ViewModelCollection 필드가 필요없다는 점입니다. 그냥 뷰 그룹에서 원하는 만큼 원하는 위치에 생성하는 것이죠.

핵심은 우리가 원하는 입맛으로 ViewModel.cs를 상속해서 구현할 수 있다는 점입니다.

 


뷰모델에 데이터 연결하기

외부 데이터 가져오는 방법은 굉장히 다양하고 많습니다.

그러나 대부분은 싱글톤으로 데이터를 관리하시는데요.

지금부턴 싱글톤에 있는 데이터를 뷰 모델에서 참조하는 방법을 알려드리겠습니다.

지금부터 나오는 코드들은 직접 아무 폴더에 스크립트 생성해서 따라 적어주시기바랍니다.

 


싱글톤

Copy
using UnityEngine;
using System;

public class GameDataManager : MonoBehaviour
{
    private static GameDataManager _instance;
    public static GameDataManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameDataManager>();
                if (_instance == null)
                {
                    GameObject go = new GameObject("GameDataManager");
                    _instance = go.AddComponent<GameDataManager>();
                }
            }
            return _instance;
        }
    }

    private int _gold = 0;
    public event Action<int> OnGoldChanged;

    public int Gold
    {
        get => _gold;
        set
        {
            _gold = value;
            OnGoldChanged?.Invoke(_gold);
        }
    }
    public void AddGold(int gold)
    {
        Gold += gold;
    }

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

골드가 바뀔 때마다 OnGoldChanged 액션을 호출하여 구독하고 있는 뷰모델들이 해당 데이터를 알 수 있게됩니다.

 

 

왜 이벤트를 안쓰고 액션을 쓸까요?

→ 사실 이건 취향의 영역입니다. 해당 상황에선 매개변수 int값인 골드만 잘 전달하면 되고 sender, 즉 보내는 제공자를 알필요가 없기 때문입니다. 어차피 싱글톤 클래스잖아요.

Copy
using UnityWeld.Binding;

namespace UnityWeld
{

    [Binding]
    public class ShopItemViewModel : ViewModel
    {
        private int _price;
        private bool _canBuy;

        [Binding]
        public int Price
        {
            get => _price;
            set
            {
                _price = value;
                OnPropertyChanged(nameof(Price));
                UpdateCanBuy();
            }
        }

        [Binding]
        public bool CanBuy
        {
            get => _canBuy;
            private set
            {
                _canBuy = value;
                OnPropertyChanged(nameof(CanBuy));
            }
        }

        private void Start() {
            Initialize(1000); //1000원 가격으로 초기화
        }

        public void Initialize(int price)
        {
            Price = price;
            GameDataManager.Instance.OnGoldChanged += _ => UpdateCanBuy();
        }

        private void UpdateCanBuy()
        {
            CanBuy = GameDataManager.Instance.Gold >= Price;
        }

        [Binding]
        public void OnPurchase()
        {
            if (!CanBuy) return;

            GameDataManager.Instance.Gold -= Price;
            // 구매 처리 로직 추가
        }
    }
}

위의 뷰모델은 상점 페이지의 하위 요소들 뷰모델입니다.

이 요소들은 싱글톤 데이터 매니저의 골드 필드가 바뀔 때마다 액션이 호출되면 UpdateCanBuy() 함수를 실행합니다.

그렇기 때문에 구매가능한 골드가 모이면 자동으로 구매 가능한 표시를 하게됩니다.

이 구매 가능한 표시는 CanBuy 변수이고 이것 또한 Binding되어있기 때문에 우리가 UI에서 해당 변수를 바인딩하면 구매버튼을 활성화/비활성화할 수 있습니다.

 

 

해당 상점 뷰 모델을 바인딩한 뷰모델 구현

우선 DemoViewModel 객체에 Shop Item View Model 컴포넌트를 붙여줍니다.

물론 실제 여러분이 만드실 땐 다른 객체에 다른 뷰 모델을 만드셔야하지만 지금은 이렇게 해놓겠습니다.

위에 만든 상점 뷰모델을 바인딩할 버튼으로 Button_Add에 OneWayPropertyBinding 컴포넌트를 줍시다.

이렇게 세팅해주시면 됩니다.

버튼의 interactable를 CanBuy 프로퍼티에 바인딩한 모습입니다.

false라면 버튼을 못 누르겠죠?

추가로 느낌이 좀 있게 버튼 못 누를 땐 버튼을 사라지게 해봅시다.

 

이렇게 버튼을 세팅해놓으시면됩니다.

이렇게만 하면 버튼이 사라지긴하지만 버튼의 내용물인 더하기 이미지는 안사라집니다.

그럼 그 이미지도 one way property로 바인딩참조하여 끄면 되는겁니다.

아주 간단한 문제입니다.

이제 Game Data Manager를 씬에 하나 만들어줍시다.

그리고 버튼도 하나 만들어서 위에 만든 싱글톤 오브젝트 참조해서 누를 때마다 Gold를 500추가합시다

싱글톤 GameDataManager에 AddGold 함수를 버튼에 참조해줍니다

일단 테스트를 위해 그냥 버튼에 했는데 이벤트 바인딩으로 어떻게 구현해야할지도 한번 고민해보시기 바랍니다.

이제 테스트 해보시면 Add 500 버튼을 두번 눌러줘야 싱글톤 데이터 매니저의 골드가 1000이되어 참조중인 뷰모델의 price 기준인 1000을 넘게 되어 더하기 버튼이 생기고 없어지는걸 볼 수 있습니다.

오늘 배운 방식으로 뷰 모델을 원하는 곳에 사용하시기바랍니다. 수고하셨습니다.

 


💡핵심 정리

 

핵심 정리:

  1. 뷰 그룹에 하위 뷰 모델을 생성하여 UI 구조를 효율적으로 관리할 수 있습니다.
  2. 싱글톤 패턴을 활용하여 게임 데이터를 중앙에서 관리하고, 뷰 모델에서 이를 참조할 수 있습니다.
  3. 데이터 바인딩을 통해 UI 요소의 상태를 동적으로 업데이트하여 사용자 경험을 향상시킬 수 있습니다.

📚 같이 보면 좋은 글

COMMENT