안녕하세요.

 

지난 시간까지 아이템 데이터와 아이템 클래스를 만들고, ItemManager 클래스에서 객체화 하여 다른 클래스에서 사용할 수 있도록 items라는 리스트에 원하는 방식으로 정렬하는 것까지는 살펴보았습니다. 

 

이번 포스팅에서는 그렇게 만들어진 아이템  소환 시스템을 만들면서 어떤 식으로 데이터 객체들이 사용할 수 있는지 함께 살펴보려고합니다. 

 

이 게임은 모바일 방치형 게임이기 때문에 아이템을 얻는 과정을 소환(일명 뽑기)하는 방식으로 한정지었습니다. 아이템을 얻는 방법이 기본적으로는 뽑기 밖에 없다는 이야기 입니다. 

 

보통 모바일 게임에서는 아이템 등급별로 확률이 있지만 이번 시간에는 아이템 객체들을 사용하는 방식을 구현해보는 것이 목적이기 때문에 모든 아이템이 동일한 확률로 나오도록 했습니다. 

 

가장 먼저 아이템을 표시할 UI를 만들어봤습니다. 

 

첫 화면에서 보물상자가 등장하고, 소환을 누르면 오른쪽 그림처럼 아이템들이 나타는 방식입니다.

 

중요한 것은 아이템을 나타내는 아이템 슬롯의 구조입니다. 

 

아이템 슬롯은 크게 3개의 이미지로 되어 있습니다. 아이템 아이콘, 아이템 배경색, 아이템 프래임입니다. 

 

아이템 배경색과 아이템 프래임은 아이템 등급에 따라서 다르게 나타나도록 했습니다. 

 

하나의 아이템 아이콘에 5종류의 배경색과 프래임을 가진 아이템이 등장한다고 보시면 되겠습니다. 

 

이 아이템 슬롯을 해당 아이템에 맞도록 스프라이트를 변경해주어야 하기 때문에 각 슬롯에는 ItemSlot이라는 스크립트를 붙여놨습니다. 

 

public class ItemSlot : MonoBehaviour
{
    [SerializeField]
    Image itemIcon; // 아이콘 이미지 컴포넌트
    [SerializeField]
    Image backGround; // 백그라운드 이미지 컴포넌트
    [SerializeField]
    Image frame; // 프래임 이미지 컴포넌트
  
    public void Setting(Item item) // 슬롯에 아이템 데이터를 가져와 세팅하는 함수
    {
        
        if(item == null) // 빈슬롯으로 나타내고 싶을때 세팅
        {
            itemIcon.sprite = ItemManager.instance.defaultBackGround;
            backGround.sprite = ItemManager.instance.defaultBackGround;
            frame.sprite = ItemManager.instance.defaultSlot;
         
        }
        else // 아이템 데이터가 들어왔을때의 세팅
        {
            itemIcon.sprite = item.itemData.icon;
            backGround.sprite = item.backGround;
            frame.sprite = item.slot;
         
        }
    }
}

 

아이템 슬롯 클래스에는 아이템의 아이콘을 나타내는 이미지 컴포넌트와 백그라운이미지 컴포넌트, 프래임 컴포넌트를 변수로 선언해줍니다. 

 

그리고 세팅 함수를 통해 item 데이터가 들어온다면 각 이미지 컴포넌트의 스프라이트를 item의 데이터가 가진 스프라이트로 입력해줍니다. 만일 빈슬롯으로 만들고 싶다면 ItemManager에서 미리 설정해놓은 디폴트 스프라이트들을 가져다 씁니다. 

 

이후, 각 슬롯에 이 스크립트를 추가하고, 각 슬롯에 이미지 컴포넌트들을 드래그 앤 드롭으로 넣어줍니다.

 

그후 본격적으로 아이템 소환을 구현할  ItemSummon 스크립트를 작성합니다.

 

public class ItemSummon : MonoBehaviour
{
    [SerializeField]
    GameObject boxPanel;
    [SerializeField]
    GameObject summonsPanel;
    [SerializeField]
    GameObject buttonPanel;
    [SerializeField]
    GameObject summonsEffect;
    [SerializeField]
    GridLayoutGroup gridLayout;

    [SerializeField]
    List<ItemSlot> itemSlots = new List<ItemSlot>();

    bool IsSummoning;

    private void OnEnable()
    {
        boxPanel.SetActive(true);
        summonsPanel.SetActive(false);
        buttonPanel.SetActive(true);
        summonsEffect.SetActive(false);

        foreach (ItemSlot it in itemSlots)
        {
            it.gameObject.SetActive(false);
        }
    }

    void ReSetting()
    {
        
        gridLayout.enabled = true;
        itemSlots[0].transform.localScale = new Vector3(1, 1, 1);

        foreach (ItemSlot it in itemSlots)
        {
            it.gameObject.SetActive(false);
        }
    }

    public void OnClick(int num)
    {
        if (!IsSummoning)
        {
            IsSummoning = true;
            ReSetting();
            StartCoroutine(SummonsItem(num));
        }
        else
        {
            return;
        }
        
    }

    IEnumerator SummonsItem(int num)
    {
        summonsPanel.SetActive(false);
        boxPanel.SetActive(true);
        summonsEffect.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        summonsEffect.SetActive(false);
        boxPanel.SetActive(false);
        summonsPanel.SetActive(true);
        boxPanel.SetActive(false);


        for (int i = 0; i < num; i++)
        {
            int nums = Random.Range(0, ItemManager.instance.items.Count);

            itemSlots[i].Setting(ItemManager.instance.items[nums]);
            ItemManager.instance.GetItem(nums);

            if (num ==1)
            {
                gridLayout.enabled = false;
                itemSlots[i].transform.localPosition = new Vector3(0, 0, 0);
                itemSlots[i].transform.localScale = new Vector3(1.2f, 1.2f, 1);
            }

            itemSlots[i].gameObject.SetActive(true);

            yield return new WaitForSeconds(0.3f);
        }

        StopAllCoroutines();
        IsSummoning = false;
    }
}

 

좀 길지만 하나씩 살펴보겠습니다. 

 

    [SerializeField]
    GameObject boxPanel; //아이템 소환 박스가 포함된 UI패널 게임오브젝트
    [SerializeField]
    GameObject summonsPanel; // 소환 버튼이 눌렸을때 활성화 되는 소환 패널 게임 오브젝트
    [SerializeField]
    GameObject buttonPanel; // 버튼이 자식오브젝트로 포함되어있는 버튼 패널 게임 오브젝트
    [SerializeField]
    GameObject summonsEffect; // 소환시에 작동하는 이팩트 게임 오브젝트
    [SerializeField]
    GridLayoutGroup gridLayout; //소환시 아이템 슬롯의 정렬을 담당하는 GridLayoutGroup 컴포넌트

    [SerializeField]
    List<ItemSlot> itemSlots = new List<ItemSlot>(); // 아이템 슬롯 클래스를 담은 리스트
    bool IsSummoning; // 현재 소환중인지 확인하는 bool변수

 

먼저 변수부분입니다. 

 

아이템 소환 UI 과정을 길게 설명하는 것은 아니기 때문에 간단하게만 설명하겠습니다. 소환하기전에는 보물상자가 보이는 box패널을 활성화 하고, 소환 버튼을 누르면 box패널은 비활성화 되고, summonsPanel이 활성화 되어 그안에 차례로 슬롯을 활성화 하는 방식으로 구현하려고 했습니다. 

 

소환에는 1개 소환 버튼이 있고 10개 소환 버튼이 있는데, 1개 소환 버튼을 눌렀을때에는 슬롯이 1개만 활성화 됩니다. 그럴때 만일 슬롯의 부모 오브젝트에 GridLayoutGroup 컴포넌트가 활성화 되어 있으면 1개가 왼쪽 위에만 달랑 나타나게 되는데, 이를 좀 보이기 좋게 만들기 위해서 1개 소환때에는 GridLayoutGroup  컴포넌트를 비활성화 시킨 후 패널의 한가운데 정렬하도록 하고, 10개 소환할때는 GridLayoutGroup 컴포넌트를 활성화 시켜서 아이템 슬롯들이 줄을 맞추어 정렬하도록 하려고 합니다. 

 

다음으로 아이템 슬롯 클래스들을 담을 리스트를 만들어 이 후에 인스팩터 창에 아이템 슬롯들을 드래그 앤 드롭으로  모두 넣어줍니다. 

 

마지막으로 소환 중에 다시 소환 버튼을 눌렀을때 반응을 하지 않도록 하기 위해 IsSummoning이라는 변수를 추가했습니다. 

 

다음 부분을 살펴보겠습니다. 

 

 private void OnEnable()
    {
        boxPanel.SetActive(true); // 보물상자 패널 활성화
        summonsPanel.SetActive(false); // 소환 패널 비활성화
        buttonPanel.SetActive(true); // 버튼 패널 활성화
        summonsEffect.SetActive(false); //소환 이팩트 비활성화

        foreach (ItemSlot it in itemSlots) //아이템 슬롯들 모두 비활성화
        {
            it.gameObject.SetActive(false);
        }
    }

 

OnEnable 함수는 유니티에서 제공하는 함수로 게임오브젝트가 활성화 될 때마다 실행되는 함수입니다. 

 

저는 UI들을 패널로 묶어서 어떤 UI가 나타나야 할때 그 패널 오브젝트 자체를 켜고 끄는 방식으로 활용합니다. 

 

그래서 OnEnable는 이 UI가 활성화 되었을때 초기화 되는 내용을 담고 있습니다. 

 

    void ReSetting()
    {
        gridLayout.enabled = true; //GridLayoutGroup 초기화
        itemSlots[0].transform.localScale = new Vector3(1, 1, 1); 
        // 1회 소환시 사용했던 슬롯의 초기화

        foreach (ItemSlot it in itemSlots)//활성화 되었던 모든 슬롯 비활성화
        {
            it.gameObject.SetActive(false);
        }
    }

 

다음은 소환을 하고 난 후 UI가 비활성화 되지 않은 채로 다시 소환을 눌렀을때 초기화를 해주기 위한ReSetting함수입니다.  이부분은 중요한 부분이 아니니 넘어가도록 하겠습니다.

 

 public void OnClick(int num)// num은 소환 회수를 받는 변수
    {
        if (!IsSummoning) // 소환중에는 작동하지 않도록 하는 플래그
        {
            IsSummoning = true;
            ReSetting(); // 슬롯 초기화
            StartCoroutine(SummonsItem(num)); // 소환 코루틴 시작
        }
        else
        {
            return;
        }
    }

 

이제 소환 버튼을 클릭했을 때 실행되는 함수입니다. 

 

소환 버튼 오브젝트의 버튼 컴포넌트에 OnClick(int num) 이벤트함수를 연결하면 int 값을 입력할 수 있습니다.

 

1회 소환 버튼에는 1을 10회 소환 버튼에는 10을 입력합니다. 

 

그럼 버튼을 눌렀을때 OnClick 함수의 num  변수에 그 버튼 컴포넌트에 할당된 수가  입력됩니다.

 

여기선 소환 회수입니다. 이제 소환 회수를 코루틴 함수에 매개변수로 입력해 실행해줍니다.

 

그리고 이제 가장 중요한 소환 부분을 살펴보도록 하겠습니다.  

 

 IEnumerator SummonsItem(int num) // num : 소환 회수
    {
        summonsPanel.SetActive(false); // 소환 이팩트 활성화
        boxPanel.SetActive(true);
        summonsEffect.SetActive(true);

        yield return new WaitForSeconds(0.5f); // 소환 이팩트 끝나는 0.5초 후에 실행

        summonsEffect.SetActive(false); // 소환 패널 활성화 나머지 비활성화
        boxPanel.SetActive(false);
        summonsPanel.SetActive(true);
        boxPanel.SetActive(false);


        for (int i = 0; i < num; i++) // 소화시작, num 만큼 반복
        {
            int nums = Random.Range(0, ItemManager.instance.items.Count);// 1. 난수 생성

            itemSlots[i].Setting(ItemManager.instance.items[nums]); 
        // 2. ItemManager클래스의 items리스트에서 nums라는 인덱스의 객체를 아이템 슬롯에 세팅
           
           ItemManager.instance.GetItem(nums);//3. 아이템을 획득했을 때 실행하는 함수

            if (num ==1)// 4. 만일 소환횟수가 1일때에는 1개의 슬롯 재배치.
            {
                gridLayout.enabled = false;
                itemSlots[i].transform.localPosition = new Vector3(0, 0, 0);
                itemSlots[i].transform.localScale = new Vector3(1.2f, 1.2f, 1);
            }

            itemSlots[i].gameObject.SetActive(true);
            // 5. 받아온 아이템 데이터로 세팅이 끝난 슬롯 활성화

            yield return new WaitForSeconds(0.3f); // 6. 0.3초후 에 다시 실행
        }

        StopAllCoroutines();// 7. 모든 소환 회수가 끝나면 코루틴 정지
        IsSummoning = false; //  8. 소환의 끝을 알려주는 플래그
    }

 

코루틴을 사용한 이유는 연속으로 아이템이 소환이 될 때 한꺼번에 나타나는게 아니라 시간간격을 두고 하나씩 보이게 하고 싶어서 사용했습니다. 위부분은  소환 이팩트를 실행시키는 부분이기 때문에 넘어가고 실제 소환시에 실행되는 부분만 자세히 살펴보겠습니다. 

 

1.  ItemManager의 items 리스트안에는 모든 아이템 데이터가 들어있습니다. 그렇기 때문에 랜덤한 아이템을 소환한다고 할때 items.Count 만큼의 범위 안에서 난 수를 생성해서 그 숫자의 인덱스를 가진 item을 소환하려고 합니다. 

 

2. 생성된  nums 난수를 가지고 ItemManager의  items[nums]에 속한 Item 객체를 가지고 옵니다. 그리고 그 Item 객체를 첫번째 아이템 슬롯에 세팅합니다.  

 

3. 이렇게 슬롯에 세팅된 아이템 객체는 플레이어가 획득한 것으로 간주함으로 이 동일한 아이템 객체를 매개변수로 넘겨서 ItemManagr의 GetItem() 함수를 실행합니다. 

 

그럼 ItemManager GetItem함수에는 어떤 일이 일어나는지 잠깐 보겠습니다. 

 

public void GetItem(int itemnum)//플레이어가 아이템을 얻었을때 동작하는 함수
    {
        gainedItems.Add(items[itemnum]);// 아이템을 gainedItems리스트에 삽입
        items.RemoveAt(itemnum); // items 리스트에서 제거
    }

 

 ItemManager의 GetItem 함수는 아이템 넘버를 매개변수로 받습니다. 이번 경우는 위에서 생성한 nums 난수를 매개변수로 받는 것입니다. 

 

그렇게 되었을때, 먼저 모든 아이템 객체가 담겨져 있는 items리스트에서 gainedItems 리스트로 해당번호의 아이템 객체를 삽입하게 됩니다. 

 

그 후에 해당 번호의 아이템 객체를 items리스트에서 제거합니다. 

 

그렇게 된다면 다음 번 소환때 같은 번호의 난수를 생성한다고 해도 한번 얻은 아이템을 얻는 것이 아닌 새로운 아이템을 얻게 됩니다. 

 

4. 만일 소화 회수가 1이라면 슬롯들의 부모 오브젝트에 있는 GridLayoutGroup  컴포넌트를 비활성화 한뒤 첫번째 슬롯의 위치를 정가운데로, 스케일을 조금 크게 조정해줍니다.(이후에 ReSetting() 함수에서 다시 원래대로 초기화 합니다.)

 

5. 그렇게 슬롯에 세팅이 끝났다면 세팅이 끝난 슬롯 오브젝트를 활성화 시킵니다. 

 

6. 소환 회수가 1번 이상이라면 0.3초의 간격을 두고 앞의 프로세스를 소환 회수만큼 반복실행합니다. 

 

7. 소환이 끝나면 코루틴을 멈춥니다. 

 

8. 다시 소환 버튼이 활성화 되도록 IsSummoning 변수를 false로 만듭니다. 

 

 

포스팅이 쓸데없이 길었지만 오늘 포스팅의 핵심은 앞에서 만든 아이템 데이터와 객체를 어떤 식으로 사용하느냐 입니다. 

 

사실 중요한 부분은 단 두줄입니다.

 

itemSlots[i].Setting(ItemManager.instance.items[nums]);
ItemManager.instance.GetItem(nums);

 

첫번째 줄을 통해 아이템 데이터를 슬롯UI에 세팅해줄때 items 리스트에서 객체를 받아와 매개변수로 넘겨주면 한 줄만에 쉽게 데이터들을 슬롯에 세팅해 줄 수 있습니다.

그리고 두번째 줄을 통해 아이템을 획득하는 과정 역시 한줄로 가능하게 만들었다는 점입니다. 

 

이렇게 ItemManager를 싱클톤화 한 후에 items와 gainedItems 리스트 안에 있는 아이템 객체들을 불러온 다면 쉽게 아이템을 획득하거나 사용하는 방식을 구현할 수 있다는 것이 오늘 포스팅의 핵심이라고 할 수 있을 것 같습니다. 

 

다음 시간에는 이렇게 획득한 아이템들을 인벤토리로 구현하는 부분을 살펴보도록 하겠습니다.

 

감사합니다. 

+ Recent posts