1.도전과제 MVC 리팩토링체크

ㄴ 도전과제 알람에 내용이 제대로 출력되지 않아서 다시 한번더 리팩토링 하였음

ㄴ 인스펙터에 컴포넌트가 미싱되어있는 상황이여 수정하였음

2.Player HP 스텟 UI와 연결

ㄴLobby 화면에 있는 체력UI를 플레이어 객체와 연결하여 사용

적에게 공격을 받아 체력이 감소
MVC 패턴으로 설계한뒤 UI매니저에 등록하여 사용 중

 

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using ScottGarland;

public class UIPlayerHPDisplayView : MonoBehaviour, IUIBase
{
    [SerializeField] private RectTransform HPFrontImg;
    [SerializeField] private TextMeshProUGUI HpText;

    public void HpRatioChange(BigInteger curHp , BigInteger maxHP)
    {
        int curHpNum = BigInteger.ToInt32(curHp);
        int maxHpNum = BigInteger.ToInt32(maxHP);
        float result = curHpNum / (float)maxHpNum;

        HPFrontImg.localScale = new Vector3(result, 1, 1);

        string curHealthString = Utils.FormatBigInteger(curHp);
        string maxHealthString = Utils.FormatBigInteger(maxHP);

        HpText.text = $"{curHealthString} / {maxHealthString}";
    }

    public void HideUI()
    {
        gameObject.SetActive(false);
    }

    public void Initialize()
    {
        
    }

    public void ShowUI()
    {
        gameObject.SetActive(true);
    }

    public void UpdateUI()
    {

    }
}

UIPlayerHPDisplayView.cs에 BigInterger를 적용하여 큰 숫자를 더욱 쉽게 출력 할 수 있게 구현

 

3.Player가 죽었을때 밀려나는 버그 해결

ㄴ Player가 죽으면 , Player의 동작이 비활성화 되어(enabled = false) 적의 공격을 맞고 멀리 날라가는 버그가 발생하였다. 

    [ContextMenu("PlayerDie")]
    public void Die()
    {
        if (!baseHpSystem.IsDead)
        {
            baseHpSystem.IsDead = true;
            Debug.Log("Player Die!!! ");
            string animName = PlayerAnimationController.DeathAnimationName;
            PlayerAnimationController.spineAnimationState.SetAnimation(0, animName, false);

            rb.velocity = Vector3.zero; //캐릭터 이동되지않게 속도를 0으로 수정
            rb.isKinematic = true;
            GameManager.Instance.GameOver();
            enabled = false;
        }
    }

    public void Respwan()
    {
        //ToDoCode : 플레이어가 죽을경우 재세팅하는 함수
        statHandler.CurrentStat.health = statHandler.CurrentStat.maxHealth;
        transform.position = Vector3.up;
        rb.isKinematic = false;
        enabled = true;
        baseHpSystem.IsDead = false;
        UIManager.Instance.ShowUI("PlayerHPDisplay");
    }

Die 메서드에서 죽으면 리지드바디의 키네마틱 옵션을 true로바꿔 물리충돌 연산을 하지 않게 막은뒤

Respwan 메서드에서 다시 부화할때 리지드바디의 키네마틱을 false로 바꿔 처리 하였다.

 

4.현재 이슈 : Stage 맵 중복 생성

스테이지가 진행될때마다 맵이 복사가 되고있다.

몬스터 헌터 과제 완료 : 몬스터 1마리를 쓰러트리면 얻는 도전과제 획득 팝업

 

구현을 목적으로 팀원이 작성한 도전과제 시스템을 리팩토링하여 UI매니저에 등록하였고 구현까지 하였다.

아직 버그가 있는지는 확인을 못한 상태이고 , 몬스터 처치 업적은 제대로 완수되는것이 확인 되었다. 

 

도전과제와 도전과제 알람 팝업창 코드를 MVC 패턴을 적용하였다.

도전과제와 도전과에 알람 팝업창을 UI매니저에 등록시키기 위해 MVC패턴을 사용 하였다. 

UIManager에 UIController로 저장을 하게 해놓고 필욜할떄 Controller를 호출하여 Model과 View에 접근하는 방식을 취하고 있다.

 

인벤토리와 아이템 정보창 UI 에셋 적용한 상태

선택한 아이템(아이템 정보창)이 왼쪽으로 좀 치우러져 있어 조정이 필요한듯 하다. 

금일 MVP가 일주일 정도 남은 기간에 테스트해보기위헤 작업하던것을 Merge를 해본 결과

UI에 필요한 Data 를 세팅하는 과정에서 각각 호출시점이 꼬여 null값이 들어가거나 잘못된 데이터가 들어가는 경우가 발생하였다.

 

판단하기로는 몇시간만에 끝날 작업은 아니라고 판단하여 , 데이터를 초기화하는 매니저들과 UI들의 호출시점을 다시 선정하고 리팩토링하기로 진행하였다.

 

이번 주말 ( 토~ 일) 안에 작업을 진행하고 , Player의 Stat 및 데미지 표기 + Stage 진행 과정을 구현 해야 MVP에 제대로 된 피드백을 받을수 있을것 같다.

 

글로 설명하기가 힘들어 간단하게라도 구조를 그려보았다.

 

ItemStatusView 클래스에 EquipBtn 변수가 있고 해당 Button변수에 컨트롤러에 있는 데이터를 인자값으로 받아와 이벤트를 등록해놨는데 이때 인자값의 초기값은 null값으로 설정이 되있었다.

 

ItemSlot 클래스에 버튼을 눌러 Item 데이터를 컨트롤러로 보내 초기화를 하였기에 null값은 사라졌다고 생각했다

왜냐하면 Class는 참조값이기에 변경될것이기 때문이다),

 

그리고 실제로 콘솔창에서는 null값이 들어온다고 오류가 발생하였다.

 

원인은 item이 참조형 변수라고 해도, 이 참조가 유효하려면 메모리 상의 객체를 가리키고 있어야 하는데 이미 처음 초기값을 null로 설정 한 뒤였고, 해당 변수는 다른 클래스(ItemStatusView)로 넘어가 있는 상태였기에 아무리 Controller에서 item이 초기화가 되더라도 view에서 받는 이벤트의 인자값은 null이 들어오는 것이였다.

 

즉, 이 문제를 일으킨 이유는 

null 빈 데이터라고만 착각을 해버려서 발생한 문제였다. 

null은 빈데이터가 아닌 주소값이 없다라는 의미이기 떄문이다.

 

C++에서는 포인터 개념이 있기에 주소값 및 할당을 하게되면 무조건 해제를 해야만하지만 

C#을 계속 사용하다보니 주소값에 대해서 깜빡했던것 같다.

C#은 포인터 개념이 없기 때문에 자동으로 Heap영역에 알아서 할당이 되었기 때문에 간단히 생각을 했던것이다. 

 

 

대처법으로는 View의 버튼 변수를 프로퍼티로 선언한뒤 호출하여 Controller에서 이벤트를 등록하였다.

public class ItemStatusController : UIController
{
    public ItemSlot SelectItem;
    private ItemStatusModel itemStatusModel;
    private ItemStatusView itemStatusView;

    public override void Initialize(IUIBase view, UIModel model)
    {
        itemStatusModel = model as ItemStatusModel;
        itemStatusView = view as ItemStatusView;

        base.Initialize(itemStatusView, itemStatusModel);
        //아이템 장착 버튼 이벤트 함수 등록 (장착 , UI 출력)
        itemStatusView.EquipButton.onClick.AddListener(() => GameManager.Instance.player.EquipItem(SelectItem.item));
        itemStatusView.EquipButton.onClick.AddListener(() => OnShow());

        //아이템 장착해제 버튼 이벤트 함수 등록 (장착해제 , UI 출력)
        itemStatusView.DisEquipButton.onClick.AddListener(() => GameManager.Instance.player.DisEquipItem());
        itemStatusView.DisEquipButton.onClick.AddListener(() => OnShow());
    }
}

 

public class ItemStatusView : MonoBehaviour, IUIBase
{
    [SerializeField] private TextMeshProUGUI curUpgradeLevelText;
    [SerializeField] private TextMeshProUGUI maxUpgradeLeveText;
    [SerializeField] private TextMeshProUGUI UpgradeCostText;
    [SerializeField] private TextMeshProUGUI itemPassiveEffectText;
    [SerializeField] private TextMeshProUGUI itemEquipEffectText;
    [SerializeField] private Image ItemIcon;
    [SerializeField] private Button UpgradeBtn;
    [SerializeField] private Button EquipBtn;
    [SerializeField] private Button DisEquipBtn;


    public Button EquipButton { get => EquipBtn; }
    public Button DisEquipButton { get => DisEquipBtn; }
    public Button UpgradeButton { get => UpgradeBtn; }
}

 

이런식으로 사용하니, 우선 item은 null값이 들어가있는 상태에서 외부에서 데이터가 초기화 되더라도 

Controller 클래스 안에 멤버변수로 되어있으니 참조값을 제대로 활용 할 수 있게 되었다. 

구현된 인벤토리

ItemDB.Json을 통하여 ItemSlot들을 생성하여 인벤토리를 구현하였고 , ItemSlot은 현재 생성/파괴를 하고있기에 , 오브젝트 풀링을 이용하여 리팩토링 해야된다. 

 

해당 구조도 MVP패턴을 이용하여 구현하였다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InventoryController : UIController
{
    private InventoryModel inventoryModel;
    private InventoryView inventoryView;

    public InventoryModel Model { get => inventoryModel; set => inventoryModel = value; }
    public InventoryView View { get => inventoryView; set => inventoryView = value; }

    public override void Initialize(IUIBase view, UIModel model)
    {
        inventoryModel = model as InventoryModel;
        inventoryView = view as InventoryView;

        base.Initialize(inventoryView, inventoryModel);
    }

    public override void OnShow()
    {
        view.ShowUI();
        UpdateView();   // 초기 View 갱신
    }

    public override void OnHide()
    {
        view.HideUI();
    }

    public override void UpdateView()
    {
        // Model 데이터를 기반으로 View 갱신
        view.UpdateUI();
    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.U2D;

public class InventoryModel : UIModel
{
    public List<Item> Items = new List<Item>(); // 소지하고있는 아이템 리스트 

    public void Initilaize()
    {
        foreach (ItemDB Data in DataManager.Instance.ItemDB.ItemsDict.Values)
        {
            Item itemObj = new Item();
            itemObj.Initialize(Data);
            Items.Add(itemObj);
        }
    }

    public void AddItem(string item)
    {

    }

    public void RemoveItem(string item)
    {

    }
}
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem.XR;

public class InventoryView : MonoBehaviour, IUIBase
{
    [SerializeField] private Transform itemSlotParent;
    [SerializeField] private ItemSlot itemSlotPrefab;
    [SerializeField] private RectTransform itemSlotBoundary;
    public InventoryController Controller;

    private List<ItemSlot> itemSlots = new List<ItemSlot>();

    public void Initialize()
    {
        if (itemSlotPrefab == null)
        {
            itemSlotPrefab = Resources.Load<ItemSlot>("Prefabs/Item/ItemSlot");
        }

        for (int i = 0; i < Controller.Model.Items.Count; i++)
        {
            itemSlots.Add(Instantiate(itemSlotPrefab, itemSlotParent));

            itemSlots[i].Initiliaze(Controller.Model.Items[i]);
        }
    }

    public void ShowUI()
    {
        Vector2 size = itemSlotBoundary.sizeDelta;
        size.y = 135 * ((itemSlots.Count / 4) + 1);
        itemSlotBoundary.sizeDelta = size;
    }

    public void HideUI()
    {
    }

    public void UpdateUI()
    {
        Debug.LogAssertion("인벤토리 UI 업데이트");
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIInventory : MonoBehaviour
{
    public string UIKey;

    private InventoryModel model;
    [SerializeField] private InventoryView[] views;
    private InventoryController controller;

    private void Start()
    {
        //Model(Data) 초기화
        model = new InventoryModel();
        model.Initilaize();
        GameManager.Instance._player.Inventory = model;

        //컨트롤러  초기화 및 View 등록
        controller = new InventoryController();
        for (int i = 0; i < views.Length; i++) 
        {
            views[i].Controller = controller;
            controller.Initialize(views[i], model);
        }

        //UI매니저에 UI 등록
        UIManager.Instance.RegisterController(UIKey, controller);
        gameObject.SetActive(false);
    }

    public void UIShow()
    {
        controller.OnShow();
    }
    
}

 

현재 로직만 구현 중이고 , 아직 Player 캐릭터와는 연결이 되어있지 않다. ItemStatus UI와 연결후 PlayerStat과 연동해서 값을 변경 해야 될것 같다. 

https://01149.tistory.com/148

 

TIL : 2024-12-09(월) :: 최종프로젝트 개발 일지(11) - Scene을 로드하는경우 파괴된 Object에 접근되는 상

Enemy 및 Projectile 을 ObjectPool Class에 넣어 관리를 하는데,Scene을 로드하는 경우 ObjectPollManager는 DontDestroy로 파괴되지 않아 데이터가 유지되지만Enemy Object들은 Scene이 로드되면서 파괴되기에 ObjectPoolM

01149.tistory.com

해당 관련된 글의 트러블 슈팅이다.

트러블 슈팅의 대처법으로 

 

[ 결론은 Scene 재로드시에 ObjectPool을 비우는 구조를 설계를 해야만 한다.  ] 

 

였기에 , 우선 호출시점을 오브젝트 풀링을 셋팅하기 전으로 설계를 해야되고 , 각 클래스별로 오브젝트 풀링세팅하는것을 한 곳으로 모아서 작업을 하였으며 , 경로 같은 고정 문자열은 따로 Const 라는 고정 상수,문자열을 저장하는 클래스를 따로 만들어 관리하기로 하였다.

 

이렇게 한 이유는 

첫번째 : 오브젝트 풀링 세팅은 한 Scene에서 세팅 후에 호출 및 반납만하기에 한 곳에서 해도 된다고 판단하였고

Scene 로드 할때 처음으로 오브젝트 풀을 비우면 중복 생성이나 파괴된 오브젝트에 접근하지 않을거라고 판단하였다.

두번째 : 경로는 보통 문자열로 이루어져있고 수정이 되지 않아야되고 접근이 가능해야되기에 Static Class로 생성 한뒤 readonly 기능을 이용하어 수정을 막았다.

 

GameSceneTrigger.cs

using System;
using UnityEngine;

public class GameSceneTigger : MonoBehaviour
{
    private void Start()
    {
        ObjectPoolManager.Instance.ObjectPoolAllClear();

        PlayerObjectPoolSetting();
        EnemyObjectPoolSetting();
        InventoryObjectPoolSetting();

        GameManager.Instance.StartGame();
    }

    private void InventoryObjectPoolSetting()
    {
    }

    private void PlayerObjectPoolSetting()
    {
        ObjectPool playerProjectilePool = new ObjectPool(Const.POOL_KEY_PLAYERPROJECTILE, Const.PLAYER_INITIAL_POOL_SIZE, Const.PLAYER_PROJECTILE_ENERGYBOLT_PATH);
        ObjectPoolManager.Instance.AddPool(Const.PLAYER_PROJECTILE_ENERGYBOLT_KEY, playerProjectilePool);
    }

    private void EnemyObjectPoolSetting()
    {
        ObjectPool goblinPool = new ObjectPool(5000, 60, Const.ENEMY_PREFEB_GOBLIN_PATH);
        ObjectPool goblinMagicianPool = new ObjectPool(5001, 60, "Prefabs/Enemy/GoblinMagician");

        ObjectPool slashPool = new ObjectPool(6000, 60, "Prefabs/Enemy/Effects/Slash");
        ObjectPool energyBoltPool = new ObjectPool(6001, 60, "Prefabs/Enemy/Effects/EnergyBolt");
        ObjectPool slashBossPool = new ObjectPool(6002, 60, "Prefabs/Enemy/Effects/SlashBoss");
        ObjectPool skillBoss1Pool = new ObjectPool(6003, 10, "Prefabs/Enemy/Effects/SkillBoss1");

        ObjectPool goblinBossPool = new ObjectPool(5500, 3, "Prefabs/Enemy/GoblinBoss");

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_POOL_KEY, goblinPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_POOL_KEY, goblinMagicianPool);

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, slashPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, energyBoltPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, slashBossPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, skillBoss1Pool);

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_BOSS_POOL_KEY, goblinBossPool);
    }
}

해당 코드의 기능은 GameScene을 로드시에 필요한 곳에서 ObjectPool을 세팅하는 Class입니다.

SceneManager의 SceneLoad를 사용해도 되지만, 세팅해야될 데이터가 많고 , 확장성을 대비해서 Class로 만든뒤 오브젝트에 컴포넌트로 적용시켜 사용 중입니다. 

Enemy 및 Projectile 을 ObjectPool Class에 넣어 관리를 하는데,

Scene을 로드하는 경우 ObjectPollManager는 DontDestroy로 파괴되지 않아 데이터가 유지되지만

Enemy Object들은 Scene이 로드되면서 파괴되기에 ObjectPoolManager에는 파괴된 Object만 가지고 있게 된다.

 

해당 트러블 슈팅을 겪고 있어 두가지 방법을 생각하고 있다.

1. Scene이 로드 될때마다 ObjectPool을 비우기 

이 방법을 사용하면 처리비용은 높지만 문제는 깔끔하게 처리가 될것이다. 

하지만 우리 프로젝트는 Stage마다 Scene을 재 로드 해서 사용하기로 했으나 , 이렇게되면

최적화에 좋지 않아 고민 중이다. 

 

2.Scene 로드 할때마다 ObjectPool을 비우지만 Stage 로드시에는 비우지 않기 

Scene로드 시에만 ObjectPool을 비우고 Stage 에서는 생성되어있는 ObjectPoll을 재활용 하여 다음 Stage로 넘어갈떄 비동기로 데이터 세팅을 하는싞으로 할지 고민 중이다.

 

결론은 Scene 재로드는 구현을 해야되기에 비우는 구조를 설계를 해야만 한다. 

해당 Stage 진행도를 나타내주는 UI

 

Stage 진행도를 나타내주는 기능은 아무래도 동적으로 값이 계속 변하다보니 MVC 패턴을 이용하여 UIManager에 등록하고 사용하는 방식으로 진행 하였다., 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem.XR;
using UnityEngine.UI;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBar : MonoBehaviour
{
    public string UIKey;

    private UIStageProgressBarModel model;
    [SerializeField]private UIStageProgressBarView view;
    private UIStageProgressBarController controller;

    private void Start()
    {
        model = new UIStageProgressBarModel();
        GameManager.Instance.StageProgressModel = model;
        controller = new UIStageProgressBarController();
        controller.Initialize(view, model);

        UIManager.Instance.RegisterController(UIKey, controller);
    }

}

해당 기능의 Model,View,Controller를 멤버변수로 가지고있어 초기화및 관리를 하고있다. 오브젝트에 컴포넌트로 적용하여 사용하는 클래스이다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using DG.Tweening;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBarView : MonoBehaviour, IUIBase
{
    [SerializeField] private RectTransform curStageProgress;

    public void Initialize()
    {
        if (curStageProgress == null)
        {
            curStageProgress = GetComponent<RectTransform>();
        }

        curStageProgress.localScale = new Vector3(0, 1, 1);

    }

    public void ShowUI()
    {
        //Boss 몬스터 등장 조건이 되지 않으면 UI 출력
        gameObject.SetActive(true);
    }

    public void HideUI()
    {
        //Boss 몬스터 등장 조건이 되면 사라지기
        Utils.StartFadeOut(this.GetComponent<CanvasGroup>(), Ease.OutBounce, 1.0f);
        gameObject.SetActive(false);
    }

    public void UpdateUI()
    {

    }

    public void UpdateUIProgree(float resultProgress)
    {
        curStageProgress.localScale = new Vector3(resultProgress, 1, 1);
    }
}

Controller로부터 데이터를 받아 Scene에 출력(업데이트)하는 클래스 영역이다. 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

[System.Serializable]
public class UIStageProgressBarModel : UIModel
{
    public event Action OnEventCurEnemyAddCount;
    private int curEnemySlayerCount; //처지한 Enemy 카운트
    private int bossTriggerEnemySlayerCount; //Boss가 등장에 필요한 쓰러트린 Enemy 카운트

    public int CurEnemySlayerCount { get => curEnemySlayerCount; private set => curEnemySlayerCount = value; }
    public int BossTriggerEnemySlayerCount { get => bossTriggerEnemySlayerCount; private set => bossTriggerEnemySlayerCount = value; }

    public void Initialize(int bossTriggerCount)
    {
        bossTriggerEnemySlayerCount = bossTriggerCount;
        curEnemySlayerCount = 0;
    }

    public void CurCountDataClear()
    {
        curEnemySlayerCount = 0;
    }

    public void AddCurEnemyCount(int slayEnemyCount)
    {
        curEnemySlayerCount += slayEnemyCount;
        Debug.Log($"현재 처치한 적의 갯수 : {curEnemySlayerCount} \" {bossTriggerEnemySlayerCount}");
        OnEventCurEnemyAddCount?.Invoke();
    }


}

Data(Enemy처치 갯수)를 관리하는 곳으로 적을 처치할때마다 EnemyCount가 올라간다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBarController : UIController
{
    private UIStageProgressBarModel stageProgressBarModel;
    private UIStageProgressBarView stageProgressBarView;

    public override void Initialize(IUIBase view, UIModel model)
    {
        base.Initialize(view, model);

        stageProgressBarModel = model as UIStageProgressBarModel;
        stageProgressBarView = view as UIStageProgressBarView;

        stageProgressBarModel.OnEventCurEnemyAddCount += UpdateView;
        stageProgressBarModel.OnEventCurEnemyAddCount += BossTriggerCheck;
    }

    public override void OnShow()
    {
        view.ShowUI();
        UpdateView();   // 초기 View 갱신
    }

    public override void OnHide()
    {
        view.HideUI();
    }

    public override void UpdateView()
    {
        float resultProgress = (stageProgressBarModel.CurEnemySlayerCount / (float)stageProgressBarModel.BossTriggerEnemySlayerCount);
        resultProgress = Mathf.Min(1,resultProgress);

        // Model 데이터를 기반으로 View 갱신
        //view.UpdateUI();
        stageProgressBarView.UpdateUIProgree(resultProgress);
    }

    public void BossTriggerCheck()
    {
        if (stageProgressBarModel.CurEnemySlayerCount >= stageProgressBarModel.BossTriggerEnemySlayerCount)
        {
            Debug.Log($"보스 등장 조건을 만족 합니다.");
            GameManager.Instance.isTryBoss = true;
            OnHide();
        }
    }
}

Controller로 Data값이 등록되면 해당 Controller에 알려주고 Controller는 해당 데이터가 들어옴에 따라 로직대로 동작하게된다. 해당 로직은 현재 잡은 Enemy수 / 목표 Enemy 처치수를 하여 백분율로 나타내고 해당 데이터를 이용하여 Boss 등장 조건을 체크하는 Class 이다. 

+ Recent posts