| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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
- C++
- git
- multi-thread
- Data Structure
- BOJ
- Toy Project
- Unity
- PS
- Online Judge
- 독서
- System Programming
- c#
- Today
- Total
I'm FanJae.
[My Turn Based Console RPG] Day 2. 직업 선택 페이지 & 기본 UI & 맵 관련 기능 구현 본문
[My Turn Based Console RPG] Day 2. 직업 선택 페이지 & 기본 UI & 맵 관련 기능 구현
FanJae 2026. 5. 16. 21:481. 시작에 앞서
- 원래 당일에 미리 적었어야 했는데, Notion에는 적어놨지만, 당일에 블로그에 정리하는 작업과 Git에 올리지 않았다. 다음에는 이런 일이 없게 미리미리 정리하고 커밋 해둬야 할 것 같다.. 그나마 노션에 적어놨던 점은 다행이다.
2. 직업 선택 페이지 구현

- 도트 특유의 크기를 조금 줄이면 화면이 깨지는 문제 때문에 원본 화질을 그대로 사용할 필요가 있었다.
- 해당 직업군이 사용하는 적당한 무기를 찾다가 둘의 크기가 크게 다르지 않은 무기를 찾아서 해당 무기로 변경하였다.
① 이미지 렌더러 분리
using System.Drawing;
namespace MyConsoleMapleRPG.UI.Rendering
{
// 이미지 파일을 읽어서 콘솔 배경색 기반의 ASCII/픽셀 아트 출력하는 렌더러
// UI 전용 유틸.
internal static class AsciiImageRenderer
{
public static void Draw(string path, int x = 0, int y = 0, int? resizeX = null, int? resizeY = null)
{
if (!File.Exists(path))
{
Console.SetCursorPosition(x, y);
Console.Write($"이미지 없음: {path}");
throw new FileNotFoundException($"이미지 파일을 찾을 수 없습니다. Path: {path}, FullPath: {Path.GetFullPath(path)}",path);
}
using Bitmap original = new Bitmap(path);
// 이미지 크기 조정. 명세가 없으면 기본 사용
int width = resizeX.HasValue ? resizeX.Value : original.Width;
int height = resizeY.HasValue ? resizeY.Value : original.Height;
using Bitmap bitmap = new Bitmap(original, new Size(width, height));
for (int row = 0; row < bitmap.Height; row++)
{
Console.SetCursorPosition(x, y + row);
for (int col = 0; col < bitmap.Width; col++)
{
Color color = bitmap.GetPixel(col, row);
if (color.A == 0) // 투명도 값이 0인 경우
{
Console.Write("\x1b[0m "); // 서식을 기본 상태로 초기화
continue;
}
Console.Write($"\x1b[48;2;{color.R};{color.G};{color.B}m ");
}
}
Console.Write("\x1b[0m");
Console.ResetColor();
}
}
}
- 이미지 정보를 찍을 때는 서식을 모두 기본 상태로 초기화 할 수 있다.
※ Screen, View, System과 같은 형태의 분리 작업은 Day 3에 진행하였지만, 해당 내용을 기록한 시점이 최종 프로젝트를 완성한 이후 시점입니다. 따라서, 최종 상태를 기준으로 적었습니다.
② CharacterSelectScreen (캐릭터 선택창 흐름 제어 클래스)
// CharacterSelectScreen.cs
using MyConsoleMapleRPG.UI;
using MyConsoleMapleRPG.UI.Audio;
using MyConsoleMapleRPG.UI.Views;
namespace MyConsoleMapleRPG.UI.Screens
{
// 직업 선택 화면의 입력 흐름을 담당하는 Screen 클래스
// 실제 출력은 CharacterSelectview에 위임. 클래스는 선택 인덱스와 입력 처리만 관리
internal class CharacterSelectScreen
{
private readonly CharacterSelectView view = new CharacterSelectView();
private readonly string[] menuItems =
{
"전사",
"마법사"
};
public int Show()
{
int selectedIndex = 0;
Console.Clear();
view.DrawBaseScreen(); // 화면 기본 창 그림
while (true)
{
view.DrawMenuItems(menuItems, selectedIndex); // 메뉴 창 그림
ConsoleKey key = Console.ReadKey(true).Key;
switch (key)
{
case ConsoleKey.LeftArrow:
case ConsoleKey.RightArrow:
selectedIndex = MenuInput.MoveHorizontal(selectedIndex, menuItems.Length, key);
break;
case ConsoleKey.Z:
case ConsoleKey.Enter:
return selectedIndex;
case ConsoleKey.Escape:
return -1;
}
}
}
}
}
- 기본적으로 Screen이라고 되어 있는 클래스는 모두 입력에 따라서 흐름을 처리한다.
- 출력과 관련한 부분은 View라고 되어 있는 클래스에서 처리해준다.
③ CharacterSelectView
// CharacterSelectView.cs
using MyConsoleMapleRPG.UI.Rendering;
namespace MyConsoleMapleRPG.UI.Views
{
// 직업 선택 화면의 출력 전용 View 클래스
// 직업 이미지와 선택 메뉴 출력.
internal class CharacterSelectView
{
private const int MenuStartX = 22;
private const int MenuY = 36;
private const int MenuSpacing = 45;
private const int MenuWidth = 14;
public void DrawBaseScreen() // 직업 선택 관련 기본 창 그림
{
ConsoleRenderer.DrawWindow(2, 1, 90, 40, "JOB SELECT");
Console.SetCursorPosition(38, 3);
Console.Write("직업을 선택하세요");
AsciiImageRenderer.Draw("Assets/image/sword.png", 10, 8);
AsciiImageRenderer.Draw("Assets/image/staff.png", 45, 8);
Console.Write("\x1b[0m");
Console.ResetColor();
}
public void DrawMenuItems(string[] menuItems, int selectedIndex) // 메뉴 창 그림 (전사, 마법사 글자)
{
for (int i = 0; i < menuItems.Length; i++)
{
Console.SetCursorPosition(MenuStartX + i * MenuSpacing, MenuY);
string text = i == selectedIndex ? $"▶ {menuItems[i]}" : $" {menuItems[i]}"; // 선택 유무에 따라 화살표 표기
Console.Write(text.PadRight(MenuWidth));
}
Console.Write("\x1b[0m");
Console.ResetColor();
}
}
}
- CharacterView에서는 직업 선택 창에 뿌려줄 화면을 처리하게 했다.
- 직업 선택 창에서 나오는 검과 스태프를 렌더러를 호출하여 그리게 하고, 본인은 Console 출력을 처리한다.
3. UI 구상
① 기본 스케치

- 잘 그렸다.
- 미술과 등지고 살았던 내게 이정도면 꽤 고퀄리티다.
② UI 기본 구상 부터 완성까지

- 처음엔 위와 같은 형태를 생각했고, 처음 화면에 테스트로 찍을때는 아래와 같은 형태였다.

- 테스트를 하기 위해 처음 구상했던 UI는 이와 같은 형태로 구상하였고, 이후 다음과 같이 처리했다.

- 프로젝트 중간에 커밋을 진행하지 않는 실수로.. 중간 코드가 남아있지 않지만, 콘솔 특유의 밋밋한 색깔을 조금은 덜어내보고자 이와 같이 색깔을 입혔다.

- 색깔을 입혀보면, 확실히 조금 덜 밋밋한 느낌이 되는 것을 확인할 수 있다.
③ 맵 표현 방식
public static readonly MapData Town = new MapData("리스 항구",
new string[]
{
"####################################################",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#...../------\\................/------\\.............#",
"#..../ SHOP \\............../ INN \\........... #",
"#....+--------+..............+--------+............#",
"#....| |..............| |............#",
"#....|___**___|..............|___;;___|............#",
"#..................................................#",
"#..................................................!",
"#......................★...........................!",
"#..................................................!",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"#..................................................#",
"####################################################"
},
23,
11
);
- 시간이 더 있었다면, 아마 하드코딩을 안하는게 좋았을 것 같다. 맵에는 실제로 저렇게 표현되고, 상점과 RPG 게임에서 흔히 볼 수 있는 여관(여기서는 사우나로 처리)한 부분은 위에서 보았듯 두개 모두 * 타일로 그려지도록 처리했다.
④ 맵을 구성하는 데이터
using MyConsoleMapleRPG.Character.Monster;
namespace MyConsoleMapleRPG.Map
{
// 하나의 맵을 구성하는 데이터 클래스
// 맵 이름, 타일 문자열, 시작 위치, 포탈, 등장 몬스터 목록을 보관
internal class MapData
{
public string Name { get; }
public string[] Tiles { get; }
public int StartX { get; }
public int StartY { get; }
public List<MonsterData> EncounterMonsters { get; } // 몬스터 인카운팅 정보
public Dictionary<(int x, int y), MapPortal> Portals { get; }
public MapData(string name, string[] tiles, int startX, int startY)
{
Name = name;
Tiles = tiles;
StartX = startX;
StartY = startY;
Portals = new Dictionary<(int x, int y), MapPortal> ();
EncounterMonsters = new List<MonsterData>();
}
}
}
- 기본적으로 맵은 이름, 타일 정보, 시작 좌표, 몬스터 인카운팅 등의 정보를 포함하고 있다. 풀숲 지형에서는 몬스터 전투도 가능하게 구현할 계획이다. (몬스터 배틀과 관련한 내용은 Day 3에서 다룬다.)
⑤ 맵 이동

- 인게임 내에서는 벽으로 막히지 않은 부분(화면의 노란색) 부분에 닿으면 이동하게 된다.
// MapScreen.cs
public event Action? MapChanged;
public event Action<Monster>? MonsterEncountered;
public event Action? ShopEntered;
public event Action? SaunaEntered;
- 맵에서는 기본적으로 맵이 담당하는 이벤트가 존재하며, 맵이 교체되거나 몬스터와 전투가 벌어지는 상황이나 상점 입장, 사우나 입장등을 처리할 수 있다. 이처럼 이벤트로 등록해두면, GameScreen에서 이동할 수 있게 처리했다.
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.cs
private void DrawAll()
{
Console.Clear(); // 기존 창 지움
gameUI.DrawMapLayout(mapScreen.MapName);
mapScreen.Draw(); // 맵을 새롭게 그려줌
gameUI.Draw();
}
- GameScreen은 이러한 흐름을 받아서, 각 3개의 요청을 진행하게 된다.
1) GameUI(View 역할)에 틀을 그릴 것을 요청
2) MapScreen에게 Draw()를 해줄 것을 요청하는데, 내부에는 MapRenderer를 호출하는 형태로 이뤄져있다.
3) GameUI가 Status, Info, Log 정보 등을 추가로 그린다.

그러면 이와 같이 맵 이동이 가능해진다.
'Projects > My Turn Based Console RPG' 카테고리의 다른 글
| [My Turn Based Console RPG] Day 5. 프로젝트 최종 정리 (0) | 2026.05.19 |
|---|---|
| [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 1. 기본 구상 및 메인 화면 구현 (0) | 2026.05.14 |