MainScene UI 개발

작명이 마음에 들지 않아 Global에서 MainScene으로 바꿨다.

메인 씬의 UI구나 하는 느낌이 들 수 있게끔 말이다.

 

 

 

UI 연결하기

 

우선 각 메뉴의 Presenter, UIPresenter, UIView와 캔버스를 만들었다.

지금은 비어있으나 딱히 상관없다.

연결하는 것이 목표기 때문이다.

 

버튼을 누르면 해당 캔버스가 켜지거나 꺼지는 기능을 만들 것이다.

 

파이프라인이 복잡해지게 하고 싶진 않아서 MainSceneUIPresenter 클래스가 UIPresenter를 들고 있지 않게 할 것이다.

대신 MainScenePresenter를 통해 Show, Hide를 담은 콜백을 전달할 것이다.

 

    public class MainSceneUIPresenterAction
    {
        public Action OnEmployeeCanvasButtonClick { get; set; }
        public Action OnArrangementCanvasButtonClick { get; set; }
        public Action OnWorldmapCanvasButtonClick { get; set; }
        public Action OnBrandCanvasButtonClick { get; set; }
        public Action OnOfferAppCanvasButtonClick { get; set; }
    }

    private MainSceneUIPresenterAction action;

 

파라미터가 더러워질 것을 예상하여 Action을 담는 클래스를 만들었다.

 

    #region Public Method
    public void Initialize(MainSceneUIPresenterAction action)
    {
        view = GetComponent<MainSceneUIView>();

        this.action = action;
        AddListners();
    }
    #endregion

    #region Private Method
    private void AddListners()
    {
        view.EmployeeCanvasButton.onClick.AddListener(() => action.OnEmployeeCanvasButtonClick.Invoke());
        view.ArrangementCanvasButton.onClick.AddListener(() => action.OnArrangementCanvasButtonClick.Invoke());
        view.WorldmapCanvasButton.onClick.AddListener(() => action.OnWorldmapCanvasButtonClick.Invoke());
        view.BrandCanvasButton.onClick.AddListener(() => action.OnBrandCanvasButtonClick.Invoke());
        view.OfferAppCanvasButton.onClick.AddListener(() => action.OnOfferAppCanvasButtonClick.Invoke());
    }
    #endregion

 

이렇게 하면 코드가 이전보단 간결해진다.

 

    uIPresenter.Initialize(new MainSceneUIPresenter.MainSceneUIPresenterAction
    {
        OnEmployeeCanvasButtonClick = () =>
        {
            employeePresenter.ShowOrHide();
            arrangementPresenter.Hide();
            brandPresenter.Hide();
            offerAppPresenter.Hide();
        },
        OnArrangementCanvasButtonClick = () => 
        {
            arrangementPresenter.ShowOrHide();
            employeePresenter.Hide();
            brandPresenter.Hide();
            offerAppPresenter.Hide();
        },
        OnWorldmapCanvasButtonClick = () => 
        {
            employeePresenter.Hide();
            arrangementPresenter.Hide();
            brandPresenter.Hide();
            offerAppPresenter.Hide();
        },
        OnBrandCanvasButtonClick = () => 
        {
            brandPresenter.ShowOrHide();
            employeePresenter.Hide();
            arrangementPresenter.Hide();
            offerAppPresenter.Hide();
        },
        OnOfferAppCanvasButtonClick = () =>
        {
            offerAppPresenter.ShowOrHide();
            employeePresenter.Hide();
            arrangementPresenter.Hide();
            brandPresenter.Hide();
        }
    });

 

이건 MainScenePresenter의 uiPresenter 초기화 코드이다.

월드맵은 항상 최하단에 켜져 있을 예정이기 때문에 자신에 대한 부분은 없다.

물론 이 대로는 좀 문제가 있어 보인다.

자신을 제외한 클래스만 Hide 하면 좋을 텐데...

잠시 후 다시 수정하고 일단 테스트부터 해보자.

    public void ShowOrHide()
    {
        if (!uIPresenter.gameObject.activeSelf)
            Show();
        else
            Hide();
    }

    public void Show()
    {
        uIPresenter.Show();
    }

    public void Hide()
    {
        uIPresenter.Hide();
    }

 

이렇게 각 프레젠터 안에서 캔버스의 액티브 상태를 보고 켜거나 끈다.

따로 Show나 Hide로 접근할 수 있도록 메서드를 분리해 놓았다.

 

 

작동은 잘 된다.

그러나 지금은 코드 중복이 많고, 더럽다.

당장 뜯어고쳐야 하는 코드이다.

 

모든 프레젠터에 쇼, 하이드 기능이 들어가고 있고 전부 자신의 ui프레젠터를 켜고 끈다는 공통된 기능을 수행한다.

그래서 PresenterBase와 UIPresenterBase라는 부모 추상 클래스를 만들어 공통된 기능을 상속시킬 것이다.

이러면 프레젠터들을 하나의 속성으로 묶어 편하게 관리할 수도 있다!

 

public abstract class UIPresenterBase : MonoBehaviour
{
    abstract public void Show();

    abstract public void Hide();
}

 

public abstract class PresenterBase : MonoBehaviour
{
    [SerializeField] protected UIPresenterBase uIPresenter;

    public void ShowOrHide()
    {
        if (!uIPresenter.gameObject.activeSelf)
            Show();
        else
            Hide();
    }

    public void Show()
    {
        uIPresenter.Show();
    }

    public void Hide()
    {
        uIPresenter.Hide();
    }
}

 

 

        public void Initialize(List<Employee> employees, EmployeeListUIPresenter employeeListUI)
        {
            this.employees = employees;
            (uIPresenter as EmployeeUIPresenter)
                .Initialize(employeeListUI, (Employee employee) => LevelUp(employee), (Employee employee) => RankUp(employee));
        }

 

UIPresenterBase 타입으로 상속받고 있기 때문에 형변환을 전부 해주었다.

 

    private void Awake()
    {
        worldmapPresenter = GetComponent<WorldmapPresenter>();
        storePresenter = GetComponent<StorePresenter>();
        presenters[(int)Menu.employee] = GetComponent<EmployeePresenter>();
        presenters[(int)Menu.arrangement] = GetComponent<ArrangementPresenter>();
        presenters[(int)Menu.brand] = GetComponent<BrandPresenter>();
        presenters[(int)Menu.offerApp] = GetComponent<OfferAppPresenter>();
    }

 

그리고 MainScenePresenter를 수정했다.

일단 enum값으로 무슨 메뉴인지 분리해 주었으며, 다른 메뉴와 다르게 작동할 Worldmap, store는 따로 작성하였다.

 

    public void Initialize()
    {
        // test
        // 여기서 세이브 데이터를 불러올 예정이기 때문.
        List<Employee> employees = new List<Employee>();
        foreach (Character character in DataManager.instance.CharacterList)
        {
            employees.Add(new Employee(character));
        }
        List<Store> stores = new List<Store>();

        EmployeeListUIPresenter employeeListUIPresenter = Instantiate(employeeListUIPresenterPrefab);
        employeeListUIPresenter.Initialize(employees);
        worldmapPresenter.Initialize();
        storePresenter.Initialize(stores, employeeListUIPresenter);

        (presenters[(int)Menu.employee] as EmployeePresenter).Initialize(employees, employeeListUIPresenter);
        (presenters[(int)Menu.arrangement] as ArrangementPresenter).Initialize();
        (presenters[(int)Menu.brand] as BrandPresenter).Initialize();
        (presenters[(int)Menu.offerApp] as OfferAppPresenter).Initialize();

        uIPresenter.Initialize(new MainSceneUIPresenter.MainSceneUIPresenterAction
        {
            OnEmployeeCanvasButtonClick = () =>
            {
                (presenters[(int)Menu.employee] as EmployeePresenter).ShowOrHide();
                SetAction(Menu.employee);
            },
            OnArrangementCanvasButtonClick = () => 
            {
                (presenters[(int)Menu.arrangement] as ArrangementPresenter).ShowOrHide();
                SetAction(Menu.arrangement);
            },
            OnWorldmapCanvasButtonClick = () => 
            {
                SetAction(Menu.worldmap);
            },
            OnBrandCanvasButtonClick = () => 
            {
                (presenters[(int)Menu.brand] as BrandPresenter).ShowOrHide();
                SetAction(Menu.brand);
            },
            OnOfferAppCanvasButtonClick = () =>
            {
                (presenters[(int)Menu.offerApp] as OfferAppPresenter).ShowOrHide();
                SetAction(Menu.offerApp);
            }
        });

        void SetAction(Menu menu)
        {
            for (int i = 0; i < presenters.Length; i++)
            {
                if (i != (int)menu)
                    presenters[i].Hide();
            }
        }
    }

 

전체 초기화 구문이다.

 

이제 SetAction이라는 로컬 메서드를 통해 자신 이외의 메뉴들을 비활성화시킨다!

물론 월드맵은 자신을 온오프 하는 기능이 없고 말이다.

 

작동하는 것은 아까랑 똑같기 때문에 패스한다.

 

 

 

 

마치며

오늘의 회고라는 말은 맞지 않을 때도 있는 것 같아 제목을 바꿨다.

오늘은 메뉴를 이동하는 기능을 넣었다.

정확히는 구조를 단단히 하는 시간을 가졌지 않나 싶다.

위의 글자(돈, 명예)에 대한 부분은 나중에 돈을 버는 기능을 만들 때 다시 살펴볼 예정.

 

내일부터는 월드맵 파트로 넘어간다.

Store

public class Store
{
    #region Property
    public StoreSize Size => size;
    #endregion

    #region Variable
    private List<Employee> employees;
    private StoreSize size;
    #endregion

    #region Public Method
    public Store(StoreSize size)
    {
        employees = new List<Employee>(3);
        this.size = size;
    }

    public void SetEmployee(int idx, Employee employee)
    {
        employees[idx] = employee;
    }

    public void SetSize(StoreSize size)
    {
        this.size = size;
    }
    #endregion
}

 

우선 스토어 클래스를 만들었다.

전에 만들었던 UML 그대로 만들었다.

점포를 만들 때 크기를 선택할 수 있게 할 것이라 생성자로 size를 받게 하였다.

 

어찌 됐든 이 store라는 클래스는 employee 클래스와 비슷하게 정보를 저장하는 역할을 할 것이고 대부분의 작업을 할 store presenter로 넘어갔다.

 

 

 

 

 

직원 배치

    public void Initialize(List<Store> stores, EmployeeListUIPresenter employeeListUI)
    {
        this.stores = stores;
        uIPresenter.Initialize(employeeListUI
            , (int idx, Employee employee) => SetEmployee(idx, employee));
    }
    
    public void SetEmployee(int idx, Employee employee)
    {
        store.SetEmployee(idx, employee);
    }

 

store presenter도 employee presenter와 비슷하게 store 리스트를 가져온다.

그리고 ui 직원을 배치하고 콜백이 넘어오면 현재 보고 있는 store 정보를 가져와서 set 해주어야 하는데 여기서 문제가 발생했다.

게임 흐름 상 store는 월드맵에서 선택해 들어가야 하는데 월드맵 기능이 아직 없다는 것.

 

그래서 다시, 초기화 때 임의의 store를 하나 생성했다.

 

    public void SetEmployee(int idx, Employee employee)
    {
        store.SetEmployee(idx, employee);

        Debug.Log(store.Employees[idx].Character.Name);
    }

 

잘 들어가는지 확인하기 위해 Debug문 추가.

 

 

 

잘 된다.

 

 

 

일정 변경

 

아무래도 월드맵을 만드는 게 우선인 것 같아 순서를 좀 바꿨다.

그전에 모든 화면에서 보일 UI도 만들고 말이다.

 

 

사실 UI는 이미 만들어 놨고 이제 기능 개발을 해야 한다.

돈을 벌거나 썼을 때 UI를 바꿔주고, 하단 버튼을 누르면 각 캔버스가 켜지도록 말이다.

참고로 점포는 월드맵에서 불러오는 거라 따로 메뉴가 있진 않다.

 

오늘 그리 한 건 없지만 주말에 안 쉬었더니 너무 피곤해서 이제 내일 하도록 하겠다.

이번 주말부터는 할 거 없다고 개발 하지 말고 확실히 쉬어야겠다...

랭크 업

 

랭크 업에 들어가는 돈을 담은 테이블을 만들었다.

그리고 레벨 업과 마찬가지로 파싱 함수 만들고 UI 연동하고...

 

어찌 됐든 문제없이 작동하는 것을 확인했다.

이거 올리고 보니 레벨 업이라 잘못 써져 있는 게 보여서 부랴부랴 수정도 했다.

 

 

 

 

점포 UI 구상

이제 점포 UI 만들기로 넘어가야 하는데 막상 만드려고 보니까 점포는 배경을 그려야 하니 리소스 준비가 많이 필요할 뿐, UI는 딱히 뭘 할 게 없었다.

그냥 직원을 편성할 수 있는 시스템만 부여해 주면 끝이다.

 

이러한 숨기고 꺼낼 수 있는 패널을 만들고 각 슬롯을 터치하면 직원 목록이 나오고, 또 아이콘을 터치하면 직원이 배치되도록 하고 싶다.

여기에는 저번에 만들어 둔 EmployeeButton을 또 사용할 것이다.

 

 

 

 

UI 개발

문제가 좀 생겼다.

점포에서도 직원 목록을 쓰려면 또 EmployeePresenter에서 List를 가져오고 똑같은 메서드를 붙이고 해야 하는데 이는 번거로울뿐더러 객체지향 원칙을 어긋나는 거라 생각했다.

 

그래서 직원 목록이라는 캔버스를 따로 만들어 빼내 여러 곳에서 접근할 수 있도록 하고 불러오는 곳에 따라 다른 콜백을 적용하는 방향으로 바꿨다.

문제는 어떤 방식으로 구현할지였다.

 

당장 생각나는 게 두 가지 있었다.

하나는 싱글턴이고,

다른 하나는 모든 캔버스를 초기화할 때 씬 프레젠터에서 캐릭터 목록을 넣어 주는 것이다.

 

싱글턴은 최대한 지양하고 싶어서 후자를 선택했다.

 

public class EmployeeListUIPresenter : MonoBehaviour
{
    #region Property
    public Sprite[] StarSprites => view.StarSprites;
    #endregion
    
    #region Variable
    private EmployeeListUIView view;
    [SerializeField] private EmployeeButton employeeButtonPrefab;

    private Action<Employee> onClick;

    private List<EmployeeButton> employeeButtons;
    #endregion

    #region Public Method
    public void Initialize(List<Employee> employees)
    {
        view = gameObject.GetComponent<EmployeeListUIView>();
        CreateEmployeeButtons(employees);

        this.onClick = null;
        AddListeners();
    }

    public void Show()
    {
        view.Show(true);
    }

    public EmployeeListUIPresenter SetEmployeeListUI(Action<Employee> onClick)
    {
        this.onClick = onClick;
        return this;
    }

    #endregion

    #region Private Method
    private void AddListeners()
    {
        view.BackButton.onClick.AddListener(() => view.Show(false));
    }
    private void CreateEmployeeButtons(List<Employee> employees)
    {
        employeeButtons = new List<EmployeeButton>();
        foreach (Employee employee in employees)
        {
            CreateEmployeeButton(employee);
        }
    }

    private void CreateEmployeeButton(Employee employee)
    {
        employeeButtons.Add(Instantiate(employeeButtonPrefab, view.EmployeeListParent)
            .SetEmployeeButton(employee, view.StarSprites[(int)employee.Character.Grade], (Employee employee) =>
            {
                onClick.Invoke(employee);
            }));
    }
    #endregion
}

 

그렇게 새로 만든 직원 목록 UI 클래스이다.

콜백을 등록할 수 있는 Set, 불러올 수 있도록 Show 메서드를 공개로 설정했다.

그리고 별 스프라이트는 직원 상세정보를 보여주는 곳에 쓰고 있었어서 어쩔 수 없이 저렇게 프로퍼티로 빼놓았는데 나중에 좋은 개선책이 생각나면 수정할 것이다.

 

public class MainScenePresenter : MonoBehaviour
{
    private EmployeePresenter employeePresenter;

    [SerializeField] private EmployeeListUIPresenter employeeListUIPresenterPrefab;


    private void Awake()
    {
        employeePresenter = GetComponent<EmployeePresenter>();
    }

    void Start()
    {

    }

    public void Initialize()
    {
        // test
        // 여기서 세이브 데이터를 불러올 예정이기 때문.
        List<Employee> employees = new List<Employee>();
        foreach (Character character in DataManager.instance.CharacterList)
        {
            employees.Add(new Employee(character));
        }

        EmployeeListUIPresenter employeeListUIPresenter = Instantiate(employeeListUIPresenterPrefab);
        employeeListUIPresenter.Initialize(employees);
        employeePresenter.Initialize(employees, employeeListUIPresenter);
        
    }

 

새로 메인 씬 프레젠터를 만들어 줬고,

씬 클래스 -> 씬 프레젠터 클래스 -> 각 기능 프레젠터 -> 각 기능 UI 프레젠터 순으로 파이프라인이 생겼다.

그래서 직원 리스트도 여기서 넣어 주었다.

 

        public void Initialize(EmployeeListUIPresenter employeeListUI, Action<Employee> onClickSararyButton, Action<Employee> onClickRankUpButton)
        {
            view = GetComponent<EmployeeUIView>();
            this.onClickSararyButton = onClickSararyButton;
            this.onClickRankUpButton = onClickRankUpButton;

            this.employeeListUI = employeeListUI;
            employeeListUI.SetEmployeeListUI((Employee employee) =>
            {
                SetSelectedEmployeePanel(employee);
                AddSelectedEmployeePanelListeners(employee);
                view.ShowSelectedEmployeePanel(true);
            });

            view.Initialize();
        }

 

이제 직원UI 초기화 부분에서 직원 목록의 콜백을 달아준다.

훨씬 간결해진 것이다.

 

 

 

캔버스 정렬 문제

그러나 하나 문제가 있다.

별개의 캔버스를 사용하는 만큼 콜백이 들어와도 직원 목록에 가려 상세 정보를 볼 수 없게 된 것이다.

그래서 캔버스를 또 분리해야 하나 생각하다가, 어차피 직원 UI 프레젠터가 하는 일은 상세 정보를 보여주는 일뿐이니 그냥 직원 캔버스를 상세 정보 캔버스로 만들어 버렸다.

 

 

문제는 없는 듯 하다.

처음 상세 정보를 불러오는 게 조금 느린 것 같기도 한데 일단 넘어갔다.

나중에 최적화를 진행하며 다시 살펴보자.

 

 


다시 점포 UI로...

콜백 함수만 달아주면 끝나기 때문에 점포 UI 클래스는 매우 간단해졌다.

public class StoreUIPresenter : MonoBehaviour
{
    #region Variable
    private StoreUIView view;
    private EmployeeListUIPresenter employeeListUI;
    #endregion

    #region Life Cycle
    private void Awake()
    {
        
    }
    #endregion

    #region Public Method
    public void Initialize(EmployeeListUIPresenter employeeListUI)
    {
        view = GetComponent<StoreUIView>();

        this.employeeListUI = employeeListUI;
        AddListeners();
    }
    #endregion

    #region Private Method

    private void AddListeners()
    {
        view.OpenArrangementButton.onClick.AddListener(() =>
        {
            view.ShowArrangementPanel(true);
            view.SwitchButton(true);
        });
        view.CloseArrangementButton.onClick.AddListener(() =>
        {
            view.ShowArrangementPanel(false);
            view.SwitchButton(false);
        });

        int idx = 0;
        foreach (Button button in view.SlotButtons) 
        {
            button.onClick.AddListener(() => 
            {
                employeeListUI.SetEmployeeListUI((Employee employee) =>
                {
                    SetEmployee(button, employee, idx++);
                }).Show();
                
            });
        }
    }

    private void SetEmployee(Button button, Employee employee, int idx)
    {
        button.image.sprite = employee.Character.ProfileSprite;
        employeeListUI.Hide();
        view.SetEmployeeSprite(idx, employee.Character.StandingSprite);
    }
    #endregion

}

 

보면 알 수 있겠지만 직원 목록 UI에 Hide도 추가하였다.

 

 

 

저 빈 직원 이미지는 나중에 코난 범인 같은 스프라이트를 넣어 줄 생각이다.

 

오늘 구현한 것은 오직 UI 부분이기 때문에 이미지를 넣어줄 뿐 가게에 직원 정보를 넣어주지는 않는다.

그러니 내일은 store라는 클래스를 만들어 직원 정보를 넣어 주고, 시간이 되는 곳까지 전체 점포 목록에서 직원을 배치할 수 있는 기능을 개발할 예정이다.

단순하게 가자

 

오늘은 쉬려고 하니 할 일이 없어서 그냥 내일 할 작업을 조금 미리 해두려고 했다.

그러고 레벨 업 하는 부분을 개발하려고 UI부터 배치하니 너무 조잡해 보이는 것이었다.

유저들도 일일이 경험치를 계산하는 것을 귀찮아할 것이고 나도 굳이 이런 메커니즘으로 갈 필요는 없는 것 같아서

 

원 터치로 다음 레벨로 레벨업 하도록 UX를 수정하였다.

결정적으로 키우기 게임 레퍼런스로 사용하고 있는 세븐나이츠 키우기에서도 원 터치로 레벨 업 하는 걸 보니 이게 요즘 트렌드인 것 같았다.

전에 뱀서 라이크를 만들 때는 무조건 경험치를 얻어 레벨업 해야 했기 때문에 이번에도 같은 룰을 적용할 생각이었는데 생각해 보니 게임 장르에 따라 다른 룰을 도입하는 것은 당연한 것이다.

 

 

 

 

데이터 찾기

        public int FindEXP(Grade grade, Rank rank, Level level)
        {
            switch(grade)
            {
                case Grade.star1:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star1;
                case Grade.star2:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star2;
                case Grade.star3:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star3;
                case Grade.star4:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star4;
                case Grade.star5:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star5;
                case Grade.star6:
                    return EXPList.Find(o => o.Rank == rank && o.Level == level).Star6;
                default:
                    return 0;
            }
            
        }

 

현재 랭크, 레벨에 맞는 경험치를 간편하게 가져오기 위해 DataManager에 메서드를 추가했다.

수입 데이터를 가져오는 메서드도 이와 비슷하다.

좀 거추장스러워 보이는데 데이터테이블을 수정하느냐 이걸 그냥 두느냐의 문제인 것 같아서 일단 그대로 뒀다.

 

 

상세 정보 패널 세팅

private void SetSelectedEmployeePanel(Employee employee)
{
    view.SetSelectedEmployeePanel(view.StarSprites[(int)employee.Character.Grade], employee.Character.ProfileSprite
        , employee.Rank.ToString(), employee.Character.Name, ((int)employee.Limit).ToString(), employee.Character.Description
        , DataManager.instance.FindEXP(employee.Character.Grade, employee.Rank, employee.Level)
        );

    view.RankUpButton.gameObject.SetActive(false);
    view.SalaryButton.gameObject.SetActive(true);

    if (employee.Level == Level.master)
    {
        view.SalaryButton.gameObject.SetActive(false);
        switch (employee.Character.Grade)
        {
            case Grade.star1:
            case Grade.star2:
                break;
            case Grade.star3:
                if(employee.Rank == Rank.employee)
                    view.RankUpButton.gameObject.SetActive(true);
                break;
            case Grade.star4:
            case Grade.star5:
            case Grade.star6:
                if (employee.Rank == Rank.employee || employee.Rank == Rank.manager)
                    view.RankUpButton.gameObject.SetActive(true);
                break;
        }
    }
}

 

직원 레벨, 랭크, 등급에 따라 레벨 업 할 수 있는지, 랭크 업 할 수 있는지 판별하여 버튼을 활성화/비활성화시키는 로직도 추가하였다.

 

따라서 employee presenter에서는 판별 로직을 수행하지 않을 예정이다.

 

이 상세 정보 패널을 세팅해 주는 부분이 너무 복잡하다는 생각이 들기도 한다.

체크해 두고 나중에 리팩토링 할 때 다시 점검해 보기로 했다.

 

 

 

 

재산 매니저

 

번역기가 재산이 프로퍼티래서 프로퍼티로 클래스 이름을 지었는데...

뭔가 다른 이름이 있으면 좋을 것 같다...

 

 

public class Property
{
    public BigInteger Money {get; set;}
    public int Fame {get; set;}
}

public class PropertyManager: MonoBehaviour
{
    #region Variable
    public static PropertyManager instance;

    public Property Property;
    #endregion

    #region Life Cycle

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

    #endregion
    
}

 

어쨌든 이제 사람을 키우려면 돈이 필요하기 때문에 재산을 관리할 클래스를 만들었다.

일단 돈은 제한 없이 벌 수 있도록 BigInteger 변수로 만들었다.

int나 long에 비해 속도가 느리기 때문에 그다지 쓰고 싶지는 않는데 계산량이 얼마나 될지 아직 감이 안오기 때문에 이 또한 나중으로 미뤘다. (맨날 미룬다;)

 

 

 

 

 

결과 UI

 

돈이 부족해 레벨 업/랭크 업 실패 시 띄울 팝업과 성공 결과 패널도 만들었다.

 

 

 

 

레벨 업 로직

        private void LevelUp(Employee employee)
        {
            int needExp = DataManager.instance.FindEXP(employee.Character.Grade, employee.Rank, employee.Level);

            if(PropertyManager.instance.Property.Money >= needExp)
            {
                PropertyManager.instance.Property.Money -= needExp;
                employee.SetLevel(employee.Level + 1);
                uIPresenter.ShowLevelUpResultPanel(true, employee);

            }
            else
            {
                uIPresenter.ShowFailPanel(true);
            }
        }

 

레벨 업 버튼을 누르면 employee presenter로 콜백이 넘어간다는 것은 저번에 얘기했다.

매개 변수는 employee 정보를 받는 것으로 변경하였다.

다음 레벨로 가기 위한 EXP가 얼마인지 찾아서 돈이 그보다 많으면 레벨업 하고 다시 ui프레젠터로 넘어가 결과창을 띄우고, 아니면 실패 창을 띄우는 간단한 로직이다.

 

 

직원 정보를 전달하는 로직은 이전 상세 정보를 띄우는 로직과 별 다를 것 없다.

잘은 되는데 문제점을 발견했다.

랭크와 레벨을 한국어로 표시하고 싶은데 지금은 그냥 enum을 그대로 사용하고 있다는 뜻이다.

그리고 상세 정보도 그냥 랭크만 표시하고 있어서 레벨업을 했는지 안했는지 잘 보이지 않는다.

 

        public string RankToKorean(Rank rank)
        {
            switch (rank) 
            {
                case Rank.employee:
                    return "직원";
                case Rank.manager:
                    return "매니저";
                case Rank.owner:
                    return "점장";
                default:
                    return "정체불명";
            }
        }

        public string LevelToKorean(Level level)
        {
            switch (level)
            {
                case Level.newcomer:
                    return "신입";
                case Level.beginner:
                    return "초보";
                case Level.faithful:
                    return "성실한";
                case Level.expert:
                    return "숙련된";
                case Level.excellent:
                    return "우수한";
                case Level.elite:
                    return "엘리트";
                case Level.master:
                    return "마스터";
                default:
                    return "정체불명";
            }
        }

 

그래서 데이터 매니저에 한글로 변환 해주는 메서드를 추가했다.

 

 

 

 

오늘의 회고

오늘은 원래 쉬는 날이었기 때문에 적당히 한 것 같다.

랭크 업 하는 부분도 오늘과 별 다를 건 없는데 랭크 업에 드는 급여 테이블을 또 만들 필요가 있어서 내일로 미뤘다.

많이 걸리지 않을 것이기 때문에 예정인 점포 UI 개발과 함께 해야겠다.

캐릭터 데이터 테이블 만들기

세계관, 스토리가 중요한 게임이 아니기 때문에 개성 강하고 코믹한 캐릭터를 만들고 싶었다.

그래서 콘셉트가 확고한 캐릭터들을 25명 구상하니 이거 만드는 데에만 오랜 시간이 걸렸다.

 

프로필 스프라이트, 스탠딩 스프라이트는 리소스 폴더 내의 주소로 적었다.

ID는 등급 별로 1000씩 차이를 줬는데 이게 문제가 되는 일은 없었으면 좋겠다.

 

 

 

폰트 추가

 

원활한 테스트를 위해 한글이 되는 폰트를 다운로드하였다.

고도체라는 상업적 이용이 가능한 무료 폰트를 사용하였다.

 

 

 

파싱

public struct Character
{
    public string Name;
    public Sprite ProfileSprite;
    public Sprite StandingSprite;
    public Grade Grade;
    public string Description;
    public int ID;

    public Character(string Name, Sprite ProfileSprite, Sprite StandingSprite, Grade Grade, string Description, int ID)
    {
        this.Name = Name;
        this.ProfileSprite = ProfileSprite;
        this.StandingSprite = StandingSprite;
        this.Grade = Grade;
        this.Description = Description;
        this.ID = ID;
    }
}

 

우선 이전의 Character 클래스를 없애고 구조체를 생성하였다.

 

public List<Character> ParseCharacterTable(DataTable table)
{
    List<Character> result = new List<Character>();

    foreach (DataRow row in table.Rows)
    {
        string name = row["Name"].ToString();
        Sprite profileSprite = Resources.Load<Sprite>(row["ProfileSprite"].ToString());
        Sprite standingSprite = Resources.Load<Sprite>(row["StandingSprite"].ToString());
        Grade grade = (Grade)Enum.Parse(typeof(Grade), row["Grade"].ToString());
        string description = row["Description"].ToString();
        int id = int.Parse(row["ID"].ToString());

        Character character = new Character(name, profileSprite, standingSprite, grade, description, id);
        result.Add(character);
    }

    return result;
}

 

파싱 함수도 새로 추가하였다.

 

        private void CreateEmployeesForTest()
        {
            foreach (Character character in DataManager.instance.CharacterList)
            {
                employees.Add(new Employee(character));
            }
        }

 

마지막으로 EmployeePresenter에 임의로 꽂아 넣었던 Character 대신에 데이터매니저를 사용하였다.

 

 

 

Config 씬 추가

여기서 초기화 사이클이 꼬이는 문제가 좀 발생했다.

데이터매니저, 직원프레젠터 둘 다 start에서 초기화를 하였더니 데이터를 로드하기 전에 캐릭터를 가져오려고 시도한 것이다.

 

이는 나중에 데이터 로드하는 로직을 따로 만들어야 문제가 없을 것인데, 우선 메인 씬 시작 전에 컨피그 신을 실행 하도록 하고 거기에 매니저 클래스들을 들이부었다.

 

어쨌든, 데이터 로드에는 시간이 걸리기 때문에 이후 로딩 로직을 만드는 것은 필수불가결하긴 하다.

그리고 지금은 Resources 폴더에서 로드하고 있으나 최대한 이 방식을 사용하고 싶지 않다.

이전에 회사에서 Addressable를 활용해 AWS S3 서버를 이용했었는데 무료로 할 수 있는 방법이 없을까 찾아봐야겠다.

보유 직원 목록 content

 

보유 직원 아이콘에 필요한 요소들을 추가하였다.

이름, 등급, 프로필 이미지라는 최소한의 정보만 들어갈 예정이다.

임의로 소환하여 사용해야 하기 때문에 프리팹으로 만들었다.

 

다른 부분은 Character 객체에서 가져와 그대로 넣어주면 되지만 등급을 나타내는 별은 Enum값으로 되어있기 때문에 매치되는 별 이미지를 넣어야 했다.

 

 

 하는 김에 별 이미지도 직접 그렸다.

적용하면 이런 느낌인데 나쁘지 않은 것 같다.

아님 말고.

 

    public class EmployeeButton : MonoBehaviour
    {
        #region Property

        #endregion

        #region Variable
        [SerializeField] private Button button;
        [SerializeField] private Image profileImage;
        [SerializeField] private Image starImage;
        [SerializeField] private TextMeshProUGUI nameTMP;

        #endregion

        #region Public Method

        public EmployeeButton SetEmployeeButton(Employee employee, Sprite starSprite, Action<Employee> onClick) 
        {
            profileImage.sprite = employee.Character.ProfileSprite;
            nameTMP.text = employee.Character.Name;
            starImage.sprite = starSprite;

            button.onClick.AddListener(() => onClick.Invoke(employee));

            return this;
        }

        #endregion

        #region Private Method

        #endregion
    }

 

이 버튼의 역할은 자신이 가리키는 직원이 누구인지만 알려주면 되기 때문에 간단하다.

캐릭터의 등급은 원래 enum 값으로 기록하고 있기 때문에 starSprite라는 매개변수를 따로 받았다.

이 sprite들은 EmployeeUIView가 가지고 있을 예정이다.

 

 

 

보유 직원 목록 불러오기&직원 버튼 누르면 상세 정보 띄우기

 

또 그림부터 그렸다.

앞으로 프로토타입을 완성할 때까지 테스트용으로 쓰일 기본 직원이다.

물론 실제로 사용할 스프라이트로 만들 때는 명암도 넣고 스탠딩 애니메이션도 만들어야 할 텐데... 일단 생각하지 말자.

 

우선, 원래대로라면 따로 json파일로 저장된 직원 목록을 불러와 리스트를 작성하는 것이 올바른 로직이다.

그러나 지금은 필요한 건 UI 테스트이기 때문에 일단 임의로 직원을 생성하겠다.

 

[CreateAssetMenu(fileName = "Character Data", menuName = "Scriptable Object/Character Data")]
public class Character : ScriptableObject
{
    public string Name;
    public Sprite ProfileSprite;
    public Sprite StandingSprite;
    public Grade Grade;
    public string Description;
    public int ID;
}

 

우선 캐릭터는 ScriptableObject로 관리한다.

이것도 나중에는 CSV파일에서 정보를 불러오도록 수정할 생각이다.

 

public class Employee
{
    public Character Character { get; private set; }
    public Rank Rank { get; private set; }
    public Level Level { get; private set; }
    public Limit Limit { get; private set; }
    public float Proficiency { get; private set; }
    public float EXP { get; private set; }

    public Employee(Character character)
    {
        Character = character;
        Rank = Rank.employee;
        Level = Level.newcomer;
        Limit = Limit.none;
        Proficiency = 0;
        EXP = 0;
    }

    public void SetEmployee(Character character)
    {
        Character = character;
        Rank = Rank.employee;
        Level = Level.newcomer;
        Limit = Limit.none;
        Proficiency = 0;
        EXP = 0;
    }

    public void SetEXP(float exp)
    {
        EXP = exp;
    }

    public void SetLevel(Level level)
    {
        Level = level;
    }

    public void SetRank(Rank rank)
    {
        Rank = rank;
    }

    public void SetLimit(Limit limit)
    {
        Limit = limit;
    }

    public void SetProficiency(float proficiency)
    {
        Proficiency = proficiency;
    }
}

 

계속 등장할 Employee라는 객체는 Character를 참조하고 있다.

이 또한 데이터를 저장할 용도일 뿐이기 때문에 C# 클래스로 작성하였다.

 

public void Initialize()
{
    employees = new List<Employee>();
    CreateEmployeesForTest();
    uIPresenter.Initialize(employees, (Employee employee) => Grow(employee));
}

#endregion

#region private Method
private void CreateEmployeesForTest()
{
    foreach (Character character in characters)
    {
        employees.Add(new Employee(character));
    }
}

 

이제 구현을 위한 EmployeePresenter의 주요 부분을 살펴보겠다.

함수 이름을 보면 알 수 있듯, 테스트를 위해 임의로 Employee를 생성하여 리스트에 집어넣었다.

uiPresenter를 초기화시키고 있는데 UI에서 급여 버튼을 눌렸을 때 해당 직원 정보를 가져와 연산을 할 예정이기 때문에 콜백 함수를 등록하였다.

 

        public void Initialize(List<Employee> employees, Action<Employee> onClickSararyButton)
        {
            view.Initialize();

            this.onClickSararyButton = onClickSararyButton;
            CreateEmployeeButtons(employees);

            AddListeners();
        }

        private void CreateEmployeeButtons(List<Employee> employees)
        {
            employeeButtons = new List<EmployeeButton>();
            foreach (Employee employee in employees) 
            {
                CreateEmployeeButton(employee);
            }
        }

        private void CreateEmployeeButton(Employee employee)
        {
            employeeButtons.Add(Instantiate(employeeButton, view.EmployeeListParent)
                .SetEmployeeButton(employee, view.StarSprites[(int)employee.Character.Grade], (Employee employee) => 
                {
                    SetSelectedEmployeePanel(employee);
                    AddSelectedEmployeePanelListeners(employee);
                    view.ShowSelectedEmployeePanel(true);
                }));
        }

        private void SetSelectedEmployeePanel(Employee employee)
        {
            // EXP 부분은 따로 남은 EXP를 구하는 수식을 만들 예정.
            // 직원 성장 기능을 추가할 때 변경하겠음.
            view.SetSelectedEmployeePanel(view.StarSprites[(int)employee.Character.Grade], employee.Character.ProfileSprite, employee.Rank.ToString(), employee.Character.Name
                , ((int)employee.Limit).ToString(), (employee.EXP).ToString(), employee.Character.Description);
        }

 

이번에는 EmployeeUIPresenter의 주요 부분이다.

초기화에서 받아온 Employee 리스트로 버튼 목록을 만들고 있다. (CreateEmployeeButtons, CreateEmployeeButton)

그리고 EmployeeButton에 콜백을 등록하여 선택한 employee 정보들로 세부 정보 화면을 구성하도록 하였다.

 

 

 

 

 

오늘의 회고

오늘은 평소보다 빨리 끝내서 시간이 많이 남는다.

그렇다고 개발을 더 하기보다는 그림을 좀 그려서 리소스를 준비하고 오래간만에 플스를 켜야겠다 ㅎㅎ

그리고 대신 내일 일정을 추가하여 캐릭터를 scriptable object가 아닌 csv에서 가져오는 방식으로 바꿀 것이다.

캐릭터 테이블 만들기는 덤.

 

데이터 테이블 만들기

 

어제 만든 enum에 key를 매칭하여 찾을 수 있게끔 경험치 테이블과 수입 테이블을 만들었다.

경험치는 이전 레벨의 두 배로, 수입은 무리 함수를 사용하여 만들었기 때문에 굳이 테이블을 만들지 않고 공식을 적용해도 되지만 이후 밸런스 조정이 편하도록 테이블로 작성하였다.

 

경험치 테이블과 수입 테이블을 보며 적절한 밸런스를 맞춰서 적절한 테이블을 만든다는 게 정말 쉽지 않았다.

솔직히 아직도 부족함이 많을 거라 생각하지만 3시간 동안 저 테이블을 붙잡고 있었으니 오늘은 이만하면 된 것 같다.

 

비어있는 부분들은 int 파싱에서 오류가 나기 때문에 파싱 하는 부분에서 예외처리 해주거나 0으로 채워주어야 한다.

나는 그냥 0으로 채웠으니 접근되지 않아야 하는 값에 접근하기 전에 힘들게 예외처리를 해야 될 예정이다.

 

데이터 테이블 파싱

우선 파싱 된 데이터를 담을 구조체를 만들었다.

public struct EXP
{
    public Rank Rank;
    public Level Level;
    public int Star1;
    public int Star2;
    public int Star3;
    public int Star4;
    public int Star5;
    public int Star6;

    public EXP(Rank Rank, Level Level, int Star1, int Star2, int Star3, int Star4, int Star5, int Star6)
    {
        this.Rank = Rank;
        this.Level = Level;
        this.Star1 = Star1;
        this.Star2 = Star2;
        this.Star3 = Star3;
        this.Star4 = Star4;
        this.Star5 = Star5;
        this.Star6 = Star6;
    }
}

public struct InCome
{
    public Rank Rank;
    public Level Level;
    public float Star1;
    public float Star2;
    public float Star3;
    public float Star4;
    public float Star5;
    public float Star6;

    public InCome(Rank Rank, Level Level, float Star1, float Star2, float Star3, float Star4, float Star5, float Star6)
    {
        this.Rank = Rank;
        this.Level = Level;
        this.Star1 = Star1;
        this.Star2 = Star2;
        this.Star3 = Star3;
        this.Star4 = Star4;
        this.Star5 = Star5;
        this.Star6 = Star6;
    }
}

 

그리고 CSV 파일을 가져와서 파싱 하는 기능을 가진 CSVParser 클래스와 파싱 된 데이터들을 관리할 DataManager 클래스를 만들었다.

 

파싱 하는 부분은 아래 블로그를 참고했다.

https://jinsso.tistory.com/8

 

[유니티/C#] DataTable의 모든 행 읽어오기

원래는 간단한 테이블만 사용해서, 코드도 간단하게 만들어서 사용하고 있었다. 그런데 게임 기획이 바뀌고 데이터를 저장해야할 일이 많아져서, 기존에 사용하던 코드로 작업을 진행했다가는

jinsso.tistory.com

 

public class CSVparser
{
    public DataTable CSVReader(string path)
    {
        DataTable table = new DataTable();

        StreamReader sr = new StreamReader(path);
        string[] headers = sr.ReadLine().Split(',');
        foreach (string line in headers)
        {
            table.Columns.Add(line);
        }

        while (!sr.EndOfStream)
        {
            string line = sr.ReadLine();
            string[] data = line.Split(',');

            DataRow row = table.NewRow();
            for (int i = 0; i < headers.Length; i++)
            {
                row[i] = data[i];
            }

            table.Rows.Add(row);
        }

        return table;
    }

    public List<EXP> ParseEXPTable(DataTable table)
    {
        List<EXP> result = new List<EXP>();

        foreach(DataRow row in table.Rows) 
        {
            Rank rank = (Rank)Enum.Parse(typeof(Rank), row["Rank"].ToString());
            Level level = (Level)Enum.Parse(typeof(Level), row["Level"].ToString());

            int star1 = int.Parse(row["Star1"].ToString());
            int star2 = int.Parse(row["Star2"].ToString());
            int star3 = int.Parse(row["Star3"].ToString());
            int star4 = int.Parse(row["Star4"].ToString());
            int star5 = int.Parse(row["Star5"].ToString());
            int star6 = int.Parse(row["Star6"].ToString());

            EXP exp = new EXP(rank, level, star1, star2, star3, star4, star5, star6);
            result.Add(exp);
        }

        return result;
    }

    public List<InCome> ParseInComeTable(DataTable table)
    {
        List<InCome> result = new List<InCome>();

        foreach (DataRow row in table.Rows)
        {
            Rank rank = (Rank)Enum.Parse(typeof(Rank), row["Rank"].ToString());
            Level level = (Level)Enum.Parse(typeof(Level), row["Level"].ToString());

            float star1 = float.Parse(row["Star1"].ToString());
            float star2 = float.Parse(row["Star2"].ToString());
            float star3 = float.Parse(row["Star3"].ToString());
            float star4 = float.Parse(row["Star4"].ToString());
            float star5 = float.Parse(row["Star5"].ToString());
            float star6 = float.Parse(row["Star6"].ToString());

            InCome inCome = new InCome(rank, level, star1, star2, star3, star4, star5, star6);
            result.Add(inCome);
        }

        return result;
    }
}
public class DataManager : MonoBehaviour
{
    public static DataManager instance;

    public List<EXP> EXPList;
    public List<InCome> InComeList;

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

    private void Start()
    {
        CSVparser csvparser = new CSVparser();
        EXPList = csvparser.ParseEXPTable(csvparser.CSVReader(Application.streamingAssetsPath + @"\exp_table.csv"));
        InComeList = csvparser.ParseInComeTable(csvparser.CSVReader(Application.streamingAssetsPath + @"\income_table.csv"));

        // test
        Debug.Log(InComeList.Find(o => o.Rank == Rank.manager && o.Level == Level.expert).Star5);
    }
}

 

테스트 결과 23.78으로 제대로 작동했다.

저 Linq 함수를 매번 쓰기 귀찮을 것 같기 때문에 DataManager에 값을 찾는 함수를 따로 파두고 싶으나... 오늘 아직 할 일이 많기 때문에 내일 직원 성장 기능을 만들 때로 미뤘다.

 

 

 

 

UI 만들기

 

간단하게 박스 UI를 만들었다.

보유 직원 목록인 그리드 뷰, 직원을 클릭했을 때 나올 상세 화면이다.

 

그리고 UI에 기능을 넣으려고 하니 할 일이 한두 개가 아니라는 걸 깨달았다.

아무래도 UI에 드는 시간을 포함하여 일정을 다시 산정해야 되겠다...

 

 

오늘의 회고

원래 계획한 분량을 다 지키지 못했으나 예상한 일이다.

개발에 본격적으로 들어가니 계획에서 놓친 부분이 계속 보였다.

오늘 일정이던 한계 초월은 뽑기 기능을 만들 때 넣는 것으로 바꾸었다.

 

 

UI 개발 부분을 쭉 넣고 나니 꽤 길어졌다.

필요하겠다 싶은 부분도 중간중간 추가했다.

확고해진 방향성

저번에 키우기 게임으로 방향을 확실히 잡았더니 기획이 간편해졌다.

지도 확장, 그리고 직원 수집과 육성 두 가지 키워드에 맞추어 머리를 열심히 굴리니 얼추 필요한 부분들의 메커니즘이 완성된 것 같다.

그리고 UML으로 주요 클래스들의 참조 관계도를 그려봤다.

아마도 개발 도중 많은 추가와 변경이 생기는 것을 막을 순 없겠지만 최대한 줄여보기 위해 힘썼다.

 

참 단순한 것 같지만 내가 제일 중독됐었던 게임인 "cell to sinqularity"라는 게임을 생각해 보면 사람을 사로잡을 수 있는 방법이 복잡한 메커니즘뿐은 아닐 거라 생각한다.

 

어찌 됐든 조미료는 프로토타입이 나온 뒤 치도록 하고 구상 단계는 이쯤에서 멈추기로 하였다.

 

 

 

계획을 세우자

이대로 개발을 시작하려다 여전히 체계가 부족하다는 느낌이 들더라.

큰 틀은 잡았지만 클래스 하나하나를 어떻게 짤 것인지에 대해 고민이 많아졌다.

 

(난 우선 레벨업 기능부터 넣으려고 했는데 그거 하나 하는 데 생각해 보니 UI를 다루는 클래스를 또 생성해야 하고 그럼 그리드 뷰에서 눌린 직원 객체를 EmployeePresenter로 보내야 할 것이고... 역으로 이 UI를 초기화할 때는 EmployeePresenter가 보유 직원 리스트를 보내줘야 하겠구나 뭐 그런... )

 

그래서 일단 언제 어디까지 구현할 것인지에 대해 일정을 잡고 그 날 구현할 부분에 대해 브레인스토밍 하는 시간을 갖고 개발에 들어가는 게 좋겠구나 싶었다.

 

 

일단 프로토타입을 만드는 것을 목표로 했다.

너무 빡빡하게 잡지 않았나 생각이 들지만 아마 어차피 얼마 안 가 수정해야 할 것이다.

내가 빼먹은 기능이 있을테니까.

 

 

계획을 세우면서도 UML이 계속 바뀌는 것을 보면 분명 그럴 것이다.

 

 

 

오늘의 회고

그림 그리는 거 어떡하지.

일단 이게 제일 큰 고민이다.

 

코딩 한 부분을 전혀 올리지 않았으나 안한 건 아니고, UML을 클래스에 그대로 옮겨넣는 부분 정도만 작성했다.

 

그리고 레벨, 랭크, 등급, 한계초월 같은 부분을 int로도 할 수 있겠으나 오류 방지와 직관성을 위해 enum으로 따로 만들어 놨는데 이게 과연 괜찮을지 고민이다.

namespace Enums
{
    public enum Grade
    {
        none,

        star1,
        star2,
        star3,
        star4,
        star5,
        star6,
    }

    public enum Level
    {
        none,

        newcomer,
        beginner,
        faithful,
        expert,
        excellent,
        elite,
        master,
    }

    public enum Rank
    {
        none,

        employee,
        manager,
        owner,
    }

    public enum Limit
    {
        none,

        limit1,
        limit2,
        limit3,
        limit4,
        limit5,
    }
}

 

뭔가 좋은 참고 자료가 없을까?

 

그리고 Character 클래스를 스크립터블 오브젝트로 만들었는데, 나중에는 csv 파일을 이용해 정보를 가져오는 것으로 변경하고 싶다.

+ Recent posts