0417 - Untiy 부트캠프 [9일차]

Hyeon O·2025년 4월 17일

Unity_BootCamp 2주차

목록 보기
4/5

📚 오늘의 학습

오늘은 5주차 강의 알고리즘에 대해서 알아보고 정리를 진행한다.

알고리즘 정리는 좀 더 깊게 고민해야 하는 부분이 많아 오늘은 <알고리즘의 기본>만 작성하고,

이후 정리는 프로젝트 진행이 끝나고 시작할 생각이다.
[참고문서 : <쉽게 배우는 알고리즘>, <자바와 함께하는 자료구조의 이해>]

과제로 개인 프로젝트TextRPG의 필수 구현과 도전 구현을 시간이 되는 대로 진행할 것이며,

진행 과정과 트러블 슈팅을 위주로 정리해보았다.

정리 리마인드

  • 노션에 기본 개념 정리, 예제 코드 분석, Unity에서의 활용까지 정리
  • 벨로그에는 내가 약한 개념과 정리 중 의문을 정리
  • 프로젝트 과정 정리, 진행 중 만난 문제와 해결 정리

C# 문법 정리

아래 노션 링크에 배웠던 개념을 정리하였다.

C# 프로그래밍

아래는 금일 정리한 개념이다.

알고리즘 기본

추가로 개인 프로젝트 구현 과정을 아래 노션에 정리하였다.

TextRPG _ SpartaDungeon

💡 약점 정리 & 의문 정리 &문제 해결

TextRPG 프로젝트 도전과제 구현 과정을 정리할 생각이다.
이전 필수 기능에 대한 구현은 위 노션 링크에 정리해두었고,
소스파일 또한 업로드 해두었다.

도전 과제 구현

1. 아이템 정보를 클래스 / 구조체로 활용해보기

클래스로 이미 구현하였으니, 구조체로 만들어보자!

구조체에 대한 개념은 아래에 정리해 두었다

메소드와 구조체

 struct Item
 {
     public string Name { get; }
     public int Attack { get; }
     public int Defense { get; }
     public string Description { get; }
     public int Price { get; }

     public bool Purchased { get; set; }
     public bool Equipped { get; set; }

     public Item(string name, int atk, int def, string desc, int price)
     {
         Name = name;
         Attack = atk;
         Defense = def;
         Description = desc;
         Price = price;
     }

     public string StatString()
     {
         if (Attack > 0) return $"공격력 +{Attack}";
         if (Defense > 0) return $"방어력 +{Defense}";
         return string.Empty;
     }
 }

이렇게 작성했는데

이럴 수가..

바로 생성자에 오류가 발생했다.

품절 상태와 장착 상태에 대한 초기화가 없어 그런 것 같다.

생성자로 구현할 때는 필요 없더만 구조체로 하니 왜 이런 에러가???

→ 구조체는 “값이 완전히 채워져야만 유효하다” 는 C# 언어 설계 원칙 때문에 사용자 정의 생성자에서Purchased·Equipped 같은 필드를 빠뜨리면 CS0171 (모든 필드를 할당해야 한다) 컴파일 오류가 발생한다.

TIP!

  • 구조체에 읽기‑전용(init 또는 readonly) 자동 프로퍼티를 선언하면,
    기본값(0/false/null)으로 간주하므로 역시 반드시 할당해야 한다.
  • struct Item { public bool Equipped { get; init; } = false; } 처럼
    필드 선언 시 디폴트 값을 지정하면 생성자에서 따로 넣지 않아도 컴파일 OK

일단 생성자에 두 프로퍼티에 대한 초기화를 해주는 코드를 작성해서 해결했다.

근데 또 하나가 더 있었다.

CS0051 컴파일 에러는 뭘까…?

바로 마이크로소프트 홈페이지 문서로 찾아가보았다.

생략으로 인해 private가 되지 않도록 주의하라고…?

아! 위에서 보면 구조체 선언할 때 접근지정자를 생략해서 private로 되었다

구조체 선언에 public을 작성해주니 해결되었다.


이제 실행을 해보자

근데 바로 문제가 생겼다.

아이템을 장착해도 상태 변화가 적용이 안된다.

두 가지를 의심해 볼만 하다

  1. 구매 로직에서 문제가 생겼다
  2. 구조체 생성자에서 초기화 해버린다. -> 이건 아닌 것 같다.

일단 다시 개념부터 생각해보자.

클래스와 구조체는 참조와 값 복사라는 점에서 차이가 있다.

→ 장착/구매할 때 로직을 바꾸어야할 수도?

→ 구입한 아이템이 인벤토리로 가는 부분도 생각해봐야한다.

그리고 리스트를 사용할 때도 박싱 문제도 있다. 일단 이건 덮어두자

그럼 장착할 때 코드에서 값 복사라는 점에서 코드를 수정해보자.

참조 형으로 했던 클래스와 달리 구조체는 값 복사이므로

구매나 장착 후 수정본을 재 할당 해야 한다.

따라서

//상점에서
_buyer.Gold -= item.Price;
item.Purchased = true;
_items[choice - 1] = item; //추가된 코드
_buyer.Inventory.AddItem(item);
Console.WriteLine("구매를 완료했습니다.");

//장착할 때
 var item = _items[choice - 1];
 item.Equipped = !item.Equipped;
 _items[choice - 1] = item;        // 수정본을 리스트에 다시 저장
 Console.WriteLine(item.Equipped ? "장착했습니다." : "장착 해제했습니다.");

수정본을 리스트에 다시 저장해야 한다

이제 잘 돌아간다 🥲

2. 아이템 정보를 배열로 관리하기

배열로 관리하라는 말은 지금까지 리스트로 구현한 것들을 배열로 바꾸는 작업을 하면 되는 것 같다.

지금까지 리스트로 인벤토리에서의 아이템, 상점에서의 아이템을 관리하였다.

이걸 배열로 바꾼다면 먼저 리스트와 배열의 차이를 기반으로 설계 후 넘어가는 것이

위 구현할 때 겪었던 것처럼, 구조체로 바로 바꾸면서 오만가지 오류를 피할 수 있는 방법일 것 같다.

일단,

리스트와 달리 배열은 동적으로 용량을 관리할 수 없다

→ 상점과 인벤토리 슬롯 수를 고정 및 정의해야한다.

  • 상수로 배열 크기를 고정하고

→ 그러면 비어있는 값은? 이것도 신경써서 구현해야한다.

  • null값을 고려해서 Nullable Reference Types 기능을 이용하자.

또한 foreach문을 바꾸어야한다.

→이거야 뭐 for문 루프로 바꾸면 될 것 같다.

그리고 아이템 추가하는 메소드 additem()을 직접 구현해야한다

아…그리고 장착하는 메소드도 재구현해야한다. 배열은 실제 사용 범위를 관리하기 때문에 LINQ 메소드를 바로 쓸 수가 없다.

흠..리스트가 더 바람직한 구현인 것 같다.

아래는 배열로 재구현한 코드이다.
파일 링크(노션)

3. 아이템 추가 - 나만의 새로운 아이템을 추가

item에 새로 만들어주기만 하면 된다.

스파르타의 검을 만들어 주었다. 절대 위에 따라한거 아니다.

4. 휴식기능 추가

다음과 같은 기능을 추가해보자

자 설계를 해보자

휴식하기라는 기능이 생긴거니까

새로운 클래스에 새로운 메소드를 구현하면되고

메소드 호출 후 조건문을 통해 사용자 입력에 따라 2가지 행동을 구현하면될 것 같다.

  1. 휴식하기
    • 골드 차감 로직 및 체력 회복 로직 작성
      • 보유 금액이 충분하면 “휴식이 완료되었습니다” 출력 및 체력 회복
      • 부족하면 “골드가 부족합니다“출력
  2. 나가기

그리고 플레이어의 체력과 골드를 참조해야 된다.

그럼 윤곽이 잡혔으니 구현해보자

우선 Rest 클래스를 생성해서 휴식 기능을 구현하자.

public class Rest
{
    public readonly Character Player;

    private int expense = 500;

    public Rest(Character player)
    {
        Player = player;

    }

    public void rest()
    {

        while (true)
        {
            Console.Clear();
            Console.WriteLine("휴식하기\n");
            Console.WriteLine($"{expense}G 를 내면 체력을 회복할 수 있습니다.(보유골드 : {Player.Gold})");
            Console.WriteLine("");
            Console.WriteLine("1. 휴식하기");
            Console.WriteLine("0. 나가기");
            Console.WriteLine("\n원하시는 행동을 입력해 주세요 : ");

            string input = Console.ReadLine();
            if (input == "0")
            {
                break;
            }
            else if (input == "1")
            {
                if (Player.Gold >= expense)
                {
                    Console.WriteLine("휴식을 완료하였습니다.");
                    Player.Gold -= expense;
                    Player.Health = 100; //set 의 private 없애기
                }
                else Console.WriteLine("Gold가 부족합니다");
                
            }
            else
            {
                Console.WriteLine("잘못된 입력입니다.");
            }
            Game.Pause();
        }
    }

}

여기서 큰 문제는 없었지만

플레이어의 체력을 참조하는데 있어서

프로퍼티를 다음과 같이 구현했어서 수정했다.

public int Health { get; private set; } = 100; //private 삭제

이후 메인 루프에 휴식 기능을 추가한다.

이 때 휴식 클래스의 메소드를 호출하기 위해 참조하는 것 또한 놓치면 안된다

다음 코드를 메인에 추가한다.

 private Rest _rest;
 ....
 ...
 
  _rest = new Rest(_player);
  
  ...
    private void MainLoop()
  {
      while (true)
      {
          Console.Clear();
          Console.WriteLine("스파르타 마을에 오신 여러분 환영합니다.");
          Console.WriteLine("이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n");
          Console.WriteLine("1. 상태 보기");
          Console.WriteLine("2. 인벤토리");
          Console.WriteLine("3. 상점");
          Console.WriteLine("4. 휴식하기\n");//추가된 기능 출력
          Console.Write("원하시는 행동을 입력해주세요: ");
          string input = Console.ReadLine();
          switch (input)
          {
              case "1":
                  _player.ShowStatus();
                  break;
              case "2":
                  _player.InventoryMenu();
                  break;
              case "3":
                  _shop.ShopMenu();
                  break;
              case "4":
                  _rest.rest();//추가된 선택지
                  break;
              default:
                  Console.WriteLine("\n잘못된 입력입니다.");
                  Pause();
                  break;
          }
      }
  }

잘 실행된다. 🫠
노션 블록 링크 <- 소스파일 있음

5. 판매하기 기능 추가

oh….자 지금까지 했던 것처럼 설계부터 고민해보자

일단 보이는 것부터 생각해보자

판매하는 선택지를 만들고

판매하는 창을 호출하는 메소드를 구현하고

그 메소드에 구매하는 것처럼 번호를 입력받아 판매하고 골드를 받으면 될 것이다.

판매 가격은 기존 아이템 가격을 참조한 변수에 85/100을 곱하면 될 것이고 이 값을 다시 보유 골드에 넣으면 된다.

그리고 아이템 목록은 인벤토리에 보유 중인 아이템을 보여주어야 한다.

아 또 장착 중인 아이템을 판매하면 장착이 해제되니 이것도 분기를 생각해야한다.

일단 판매 메소드를 복붙해서 부분적으로 수정만 하면 될 것 같다.

if (int.TryParse(input, out int choice) && choice >= 1 && choice <= _items.Count)
{
    var item = _items[choice - 1];
		//필요 없는 부분은 주석처리
    //if (item.Purchased)
    //{
    //    Console.WriteLine("이미 구매한 아이템입니다.");
    //}
    //else if (_buyer.Gold >= item.Price)
    //{
        _buyer.Gold += item.Price * (85 / 100); //수정된 코드
        item.Purchased = false; //수정된 코드 : true -> false
    if (item.Equipped) //장착 중인 아이템이면
    {
        //장착 해체 및 리스트에 아이템 제거
       
    }
    else
    {
        //리스트에 아이템 제거
    }
        
    //}
    //else
    //{
        Console.WriteLine("Gold 가 부족합니다.");
    //}

조건문을 채우면 될 것 같은데..

리스트에 아이템을 제거해야 하니 새로운 메소드를 생성해야한다.

  public void AddItem(Item item) => _items.Add(item);

  //아이템 판매를 위한 아이템 제거 메소드
  public void DeleteItem(Item item) => _items.Remove(item);

또한 아이템 목록을 인벤토리에서 가져와야한다.

구현은 했는데 버그가 생겼다.

판매하는 아이템의 판매가격이 0으로 되버린다.

원인은 85%가격을 만드는 코드에 있었다.

C#에서 85/100은 정수 나눗셈으로 계산되어 0이 되기 때문이다.

위 코드에서

((float)item.Price)*(85/100) 을 바꿔주자.

for (int i = 0; i < _buyer.Inventory._items.Count; i++)
{
    var item = _items2[i];
    int sellPrice = (int)(item.Price * 0.85f);
    string equipTag = item.Equipped ? "[E]" : "";
    Console.WriteLine($"- {i + 1} {equipTag}{item.Name,-14} | {item.StatString()} | {item.Description} | {sellPrice}G");
}

Console.WriteLine("\n0. 나가기");
Console.Write("\n원하시는 행동을 입력해주세요: ");
string input = Console.ReadLine();
if (input == "0") return;

if (int.TryParse(input, out int choice) && choice >= 1 && choice <= _items2.Count)
{
    var item = _items2[choice - 1];

    int sellPrice = (int)(item.Price * (85f / 100f));
    if (item.Equipped) //장착 중인 아이템이면
    {
        //장착 해체 및 리스트에 아이템 제거
        item.Equipped = false;
     
        _buyer.Gold += sellPrice;
        _buyer.Inventory.DeleteItem(item);
        Console.WriteLine("판매를 완료했습니다.");
    }
    else
    {
        //리스트에 아이템 제거
        
        _buyer.Gold += sellPrice;
        _buyer.Inventory.DeleteItem(item);
        Console.WriteLine("판매를 완료했습니다.");
    }        
}
else
{
    Console.WriteLine("잘못된 입력입니다.");
}

이제 문제없다

다만 구입하고 다시 판매한 상품이 여전히 구매 완료로 표시되어 있는 문제가 발생했다.

판매한 상품은 다시 상점에서 구매가 가능한 상태로 만들어야 한다.

그러면 상점의 아이템 리스트를 참조하여 Purchased 값을 false 로 만들어 주면 된다.

구현 완료! 😵‍💫

노션 블록 링크

💭 오늘의 회고

  • 알고리즘 강의를 통해서 알고리즘 기본 개념에 대해서 복습할 수 있었다.

  • TextRPG 프로젝트 개발 과정에서, 크고 작은 문제를 만나면서 학습자의 관점이 아닌 개발자의 관점에서 이 문제를 어떻게 바라보는지에 대해 고민하면서 어떻게든 스스로의 힘으로 해결해보려고 노력했다.

  • 엉뚱한 삽질같은 고민과 접근법이 많았지만, 굳은 살이 배기고 어딜 파야하는이제 대한 인사이트가 조금씩 트이는 느낌이다.

  • 개발을 통해서 지식으로 기억한 문법들이 "내 것"으로 체화되는 것 같다. 마냥 기억하고 있는 문법은 다시 찾아보고 비교하면서 구현하지만, 내 것이 된 것들은 그냥 자동을 구현이 되고 문제가 생겨도 바로 핵심적인 문제를 잡아내는 것 같다.

  • 내일은 도전 과제( 장착 개선, 레벨업 기능, 던전 입장 기능, 게임 저장)을 구현하고 알고리즘 정리를 시작할 생각이다.

profile
천천히, 꾸준하게, 끝까지

0개의 댓글