카테고리 없음

내일배움캠프 8일차 TIL 과제 마개조는 못참지

joseph2518 2024. 9. 23. 20:34

20240923 / Unity_6차   3주차 월요일

 

C# 문법 주차에 특이한 개인과제를 받았다.

 

바로 콘솔창에서 돌아가는 텍스트 게임을 만드는 것이다.

 

콘솔창에서 돌아가는 텍스트 RPG

 

위와 같이 콘솔창에 텍스트를 계속해서 띄워 주고 사용자의 입력을 받아 진행하는 게임이다.

 

 

 

허나.....

 

 

 

넘쳐 흐르는 아이디어를 참을 수는 없지!!

 

도전하는 자가 아름답다 했다.

 

계속 텍스트를 줄줄이 써내려가는 것이 아닌, 고정된 좌표에서 이미지(같은 글자)를 계속 덧씌우는

비주얼-텍스트 RPG를 만들기로 한 것이다!

 

 

 

막상 일을 벌이려니 제한시간 내에 못할 게 뻔해서 주말에도 작업을 약간 해 왔다.

 

오늘까지 해온 게임 구상 과정을 적어 보도록 하겠다.

 

 

 

 

1. 가장 먼저 게임을 콘솔창에 시각적으로 보여 주는 걸 구현하고 싶었다.


   스크린 클래스를 만들어 화면 구성 요소를 string 배열로 저장한 뒤 한줄씩 쓰기로 했다.


   그런데 프레임마다 Console.Clear()를 하고 그리고를 반복하려니 글자가 계속 깜빡거려서 도저히 못봐주겠더라.


   그래서 이전 스크린과 현재 스크린을 비교하여 변한 부분만 SetCursorPosition 함수와 Write 함수로 바꿔 쓰기로 했다.


   구현해 보니, 한글이 다른 문자보다 2배 더 두꺼워 뒤의 문자가 밀려나는 문제를 바로 발견했다.

 

   그래서 스크린을 구성할 변수를 스트링 배열이 아니라 int형 2차원 배열로 만들고,
   한글이 있을 때는 한 글자마다 뒤에 공백을 표현하는 문자 'null'(아스키코드 0)를 포함하기로 했다.

 

   내가 원하는 화면의 모습은 아래와 같다.

 

 

   예시) 전투 스테이지
  
                          나레이션 창
    ------------------------------------------------------
                       |                          |    
    아군정보     | 전투진행 화면  |   적군 정보
                       |                          |
    ------------------------------------------------------
                         명령 선택 창

 

 

   필요한 부분에만 텍스트를 계속 덧씌우면서 마치 쯔꾸르 게임을 하는 것처럼 만드는 것이 목적이다.

 

   스크린 컨트롤러 클래스 개요)
   필드 - int형 2차원 배열 2개(현재 화면, 덧씌울 화면)
   메서드 - 전체클리어, 한줄클리어, 덮어쓰기(y,x,width)->파싱(한글 변환, 프라이빗), 한줄 덮어쓰기(y), 업데이트

   아래는 스크린 컨트롤러 클래스다.

class ScreenController
{
    int[,] screenCurrent;
    int[,] screenUpdate;

    public ScreenController()
    {
        Console.SetWindowSize(Const.screenW + 1, Const.screenH + 1);

        screenCurrent = new int[Const.screenH, Const.screenW];
        screenUpdate = new int[Const.screenH, Const.screenW];

        for (int y = 0; y < Const.screenH; y++)
        {
            for (int x = 0; x < Const.screenW; x++)
            {
                screenCurrent[y, x] = 32;
                screenUpdate[y, x] = 32;
            }
        }
        Update();
    }

    //지우기
    public void ClearAll()
    {
        for (int y = 0; y < Const.screenH; y++)
        {
            for (int x = 0; x < Const.screenW; x++)
            {
                screenUpdate[y, x] = 32;
            }
        }
    }
    public void ClearLine(int _y)
    {
        if (_y < 0 || _y >= Const.screenH)
        {
            return;
        }

        for (int x = 0; x < Const.screenW; x++)
        {
            screenUpdate[_y, x] = 32;
        }
    }
    //덮어쓰기
    public void WriteLine(int _y, string _s)
    {
        if (_y < 0 || _y >= Const.screenH)
        {
            return;
        }

        int len = _s.Length;
        int l = 0;

        int x;
        for (x = 0; l < len && x < Const.screenW; x++)
        {
            int c = (int)_s[l];
            if (c > 128) //2칸 차지
            {
                if (x + 1 < Const.screenW) //2칸 쓰기
                {
                    screenUpdate[_y, x] = c;
                    screenUpdate[_y, ++x] = 0;
                    l++;
                }
                else //공백으로 처리
                {
                    screenUpdate[_y, x] = 32;
                    break;
                }
            }
            else
            {
                screenUpdate[_y, x] = c;
                l++;
            }
        }

        if (l == len) //주어진 글 다 쓰고 자리가 남으면 공백으로 채움
        {
            for (int i = x; i < Const.screenW; i++)
            {
                screenUpdate[_y, i] = 32;
            }
        }
    }
    public void Write(int _y, int _x, int _w, string _s)
    {
        if (_y < 0 || _y >= Const.screenH)
        {
            return;
        }

        int x_end = (Const.screenW < _x + _w) ? Const.screenW : _x + _w;

        int len = _s.Length;
        int l = 0;

        int x;
        for (x = _x; l < len && x < x_end; x++)
        {
            int c = (int)_s[l];
            if (c > 128) //2칸 차지
            {
                if (x + 1 < x_end) //2칸 쓰기
                {
                    screenUpdate[_y, x] = c;
                    screenUpdate[_y, ++x] = 0;
                    l++;
                }
                else //공백으로 처리
                {
                    screenUpdate[_y, x] = 32;
                    break;
                }
            }
            else
            {
                screenUpdate[_y, x] = c;
                l++;
            }
        }

        if (l == len) //주어진 글 다 쓰고 자리가 남으면 공백으로 채움
        {
            for (int i = x; i < x_end; i++)
            {
                screenUpdate[_y, i] = 32;
            }
        }
    }
    //최종 적용
    public void Update()
    {
        for (int y = 0; y < Const.screenH; y++)
        {
            for (int x = 0; x < Const.screenW; x++)
            {
                if (screenCurrent[y, x] != screenUpdate[y, x])
                {
                    Console.SetCursorPosition(x, y);
                    Console.Write((char)screenUpdate[y, x]);
                    screenCurrent[y, x] = screenUpdate[y, x];
                }
            }
        }
        Console.SetCursorPosition(0, 25);
    }
}

 

 

 

 

2. 스크린을 구현하고 나니 스크린 클래스에서 커서의 위치를 계속 바꾸기 때문에

   일반적인 텍스트 RPG 처럼 사용자 입력을 받을 수 없다는 것을 알았다.
   (스크린 업데이트를 하고 있지 않으면 사용자의 입력값을 볼 수 있긴 하다)


   사실 그보다 게임이 사용자 입력을 받지 않는 동안에도

   사용자 입력이 있으면 자체적으로 버퍼에 받아두었다가 읽어야 할 때 한번에 가져오기 때문에
   사용자가 엔터라도 눌러두었다면 다음 Console.Read는 그냥 넘겨버리는 문제를 해결하고 싶었다.


   그렇게 키 컨트롤러도 만들기로 했다.

 

   구현은 이전 스네이크 게임 만들 때 얻은 아이디어가 있어 금방 구현했다.
   가장 최근의 유효값만 받아오고 버퍼를 비우는 함수를 만들어 주기적으로 호출하면 된다.

   이렇게만 만들면 쉬운데, 중간에 문장으로 된 치트를 받고 싶다는 생각이 들었다.

   그렇게 하려면 버퍼를 그냥 비우면 안되고 순차적으로 치트코드가 들었는지 검사해야 한다.

   그때그때 필요한 치트도 다르게 만들고 한번에 최대 3개의 치트를 검사할 수 있도록 만들기로 했다.

   키 컨트롤러 클래스 개요
   필드 - 치트코드 1,2,3(null 값을 가지면 검사하지 않겠다는 뜻), 치트 입력 진행상황 1,2,3
   메서드 - 입력값 받아오기(유효값 필터를 매개변수로 받고 유효값을 반환, None이면 없음.

                                            발동된 치트 번호 out 키워드로 출력,0이면 없음. )
                 치트 초기화, 치트 등록

 

   아래는 키 컨트롤러 클래스다.

class KeyController
{
    //치트코드는 영문자와 스페이스만 받음
    int[] cheat1 = null;
    int[] cheat2 = null;
    int[] cheat3 = null;
    int cheat1Fill = 0;
    int cheat2Fill = 0;
    int cheat3Fill = 0;


    public ConsoleKey GetUserInput(ConsoleKey[] filter, out int cheatActive)
    {
        ConsoleKey keyReturn = 0;
        ConsoleKey keyInput;
        cheatActive = 0;

        while (Console.KeyAvailable)
        {
            keyInput = Console.ReadKey(true).Key;
            foreach (ConsoleKey k in filter)
            {
                if (keyInput == k) keyReturn = keyInput;
            }

            //치트 검사
            if (cheat1 != null)
            {
                if (cheat1Fill < cheat1.Length)
                {
                    if (cheat1[cheat1Fill] == (int)keyInput)
                    {
                        cheat1Fill++;
                        if(cheat1Fill == cheat1.Length)
                        {
                            cheatActive = 1;
                            cheat1Fill = 0;
                        }
                    }
                    else
                    {
                        cheat1Fill = 0;
                    }
                }
            }
            if (cheat2 != null)
            {
                if (cheat2Fill < cheat2.Length)
                {
                    if (cheat2[cheat2Fill] == (int)keyInput)
                    {
                        cheat2Fill++;
                        if (cheat2Fill == cheat2.Length)
                        {
                            cheatActive = 2;
                            cheat2Fill = 0;
                        }
                    }
                    else
                    {
                        cheat2Fill = 0;
                    }
                }
            }
            if (cheat3 != null)
            {
                if (cheat3Fill < cheat3.Length)
                {
                    if (cheat3[cheat3Fill] == (int)keyInput)
                    {
                        cheat3Fill++;
                        if (cheat3Fill == cheat3.Length)
                        {
                            cheatActive = 3;
                            cheat3Fill = 0;
                        }
                    }
                    else
                    {
                        cheat3Fill = 0;
                    }
                }
            }
        }

        return keyReturn;
    }

    public void ClearCheat()
    {
        cheat1 = null;
        cheat2 = null;
        cheat3 = null;
        cheat1Fill = 0;
        cheat2Fill = 0;
        cheat3Fill = 0;
    }
    public void SetCheat(int n, string s)
    {
        if (n < 1 || n > 3)
        {
            return;
        }

        List<int> cheatCodeInt = new List<int>();
        string cheatCodeStr = s.ToUpper();

        for (int i = 0; i < cheatCodeStr.Length; i++)
        {
            if (cheatCodeStr[i] == ' ' || ('A' <= cheatCodeStr[i] && cheatCodeStr[i] <= 'Z'))
            {
                cheatCodeInt.Add(cheatCodeStr[i]);
            }
        }

        switch (n)
        {
            case 1: cheat1 = cheatCodeInt.ToArray(); break;
            case 2: cheat2 = cheatCodeInt.ToArray(); break;
            case 3: cheat3 = cheatCodeInt.ToArray(); break;
        }
    }
}

 

 

 

 

 

3. 유니티를 사용하면서 중요한 것을 알게 되었다. 

   

   객체지향 언어에서는 게임이 실행중인 모든 순간순간을 전부 씬으로 구분해야 편하다는 것이다.


   근데 유니처럼 씬 내에 오브젝트를 만들 수 없으니 시시콜콜한 차이까지 전부 씬으로 구분해야 할 것 같다.

   씬이 무지막지하게 많아야 한다.


   수많은 씬마다 배경을 따로 만들 수는 없으니 주요 화면 구성 몇개만 배경 씬으로 만들고

   그 안에서 벌어질 다양한 일들은 이벤트 씬으로 구분하기로 했다.

   (맨 위의 Ascii Art는 배경씬 중 하나다)


   수많은 씬들을 어떻게 관리할까 생각하다가 씬 컨트롤러도 만들어

   그 안에 배경 씬과 이벤트 씬을 리스트로 만들어 관리하기로 했다.

 

3종 컨트롤러 구현

 

 

 

 

4. 문제가 발생했다.

 

   플레이어 이름을 받아올 때 만큼은 사용자 입력을 볼 수 없는 키 컨트롤러 대신에

   일반적인 Console.ReadLine()로 입력받고 싶었다.


   그런데 그렇게 사용자가 적어 놓은 입력이 사라지지 않는다!


   Console.Clear로 지우거나 Console.Write 로 덮어쓰거나 Console.MoveBufferArea로 이동해 봐도

   당장만 사라져 보일 뿐,

   썼던 위치로 커서를 다시 옮겨 보면 오른쪽 방향키만 눌러도 과거 입력이 다시 살아나는 걸 볼 수 있다.


   물론 이름 외엔 이런 식으로 입력받을 일이 없을 테니 무시해도 상관 없긴 한데

   혹시라도 스크린의 기능에 잠재적 문제가 생길 것 같았다.


   결국은 Console.ReadLine()을 쓸때만 커서를 아래로 한참 내려 따로 입력받고 돌아오기로 했다.

 

   스크린 좌표는 소중하니까.


   (키 컨트롤러로 사용자가 적은 이름을 실시간 출력해 주는것도 생각해 봤는데

    한글도 쓸 수 있게 만들고 싶기에 패스했다)

 

 

 

 

 

 

 

 

하여튼 우여곡절이 있었지만 문제는 이제 막 기반만 닦았을 뿐, 게임 만들기는 지금부터라는 것이다.

 

당장 만들 수많은 씬과 이벤트는 제쳐 두더라도 전투, 상점, 인벤토리, 데이터 등 할게 너무 많다.

 

 

 

다 할 수 있을까...