0418 - Unity 부트캠프 [10일차]

Hyeon O·2025년 4월 18일

Unity_BootCamp 2주차

목록 보기
5/5

📚 오늘의 학습

  • TextRPG 프로젝트 완성하기
    • 도전 기능 구현하기
      1. 중복 장착 방지 기능
      2. 레벨 시스템
      3. 던전 입장 및 클리어
      4. 게임 저장하기
  • 정렬 알고리즘 정리

[알고리즘 참고 문서] <쉽게 배우는 알고리즘, 한빛미디어>


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

C# 문법 정리

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

C# 프로그래밍

아래는 금일 정리한 개념이다.
정렬 알고리즘

프로젝트 모든 진행 과정 기록은 너무 길어 Notion에 따로 빼두었다.

TextRPG _ SpartaDungeon

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

도전 기능을 구현해보자. 이전까지 구현 코드 구조는 다음과 같다.

1. 장착 개선

각 타입별로 하나의 아이템만 장착 가능하다는 것은

무기와 방어구의 종류를 식별하고

코드에서 단 하나의 Equippedtrue가 되어야 한다고 생각할 수 있다.

왜냐하면 item클래스의 속성으로 Equipped를 불리언 값으로 구현했기 때문이다.

로직을 생각해보자

장착 중인 아이템이 없는 상태에서부터 시작해보자

  1. 아이템을 구입 후 인벤토리에 가서 아이템을 장착
    1. Shop 리스트의 item 속성이Equipped가 true로 변하고
    2. Inventory 리스트에 아이템이 추가
    3. 그리고 Inventry 리스트에 아이템 속성 Equipped가 true로 변함
  2. 인벤토리에 같은 타입의 아이템 장착 시
    1. 아이템 속성에 타입을 알려주는 속성을 하나 만들고
    2. 그 속성이 같다면
      1. Equipped가 true인 장비는 false로
      2. Equipped가 fasle인 장비는 true로
    3. 다르다면
      1. Equipped가 true로

어느 정도 윤곽이 잡힌 것 같다. 내 생각대로라면

  1. 아이템 클래스에 아이템 타입을 명시하는 속성을 추가하고
  2. Shop 클래스에 아이템마다 타입을 명시하고
  3. 인벤토리 클래스에서 조건문에 위 로직을 추가하면 해결될 듯 하다.
private void ManageEquip()
{
    while (true)
    {
        Console.Clear();
        Console.WriteLine("인벤토리 - 장착 관리");
        Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.\n");
        Console.WriteLine("[아이템 목록]");

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

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

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

            Console.WriteLine(item.Equipped ? "장착했습니다." : "장착 해제했습니다.");
        }
        else
        {
            Console.WriteLine("잘못된 입력입니다.");
        }
        Game.Pause();
    }
}

위 코드는 중복 장착 방지 기능이 구현되지 않은 기존 코드이다.

if (int.TryParse(input, out int choice) && choice .……
이 조건문을 수정하면 될 것 같다.

입력을 받고

번호에 맞는 아이템을 불러와서

Equipped 값을 바꾸어 주는 흐름이다

중복 장착 방지 로직을 추가하면

입력을 받고

번호에 맞는 아이템을 불러오는데

장착 중인 장비를 찾고 그 장비의 타입이 == 지금 장착하려는 장비 타입 이라면 장착 중인 장비 해제, 새로운 장비 장착

다르다면 바로 장착한다.

if (int.TryParse(input, out int choice) && choice >= 1 && choice <= _items.Count)
{
    var item = _items[choice - 1];
    if (!item.Equipped)
    {
        // 1) 같은 타입의 기존 장착 해제
        var equippedSameType = _items
            .FirstOrDefault(x => x.Type == item.Type && x.Equipped);
        if (equippedSameType != null)
        {
            equippedSameType.Equipped = false;
            Console.WriteLine($"{equippedSameType.Name}의 장착을 해제했습니다.");
        }

        // 2) 선택 장비 장착
        item.Equipped = true;
        Console.WriteLine($"{item.Name}을(를) 장착했습니다.");
    }
    else
    {
        // 이미 장착된 아이템을 다시 선택하면 해제
        item.Equipped = false;
        Console.WriteLine($"{item.Name}의 장착을 해제했습니다.");
    }
}

LINQ 의 FirstOrDefault() 를 사용하였는데

주어진 조건에 첫 번쨰로 일치하는 요소를 반환한다.

그리고 람다식 x => x.Type == selected.Type && x.Equipped 을 통해 같은 타입인지 비교한다.

참일 경우 conflict 변수에 장착 중인 장비 객체를 반환하고

거짓일 경우 conflict 는 null이 된다.(코드 흐름 상 null 되는 것은 문제가 없다…아직은?)

구현 완료! 😆

아 로직 구현하는데 집중해서 뒤로 돌아가는 기능이 빠졌다…다음 기능 추가할 때 수정하겠다.

2. 레벨 업 기능 추가

오호 던전을 클리어 할수록?

이 설계는 조금 다양한 방식으로 생각되는데 한번 잘 설계해보자.

일단 구현된 것부터 생각해보자.

레벨은 캐릭터 클래스에서 속성으로 관리하고 있다.

그러면 레벨을 관리하는 클래스를 만들어서

캐릭터 레벨 속성을 참조하고

이후에 던전 기능(던전클래스)을 구현할 것 까지 생각하면

던전 클래스에 예를 들어 현재 던전 레벨 속성이 있다면

이 속성을 가져와서 레벨 업을 진행시키고

레벨 당 추가 능력치를 캐릭터 클래스의 속성을 참조해서 갱신하면 될 것 같다.

여기까지 생각해보았을 때,

던전 클래스에 대한 설계도 같이 진행해봐야 할 것 같다는 결론이 나온다.

캐릭터 매니저 - 레벨 매니저 - 던전 매니저 이런 느낌으로

던전 클래스에 대한 설계를 무시하고 하면…

캐릭터 클래스에 현재 던전 레벨 속성을 만들어 주어도 될 수도 있겠다.

그렇게 되면 이후에 던전 입장 기능 구현에서 이 속성을 참조하면 될 것 같기도 하다.

이후에 어떤 확장을 진행하는지에 따라 이 선택이 중요할 것 같다.

일단, 캐릭터 속성에 던전 클리어 횟수를 만들어서 진행해보자!

이 횟수에 따라 레벨이 증가하고

레벨이 증가함에 따라 기본 능력치가 증가하도록 구현하면 될 것 같다.

정리하면

  1. 캐릭터 속성에 던전 클리어 횟수 추가

  2. 레벨 매니저 클래스 생성
    a. 던전 클리어 횟수에 따라 레벨 증가시키기
    b. 레벨에 따라 기본 능력치 증가시키기

    이제 구현해보자.

 public class Character
 {
     public string Name { get; }
     public string Job { get; }
     public int Level { get; private set; } = 1;
     public double BaseAttack { get; private set; } = 10;
     public double BaseDefense { get; private set; } = 5;
     public int Health { get; set; } = 100;
     public int Gold { get; set; } = 1500;
			
			//추가된 코드 : 던전 클리어 횟수와 레벨매니저 참조
     public int DungeonClears { get;  set; } = 0;
     public LevelManager LevelManager { get; }
     
     
     public Inventory Inventory { get; }

     public Character(string name, string job)
     {
         Name = name;
         Job = job;
         Inventory = new Inventory(this);
         LevelManager = new LevelManager(this);
     }
     ....
     //추가된 코드 : 던전클리어 횟수 증가 메소드와 능력치 상승 메소드
     public void AddDungeonClear() => DungeonClears++;
		 public void IncreaseStats()
		{
		    BaseAttack += 0.5;
		    BaseDefense += 1.0;
		}
		....
			//추가된 클래스 : 레벨 관련 로직 처리
		 public class LevelManager
    {
        private readonly Character _character;

        public LevelManager(Character character) => _character = character;

        public void RegisterDungeonClear()
        {
            _character.AddDungeonClear();
            Console.WriteLine("던전 클리어 횟수가 1 증가했습니다.");
						
						//던전 클리어 횟수가 레벨보다 높을 때
            while (_character.DungeonClears >= _character.Level)
            {
		            //레벨 업 로직 진행
                _character.DungeonClears -= _character.Level; //클리어 횟수 초기화
                _character.IncreaseStats();
                // 레벨 프로퍼티 직접 호출로 설정
                _character.GetType().GetProperty("Level").SetValue(_character, _character.Level + 1);
                Console.WriteLine($"레벨업! 새로운 레벨: {_character.Level}");
                Console.WriteLine("기본 공격력 +0.5, 기본 방어력 +1.0");
            }
        }
    }

잘 실행된다 🤪

3. 던전 입장 기능 추가

던전입장 클래스를 만드는 것은 당연한 것 같고

그 외에는 이제 조건분기랑 클리어 시 보상 로직을 차근차근 구현하면 될 것 같다.

위에는 없지만 체력 0이 될 경우의 분기도 만들어 주면 좋을 듯하다.

일단 명시된 조건만 구현을 해보자

위에 레벨이랑 유기적으로 구현해야 하니,

조금 생각을 정리하면

5번 입력을 통해 메인 루프에서 던전 입장 : 던전입장메소드 호출

→ 던전에 입장하면 던전메뉴가 나오고 3가지 던전에 대한 선택을 입력 받고

→ 각 던전은 쉬움, 일반, 어려움으로 나뉘며

→ 권장 방어력을 설정해야하니 던전클래스에 변수로 만들어 두고

→ 조건문을 통해 입력 받은 던전 난이도에 따라 로직을 구성하자.

→ 각 로직에는 보상에 대한 로직도 구현하면 될 것 같고

→ 던전 클리어 시 text를 clear 하고 클리어 메뉴가 출력하면 될 것 같다.

그러면 던전 클래스에 메소드 2개(입장과 클리어)를 만들고

클리어시 LevelManager의 RegisterDungeonClear() 를 호출하면 될 것 같다.

추가로 위에 구현 조건은 없지만,

체력이 0이 되면 던전 입장을 방지하기 위해 막는 로직을 추가 구현하였다.

 public class Game
 {
     private Character _player;
     private Shop _shop;
     private Rest _rest;
     private Dungeon _dungeon; //추가된 코드 : 던전 입장 메소드를 참조하기 위해
     ...
     private void Init()
		{
    Console.WriteLine("스파르타 던전에 오신 여러분 환영합니다!");
    Console.Write("이름을 입력해주세요: ");
    string name = Console.ReadLine()?.Trim();

    Console.WriteLine("원하는 직업을 선택하세요: 1. 전사  2. 도적");
    string job = Console.ReadLine() == "2" ? "도적" : "전사";

    _player = new Character(name, job);
    _shop = new Shop(_player);
    _rest = new Rest(_player);
    _dungeon = new Dungeon(_player); //추가된 코드
		}
     
     while (true)
            {
                Console.Clear();
                Console.WriteLine("스파르타 마을에 오신 여러분 환영합니다.");
                Console.WriteLine("이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n");
                Console.WriteLine("1. 상태 보기");
                Console.WriteLine("2. 인벤토리");
                Console.WriteLine("3. 상점");
                Console.WriteLine("4. 휴식하기");
                Console.WriteLine("5. 던전 입장\n");
                Console.Write("원하시는 행동을 입력해주세요: ");
                switch (Console.ReadLine())
                {
                    case "1": _player.ShowStatus(); break;
                    case "2": _player.InventoryMenu(); break;
                    case "3": _shop.ShopMenu(); break;
                    case "4": _rest.RestAction(); break;
                    case "5": _dungeon.Enter(); break;//추가된 코드
                    default:
                        Console.WriteLine("잘못된 입력입니다.");
                        Pause();
                        break;
                }
            }
            
          
      .....
public class Dungeon
{
    private readonly Character _player;
    private readonly Random _rand = new Random();
    private readonly (string Name, float RecDef, float BaseGold)[] _levels = new[]
    {
        ("쉬운 던전", 5f, 1000f),
        ("일반 던전",11f,1700f),
        ("어려운 던전",17f,2500f)
    };

    public Dungeon(Character player) => _player = player;

    public void Enter()
    {
        while (true)
        {
            Console.Clear();
            Console.WriteLine("던전 입장\n");

            if (_player.Health <= 0f)
            {
                Console.WriteLine("체력이 0이 되어 더 이상 던전을 진행할 수 없습니다.");
                Game.Pause();
                return;
            }
            else
            {
                for (int i = 0; i < _levels.Length; i++)
                {
                    var lv = _levels[i];
                    Console.WriteLine($"{i + 1}. {lv.Name} | 방어력 {lv.RecDef} 이상 권장");
                }
                Console.WriteLine("0. 나가기\n");
                Console.Write("선택: ");
                if (!int.TryParse(Console.ReadLine(), out int choice) || choice < 0 || choice > _levels.Length)
                {
                    Console.WriteLine("잘못된 입력입니다."); Game.Pause(); continue;
                }
                if (choice == 0) return;
                RunLevel(choice - 1);
                return;
            }
        }
            
    }

    private void RunLevel(int idx)
    {
        var (name, recDef, baseGold) = _levels[idx];
        Console.Clear();
        float playerDef = _player.BaseDefense + _player.Inventory.EquippedDefenseBonus();
        bool isOk = playerDef >= recDef;
        float diff = recDef - playerDef;

        float min = Math.Max(0f, 20f + diff); //최소 대미지, 0보다 이하는 없음
        float max = 35f + diff; // 최대 대미지 : 35 + 방어력차이
        int dmgMin = (int)min; 
        int dmgMax = (int)max;
        int dmgInt = _rand.Next(dmgMin, dmgMax + 1); //20~35 +차이 값 중 하나 랜덤으로 저장
        float damage = dmgInt;

        bool clear = true;
        if (!isOk && _rand.Next(100) < 40) //권장 방어력 보다 낮다면 40%확률로 실패
        {
            clear = false;
            _player.Health /= 2f; //피 절반 깍
        }

        float oldHp = _player.Health;
        float oldG = _player.Gold;
        _player.Health = Math.Max(0f, _player.Health - damage);

        float gain = 0f; //보상
        if (clear)
        {
            _player.LevelManager.RegisterDungeonClear();
            float atk = _player.BaseAttack + _player.Inventory.EquippedAttackBonus();//플레이어 공격력 합
            int per = _rand.Next((int)atk, (int)(atk * 2f) + 1);//공격력 비율 추가 보상, +1을 해줘야 범위가 a ~ a*2가됨
            gain = baseGold + baseGold * per / 100f; //보상 지급
            _player.Gold += gain;
        }

        Console.WriteLine(clear ? "던전 클리어! 축하합니다!!" : "던전 실패... 아쉽군요...");
        Console.WriteLine($"[{name} 탐험 결과]\n");
        Console.WriteLine($"체력: {oldHp} -> {_player.Health}");
        Console.WriteLine($"Gold: {oldG} -> {_player.Gold}");
        Console.WriteLine("\n0. 나가기"); Console.ReadLine();
    }
}

구현하면서 과정 중 기억에 남는 부분은

  1. 초기에 숫자 관련한 변수 타입을 정수형으로 구현해두어서 계산하는데 자꾸 캐스팅 오류가 발생해서 조금 고생했다. 가능한 숫자형을 float로 통일하였다.
  2. 플레이어 체력이 음수가 되지 않도록 하기 위해 Max() 함수로 구현하였다.
  3. Enter()에 체력이 0일 경우 던접 입장을 제한하는 코드를 추가하였다.

잘 실행된다 😇

4. 게임 저장하기 추가

앱을 껏다 켜도 사용되던 정보를 유지하도록 구현해보자.

이거는 설계고 머고 모르겠다

파이어베이스를 사용하…려나?

일단 이에 대한 방법을 몇가지 찾아보자

  • 로컬 파일 저장(이걸로 하는게 좋을 것 같은데)
    • JSON/XML 직렬화
      • Character, Inventory, Item 등의 객체를 JSON이나 XML 형태로 직렬화하여 텍스트 파일로 저장 (System.Text.Json, Newtonsoft.Json, System.Xml.Serialization 등 사용).
      • 장점: 사람이 읽고 고치기도 쉽고, 버전 관리도 편리
      • 단점: 복잡한 객체 계층 구조에서 직렬화 설정이 조금 번거로울 수 있음
    • Binary 직렬화
      • .NET의 BinaryFormatter(구버전) 혹은 System.Runtime.Serialization.Formatters.Binary 대안 라이브러리 사용
      • 장점: 더 빠르고 파일 크기가 작음
      • 단점: 이진 포맷이라 디버깅이 어렵고, 버전 호환성 관리가 까다로움
  • 경량 데이터베이스 사용
    • SQLite
      • sqlite-net 같은 ORM 라이브러리로 객체를 테이블에 매핑하여 저장
      • 장점: 관계형 스키마로 복잡한 쿼리·인덱스 활용 가능, 안정성 높음
      • 단점: 외부 라이브러리 추가, SQL 관리 필요
    • LiteDB
      • .NET 전용 경량 문서형 데이터베이스, BSON 방식 저장
      • 장점: MongoDB 스타일로 바로 객체 저장·조회, 파일 하나로 관리
  • 플랫폼별 내장 저장소 (이것도 아니고)
    • 플레이어 프리퍼런스(PlayerPrefs)
      • Unity 환경이라면 PlayerPrefs에 간단한 키–값 형태로 저장 가능 (int, float, string)
      • 단점: 복잡한 구조 저장엔 부적합
    • Xamarin.Essentials: Preferences
      • 모바일 앱(Xamarin)이라면 Preferences.Set()/Preferences.Get()로 간단히 저장
  • 클라우드 동기화
    • Firebase Realtime Database/Firestore
      • 클라우드에 사용자별 데이터를 저장해 다른 기기에서도 이어서 플레이 가능
    • PlayFab, GameSparks
      • 게임 전용 백엔드 서비스로 세이브·리더보드·인벤토리 관리

GPT 선생님의 도움을 받은 결과,

결론적으로 현재 개발 환경이 NET 6 콘솔 앱이라는 점에서

JSON 파일에 게임 상태를 직렬화해서 저장하고 로드하는 방식이 좋다고 한다.

만일 쿼리나 인덱싱 등, 더 강령한 기능이 필요하다면 SQLite나 LIteDB를 사용해도 좋다고 한다.

그렇다면 구현을 해보자

아래는 찾아본 웹 검색이다

구글에서 검색한 "json 기반 콘솔 앱 게임 저장하기 기능"
각 과정을 한번 정리해 볼까?

  1. 데이터 구조를 정의 : 저장할 데이터의 뼈대를 먼저 세우기 (무엇을 저장할지)
  2. JSON 직렬화 및 역직렬화
    • System.Text.JSON을 사용해서 DTO 객체를 JSON으로, JSON을 DTO로 복원하도록
      • JsonSerializer.Serialize(obj) → 객체를 JSON 문자열로
      • JsonSerializer.Deserialize<T>(json) → JSON 문자열을 T타입 객체로
  3. 파일 입출력 : 만든 JSON문자열을 텍스트 파일로 쓰고, 반대로 앱 시작 시 읽어들이는 과정
    • File.WriteAllText(path, json) 으로 쓰고
    • File.ReadAllText(path) 으로 읽는다.
  4. 저장 및 로드 기능 코드 작성 : 저장하고 로드하는 코드를 실행 파일에 작성하기
  5. 예외 처리

이제 이 과정을 따라 구현해보자

데이터 구조를 먼저 정의해보면….

public class GameState
{
    public CharacterState Player { get; set; }
    public List<ItemState> Inventory { get; set; }
    public float Health { get; set; }
    public float Gold { get; set; }
}

public class CharacterState
{
    public string Name { get; set; }
    public string Job { get; set; }
    public int Level { get; set; }
    public float BaseAttack { get; set; }
    public float BaseDefense { get; set; }
}

public class ItemState
{
    public string Name { get; set; }
    public float Attack { get; set; }
    public float Defense { get; set; }
    public string Description { get; set; }
    public float Price { get; set; }
    public int Type { get; set; }
    public bool Purchased { get; set; }
    public bool Equipped { get; set; }
}
  1. GameState: 세이브 파일에 기록할 전체 게임 상태
  • Player: 플레이어 속성
  • Inventory: 보유 아이템 목록
  • Level, Health, Gold: 핵심 프로퍼티
  1. CharacterState: Character 객체에서 직렬화할 속성
  2. ItemState: Item 클래스에서 저장할 필드

이어서 직렬화 과정과 파일 입출력은

using System.Text.JSON 을 추가하고

Game클래스 안에 메소드로 작성하면 된다.

파일 입출력안에 예외처리도 같이 구현하면 된다.

아래는 추가된 코드만 작성하였다.

 private const string SaveFile = "savegame.json";

     public void Start()
 {
     // 저장 파일이 있으면 불러오고, 없으면 새 게임
     if (File.Exists(SaveFile)) LoadGame();
     else InitNewGame();

     // 서비스 객체 초기화
     _shop = new Shop(_player);
     _rest = new Rest(_player);
     _dungeon = new Dungeon(_player);
     MainLoop();
 }
 
   private void InitNewGame()
  {
      Console.WriteLine("스파르타 던전에 오신 여러분 환영합니다!");
      Console.Write("이름을 입력해주세요: ");
      string name = Console.ReadLine()?.Trim() ?? "Chad";

      Console.WriteLine("원하는 직업을 선택하세요: 1. 전사  2. 도적");
      string job = Console.ReadLine() == "2" ? "도적" : "전사";

      _player = new Character(name, job);
  }

private void LoadGame()
{
    try
    {
        string json = File.ReadAllText(SaveFile);
        var state = JsonSerializer.Deserialize<GameState>(json);
        if (state == null) throw new Exception("세이브 데이터 파싱 실패");

        _player = new Character(state.Player.Name, state.Player.Job)
        {
            Level = state.Player.Level,
            BaseAttack = state.Player.BaseAttack,
            BaseDefense = state.Player.BaseDefense,
            Health = state.Health,
            Gold = state.Gold
        };

        foreach (var itemState in state.Inventory)
        {
            var item = new Item(
                itemState.Name,
                itemState.Attack,
                itemState.Defense,
                itemState.Description,
                itemState.Price,
                itemState.Type)
            {
                Purchased = itemState.Purchased,
                Equipped = itemState.Equipped
            };
            _player.Inventory.AddItem(item);
        }

        Console.WriteLine("세이브 데이터를 불러왔습니다.");
        Pause();
    }
    catch
    {
        Console.WriteLine("로드 오류 발생, 새 게임을 시작합니다.");
        Pause();
        InitNewGame();
    }
}

		// 상태 변경 시 이 메소드를 호출해서 저장해야한다.
		//메인 루프에서 상태 변경 시 호출되도록 구현해보자
		//이 메소드를 호출해야 게임이 저장된다.
private void SaveGame()
{
    var state = new GameState
    {
        Player = new CharacterState
        {
            Name = _player.Name,
            Job = _player.Job,
            Level = _player.Level,
            BaseAttack = _player.BaseAttack,
            BaseDefense = _player.BaseDefense
        },
        Health = _player.Health,
        Gold = _player.Gold,
        Inventory = _player.Inventory._items.Select(i => new ItemState
        {
            Name = i.Name,
            Attack = i.Attack,
            Defense = i.Defense,
            Description = i.Description,
            Price = i.Price,
            Type = i.Type,
            Purchased = i.Purchased,
            Equipped = i.Equipped
        }).ToList()
    };

    try
    {
        var options = new JsonSerializerOptions { WriteIndented = true };
        string json = JsonSerializer.Serialize(state, options);
        File.WriteAllText(SaveFile, json);
    }
    catch
    {
        Console.WriteLine("저장 오류 발생");
    }
}
    
    
private void MainLoop()
{
    while (true)
    {
        Console.Clear();
        Console.WriteLine("스파르타 마을에 오신 여러분 환영합니다.");
        Console.WriteLine("이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n");
        Console.WriteLine("1. 상태 보기");
        Console.WriteLine("2. 인벤토리");
        Console.WriteLine("3. 상점");
        Console.WriteLine("4. 휴식하기");
        Console.WriteLine("5. 던전 입장\n");
        Console.Write("원하시는 행동을 입력해주세요: ");
        switch (Console.ReadLine())
        {
            case "1": _player.ShowStatus(); break;
            case "2": _player.InventoryMenu(); break;
            case "3": _shop.ShopMenu(); break;
            case "4": _rest.RestAction(); break;
            case "5": _dungeon.Enter(); break;
            default:
                Console.WriteLine("잘못된 입력입니다.");
                Pause();
                break;
        }
        SaveGame(); //상태 변화시 저장하는 코드
    }
}
     

잘 실행된다. 🤩

게임 데이터를 저장하는 기능 구현의 과정에서 단계별로 다시 한번 정리해보고 마무리하자

  1. 데이터 구조 정의
    • GameState, CharacterState, ItemState 세 개의 DTO 클래스를 만들어, 게임 전체 상태(플레이어 속성·인벤토리·체력·골드 등)를 담을 구조를 설계했다.
  • JSON 직렬화·역직렬화 구현
    • .NET 내장 System.Text.Json을 이용해 DTO를 JSON 문자열로 바꾸는 JsonSerializer.Serialize()와, 저장된 JSON을 DTO로 복원하는 JsonSerializer.Deserialize<T>() 메서드를 사용하도록 코드를 작성했다.
  • 파일 입·출력 로직 추가
    • savegame.json 파일에 텍스트로 JSON을 기록하기 위해 File.WriteAllText(), 읽어오려면 File.ReadAllText()File.Exists()를 사용했다.
    • 입·출력 중 예외가 발생하면 사용자 메시지를 띄우고 안전하게 회복하도록 try-catch 구문을 넣었다.
  • 게임 흐름에 통합
    • 앱 시작부(Game.Start)에 저장 파일이 있으면 LoadGame(), 없으면 InitNewGame()을 호출해 초기 상태를 결정하도록 하고,
    • 메인 루프 각 턴마다(그리고 종료 시) SaveGame()을 자동 호출해서 상태 변경이 즉시 저장되도록 구현했다.
  • 예외 처리 및 검증 메시지
    • LoadGame()SaveGame() 각각에 예외 처리를 넣어 “로드 오류”/“저장 오류” 메시지를 보여주고,
    • 로드 실패 시 새 게임으로 안전하게 전환하도록 구현하였다.

💭 오늘의 회고

  • 프로젝트를 진행하면서 얻은 인사이트
    • 프로젝트를 진행하면서 설계에 대한 시야가 넓어졌고, 문법 공부가 실전에서 어떻게 활용되는지 체감할 수 있었다.
    • 특히 오류가 발생했을 때, 에러 메시지를 읽고 문제를 추론하는 습관이 생긴 점이 가장 큰 성장이라고 느꼈다.
    • JSON을 활용한 게임 저장 기능 구현 과정에서는 파일 입출력과 직렬화, 역직렬화의 전체 흐름을 체계적으로 익힐 수 있었다.
    • 실수를 두려워하기보다 실험하고 실패하며 개선하는 방식이 지금 나에게 성장을 위한 방식이라고 확신이 들었다.
  • 알고리즘 정리를 진행하면서 얻은 인사이트
    • 정렬 알고리즘을 다 정리하려고 했는데...어떻게 하면 잘 정리할지에 대해서 고민하다가 다 못했다...
    • 주말에 노트북 들고 카페가서 여유롭게 정리해볼 생각이다.
profile
천천히, 꾸준하게, 끝까지

0개의 댓글