이전에 비슷한 것을 해보았지만 더 깊이 들어가서 고민을 많이 한 시간이어서 더 좋았다. (특히 구조)
피드백(개인 과제)
개인 과제 수행을 바탕으로 피드백을 받을 수 있었는데 정말 피가 되고 살이 되는 조언들이었다. 대표적으로 내가 놓친 부분들(불필요한 주석, 불필요한 using문, 인터페이스 변수명 명확화) 등 관성으로만 또는 귀찮아서 안 한 것들이 코드 가독성을 해치게 된다는 사실을 알고 바로 적용하기로 했다. 나머지로 정보은닉이나 콜백 남용이 조금 있었다는 피드백이 있었는데 모두 팀 프로젝트에 체크하면서 적용하고 계속 습관화 시키도록 해야겠다.
다음 주 목표
다음 주는 팀 과제로 TextRPG를 팀원과 같이 구현하게 된다. 나 뿐만 아니라 다른 사람들과의 협업에서 더 많은 기능과 더 많은 아이디어를 구현할 수 있게 되어 좋다. 탄력은 받았으니 계속 달리기만 하면 될 것 같다. 다음 주도 파이팅!
오늘은 새로운 팀원과 함께 하는 TextRPG 구현의 기반을 다졌다. 저번에 해봤던 Git으로 하는 미니 팀 프로젝트와 개인 과제로 주어진 TextRPG를 수행하고 나니까 작업이 훨씬 더 빠르게 진행 되어서 좋았다. 또한 이미 구현한 것을 더 좋게 만드는 과정도 더 깊은 고민을 할 수 있게 해서 더 좋은 시간이었다.
내일은 아마 다른 팀원분들이 구현한 것과 합치고 수정하는 작업 + 소비 아이템 관련해서 작업이 예상되는데 재미있을 것 같다. 내일도 열심히 해보자!
리팩토링을 하기 전에 구조 부터 문제라는 생각이 들어 간단한 시스템 흐름도를 만들어 보았다.
아래 그림으로 보면 별 문제가 없어보이지만..
문제 1 : 너무 많은 gameState
모든 상태를 gameState에 넣어놓고 현재 gameState에 따라 함수로 진행하는 방법이다.
이러면 분기가 하나 늘어날 때마다 gameState가 많아질 것이며 코드의 복잡도가 더 증가할 것이라고 판단했다.
이것에 대한 내가 생각한 아이디어 중 하나는 다음 단계로 이동할 때 현재의 씬을 스택에 담는 것이다.
스택에 담는다면 나가기 버튼을 눌렀을 때도 이전 씬을 담으니까 그 씬을 돌려주면 된다.
// 현재 게임 상태
GameState gameState;
// UI랑 게임 플레이랑 나눌 필요성이 있어보임
// 문제점 : 게임 스테이트가 너무 많음
// 아이디어 1: 연결 되어있는 것이라면 Stack으로 씬을 담는 것
public enum GameState
{
Town,
State,
Inventory,
Equipment,
ItemSort,
Shop,
Purchase,
Sell,
Dungeon,
INN,
}
public void Start()
{
JsonUtility.Save(character.Info);
switch (gameState)
{
case GameState.Town:
EnterTown();
break;
case GameState.State:
ShowState();
break;
case GameState.Inventory:
ShowInventory();
break;
case GameState.Equipment:
ManageEquiment();
break;
case GameState.ItemSort:
ItemSort();
break;
case GameState.Shop:
ShowShop();
break;
case GameState.Purchase:
PurchaseItem();
break;
case GameState.Sell:
SellItem();
break;
case GameState.Dungeon:
EnterDungeon();
break;
case GameState.INN:
EnterINN();
break;
default:
break;
}
}
gameState를 메인에서 선택하는 5가지로 줄이니 우측 스크롤 바 쪽에 빨간줄을 여럿 볼 수 있다. ㅠㅠ
나의 과오니 내가 치워야지..
* 추가 : 어차피 맨 처음에 마을으로 가니까 gameState가 꼭 필요하지 않다고 판단해서 gameState 관련 로직을 제거하게 됐다.
gameState 관련 로직을 빼도 나가기가 잘 되는 모습
문제 1-1 : 호출 스택 문제 (스택 오버 플로우)
만들면서 뭔가 쎄한 느낌이 들어서 확인해보니 마을로 가지 않고서는 호출 스택을 탈출할 수 없다는 문제가 있었다.
'함수 호출으로 넘어가는 것이 문제인가?' 라는 생각이 들어 바로 적용해보기로 했다.
그럼 그렇지 내가 너무 복잡하게 생각해서 돌아가는 경우였다. 그냥 return 하면 되는 쉬운 방법이 있었다니!
함수에서 다른 함수로 이동할 때 다시 이전 함수로 돌아가는 경우라면 그냥 return을 쓰면 된다. 어렵게 생각하지 말자!
문제 2 : gameState와 맞물려 있는 안내 UI
일단 만들고 나중에 고치자고 생각했지만 만들면서도 계속 반복되는 게 많아 불편한 마음이 들었던 UI도 문제였다.
아래의 2개의 코드는 각각 마을과 아이템 장착 관리인데 비슷한 부분이 많아 변수를 넘겨주는 방식으로 통합하려 한다.
Console.Write
(
"스파르타 마을에 오신 여러분 환영합니다\n" +
"이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.\n\n" +
"1. 상태보기\n" +
"2. 인벤토리\n" +
"3. 상점\n" +
"4. 던전입장\n" +
"5. 휴식하기\n" +
"\n" +
"원하시는 행동을 입력해주세요.\n" +
">> "
);
Console.Write
(
titleStr +
"보유 중인 아이템을 관리할 수 있습니다.\n\n" +
"[아이템 목록]\n" +
itemListStr +
choiceStr +
"원하시는 행동을 입력해주세요.\n" +
">> "
);
이전엔 Item 클래스를 상속 받는 객체를 따로 생성하여 받는 방식으로 구현 하였는데 객체가 생길 때마다 생성 해야 되는 문제가 있었다.
이미 Item 클래스에 SetInfo라는 함수가 있으니 생성자로 접근하여 세팅하도록 바꾸려고 한다.
불필요한 객체 생성을 줄이고 생성자로 정보를 넘겼다.
public ItemShop()
{
ItemList.AddRange(new Item[]
{
new Item(ItemType.Armor, "수련자 갑옷", 0, 5, "수련에 도움을 주는 갑옷입니다.", 1000),
new Item(ItemType.Armor, "무쇠갑옷", 0, 5, "무쇠로 만들어져 튼튼한 갑옷입니다." , 600),
new Item(ItemType.Armor, "스파르타의 갑옷", 0, 15, "스파르타의 전사들이 사용했다는 전설의 갑옷입니다.", 3500),
new Item(ItemType.Weapon, "낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.", 600),
new Item(ItemType.Weapon, "청동 도끼", 5, 0, "어디선가 사용됐던 것 같은 도끼입니다.", 1500),
new Item(ItemType.Weapon, "스파르타의 창", 7, 0, "스파르타의 전사들이 사용했다는 전설의 창입니다.", 2400)
});
}
문제 4 : 변수 접근 제한
이전에 그냥 Get Set 프로퍼티로 접근했는데
데이터 저장을 위해 프로퍼티로 넘긴다고 하면 Set은 Private로 해주고 실수 방지를 위해 변수가 아닌 함수로만 통해 접근하는 것이 좋아 보였다.
구조체도 인터페이스 상속이 가능하다는 것을 이번에 튜터님께 질문 하면서 알게 되었다.
추가로 함수도 사용 가능하다.
public struct ItemInfo : IItem
{
public ItemType Type { get; private set; }
public string Name { get; private set; }
public int AttackModifier { get; private set; }
public int DefenseModifier { get; private set; }
public string Description { get; private set; }
public int Price { get; private set; }
public bool IsEquiped { get; private set; }
public void SetInfo(ItemType type, string name, int attack, int defense, string description, int price, bool isEquiped)
{
Type = type;
Name = name;
AttackModifier = attack;
DefenseModifier = defense;
Description = description;
Price = price;
IsEquiped = isEquiped;
}
public void Equip(bool isEquip)
{
IsEquiped = isEquip;
}
}
오늘의 회고
오늘은 리팩토링 하는 시간을 가졌다. 리팩토링을 끝내고 리드미 파일도 정리하고 시간이 남으면 알고리즘도 공부해보려 했는데 시간이 부족해 리팩토링도 다 하지 못했다. 아쉽지만 오히려 혼자 고민 해본 것과 튜터님께 질문 드린 것을 통해 배운 것이 많아서 아쉬울 건 없는 하루였던 것 같다.
내일은 리팩토링(데이터 저장, 상태 콜백, 클래스 나누기)를 하고 리드미 파일을 작성하려고 한다. 내일도 파이팅!
시도한 것 : 저장할 때 JsonSerializer.Serialize(클래스)로 불러오려고 했다.
문제 : JsonSerializer.Serialize는 클래스로 불러오면 클래스 안의 구조체까지는 접근하지 못한다. 또한 프로퍼티 값이어야 읽어올 수 있다.
해결 : 사용할 때 JsonSerializer.Serialize(클래스명.클래스구조체)로 세이브 했다.
public struct CharacterInfo
{
public int Level { get; set; }
public string Name { get; set; }
public float Attack { get; set; }
public float Defense { get; set; }
public float Health { get; set; }
public float Gold { get; set; }
public bool IsDead { get; set; }
public float MaxHealth { get; set; }
public float DefaultAttack { get; set; }
public float DefaultDefense { get; set; }
}
class JsonUtility
{
public static void Save<T>(T t)
{
string json = JsonSerializer.Serialize(t);
File.WriteAllText(@"D:\data.json", json);
}
public static T Load<T>(string path)
{
var json = File.ReadAllText(path);
if (json != null)
{
return JsonSerializer.Deserialize<T>(json);
}
else
{
return default(T);
}
}
}
static void Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Green;
Game game = new Game();
// TODO : 추후 캐릭터 선택창 만들 것
// 시작 : 캐릭터를 선택하시오
// 끝 : 캐릭터 인스턴스 생성
character = new Warrior();
var info = JsonUtility.Load<Character.CharacterInfo>(@"D:\data.json");
if (info.Name != null)
{
character.Info = info;
}
while (true)
{
game.Start();
}
}
개인 과제 질문 피드백
기능 구상이 어려울 때
- 처음 기능이 끝까지 갈 수가 없음 - 나중에 바뀌기 마련 - 개방 폐쇄 원칙(Open Close) : 확장에는 개방 수정에는 폐쇄 - 플로우 차트 만들어보기 (와이어 프레임)
변수 정하기
- 왼쪽과 오른쪽은 기획에서 달라질 수 있는 부분이다.
- 왼쪽이 필요한 경우라면 공격력이 필요한 방어구(가시방패)가 필요 한다던지 하는 경우
- 오른쪽은 그런 것이 필요하지 않을 때
가장 중요한 것은 왜 이 방식을 선택했는지에 대한 근거가 필요하다.
클래스를 어떻게 나누는가?
- 외부로 둔다면 영향을 줄 수 있는 범위가 커진다. - 최대한 분리한다면 영향을 적게 준다. 하지만 접근하는데 불편할 수 있다.
- 이것도 상황 판단 하에 정하기 -> 장단점을 파악하면서 사용
추가 : if else와 try catch문 어떨 때 쓰는 게 좋은가
- 입력을 받았을 때 입력이 예상 가능하면 if문 - 입력이 예상 불가능 하면 try catch문
오늘의 회고
오늘은 TextRPG 구현을 마무리하고 제출하는 날이었다. 시간이 촉박했기에 부랴부랴 만드느라 코드에 대한 고민이 조금 부족했던 것 같다. 다른 사람의 코드도 보면서 좋은 방법들이 있다면 질문해보고 내 것에 적용하는 시간(리팩토링)을 가져야겠다. 팀 프로젝트도 TextRPG의 연장선으로 진행한다고 하는데 좋은 방법들을 많이 적용하도록 하고 글로 또 남겨서 헷갈릴 때마다 다시 보고 해야겠다.
내일은 이전에 언급했다싶이 리팩토링을 진행할 생각이다. 또한 깃허브에 ReadMe파일에 정리하는 시간을 가지도록 해야겠다. 내일도 파이팅!
해결 : Console.ForegroundColor = ConsoleColor.Green;을 사용하여 콘솔 색을 초록색으로 바꾸었다.
오늘의 회고
오늘은 상점 구매와 판매, 정렬을 구현하고 줄맞춤 버그를 수정했다. 정렬에서 Comparison을 사용하는데 좀 걸렸지만 int 값을 반환하는 것을 알고 내림차순으로 쉽게 정렬할 수 있었다. 줄 맞춤 버그는 어제부터 시달렸던 과제인데 한글이 2바이트를 먹는다는 것과 한글을 검출해서 공백에 패딩을 한 자리 주는 것으로 해결할 수 있었다.
내일은 장착 개선, 던전입장, 휴식, 레벨업, 게임 저장을 해볼 생각이다. 이를 내일 제출(오후 6시)까지 구현하는 것은 어려울 수 있지만 최대한 구현해 볼 생각이고 이후로도 이번주 안에는 작업할 생각이다. 내일 열심히 해서 최대한 달성해보자. 내일도 파이팅!
문제 : 오늘은 인벤토리 구현 중에 문제를 맞닥뜨렸다. 그것은 아이템 목록을 정렬하는 것인데 여러 아이템을 정렬된 상태로 출력 하는 것이었다.
시도한 것 1 : 처음엔 그냥 저번에 배웠듯이 PadRight를 사용하기로 했다. 하지만 아래에 나와있듯이 자꾸 무쇠 갑옷이 삐져나오는 문제가 생겼다.
찾아본 원인 : C#에서는 한글과 다른 문자들의 바이트 크기가 달라서 생기는 문제라는 것이다. (한글 2바이트, 다른 것 1바이트)
또 다른 문제 : 하지만 생각했던 대로 바이트 개수가 나오지 않았다. (한글 3바이트, 다른 것 1바이트)
원인 : 이는 Encoding.Default가 UTF-8로 잡혀있어서 한글은 3바이트로 나오던 것이다.
시도한 것 2 : 그래서 Encoding.Unicode로 지정했더니 모든 문자가 2바이트로 나오게 되었다.
마지막 문제 : 한글이 2문자를 차지하니 한글과 공백을 구분하는 로직이 필요하다.
시도한 것 3 : 한글을 EncodingDefault.GetByteCount == 3인지 검사해 개수를 세고 -1한 값을 패딩을 줄 전체 바이트 길이에서 빼줬다. (한글이 2글자 공백이 1글자를 차지하므로 공백일 시 패딩을 1개씩 늘림)
string ArrangeInfo(string str, int totalLength, char padChar = ' ')
{
// 한글 2바이트
int krByteLength = totalLength;
int byteCount = Encoding.Unicode.GetByteCount(str);
int krCount = 0;
string[] strArr = str.Split();
foreach (var c in str)
{
// 한글이면 count++;
if (Encoding.Default.GetByteCount(c.ToString()) == 3)
{
krCount++;
}
}
// 정렬이 안 되는 문제 발생
// 2바이트니 / 2 할 때 한글의 개수가 홀수면 1 짝수면 2씩 감소시키기
// 12
//byteCount = krCount * 2;
// 공백, 영어, 숫자 1바이트
// 한글은 2바이트 크기는 2배
// 한글 10글자 기준 30바이트
// 한글 4개 12바이트
// 8 8 10 10 14
//int bytes = Encoding.Unicode.GetByteCount("무쇠갑옷");
//int bytes1 = Encoding.Unicode.GetByteCount("낡은 검");
//int bytes2 = Encoding.Unicode.GetByteCount("린넨 셔츠");
//int bytes3 = Encoding.Unicode.GetByteCount("나무 망치");
//int bytes4 = Encoding.Unicode.GetByteCount("스파르타의 창");
// * 조심 : Default시 UTF-8로 잡혀서 3바이트로 나옴
// int bytes = Encoding.Default.GetByteCount("무쇠갑옷");
string tempStr = str.PadRight(totalLength + totalLength - krCount, padChar);
return tempStr;
}
오늘의 회고
오늘은 인벤토리와 장착 기능 구현을 마무리 했다. 추가적으로 구현할 기능들이 여럿 보이지만 일단 선택 구현 사항부터 마무리 지으려고 한다. 오늘은 문자열 정렬 쪽에서 시간을 많이 뺏긴 것 같은데 그만큼 내가 모르고 있었다는 것이니까 다음 번에는 틀리지 않도록 다시 공부해봐야겠다.
내일은 콘솔 쪽을 꾸미고 인벤토리를 정렬하는 부분을 좀 해봐야겠다. 정렬 쪽은 특히나 게임 쪽에서 자주 나오는 개념이니 만큼 모른다면 확실히 알아가고 구현이 가능하다면 어떻게 하면 더 나은 구현 방법일지를 고민해봐야겠다. 내일도 파이팅!