내일배움캠프/프로젝트

개인프로젝트 - 텍스트RPG : 상점 구현

서보훈 2024. 9. 23. 17:54

상점 기능을 구현하여 아이템을 사고 파는 기능을 만들어주었습니다.

해당 내용 구현을 위해 상점을 당담하는 새로운 클래스 Shop 을 추가하였습니다.


Character 클래스 변경점 - 경제관련 함수 추가

상점에서 돈을 사용해야하기 때문에, 돈과 관련된 함수가 추가되었습니다.

//돈 사용
public bool UseGold(int usedGold)
{
    if (PlayerGold < usedGold)
    {
        return false;
    }
    else
    {
        PlayerGold -= usedGold;
        return true;
    }
}

//돈 획득
public void GetGold(int gold)
{
    PlayerGold += gold;
}

돈을 획득할경우에는 PlayerGold 프로퍼티에 값을 추가하는 기능만을 하지만, 돈을 사용할 경우에는 돈이 충분한지 확인하고 사용해야하기 때문에 매개변수와 현재 소지금을 비교하고, 소지금이 충분한 경우에만 돈이 사용됩니다.

 

또한 이 함수를 통해서 사용 성공, 실패를 판단할 수 있도록 bool 값을 반환하도록 만들어주었습니다.

 

아이템 구매시 인벤토리가 꽉 차있으면 구매를 막기 위해서 이를 알 수 있는 함수를 만들어주었습니다.

//인벤토리가 꽉찬 상태인지 여부 -> true면 꽉찬 상태
public bool FullInven()
{
    foreach(Item item in inventory)
    {
        if(item == null)
        {
            return false;
        } 
    }

    return true;
}

 

아이템 판매시, 인벤토리에서 아이템을 삭제해주어야 하기 때문에, 해당 기능을 하는 함수를 만들어주었습니다.

//인벤토리에서 아이템 삭제
public void RemoveItem(int index)
{
    //비어있는 슬롯 삭제 시도시 아무것도 하지 않음
    if (inventory[index] == null)
    {
        return;
    }
    //장착중인 아이템 삭제 시도시 아무것도 하지 않음
    if (inventory[index].IsEquipped)
    {
        return;
    }

    inventory[index] = null;
}

 

Character 클래스의 변경점은 여기까지입니다.


ItemDatabase 클래스 수정 - 아이디를 통해 아이템 찾기

Shop 클래스를 만들때, 상점의 아이템 배열에 쉽게 아이템을 저장하기위해서 아이템 아이디를 통해 아이템을 찾을수 있는 기능을 추가하였습니다.

//아이템 정보를 아이템에 부여된 아이디를 통해 가져옴
public Item GetItemsByID(int _itemId)
{
    Item returnItem = items.First();

    foreach(Item item in items)
    {
        if(item.ItemId == _itemId)
        {
            returnItem = item;
            break;
        }
    }

    return returnItem;
}

만약, 매개변수로 사용한 아이템 id가 데이터베이스에 존재하지 않을경우 가장 처음 만들어진 아이템인 아이디 1번 : 처음만들어진 검이 반환됩니다.

 

즉, 해당 아이템은 잘못된 경로로 아이템 획득시 생성되는 디버그용 아이템이 될 예정입니다.


새로운 클래스 : Shop 클래스

해당 클래스는 상점에서 판매할 아이템 배열과 아이템을 구매, 판매할때 호출되는 함수를 가지고 있는 클래스입니다.

아래는 클래스의 코드 전체입니다.

더보기
namespace TextRPG_SBH
{
    internal class Shop
    {
        private Item[] shopItems = new Item[6];
        public Item[] ShopItems { get { return shopItems; } }
        private bool[] isSold = new bool[6];

        private ItemDatabase itemDb;

        private Character playerChar;

        public Shop(Character _playerChar, ItemDatabase db)
        {
            //캐릭터 생성 후, 객체를 메인에서 가져옴
            itemDb = db;
            playerChar = _playerChar;

            //아이템 데이터베이스에서 상점에 판매할 아이템 정보를 가져옴
            for (int i = 0; i < shopItems.Length; i++)
            {
                shopItems[i] = itemDb.GetItemsByID(i + 2);
                isSold[i] = false;
            }
        }

        //플레이어의 아이템 판매
        public int SellItem(Item soldItem , bool forText = false)
        {
            int itemPrice = (int)(soldItem.ItemPrice * 0.5f);
            //상점에 있는 아이템 판매시, 재입고
            for(int i = 0; i < shopItems.Length; i++)
            {
                if(shopItems[i].ItemId == soldItem.ItemId && forText == false)
                {
                    isSold[i] = false;
                }
            }

            return itemPrice;
        }

        //플레이어의 아이템 구매
        public bool BuyItme(int indexNum)
        {
            //인벤토리가 꽉차있으면 작동하지 않음
            if(playerChar.FullInven())
            {
                return false;
            }

            //돈이 충분하면 돈을 차감하고 아이템 추가
            if (playerChar.UseGold(shopItems[indexNum].ItemPrice))
            {
                playerChar.GetItem(shopItems[indexNum]);
                //상점 아이템 구매시, 매진 표기
                isSold[indexNum] = true;
                return true;
            }
            else
            {
                return false;
            }
        }

        //상점 아이템 정보 출력
        public string ShopItemText(int indexNum)
        {
            string itemText;
            //팔린 아이템이면 매진 출력
            if (isSold[indexNum])
            {
                itemText = "----Sold Out----";
            }
            else
            {
                itemText = "- " + ShopItems[indexNum].InvenItemText() + " | " + ShopItems[indexNum].ItemPrice + " G";
            }

            return itemText;
        }
    }
}

 

클래스의 필드와 생성자 입니다.

상점에서 판매하는 아이템의 배열과 이 배열에 접근할 수 있는 프로퍼티를 가지고 있으며, 판매 여부를 확인할 bool 배열을 필드로 선언해주었습니다.

 

또한 이 클래스에서 사용할 아이템 데이터베이스 클래스와 캐릭터 클래스를 필드에 선언하였습니다.

private Item[] shopItems = new Item[6];
public Item[] ShopItems { get { return shopItems; } }
private bool[] isSold = new bool[6];

private ItemDatabase itemDb;

private Character playerChar;

public Shop(Character _playerChar, ItemDatabase db)
{
    //캐릭터 생성 후, 객체를 메인에서 가져옴
    itemDb = db;
    playerChar = _playerChar;

    //아이템 데이터베이스에서 상점에 판매할 아이템 정보를 가져옴
    for (int i = 0; i < shopItems.Length; i++)
    {
        shopItems[i] = itemDb.GetItemsByID(i + 2);
        isSold[i] = false;
    }
}

 

필드에서 클래스를 선언만 하고 생성자에서 해당 클래스의 객체를 가져오게됩니다.

생성자를 통해 객체를 가져오지 않을경우, 메인에서 사용되는 캐릭터클래스, 아이템 데이터베이스와 객체가 달라지기 때문에 오류가 발생하게됩니다.

 

특히 아이템 데이터베이스는 함수를 통해 메인에서 초기화 시켜주기 때문에 메인에서 해당 클래스의 객체를 가져와주어야 합니다

 

실제로 유니티에서 게임을 만들경우, 이러한 클래스를 싱글톤화 시켜줄 필요가 있습니다.

 

플레이어가 상점에 아이템을 팔 경우 호출되는 함수 입니다.

이 함수에서 판매가격을 계산하여 표기하기 때문에 int 값을 반환하도록 만들어주었습니다.

//플레이어의 아이템 판매
public int SellItem(Item soldItem , bool forText = false)
{
    int itemPrice = (int)(soldItem.ItemPrice * 0.5f);
    //상점에 있는 아이템 판매시, 재입고
    for(int i = 0; i < shopItems.Length; i++)
    {
        if(shopItems[i].ItemId == soldItem.ItemId && forText == false)
        {
            isSold[i] = false;
        }
    }

    return itemPrice;
}

상점에서 판매하는 아이템을 팔 경우 상점의 매진상태를 해제해줍니다.

여기서 텍스트 출력용으로 해당 함수를 사용할경우에도 매진상태가 해제되는 문제가 발생하기 때문에 매개변수로 forText 를 선언하여 해당 매개변수가 true로 들어올경우 매진상태를 해제하지 않도록 만들어주었습니다.

 

플레이어가 아이템을 구매할 때 사용되는 함수입니다.

//플레이어의 아이템 구매
public bool BuyItme(int indexNum)
{
    //인벤토리가 꽉차있으면 작동하지 않음
    if(playerChar.FullInven())
    {
        return false;
    }

    //돈이 충분하면 돈을 차감하고 아이템 추가
    if (playerChar.UseGold(shopItems[indexNum].ItemPrice))
    {
        playerChar.GetItem(shopItems[indexNum]);
        //상점 아이템 구매시, 매진 표기
        isSold[indexNum] = true;
        return true;
    }
    else
    {
        return false;
    }
}

먼저 플레이어의 인벤토리가 꽉 찬 상태인지 확인하고 꽉차있지 않은 경우에만 아이템을 구매 할 수 있도록 해주었습니다.

 

이후 소지금 여부를 판정하여, 소지금이 충분할경우에만 아이템을 얻을수 있도록 만들어주었습니다.

 

마지막으로 상점에서 아이템의 정보텍스트를 출력해주는 함수입니다.

//상점 아이템 정보 출력
public string ShopItemText(int indexNum)
{
    string itemText;
    //팔린 아이템이면 매진 출력
    if (isSold[indexNum])
    {
        itemText = "----Sold Out----";
    }
    else
    {
        itemText = "- " + ShopItems[indexNum].InvenItemText() + " | " + ShopItems[indexNum].ItemPrice + " G";
    }

    return itemText;
}

해당 함수의경우, 상점에서는 아이템의 가격을 표기해야하며, 아이템을 구매했을경우 매진 표기를 해 주어야하기 때문에 만들어주었습니다.

 


메인함수 - 상점 구현

게임이 진행되는 메인 함수에 상점을 구현해주었습니다.

추가적인 변경점으로, 기존 if ~ else if 문이였던 메인메뉴 입력을 switch 문으로 변경하였습니다.

case 3:
    {
        //상점 반복
        while (true)
        {
            int resetPos = OpenShop();
            Console.WriteLine();
            Console.WriteLine("1.아이템 구매\n2.아이템 판매\n0.나가기");
            input = InputInt(Console.ReadLine());
            if (input == 1)
            {
                ResetShop(resetPos);
            }
            else if (input == 2)
            {
                SellItem(resetPos);
            }
            else
            {
                Console.Clear();
                break;
            }
        }
        break;
    }

1, 2 이외의 입력을 받기 전까지 이 내용을 계속 반복하게되며, 이외의 입력을 받으면 메뉴를 표기하는 while문으로 돌아갑니다.

 

 

해당 내용을 만들기 전 캐릭터 생성을 확인할 때, 1을 눌러 캐릭터 생성을 완료하면 상점의 객체를 생성합니다.

이때 상점에 Character 클래스와 ItemDatabase 클래스의 객체를 생성자로 전달하게 됩니다.

//필드에 선언
public static Shop shop;

/*
    생략...
*/

//메인 함수 내부
while (isRight != 1)
{
    Console.Clear();
    Console.WriteLine($"당신의 이름 : {inputName}");
    Console.WriteLine($"당신의 직업 : {playerChar.KorJobName}");

    Console.WriteLine("이 내용이 맞습니까?");
    Console.WriteLine("1. 예    2. 아니오");
    isRight = InputInt(Console.ReadLine());
    Console.Clear();

    if (isRight == 1)
    {
        //캐릭터 생성 완료시, 상점 객체 생성
        shop = new Shop(playerChar, itemDb);
        break;
    }

 

상점을 선택했을때 상점의 내용을 표기하는 함수입니다.

상점을 이용함에 따라서 보유골드와 상점 판매 품목의 변화가 생기기때문에, 내용의 수정이 필요한 커서의 위치를 저장하여 반환하게 만들어주었습니다.

private static int OpenShop()
{
    Console.Clear();
    TextCreater("[상점]");
    TextCreater("아이템을 사거나 팔 수 있습니다.");
    Console.WriteLine();
    //보유골드와, 아이템 정보를 수정하는 시작점
    int resetPosition = Console.GetCursorPosition().Top;

    string moneyText = "보유골드 : " + playerChar.PlayerGold + " G";
    TextCreater(moneyText);
    Console.WriteLine();

    TextCreater("판매 아이템 목록");
    for(int i = 0; i < shop.ShopItems.Length; i++)
    {
        TextCreater(shop.ShopItemText(i));
    }

    //시작점 반환
    return resetPosition;
}

이 외에는 인벤토리와 비슷하게 Shop 클래스로부터 상점 텍스트를 받아 화면에 상점 내용을 표기하게 됩니다.

 

다음으로 상점에서 구매기능을 선택하였을때 사용되는 함수입니다.

상점에서 판매중인 물건 1 ~ 6 이외의 입력을 할경우 해당창이 종료되고 상점 기능 선택 창으로 돌아가게 됩니다.

//아이템 구매시 사용되는 함수
private static void ResetShop(int resetPos)
{
    while (true)
    {
        //상점 정보 초기화 시작지점을 받아온뒤, 그 아래의 텍스트를 모두 지움
        LineClear(Console.GetCursorPosition().Top - resetPos);
        Console.SetCursorPosition(0, resetPos);
        //골드 표기 새로고침
        string moneyText = "보유골드 : " + playerChar.PlayerGold + " G";
        Console.WriteLine(moneyText);
        Console.WriteLine();
        
        //판매 아이템 목록 새로고침
        Console.WriteLine("판매 아이템 목록");
        for (int i = 0; i < shop.ShopItems.Length; i++)
        {
            //상점용 텍스트를 Shop 클래스에서 가져옴
            Console.WriteLine((i + 1) + "." + shop.ShopItemText(i));
        }

        Console.WriteLine();

        TextCreater("구매할 아이템 번호 입력 (이외 입력시 종료)");
        int input = InputInt(Console.ReadLine());
        if (input <= 6 && input >= 1)
        {
            if(shop.BuyItme(input - 1))
            {
                TextCreater("---구매 완료---");
            }
            else
            {
                TextCreater("돈이 부족합니다.");
                Thread.Sleep(100);
            }
        }
        else
        {
            break;
        }
    }
}

텍스트를 모두 지우고 다시 출력하며, 이때 아이템 목록 옆에 숫자를 표기하여 선택에 도움을 줍니다.

 

이후 입력을 받아 목록에 표기된 번호를 입력할경우 소지금의 상황에 따라 아이템의 구매에 성공하거나 실패합니다.

이후 해당 내용을 다시 표기하여 돈과 상점 상태를 업데이트해주며, 1 ~ 6 이외의 입력시 이전화면인 상점기능 선택화면으로 돌아갑니다.

 

아이템 판매시 사용되는 함수입니다.

이전 함수와 마찬가지로 상태 업데이트를 위한 기능이 포함되어있습니다.

//아이템 판매시 사용되는 함수
private static void SellItem(int resetPos)
{
    while(true)
    {
        //상점 정보 출력 초기화용
        LineClear(Console.GetCursorPosition().Top - resetPos);
        Console.SetCursorPosition(0, resetPos);
        //골드 새로고침
        string moneyText = "보유골드 : " + playerChar.PlayerGold + " G";
        Console.WriteLine(moneyText);
        Console.WriteLine();

        TextCreater("인벤토리 아이템 목록");
        //인벤토리 아이템 목록 출력, 없으면 번호만 출력함
        for(int i = 0; i < playerChar.inventory.Length; i++)
        {
            string itemText;
            if (playerChar.inventory[i] != null)
            {
                itemText = (i + 1) + ". " + playerChar.inventory[i].InvenItemText() + " | " + shop.SellItem(playerChar.inventory[i], true) + " G";
            }
            else
            {
                itemText = (i + 1) + ". ";
            }
            Console.WriteLine(itemText);
        }
        Console.WriteLine();
        Console.WriteLine("판매할 아이템의 번호를 입력해주세요.(이외 입력시 종료)");
        int input = InputInt(Console.ReadLine());
        if(input>= 1 && input <= 10)
        {
            //선택한 인벤토리 슬롯이 비어있으면 판매 실패
            if (playerChar.inventory[input - 1] != null)
            {
                //아이템이 장착중이면 판매되지 않음
                if (playerChar.inventory[input - 1].IsEquipped == false)
                {
                    playerChar.GetGold(shop.SellItem(playerChar.inventory[input - 1]));
                    playerChar.RemoveItem(input - 1);
                    TextCreater("판매 성공");
                    Thread.Sleep(200);
                }
                else
                {
                    TextCreater("장착중인 아이템은 판매할 수 없습니다.");
                    Thread.Sleep(200);
                }
            }
            else
            {
                TextCreater("선택한 슬롯이 비어있습니다.");
            }
        }
        else
        {
            break;
        }
    }
}

이전과 다르게 상점이 아닌 인벤토리의 내용을 표기하게되며, 1 ~ 10을 선택하여 인벤토리의 아이템을 판매하게 됩니다.

마찬가지로 1 ~ 10 이외의 입력을 할 경우 이전창인 상점 기능 선택창으로 돌아가게 됩니다.

 

또한 선택한 번호의 인벤토리 슬롯이 비어있을경우, 해당 아이템이 장착되어있을경우 판매에 실패하며 판매에 성공시 Shop 클래스의 함수를 통해 Item의 ItemPrice 프로퍼티의 절반의 골드를 얻게됩니다.

 


마지막으로 아이템 데이터베이스에 추가된 아이템 목록입니다.

private List<Item> items = new List<Item>();
public List<Item> Items {  get { return items; } }

public void ResetItemList()
{
    items.Add(new Item(1, "시작의 검", "이 세상에 처음 만들어진 검", ItemType.Weapon, 1, 0, 50));
    items.Add(new Item(2, "조잡한 검", "이름없는 대장장이가 처음 만들어낸 검", ItemType.Weapon, 3, 0, 150));
    items.Add(new Item(3, "천 갑옷", "가볍고 평범한 갑옷", ItemType.Armor, 0, 2, 100));
    items.Add(new Item(4, "롱소드", "평범한 직검", ItemType.Weapon, 8, 0, 700));
    items.Add(new Item(5, "가죽 갑옷", "질긴 가죽으로 만들어진 갑옷", ItemType.Armor, 0, 5, 600));
    items.Add(new Item(6, "츠바이핸더", "높은 파괴력의 양손검", ItemType.Weapon, 20, 0, 2000));
    items.Add(new Item(7, "판금 갑옷", "금속판으로 만들어진 튼튼한 갑옷", ItemType.Armor, 0, 15, 1800));
}

프로그램 시작과 동시에 리스트에 아이템이 추가되는 방식이기 때문에, 이 클래스의 새로운 객체를 만들경우 문제가 발생하게 됩니다.

 

즉, 해당 클래스는 단 하나만 존재해야하며 Shop과 Character 의 인벤토리에 자유로운 접근이 필요한 클래스이기 때문에 싱글톤 패턴화 시켜줄 대상이 됩니다.

 

 

이상으로 상점기능의 구현 과정입니다.

 

다음 목표는 던전기능의 구현입니다.