| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- Network Programming
- git
- c#
- PS
- 독서
- C++
- Data Structure
- BOJ
- System Programming
- Toy Project
- multi-thread
- Online Judge
- Unity
- Today
- Total
I'm FanJae.
[My Turn Based Console RPG] Day 5. 프로젝트 최종 정리 본문
[My Turn Based Console RPG] Day 5. 프로젝트 최종 정리
FanJae 2026. 5. 19. 23:551. 프로젝트 개요
- 콘솔 환경에서 동작하는 턴제 RPG
- 로컬 환경에서 플레이하는 턴제 RPG 게임을 구현하였다.
- 개발 인원 : 1인
- 개발 기간 : 05.13~05.17
- 실행 환경 : .NET 8.0 (런타임 설치 필요)
2. 프로젝트 정리
- 이번 콘솔 프로젝트에서 내가 가장 목표했던 것은 게임에서 활용되는 여러 시스템을 고려한 구현이었다. 즉, 게임 자체의 볼륨을 높이는 것보다 게임에서 활용되는 여러 시스템을 만들어 보는 것에 초점을 맞췄다.
① UI를 표현할 때 어떤 느낌으로 표현할 것인가?
② 아이템 이라는 것은 어떤 정보를 가져야 할까?
③ 캐릭터라는 것에서 Playerable한(즉, 플레이 할 수 있는) 캐릭터와 몬스터를 어떻게 분리할 것인가?
④ 인벤토리에서는 실제 아이템 객체를 가지고 있는 것이 이상적일까? 아니면 이를 분리하는 것이 이상적인가? 또, 많은 게임을 보면 인벤토리에서 개수 정보를 가지는 것과 그렇지 않은 것을 어떻게 분리할 것인가?
⑤ 저장 / 로드 상황에서 고려해야 할 점은 무엇일까?
⑥ 각 클래스는 어떤 정보를 알아야 하고, 어떤 정보는 몰라야 하는가? 또, 클래스의 책임 단위는 어디까지 나눠야 할까?
※ 이전에 작업했던 코드와는 다르게 이번에는 최대한 분리하여 작업하기 위해 노력 했고, 꽤 많은 부분에서 책임을 나눴다.
3. 프로젝트 전체 구조 설명
- 각 프로젝트는 기능 단위와 역할에 따라 폴더를 분리하였다.
- 예를 들어, 플레이어와 몬스터와 같은 게임 내 객체는 Character로 분리하였다.
- 실제 게임 진행과 관련된 로직은 System 폴더, 콘솔 출력이나 화면 구성을 담당하는 클래스는 UI와 같은 식으로 분리 작업을 진행하였다.
- 즉, 전체적인 설계의 목적은 게임 로직과 출력 로직이 한 클래스에 섞이지 않도록 하는게 목적이다.
① Character
- Character 폴더는 전투에 참여하는 객체들을 관리한다.
- 공통 부모 클래스인 Character를 기준으로, Player와 Monster가 상속 구조를 이루고, Player는 Warrior와 Mage로 직업을 나누었다.
- 전투에 참여하는 객체(Player, Monster)에 대한 여러 가지 정보(상태이상, 착용 장비, 스킬)등의 정보도 이 폴더에서 관리하고 있다.
Character 폴더의 구성 요소
Character
├── Character.cs - 플레이어와 몬스터의 공통 능력치 / 전투 기능 정의
├── Player.cs - 플레이어의 레벨, 경험치, 골드, 인벤토리, 장비, 스킬 관리
│ ├── Warrior.cs - 전사 직업의 초기 능력치, 성장치, 스킬 정의
│ └── Mage.cs - 마법사 직업의 초기 능력치, 성장치, 스킬 정의
├── Monster
│ ├── Monster.cs - 실제 전투에 사용되는 몬스터 객체
│ ├── MonsterData.cs - 몬스터 원본 데이터 구조 정의
│ ├── MonsterDatabase.cs - 몬스터 데이터 목록 관리 및 조회
│ └── MonsterFactory.cs - MonsterData를 Monster 객체로 생성
├── StatusEffects
│ ├── StatusEffect.cs - 상태이상의 공통 구조 정의
│ └── PoisonEffect.cs - 독 상태 이상 효과 구현
├── Equipment.cs - 플레이어의 무기 / 방어구 장착 및 장비 보너스 관리
└── Skill.cs - 스킬 정보, MP 소모, 데미지 처리 관리
② Enums
- Enums 폴더는 게임에서 사용하는 상태값과 타입값을 enum으로 관리한다.
- 문자열이나 숫자를 직접 사용하는 대신 enum를 사용해 코드 의미를 명확하게 했다.
Enums 폴더의 구성 요소
Enums
├── BattleResult.cs - 전투 결과
├── CountEffectType.cs - 상태 이상 종류
├── EquipmentType.cs - 장비 종류
├── ItemType.cs - 아이템 종류
├── JobType.cs - 작업 종류
├── MainMenuResult.cs - 메인 메뉴 선택 결과
├── ShopMode.cs - 상점 모드
└── SkillType.cs - 스킬 종류
③ Items
- Items 폴더는 아이템과 인벤토리 관련 기능을 관리한다.
- 아이템의 원본 데이터는 ItemData와 ItemDatabase가 관리한다.
- 플레이어가 보유한 아이템은 Inventory와 InvetorySlot 형태로 나눠 관리한다.
- 소비 아이템 효과는 IItemEffect 인터페이스 형태로 나눠서, 아이템 효과를 확장할 수 있게 분리하였다.
Items 폴더의 구성 요소
Items
├── DropItem.cs - 몬스터가 드랍할 아이템 ID, 수량, 드랍 확률 관리
├── EquipmentStats.cs - 장비 아이템의 장비 타입, 착용 가능 직업, 공격력 / 방어 보너스 관리
├── HealEffect.cs - HP 회복 아이템 효과 구현
├── IItemEffect.cs - 소비 아이템 효과의 공통 인터 페이스
├── Inventory.cs - 플레이어가 가진 아이템 슬롯 목록 관리
├── InventorySlot.cs - 인벤토리 한 칸의 아이템 ID, 수량, 장착 여부 관리
├── ItemData.cs - 아이템의 원본 데이터 정의
├── ItemDatabase.cs - 아이템 원본 데이터를 Dictionary로 관리
└── RestoreMpEffect.cs - MP 회복 아이템 구현
④ Map
- Map 폴더는 게임 내 맵 이동과 포탈 이동 기능을 관리하는 영역이다.
- MapData는 하나의 맵 정보 저장, MapPortal은 연결 정보 저장을 한다.
- Maps 클래스에서는 게임 전체 맵 데이터를 생성하고 관리하도록 구성했다.
Maps 폴더의 구성 요소
Map
├── MapData.cs - 맵 이름, 설명, 포탈 목록 등 맵 정보 관리
├── MapPortal.cs - 연결된 맵과 이동 위치 정보 관리
└── Maps.cs - 전체 맵 데이터 생성 및 관
⑤ Save
- Save 폴더는 게임 저장 및 불러오기에 필요한 데이터 구조를 관리한다.
- SaveData 클래스는 게임 진행에 필요한 주요 데이터를 저장하고, 현재 플레이 상태를 하나의 객체로 정리하여 저장할 수 있게 구성했다.
- InventorySaveData 클래스는 인벤토리 저장 전용 데이터 구조다. 인벤토리 슬롯의 아이템 ID, 수량, 장착 여부 등을 저장하며, 불러오기 시 Inventory 복원에 사용한다.
- Save 폴더의 클래스들은 저장 가능한 데이터 구조를 정의하는 역할만 담당한다.
Save 폴더의 구성 요소
Save
├── InventorySaveData.cs - 인벤토리 저장용 데이터 구조 관리
└── SaveData.cs - 플레이어 상태 및 게임 진행 정보 저장
⑥ Shops
- Shop 폴더는 상점에서 판매되는 상품 데이터를 관리한다.
- 현재는 ShopItem 클래스만 포함되어 있다.
Shops 폴더의 구성 요소
Shops
└── ShopItem.cs - 상점에서 판매하는 아이템 ID, 가격, 재고 등 상점 상품 정보 관리
⑦ Systems
- System 폴더는 게임의 핵심 진행 로직을 담당한다.
- System 폴더는 데이터나 화면 구성을 담당하는 객체를 연결 해 실제 게임 기능을 동작하도록 처리하는 것으로 구성되어 있다.
- GameController가 전체 게임 흐름을 제어하고, BattleSystem, ShopSystem, SaveService, DropService와 같은 클래스 들이 각각 전투, 상점, 저장, 드랍 기능을 담당하여 게임 진행 로직을분리했다.
Systems 폴더의 구성 요소
System
├── BattleSystem.cs - 턴제 전투 흐름과 전투 결과 처리
├── DropService.cs - 몬스터 처치 후 아이템 드랍 처리
├── EncounterService.cs - 랜덤 몬스터 등장 처리
├── GameController.cs - 게임 전체 흐름 제어
├── ItemUseService.cs - 인벤토리 아이템 사용 처리
├── PlayerFactory.cs - 직업 선택에 따른 플레이어 객체 생성
├── SaunaService.cs - 사우나 회복 기능 처리
├── SaveService.cs - 게임 저장 및 불러오기 처리
└── ShopSystem.cs - 상점 구매 / 판매 로직 처리
⑧ UI (Input, Rendering)
(1) UI 전체 구조 설명
- UI 폴더는 콘솔 화면 출력과 사용자 입력을 담당한다.
- UI 내부에서도 역할에 따라 Input, Rendering, Screens, Views, Settings 등으로 세분화하였다.
(2) Input
- Input 폴더는 사용자 입력을 처리하는 클래스들을 관리한다.
- 현재는 MenuInput 클래스만 존재하며, 선택 가능한 범위 안의 값인지 검사하고 결과를 반환하는 역할만 담당한다.
(3) Rendering
- Rendering 폴더는 콘솔 출력에 공통적으로 사용되는 렌더링 기능을 관리한다.
- ConsoleRenderer, AsciiImageRenderer, MapRenderer는 각각 공통 콘솔 출력, ASCII 이미지 출력, 맵 출력 기능을 담당한다. 이를 통해 화면 클래스에서 입력 처리와 출력 세부 구현이 반복되지 않게 하고 있다.
(4) Settings
- Settings 폴더는 콘솔 창과 화면 배치에 필요한 설정 값을 관리한다. 콘솔 환경 설정과, 화면 크기 같은 상수 값을 처리한다.
(5) Screens
- Screens 폴더는 화면 단위 흐름을 담당한다.
- 각 화면마다 별도의 Screen 클래스를 두어, 사용자 입력을 받고 필요한 기능을 호출하도록 처리했다.
(6) Views
- Views 폴더는 각 화면에 실제로 출력되는 내용을 구성한다.
- Screen 폴더가 화면 흐름과 입력 처리를 하면, Views 폴더는 화면에 어떤 목록을 출력할지 처리해준다. 이를 통해 화면 흐름 로직과 출력 코드를 나눠 처리했다.
UI 폴더의 구성 요소
UI
├── Input
│ └── MenuInput.cs - 메뉴 선택 입력 처리
├── Rendering
│ ├── AsciiImageRenderer.cs - ASCII 이미지 출력처리
│ ├── ConsoleRenderer.cs - 콘솔 출력 공통 기능 제공
│ └── MapRenderer.cs - 맵 화면 출력 처리
├── Screens
│ ├── BattleScreen.cs - 전투 화면 흐름과 입력 처리
│ ├── CharacterSelectScreen.cs - 캐릭터 선택 화면 흐름 처리
│ ├── GameScreen.cs - 게임 메인 화면 흐름 처리
│ ├── InventoryScreen.cs - 인벤토리 화면 흐름과 아이템 선택 처리
│ ├── MainMenu.cs - 메인 메뉴 화면 흐름 처리
│ ├── MapScreen.cs - 맵 화면 흐름과 이동 선택 처리
│ ├── SaunaScreen.cs - 사우나 화면 흐름 처리
│ └── ShopScreen.cs - 상점 화면 흐름과 구매/판매 선택 처리
├── Settings
│ ├── ConsoleSetting.cs - 콘솔 창 크기 및 환경 설정
│ └── Layout.cs - 화면별 레이아웃 상수
├── Views
│ ├── BattleUI.cs - 전투 UI 출력
│ ├── CharacterSelectView.cs - 캐릭터 선택 화면 출력
│ ├── GameUI.cs - 게임 메인 UI
│ ├── InventoryView.cs - 인벤토리 목록 및 아이템 정보 관련
│ ├── MainMenuView.cs - 메인 메뉴 화면 출력
│ ├── SaunaView.cs - 사우나 화면 출력
│ └── ShopView.cs - 상점 상품 및 구매/판매 화면
├── LogoArt.cs - 게임 로고 및 ASCII 아트
└── Program.cs - 프로그램 시작 지점
5. 주요 설계
5-1. Screen / View / System 단위 책임 분리

- 기본적으로 프로그램의 흐름은 위의 4단계를 근거로 이뤄지게 처리했다.
- 특정 Screen에서 사용자 입력에 따른 흐름에 따라서, System을 호출해준다. 이후, System이 주요 로직을 처리합니다. System이 호출되면 System은 필요한 객체를 가져와서 로직을 처리한다.
- 로직 처리가 완료되면, 갱신된 정보를 화면에 새로고침 하여, Views 또는 Renderer가 이 역할을 처리해준다.
(1) 실제 예시
① 몬스터 전투 처리
(1) 입력(Screen) : 사용자가 BattleScreen에서 공격을 선택한다.
(2) 처리(System) : BattleSystem이 호출되어, Player의 공격력을 가져오고, Monster 객체의 TakeDamage() 메서드를 실행하여, HP 데이터를 깎는다.
- 몬스터가 사망시, DropService가 ItemDatabase를 참조하여 드롭 아이템을 계산하고, Inventory에 추가한다.
(3) 출력 (Views or Renderer) : BattleUI가 갱신된 플레이어와 몬스터의 HP 상태를 읽어와 화면을 새로 고침한다.
5-2. 이벤트 기반 화면 전환 구조
public event Action? MapChanged;
public event Action<Monster>? MonsterEncountered;
public event Action? ShopEntered;
public event Action? SaunaEntered;
- MapScreen은 전투나 상점 화면을 직접 실행하지 않고, 이벤트를 통해 외부에 상태를 전달하도록 구성했다.
- 이를 통해 맵 이동 로직과 화면 전환을 분리했고, MapScreen이 특정 화면 구현에 의존하지 않게 했다.
public GameScreen(Player player)
{
this.player = player;
mapScreen = new MapScreen(Maps.Town, player);
gameUI = new GameUI(player);
mapScreen.MapChanged += DrawAll;
mapScreen.MonsterEncountered += StartBattle;
mapScreen.ShopEntered += OpenShop;
mapScreen.SaunaEntered += OpenSauna;
}
- GameScreen이 객체 생성 시점에 해당 이벤트를 구독하는 것을 확인할 수 있다.
public void Dispose() // 이벤트 정리
{
mapScreen.MapChanged -= DrawAll;
mapScreen.MonsterEncountered -= StartBattle;
mapScreen.ShopEntered -= OpenShop;
mapScreen.SaunaEntered -= OpenSauna;
}
- 게임에서 나갈 때 구독했던 이벤트를 정리하도록 처리했다.
5-3. ID 기반 인벤토리 설계
using MyConsoleMapleRPG.Items;
namespace MyConsoleMapleRPG.Items.Inventory
{
// 플레이어가 가진 아이템 슬롯 목록을 관리하는 클래스
// ItemId와 Count만 저장, 실제 데이터는 ItemDatabase에서 조회
internal class Inventory
{
private readonly List<InventorySlot> slots = new();
public IReadOnlyList<InventorySlot> Slots => slots;
// 아이템 추가
public bool AddItem(ItemData itemData, int count = 1)
{
if (itemData == null)
return false;
if (count <= 0)
return false;
if (itemData.IsStackable) // 쌓이는 아이템 추가
{
AddStackableItem(itemData, count);
return true;
}
AddNonStackableItem(itemData.Id, count); // 쌓이지 않는 아이템 추가
return true;
}
public bool TryGetSlot(int slotIndex, out InventorySlot? slot) // 슬롯 정보 획득
{
if (slotIndex < 0 || slotIndex >= slots.Count)
{
slot = null;
return false;
}
slot = slots[slotIndex];
return true;
}
public bool RemoveItemAt(int slotIndex, int count = 1) // 특정 위치 아이템 제거
{
if (count <= 0)
return false;
if (!TryGetSlot(slotIndex, out InventorySlot? slot))
return false;
if (slot.Count < count) // 제거해야할 개수보다 슬롯 아이템 개수가 적음
return false;
slot.RemoveCount(count); // 개수 깎기
if (slot.Count <= 0) // 슬롯에 아이템이 없으면 해당 슬롯 삭제
slots.RemoveAt(slotIndex);
return true;
}
public bool RemoveItem(int itemId, int count)
{
// 0개 이하일때 차감 불가
if (count <= 0)
return false;
int totalCount = slots.Where(slot => slot.ItemId == itemId).Sum(slot => slot.Count); // Linq
// 해당 아이템의 보유량 계산
if (totalCount < count) return false;
int remaining = count; // 남은 아이템 개수(목표 차감량)
// 기본적으로 뒷쪽부터 순차제거 하는 방식으로 구현
for (int i = slots.Count - 1; i >= 0 && remaining > 0; i--)
{
InventorySlot slot = slots[i];
if (slot.ItemId != itemId)
continue;
// 현재 슬롯에서 차감할 수량
int removeCount = Math.Min(slot.Count, remaining);
slot.RemoveCount(removeCount);
remaining -= removeCount;
if (slot.Count <= 0)
slots.RemoveAt(i);
}
return true;
}
private void AddStackableItem(ItemData itemData, int count) // 쌓이는 아이템 처리
{
int remainingCount = count;
foreach (InventorySlot slot in slots)
{
if (slot.ItemId != itemData.Id) // 아이템이 다르면 다음 슬롯으로
continue;
int availableSpace = itemData.MaxStackCount - slot.Count; // 할당 공간 계산
if (availableSpace <= 0)
continue;
int addCount = Math.Min(availableSpace, remainingCount); // 추가 가능한 개수 만큼 추가
slot.AddCount(addCount);
remainingCount -= addCount;
if (remainingCount <= 0)
return;
}
while (remainingCount > 0) // 추가 가능할때까지 계속 넣ㄱ
{
int addCount = Math.Min(itemData.MaxStackCount, remainingCount);
slots.Add(new InventorySlot(itemData.Id, addCount));
remainingCount -= addCount;
}
}
private void AddNonStackableItem(int itemId, int count) // 쌓기 안되는 아이템 추가
{
for (int i = 0; i < count; i++)
{
slots.Add(new InventorySlot(itemId, 1));
}
}
public InventorySlot AddSlotForLoad(int itemId, int count, bool isEquipped) // 세이브 데이터 정보를 불러올때 인벤토리 복원
{
InventorySlot slot = new InventorySlot(itemId, count); // 인벤토리 슬롯에 아이템 재할당
slot.SetEquipped(isEquipped); // 장비 착용 처리
slots.Add(slot);
return slot;
}
}
}
namespace MyConsoleMapleRPG.Items.Inventory
{
// 인벤토리의 한 칸을 표현하는 클래스
// 어떤 아이템인가와 몇 개인가에 대한 보관
internal class InventorySlot
{
public int ItemId { get; }
public int Count { get; private set; }
public bool IsEquipped { get; private set; }
public InventorySlot(int itemId, int count) // 인벤토리 슬롯
{
if (count <= 0) throw new ArgumentException("아이템 수량은 1 이상이어야 합니다.");
ItemId = itemId;
Count = count;
}
public void SetEquipped(bool value) // 장비 착용 설정
{
IsEquipped = value;
}
public void AddCount(int amount) // 개수 늘리기
{
if (amount <= 0) return;
Count += amount;
}
public void RemoveCount(int amount) // 개수 줄이기
{
if (amount <= 0) return;
Count = Math.Max(0, Count - amount);
}
}
}
- 인벤토리에는 아이템 객체 자체를 직접 저장하지 않고, ItemID를 통해 관리하고 있다.
- 별도의 InventorySlot을 통해, ItemId, Count, IsEquipped 같은 보유 상태만 저장하도록 설계했다.
- 아이템의 이름, 설명, 가격, 타입 같은 아이템의 원본 정보는 ItemData / ItemDataBase에서 관리한다.
- InventorySlot은 플레이어가 해당 아이템을 몇개 가지고 있는지, 장착 중인지와 같은 상태만 처리한다..
- 이를 통해, 아이템 원본 데이터와 플레이어가 가진 아이템 데이터를 분리했고, 저장/로드 시에도 ItemId와 수량 중심으로 처리했다.
6. 플레이 영상
https://youtu.be/cmpc88eea0M?si=KzTv0UgK27L4_FHr
7. Git
https://github.com/fanjae/GM07_KimHanjae_MyConsoleMapleRPG
GitHub - fanjae/GM07_KimHanjae_MyConsoleMapleRPG: C# 콘솔 기반 턴제 RPG 구현
C# 콘솔 기반 턴제 RPG 구현. Contribute to fanjae/GM07_KimHanjae_MyConsoleMapleRPG development by creating an account on GitHub.
github.com
'Projects > My Turn Based Console RPG' 카테고리의 다른 글
| [My Turn Based Console RPG] Day 5. 저장 / 불러오기 기능 추가 (0) | 2026.05.19 |
|---|---|
| [My Turn Based Console RPG] Day 4. 아이템, 상점, 인벤토리 기능 구현 (0) | 2026.05.18 |
| [My Turn Based Console RPG] Day 3. 배틀 시스템 구현 및 기능 단위 클래스 분리 (0) | 2026.05.17 |
| [My Turn Based Console RPG] Day 2. 직업 선택 페이지 & 기본 UI & 맵 관련 기능 구현 (0) | 2026.05.16 |
| [My Turn Based Console RPG] Day 1. 기본 구상 및 메인 화면 구현 (0) | 2026.05.14 |