전체 글 (204)

  • 2024.01.17
  • 2024.01.16
  • 2024.01.15
  • 2024.01.13
  • 2024.01.12
  • 2024.01.11
  • 2024.01.10
  • 2024.01.10
  • 2024.01.08
  • 2024.01.05
  • 2024.01.04
  • 2024.01.03
  • 01
    17

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 18일차

    부분진행도 : Chapter2.2 - 7일차

    작성일자 : 2024.01.17(수)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    어제자 일지에, Velog로 환승한다는 내용을 적어놓았는데, 이제까지 티스토리에 적어놓은 것도 있고, 중간에 잘라서 Velog에도 도중부터 적혀있으면 모양새가 좀 그렇기 때문에 일단 티스토리를 계속 이용하기로 생각했다. 코드블럭 스타일이나 작성 중의 불편함은 이제까지도 어떻게든 잘 해왔기 때문에 조금 불편할 뿐이지 큰 문제는 아니다.

    오늘은 과제 발표날이다. 오전은 평소처럼 알고리즘 문제를 풀고, 오후 2시부터 발표를 시작하였다. 우리 팀의 발표는 다른 팀원분이 준비 및 발표를 하셨는데, 연습을 많이 하셨는지 리허설때보다 더 좋은 스피드와 딕션으로 프로젝트 소개를 잘 해주셨다. 팀 당 발표시간 7분정도를 준수하라고 공지가 되었었는데, 프로젝트 분량이 워낙 방대해서 10분, 15분을 넘기는 조가 대부분이었다. 개성적인 기능 구현 부분 등을 잘라내며 시간을 줄이고 소감 등도 다 쳐냈기 때문에 발표 내용이 부실해졌다는 느낌이 강했는데, 아쉽지만 발표시간을 준수했다는 사실에 대해 만족해야 할 듯.

    오후에는 치킨이랑 커피가 걸린 미니게임 이벤트와 '좋은 개발자란'을 주제로 강의 세션이 열렸다. 발표를 마치고 잠시 쉬어가며 보내는 시간이었다.

    팀이 바뀌기 전 마지막 밤, 팀원들과 진로 계획과 방향성, Unity나 프로젝트 관련하여 정보 공유 등 이런저런 얘기를 하며 하루를 마무리했다.

     

    2. 오늘 학습에 대해

    아래는 다른 조들의 발표를 보며 아이디어가 좋았거나 배울 점 등 인상적인 부분을 위주로 메모된 내용이다.

    <10조 독점 메모>
    리포지토리의 Wiki 탭 활용하여 작성 및 설명
    뭐야? 다이어그램 뭐쓴거임? 나중에 다시 돌려보자 > DB스키마?
    스토리 스크립트 재생 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 와......
    퀘스트 디테일 뭔데...? 이걸 구현했다고?
    전직 뭐임.....? 아니 진짜뭐임? 어케함?
    낚시 미니게임 뭔데...
    물고기 팔기는 또 뭔데....... 뭐임
    아이템 강화는 또 뭔데...
    똑같은 7일짜리 프로젝트가 맞는지 의심 될 정도의 아이디어와 퀄리티

    <13조 독점 메모>
    피피티에 기능별로 설명해놓는 게 무난한듯
    공방 외에 다양한 스탯 : Key Value 형태로 저장?
    가능한 글자색을 밝게 해야 발표에서는 보기 좋은 듯.
    장비에 따라 마이너스 스탯도 있음 ㅋㅋㅋ
    던전 내 같은 종류의 몬스터에 A,B...등을 이름 뒤에 붙여 구별
    속도 스탯도 있어, 플레이어보다 빠를 시 몬스터가 먼저 공격하는 등 행동
    몬스터도 스킬? 찢었다...
    기본적으로 폰트를 ConsoleColor.White로 한듯. 밝아서 보기 좋은듯? 구현 상으로 어려울 수 있음.
    바이너리 저장 -> 클래스 자체를 직렬화 할 수 있어 구현이 편함, 보안성 강화
    PPT 마지막화면 센스(코딩해요 콘)
    기능구현에 있어 모티브가 된 게임에 대해 소개

    <미스>
    인벤토리에 '회복약' 넣는 거 깜빡함

    <발표>
    PPT 예쁜 템플릿과 깔끔한 정리
    다른 수강생들에게 도움이 될만한 내용을 끼워넣으면 좋을듯
    회고, 어떤 시도를 했는지 등. 기술적 회고가 있으면 더 좋음.
    현장 시연을 위한 치트키
    깃로그 시각화

    <기술과 디자인>
    콘솔 내에 이모지 사용 가능한듯
    스킬명과 수치에 모두 색깔 넣어주기
    레벨업 시 스탯포인트 아이디어
    직업별 다른 상점물품 장비
    MP포션
    저장 슬롯 여러개

    <고급 용어 사용>
    예)던전 스케일링

    <이외 피드백>
    어택 등 플레이어가 사용하는 기능들은 플레이어 클래스에 모아두는 등 하는 게 좋음
    코드컨벤션 준수, 커밋 메시지 등도 신경쓰기
    메인브렌치에서 여러 브렌치로 나누어 사용하는 방법 좋음

    <???>
    30개 이상의 cs파일(17조 이미지)
    제비뽑기로 발표자를 정함
    진행을 맡은 *** 매니저님은 회고 파트를 좋아하시는듯

    <우리팀 피드백>
    화면공유를 해도 작아서 잘 보이지 않는 부분이 곳곳에 존재. 음량 세팅과 함께 체크하면 좋을 듯.

     

    3. 과제에 대해

    오는 주차는 다시 Unity로 복귀하여 개인과제를 진행한다. 앞으로도 대략 한 주 단위로 프로그램이 바뀌는 듯 하다.

    당장 마주하는 프로젝트도 잘 참여하고, 최대한 알찬 결과물을 뽑아가고싶다.

     

    반응형
    COMMENT
     
    01
    16

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 17일차

    부분진행도 : Chapter2.2 - 6일차

    작성일자 : 2024.01.16(화)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    과제 제출과 발표에 대해 회의, 게임 내 장면의 사이클 위주로 발표를 진행하고, 시연에서 혹여나 빠진 내용은 슬라이드에 별첨하여 설명을 할 예정. 팀원 각자의 소감을 발표 마지막에 배치할 예정이다.

    티스토리 글작성이나 테마 그리고 코드블럭 등의부분에서 불편하다고 느껴져서 내일 글부터는 Velog를 사용 해 볼 생각이다.

     

    2. 오늘 학습에 대해

     

    (1) 알고리즘 풀이 : 풀이 중 검색을 했던 내용을 위주로  작성

     

    프로그래머스 - 두 개 뽑아서 더하기

    • 전체 코드
    using System;
    using System.Collections.Generic;
    
    public class Solution {
        public int[] solution(int[] numbers) {
            int[] answer = new int[] {};
            // 동적관리를 위한 List<int> 사용
            List<int> answerList = new List<int>();
            
            for(int i=0; i<numbers.Length-1; i++){
                for(int j=i+1; j<numbers.Length; j++){
                    int sum = numbers[i]+numbers[j];
                    if (!answerList.Contains(sum)) {
                        answerList.Add(sum);
                    }
                }
            }
            answerList.Sort();
            answer = answerList.ToArray();
            return answer;
        }
    }
    • 코드 특징
      • 좋은 로직이 떠오르지 않아 정공법 작성
    • 매번 나오는 값을 array에 추가하야 하는 경우 동적 관리를 위해 List로 바꾸어 사용하는 것이 좋다.
      • List와 그 메서드는 아래와 같이 사용 가능
    // 리스트를 사용하기 위해 using
    using System.Collections.Generic;
    
    // 리스트에 요소를 추가
    answerList.Add(value);
    
    // 리스트 내에 요소가 들어있는지 확인
    if (!answerList.Contains(value)) {}
    
    // 리스트 정렬
    answerList.Sort(); // 리스트 정렬
    
    // answerList라는 List<T> 객체의 모든 요소를 포함하는 새로운 배열을 생성 후, answer라는 배열 변수에 할당
    answer = answerList.ToArray();

     

    프로그래머스 - 가장 가까운 같은 글자

    • 전체 코드
    using System;
    using System.Collections.Generic;
    
    public class Solution {
        public int[] solution(string s) {
            int[] answer = new int[s.Length];
            
            // O(N^2)도 되겠지만, 더 효율적인 알고리즘으로...
            // s가 영어 소문자로만 이루어져 있다고 하니, 26개 소문자의 index 리스트를 작성하자
            Dictionary<char, int> alphaIndex = new Dictionary<char, int>();
            
            // a부터 z까지 -1 설정
            for (char c = 'a'; c <= 'z'; c++) {
                alphaIndex.Add(c, -1);
            }
            
            // s를 처음부터 순회하며 answer와 alphaIndex 갱신
            for (int i = 0; i<s.Length; i++){
                if(alphaIndex[s[i]]==-1) answer[i] = -1;
                else answer[i] = i-alphaIndex[s[i]];
                
                alphaIndex[s[i]] = i;
            }
            // 시간복잡도 O(N)
            return answer;
        }
    }
    • 코드 특징
      • 시간복잡도 O(N)으로 해결
      • 사전형에 26개의 알파벳을 키로 갖는 index를 마련하여, s를 순회하며 매 문자마다 즉각 갱신
    • C#에서 영문 소문자 26개를 key로, int형을 value로 쓰는 사전형을 사용하는 법
      • Dictionary<char, int> 타입을 사용하여 수행. 아래와 같이 사용 가능
    // 사전형을 사용하기 위해 using
    using System.Collections.Generic;
    
    // 선언과 초기화
    Dictionary<char, int> dict = new Dictionary<char, int>();
    
    // 키값 'a'부터 'z'까지, -1 값으로 로 초기화
    for (char c = 'a'; c <= 'z'; c++) {dict.Add(c, -1);}
    
    // 사전형 값 설정
    dict['a'] = 1; // 'a'의 값을 1로 설정
    
    // 미리 설정해주지 않은 키 값에 위 구문처럼 값을 대입하려고 하면, KeyNotFoundException 발생
    // TryGetValue, ContainsKey 메서드를 사용하여 키가 존재하는지 확인하는 조건문을 두면 좋음
    if (dict.TryGetValue('a', out value)) {}
    if (dict.ContainsKey(key)) {}
    
    // 사전 내용 출력
    foreach (KeyValuePair<char, int> entry in dict) {
        Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
    }

     

    3. 과제에 대해

    • 발표 돕기
    • Velog 환승작업

     

    4. 참고자료

    • 없음
    반응형
    COMMENT
     
    01
    15

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 16일차

    부분진행도 : Chapter2.2 - 5일차

    작성일자 : 2024.01.15(월)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    콘솔 던전 RPG 게임 팀 과제의 개발부분은 오늘 마무리되었다. 큰 문제가 나오지 않는다면, 내일 발표에 관해서 회의를 진행하고 발표 자료와 준비를 할 예정으로 보인다.

    아침에 1시간씩 진행하는 CS는 Level1(캠프에서 문제 잔뜩 모아놓고 레벨 매겨줌)부터 시작해서 현재 Level3 문제들을 지나고 있는데, 슬슬 한 문제당 푸는 시간이 늘어나는 게 느껴진다. 아직 간단한 문제지만 문제당 이제 평균 10분을 넘어가는 듯 한 기분. 문제를 풀기보다 매번 내가 찾아보는 게 대체 무엇인지 메모해두는 게 좋을 것 같다. 오답노트 공부하듯이 나중에 CS부분만 몰아보면 좋을 듯. 유감이지만 오늘은 메모한 것이 없다.

     

    2. 오늘 학습에 대해

    콘솔 UI에 파티클 로직을 구현하였다. 모든 파티클이 랜덤으로 깜빡인다거나 하는 건 아니고, 주기적으로 파티클 문자 n개를 화면에 띄우고 지우고를 반복한다.

    콘솔 화면에 파티클 이펙트를 보여주는 코드의 진행

    string titleArt = "{아\n스\n키\n 아\n트}" // 콘솔에 출력할 내용
    InitializeScreenBuffer(titleArt); // 콘솔에 출력하기 전, 내용을 저장
    DrawAsciiArt(titleArt); // 콘솔에 WriteLine() 할 뿐인 메서드
    
    // '+' 문자를 화면에 {num}개 만큼 생겼다 없어지는 파티클 이펙트.
    // 너비 95, 높이 41의 범위에 800ms마다 갱신되도록 했다.
    int width = 95; int height = 41; int num = 15; int msDelay = 800;
    effectThread = new Thread(() => UpdateEffect(width, height, num, msDelay)); effectThread.Start();
    
    // 키 입력을 받으면 스레드를 종료하여 다음 장면으로 넘어갈 준비를 한다.
    Console.ReadKey();
    keepRunning = false; // 스레드에 종료 신호를 보냄
    effectThread.Join(); // 스레드가 종료될 때까지 대기
    
    // 다음 장면으로 넘어감


    (1) InitializeScreenBuffer
    해당 메서드의 목적은 콘솔 화면에 표시할 내용을 '버퍼(buffer)'라는 메모리 구조에 저장하는 것

    버퍼 : 데이터를 임시로 저장하는 메모리 공간

    static void InitializeScreenBuffer(string s) // 내용을 버퍼에 저장하기
    {
        int width = Console.WindowWidth;
        int height = Console.WindowHeight;
        screenBuffer = new char[height, width]; // screenBuffer는 함수 밖에 전역변수로 선언을 해두었다
        // 아스키 아트를 버퍼에 저장
        using (var reader = new StringReader(s))
        {
            string line;
            int y = 0;
            while ((line = reader.ReadLine()) != null)
            {
                for (int x = 0; x < line.Length; x++)
                    screenBuffer[y, x] = line[x];
                y++;
            }
        }
    }

    Console.WindowWidth 와 Console.WindowHeight 를 사용하여 현재 콘솔 창의 너비와 높이를 불러온다.
    콘솔 창의 너비와 높이에 해당하는 배열의 크기를 갖는 2차원 배열 screenBuffer 를 생성.
    screenBuffer 는 버퍼의 역할을 하며, 콘솔 화면에 표시될 각 문자의 위치를 저장.
    + Height를 콘솔의 크기보다 높게 설정해야 하는 경우도 생길 수 있지만, 일단은 콘솔 높이를 사용.

    StringReader를 사용하여 주어진 문자열 s 를 한 줄씩 읽는다.
    각 줄을 순회하며 각 문자를 screenBuffer 배열에 저장.
    y 변수는 현재 처리 중인 줄(행)의 번호. 
    x 변수는 줄 내의 문자(열) 위치.

    screenBuffer에는 asciiArt의 각 문자가 콘솔 화면상의 해당 위치에 매핑된다.
    이후에 콘솔 화면에 어떤 변화가 필요할 때, 이 버퍼를 참조하여 어떤 문자가 원래 어느 위치에 있었는지 알 수 있다.

    예시) "AB\nCD" 를 버퍼에 저장
    screenBuffer[0, 0] = 'A'
    screenBuffer[0, 1] = 'B'
    screenBuffer[1, 0] = 'C'
    screenBuffer[1, 1] = 'D'

    (2) 이후 콘솔에 내용을 출력하고( DrawAsciiArt(titleArt) )

     

    (3) 매개변수로 (1) 파티클이 나올 직사각형 범위, (2) 파티클 갯수, (3) 깜빡이는 주기 를 넘겨 일정 시간마다 랜덤한 위치에 파티클 효과가 보이도록 한다.

    + 지금 더 갱신된 코드는 파티클로 쓸 문자 char, 버퍼를 사용하지 않아도 작동하도록 하는 bool isBuffer 등을 추가하여 확장성을 높였다.

    파티클이 지워지는 타이밍에, 버퍼에 저장되어 있던 내용을 기반으로 원래 문자를 복원한다.

     

    (4) 마지막으로 키 입력을 받으면 스레드를 종료하도록 조건문을 넣어두었다.

     

    콘솔 내 전각문자의 처리

    위 내용의 구현 중 가장 큰 문제는 전각기호의 처리였는데, 반각문자인 파티클 '+'가 전각기호인 한글 문자 자리 등에 나타난다면, 한글문자가 이리밀리고 저리밀리고 해서 난장판이 되어버린다. 그래서 해당 위치가 원래 전각기호가 위치했는지 판별하는 동시에 전각기호의 왼쪽 혹은 오른쪽 중 어느 쪽에 '+'가 들어갔는지 판별하여 " +" 또는 "+ "와 같이 공백문자를 포함하여 해당 전각문자를 덮도록 설계하였다.

    또한 전각기호를 복원할 때에도, 한칸 밀린 방향에 복원되지 않도록 조건 분기를 잘 나누어 제대로 된 위치에 다시 복원되도록 신경썼다.

    위의 두 요소를 신경쓰니 전각문자가 이리저리 치이는 문제는 사라졌다.

    아래는 문자가 전각문자인지 판별하는 코드이다

    아마 모든 전각문자를 포함하고 있진 않을 것. 대표적인 범위만 설정.

    static bool IsFullWidth(char c) // 전각문자인지 판별
    {
        int[] ranges = { 
            0x1100, 0x115F, // 한글 자음과 모음
            0x2E80, 0x2EFF, // CJK(중국, 일본, 한국) 급진 부호
            0x3000, 0x303F, // CJK 심볼 및 구두점
            0x3200, 0x32FF, // CJK 호환성 음절
            0x3400, 0x4DBF, // CJK 통합 한자 확장
            0x4E00, 0x9FFF, // CJK 통합 한자
            0xAC00, 0xD7AF, // 한글 음절
            0xF900, 0xFAFF, // CJK 호환성 한자
            0xFE30, 0xFE4F, // CJK 호환성 형태
            0xFF01, 0xFFEF, // 하프와이드 및 풀와이드 형태
        };
        int code = (int)c;
    
        for (int i = 0; i < ranges.Length; i += 2)
        {
            if (code >= ranges[i] && code <= ranges[i + 1])
            {
                return true; // 전각
            }
        }
        return false; // 반각
    }

    전각문자 판별 메서드는 콘솔 한정 앞으로도 유용하게 써먹을 수 있을 것으로 보인다. 예를 들어 영어와 한글이 섞인 문자열끼리 폭을 같게 맞추도록 공백문자를 채워넣는 등 당장 생각나는 것도 몇 개 있다.

     

    실행파일 생성

    마지막으로 실행파일로 만들어서 다른 경로에서 직접 실행 해 보자고 갑자기 생각나서 해 본 내용.

    크게 과정이 복잡하진 않았고, 

    1. 빌드 세팅(1) - 프로젝트 속성

    2. 빌드 세팅(2) - 대상 OS 버전 대충 win7 등 고름

    3. 빌드 - Debug에서 Release로 바꿔준 후, 빌드

    4. exe 파일 기본 생성 위치, 다른 곳으로 가져갈 때 파일 세개만 챙기면 잘 실행되는 것으로 보임

    우리 조 슬랙에 설명용 쓰려고 대충 찍은 스크린샷이라 크기가 들쭉날쭉하다.

    저렇게 생성된 파일 중, 체크표시한 유형 세가지만 챙겨가면, 다른 환경에서도 잘 실행 될 것으로 예상된다.

     

    3. 과제에 대해

    • 내일부터 CS 중 검색하는 내용 생기면 무조건 적어두기
    • 발표 준비 돕기(본인 발표자가 아님)

     

    4. 참고자료

    반응형
    COMMENT
     
    01
    13

    C# 공부를 하며 계속 긴가민가 헷갈렸던 내용이 있어 정리.

    2차원 이상의 배열을 사용하려고 할 때, 두 가지 방식으로 배열을 사용 할 수 있다.

    int[,] array = new int[3, 2];
    
    int[][] jaggedArray = new int[3][];

    위의 방식처럼 [ , ] 한개의 대괄호 안에 쉼표가 들어가있는 모습과, 아래의 방식처럼 [ ][ ] 두 쌍의 대괄호를 사용하는 방식이다.

    (어느쪽이 맞는지 매번 헷갈리다가, 둘 다 사용한다는 걸 이제 알았음)

     

    1. 다차원 배열(Rectangular Arrays)

    int[,] array = new int[3, 2];

    괄호 내 쉼표를 사용하여 다차원 배열을 사용할 수 있다.

    특징으로는 모든 행이 동일한 수의 열을 갖는다는 것.

     

    2. 가변 배열(Jagged Arrays)

    int[][] jaggedArray = new int[3][];
    jaggedArray[0] = new int[4];
    jaggedArray[1] = new int[5];
    jaggedArray[2] = new int[3];

    가변 배열(Jagged Arrays) 또는 배열의 배열(Array of Arrays) 로 불리는 방식.

    위의 예시에서는 첫 번째 배열(jaggedArray[0])은 4개의 요소를 갖고, 두 번째 배열(jaggedArray[1])은 5개의 요소를 갖는 등 각 행마다 다른 수의 요소를 가질 수 있다.

     

    3. 두 배열의 차이점

    구조: 다차원 배열은 모든 행과 열의 크기가 같은 반면, 가변 배열은 각 행이 서로 다른 크기를 가질 수 있다.

    메모리 할당: 다차원 배열은 단일 연속된 메모리 블록에 할당되는 반면, 가변 배열은 각 행마다 별도의 메모리 블록에 할당된다.

    성능: 일반적으로 가변 배열은 다차원 배열보다 접근 속도가 빠르다. 이는 다차원 배열이 메모리에서 더 복잡하게 처리되기 때문.

    사용 용도: 다차원 배열은 데이터가 균일한 그리드 형태일 때 유용하고, 가변 배열은 각 행의 길이가 다를 수 있는 더 복잡한 데이터 구조를 다룰 때 유용하다.

    반응형
    COMMENT
     
    01
    12

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 15일차

    부분진행도 : Chapter2.2 - 4일차

    작성일자 : 2024.01.12(금)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    팀 과제(콘솔 텍스트 던전 RPG) 진행률이 거의 막바지이다. 오는 월요일에 던전 내 전투 중 장면 구성을 손질하면 게임 내적인 부분은 완료가 되어있을 것 같다. 

    UI.cs 클래스를 새로 마련하여 플레이어의 선택지 패널, 아스키아트 프리셋, 텍스트와 콘솔에 관련된 편의기능을 압축한 메서드 등 여러가지를 구현하고, 디자인 개선을 마쳤다. 전후 이미지를 같이 보여주면 좋을 것 같은데 그렇게까지 하기에는 에너지가 모자라다. 기록으로 남기는게 무조건 좋을텐데 라고 생각 중.

    매일 그렇지만 요즘 다른 하는 일이 없어서 그런지 팀 과제랑 알고리즘 풀기에 모든 에너지를 쏟고 있다. 수면 시간도 너무 적어져서 그런지 두통이 심해졌다.

     

    2. 오늘 학습에 대해

    팀 과제

    Github Desktop으로 하루 종일 팀 과제를 같이하다보니 깃은 이제 수월하게 다루고 있는 것 같다. 특히 팀원들이 Pull Request를 수행하기 전 Main함수로부터 업데이트를 잘 한 뒤에 PR을 진행했기 때문에 충돌이 거의 없었던 편.

    오늘은 UI의 끝을 보자고 생각하여(갑자기 그런 기분이 들어) 구현을 많이 하였는데, 그 내용은 아래와 같다.

    1. 타이틀 구현과 전반적인 UI 스타일 구현

    타이틀이 필요하다고 개인적으로 종종 생각이 들었었기에, 오늘 UI 구현을 하며 타이틀 장면도 마련 해 두었다.

    아스키아트가 화면을 꽉 채우도록 하고, 특정 좌표로 커서를 옮겨 텍스트를 구현하였다. 이 상태에서 아무 키나 누르면

    데이터를 불러온다는 안내 메시지를 잠시 보여준 후, 다음 장면으로 넘어간다. 가운데 정렬이 말끔해보여서 좋았다.

    캐릭터 생성 장면은 더 화려하게 꾸밀 수 있었지만, 생성 단계는 처음이라는 느낌답게 조촐하게 보여주는 것도 좋지 않을까 생각하게 되어, 필요로 하는 간단한 텍스트만 표시하도록 하였다.

    그래도 왼쪽에 여백을 두거나 줄바꿈을 두고, '>>>' 문자를 두는 등 가독성에 신경을 썼다. 뒤따라 나오는 직업 선택이나 확인 장면도 거의 동일

    직업까지 선택을 하면 아래와 같은 메인 메뉴를 볼 수 있다.

    메인 메뉴이지만 현재 아이디어 고갈로 검소한 디자인을 하고있다.

    우측 빈 부분에 아트를 넣거나 하면 좋을듯.

    신경쓰이는 점은 상단 아스키아트가 너무 긴데, 내가 작업한 부분이 아니라 건드리기 조심스러운 부분도 있고 마땅한 대체재를 생각해 두지 않았기 때문에 일단은 현상유지이다.

    메인메뉴를 제외하면 모두 하단의 유저 입력 전용 패널과 동일한 폭으로 맞춰두었다. 예를 들어 회복아이템 메뉴는 아래와 같이 균형잡힌 배치를 가지고 있다.

    유저에게 보여지는 장면의 크기에 맞추어 콘솔창의 크기도 줄여보는 시도를 했었지만, 어째서인지 콘솔 폭을 120자 미만으로 줄이게 되면 콘솔창 하단에 좌우 스크롤바가 생겨버리기 때문에 일단 콘솔창은 넓게 쓰고 있다.

    메인 컨텐츠인 던전에 들어가는 입구 장면이다. 간단한 아스키아트를 넣어두었다.

    E 키를 통해 내 정보를 열어볼 수 있는데(정보열기는 다른 팀원이 구현)

    스테이터스는 밝은 노란색으로 표시되게 하여 가독성을 높였고, 길어진 높이만큼 빈 공간에 아트도 더 넣어두었다.

     

    2. UI.cs

    오늘 UI.cs 파일을 새로 작성하고, 구현한 메서드가 여럿 있다.

     

    (a) 유저 입력 패널

    public static string UserInput(string alert="", bool positive=false, string reqMsg= "다음 행동을 입력하세요.")
    {
        Console.WriteLine("");
        Console.WriteLine(".──────────────────────────────────────────────────────────────────── .");
        Console.WriteLine($"  {reqMsg}");
        Console.WriteLine("");
        Console.WriteLine(""); Console.ForegroundColor = (positive)?ConsoleColor.Green:ConsoleColor.Red;
        Console.WriteLine($"  {alert}"); Console.ResetColor();
        Console.WriteLine("  >>>"); 
        Console.WriteLine("'──────────────────────────────────────────────────────────────────── '");
        Console.SetCursorPosition(6, Console.CursorTop-2);
        return Console.ReadLine();
    }

    UserInput()을 사용하여 위와 같은 패널을 그린다.

    경고 메시지와, 경고메시지의 성격(true:녹색 / false:빨간색), 그리고 기본 메시지인 "다음 행동을 입력하세요." 도 바꾸어 매개변수로 전달할 수 있다.

     

    (b) 아스키아트

    게임 내 모든 아스키아트를 모아두었다. 열거형인 AsciiPreset에 각자 정해둔 이름을 사용하여 AsciiArt 메서드를 호출한다.

    // 열거형
    public enum AsciiPreset
    {
        TitleArt,
        CreateCharacter,
        SelectClass,
        MainMenu,
        Battle,
        Status,
    	...
    }
    
    // 아스키아트 호출
    public static void AsciiArt(AsciiPreset preset)
    {
        ConsoleColor statusColor = ConsoleColor.Gray;
        int x, y;
        switch (preset)
        {
            ...
            case AsciiPreset.PotionInventory:
                Console.WriteLine("                                                                     ");
                Console.WriteLine("    .-.   p--- .'~`. .-=~=-.   :~:      |~|   .-~~8~~-.  |~|  .-.    ");
                Console.WriteLine("  .'__( .'~`.  `. .'  )___(  .'   `.    | |   |~~---~~|  | |  )__`.  ");
                Console.WriteLine("  | l | | m |  .'n`. (  o  ) |  p  |] .' q `. |   r   | .'s`. | t |  ");
                Console.WriteLine("  |___| |___|  `._.'  `._.'  |_____|  `.___.' `._____.' `._.' |___|  ");
                Console.WriteLine("                                                                     ");
                Console.ForegroundColor = statusColor;
                Console.WriteLine("----------------------------[회복 아이템]----------------------------");
                Console.ResetColor();
                Console.WriteLine("");
                break;
            ...
            case AsciiPreset.DungeonEntrance3:
                x = 41; y = 25;
                DrawOnSpecificPos("" +
                    "      |\\      _,,,---,,_\n" +
                    "ZZZzz /,`.-'`'    -.  ;-;;,_\n" +
                    "     |,4-  ) )-,_. ,\\ (  `'-'\n" +
                    "    '---''(_/--'  `-'\\_)\n",
                    x, y);
                break;
            ...
        }
    }

    간단하게 줄여본 코드는 위와 같다. 그냥 적어둔 내용대로 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);} 만큼 획득!");

    그냥 사용해도 편하고, 더 좋은 점은 문자열 도중에 특정 단어만 색을 입히는 것이 편하다. 위 내용을 메서드 없이 깡으로 구현한다면 아래와 같이 매우 길어지게 된다.

    // 예시1
    Console.ForegroundColor = color;
    Console.WriteLine("골드", ConsoleColor.Yellow);
    Console.ResetColor();
    
    // 예시2
    Console.Write($"골드를 ");
    Console.WriteLine("골드", ConsoleColor.Yellow);
    Console.Write($"{"500"}");
    Console.ResetColor();
    Console.WriteLine($" 만큼 획득!");

    Write와 WriteLine을 섞어 쓸 수 밖에 없게 되버리는 점도 문제점.

    RandomColoredWrite(string s) 메서드는 받은 문자열을 문자 단위로 랜덤한 색을 입히게 하는 신나는 메서드이다.

     

    알고리즘 풀이 중 자주 잊어버릴 만한 내용에 대해

    캠프에서 소개하는 알고리즘 문제 중 40문제 이상을 풀었지만, 자주 잊어버려 찾아보고 코드 작성을 했던 내용이다.

    복습하는 기분으로 문제들을 다시 열어보며 작성하였다.

     

    No.3[120805 몫 구하기]

        public int solution(int num1, int num2) {
            int answer = num1 / num2;
            return answer;
        }

     

    • int 형끼리의 나눗셈 계산은 int 형을 반환한다.

     

    No.7[120806 두 수의 나눗셈]

        public int solution(int num1, int num2) {
            int answer = 0;
            answer = (int)((float)num1/(float)num2*1000);
            return answer;
        }
    • float로 캐스트 된 수 끼리 나눗셈을 해야 제대로 된 결과가 나온다.
    • 현재 문제에서는 결과값의 소수 부분을 버리고 정수값만 출력.

     

    No.10[120817 배열의 평균값]

        public double solution(int[] numbers) {
            double answer = 0;
            for(int i=0; i<numbers.Length; i++){
                answer += numbers[i];
            }
            answer /= numbers.Length;
            return answer;
        }
    • 배열을 전달받을 때에는 int[] numbers 의 형식을 사용.
    • 배열은 .Length 를 사용하여 길이를 반환한다. 리스트의 경우는 .Count 가 길이를 반환한다. 둘 다 뒤에 ()가 없음에 주의.

     

    No.11[12937 짝수와 홀수]

    • (영단어)짝수는 'Even', 홀수는 'Odd' 

     

    No.12[12944 평균 구하기]

        public double solution(int[] arr) {
            double answer = 0;
            answer /= arr.Length;
            ...
        }
    • 위와 같이 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;
        }
    • 일부 테스트케이스에서 실패하는 경우, 오버플로가 일어나는지 확인하자.
    • 해당 문제는 int의 범위를 벗어나는 값을 다루기 때문에 long으로 캐스팅하여 해결.

     

    No.17[12932 자연수 뒤집어 배열로 만들기]

    int[] answer = new int[numStr.Length];
    • 새로운 배열의 선언을 해 줄 때의 형식
    answer[index++] = (int)(num%10);
    • 전치 및 후치 연산자를 활용하여 코드 길이 단축

     

    3. 과제에 대해

    • 전투 중 UI 디자인 수정
    • README.md 작성하기

     

    4. 참고자료

    없음

    반응형
    COMMENT
     
    01
    11

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 14일차

    부분진행도 : Chapter2.2 - 3일차

    작성일자 : 2024.01.11(목) 

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    캠프에서 그제와 어제 알고리즘 특강을 진행하고, 오늘부터 하루 한개 이상 알고리즘 문제 풀기를 아침 09시부터 10시까지 자율적으로 진행한다. 새벽에 잠이 안와서 먼저 시작했었는데, 계속 풀다보니 40문항정도 제출 완료. 한 두 줄로 해결 할 수 있는 기초 알고리즘 문제부터 시작하기 때문에, 사실 지금 시점에 제출한 문항 수는 크게 의미는 없어 보인다.

    팀 과제는 3일차인데 진행이 무척 빠른 것 같다. 하루 10시간씩 잡고있으니 당연하다면 당연한가 싶기도 하다. 스킬과 마나 시스템을 더 손보고, 나머지는 기존 기능에 대한 개선을 진행할 것으로 보인다.

     

    2. 오늘 학습에 대해

    오늘은 특별히 새로운 것을 배우거나 한 것은 없다. 개발파트가 순조로워 오늘 진행한 내용이 무척 많다. 오후 팀 스크럼에서는 내가 오늘 개발했던 내용도 많이 잊어버려 Github Desktop에 적혀있는 커밋 로그를 보며 프레젠테이션을 진행했다.

    오늘 내가 진행한 커밋 내용은 아래와 같다.

    Feat: Potion클래스 및 '회복 아이템' 장면 구현
    - Item 을 상속받는 Potion 클래스 구현
    - new Potion 객체로 [물] 추가(일단은 포션 역할로 사용, 포션 종류가 확장될 가능성이 있기 때문에 이름을 포션으로 두진 않음)
    - 메인메뉴 - 회복아이템 장면 구현

    Feat: 던전 승리 시 포션 및 골드를 획득

    Refactor: Potion 클래스 내 메서드 개편 및 해당 메서드를 사용했던 장면에서 코드 단축

    Design: 회복아이템 아트 수정

    Refactor: 경고문 시스템 도입

    Feat: 던전 전투 중 회복약 사용 기능 구현
    - 던전 전투 중 회복약 사용 기능 구현, isUseItem를 true상태로 주어 활성화.
    - 아이템 이름 [물]을 [회복약]으로 수정

    Feat: 전투 중 치명타, 몬스터회피 실적용

     

    그리고 오늘부터 알고리즘 문제를 풀어 하루 하나 이상씩 제출을 하는 시간을 갖게 되었는데, C#문법에서 헷갈리는 게 많아 많이 찾아보았는데, 이에 관해서도 이후에 문제를 다시 훑어보며 헷갈리는 부분을 정리 해 두면 좋을 것 같다.

     

    3. 과제에 대해

    - 디자인에 신경쓰며 팀 과제 개선해나가기

     

    4. 참고자료

    - 없음

     

    반응형
    COMMENT
     
    01
    10

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 13일차

    부분진행도 : Chapter2.2 - 2일차

    작성일자 : 2024.01.10(수)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    콘솔 텍스트 던전 RPG 게임의 팀 과제 진행중.

    구현할 기능별로 역할을 분담하여 진행하는 방식을 택하였다. 수정할 파일이 서로 겹치는 경우가 빈번할 것 같아 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"을 출력할 것이기 떄문에 큰 문제는 없을 것으로 보인다.

    Potion potion = items.OfType<Potion>().FirstOrDefault();
    Console.WriteLine($"{(potion != null ? potion.count.ToString() : "0")}")

    이외로는 사양에 맞추어 여러 조건식에 따라 기능을 구현한 내용이 대부분이기 때문에 이 정도로 마무리.

     

    3. 과제에 대해

    필수 구현 사양은 모두 만족한 것으로 보이고, 선택 구현사항을 각자 또 정하여 구현중에 있다. 큰 문제는 없을것으로 보여 계속 기능을 넣고 점검을 하며 완성도를 높이는 것이 좋겠다.

     

    4. 참고자료

    없음

    반응형
    COMMENT
     
    01
    10

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 12일차

    부분진행도 : Chapter2.2 - 1일차

    작성일자 : 2024.01.09(화)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    어제까지 개인 프로젝트로 진행했던 콘솔 텍스트 던전 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가 없었다고 생각해본다.

     

    3. 과제에 대해

    분담된 역할에 대해 팀 과제 수행, merge 후 정상적인 작동까지 확인하기

     

    4. 참고자료

    없음

    반응형
    COMMENT
     
    01
    08

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 11일차

    부분진행도 : Chapter2.1 - 6일차

    작성일자 : 2024.01.08(월)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    개인과제 제출을 마치고, 부족한 부분은 재제출을 하여 마무리를 하는 날이다. 본인은 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;
        }
    }



    3. 참고자료

    없음

    반응형
    COMMENT
     
    01
    05

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 10일차

    부분진행도 : Chapter2.1 - 5일차

    작성일자 : 2024.01.05(금)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    콘솔 텍스트 던전 RPG 게임의 요구사항을 모두 반영하여 제출까지 완료하였다.

     

    2. 오늘 학습에 대해

    오늘 팀원들끼리 각자 작성한 게임 코드를 리뷰하는 시간을 가졌는데, 각자의 스타일이 상상 이상으로 달라 충격을 받았다.

    내 경우에는 한개 파일에 순차적으로 필요한 코드를 모두 때려박아 넣으며 최대한 주어진 사양(UI디자인)을 훼손하지 않는는 방향성을 가졌다.

    팀원 한 명의 코드는 한 파일 안에 코드를 모두 작성하였지만 그 안에서도 다양한 확장안을 생각하고 여러가지 이유를 들어가며 생각치 못한 부분에서도 분리를 확실히 해 두었고, UI의 커스텀 디자인에도 상당히 신경 쓴 모습을 보여주었다.

    다른 팀원의 코드는 나와 비슷하게 순차적으로 코드를 작성했지만 프로그래머의 입장에서 리뷰하기에 좋은 가독성 높은 코드를 작성하는데에 주력하는 것이 보였다.

    또 다른 팀원의 코드는 장면과 기능을 담은 여러가지 파일을 구성하여 메인이 되는 코드 내에는 호출 하나만으로 그 씬이 모두 구현되게 하는 등 내가 보기에는 신기한 스타일을 하였는데, 이전에 윈도우 API로 게임을 만들어본 경험이 있어 이런 스타일을 쓴다고 하신다. 외에도 enum 을 한계까지 활용하는 모습 등 확장성에 극단적으로 좋은 모델로 보였다.

    코드의 구성 외에도 아래와 같은 여럿 아이디어를 얻었다.

    • 아스키 아트를 사용하여 그림을 넣기
    • 종료 시에도 저장되도록 하기
    • 데이터 저장형식을 json으로 하기
    • 아이템에 고유 번호 매기기
    • 체력을 0 아래로 내려가지 않게 하기
    • 아이템의 정보를 저장할 데이터 파일을 따로 만들어두기

    그리고 VSCode에서 C#을 사용할 수 있었다는 사실, Visual Studio의 파일 리스트 창을 Code처럼 왼쪽으로 옮길 수 있다는 사실 등 자잘한 팁도 여러가지 획득.

    그리고 코드 리뷰에서 질문을 받으며 Linq 관련 질문을 받았는데, '링크...? 링크가 뭐지'하며 혼란 상태에 걸렸었는데, 나중에 찾아보고 Linq였구나 알게 되었다. 파이썬을 다룰 때에는 in이나 where 비슷한 것들은 너무 자연스럽게 사용하는 기능들이었기 때문에 C#강의의 Linq 파트를 빠르게 대충 넘겨 알아듣지 못했었는데, 이와 비슷하게 용어 관련으로는 많이 혼동이 오는 편. 예)프로퍼티, 필드 등

    그 외에도 '싱글톤'이라는 것에 대한 개념도 없어 리뷰 타임에 그런것도 있구나 생각하며 듣고있었다.

    싱글톤

    특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이며, 생성자를 통해 여러 번 호출이 되더라도 인스턴스를 새로 생성하지 않고 최초 호출 시에 만들어두었던 인스턴스를 재활용하는 패턴이다.

     

    3. 앞으로의 과제

    오는 주차에는 이번 주차 개인 프로젝트를 베이스로 팀 프로젝트를 할 것 같다. 디자인이나 기능 개선 및 추가 등을 주 목적으로 예상하고 한 주간 팀 프로젝트를 진행한다. 잘 소통해가며 프로젝트를 무사히 마치기를 목표한다. 당장 구체적인 과제는 없고, 주말을 활용하여 C# 문법 후반부 복습을 할 예정이다.

     

    4. 참고자료

    반응형
    COMMENT
     
    01
    04

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 9일차

    부분진행도 : Chapter2.1 - 4일차

    작성일자 : 2024.01.04(목)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    간단한 던전 RPG 텍스트 콘솔 게임의 개인과제 제출이 내일 오후 6시이다.

    팀원들 모두 각자 열심히 진행을 하는 중.

    나는 선택 기능 구현 목록을 오늘 마무리 하였고, 현재 디버그를 하며 코드 상의 어색한 부분을 계속 수정중이다.

    선택 기능 구현 모두 무게가 있는 기능이라 모든 부분에서 힘을 쓰다 보니, 800줄 가까이 되는 코드를 작성했다.

    오늘의 기록을 위해 영상을 찍으면서도 글자 색 지정을 안했거나 하는 자잘한 미스도 보였다.

     

    2. 오늘 학습에 대해

    기능 구현을 모두 마친 후 돌아보니, 기존 작성했던 코드에서는 존재하는 아이템을 모두 shopItems 리스트에 Item 클래스 형태의 객체로 추가 해 두고, inventory 리스트에 추가하거나 빼는 방식을 사용하고 있었는데, 앞으로 개발이 계속된다면 퀘스트로 얻는 아이템이나 몬스터 드랍템 등 Shop에서 취급하지 않는 아이템도 많이 생길 것이기 때문에, 전체 아이템을 관리하는 리스트인 gameItems를 사용하는 구조로 바꾸는데에 많은 시간을 쏟았다.

     

    게임 구조

    기본적으로 모든 씬은 while(true) 안에 두어 플레이어에게 잘못된 입력값을 받으면 그 씬이 다시 로드되도록 하였다.

    main(){
    	ShowMainMenu(); // 메인 씬
    }
    ShowMainMenu(){
        while(true){
            (if 입력값 == 1){ 상태; }
            (if 입력값 == 2){ 상점; }
            (if 입력값 == 3){ 던전; }
            (if 입력값 == 0){ 게임종료; }
        }
    }
    
    상태(){
        while(true){
        
        	// 상태 씬 구성
            
        	(if 입력값 == 0){ break; // 뒤로가기 }
        }
    }
    
    상점(){
        while(true){
        
        	// 상점 씬 구성
            
        	(if 입력값 == 1){ 아이템구매 }
        	(if 입력값 == 2){ 아이템판매 }
        	(if 입력값 == 0){ break; // 뒤로가기 }
        }
    }
    ...

    먼저 계산을 마칠 필요성이 있는 구문들은 while문 위에 작성하거나, 아래의 코드처럼 같은 화면을 로드하지만 경고문을 같이 보여주는 경우도 자주 있다.

    아이템구매(){
    	alertMessage = ""
    	while(true){
        
        	// 기존 아이템 구매 씬의 내용
            
            // Write를 사용하면 alertMessage가 빈 문장이어도 티가 나지 않는다
            Console.Write(alertMessage)
        
        	(if 골드부족){ alertMessage = "골드가 부족해요\n\n" }
        }
    }

     

    기능 구현에 관해서는 전체적으로 비슷한 무게를 두고 구현했던 기분이기 때문에, 주석을 적은 전체 코드를 아래와 같이 적어놓았다.

    더보기

     

    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()
    - 던전 탐험 시 레벨업 로직을 작성
    - 게임데이터를 로드 하였을 경우에도 올바르게 적용되어야하기 때문에, '레벨에 따라' 기본 스텟을 연산하는 로직을 작성하였다.

    SaveGame()
    - 레벨, 이름, 직업, 기본공격력, 기본방어력, 현재체력, 골드, 경험치, 인벤토리목록+장착여부, 상점템구매여부
    - savegame.txt로 저장하여 보안요소는 없다.

    LoadGame()
    - 인벤토리 및 장착여부와 상점아이템 구매 여부를 반영하는 데에 코드가 길어졌다.
    - inventory.Clear(); 로 쉽게 리스트 비우기가 가능
    - 아이템들은 모두 객체참조를 하여 아이템 내 필드를 쉽게 관리할 수 있도록 하였다.

    ResetGame()
    - 진행도를 초깃값으로 초기화

     

    3. 시연 영상(gif, mp4)

    길이가 3분정도라서, 가능하면 동영상으로 보는 게 좋을 듯 하다.

     

     

     

    4. 과제에 대해

    - 주어진 요구사항에는 모두 만족하지만, 플레이어 입장에서 불친절한 화면 구성이 많다. 당장 제출이 내일이기 때문에, 현재 캠프에서 이 프로젝트의 앞으로의 방향성을 듣고 난 이후 개선할 지 검토를 해봐야 한다.

    - '=>' 같은 기호는 쓰기는 하는데 볼 때마다 무슨 기능인지 계속 헷갈린다. 파이썬에서도 데코레이트라던가 람다 함수라던가 필요하면 찾아는 쓰는데 볼 때마다 까먹는 요소들이 엄청 많은데 그야말로 과제요소가 아닌가 싶다. 숙련도 부족 이슈.

     

    5. 참고자료

    반응형
    COMMENT
     
    01
    03

    과정명 : 내일배움캠프 Unity 게임개발 3기

    전체진행도 : 8일차

    부분진행도 : Chapter2.1 - 3일차

    작성일자 : 2024.01.03(수)

    개발일지 목록 : 클릭


    1. 진행중인 과정에 대해

    개인 과제 주간의 3일차이다.

    C# 문법강의는 모두 빠르게 보기는 하였지만, 제네릭과 out 및 ref 키워드, 알고리즘 연습 외 몇 가지를 이후 복습 할 예정이다. 오늘

    오늘은 개인 과제의 필수 구현 요소를 확인하며, 선택 구현 요소 중 세 가지를 구현하였다.

     

    2. 오늘 학습에 대해

    오늘자 강의 중, 사용자 정의 예외 처리를 하는 예시

    // Exception을 상속받아 예외 클래스를 생성한다.
    public class NegativeNumberException : Exception
    {
        public NegativeNumberException(string message) : base(message)
        {
        }
    }
    try
    {
        int number = -10;
        if (number < 0)
        {
        	// 매개변수 message로 사용할 문자열을 전달하며, 예외를 발생
            throw new NegativeNumberException("음수는 처리할 수 없습니다.");
        }
    }
    catch (NegativeNumberException ex)
    {
        Console.WriteLine(ex.Message); // "음수는 처리할 수 없습니다."
    }
    catch (Exception ex)
    {
    	// NegativeNumberException 이외의 예외처리
        Console.WriteLine("예외가 발생했습니다: " + ex.Message);
    }

     

    개인 과제에 대해서는 내일 선택 요구사항까지 구현을 마친 후, 코드의 분석을 별도의 게시물로 작성할 예정이다.

     

    3. 앞으로의 과제에 대해

    - 개인 과제 완성, GPT를 통한 리뷰와 리팩토링

    - 강의 중 이해가 부족한 부분 재학습

     

    4. 참고자료

    - 없음

    반응형
    COMMENT