오늘은 C#으로 던전을 떠나기 전 마을에서 무기를 구하는 콘셉트의 Txt 게임을 만들었다.
[ 변수 참조하기 ]
문제점
public static void State()
{
int gold = 1500;
Console.WriteLine($"Gold: {gold} G\n");
}
public static void Store()
{
Console.WriteLine("[보유 골드]");
Console.WriteLine("{gold} G\n"); //에러
}
Store 메서드에서 State 메서드에 있는 gold를 참조하고 싶은데, 계속해서 에러가 났다.
시도한 것
public static void Store()
{
int state = State();
}
객체를 생성해서 참조하려고 했지만, State 메서드가 반환값 타입이 void라서 int 값을 가져올 수 없었다.
해결 방법
static int gold = 1500;
State 메서드 밖으로 빼고 static 키워드를 추가했다. 값에 변화를 주는 다른 변수들도 클래스 레벨로 이동했다.
그런데 전부 static으로 하는 것이 과연 정답일까? 당장은 해결이 됐지만, 아직 의문이 남는다.
알게 된 것
static은 클래스 레벨에서 정의할 수 있다.
static 키워드를 통해 모든 객체들이 공유하는 하나의 인스턴스가 돼서, 전역적으로 접근이 가능하다.
따로 객체를 생성해서 접근하지 않고, static 키워드를 통해 Program.gold로 접근할 수 있다.
[ 클래스 나누기 ]
문제점
변수를 모두 static으로 지정하고, 모든 메서드가 Program 클래스 하나에 있으려니 구분이 잘 되지 않았다.
시도한 것
클래스를 Program, GameManager, StateManager, InventoryManager, StoreManager로 나누고, 각 메서드를 맞는 클래스 내부로 옮겼다.
해결 방법
using System;
namespace TxtGame
{
internal class Program
{
static void Main(string[] args)
{
GameManager gameManager = new GameManager();
gameManager.StartGame();
}
class GameManager
{
public int level = 1;
public int gold = 1500;
public int attackP = 10;
public int defenseP = 5;
public int hp = 100;
public void StartGame()
{
중략~
}
}
class StateManager
{
public static void State(GameManager game)
{
중략~
}
}
class InventoryManager
{
public static void Inventory(GameManager game)
{
중략~
}
}
class StoreManager
{
public static void Store(GameManager game)
{
중략~
}
}
}
}
각 클래스에 맞는 변수와 메서드를 옮겨 닮아서 정리했다. 그리고 Main 메서드에서 GameManager에 싱글톤 패턴을 구현했다.
알게 된 것
유니티에서 역할 구분에 따른 스크립트를 나누는 것처럼, 하나의 스크립트에서도 클래스를 나누어서 작성할 수 있다.
클래스를 나누면 내용 구분과 참조가 쉬워진다.
싱글톤을 해야 스크립트 내 전역 참조가 가능하다.
[ 메인 화면으로 돌아가기 ]
문제점
상태 보기, 인벤토리, 상점에 갔다가 나가기를 선택하면 다시 메인 화면으로 돌아와야 하는데, 되돌아가는 로직을 어떻게 구현해야 할지 몰랐다.
시도한 것
싱글톤한 GameManager의 StartGame 메서드를 불러와서 다시 돌아갔다.
해결 방법
public static void Store(GameManager game)
{
중략~
switch (input)
{
case "0": //나가기 → 메인화면 복귀
Console.Clear();
game.StartGame();
break;
}
}
알게 된 것
매개 변수로 GameManager의 객체를 받아 다른 클래스의 메서드에 접근할 수 있다.
[ 같은 클래스 내 다른 함수 호출 ]
문제점
class StoreManager
{
public static void Store(GameManager game)
{
중략~
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
}
}
public static void Item(GameManager game)
{
중략~
string item1 = $"- {name[0]} | {pType[0]}+{power[0]} | {explain[0]} | {price[0]} G";
Console.WriteLine(item1); //item6까지 존재
}
Store 메서드에서 Item 메서드에 있는 출력 코드를 참조하는 것이 불가능했다.
시도한 것
//Item 메서드
public string item1 = $"- {name[0]} | {pType[0]}+{power[0]} | {explain[0]} | {price[0]} G";
Item 메서드의 변수에 public을 붙였지만, 로컬 변수에는 접근 제한자를 사용할 수 없었다.
//Store 메서드
Console.WriteLine(Item(game)); //1번 방법
Console.WriteLine(Item(item1)); //2번 방법
Console.WriteLine(item1); //3번 방법
string list = Item(game); //4번 방법
string item1 = $"- {name[0]} | {pType[0]}+{power[0]} | {explain[0]} | {price[0]} G"; //5번 방법
Console.WriteLine(item1);
1번 방법은 '인수: void에서 bool로 변환할 수 없습니다'라는 에러가 떴다.
2번 방법은 'item1 이름이 현재 컨텍스트에 없습니다'라는 에러가 떴다.
3번 방법은 'item1 이름이 현재 컨텍스트에 없습니다'라는 에러가 떴다.
4번 방법은 '암시적으로 void 형식을 string 형식으로 변환할 수 없습니다'라는 에러가 떴다.
1, 4번은 Item 메서드가 아무것도 반환하지 않는데, 반환값을 받으려고 해서 발생했다.
2, 3번은 Store 메서드 내에 item1 변수가 없어서 발생했다.
5번 방법으로 코드를 복제해서 당장의 문제는 해결했지만, 코드가 복잡해지고 수정하는 데 어려움이 있었다.
해결 방법
public static void Store(GameManager game)
{
Item(game);
}
Item 메서드는 그대로 두고, Console.WriteLine() 없이 그냥 함수를 호출한다.
알게 된 것
출력값이 있는 함수를 호출하기만 하면 호출된 메서드 내의 출력값이 표시된다.
[ 아이템 앞에 번호 붙이기 ]
문제점
아이템 구매를 선택했을 때 아이템 정보를 담고 있는 Item 메서드에서 번호가 보이게 코드를 수정하면, Item 메서드를 참조하고 있는 Store 메서드에도 영향이 간다. 출력 순서는 Store가 더 빠르기 때문에, 아이템 구매를 선택하기도 전에 선택을 위한 번호가 먼저 생성된다.
시도한 것
public static void BuyItem(GameManager game)
{
중략~
Console.WriteLine("[아이템 목록]");
//Item 메서드 전체 복제 후 번호 추가한 코드
중략~
}
아이템 정보 전체를 복제해서 아이템 구매를 선택했을 때 새로 생성한 BuyItem 메서드로 이동하게 했더니, 코드가 복잡해지고 중복되는 코드가 너무 많아졌다.
if (itemNum)
{
for(int i = 0; i < num.Length; i++)
{
string itemInfo[i] = $"- {num[i]}"; //에러
}
Console.WriteLine($"- {num[0]} {item1}");
Console.WriteLine("-" + num[1] + item2);
}
인자로 불리언 값을 받아서 온오프 기능을 구현하려고 했는데, 반복문을 통해 여러 변수들을 선언 및 초기화하는 방법을 찾지 못했다. for문 안의 itemInfo[i]에서 '변수 선언에는 배열 크기를 지정할 수 없습니다'라는 에러가 뜬다.
해결 방법
public static void Store(GameManager game)
{
중략~
Item(game, false);
switch (input)
{
case "1":
Console.Clear();
BuyItem(game);
break;
}
}
public static void Item(GameManager game, bool itemNum)
{
중략~
int[] num = { 1, 2, 3, 4, 5, 6 };
string[] items = { item1, item2, item3, item4, item5, item6 };
if (itemNum)
{
for(int i = 0; i < items.Length; i++)
{
Console.WriteLine($"- {num[i]} {items[i]}");
}
}
else
{
for (int i = 0; i < items.Length; i++)
{
Console.WriteLine($"- {items[i]}");
}
}
}
public static void BuyItem(GameManager game)
{
중략~
Item(game, true);
}
Store 메서드에서는 불리언 값을 false로 해서 번호가 나타나지 않게 하고, BuyItem 메서드에서는 true로 해서 번호가 보이게 했다. 그리고 배열을 이용해서 번호와 아이템 변수들을 담아 놓은 변수를 준비하고, 반복문에서 인덱스를 이용해 포맷팅 한다.
알게 된 것
포맷팅에서도 인덱스를 사용할 수 있다.
인수에 불리언 값을 받아서 조건문을 통해 온오프를 할 수 있다.
[ 아이템 정보 참조하기 ]
문제점
아이템 구매 후 인벤토리로 옮기기 위해서 아이템을 참조해야 하는데, 불가능해서 다른 변수들처럼 GameManager 클래스로 옮겼지만, game.을 삭제하지 않았을 때는 'game 이름이 현재 컨텍스트에 없습니다.'라는 에러가 뜨고 삭제하면 '필드 이니셜라이저는 static이 아닌 필드, 메서드 또는 Program.GameManager 속성을 참조할 수 없습니다.'라는 에러가 뜬다.
시도한 것
class InventoryManager
{
public static void InventoryList(GameManager game)
{
string[] item = new string[6];
}
}
class StoreManager
{
public static void BuyItem(GameManager game)
{
중략~
else if (StateManager.gold >= int.Parse(StoreManager.price[itemIndex]))
{
StateManager.gold -= int.Parse(StoreManager.price[itemIndex]);
InventoryManager.items = StoreManager.items[itemIndex]; //에러
price[itemIndex] = "구매완료";
Console.Clear();
Console.WriteLine("구매를 완료했습니다.\n");
BuyItem(game);
}
}
InventoryList 메서드에 item 배열을 만들고, 할당하려고 했지만 '암시적으로 string 형식을 System.Collections.Genaric.List<string> 형식으로 변환할 수 없습니다'라는 에러가 떴다.
해결 방법
class InventoryManager
{
public static List<string> items = new List<string>();
public static int[] num = { 1, 2, 3, 4, 5, 6 };
public static List<string> wear = new List<string>();
}
class StoreManager
{
public static void BuyItem(GameManager game)
{
중략 ~
if (input == "0")
{
Console.Clear();
Store(game);
}
else if (int.TryParse(input, out int itemNum) && itemNum >= 1 && itemNum <= StoreManager.price.Length)
{
int itemIndex = itemNum - 1;
if (StoreManager.price[itemIndex] == "구매완료")
{
Console.Clear();
Console.WriteLine("이미 구매한 아이템입니다.\n");
BuyItem(game);
}
else if (StateManager.gold >= int.Parse(StoreManager.price[itemIndex]))
{
StateManager.gold -= int.Parse(StoreManager.price[itemIndex]);
InventoryManager.items.Add(StoreManager.items[itemIndex]);
price[itemIndex] = "구매완료";
Console.Clear();
Console.WriteLine("구매를 완료했습니다.\n");
BuyItem(game);
}
}
아이템 정보는 StoreManager 클래스에 그대로 두고, BuyItem 메서드에서 아이템을 구매하면 InventoryManager 클래스에 있는 items 리스트에 요소를 추가하는 코드를 작성했다.
알게 된 것
데이터를 바로 다른 클래스나 메서드로 보내는 것이 아니라, 리스트나 배열 등을 통해 전달하는 것이 효과적이다.
[ static 참조하기 ]
문제점
'static이 아닌 필드, 메서드 또는 속성 '~'에 개체 참조가 필요합니다'라는 에러가 전반적으로 계속해서 나타났다.
시도한 것
class StateManager
{
public int level = 1;
public static void State(GameManager game)
{
StateManager state = new StateManager();
Console.WriteLine($"Lv. {state.level:00}");
}
}
State 메서드에 StateManager 인스턴스를 생성해서 변수를 불러왔다.
해결 방법
class StateManager
{
public static int level = 1;
public static void State(GameManager game)
{
Console.WriteLine($"Lv. {level:00}");
}
}
해당 메서드 말고 다른 메서드에서도 사용되는 변수이기 때문에, 변수 자체에 static을 붙여줬다.
알게된 점
class StoreManager
{
public void Store(GameManager game)
{
Console.WriteLine($"{StateManager.gold} G\n");
}
}
다른 클래스에서 참조하려면 '클래스명.변수명'을 사용하면 된다.
[ 아이템 구매 정보 표시 오류 ]
문제점
if (input == "0")
{
Console.Clear();
Store(game);
}
else if (int.Parse(input) >= 1 && int.Parse(input) <= 6)
{
for (int p = 0; p < StoreManager.price.Length; p++)
{
if (StoreManager.price[p] == "구매완료")
{
Console.Clear();
Console.WriteLine("이미 구매한 아이템입니다.\n");
BuyItem(game);
}
else
{
for (int g = 0; g < StoreManager.price.Length; g++)
{
if (StateManager.gold >= int.Parse(StoreManager.price[g]))
{
StateManager.gold -= int.Parse(StoreManager.price[g]);
StoreManager.price[g] = "구매완료";
Console.Clear();
Console.WriteLine("구매를 완료했습니다.\n");
BuyItem(game);
}
else
{
Console.Clear();
Console.WriteLine("Gold가 부족합니다.\n");
BuyItem(game);
}
}
}
}
}
골드가 500 있고 금액은 3500인데 계속해서 "이미 구매한 아이템입니다."라고 출력됐다.
시도한 것
아이템 구매 과정에서 디버깅을 통해 문제점을 찾아냈다. 어떤 번호를 입력해도 "이미 구매한 아이템입니다."라는 문구가 출력됐던 이유는, 여덟 번째 줄에서 for문을 통해 실행할 때마다 모든 번호에 대해서 반복하고 인덱스 0번이 제일 먼저 조건에 검사 되기 때문에 true가 떠서 "이미 구매한 아이템입니다."가 출력된다. 앞에서 이미 조건을 만족했으므로 if문을 빠져나와 다른 번호들은 검사 되지 않는다.
해결 방법
if (input == "0")
{
Console.Clear();
Store(game);
}
else if (int.TryParse(input, out int itemNum) && itemNum >= 1 && itemNum <= StoreManager.price.Length)
{
int itemIndex = itemNum - 1;
if (StoreManager.price[itemIndex] == "구매완료")
{
Console.Clear();
Console.WriteLine("이미 구매한 아이템입니다.\n");
BuyItem(game);
}
else if (StateManager.gold >= int.Parse(StoreManager.price[itemIndex]))
{
StateManager.gold -= int.Parse(StoreManager.price[itemIndex]);
StoreManager.price[itemIndex] = "구매완료";
Console.Clear();
Console.WriteLine("구매를 완료했습니다.\n");
BuyItem(game);
}
else
{
Console.Clear();
Console.WriteLine("Gold가 부족합니다.\n");
BuyItem(game);
}
for문을 제거해서 모든 아이템을 검사하는 로직을 없애고, 조건문을 이용해서 선택한 아이템만 실행하는 코드로 수정했다.
알게 된 것
for문을 이용하면 모든 아이템의 정보를 담아낼 수 있지만, for문과 if문을 중첩했을 때에는 앞에서 이미 조건을 만족하면 이후의 요소는 검사 조차 되지 않는다.
코드를 잘 수정하면 5중첩문에서 2중첩문까지로 줄이는 것도 가능하다.
[ 변수 값 업데이트 반영 ]
문제점
public static string[] price = { "1000", "구매완료", "3500", "600", "1500", "구매완료" };
public static string item1 = $"{StoreManager.name[0]} | {StoreManager.pType[0]}+{StoreManager.power[0]} | {StoreManager.explain[0]}
public static string[] items = { item1, item2, item3, item4, item5, item6 };
아이템을 구매하고 나서 price 변수에 구매한 아이템은 "구매완료"라고 바뀌는 코드가 실행되지 않았다.
데이터가 변수에 할당은 되지만 StoreManager가 새롭게 초기화가 되지 않아서 값이 변경된 상태로 출력되지 않는다.
시도한 것
변수에 있는 static 키워드를 제거해 봤더니 StoreManager.변수명 코드에서 위에서 봤던 'static이 아닌 필드, 메서드 또는 속성 '~'에 개체 참조가 필요합니다' 에러가 다시 발생하기 시작했다.
해결 방법
public static string[] items = new string[name.Length];
public static void ItemList(GameManager game, bool itemNum)
{
for (int num = 0; num < name.Length; num++)
{
items[num] = $"{StoreManager.name[num],-11}| {StoreManager.pType[num]}+{StoreManager.power[num],-5}| {StoreManager.explain[num],-30}| {StoreManager.price[num]} G";
}
}
StoreManager 리스트 내 ItemList 메서드에 반복문을 활용해서 전역변수 items에 할당했다.
알게 된 것
메서드 내부에서는 접근 제어자나 static 같은 키워드를 사용할 수 없다.
[ 아이템 착용 여부 표시 ]
문제점
아이템을 미착용하고 있을 때 아이템을 선택하면 [E]가 생성되고 착용했을 때 선택하면 [E]가 사라져야 하는데, 선택할 때마다 계속해서 [E]가 생겨났다.
시도한 것
//InventoryManager 클래스 Equip 메서드
if (input == "0")
{
Console.Clear();
Inventory(game);
}
else if (int.TryParse(input, out int itemNum) && itemNum >= 1 && itemNum <= items.Count)
{
int itemIndex = itemNum - 1;
Dictionary<string, string> wear = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> itemPair in wear)
{
if (!wear.ContainsKey(input))
{
Console.Clear();
items[itemIndex] = ($"- [E]{items[itemIndex]}");
wear.Add(input, items[itemIndex]);
Equip(game);
}
else
{
Console.Clear();
items[itemIndex] = ($"- {items[itemIndex]}");
wear.Remove(input);
Equip(game);
}
}
}
로직은 다음과 같다.
1. InventoryManager 전역변수로 키는 input 값, 값은 아이템 인덱스로 wear 딕셔너리를 생성한다.
2. Equip 메서드에서 foreach문을 이용해 wear 딕 셔너리를 한 번 훑어본다.
3. 만약 wear 딕셔너리에 input 값 키가 없다면, [E] 표시를 추가하고 wear 딕셔너리에 추가한다.
4. 있다면 [E] 표시를 제거하고 wear 딕셔너리에서 삭제한다.
해당 코드를 실행했을 때 몇가지 문제점으로 인해 제대로 작동하지 않았다.
1. 딕셔너리가 지역적이라 호출할 때마다 새로운 딕셔너리를 생성해서 이전의 값이 유지되지 않는다.
2. 그로 인해 foreach문도 항상 비어있는 딕셔너리를 검사하기 때문에 반복 자체가 일어나지 않는다.
3. [E] 표시 추가 및 제거 코드가 items[itemIndex] = ($"- [E]{items[itemIndex]}와 같이 [E]가 이미 추가된 값을 다시 불러와서 그 앞에 또 [E]를 추가하는 구조였다.
해결 방법
public static Dictionary<int, string> wear = new Dictionary<int, string>();
if (input == "0")
{
Console.Clear();
Inventory(game);
}
else if (int.TryParse(input, out int itemNum) && itemNum >= 1 && itemNum <= items.Count)
{
int itemIndex = itemNum - 1;
if (!wear.ContainsKey(itemIndex))
{
items[itemIndex] = "[E] "+items[itemIndex];
wear.Add(itemIndex, items[itemIndex]);
}
else
{
items[itemIndex] = items[itemIndex].Replace("[E] ", "");
wear.Remove(itemIndex);
}
Console.Clear();
Equip(game);
}
1. 딕셔너리에 값을 누적해서 변화시키기 위해 전역 변수로 설정했다.
2. foreach문을 삭제하고 if문으로 수정했다.
3. 비교값을 명확히하기 위해 키 값으로 input 대신 itemIndex로 변경했다.
4. [E] 표시를 나타낼 때 포맷팅을 사용하지 않고 추가 시에는 "[E] " 문자열 추가, 제거 시에는 "" 문자열로 대체했다.
5. 중복되는 코드인 Console.Clear();과 Equip(game);를 조건문이 끝난 뒤 일괄 실행되도록 수정했다.
알게 된 것
foreach문은 반복할 대상의 값이 없을 때는 아예 실행 자체가 되지 않는다.
포맷팅을 이용해서 변수에 자기 자신을 참조하여 값을 변경하는 경우, 기존 값에 변화를 추가하는 형식으로 결과가 업데이트된다.
[ 회고 ]
지금껏 강의나 구글링 등을 통해서 다양한 지식들을 배웠었는데, 이를 직접 코드로 구현하려니까 막상 알고 있던 것도 생각이 잘 나지 않고 그 지식을 어떻게 사용해야 할지도 마음처럼 잘 되지 않았다. 특히나 클래스와 메서드를 나누고, 각각 코드를 어떻게 배분해서 담아야 하는지 그 기준을 잘 모르겠어서 맨 처음에는 하나의 클래스에 모든 정보를 다 담았었다. 그러다 보니 에러가 점점 많아져서 먼저 크게 5개의 클래스로 나누고, 그 안에서 각각의 기능을 담은 메서드를 나누는 데 시간을 많이 쏟았다.
이번 게임을 만들면서 가장 크게 배운 것은 적절히 코드를 분배해야 한다는 것이었다. 이번에는 시간이 촉박하기도 했고 많은 양의 코드를 다뤄보는 것이 처음이라서 많이 헤맸지만, 좀 더 코드를 세분화해서 나누는 연습을 더 해봐야겠다는 생각이 들었다. 그러기 위해서 클래스나 메서드에 관한 강의를 듣고 다른 예제들을 많이 찾아봐야겠다.
그리고 아직 배열을 활용하는 것이 익숙하지 않아서 foreach나 list를 많이 사용하지 못했는데, 게임을 수정하면서 기존의 비효율적인 코드들을 수정해야겠다. 앞으로의 공부 계획은 하나의 지식을 터득하면, 이를 직접 코드로 구현해 보는 연습을 많이 하는 것이다. 아니면 이 텍스트 게임을 중점으로 새로운 지식을 얻을 때마다 더 효율적인 코드로 수정해 보는 것도 좋은 공부가 될 것 같다.
'Coding > C#' 카테고리의 다른 글
[내일배움캠프 11일차 TIL] 배열과 리스트 비교, 로직 설계 방법 (0) | 2024.04.29 |
---|---|
[내일배움캠프 10일차 TIL] 멤버, 클래스 상속, 유니티 라이프 사이클 (0) | 2024.04.26 |
[내일배움캠프 8일차 TIL] 비트 연산자, 시프트 연산, 단축 평가, 비트 플래그, 2진법 (0) | 2024.04.24 |
[내일배움캠프 7일차 TIL] 배열, 컬렉션, 메서드, 구조체, 조건문, 반복문 (0) | 2024.04.23 |
[내일배움캠프 6일차 TIL] C#, 객체 지향, 변수, 자료형, 형변환 (0) | 2024.04.22 |