팀 과제(콘솔 텍스트 던전 RPG) 진행률이 거의 막바지이다. 오는 월요일에 던전 내 전투 중 장면 구성을 손질하면 게임 내적인 부분은 완료가 되어있을 것 같다.
UI.cs 클래스를 새로 마련하여 플레이어의 선택지 패널, 아스키아트 프리셋, 텍스트와 콘솔에 관련된 편의기능을 압축한 메서드 등 여러가지를 구현하고, 디자인 개선을 마쳤다. 전후 이미지를 같이 보여주면 좋을 것 같은데 그렇게까지 하기에는 에너지가 모자라다. 기록으로 남기는게 무조건 좋을텐데 라고 생각 중.
매일 그렇지만 요즘 다른 하는 일이 없어서 그런지 팀 과제랑 알고리즘 풀기에 모든 에너지를 쏟고 있다. 수면 시간도 너무 적어져서 그런지 두통이 심해졌다.
2. 오늘 학습에 대해
팀 과제
Github Desktop으로 하루 종일 팀 과제를 같이하다보니 깃은 이제 수월하게 다루고 있는 것 같다. 특히 팀원들이 Pull Request를 수행하기 전 Main함수로부터 업데이트를 잘 한 뒤에 PR을 진행했기 때문에 충돌이 거의 없었던 편.
오늘은 UI의 끝을 보자고 생각하여(갑자기 그런 기분이 들어) 구현을 많이 하였는데, 그 내용은 아래와 같다.
1. 타이틀 구현과 전반적인 UI 스타일 구현
타이틀이 필요하다고 개인적으로 종종 생각이 들었었기에, 오늘 UI 구현을 하며 타이틀 장면도 마련 해 두었다.
아스키아트가 화면을 꽉 채우도록 하고, 특정 좌표로 커서를 옮겨 텍스트를 구현하였다. 이 상태에서 아무 키나 누르면
데이터를 불러온다는 안내 메시지를 잠시 보여준 후, 다음 장면으로 넘어간다. 가운데 정렬이 말끔해보여서 좋았다.
캐릭터 생성 장면은 더 화려하게 꾸밀 수 있었지만, 생성 단계는 처음이라는 느낌답게 조촐하게 보여주는 것도 좋지 않을까 생각하게 되어, 필요로 하는 간단한 텍스트만 표시하도록 하였다.
그래도 왼쪽에 여백을 두거나 줄바꿈을 두고, '>>>' 문자를 두는 등 가독성에 신경을 썼다. 뒤따라 나오는 직업 선택이나 확인 장면도 거의 동일
직업까지 선택을 하면 아래와 같은 메인 메뉴를 볼 수 있다.
메인 메뉴이지만 현재 아이디어 고갈로 검소한 디자인을 하고있다.
우측 빈 부분에 아트를 넣거나 하면 좋을듯.
신경쓰이는 점은 상단 아스키아트가 너무 긴데, 내가 작업한 부분이 아니라 건드리기 조심스러운 부분도 있고 마땅한 대체재를 생각해 두지 않았기 때문에 일단은 현상유지이다.
메인메뉴를 제외하면 모두 하단의 유저 입력 전용 패널과 동일한 폭으로 맞춰두었다. 예를 들어 회복아이템 메뉴는 아래와 같이 균형잡힌 배치를 가지고 있다.
유저에게 보여지는 장면의 크기에 맞추어 콘솔창의 크기도 줄여보는 시도를 했었지만, 어째서인지 콘솔 폭을 120자 미만으로 줄이게 되면 콘솔창 하단에 좌우 스크롤바가 생겨버리기 때문에 일단 콘솔창은 넓게 쓰고 있다.
메인 컨텐츠인 던전에 들어가는 입구 장면이다. 간단한 아스키아트를 넣어두었다.
E 키를 통해 내 정보를 열어볼 수 있는데(정보열기는 다른 팀원이 구현)
스테이터스는 밝은 노란색으로 표시되게 하여 가독성을 높였고, 길어진 높이만큼 빈 공간에 아트도 더 넣어두었다.
간단하게 줄여본 코드는 위와 같다. 그냥 적어둔 내용대로 Write를 하고 있기 때문에 복잡한 내용은 없다.
고양이 아트를 예로 든 DrawOnSpecificPos(string, int x, int y) 메서드는 특정 좌표를 지정하여 그릴 수 있도록 마련해둔 메서드이다. 그 내용은 아래와 같다.
(c) 특정 좌표에 그림을 그리는 DrawOnSpecificPos 메서드
public static void DrawOnSpecificPos(string s, int xInput, int yInput)
{
// 커서 위치를 저장해두어, Draw가 끝나면 커서 위치를 원상복귀
int ySave = Console.CursorTop;
int xSave = Console.CursorLeft;
int x = xInput;
int y = yInput;
string[] lines = s.Split('\n');
foreach (string line in lines)
{
Console.SetCursorPosition(x, y);
Console.Write(line);
y++;
}
Console.SetCursorPosition(xSave, ySave);
}
진행중이던 커서 위치는 저장해두고, 특정 위치를 기점으로 아트를 그린 후, 원래의 커서 위치로 되돌아온다. 이렇게 만들어진 것이 아래의 고양이와 캠프장.
(d) 텍스트에 색을 입히는 메서드들
public static void ColoredWriteLine(string s, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(s);
Console.ResetColor();
}
public static void ColoredWrite(string s, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.Write(s);
Console.ResetColor();
}
public static void RandomColoredWrite(string s)
{
Random random = new Random();
foreach (char c in s)
{
Console.ForegroundColor = (ConsoleColor)random.Next(9, 16);
Console.Write(c);
}
Console.ResetColor();
}
메서드 이름만큼 직관적으로, 그리고 한 줄로 사용할 수 있어 나름 편리할 메서드이다.
Console.Write 대신 사용할 수 있고, ConsoleColor.Red 와 같은 매개변수를 함께 넣어 색을 지정해준다.
// 단일 사용
UI.ColoredWriteLine("골드", ConsoleColor.Yellow);
// 연계 사용
Console.WriteLine($"골드를 {UI.ColoredWriteLine("500", ConsoleColor.Yellow);} 만큼 획득!");
그냥 사용해도 편하고, 더 좋은 점은 문자열 도중에 특정 단어만 색을 입히는 것이 편하다. 위 내용을 메서드 없이 깡으로 구현한다면 아래와 같이 매우 길어지게 된다.
위와 같이 double 형을 int 형으로 나눌 경우에는 오류가 발생하지 않음. 변수 간 포함관계에 따라서 허용되는 것으로 추정.
No.13[12931 자릿수 더하기]
public int solution(int n) {
int answer = 0;
string nStr = n.ToString();
foreach(char c in nStr){
answer += int.Parse(c.ToString());
}
return answer;
}
'문자열' 을 int형으로 바꿀 경우에는 int.Parse() 사용. 문자열이 아닌 문자 는 사용할 수 없기 때문에 .ToString() 메서드를 통해 문자열로 변환 해 주어야 한다.
foreach문은 foreach(자료형 이름 in nStr){ 순회 내용 } 과 같이 작성. in 키워드 말고도 다른 방식도 여럿 있을 것으로 생각되어짐.
No.16[12954 x만큼 간격이 있는 n개의 숫자]
public long[] solution(int x, int n) {
long[] answer = new long[n];
for(int i=1;i<=n;i++){
answer[i-1] = (long)i*(long)x;
}
return answer;
}
캠프에서 그제와 어제 알고리즘 특강을 진행하고, 오늘부터 하루 한개 이상 알고리즘 문제 풀기를 아침 09시부터 10시까지 자율적으로 진행한다. 새벽에 잠이 안와서 먼저 시작했었는데, 계속 풀다보니 40문항정도 제출 완료. 한 두 줄로 해결 할 수 있는 기초 알고리즘 문제부터 시작하기 때문에, 사실 지금 시점에 제출한 문항 수는 크게 의미는 없어 보인다.
팀 과제는 3일차인데 진행이 무척 빠른 것 같다. 하루 10시간씩 잡고있으니 당연하다면 당연한가 싶기도 하다. 스킬과 마나 시스템을 더 손보고, 나머지는 기존 기능에 대한 개선을 진행할 것으로 보인다.
2. 오늘 학습에 대해
오늘은 특별히 새로운 것을 배우거나 한 것은 없다. 개발파트가 순조로워 오늘 진행한 내용이 무척 많다. 오후 팀 스크럼에서는 내가 오늘 개발했던 내용도 많이 잊어버려 Github Desktop에 적혀있는 커밋 로그를 보며 프레젠테이션을 진행했다.
오늘 내가 진행한 커밋 내용은 아래와 같다.
Feat: Potion클래스 및 '회복 아이템' 장면 구현 - Item 을 상속받는 Potion 클래스 구현 - new Potion 객체로 [물] 추가(일단은 포션 역할로 사용, 포션 종류가 확장될 가능성이 있기 때문에 이름을 포션으로 두진 않음) - 메인메뉴 - 회복아이템 장면 구현
Feat: 던전 승리 시 포션 및 골드를 획득
Refactor: Potion 클래스 내 메서드 개편 및 해당 메서드를 사용했던 장면에서 코드 단축
Design: 회복아이템 아트 수정
Refactor: 경고문 시스템 도입
Feat: 던전 전투 중 회복약 사용 기능 구현 - 던전 전투 중 회복약 사용 기능 구현, isUseItem를 true상태로 주어 활성화. - 아이템 이름 [물]을 [회복약]으로 수정
Feat: 전투 중 치명타, 몬스터회피 실적용
그리고 오늘부터 알고리즘 문제를 풀어 하루 하나 이상씩 제출을 하는 시간을 갖게 되었는데, C#문법에서 헷갈리는 게 많아 많이 찾아보았는데, 이에 관해서도 이후에 문제를 다시 훑어보며 헷갈리는 부분을 정리 해 두면 좋을 것 같다.
구현할 기능별로 역할을 분담하여 진행하는 방식을 택하였다. 수정할 파일이 서로 겹치는 경우가 빈번할 것 같아 Merge도중 충돌이 많이 일어날 것 같았는데, 걱정과는 달리 엄청 순조로운 진행을 보였다.
나는 던전 전투 중 데미지 계산(일정 확률의 치명타, 일정 확률의 적 회피) 메서드의 작성을 먼저 맡아 완료한 후, Potion 클래스에 관해서도 새로 작성 한 후 포션과 관련된 게임 내 기능들을 작성중에 있다.
필수적인 요소는 모두 마무리가 되어 각자 선택요소 중 원하는 기능들을 구현하고 있다.
본인은 포션 관련한 기능을 모두 작성하면, 게임을 계속 돌려보며 디자인적으로 계속 개선해보는 역할도 맡았다.
2. 오늘 학습에 대해
오전에는 Stack, Queue, 정렬알고리즘(Quick 등)에 대한 내용의 특강이 있었다. 해당 요소들을 코드적으로 어떻게 구현하는지가 주된 내용이었는데, --index 처럼 전위연산을 수행한다던가 하는 자잘한 스킬로 구현을 더 쉽게 하는 부분이 인상적이었다. 함께 정리를 하고싶지만, 밤이 늦어 일단 팀 과제에 대한 내용을 먼저 주로 작성. 특강에 관해서는 복습한다던가 하면 이 포스트에 추가 작성을 할 것 같다.
데미지 계산(일정 확률의 치명타, 일정 확률의 적 회피) 메서드 구현
데미지 계산 메서드를 어떻게 구현할지 고민을 많이 하였고, 계속 정제를 한 끝에 편의성 측면에서 나름 정답을 찾은 것 같아 구현을 깔끔하게 할 수 있었다.
public (bool, bool, int) CalculateExDamage(int originDamage, bool isSkill)
{
// 입력: 원래의 데미지, 스킬사용여부
// 반환: 치명타 성공 여부, 회피 여부, 실 데미지
// 반환값 목록 선언 및 초기화
bool isCritical = false;
bool isDodged = false;
int calculatedDamage = originDamage;
// 치명타 계산
Random random = new Random();
double chance = random.NextDouble();
if (chance <= 0.15) // 15% 의 확률로 치명타 발생
{
isCritical = true;
calculatedDamage = (int)(originDamage * 1.6f); // 160% 데미지
}
// 회피 계산
chance = random.NextDouble();
if (chance <= 0.1 && !isSkill) // 스킬공격이 아닐 시, 10% 의 확률로 회피
{
isDodged = true;
calculatedDamage = 0; // 0의 데미지
}
return (isCritical, isDodged, calculatedDamage);
// (bool val1, bool val2, int val3) = player.CalculateExDamage(originDamage, isSkill); 와 같이 사용
}
계산된 데미지 수치 이외에도, 치명타가 터지거나 적 몬스터가 회피할 경우 전투 장면 내에 이와 관련한 텍스트를 보여주어야 했기 때문에, isCritical 과 isDodged 의 반환도 필요했다. 즉 여러 개의 변수를 한번에 리턴하고 싶은상황.
이러한 경우, 소괄호를 사용하여 return (값1, 값2, 값3); 과 같이 튜플값의 반환을 해 주는 걸로 해결을 할 수 있었다.
매개변수로는 int의 기본공격력, bool의 스킬사용여부를 받아와, 두 변수로부터 치명타와 회피를 계산하도록 하였다.
전투 장면에서는 아래 코드처럼 메서드를 호출, 세 개의 변수에 대입하여 사용 할 수 있다.
(bool isCrit, bool isDodge, int calculatedDamage) = player.CalculateExDamage(originDamage, isSkill);
포션 클래스 및 관련 기능 작성
Item을 상속받는 Potion 클래스를 아래와 같이 작성했다.
class Potion : Item
{
public int heal { get; private set; }
public int count { get; private set; }
public Potion(int id, string name, string desc, int price, int heal) : base(id, name, "체력회복", desc, price)
{
this.heal = heal;
this.count = 3;
}
public void UsePotionOnDungeon()
{
// 포션 사용 구현.
// 선택1. 플레이어 객체를 함께 받아, player.Hp += heal;
// 선택2. heal값을 반환하여 장면 내에서 구현하기
// 2번 선택. 이유가 있었는데, 밥먹다가 망각함... 곧 작성.
}
public void UsePotionOnInventory() => count--;
public void GetPotion(int n) => count += n;
}
메인 메뉴에서 포션을 사용할 시에는 UsePotionOnInventory(), 던전 내에서 포션을 사용할 시에는 UsePotionOnDungeon() 의 메서드를 사용하도록 설계하였다. 현재 계속 구현 중인 부분이라 미완성이다.
메인메뉴의 '회복 아이템' 메뉴로 진입하면 아래와 같은 사양의 장면을 구성한다.
즉 당장 요구되는 사양으로는 한 종류의 포션만 구현하는 것.
그렇기 때문에, items 배열에는 Potion클래스를 갖는 객체는 현재로써는 유일하기 때문에, 아래와 같이 FirstOrDefault() 메서드를 활용하여 포션 객체참조를 하였다.
// 정말 만약에 Potion 객체가 shop.items에 없을 경우를 위해 대비한 로직
// 참조 타입 변수에 null이 할당될 가능성이 있을 때 나오는 경고문 때문에 아래와 같이 라인 수 많아짐.
Potion? potion = shop.items.OfType<Potion>().FirstOrDefault();
if (potion != null)
{
Console.WriteLine($"포션을 사용하면 체력을 30 회복 할 수 있습니다. (남은 포션 : {potion.count}개)");
}
else
{
Console.WriteLine("게임에 포션이 구현되지 않아 갯수를 표시 할 수 없습니다.");
}
또한 아래와 같이 두줄 작성도 가능하긴 하였지만, 위 코드의 주석에도 적혀있듯 참조 타입 변수에 null이 할당 될 가능성이 있으면 보기 좋지 않은 경고문이 나오기 때문에 코드가 길어지더라도 위와 같이 작성하였다.
또한 아래처럼 작성하더라도 items에 Potion객체가 하나도 없다면 "0"을 출력할 것이기 떄문에 큰 문제는 없을 것으로 보인다.
어제까지 개인 프로젝트로 진행했던 콘솔 텍스트 던전 RPG 게임에 대해, 앞으로 일주일 간 팀 프로젝트로 기능구현을 추가로 진행한다. 필수 기능으로는 던전 내 배틀 시스템 구현, 선택적으로 이외 여러 기능을 구현하도록 가이드라인이 제시되었다.
결과물 제출 기한은 오는 주 같은 요일인 1.16(화)의 21시이다.
팀 회의에서 앞으로의 방향성을 위주로 회의를 하였다. 개인 프로젝트때와는 달리, 이번에는 커스텀은 최후순위에 두고, 필수 개발 구현에 집중하기로 하였다. 이전 팀 내에서 진행했던 코드 리뷰를 했던 느낌으로 결과물 발표를 하기로 일단 계획하였다. 또한 안내받은대로 팀 내 컨벤션을 착실히 두어, 코드 내 규칙과 Commit 메시지의 규칙, README의 작성도 스텐다드를 준수하도록 해 보기로 했다.
2. 오늘 학습에 대해
당연하다면 당연한 내용이지만 Git History 상 포함관계가 성립할 시, Merge 하였을 경우 Conflict는 발생하지 않는다는 것을 확인했다. 예를 들어, 아래와 같은 경우 BranchB와 Main에 다른 내용이 같은 라인에 적혀있지만, 컨플릭트가 발생하지 않는 예시.
Main : ""
// Branch 2개 생성
Main : ""
BranchA : ""
BranchB : ""
// A와 B 각자 편집, A가 Commit, Push 후 Main에 Merge.
Main : "Im A"
BranchA : "Im A"
BranchB : "B is me"
// B가 Update From Main을 하였고 Conflict 발생, B는 "Im A" 대신 "B is me"를 선택
Main : "Im A"
BranchA : "Im A"
BranchB : "B is me"
// B가 Commit, Push 후 Main에 Merge. -> 같은 라인에 다른 내용이지만 Conflict 없음.
Main : "B is me"
BranchA : "Im A"
BranchB : "B is me"
Main에 Merge된 BranchA의 로그 이후, BranchB의 히스토리에 "Im A"를 Deny하고 "B is me"를 선택했다는 기록 등이 남아 Conflict가 없었다고 생각해본다.
개인과제 제출을 마치고, 부족한 부분은 재제출을 하여 마무리를 하는 날이다. 본인은 C# 기초강좌 중 부족한 부분을 복습하며 하루를 보냈다. 하루종일 피곤하여 엎어져서 자거나 졸기도 했다.
2. 오늘 학습에 대해
제너릭, ref 및 out 키워드 그리고 인터페이스에 대해 조금 정리
제너릭
클래스 또는 메서드를 일반화시켜 여러 자료형에 대응할 수 있게 한다.
C#에서는 <T> 키워드를 이용. (T 대신 다른 문자나 문자열도 맞춰줘도 상관 없어 보인다.)
아래의 예시에서는 제너릭 클래스 Stack을 선언하여, int형을 관리하는 인스턴스를 만든다.
// 제너릭 클래스 선언 예시
class Stack<T, Ty> // 제너릭으로 사용할 문자열들을 명시
{
private T[] elements; // 자료형이 쓰일 자리에 사용
private int top;
public Stack() // Stack 생성자. Push와 Pop도 구현하여 스택기능을 구현
{
elements = new T[100]; // 역시 자료형 대신 제너릭 문자열을 대신 사용 가능. 100 크기의 배열을 준비
top = 0; // 현재 index
}
public void Push(T item) // Push 기능 구현. T형의 item을 매개변수로 전달받음
{
elements[top++] = item; // 현재 index에 입력받은 item을 저장하고, top을 1 증가(후위연산)
}
public T Pop() // Pop 기능 구현. T형의 리턴값
{
return elements[--top]; // top을 1 감소시킨 후 해당 위치의 원소를 반환한다.(원소 삭제의 구현은 없음)
}
public Ty TyTest(Ty tyInput) // 스택 기능은 아니지만 제너릭으로 여러개를 쓸 수 있는지 테스트
{
Ty tyElement = tyInput; // 입력받은 Ty형을 그대로 반환하도록 했다. 아래 예시에서는 string으로 객체를 생성
return tyElement;
}
}
static void Main(string[] args)
{
Stack<int, string> intStack = new Stack<int, string>(); // 객체 생성 시, 제너릭 클래스에서 요구하는 수의 자료형을 입력
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // 출력 결과: 3
Console.WriteLine(intStack.TyTest("나는 intStack이오.")); // 출력 결과: "나는 intStack이오."
}
ref와 out 키워드
매개변수 전달 시, 변수를 복사하는 것이 아닌 참조의 형태로 사용할 수 있게 한다
// ref 키워드 사용 예시
void Swap(ref int a, ref int b) // 매개변수로 두 변수를 전달받아 교환하는 메서드
{
int temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y); // x와 y를 참조형태로 전달한다
Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1
out은 ref와 비슷하지만, out 키워드를 사용한 변수를 메서드 내에서 값을 할당해주지 않으면 오류가 발생한다.
ref와는 달리 위와 같은 제약사항을 필요로 하는 경우에 사용.
// out 키워드 사용 예시
void Divide(int a, int b, out int quotient, out int remainder)
{
quotient = a / b; // quotient와 remainder에 값을 지정해주지 않으면 오류가 발생
remainder = a % b;
}
int quotient, remainder;
Divide(7, 3, out quotient, out remainder);
Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1
ref 와 out 매개변수는 값을 복사 없이 메서드 내에서 직접 접근을 할 수 있게 해주기 때문에 성능상의 이점이 있음.
out 매개변수는 메서드 내에서 반드시 값을 할당하기 때문에, 변수의 이전값이 유지되지 않음에 주의.
인터페이스
코드의 재사용성, 다중 상속 제공, 유연한 설계 등을 위해 인터페이스를 사용
인터페이스는 클래스가 구현해야 하는 멤버들을 정의함. 클래스의 일종이 아니며, 클래스에 대한 제약조건을 명시해주는 역할.
즉, 클래스가 인터페이스를 구현할(상속받을) 경우, 모든 인터페이스 멤버를 구현해야 한다.
구현 시, [interface 인터페이스명{구현할 내용}] 의 형태를 가지며, 인터페이스명의 이름은 보통 대문자 I로 시작하는 것을 추천함.
// 인터페이스 및 멤버를 정의하는 예시
interface IMyInterface
{
void Method1();
int Method2(string str);
}
// 인터페이스 구현
class MyClass : IMyInterface
{
public void Method1()
{
// 구현
}
public int Method2(string str)
{
// 구현
return 0;
}
}
오늘 팀원들끼리 각자 작성한 게임 코드를 리뷰하는 시간을 가졌는데, 각자의 스타일이 상상 이상으로 달라 충격을 받았다.
내 경우에는 한개 파일에 순차적으로 필요한 코드를 모두 때려박아 넣으며 최대한 주어진 사양(UI디자인)을 훼손하지 않는는 방향성을 가졌다.
팀원 한 명의 코드는 한 파일 안에 코드를 모두 작성하였지만 그 안에서도 다양한 확장안을 생각하고 여러가지 이유를 들어가며 생각치 못한 부분에서도 분리를 확실히 해 두었고, UI의 커스텀 디자인에도 상당히 신경 쓴 모습을 보여주었다.
다른 팀원의 코드는 나와 비슷하게 순차적으로 코드를 작성했지만 프로그래머의 입장에서 리뷰하기에 좋은 가독성 높은 코드를 작성하는데에 주력하는 것이 보였다.
또 다른 팀원의 코드는 장면과 기능을 담은 여러가지 파일을 구성하여 메인이 되는 코드 내에는 호출 하나만으로 그 씬이 모두 구현되게 하는 등 내가 보기에는 신기한 스타일을 하였는데, 이전에 윈도우 API로 게임을 만들어본 경험이 있어 이런 스타일을 쓴다고 하신다. 외에도 enum 을 한계까지 활용하는 모습 등 확장성에 극단적으로 좋은 모델로 보였다.
코드의 구성 외에도 아래와 같은 여럿 아이디어를 얻었다.
아스키 아트를 사용하여 그림을 넣기
종료 시에도 저장되도록 하기
데이터 저장형식을 json으로 하기
아이템에 고유 번호 매기기
체력을 0 아래로 내려가지 않게 하기
아이템의 정보를 저장할 데이터 파일을 따로 만들어두기
그리고 VSCode에서 C#을 사용할 수 있었다는 사실, Visual Studio의 파일 리스트 창을 Code처럼 왼쪽으로 옮길 수 있다는 사실 등 자잘한 팁도 여러가지 획득.
그리고 코드 리뷰에서 질문을 받으며 Linq 관련 질문을 받았는데, '링크...? 링크가 뭐지'하며 혼란 상태에 걸렸었는데, 나중에 찾아보고 Linq였구나 알게 되었다. 파이썬을 다룰 때에는 in이나 where 비슷한 것들은 너무 자연스럽게 사용하는 기능들이었기 때문에 C#강의의 Linq 파트를 빠르게 대충 넘겨 알아듣지 못했었는데, 이와 비슷하게 용어 관련으로는 많이 혼동이 오는 편. 예)프로퍼티, 필드 등
그 외에도 '싱글톤'이라는 것에 대한 개념도 없어 리뷰 타임에 그런것도 있구나 생각하며 듣고있었다.
싱글톤
특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이며, 생성자를 통해 여러 번 호출이 되더라도 인스턴스를 새로 생성하지 않고 최초 호출 시에 만들어두었던 인스턴스를 재활용하는 패턴이다.
3. 앞으로의 과제
오는 주차에는 이번 주차 개인 프로젝트를 베이스로 팀 프로젝트를 할 것 같다. 디자인이나 기능 개선 및 추가 등을 주 목적으로 예상하고 한 주간 팀 프로젝트를 진행한다. 잘 소통해가며 프로젝트를 무사히 마치기를 목표한다. 당장 구체적인 과제는 없고, 주말을 활용하여 C# 문법 후반부 복습을 할 예정이다.
나는 선택 기능 구현 목록을 오늘 마무리 하였고, 현재 디버그를 하며 코드 상의 어색한 부분을 계속 수정중이다.
선택 기능 구현 모두 무게가 있는 기능이라 모든 부분에서 힘을 쓰다 보니, 800줄 가까이 되는 코드를 작성했다.
오늘의 기록을 위해 영상을 찍으면서도 글자 색 지정을 안했거나 하는 자잘한 미스도 보였다.
2. 오늘 학습에 대해
기능 구현을 모두 마친 후 돌아보니, 기존 작성했던 코드에서는 존재하는 아이템을 모두 shopItems 리스트에 Item 클래스 형태의 객체로 추가 해 두고, inventory 리스트에 추가하거나 빼는 방식을 사용하고 있었는데, 앞으로 개발이 계속된다면 퀘스트로 얻는 아이템이나 몬스터 드랍템 등 Shop에서 취급하지 않는 아이템도 많이 생길 것이기 때문에, 전체 아이템을 관리하는 리스트인 gameItems를 사용하는 구조로 바꾸는데에 많은 시간을 쏟았다.
게임 구조
기본적으로 모든 씬은 while(true) 안에 두어 플레이어에게 잘못된 입력값을 받으면 그 씬이 다시 로드되도록 하였다.
using System;
using System.Collections.Generic;
namespace Chapter2
{
internal class Program
{
enum ItemType
{
Armor,
Weapon,
Potion
}
enum DungeonDifficulty
{
Easy,
Normal,
Hard
}
// 아이템 클래스 정의
class Item
{
public string Name { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }
public string Description { get; set; }
public bool IsEquipped { get; set; }
public int Price { get; set; }
public bool IsPurchased { get; set; }
public ItemType Type { get; set; }
public bool IsAvailableInShop { get; set; }
}
static int level = 1;
static string name = "말랑단단장";
static string job = "전사";
static float baseAttack = 10;
static int baseDefense = 5;
static int health = 100;
static int gold = 2500;
static int currentDungeonClears = 0; // 현재 레벨에서의 던전 클리어 횟수
// 인벤토리 리스트
static List<Item> inventory = new List<Item>();
// 상점 아이템 목록
static List<Item> gameItems = new List<Item>
{
new Item { Name = "수련자 갑옷", Defense = 5, Type = ItemType.Armor, Description = "수련에 도움을 주는 갑옷입니다.", Price = 1000, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "무쇠갑옷", Defense = 9, Type = ItemType.Armor, Description = "무쇠로 만들어져 튼튼한 갑옷입니다.", Price = 2000, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "스파르타의 갑옷", Defense = 15, Type = ItemType.Armor, Description = "스파르타의 전사들이 사용했다는 전설의 갑옷입니다.", Price = 3500, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "낡은 검", Attack = 2, Type = ItemType.Weapon, Description = "쉽게 볼 수 있는 낡은 검 입니다.", Price = 600, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "청동 도끼", Attack = 5, Type = ItemType.Weapon, Description = "어디선가 사용됐던거 같은 도끼입니다.", Price = 1500, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "스파르타의 창", Attack = 7, Type = ItemType.Weapon, Description = "스파르타의 전사들이 사용했다는 전설의 창입니다.", Price = 2500, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "힘의 비약", Attack = 1, Type = ItemType.Potion, Description = "마을 뒷편의 폭포수가 담기면 영험한 기운이 솟아납니다.", Price = 200, IsPurchased = false, IsAvailableInShop = true },
new Item { Name = "방어의 비약", Defense = 1, Type = ItemType.Potion, Description = "마을 우물의 물이 담기면 수호신의 기운이 깃듭니다.", Price = 200, IsPurchased = false, IsAvailableInShop = true }
};
static void Main(string[] args)
{
ShowMainMenu();
}
static void ShowMainMenu()
{
bool isRunning = true;
string alertMessage = "";
while (isRunning)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[스파르타 마을]");
Console.ResetColor();
Console.WriteLine("스파르타 마을에 오신 여러분 환영합니다.");
Console.WriteLine("이곳에서 던전으로 들어가기 전 활동을 할 수 있습니다.");
Console.WriteLine("");
Console.WriteLine("1. 상태 보기");
Console.WriteLine("2. 인벤토리");
Console.WriteLine("3. 상점");
Console.WriteLine("4. 던전 입장");
Console.WriteLine("5. 휴식하기");
Console.WriteLine("");
Console.WriteLine("6. 저장하기");
Console.WriteLine("7. 불러오기");
Console.WriteLine("8. 초기화");
Console.WriteLine("");
Console.WriteLine("0. 종료");
Console.WriteLine("");
Console.WriteLine("");
Console.Write(alertMessage);
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "1")
{
ShowCharacterStatus();
}
else if (input == "2")
{
ShowInventory();
}
else if (input == "3")
{
ShowShop();
}
else if (input == "4")
{
EnterDungeon();
}
else if (input == "5")
{
Rest();
}
else if (input == "6")
{
alertMessage = SaveGame();
}
else if (input == "7")
{
alertMessage = LoadGame();
}
else if (input == "8")
{
alertMessage = ResetGame();
}
else if (input == "0")
{
isRunning = false; // 게임 종료
Console.WriteLine("");
Console.WriteLine(" ¯\\_(ツ)_/¯ 게임을 종료합니다 ¯\\_(ツ)_/¯");
}
}
}
static void ShowCharacterStatus()
{
float totalAttack = baseAttack;
int totalDefense = baseDefense;
foreach (var item in inventory)
{
if (item.IsEquipped)
{
totalAttack += item.Attack;
totalDefense += item.Defense;
}
}
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[상태 보기]");
Console.ResetColor();
Console.WriteLine("캐릭터의 정보가 표시됩니다.\n");
Console.WriteLine($"Lv. {level.ToString("D2")}");
Console.WriteLine($"{name} ( {job} )");
Console.WriteLine($"공격력 : {totalAttack} (+{totalAttack - baseAttack})");
Console.WriteLine($"방어력 : {totalDefense} (+{totalDefense - baseDefense})");
Console.WriteLine($"체 력 : {health}");
Console.WriteLine($"Gold : {gold} G");
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "0")
{
break; // [스파르타 마을]로 돌아감
}
}
}
static void ShowInventory()
{
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[인벤토리]");
Console.ResetColor();
Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.");
Console.WriteLine("");
Console.WriteLine("[아이템 목록]");
if (inventory.Count == 0)
{
Console.WriteLine("보유한 아이템이 없습니다.");
}
else
{
for (int i = 0; i < inventory.Count; i++)
{
string stats = GetItemStats(inventory[i]);
string equipped = inventory[i].IsEquipped ? "[E]" : "";
Console.WriteLine($"({i + 1}) {equipped}{inventory[i].Name} | {stats} | {inventory[i].Description}");
}
}
Console.WriteLine("");
Console.WriteLine("1. 장착 관리");
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "1")
{
ManageEquipment();
}
else if (input == "0")
{
break; // [스파르타 마을]로 돌아감
}
}
}
static void ManageEquipment()
{
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[인벤토리 - 장착 관리]");
Console.ResetColor();
Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.");
Console.WriteLine("");
Console.WriteLine("[아이템 목록]");
for (int i = 0; i < inventory.Count; i++)
{
string stats = GetItemStats(inventory[i]);
string equipped = inventory[i].IsEquipped ? "[E]" : "";
Console.WriteLine($"{i + 1}. {equipped}{inventory[i].Name} | {stats} | {inventory[i].Description}");
}
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "0")
{
break; // [인벤토리]로 돌아감
}
else if (int.TryParse(input, out int itemIndex) && itemIndex > 0 && itemIndex <= inventory.Count)
{
Item selectedItem = inventory[itemIndex - 1];
if (selectedItem.Type != ItemType.Potion)
{
// 같은 타입의 다른 아이템을 해제
foreach (var item in inventory)
{
if (item.Type == selectedItem.Type && item != selectedItem)
{
item.IsEquipped = false;
}
}
}
// 선택한 아이템 장착 상태 토글
selectedItem.IsEquipped = !selectedItem.IsEquipped;
}
}
}
static void ShowShop()
{
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[상점]");
Console.ResetColor();
Console.WriteLine("필요한 아이템을 얻을 수 있는 상점입니다.");
Console.WriteLine("");
Console.WriteLine("[보유 골드]");
Console.WriteLine($"{gold} G");
Console.WriteLine("");
Console.WriteLine("[아이템 목록]");
foreach (var item in gameItems.Where(item => item.IsAvailableInShop))
{
string stats = GetItemStats(item);
string purchaseStatus = item.IsPurchased ? "구매완료" : $"{item.Price} G";
Console.WriteLine($"- {item.Name} | {stats} | {item.Description} | {purchaseStatus}");
}
Console.WriteLine("");
Console.WriteLine("1. 아이템 구매");
Console.WriteLine("2. 아이템 판매");
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "1")
{
PurchaseItem();
}
else if (input == "2")
{
SellItem();
}
else if (input == "0")
{
break; // [스파르타 마을]로 돌아감
}
}
}
static void PurchaseItem()
{
string alertMessage = "";
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[상점 - 아이템 구매]");
Console.ResetColor();
Console.WriteLine("필요한 아이템을 얻을 수 있는 상점입니다.");
Console.WriteLine("");
Console.WriteLine("[보유 골드]");
Console.WriteLine($"{gold} G");
Console.WriteLine("");
Console.WriteLine("[아이템 목록]");
for (int i = 0; i < gameItems.Count; i++)
{
string stats = GetItemStats(gameItems[i]);
string purchaseStatus = gameItems[i].IsPurchased ? "구매완료" : $"{gameItems[i].Price} G";
Console.WriteLine($"{i + 1}. {gameItems[i].Name} | {stats} | {gameItems[i].Description} | {purchaseStatus}");
}
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write(alertMessage);
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (int.TryParse(input, out int itemIndex) && itemIndex > 0 && itemIndex <= gameItems.Count)
{
Item selectedItem = gameItems[itemIndex - 1];
if (selectedItem.IsPurchased)
{
alertMessage = "이미 구매한 아이템입니다.\n\n";
}
else if (gold >= selectedItem.Price)
{
selectedItem.IsPurchased = true;
inventory.Add(selectedItem);
gold -= selectedItem.Price;
alertMessage = "구매를 완료했습니다.\n\n";
}
else
{
alertMessage = "Gold가 부족합니다.\n\n";
}
}
else if (input == "0")
{
break; // [상점]으로 돌아감
}
}
}
static void SellItem()
{
string alertMessage = "";
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[상점 - 아이템 판매]");
Console.ResetColor();
Console.WriteLine("필요한 아이템을 판매할 수 있는 상점입니다.\n");
Console.WriteLine("[보유 골드]");
Console.WriteLine($"{gold} G\n");
Console.WriteLine("[아이템 목록]");
for (int i = 0; i < inventory.Count; i++)
{
string stats = GetItemStats(inventory[i]);
int sellPrice = (int)(inventory[i].Price * 0.85);
Console.WriteLine($"{i + 1}. {inventory[i].Name} | {stats} | {inventory[i].Description} | {sellPrice} G");
}
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write(alertMessage);
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (int.TryParse(input, out int itemIndex) && itemIndex > 0 && itemIndex <= inventory.Count)
{
Item selectedItem = inventory[itemIndex - 1];
int sellPrice = (int)(selectedItem.Price * 0.85);
gold += sellPrice;
selectedItem.IsEquipped = false; // 장착 해제
// 상점에서 구매 여부 업데이트
var shopItem = gameItems.FirstOrDefault(item => item.Name == selectedItem.Name);
if (shopItem != null)
{
shopItem.IsPurchased = false;
}
inventory.RemoveAt(itemIndex - 1); // 아이템 목록에서 제거
alertMessage = $"{selectedItem.Name}를 {sellPrice} G에 판매했습니다.\n\n";
}
else if (input == "0")
{
break; // [상점]으로 돌아감
}
}
}
static void EnterDungeon()
{
float totalAttack = baseAttack;
int totalDefense = baseDefense;
foreach (var item in inventory)
{
if (item.IsEquipped)
{
totalAttack += item.Attack;
totalDefense += item.Defense;
}
}
while (true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[던전 입장]");
Console.ResetColor();
Console.WriteLine("");
string healthStr = $"│ 체력 : {health}";
string attackStr = $"│ 공격력 : {totalAttack}";
string defenseStr = $"│ 방어력 : {totalDefense}";
string goldStr = $"│ 골드 : {gold}";
Console.WriteLine("┌──────────────────────┐");
Console.WriteLine(PadRightToLength(healthStr, 20) + "│");
Console.WriteLine(PadRightToLength(attackStr, 20) + "│");
Console.WriteLine(PadRightToLength(defenseStr, 20) + "│");
Console.WriteLine(PadRightToLength(goldStr, 20) + "│");
Console.WriteLine("└──────────────────────┘");
if (health <= 0) Console.WriteLine("\n※ 던전 입장을 위해 체력이 1 이상 필요합니다");
Console.WriteLine("");
Console.WriteLine("1. 쉬운 던전 | 방어력 5 이상 권장");
Console.WriteLine("2. 일반 던전 | 방어력 11 이상 권장");
Console.WriteLine("3. 어려운 던전 | 방어력 17 이상 권장");
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "1" && health>0)
{
ProcessDungeon(DungeonDifficulty.Easy, 5, 1000);
}
else if (input == "2" && health > 0)
{
ProcessDungeon(DungeonDifficulty.Normal, 11, 1700);
}
else if (input == "3" && health > 0)
{
ProcessDungeon(DungeonDifficulty.Hard, 17, 2500);
}
else if (input == "0")
{
break; // [스파르타 마을]로 돌아감
}
}
}
static void ProcessDungeon(DungeonDifficulty difficulty, int recommendedDefense, int baseReward)
{
float totalAttack = baseAttack;
int totalDefense = baseDefense;
foreach (var item in inventory)
{
if (item.IsEquipped)
{
totalAttack += item.Attack;
totalDefense += item.Defense;
}
}
bool isSuccess = true; // 던전 성공 여부
int healthLoss = new Random().Next(20, 36); // 기본 체력 감소량
int reward = baseReward; // 기본 보상
// 방어력이 권장 수치보다 낮은 경우 실패 확률 적용
if (totalDefense < recommendedDefense)
{
isSuccess = new Random().NextDouble() >= 0.4;
healthLoss += recommendedDefense - totalDefense; // 체력 감소량 증가
}
else
{
healthLoss -= totalDefense - recommendedDefense; // 체력 감소량 감소
}
// 체력 감소 적용
health -= Math.Max(healthLoss, 0);
// 보상 계산
if (isSuccess)
{
int attackBonus = new Random().Next((int)totalAttack, (int)(totalAttack * 2 + 1));
reward += (int)(baseReward * (attackBonus / 100.0));
}
// 결과 출력
while (true)
{
Console.Clear();
if (isSuccess)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("[던전 클리어]");
Console.ResetColor();
Console.WriteLine($"축하합니다!!\n{GetDifficultyString(difficulty)} 던전을 클리어 하였습니다.");
Console.WriteLine($"\n[탐험 결과]\n체력 {health + healthLoss} -> {health}\nGold {gold} G -> {gold + reward} G");
gold += reward;
// 레벨업 로직
currentDungeonClears++;
if (currentDungeonClears >= level)
{
LevelUp();
Console.WriteLine($"\n축하합니다! 레벨이 {level}로 상승했습니다.");
}
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("[던전 실패]");
Console.ResetColor();
Console.WriteLine($"아쉽게도 {GetDifficultyString(difficulty)} 던전을 클리어하지 못했습니다.");
Console.WriteLine($"\n[탐험 결과]\n체력 {health + healthLoss / 2} -> {health}");
}
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "0")
{
break; // [던전 입장]으로 돌아가기
}
}
}
static void Rest()
{
string alertMessage = "";
while(true)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("[휴식하기]");
Console.ResetColor();
Console.WriteLine($"500 G를 내면 체력을 회복할 수 있습니다. (보유 골드 : {gold} G)");
Console.WriteLine("");
Console.WriteLine("1. 휴식하기");
Console.WriteLine("");
Console.WriteLine("0. 나가기");
Console.WriteLine("");
Console.Write(alertMessage);
Console.Write("원하시는 행동을 입력해주세요.\n>> ");
string input = Console.ReadLine();
if (input == "1")
{
if (health == 100)
{
alertMessage = "체력이 이미 최대치입니다.\n\n";
}
else if (gold < 500)
{
alertMessage = "보유 골드가 부족합니다.\n\n";
}
else
{
gold -= 500;
health = 100;
alertMessage = "휴식을 완료했습니다. 체력이 100이 되었습니다.\n\n";
}
}
else if (input == "0")
{
break; // [스파르타 마을]로 돌아가기
}
}
}
// 아이템 능력치 반환
static string GetItemStats(Item item)
{
List<string> stats = new List<string>();
if (item.Attack > 0)
{
stats.Add($"공격력 +{item.Attack}");
}
if (item.Defense > 0)
{
stats.Add($"방어력 +{item.Defense}");
}
return string.Join(" ", stats);
}
// 던전 Difficulty에 대한 문자열 반환
static string GetDifficultyString(DungeonDifficulty difficulty)
{
switch (difficulty)
{
case DungeonDifficulty.Easy:
return "쉬운";
case DungeonDifficulty.Normal:
return "일반";
case DungeonDifficulty.Hard:
return "어려운";
default:
return "알 수 없는";
}
}
// 문자열의 우측을 공백으로 채움
static string PadRightToLength(string str, int totalLength)
{
return str.PadRight(totalLength);
}
static void LevelUp()
{
level++;
baseAttack = 10 + (level - 1) * 0.5f; // 레벨에 따른 기본 공격력 재계산
baseDefense = 5 + (level - 1); // 레벨에 따른 기본 방어력 재계산
currentDungeonClears = 0; // 클리어 횟수 초기화
}
static string SaveGame()
{
using (StreamWriter file = new StreamWriter("savegame.txt"))
{
file.WriteLine(level);
file.WriteLine(name);
file.WriteLine(job);
file.WriteLine(baseAttack);
file.WriteLine(baseDefense);
file.WriteLine(health);
file.WriteLine(gold);
file.WriteLine(currentDungeonClears);
// 인벤토리 저장 (아이템 이름과 장착 여부)
foreach (var item in inventory)
{
file.WriteLine($"{item.Name},{item.IsEquipped}");
}
// 상점템 구매 여부 저장
foreach (var item in gameItems)
{
if (item.IsPurchased)
{
file.WriteLine($"ShopItem,{item.Name}");
}
}
}
return "진행도를 저장하였습니다.\n\n";
}
static string LoadGame()
{
if (File.Exists("savegame.txt"))
{
using (StreamReader file = new StreamReader("savegame.txt"))
{
level = int.Parse(file.ReadLine());
name = file.ReadLine();
job = file.ReadLine();
baseAttack = float.Parse(file.ReadLine());
baseDefense = int.Parse(file.ReadLine());
health = int.Parse(file.ReadLine());
gold = int.Parse(file.ReadLine());
currentDungeonClears = int.Parse(file.ReadLine());
// 상점 아이템 구매 여부 초기화
foreach (var item in gameItems)
{
item.IsPurchased = false;
}
// 인벤토리 로드
inventory.Clear();
string line;
while ((line = file.ReadLine()) != null)
{
string[] parts = line.Split(',');
if (parts[0] == "ShopItem")
{
// 상점 아이템 구매 여부 로드
var itemToPurchase = gameItems.FirstOrDefault(item => item.Name == parts[1]);
if (itemToPurchase != null)
{
itemToPurchase.IsPurchased = true;
}
}
else
{
// 인벤토리 아이템 로드
var itemToAdd = gameItems.FirstOrDefault(item => item.Name == parts[0]);
if (itemToAdd != null)
{
inventory.Add(itemToAdd);
itemToAdd.IsEquipped = bool.Parse(parts[1]);
}
}
}
}
return "진행도가 로드되었습니다.\n\n";
}
else
{
return "저장된 데이터가 없습니다.\n\n";
}
}
static string ResetGame()
{
level = 1;
name = "말랑단단장";
job = "전사";
baseAttack = 10;
baseDefense = 5;
health = 100;
gold = 2500;
currentDungeonClears = 0;
inventory.Clear(); // 인벤토리 초기화
gameItems.ForEach(item => item.IsPurchased = false); // 상점 아이템 초기화
return "진행도가 초기화되었습니다.\n\n";
}
}
}
위 코드에 대한 설명
아이템 클래스 정의 - Item 클래스에 아이템의 구성요소를 필드로 두어 관리
캐릭터 관련 기본적인 스테이터스들 - 굳이 Player 클래스로 둘 필요가 있을까 고민하다가 굳이 그렇게 할 필요성이 없어 보여 전역 필드로 사용
전체적인 게임아이템을 관리하는 List와, 인벤토리 List - Item 클래스 형태를 멤버로 갖는 리스트 형태로 관리 - 초기화를 진행하지 않은 필드는 ["", 0, False] 등의 값을 가진다. - 즉 IsPurchased 필드는 현재 작성된 코드처럼 굳이 false로 지정해줄 필요는 없음.
Main() - ShowMainMenu() 메서드를 호출하여 게임 시작
ShowMainMenu() - 모든 씬(Show메서드)은 while문을 사용하여 그리며, 잘못된 입력을 받을 시 현재 메뉴를 다시 보여줌.
ShowCharacterStatus() - while문 안에는 최소한의 코드를 두어 자원을 절약 - $문자열 적극 사용
ShowInventory() - 필요 시, alertMessage에 문구를 저장하여 화면 갱신 시 보여줌. - inventory.Count만큼 반복하여 아이템의 정보를 표시. - GetItemStats() 메서드를 통해 아이템의 모든 능력치를 문자열로 반환받는다. - string equipped = inventory[i].IsEquipped ? "[E]" : ""; - inventory[i].IsEquipped 가 참일 경우, "[E]"를, 거짓일 경우 ""를 equipped 에 저장한다.
ManageEquipment() - 장비 장착 목적의 입력값에 대해 - int형으로 Parse가 가능하면 true를 반환하고, else if 문 내에서 itemIndex 변수에 저장 - 또한 itemIndex가 인벤토리 크기 범위 내인지 확인하여 만족한다면 장비를 장착하거나 해제
ShowShop(), PurchaseItem(), SellItem() - 아이템목록 표시 시, IsPurchased 가 true일 경우, "구매완료" 문구를 함께 표시 - 아이템 구매를 성공할 경우, 해당 Item 객체는 inventory 리스트에 추가하여 관리할 수 있도록 함. 복사본이 아닌 객체 참조 형태. - 아이템 판매를 성공할 경우, 85% 가격을 돌려받으며, IsEquipped을 false로, inventory.RemoveAt(index)를 통해 List에서 제거한다.
EnterDungeon(), ProcessDungeon() - 던전 관련 장면을 구현. 선택한 난이도의 던전 입장 시, 캐릭터 스테이터스와 확률에 따라 성공과 실패를 한다. - 캐릭터 스테이터스를 요구하기 때문에, 구현 기능목록에는 없었지만 캐릭터 스테이터스를 기본적으로 보여주도록 함. - 여러가지 계산 관련하여 코드가 길어졌다. - 체력이 0 이하일 경우, 던전에 진입 불가. 이것도 요구사항에는 없었지만 기본적으로 이 정도 제한은 당연하다고 생각하여 구현.
Rest() - 500골드를 지불하고 체력을 100으로 만든다
string GetItemStats(Item item) - item의 공격력, 방어력 중 0보다 높은 능력치를 집계하여 문자열로 반환
string GetDifficultyString(DungeonDifficulty difficulty) - 던전 Difficulty에 대한 한글로 된 문자열 반환
string PadRightToLength(string str, int totalLength) - 문자열의 우측을 공백으로 채움
LevelUp() - 던전 탐험 시 레벨업 로직을 작성 - 게임데이터를 로드 하였을 경우에도 올바르게 적용되어야하기 때문에, '레벨에 따라' 기본 스텟을 연산하는 로직을 작성하였다.
- 배열과는 다르게 크기가 가변적 -System.Collections.Generic 네임스페이스 추가 필요
컬렉션:List 의 예시
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
list.Remove(2);
foreach(int i in list){Console.WriteLine(i);}
for(int i=0; i<list.Count; i++){Console.WriteLine(list[i]);}
// 1 3
컬렉션:Dictionary 의 예시
Dictionary<string, int> scores = new Dictionary<string, int>();
scores.Add("Alice",100);
foreach(DeyValuePair<string, int> pair in scores){Console.WriteLine(pair.Key + ": " + pair.Value);
컬렉션:Stack 의 예시
Stack<int> stack1 = new Stack<int>();
stack1.Push(1);
stack2.Push(2);
int value = stack1.Pop(); // value = 2
컬렉션:Queue 의 예시
Queue<int> queue1 = new Queue<int>();
queue1.Enqueue(1);
queue1.Enqueue(2);
int value = queue1.Dequeue(); // value = 1
컬렉션:HashSet(중복되지 않은 요소로 이루어진 집합) 의 예시
HashSet<int> set1 = new HashSet<int>();
set1.Add(1);
set1.Add(2);
foreach(int element in set1){ Console.WriteLine(element); }
구조체
여러 개의 데이터를 묶어 하나의 사용자 정의 형식으로 만듦
struct Person
{
public string Name;
public int Age;
public void PrintInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
Person person1;
person1.Name = "John";
person1.Age = 25;
person1.PrintInfo();
클래스:용어메모와 구조체와의 비교
- 특징 : 캡슐화/상속/다형성/추상화/객체 - 구성요소 : 필드/메서드/생성자/소멸자 - 구조체와 클래스는 모두 사용자 정의 형식을 만드는 데 사용될 수 있다. - 구조체는 값 형식으로, 스택에 할당되고 복사될 때 값이 복사된다. - 클래스는 참조 형식으로, 힙에 할당되고 참조로 전달되어 성능 측면에서 구조체와 차이가 있다. - 구조체는 상속을 받을 수 없지만, 클래스는 단일 상속 및 다중 상속이 가능하다. - 구조체는 작은 크기의 데이터 저장이나 단순한 데이터 구조에 적합하며, 클래스는 더 복잡한 객체를 표현하고 다양한 기능을 제공하기 위해 사용한다.
접근제한자
- public : 외부에서도 자유 접근 가능 - private : 같은 클래스 내부에서만 접근 가능 - protected : 같은 클래스 내부와 상속받은 클래스에서 접근 가능
프로퍼티(Property)
- 객체의 필드에 직접 접근하지 않고 간접적으로 필드값을 읽거나 설정하는 데에 사용되는 접근자(Accessor) 메서드의 조합. - 필드에 대한 접근제어와 데이터 유효성 검사 등을 수행 - 필드와 마찬가지로 객체의 상태를 나타내는 데이터 역할을 하지만, 외부에서 접근할 때 추가적인 로직을 수행 할 수 있음. - get과 set 접근자를 사용하여 동작을 정의한다 - get 접근자는 프로퍼티의 값을 반환하고, set 접근자는 프로퍼티의 값을 설정한다. - 필요에 따라 두 접근자 중 하나를 생략하여 읽기 전용 혹은 쓰기 전용 프로퍼티를 정의할 수 있다.
class Person
{
private string name;
private int age;
public string Name
{
get { return name; }
private set { name = value; }
}
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
}
}
}
Person person = new Person();
person.Name = "John"; // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10; // 유효성 검사에 의해 나이 값이 설정되지 않습니다.
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.
또한, 다음과 같은 자동 프로퍼티 구문으로 간단하게 정의하고 사용이 가능
[접근 제한자] [데이터 타입] 프로퍼티명 { get; set; }
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Person person = new Person();
person.Name = "John"; // 값을 설정
person.Age = 25; // 값을 설정
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // 값을 읽어 출력
상속
- 여러 부모의 클래스를 하나에 자식에 상속받는 다중상속은 C#에서 불가능 - 단, 하나의 클래스와 동시에 여러 개의 인터페이스를 상속받는 것은 가능
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void Eat() { Console.WriteLine("Animal is eating."); }
public void Sleep() { Console.WriteLine("Animal is sleeping."); }
}
public class Dog : Animal
{
public void Bark() { Console.WriteLine("Dog is bark."); }
}
다형성 : 가상 메서드
- 자식 클래스에서 재정의 할 수 있는 메서드
public class Unit{
public virtual void Move(){
Console.WriteLine("두발로 걷기");
}
}
public class Marine:Unit{
public override void Move(){
Console.WriteLine("네발로 걷기");
}
}
Marine marine = new Marine();
marine.Move(); // virtual/override 를 사용하지 않았어도, 문제 없이 "네 발로 걷기"를 출력
List<Unit> list = new List<Unit>();
list.Add(new Marine()); // 부모의 형태로 관리하는 경우, virtual/override를 사용하여 실형태인 자식을 탐색할 수 있도록 함
foreach(Unit unit in list) { unit.Move(); } // 즉, virtual/override를 사용하지 않았다면, "두발로 걷기"를 출력
추상 클래스와 메서드
- 추상 클래스는 직접적인 인스턴스를 생성 불가 - 상속을 위한 뼈대 클래스로 사용 - 추상 클래스는 abstract 키워드를 사용하여 선언되고, 추상 메서드를 포함 할 수 있다. - 추상 메서드는 구현부가 없는 메서드이며, 자식 클래스에서 의무적으로 구현을 요구.
abstract class Shape{ public abstract void Draw(); }
class Circle : Shape{ public override void Draw() { Console.WriteLine("Drawing a Circle"); } }
용어 혼동 주의
- 오버라이딩(Overriding) : 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의 하는 것 - 오버로딩(Overloading) : 매개변수의 갯수와 타입에 따라 동일한 이름의 여러개의 메서드를 정의하는 것
제너릭 / out, ref 키워드에 대한 학습과 정리는 내일 중 진행
3. 과제에 대해
- 개인 과제의 필수 항목은 1차적으로 완성하였고, UI 개편과 선택 항목 구현의 진행이 필요.
['./lala_data/searchImg/defaultDelay10/test.png']
Exception in thread ./lala_data/searchImg/defaultDelay10/test.png:
Traceback (most recent call last):
File "threading.py", line 950, in _bootstrap_inner
File "buff_alarm_tray.py", line 94, in run
File "lalatools\DigitalImageProcessing\ImageProcess.py", line 44, in match
File "lalatools\DigitalImageProcessing\ImageProcess.py", line 22, in __init__
cv2.error: OpenCV(4.6.0) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\templmatch.cpp:1164: error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type() && _img.dims() <= 2 in function 'cv::matchTemplate'
아마 실행파일(exe) 뿐 아니라 터미널에서 실행하였어도 같은 문제가 발생했을 것으로 예상된다.
본인의 경우에는 아래와 같이 Pyinstaller로 작성한 실행파일을 실행하였을 경우, 콘솔창에 위와 같은 로그가 남았다.
작성했던 코드에서는 특별한 문제가 없었지만 위와 같은 에러와 함께 cv2가 이미지를 불러오지 못하는 경우
관리자 권한으로 실행하면 문제를 해결 할 수 있었다.
매번 관리자 권한으로 실행을 선택하기 귀찮은 경우 아래와 같이 파일 속성에서 설정 해 둘 수도 있다.
위와 같은 Unhandled exception in script 에러창이 팝업되며 이하 전문은 아래와 같다.
Traceback (most recent call last):
File "buff_alarm_tray.py", line 27, in <module>
File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
File "PyInstaller\loader\pyimod02_importers.py", line 419, in exec_module
File "autoit\__init__.py", line 6, in <module>
File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
File "PyInstaller\loader\pyimod02_importers.py", line 419, in exec_module
File "autoit\autoit.py", line 26, in <module>
OSError: Cannot load AutoItX from path: C:\Users\happy\AppData\Local\Temp\_MEI493962\autoit\lib\AutoItX3_x64.dll
즉 해당 경로에서 AutoItX3_x64.dll 파일을 찾을 수 없다는 내용.
이 경우, pyinstaller를 실행할 때, --add-data 옵션으로 경로를 직접 지정해 줄 수 있다.
# 수정 전
pyinstaller --onefile --icon='./lala_data/exeIcon.ico' .\buff_alarm_tray.py
# 수정 후
pyinstaller --onefile --add-data "C:\Users\happy\AppData\Local\Programs\Python\Python39\Lib\site-packages\autoit\lib\AutoItX3_x64.dll;autoit\lib" --icon='./lala_data/exeIcon.ico' .\buff_alarm_tray.py