내일배움캠프 Unity 16일차 TIL - 팀 조랑말 - 상점 씬 구현(2)

Wooooo·2023년 11월 20일
0

내일배움캠프Unity

목록 보기
18/94

오늘의 키워드

어제 상점 씬의 구매 기능만 구현 했으니, 오늘은 판매 기능을 포함하여 상점 씬의 기능 구현을 끝냈다.
구현 중에 고민했던 것들, 고쳐본 것들, 앞으로 추가하면 좋은 것들을 정리해보려한다.


판매 기능

1. 구매 모드/판매 모드

판매 기능을 추가하기 앞서, 어제 내가 구현한 상점 씬은 상점의 구매 화면이다.
씬에는 한 화면만 그려져야하니까, 상점 씬을 구매 화면/판매 화면 둘로 나눠야 할 필요가 있었다.
따라서, 상점 씬에 토글용 변수를 하나 두고 좌우 방향키를 이용해서 구매 모드와 판매 모드를 오갈 수 있는 구조로 설계해봤다.

2. 모드에 따른 선택지 스왑

현재는 Scene에서 Options라는 Action의 리스트로 선택지들을 관리하고 있다.
그리고 나는 이 선택지들을 활용해서 아이템 구매 기능을 만들었었다.

            // 구매 리스트 선택지 생성
            buyModeOptions.Clear();
            for (int i = 0; i < shopSaleItem.Count; i++)
            {
                var buyItem = shopSaleItem[i].DeepCopy();
                buyModeOptions.Add(new("", "", () =>
                {
                    if (Game.Player.Inventory.HasSameItem(buyItem) && !buyItem.StackCount.HasValue)
                    {
                        msg = "보유 중인 아이템입니다.";
                        return;
                    }
                    if (Game.Player.Gold < buyItem.Price)
                    {
                        msg = "골드가 부족합니다.";
                        return;
                    }
                    Game.Player.Inventory.Add(buyItem.DeepCopy());
                    Game.Player.ChangeGold(-buyItem.Price);
                    msg = $"{buyItem.Name}을/를 샀습니다.";
                }));
            }

Options에 상점에서 구매할 수 있는 아이템의 목록을 넣어서 구매 기능을 구현했는데,
판매 모드에서 사용할 Options가 또 필요해졌다.

상점에 모드에 따라 사용할 buyModeOptionssaleModeOptions를 각각 만들어두고,
상점의 모드를 정하는 토글 변수에 따라 Scene에서 상속받은 Options가 상황에 맞는 선택지 리스트를 참조하도록 했다.

            // 판매 리스트 선택지 생성
            saleModeOptions.Clear();
            for (int i = 0; i < playerSaleItem.Count; i++)
            {
                var saleItem = playerSaleItem[i];
                saleModeOptions.Add(new("", "", () =>
                {
                    if (saleItem.StackCount > 1)
                        saleItem.StackCount--;
                    else
                    {
                        Game.Player.Inventory.Remove(saleItem);
                        UpdateSaleListOptions();
                    }
                    Game.Player.ChangeGold((int)(saleItem.Price * 0.85));
                    msg = $"{saleItem.Name}을/를 팔았습니다.";
                }));
            }
            // 구매/판매 모드 토글
            do
            {
                if (shopModeToggle)
                    Options = buyModeOptions;
                else
                    Options = saleModeOptions;
                DrawScene();
            }
            while (ManageInput());

3. 버그해결, 최적화

판매 기능 구현 중 겪었던 버그와 해결 방법을 적어본다.

아이템 무한 판매

첫 테스트에서, 아이템을 판매하면 실제로 인벤토리에서 아이템은 사라지지만, 씬 화면에 동기화된 리스트를 업데이트해주지 않아서 이미 판매된 아이템을 무한정 팔 수 있었다.

이 버그는 아이템 판매 -> 판매 리스트 업데이트 -> DrawScene()를 한 과정으로 묶어주면서 해결할 수 있었다.

하지만 문제는 여기서 그치지 않았다 ...

DrawScene() 최적화, 코드 리팩토링

소모품과 같은 경우엔 반복해서 사거나 팔게 되는 경우가 잦은데, 계속해서 씬 자체를 새로 그리면 화면이 깜빡거리거나, 버튼을 꾹 누르고 있는 경우에는 화면을 그리는 것보다 실행 간격이 짧아져서 화면이 아예 나가버린다...

체력 포션 판매를 광클하다가 꾹 누르는 장면

따라서, 필요한 부분만 다시 그려내고, 다시 그려낼 필요가 없는 부분은 지워지지 않게 Draw call을 최적화 할 필요성이 생겼다.

현재 씬의 테두리 부분과 최상단의 타이틀 부분은 보더라는 명칭으로 그려지고 있는데, 이 부분은 다시 그려질 필요가 없으므로 씬에 처음 입장했을 때 한 번만 실행되는 EnterScene()으로 옮겨줬다.

그리고 필요한 부분만 다시 그리기 위해 Renderer 클래스에 지정한 부분만 지울 수 있는 ClearLine() 메서드를 작성했다.

        /// <summary>
        /// 선택한 줄의 그려진 메시지를 지웁니다. DrawBorder()로 그려진 테두리는 지워지지 않습니다.
        /// </summary>
        /// <param name="line">지울 줄의 번호입니다.</param>
        public static void ClearLine(int line) => Print(line, "".PadLeft(width - 3, ' '));

또한, 예외처리 메시지 출력이 여기저기 중구난방으로 흩어져 있어서 아이템 구매할 때, 아이템 판매할 때, 돈이 부족할 때, 이미 가진 아이템일 때, 온갖 경우 온갖 메서드에서 Renderer.Print()를 호출하고 있었는데, 이것도 별로 보기 좋지 않았다.

최적화를 하는 김에 리팩토링도 같이 진행하기로 했다. 그리는 부분은 DrawScene()에서만 진행될 수 있게!!

먼저, 예외 처리나 구매/판매 완료 메시지 등 사용자 조작에 의한 안내 메시지를 기억하기 위한 msg라는 변수를 씬에 선언해주고, 각 처리마다 필요한 메시지를 msg에 담기만 해줬다. 메시지의 출력은 나중에 DrawScene()에서 적당한 위치에 진행하게 해줄 것이다.

마지막으로, DrawScene()에서 처리해야할 일들을 찬찬히 살펴보자.

  • 다시 그리기 위한 부분을 지운다.
  • 다시 그려야 하는 부분을 그린다. (빨간 네모들)
    • 상황에 따라 다르게 그려야 하는 부분이 있다. (2번 네모)
      • 구매 모드의 구매 가능한 아이템 리스트
      • 판매 모드의 판매 가능한 아이템 리스트
    • 리스트 테이블의 길이에 따라 다른 위치에 그려져야 하는 부분이 있다.
      • 사용자 조작에 의한 안내 메시지 (3번 네모)

위의 로직에 맞게 DrawScene()을 리팩토링 해봤다.

        protected override void DrawScene()
        {
            // Clear Table...
            int row = 4;
            for (int i = 0; i < 20; i++)
                Renderer.ClearLine(row + i);

            // Draw Scene...
            row = Renderer.Print(row, "아이템을 구매하거나 판매할 수 있습니다.");
            row = Renderer.Print(row, $"[아이템 {(shopModeToggle ? "구매" : "판매")}]");
            row = Renderer.Print(row, $"현재 골드 : {Game.Player.Gold:#,##0} G");
            if (shopModeToggle)
                row = Renderer.DrawItemList(++row, shopSaleItem, buyModeFormatters, selectionIdx);
            else
                row = Renderer.DrawItemList(++row, playerSaleItem, saleModeFormatters, selectionIdx);
            Renderer.Print(row + 1, msg);
            Renderer.PrintKeyGuide("[방향키 ↑ ↓ : 선택지 이동] [방향키 ← → : 구매/판매 모드 변경] [Enter : 선택] [ESC : 뒤로가기]");
        }

이제 DrawScene()을 제외하면, 다른 어떤 메서드에서도 씬에 아무것도 그려내지 않는다.
즉, 씬에 그려지는 뭔가를 바꾸고 싶다면 DrawScene() 메서드만 보면 된다는거다!!

최적화 후 깜빡임이 없는 모습

이제 깜빡임이 거의 없다. 만족스럽게 동작하는 것 같다.


4. 앞으로 추가하면 좋을 것들

현재 아이템의 스탯을 보여주기 위해 아이템의 설명은 테이블에 노출하지 않고 있다.
장비의 경우는 스탯이 중요하고 소모품의 경우는 효과의 설명이 중요해서 유저가 아이템의 효과를 파악하기 위한 column이 서로 다른 상황이다.
이 부분을 나중에 장비는 스탯, 소모품은 설명을 테이블에 노출하도록 개선하는 방법도 좋을 것 같다.


마치며

오늘은 상점 씬 기능 구현의 대략적인 마무리와 씬 드로우 최적화 작업을 진행했는데, 씬 드로우 최적화는 아직 적용이 안된 다른 씬이 있어서, 다른 씬의 씬 드로우도 최적화하기로 했다.

profile
game developer

0개의 댓글