22.02.15 업적 창 구현하기

2022. 2. 15. 19:02Unity3D/수업 내용

업적 창은 지난 번 포스팅했던 상점 구현과 비슷하다.

데이터를 불러오고, UI에 표기하는 것.


[도전 과제]

  • 보상 수령이 가능한 업적이 맨 위에, 진행 중인 업적이 가운데에, 모두 완료한 업적은 맨 밑에 나열되도록 할 것이다.
  • 업적 1단계를 완료하고 보상을 받은 후, 보상이 늘어난 다음 단계의 업적 달성 조건을 보여준다.
  • 업적 달성 정도에 따라 별 개수가 달라지고, 버튼 모양 또한 달라지게 한다.
  • 업적 리스트가 실시간으로 갱신된다.

 

[해결 과정]

  • Achievement Info 클래스를 만들어서 게임의 모든 업적 진행 상황을 담았다. 이 인포 클래스를 참조해서 업적이 진행 중인지, 완료된 상태인지, 보상 수령이 가능한 상태인지 판별한다.
  • 업적 창에서 업적의 진행 상태에 따라서 위에 둘 것인지, 밑에 둘 것인지를 정하기 위해 상태별로 리스트를 만들었다. 완료된 업적의 인덱스는 리스트에 들어가서 나중에 슬롯을 생성할 때 가장 마지막에 생성하도록 한다. 
  • 버튼의 Onclick.AddListener로 대리자 함수를 실행하게 했다. 대리자에는 업적 창을 새로 갱신하는 코드가 들어있어서, 버튼을 눌러 보상을 받고난 다음에는, 그 업적 슬롯이 밑으로 내려가도록 했다. 

 

실행 결과


 

소스 코드


public class GameInfo
{
    public int score;
    public int heart;
    public int gold;
    public int gem;
    public HeroInfo hero;
    public List<ItemInfo> inventory;
    public List<StageInfo> stageInfos;
    public List<AchievementInfo> achievementBook;

    //for be referenced by achievement's slider.
    public int collectedGoldSum;
    public int killedMonsterSum;

    public void InitGameInfo()
    {
        hero = new HeroInfo();
        hero.Init();
        inventory = new List<ItemInfo>();
        InitInventory();
        stageInfos = new List<StageInfo>();
        achievementBook = new List<AchievementInfo>();
        InitAchievementBook();
    }

    void InitAchievementBook()
    {
        Dictionary<int, AchievementData> dict = DataManager.GetInstance().GetDictAchievementData();
        foreach(int data in dict.Keys)
        {
            achievementBook.Add(new AchievementInfo(data, 0, false));
        }
    }
}
public class AchievementInfo
{
    public int id;
    public int star;
    public bool isFull;

    public AchievementInfo(int id, int star, bool isFull)
    {
        this.id = id;
        this.star = star;
        this.isFull = isFull;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIMissionControl : MonoBehaviour
{
    [SerializeField] GameObject goPopUp;
    [SerializeField] GameObject grid;
    [SerializeField] GameObject slotPrefab;

    List<int> finishAchiIndex = new List<int>();
    List<int> undergoingAchiIndex = new List<int>();
    List<int> isFullAchiIndex = new List<int>();
    List<GameObject> genedSlots = new List<GameObject>();

    public void BtnTest()
    {
        DataManager.GetInstance().AddUpGold(500);
        DataManager.GetInstance().AddUpKilledMonster(1000);
        LoadAchiSlot();
    }

    public void ClosePopUp()
    {
        goPopUp.SetActive(false);
    }

    public void OpenPopUp()
    {
        LoadAchiSlot();
        goPopUp.SetActive(true);
    }

    void LoadAchiSlot()
    {
    	//업적 창을 초기화하기 위해서
        foreach(GameObject go in genedSlots)
        {
            Destroy(go);
        }
        genedSlots.RemoveRange(0, genedSlots.Count);
        finishAchiIndex.RemoveRange(0, finishAchiIndex.Count);
        isFullAchiIndex.RemoveRange(0, isFullAchiIndex.Count);
        undergoingAchiIndex.RemoveRange(0, undergoingAchiIndex.Count);
		//~업적 창을 초기화하기 위해서

        Dictionary<int, AchievementData> dict = DataManager.GetInstance().GetDictAchievementData();

        foreach(AchievementData data in dict.Values)
        {
            AchievementInfo info = DataManager.GetInstance().GetAchiInfo(data.id);
            AchievementProgressData data2 = DataManager.GetInstance().GetAchiProgressData(data.progressId + (int)Mathf.Clamp(info.star, 0, 4));
			//업적을 달성한 횟수(== 별 갯수)에 따라서 
            //업적의 다음 단계를 순차적으로 보여주게 한다.
            
            //몬스터 100마리를 잡으세요인 조건일 때, 
            //정보를 보고 100마리를 잡았다면 보상수령이 가능한 상태로 만든다(== 'isFull = true')
            if((float)DataManager.GetInstance().GetSliderReference(data.typeIndex) / (float)data2.numForPercentage >= 1f)
            {
                info.isFull = true;
            }
		
        	//위에서부터 '완료한 상태', '보상수령이 가능한 상태', '진행 중'으로 구분하고, 
            //각각의 리스트가 구분된 인덱스를 가져간다. 
            if (info.star >= 5)
            {
                finishAchiIndex.Add(info.id);
            }
            else if (info.isFull)
            {
                isFullAchiIndex.Add(info.id);
            }
            else if (!info.isFull)
            {
                undergoingAchiIndex.Add(info.id);
            }
        }
C# 
		//각각의 리스트가 담고 있는 인덱스를 차례대로 꺼내어 업적 창에 순서대로 진열한다.
        foreach(int index in isFullAchiIndex)
        {
            GameObject go = Instantiate(slotPrefab, grid.transform);
            UIAchiSlotControl slot = go.GetComponent<UIAchiSlotControl>();
            slot.InitSlot(index);
            //이 구문이 업적 슬롯에 실제로 정보를 부여하는 코드이다. 
            //인덱스(업적id)에 따라 업적 슬롯에 해당 업적에 대한 정보를 채워넣는다.
            
            slot.whenPressBtnClaim = LoadAchiSlot;
            //버튼을 누를때마다 업적창을 갱신하기 위해 대리자에 함수를 넣어둔다.
            
            genedSlots.Add(go);
            //업적창을 초기화할때 사용하기 위해 담아둔다.
        }
        foreach(int index in undergoingAchiIndex)
        {
            GameObject go = Instantiate(slotPrefab, grid.transform);
            UIAchiSlotControl slot = go.GetComponent<UIAchiSlotControl>();
            slot.InitSlot(index);
            slot.whenPressBtnClaim = LoadAchiSlot;
            genedSlots.Add(go);
        }
        foreach (int index in finishAchiIndex)
        {
            GameObject go = Instantiate(slotPrefab, grid.transform);
            UIAchiSlotControl slot = go.GetComponent<UIAchiSlotControl>();
            slot.InitSlot(index);
            slot.whenPressBtnClaim = LoadAchiSlot;
            genedSlots.Add(go);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.U2D;

public class UIAchiSlotControl : MonoBehaviour
{
    int id;
    [SerializeField] Image icon;
    [SerializeField] Slider slider;
    [SerializeField] Text nameText;
    [SerializeField] Image budgetIcon;
    [SerializeField] Text rewardText;
    public GameObject[] BtnClaim;
    [SerializeField] GameObject[] star;
    [SerializeField] SpriteAtlas atlas;
    public System.Action whenPressBtnClaim;

    private void Start()
    {
        BtnClaim[1].GetComponent<Button>().onClick.AddListener(InitBtnClaim);
    }

    public void InitSlot(int id)
    {
        this.id = id;
        LoadSlotInfo();
    }

    void InitBtnClaim()
    {
        AchievementData data = DataManager.GetInstance().GetAchievementData(id);
        AchievementInfo info = DataManager.GetInstance().GetAchiInfo(data.id);
        AchievementProgressData data2 = DataManager.GetInstance().GetAchiProgressData(data.progressId + (int)Mathf.Clamp(info.star, 0, 4));
        BudgetData data3 = DataManager.GetInstance().GetBudgetData(data.budgetId);

        info.isFull = false;
        info.star++;
        if (info.star >= 5)
        {
            info.star = 5;
            this.slider.value = 1f;
        }
        else
        {
            this.slider.value = 0f;
        }
        DataManager.GetInstance().PressBtnClaim(data.budgetId, data2.rewardText);
        whenPressBtnClaim();
    }

    public void LoadSlotInfo()
    {
        AchievementData data = DataManager.GetInstance().GetAchievementData(id);
        AchievementInfo info = DataManager.GetInstance().GetAchiInfo(data.id);
        AchievementProgressData data2 = DataManager.GetInstance().GetAchiProgressData(data.progressId + (int)Mathf.Clamp(info.star, 0, 4));
        BudgetData data3 = DataManager.GetInstance().GetBudgetData(data.budgetId);

        this.icon.sprite = atlas.GetSprite(data.spriteName);
        this.slider.value = (float)DataManager.GetInstance().GetSliderReference(data.typeIndex) / (float)data2.numForPercentage;
        this.nameText.text = data2.name;
        this.budgetIcon.sprite = atlas.GetSprite(data3.spriteName);
        this.rewardText.text = data3.textColor + data2.rewardText + "</color>";
        ChooseClaimBtnFrame(info, data, data2);
        FillStar(info);
    }

    void ChooseClaimBtnFrame(AchievementInfo info, AchievementData data, AchievementProgressData data2)
    {
        foreach (GameObject go in BtnClaim)
        {
            go.SetActive(false);
        }
        if (info.star >= 5)
        {
            BtnClaim[2].SetActive(true);
        }
        else if(info.isFull)
        {

            BtnClaim[1].SetActive(true);
        } else if (!info.isFull)
        {
            BtnClaim[0].SetActive(true);
        }
    }

    void FillStar(AchievementInfo info)
    {
        for(int s = 0; s < info.star; s++)
        {
            star[s].SetActive(true);
        }
    }
}

 

[피드백]

 

※ 업적이 여러 단계로 되어있을 경우 메인테이블에서 세분된 테이블로 한 번 더 넘어가주는 게 좋음.

시작 id의 위치를 간단하게 알 수 있었다.

 

※ UI 디자인부터 마치고 나서 스크립트 작업을 했을 때 UI의 구조가 한 눈에 들어와서 코드를 설계하는 게 수월했다.

 

※ 서로 다른 테이블에 있는 데이터를 참조할 때 참조하는 id를 주의깊게 살피자. 인덱스오류가 자주 발생하는 부분이다.

 

※ 리스트는 사용하기 직전에 new를 쓰면 정상적으로 동작하지 않는다. new와 사용하는 구문 사이에 시간 차를 조금 둬야 한다.

 

※ 버튼의 AddListener에 들어가는 함수는 들어갈 당시의 상태와 동일하게 작동한다. 버튼을 누르는 때마다 상태가 달라져야 한다면 누를 때마다 참조를 새로 구해야 한다.