게임개발/C#

24-09-25 Text RPG 구현하기

몰록 2024. 9. 25. 21:03

 

오늘 드디어 Text RPG를 마무리하고 재출하는 날이 되었다. 그동안 작성한 코드들이 정말 많았으나,

어떻게든 이 글에서 구현하는동안 배운것과 구현한 과정들을 요약 정리 해보도록 하겠다.


 

 

프로젝트를 한 클래스 안에서 클래스를 또 정의하고 메소드를 만들고

하다가 몇백 줄이 넘어갔었는데,

유니티에서 했던것처럼 스크립트 파일을 class별로 나누어 정리를 하다

보니 이번엔 폴더를 쫙 펼치니 파일 주소 목록이 이렇게 길어졌다 ㅋㅋ

 

하지만 이렇게 나누어 관리를 하는것이 그렇지 않은것보다 몇백배는 더 편했고, 또 기존에 정의한 class를 파일로 빼는 단축키로..아....................어.........

......지금은 까먹어 버렸다......GPT 비서야 도와줘!

 

[GPT가 답변해준 것]

{

Visual Studio에서는 "형식을 파일로 이동(Move Type to File)" 기능을 사용하여 클래스나 인터페이스를 손쉽게 별도의 .cs 파일로 분리할 수 있습니다.

 

단축키를 이용한 방법

  1. 클래스 또는 인터페이스의 이름 위에 커서를 놓습니다.
  2. 키보드에서 Ctrl + . (컨트롤 키와 마침표 키)를 누릅니다.
    • 그러면 "빠른 작업 및 리팩터링" 메뉴가 나타납니다.
  3. 나타난 메뉴에서 "형식을 '{클래스명}.cs'로 이동" 또는 "Move Type to '{클래스명}.cs'" 옵션을 선택합니다.
    • 화살표 키로 해당 옵션을 선택한 후 Enter 키를 누르거나, 마우스로 클릭합니다.

}

정확히 답변해줬다. 이렇게 해서 기존에 작성한 스크립트 내 class/interface를 cs파일로 이동시켜 관리해줄수 있다. 

 

그러면 파일간의 데이터 상호작용이 궁금할수 있는데, 폴더가 나뉘어진 경우엔 using문을 쓰면 되고, 한 폴더 안에 있으면 public 변수를 문제없이 접근할수 있다.

 

 


 

그래서 어떻게 게임을 만들었는가?


1) 캐릭터 생성

 

나는 개발을 배우기 이전에 진성 게이머였고 RPG게임(롤 플레잉 게임)을 위해선 플레이어가 말그대로 롤 플레이을 할수 있어야 한다고 생각했다. 그래서 과제에서 요구한 필수 구현 요소를 다 건너뛰고(...) 캐릭터 생성 기능부터 만들었다.

 

현재까지 개발이 진행되고 있는 고전 로그라이크 게임 던전 크롤의 캐릭터 선택 시스템
발더스 게이트 3 캐릭터 커마
발더스 게이트 3 캐릭터 선택

 

플레이어가 RPG게임을 시작하면 처음 맞이하는 화면이기 때문에,적절한 선택지의 구현 없이는 다음 단계를 생각조차 할수 없었다. 

 

하지만 그 이전에 종족과 직업 선택이 변하게 해줄 능력치의 정의 없이는 그것또한

구현할수 없었다. 그래서 Character class를 정의하기 시작했다.

 

rpg게임을 플레이하던 경험을 살려

이름,  종족, 직업,

체력,  마력, 레벨,

힘, 민첩, 지능 변수를 정의해줬다.

 

그리고 상상력을 발휘해 여러가지 종족과 여러가지 직업을 만들어내던 와중에

못 다들은 강의를 마저 듣다 그제서야 인터페이스 개념을 배우게 되어 그걸 적용시키기 시작했다.

 

 

Race 폴더에 IRace 인터페이스와 그걸 상속받는 클래스들, 그리고 Class 폴더엔 IClass~..

 

 

이렇게 인터페이스로 묶어 관리를 해주니 똑같은 변수를 조정하는 여러가지 클래스를 더 용이하게 관리할수 있었다.

 

더불어 Character 또한 인터페이스로 관리하고 기존 character 클래스를 player로 개명하고, enemy 클래스를 만들어 player와 똑같은 방식으로 게임과 상호작용 하도록 했다.

 

종족과 클래스가 캐릭터의 능력치를 변동시키는 방식

종족을 먼저 선택해 기본 능력치를 셋팅하고, 그뒤에 클래스를 선택해 능력치가 더해지도록 했다

( 워락같은 경우엔 체력이 줄어들기도 한다)

 

메인 함수에서의 사용 : char를 입력받아 switch문으로 검사해줬다.

 

선택을 잘못 해도 다시 돌아올수 있도록 while문을 걸어주고 switch문을 실행해 입력값에 따라 종족과 직업을 지정하게 해줬다.

 

 

Console

능력치가 잘 적용되서 캐릭터가 생성된 모습이다!! 

 

 


2) 상점과 아이템 시스템

다음으로 만든 시스템은 필수 구현과제인 상점인데, 상점을 만들며 아이템을 출력시키려고 하니, 아이템 클래스를 만들어놓고 상점에서 아이템을 새로 생성시켜줘야했다. 근데 나는 이점이 맘에 들지 않았다.

 

나는 아이템이 사전에 전부 만들어져 있고 상점에서 그것을 가져올수 있도록 하고 싶었다.

그래서 GPT에게 물어보니 추천해준 패턴이 : ItemRepository 패턴! 

 

[GPT,  ItemRepository 패턴이 뭐야?]

{

ItemRepository 패턴이란, 아이템과 관련된 데이터를 중앙에서 관리하고,   

애플리케이션의 다른 부분에서 이 데이터를 일관성 있게 사용할 수 있도록 하는 디자인 패턴입니다.

이를 통해 다음과 같은 장점을 얻을 수 있습니다:

  • 중앙 집중식 관리: 아이템 데이터를 한 곳에서 관리하므로, 아이템을 추가하거나 수정할 때 코드의 여러 부분을 변경할 필요가 없습니다.
  • 재사용성 향상: 동일한 아이템을 게임의 여러 부분에서 쉽게 재사용할 수 있습니다.
  • 유지보수 용이성: 아이템 관련 로직을 한 곳에 모아두어 유지보수가 쉽습니다.
  • 데이터 일관성 보장: 아이템에 대한 변경 사항이 모든 곳에 일관되게 반영됩니다.

내가 마침 찾고있던 방식이 이미 업계에서 넓게 사용되는 디자인 패턴이라니!! 원하는 결과를 잘 찾아다 주는 GPT 칭찬해..

그래서 이 아이템 저장소를 만들어주기 전에, 아이템도 종류별로 클래스와 인터페이스를 나누어 관리해줬다.

 

 

상위 인터페이스 IItem, 그 인터페이스를 상속받는 IConsumable과 IEquippable 인터페이스가 장착 가능한

아이템인 (Armor,Weapon)과 소비 가능한 아이템 (Potion) 을 구분, 상속시켜 관리하게 했다.

 

 

 

[ 빈칸을 채워줄 아무개 단어 대괄호들 ~~ ]

 

[ 빈칸을 채워줄 아무개 단어 대괄호들 ~~ ]

 

 

 

 

ItemRepository.cs

 

상점의 아이템 나열 방식을 다양화시키기 위해 리스트를 아이템 클래스별로 만들어주고

 

ItemRepository.cs

생성자로 아이템들을 생성 관리해주었다. 이렇게 해주면 게임 내의 모든 아이템 데이터를 이 파일 안에서 관리 할수 있기 때문에 굉장히 용이해진다. 

 

그래서 이걸 상점에 어떻게 가져올것이냐..

 

ItemRepository.cs

그것은 아이템을 가져오는 메소드를 만들어주는 것인데,

메소드에서 람다 등을 사용해 값을 가져와 return해 불러올수 있도록 해주었다 ( 작동 방식을 완벽히 이해하진 못했다 )

 

Shop.cs

 

미리 생성해준 Shop Class에서 메소드를 호출해 아이템을 출력해줬다.

 

아이템 시스템에서 언급할 내용이 이것 말고도 또 있는데, 그것은 클래스를 선택할때 주어지는 기본 장비 구현이다.

 

IClass.cs / Fighter.cs

그걸 위해 클래스 인터페이스와 각 클래스들에 리스트를 선언해주고, 생성자에서 리스트에 아이템을 추가해주도록 했다.

 

Player.cs

그리고 Player 클래스의 생성자에 반복문을 추가해줘 장착 메소드를 실행시키게 하였다.

 

기타 관련 코드 블록 : Main class 내의 메소드들

더보기
public static void Inventory(Player player)
{
    if (player.Inventory.Count == 0)
    {
        Console.WriteLine("인벤토리가 비어 있습니다.");
        return;
    }
    Console.WriteLine();
    Console.WriteLine("---------------------------------------------");
    Console.WriteLine("인벤토리 아이템 목록:");
    for (int i = 0; i < player.Inventory.Count; i++)
    {
        IItem item = player.Inventory[i];
        string itemType = item is Weapon ? "무기" : item is Armor ? "방어구" : "아이템";

        // 아이템이 장착된 상태인지 확인
        bool isEquipped = false;
        if (item == player.EquippedWeapon || item == player.EquippedArmor)
        {
            isEquipped = true;
        }
        // 장착된 아이템 앞에 [E] 추가
        string equippedIndicator = isEquipped ? "[E] " : "" ;
        

        Console.WriteLine($"{i + 1}. {equippedIndicator}[{itemType}] {item.Name}");
    }
    Console.WriteLine("---------------------------------------------");
    Console.WriteLine();

    Console.WriteLine("a. 아이템 장착(사용) ");
    Console.WriteLine("b. 아이템 장착해제 ");
    string Choice = Console.ReadLine();

    if (Choice == "a")
    {
        UseItem(player);
    }
    else if (Choice == "b") 
    {
        UnequipItem(player);
    }
    else
    {
        Console.WriteLine("잘못된 입력입니다.");
        return;
    }

}

public static void UseItem(Player player)
{
    

    Console.WriteLine("사용하거나 장착할 아이템의 번호를 입력하세요: ");
    int choice = int.Parse(Console.ReadLine());

    if (choice > 0 && choice <= player.Inventory.Count)
    {
        IItem item = player.Inventory[choice - 1];

        if (item is IConsumable consumable)
        {
            consumable.Consume(player);
            player.Inventory.Remove(item);
        }
        else if (item is IEquippable equippable)
        {
            equippable.Equip(player);
        }
        else
        {
            Console.WriteLine("이 아이템은 사용할 수 없습니다.");
        }
    }
    else
    {
        Console.WriteLine("잘못된 선택입니다.");
    }
}

public static void UnequipItem(Player player)
{
    Console.WriteLine("해제할 장비를 선택하세요:");
    Console.WriteLine("1. 무기 해제");
    Console.WriteLine("2. 방어구 해제");
    Console.WriteLine("3. 취소");
    string choice = Console.ReadLine();

    switch (choice)
    {
        case "1":
            if (player.EquippedWeapon != null)
            {
                player.EquippedWeapon.Unequip(player);
            }
            else
            {
                Console.WriteLine("장착된 무기가 없습니다.");
            }
            break;
        case "2":
            if (player.EquippedArmor != null)
            {
                player.EquippedArmor.Unequip(player);
            }
            else
            {
                Console.WriteLine("장착된 방어구가 없습니다.");
            }
            break;
        case "3":
            Console.WriteLine("취소되었습니다.");
            break;
        default:
            Console.WriteLine("잘못된 선택입니다.");
            break;
    }
}

 

3) 던전 진행

 

던전 진행은 Stage 클래스 내에서 관리했는데, 던전 진행 디자인은 다음과 같다 : 

 

  • 진행도 변수를 추가하고, 최대 진행도 또한 추가해 설정해준다.
  • 던전 진행을 실행할때마다 진행도가 상승하며, 진행도가 상승할때마다 랜덤 인카운터가 발생한다.
  • 던전에 처음 입장할때 '계단' 을 찾을 위치인 특정 진행도가 랜덤으로 정해지며, '계단'을 찾으면 다음 단계로 나갈수 있는 선택지가 열린다.
  • 랜덤 인카운터는 50%로 적 조우, 30%로 상점 발견, 10%로 아이템 발견 이다.

 

그럼 필요한 변수부터 보여주겠다.

Stage.cs

스태이지 층, 진행도와 최대 진행도, 계단과 상점이 발견되었는지와 진행도가 찼을때를 체크할 불리언값까지 정해줬다.

 

Stage.cs

스테이지 생성자에서 필요한 변수들을 설정시키고 Console.write와 Thread.Sleep을 통해 연출을 해주기 시작했다.

이제 이 Stage 클래스를 메인 에서 불러와 반복문이 시작고, 게임 진행 메소드를 호출하면서

게임 플레이가 진행되는 것이다.

 

Stage.cs 내의 메소드들 : Progress를 진행시켜주고 계단 탐색과 랜덤 인카운터 메소드를 호출시킨다.

더보기

Explore 메소드 :

public void Explore(Player player)
{
    if (isCompleted)
    {
        Console.WriteLine("더 탐험할게 없습니다.");
        return;
    }
    else
    {
        Progress += 10;
        int randomtext = random.Next(1, 4);
        switch (randomtext)
        {
            case 1:
                Console.Write("당신은 어두운 방과 던전 사이를 걷습니다.");
                break;
            case 2:
                Console.Write("당신의 길은 어둠과 미지로 가득차있습니다.");
                break;
            case 3:
                Console.Write($"[{player.Name}] : 온통 어둠과 먼지 뿐이군.");
                break;
        }

        Thread.Sleep(500); Console.Write("."); Thread.Sleep(500); Console.Write("."); Thread.Sleep(500); Console.Write("."); Thread.Sleep(500); Console.WriteLine(".");
    }
    
    Console.WriteLine($"진행도 : {Progress}/{MaxProgress}");
    Thread.Sleep(200);

    FindStair(player);
    TriggerEvent(player);

    if(Progress >= MaxProgress)
    {
        if (!StairFound)
        {
            Console.WriteLine("무언가 잘못되었습니다. 스테이지를 다시 시작합니다.");
            //리스타트
        }
        else
        {
            Console.WriteLine($"{StageNumber}층 탐험이 완료되었습니다!");
        }
    }
}

 

Findstair 메소드 : 

public void FindStair(Player player)
{
    if (Progress >= stairposition && !StairFound)
    {
        StairFound = true;
        Console.WriteLine("계단을 발견했습니다! 다음 스테이지로 이동 가능합니다.");

        // 다음 스테이지 진행 선택지 활성화 로직

    }
    else
    {
        return;
    }
}

 

 

TriggerEvent 메소드 :  

public void TriggerEvent(Player player)
{
    int eventChance = random.Next(1, 101);

    if (eventChance <= 50)
    {
        EncounterEnemy(player);
    }
    else if (eventChance <= 70)
    {
        FindShop(player);
    }
    else if (eventChance <= 90)
    {
        FindItem(player);
    }
    else
    {
        Console.WriteLine("아무일도 일어나지 않았습니다...");
    }

}

 

EncounterEnemy 메소드 : 

 private void EncounterEnemy(Player player)
 {
     Console.WriteLine("어둠속에서 무언가가 움직입니다....!!");
     Thread.Sleep(800);
     Console.WriteLine();
     int encountertext = random.Next(1, 3);

     switch(encountertext)
     {
         case 1: 
             Console.WriteLine("역시 적입니다! 전투 준비!");
                 break;
         case 2: 
             Console.WriteLine("적을 만났습니다! 전투에 들어갑니다!");
                 break;
         case 3:
             Console.WriteLine($"[{player.Name}] : 덤벼라!! 너같은 애송이가 내 길을 막게 두지 않겠다!");
                 break;
     }
     Thread.Sleep(800);
     Console.WriteLine();
     Console.WriteLine();
     Console.WriteLine("계속__(아무키나 입력해 진행)");
     Console.ReadLine();
     Console.Clear();

     Combat combat = new();
     //Enemy enemy = null;
     RandomEnemy(out Enemy enemy);

     combat.combat(player, enemy);

     // 전투 로직 구현

 }

 

FindShop 메소드 : 

private void FindShop(Player player)
{
    Console.WriteLine("상점을 발견했습니다!");
    Thread.Sleep(500);
    int encountertext = random.Next(1, 3);

    switch (encountertext)
    {
        case 1:
            Console.WriteLine("이런곳에 상점이라니 기이하군요.");
            break;
        case 2:
            Console.WriteLine("이곳에서 물건을 사거나 휴식할수 있습니다.");
            break;
        case 3:
            Console.WriteLine($"{player.Name} : 이런곳에 상점이라니?");
            break;
    }
    ShopFound = true;
    //상점 선택지 활성화 로직
}

 

FindItem 메소드 : 

private void FindItem(Player player)
{
    Console.WriteLine("무언가 반짝이는게 보입니다!");
    Thread.Sleep(500);
    int eventChance = random.Next(1, 101);
    if (eventChance <= 50)
    {
        int randomGold = random.Next(5 + (StageNumber * 2), 500 + (StageNumber * 5));
        Console.WriteLine("금화를 발견했습니다!");
        player.Gold += randomGold;
    }
    else
    {
        Console.WriteLine("포션을 발견했습니다!");
        player.Inventory.Add(ItemRepository.GetRandomPotion());
        //인벤토리에 포션 추가

    }
    Thread.Sleep(500);
    int encountertext = random.Next(1, 3);
    switch (encountertext)
    {
        case 1:
            Console.WriteLine("횡재로군요.");
            break;
        case 2:
            Console.WriteLine("앞으로의 여정에 도움이 될겁니다.");
            break;
        case 3:
            Console.WriteLine($"{player.Name} : 횡재로군.");
            break;
    }
}

 

 

 

메인 함수 내의 반복문

더보기
while (currentStage <= 20 || currentStage >= 1)
{
    Stage stage = new(currentStage);
    shop = null;
    while (!stage.isCompleted)
    {
        Console.WriteLine("--------------------------------------------------------------------");
        Console.WriteLine("무엇을 하시겠습니까?");
        Console.WriteLine();
        Console.WriteLine("a. 탐험하기");
        Console.WriteLine("c. 캐릭터 정보 보기");
        Console.WriteLine("i. 인벤토리 확인 및 아이템 사용");
        if (stage.ShopFound)
        {
            Random random = new Random();
            int randomshop = random.Next(1, 3);
            switch (randomshop)
            {
                case 1:
                    shop = new Shop(eShop.Armor);
                    break;
                case 2:
                    shop = new Shop(eShop.Consum);
                    break;
                case 3:
                    shop = new Shop(eShop.Weapon);
                    break;
            }
            Console.WriteLine("s. 상점 입장");
        }
        if (stage.StairFound)
        {
            Console.WriteLine("n. 다음 스테이지로");
        }
        Console.WriteLine("E. 게임 종료");
        Console.WriteLine("--------------------------------------------------------------------");

        string input = Console.ReadLine() ??"";
        switch (input)
        {
            case "a":
                stage.Explore(player);
                break;
            case "c":
                player.DisplayCharacterInfo();
                break;
            case "i":
                Inventory(player);
                break;
            case "s":
                if (!stage.ShopFound)
                {
                    Console.WriteLine("잘못된 입력입니다.");
                }
                else
                {
                    Console.WriteLine("당신은 상점으로 향합니다.");
                    shop.BuyItem(player);
                }
                    break;
            case "n":
                if (!stage.StairFound)
                {
                    Console.WriteLine("잘못된 입력입니다.");
                }
                else
                {
                    stage.isCompleted = true;
                }
                break;
            case "E":
                Console.WriteLine("게임을 종료합니다.");
                return;
            default:
                Console.WriteLine("잘못된 입력입니다.");
                break;
        }
        
        if (player.Health <= 0)
        {
            Console.WriteLine("죽었습니다...........");
            return;
        }

    }

 

4) 전투

 

전투 방식은 D&D TRPG 룰을 적용시키고 싶어서, Dice 클래스를 추가해 판정을 만들어줬다.

 

Dice.cs

 

주사위 굴리기 메소드와 주사위 여러개 돌리기 매소드를 생성했다.

 

그리고 Combat 클래스를 생성해 전투 씬을 출력시켜주었다.

 

// 추후에 추가로 기술//

 


 

이렇게 rpg를 구현해주니 도파민이 막 돌고 프로젝트를 개선시킬 방법들이 눈을 감으면 떠오르곤 했다..(잠도 안옴)

아직도 추가하고싶은 요소들이 많다.. 

 

도전 과제에 들어가있는 못다한 요소도 그렇고, 마법, 스킬, 아이템별 능력치 판정 변동등.. 시간이 많이 있으면 계속 많이 추가해보고 싶다. 그래도 게임을 만들며 느껴볼 도파민을 잘 즐긴것 같다.

 

프로젝트 깃허브

 

https://github.com/moloch-kim/-Text-TRPG