오늘은 TRPG의 마무리 작업을 하고, C#강의를 마저 수강할 예정이다.
어제 TRPG의 주요 기능들을 전부 완성했고, 이제 Json을 통해 저장/불러오기 기능을 구현해보겠다. 만약 시간이 남으면 추가적인 기능들을 더 구현해보고 마무리하겠다.
JSON 기반의 데이터 저장/불러오기 기능을 구현하기 전에 먼저 JSON이 무엇인지 알아보겠다.
게임에서 세이브 데이터를 저장하거나, 서버와 상태를 주고받을 때 JSON을 사용하면 편리하다.
save.json 같은 파일로 저장한다.Player와 Inventory에 적용해 이전 세션을 이어갈 수 있다.Name(string)Job (string)Level, Exp (int)Hp, MaxHp (int)Atk, Def (float)Gold (int)IsOccupied == true인 아이템들IsEquipped == true인 아이템들Name또는 인덱스)SaveData.cs를 생성.Player와 Item에 필요한 데이터들을 자동 구현 프로퍼티를 이용하여 접근한다.// SaveData.cs
public class SaveData
{
public PlayerData player { get; set; }
public List<ItemData> Inventory { get; set; }
}
public class PlayerData
{
public string Name { get; set; }
public string Job { get; set; }
public int Level { get; set; }
public int Exp { get; set; }
public int Gold { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public int MaxHp { get; set; }
public int Hp { get; set; }
}
public class ItemData
{
public string Name { get; set; }
public string Type { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public string Description { get; set; }
public int Price { get; set; }
public bool IsEquipped { get; set; }
public bool IsOccupied { get; set; }
}
SaveManager.cs 생성using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
public static class SaveManager
{
private static readonly string SaveFilePath = "save.json";
// 게임 데이터를 JSON으로 저장
public static void SaveGame()
{
// PlayerData 객체에 현재 플레이어 상태를 담는다
var playerDto = new PlayerData
{
Name = Player.Name,
Job = Player.Job,
Level = Player.Level,
Exp = Player.Exp,
Hp = Player.Hp,
MaxHp = Player.MaxHp,
Atk = (int)Player.Atk,
Def = (int)Player.Def,
Gold = Player.Gold
};
// Inventory에 보유 중인 아이템들만 DTO로 변환
var inventoryDto = Item.AllItems
.Where(item => item.IsOccupied)
.Select(item => new ItemData
{
Name = item.Name,
Type = item.Type,
Atk = item.Atk,
Def = item.Def,
Description = item.Description,
Price = item.Price,
IsEquipped = item.IsEquipped,
IsOccupied = item.IsOccupied
})
.ToList();
// 최종 저장 구조체
var saveData = new SaveData
{
player = playerDto,
Inventory = inventoryDto
};
// JSON 직렬화(들여쓰기)
var options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
string json = JsonSerializer.Serialize(saveData, options);
// 파일에 쓰기 (없으면 생성, 있으면 덮어쓰기)
File.WriteAllText(SaveFilePath, json);
}
// 저장된 JSON을 읽어와 게임 상태를 복원
public static bool LoadGame()
{
if (!File.Exists(SaveFilePath))
return false; // 저장 파일이 없으면 로드 실패
string json = File.ReadAllText(SaveFilePath);
var saveData = JsonSerializer.Deserialize<SaveData>(json);
if (saveData == null)
return false;
// Player 상태 복원
var pd = saveData.player;
Player.Name = pd.Name;
Player.Job = pd.Job;
Player.Level = pd.Level;
Player.Exp = pd.Exp;
Player.Hp = pd.Hp;
Player.MaxHp = pd.MaxHp;
Player.Atk = pd.Atk;
Player.Def = pd.Def;
Player.Gold = pd.Gold;
// Inventory 초기화 후, 저장된 아이템들만 적용
foreach (var item in Item.AllItems)
{
item.IsOccupied = false;
item.IsEquipped = false;
}
foreach (var id in saveData.Inventory)
{
var item = Item.AllItems.FirstOrDefault(x => x.Name == id.Name);
if (item != null)
{
item.IsOccupied = true;
item.IsEquipped = id.IsEquipped;
}
}
// Inventory 리스트 갱신
Inventory.renew();
return true;
}
}
게임 시작시 LoadGame() 메서드를 불러올 수 있도록 Program.cs를 수정했다.
//게임 시작씬
Console.WriteLine(" 1. 새 게임 ");
Console.WriteLine(" 2. 불러오기 ");
Console.WriteLine(" 3. 종료 ");
Console.WriteLine("");
Console.Write(">> ");
var choice = Console.ReadLine();
if (choice == "1")
{
Player.Reset();
PlayerSetup.Start();
MainScene.Start();
break;
}
else if (choice == "2")
{
if (SaveManager.LoadGame())
{
Console.Clear();
MainScene.Start();
break;
}
else
{
Console.WriteLine("\n저장된 게임이 없습니다.");
Console.WriteLine("새 게임을 시작하려면 아무키나 누르세요.");
Console.ReadKey();
Player.Reset();
PlayerSetup.Start();
MainScene.Start();
}
}
else if (choice == "3")
{
return;
}
else
{
Console.Clear();
Console.WriteLine("\n잘못된 입력입니다. 다시 선택해주세요.");
Console.ReadKey();
}
}
또한 각 메뉴에서 나올때마다 저장하도록 수정하였다.
// 예시: 휴식하기
Console.WriteLine("0. 나가기");
.
.
.
string input = Console.ReadLine();
if (input == "0")
{
Console.Clear();
Console.WriteLine("게임이 저장되었습니다.");
SaveManager.SaveGame();
Thread.Sleep(1000);
Console.Clear();
break;
}
몇 가지 자잘한 오류들을 수정하고 마무리했다.
먼저 라이브 세션에서 제시한 반복문을 이용하여 직각 삼각형 모양의 별을 찍는 과제를 간단하게 수행해보겠다.
for (int i = 0; i < 5; i++)
{
for (int j = 0; j <= i; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
Delegate란, "위임하다"라는 뜻으로, 어떤 메서드를 개체에게 위임하게 해주는 역할을 한다.
// 반환형이 int, 매개변수로 int 두 개를 받는 델리게이트
public delegate int Operation(int x, int y);
// Delegate의 타입에 맞는 메서드
int Add(int a, int b) => a + b;
// 델리게이트 변수에 메서드 연결
Operation op = Add;
int result = op(3, 4); // Add(3,4)를 호출한 것과 동일하게 7을 반환
op += Subtract; // Subtract(int,int)도 연결
op(10, 5); // 내부적으로 Add → Subtract 순서대로 호출
// 델리게이트 구조와 출력 예시
using System;
namespace DelegateExample
{
// 1. 델리게이트 선언: string 하나를 받는 void 반환형
public delegate void Notify(string message);
class Program
{
// 2. 델리게이트 인스턴스
static Notify notifier;
static void Main(string[] args)
{
// 3. 메서드 연결
notifier += ShowOnScreen;
notifier += LogToConsole;
Console.WriteLine("프로그램 시작");
// 4. 델리게이트 호출: 연결된 두 메서드가 순서대로 실행
notifier("Delegate 호출 테스트");
// 5. ShowOnScreen 제거 후 다시 호출
notifier -= ShowOnScreen;
notifier("두 번째 호출");
Console.WriteLine("프로그램 종료");
}
// 6-1. 델리게이트에 연결된 첫 번째 메서드
static void ShowOnScreen(string msg)
{
Console.WriteLine($"[Screen] {msg}");
}
// 6-2. 델리게이트에 연결된 두 번째 메서드
static void LogToConsole(string msg)
{
Console.WriteLine($"[Log] {msg}");
}
}
}
프로그램 시작
[Screen] Delegate 호출 테스트
[Log] Delegate 호출 테스트
[Log] 두 번째 호출
프로그램 종료
// 메서드로 구현했을 때
static void Main()
{
int result = Add(3, 5);
Console.WriteLine($"정식 메서드 결과: {result}");
}
static int Add(int x, int y)
{
return x + y;
}
//람다로 구현했을 때
static void Main()
{
// 1) Func 델리게이트에 람다 대입
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5);
Console.WriteLine($"람다 표현식 결과: {result}");
}
LINQ란?
기본 구조
var 결과 =
from 항목 in 데이터소스 // ① 데이터의 출발점
[where 조건식] // ② (선택) 필터링
[orderby 정렬키] // ③ (선택) 정렬
select 변환식; // ④ (선택) 꺼내올 값
var: 결과 컬렉션의 타입을 컴파일러가 자동으로 결정from: 순회할 원본 데이터 지정where: 조건에 맞는 항목만 걸러내기orderby: 결과를 원하는 순서대로 정렬하기select: 각 항목에서 최종적으로 꺼내올 값을 지정장점
일반적인 값 형식 변수에는 null값을 가질 수 없지만, nullable을 사용하면 null값을 허용할 수 있다.
Nullable을 쓰는 이유
사용 예시
int? score = null; // 아직 점수가 없다는 의미
if (score.HasValue)
Console.WriteLine(score.Value);
else
Console.WriteLine("점수가 없습니다.");
score = 85;
Console.WriteLine(score.GetValueOrDefault()); // 85
일반 string은 불변(immutable)이어서, 문자열을 수정할 때마다 내부적으로 새 인스턴스를 할당해야 한다.
StringBuilder는 내부에 가변 버퍼(buffer)를 두고, 이 버퍼를 직접 조작해 문자열을 추가·삽입·삭제·치환할 수 있도록 설계된 클래스이다.
Append(string s): 버퍼 끝에 문자열 s를 추가AppendLine(string s): Append(s) 후 Environment.NewLine 추가Insert(int pos, string s): 버퍼의 pos 위치에 문자열 s를 삽입Remove(int pos, int len): pos 위치부터 len 문자만큼 삭제Replace(string old, string @new): 버퍼 전체에서 old를 찾아 new로 모두 교체Clear(): Length = 0으로 설정해 내용을 모두 삭제, Capacity는 유지ToString(): 현재 버퍼 내용을 string으로 반환EnsureCapacity(int min): 최소 min 용량이 되도록 버퍼 크기 조정Capacity 프로퍼티: 현재 버퍼 최대 크기 조회/설정Length 프로퍼티: 현재 버퍼에 저장된 문자 수 조회/설정class Program
{
static void Main()
{
// 1) 초기용량 지정 없이 생성 (기본 16)
StringBuilder sb = new StringBuilder();
// 2) Append, AppendLine
sb.Append("Hello");
sb.Append(' ');
sb.AppendLine("World!");
// sb: "Hello World!\r\n"
// 3) Insert
sb.Insert(5, ",");
// sb: "Hello, World!\r\n"
// 4) Replace
sb.Replace("World", "C#");
// sb: "Hello, C#!\r\n"
// 5) Remove
sb.Remove(sb.Length - 2, 2);
// sb: "Hello, C#!"
// 6) 최종 출력
Console.WriteLine(sb.ToString());
// 출력: Hello, C#!
}
}
오늘도 한시간정도 일찍 출석하여 공부를 진행했다. 어제와 마찬가지로 집중이 흐트러지지는 않았지만, 마음이 급해서인가, 아직 시간이 부족하다는 생각이 든다.
TRPG 프로젝트 JSON 파일 생성 및 저장/불러오기 기능까지 적용하는 것으로 마무리했다.
유니티 에디터를 사용하면 조금 더 직관적인 구현이 가능했을 것 같다는 생각은 들지만, C#을 통해서만 구현하다보니 지금까지 배운 C# 문법의 활용에 대한 전반적인 이해가 올라간 것 같다.
하지만 이후 배운 내용들에 대한 응용은 아직 많이 힘들다.
이런 프로젝트를 계속 반복하고 따로 또 만들어 보면서 특정 기능들을 어느 상황에서 사용해야 하고 사용하지 말아야 하는지에 대한 감각을 살리는 것이 중요하다고 생각한다.
강의를 통해 델리게이트, 람다 등의 이점과 한계를 모두 살펴보았다.
지금까지 프로젝트를 진행할 때는 "구현이 되느냐 마느냐"에 집중했는데, 이를 통해 "코드 가독성"과 "유지보수성" 등을 모두 신경써야 한다는 점을 다시금 깨달았다.
이 기능들도 복습하고 적용해보며 상황별로 적절하게 사용할 수 있도록 노력하겠다.
다음주부터 팀 프로젝트를 시작한다는 얘기가 도는데, 팀에 코딩을 많이 접해보신 분들도 계시고, 기획 쪽을 공부하신 분들도 계시다고 하여 내심 안심이 되긴하지만, 그 속에서도 반드시 내 역할을 드러내보여야 한다고 생각한다.
지금까지 배운 내용들을 적절히 적용해보는 것에 집중하고, 많은 커뮤니케이션과 리뷰를 통해 이번 기회를 많이 배울 수 있는 기회로 삼겠다.