현재 UI 구조에 문제가 있었다.

MVC 구조를 이용하는 UI 객체들이 [UI + 이름]클래스 에서 MVC구조에 맞게 초기화를 하여 작업을 하고있다.

하지만 이 초기화 할때 각 클래스별로 "하드코딩"으로 작업이 진행되고 있었다.

이렇게 되면 문제가 발생하게되는데: 해당 구조를 모르는 작업자가 해당 작업 진행시 구조를 모르기에 작업 효율이 떨어지고 가독성이 떨어지는 문제가 발생하였다. 

 

그래서 두가지 방법이 있다.

Controller 부분에서 초기화 작업을 진행 ( Controller 파트는 이미 UIController 라는 상위 클래스로 상속받고 있는상태)

하지만 Controller에는 Monobehaviour가 상속 되어있지 않고 이미 작업을 해온게 있어 다 갈아 엎기는 문제가 있는 상태였다.

 

두번쨰 방법으로 현재 초기화 하는 클래스의 상위 클래스를 만들어 상속을 시켜주는 방식을 사용하는것이다. 

 

using UnityEngine;

//Model : 해당 UI에 관련된 데이터(모델)
//View : 데이터를 유저의 눈에 보여주는 출력물(View)
//Controller : Model과 View를 중재하는 관리자

[System.Serializable]
public abstract class UIBase<M,V,C> : MonoBehaviour
    where M : UIModel, new()
    where V : IUIBase
    where C : UIController, new()
{ 
    protected M model;
    [SerializeField] protected V[] views;
    protected C controller;

    public virtual void Start()
    {
        //Model(Data) 초기화
        //model = new M();

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

        //UI매니저에 UI 등록
        UIManager.Instance.RegisterController(typeof(C).ToString(), controller);    
    }
}

 

해당 코드가 UI MVC구조를 초기화 해주는 코드이고 ,Modeldms 각각 필요한 데이터가 다르기도 하고 호출 시점도 조금씩 다르기  Model 영역은 자식클래스에서 초기화를 담당하고 있다. 

 

public class PlayerInfo : UIBase<PlayerInfoModel,PlayerInfoView, PlayerInfoController>
{
    public override void Start()
    {
        model = new PlayerInfoModel();
        base.Start();

        gameObject.SetActive(false);
    }
}

사용 예시로 , 플레이어 정보창 UI 초기화를 담당하는 클래스이다. 

우리 프로젝트에서 적용해야 될점
1.가챠 연출씬 상승 시켜야됨 -> 다른 가챠겜 연출 참고할것

 

Soul키우기 피드백

1.배경 툰쉐이더 적용 시켜볼것 ->3D Object에 툰 쉐이더
2.GameSceneTrigger에 각종 매니저들이 누락될 가능성이 높다.
SceneManager.sceneLoaded 이벤트 함수를 사용하거나 중계자(Init을 관리하는 해당 함수들)를 사용해서 만들어놓을것
ㄴ 즉, 게임이 재시작되거나 , 다음 Stage로 넘어갈때 메서드에 모아놓은뒤 메서드만 호출하면 다 동작하게끔 만들어 놓을것
3.외부 데이터를 Index(순서) 및 Key값을 사용하여 참조 할 수 있게 설계
ㄴData를 지역변수로 들고있는건 상관없다,
ㄴ 서버가 없어서 초기 런타임시 Json으로 데이터를 파싱해서 가지고 있는게 맞음
ㄴ 서버가 생기면 구조가 바뀔수 있다고 인지는 하고 있어야한다 -> 면접관련
ㄴ 서버가 있다면 : Dict key값 , 주소를 써서 서버에 호출하여 데이터를 받아와야된다. ,
ㄴ DB 컨테이너로 통째로 호출해서 사용하지 말것
4.Skill 쿨타임 에서 List가 아닌 Queue (선입선출) 자료구조 추천
5.ShowUI나 ObjectPool에서 string Key값이 아닌 제네릭을 사용할것을 추천
ㄴ string으로 사용시에 아무래도 문자열 누락 가능성인 높기에 제네릭으로 예방할것
6.MVC 패턴의 UIInventory에서 Model과 Controller의 역할을 Controller가 전부 책임을 맡아도 된다고 함
ㄴ 인벤토리 뿐만 아닌 모든 UI 구조가 현재 UI + 이름 에서 MVC 세 Class를 초기화 하는데 이렇게 하게 되면 문제가 있음
문제 : 해당 구조를 모르는 작업자가 해당 작업 진행시 구조를 모르기에 누락될 가능성이 있음
대처 1 : Controller에서 Model과 View를 초기화 하는 방법 ->하지만 이방법은 현재 Controller가 Monobehaviour가 없기에 사용 불가
대처 2 : UI + 이름 Class에 부모클래스를 만들어 상속 시켜 사용하게 할것 ,
대처 2의 방법으로 리팩토링 진행 예정
7.ScottGaland라고 BigIntegeer 외부라이브러리를 사용하였는데 ReadMe에 참조를 해주고 작성할것
8.로딩씬에서 코루틴 내부 안에 Async를 사용해서 , 코루틴 안에 비동기(Async) 메서드가??(코루틴은 비동기가 아니다!!)
 구조가 이상함 ->로딩씬 리팩토링 진행 예정

 

다른 팀 발표 피드백


1.구조적인 복잡도가 올라가도 괜찮다 -> 너무 신경쓰지 마라
2.역참조를 최대한 지양해라
3.이중 반복문(N^2) 사용해도 된다. 하지만 시돗횟수가 3000회 이상 올라가게 된 경우, 다른 알고리즘을 사용해보아야한다.
4.UI Code에서 데이터 가져오는 행위 => MVC 위반
4-1.OpenUI 실행하기전에 UI를 Update하고 Poen해야된다.
4-2.Data가 필요하면 인자값으로 넣는식으로 사용
4-3.Data가 필요하면 넣는 식으로 사용하는것이 좋음
5.Code는 엄청 세분화 되지 않아도 된다.
5-1.이유 : 면접관이 보기가 힘듬 + 개발 속도가 떨어짐 -> 스크립트의 크기가 커지기전에 나누는 행위 -> 좋지 않다.
5-2. 스크립트가 커지면 나누는것을 지향
6.장착 관련 : ItemManager에서 장착까지 관리하여도 좋다., 따로 Equip 매니저를 안ㅁ들어도 된다는 뜻
7.Find 쓰지마라
8.Skill 같은 경우 범위나 이펙트를 동적 생성해야함 , 결국 일반공격도 결국 Skill로 활용 가능
현재 무기에 데미지가 적용되어 있으면 결국 하드코딩이 되게 된다.
9.폴더링 신경써라
10.데이터 매니저를 제네릭을 사용해서 너무 일반화 하였다 -> 데이터들은 각각 고유의 값과 형식ㅇ ㅣ많기에
일반화(제네릭)을 하지않는것이 좋다. -> DB에 각각 사용 할수있게 Class내에서 변환해서 사용하는 방식이 좋을것같다.
11.리소스 용량이 큰 경우 , 아틀라스를 참고하여 사용 할것

오늘은 2024-12-23(월) 중간발표를 할 자료를 만들기로 하였고 

팀원들에게 각자 자기가 작업하였던 파트를 문서화 하라고 지시한 뒤

오전에는 영상작업 진행을 하였다.

 

그러나 영상 녹화를 하면서 생각밖으로 치명적인 버그가 있었는데

치명적인 버그로 3스테이지 보스의 스킬로 사용하는 투사체가 오브젝트 풀로 관리를하는데 Destroy()메서드로 파괴되는 로직이 있어서, 해당 작업을 진행한 팀원에게 버그 수정을 요청하였다.

 

PPT 및 영상 자료를 제작한 뒤 내일은 모의면접 준비 및 코드 리팩토링 + 폴더링의 시간을 가질것 같다.

영상 자료의 일부

 

 

MVP 이전 중간점검(튜터님 지도)

1.MVP 이전 폴더링 및 코드 가독성 올릴것

2.Boss 등장시 연출 추가할것

3.Player가 죽을경우 , 가식성을 올릴것 

 

MVP 이후 진행 예정 (약 2주)

1.Player의 인벤토리, Soul(Skill) 시스템을 Player와 연동하여 UserData로 저장/불러오기 사용하게 할것

2.Boss Try 기능 추가

3.Stage 추가(숲 맵)

4.Player 무기 교체 이미지 추가

5.Player 계정 레벨 및 계정 경험치 구현

6.추가 컨텐츠 기획하기 -> 보스 러쉬 및 요일던전 등등


 Boss 등장시 연출 컷씬 추가 

임시 테스트로 적용 한상태 , 나중에 MVP 이후 교체 예정

 

UI이기에 코드는 MVP 패턴에 UI매니저를 추가하였고 , All In 1 Sprite 에셋을 사용하여 쉽게 이펙트를 적용 시켰다


 

 Unity Mask 적용

 

마스크로 쓸것을 부모 객체로 ,마스크를 적용시킬 이미지(Target)을 자식객체로 선언한뒤

부모 객체는 Mask 컴포넌트를 적용시키면 정상적으로 동작하게된다. 

원 프레임에 들어가게되는 버튼

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 클래스 안에 멤버변수로 되어있으니 참조값을 제대로 활용 할 수 있게 되었다. 

+ Recent posts