내일배움캠프/프로젝트

개인프로젝트 - 텍스트 RPG : 저장,불러오기 구현

서보훈 2024. 9. 24. 19:41

데이터를 저장하고 불러오는 기능을 구현하였습니다.

 


새로운 클래스 : GameSaveManager

게임 저장과 불러오기를 당담하는 클래스 GameSaveManager를 추가하였습니다.

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

더보기
internal class GameSaveManager
{
    private string folderPath;
    private DirectoryInfo directoryInfo;
    private string txtPath;

    private Character charSave;
    private Shop shopSave;

    private List<string> saveDataList = new List<string>();
    public List<string> SaveDataList {  get { return saveDataList; } }

    public GameSaveManager(Character _charSave, Shop _shopSave)
    {
        //객체 생성시, 현재 경로에 save폴더 찾기 경로 지정
        folderPath = Directory.GetCurrentDirectory() + "\\save";
        //경로에 폴더가 없으면 새로만들기
        directoryInfo = new DirectoryInfo(folderPath);
        if (directoryInfo.Exists == false)
        {
            directoryInfo.Create();
        }
        //저장한 텍스트파일 경로
        txtPath = folderPath + "\\savedata.txt";

        charSave = _charSave;
        shopSave = _shopSave;
    }

    public void SaveGame()
    {
        //저장할 리스트를 만들어줌
        ListMaker();
        //리스트의 모든 내용 텍스트에 저장
        File.WriteAllLines(txtPath, saveDataList);
        
    }

    public bool LoadGame()
    {
        //파일이 없으면 불러오기 실패
        if(!File.Exists(txtPath))
        {
            return false;
        }
        else
        {
            //리스트를 비워서 불러올 준비
            saveDataList.Clear();
            //줄 불러오기
            StreamReader reader = new StreamReader(txtPath);
            string readLine;
            while((readLine = reader.ReadLine()) != null)
            {
                //각 줄을 리스트에 저장
                saveDataList.Add(readLine);
            }
            //파일 사용 끝
            reader.Close();
            return true;
        }
    }

    //리스트에 저장할 텍스트를 생성하는 함수
    private void ListMaker()
    {
        //리스트를 비우고 저장할 내용 추가
        saveDataList.Clear();
        saveDataList.Add(charSave.UserName);
        saveDataList.Add(charSave.PlayerJobs.ToString());
        saveDataList.Add(charSave.Level.ToString());
        saveDataList.Add(charSave.HealthPoint.ToString());
        saveDataList.Add(charSave.Exp.ToString());
        saveDataList.Add(charSave.PlayerGold.ToString());

        for(int i = 0; i < charSave.inventory.Length; i++)
        {
            if (charSave.inventory[i] == null)
            {
                saveDataList.Add((-1).ToString());
            }
            else
            {
                saveDataList.Add(charSave.inventory[i].ItemId.ToString());
            }
        }

        for (int j = 0; j < charSave.inventory.Length; j++)
        {
            if (charSave.inventory[j] == null)
            {
                saveDataList.Add((-1).ToString());
            }
            else
            {
                if (charSave.inventory[j].IsEquipped == true)
                {
                    saveDataList.Add(j.ToString());
                }
                else
                {
                    saveDataList.Add((-1).ToString());
                }
            }
        }

        for(int i = 0; i < shopSave.IsSold.Length; i++)
        {
            if (shopSave.IsSold[i] == true)
            {
                saveDataList.Add("true");
            }
            else
            {
                saveDataList.Add("false");
            }
        }
    }
    
}

 

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

//세이브 폴더 경로
private string folderPath;
//세이브 폴더의 존재여부 확인용
private DirectoryInfo directoryInfo;
//세이브 텍스트 파일 경로
private string txtPath;

//저장을 위해 사용될 캐릭터, 상점 객체
private Character charSave;
private Shop shopSave;

//저장 및 불러오기시 사용되는 리스트
private List<string> saveDataList = new List<string>();
public List<string> SaveDataList {  get { return saveDataList; } }

public GameSaveManager(Character _charSave, Shop _shopSave)
{
    //객체 생성시, 현재 경로에 save폴더 찾기 경로 지정
    folderPath = Directory.GetCurrentDirectory() + "\\save";
    //경로에 폴더가 없으면 새로만들기
    directoryInfo = new DirectoryInfo(folderPath);
    if (directoryInfo.Exists == false)
    {
        directoryInfo.Create();
    }
    //저장한 텍스트파일 경로
    txtPath = folderPath + "\\savedata.txt";

    charSave = _charSave;
    shopSave = _shopSave;
}

생성자에서 현재 게임 프로그램이 저장된 경로에 save 라는 이름의 폴더를 검색합니다.

여기서 save 폴더가 없을경우 새로 생성하게됩니다.

 

이후 해당 경로에 텍스트파일 경로를 추가로 붙여서 세이브에 사용될 텍스트파일을 지정한 위치에 생성하거나 불러올 수 있도록 생성자를 만들어주었습니다.

또한 불러오기 시도시 캐릭터와 상점 클래스에 접근해야할 필요가 있기 때문에 두 클래스의 객체도 생성자로 받아줍니다.

 

저장을 시도할경우, 저장할 내용을 임시로 리스트에 넣어 각 리스트 내용을 한줄씩 사용하게 되며 불러오기 시도시 텍스트파일에 저장된 내용을 리스트에 넣어 필요한 클래스에서 사용하게됩니다.

 

세이브 코드 입니다.

public void SaveGame()
{
    //저장할 리스트를 만들어줌
    ListMaker();
    //리스트의 모든 내용 텍스트에 저장
    File.WriteAllLines(txtPath, saveDataList);
}

ListMaker 함수는 저장할 내용을 리스트에 담아주는 함수입니다.

저장을 요청할 때 마다 텍스트파일을 덮어쓰는 방식을 사용하고 있습니다.

 

ListMaker 함수입니다.

이름, 직업, 레벨, 현재 체력, 경험치, 골드량, 아이템 인벤토리 상태와 장착상태, 상점 아이템의 매진 여부를 가져와서  리스트에 저장합니다.

//리스트에 저장할 텍스트를 생성하는 함수
private void ListMaker()
{
    //리스트를 비우고 저장할 내용 추가
    saveDataList.Clear();
    saveDataList.Add(charSave.UserName);
    saveDataList.Add(charSave.PlayerJobs.ToString());
    saveDataList.Add(charSave.Level.ToString());
    saveDataList.Add(charSave.HealthPoint.ToString());
    saveDataList.Add(charSave.Exp.ToString());
    saveDataList.Add(charSave.PlayerGold.ToString());

    for(int i = 0; i < charSave.inventory.Length; i++)
    {
        if (charSave.inventory[i] == null)
        {
            saveDataList.Add((-1).ToString());
        }
        else
        {
            saveDataList.Add(charSave.inventory[i].ItemId.ToString());
        }
    }

    for (int j = 0; j < charSave.inventory.Length; j++)
    {
        if (charSave.inventory[j] == null)
        {
            saveDataList.Add((-1).ToString());
        }
        else
        {
            if (charSave.inventory[j].IsEquipped == true)
            {
                saveDataList.Add(j.ToString());
            }
            else
            {
                saveDataList.Add((-1).ToString());
            }
        }
    }

    for(int i = 0; i < shopSave.IsSold.Length; i++)
    {
        if (shopSave.IsSold[i] == true)
        {
            saveDataList.Add("true");
        }
        else
        {
            saveDataList.Add("false");
        }
    }
}

 

불러오기 코드 입니다.

public bool LoadGame()
{
    //파일이 없으면 불러오기 실패
    if(!File.Exists(txtPath))
    {
        return false;
    }
    else
    {
        //리스트를 비워서 불러올 준비
        saveDataList.Clear();
        //줄 불러오기
        StreamReader reader = new StreamReader(txtPath);
        string readLine;
        while((readLine = reader.ReadLine()) != null)
        {
            //각 줄을 리스트에 저장
            saveDataList.Add(readLine);
        }
        //파일 사용 끝
        reader.Close();
        return true;
    }
}

 

bool 형태를 반환하여 세이브 파일의 존재 여부를 판단하며 세이브파일이 있을경우 텍스트 파일을 열어 한줄씩 리스트에 추가합니다.

 


Character 클래스 - 불러오기 기능

세이브파일을 불러오면 세이브에서 정보를 가져와 캐릭터 정보를 설정하는 기능을 하는 함수입니다.

public void LoadPlayerData(List<string> saveData)
{
    ItemDatabase itemData = new ItemDatabase();
    string name = saveData[0];
    Jobs job;
    switch (saveData[1])
    {
        case "Warrior":
            job = Jobs.Warrior;
            break;
        case "Wizard":
            job = Jobs.Wizard;
            break;
        case "Theif":
            job = Jobs.Theif;
            break;
        case "Archer":
            job = Jobs.Archer;
            break;
        default:
            job = Jobs.Warrior;
            break;
    }
    //이름, 직업을 불러오면서 기본 스텟 세팅
    SetBaseStatus(name, job);
    //레벨 세팅과 동시에 스텟 지정
    Level = int.Parse(saveData[2]);
    baseAttack += (Level - 1) * 0.5f;
    baseDefence += (Level - 1) * 1f;
    HealthPoint = float.Parse(saveData[3]);
    Exp = int.Parse(saveData[4]);
    PlayerGold = int.Parse(saveData[5]);
    //인벤토리 불러오기
    for(int i = 0; i < inventory.Length; i++)
    {
        if (int.Parse(saveData[i + 6]) == -1)
        {
            inventory[i] = null;
        }
        else
        {
            //i+6 번부터 인벤토리 아이템 아이디 저장
            inventory[i] = itemData.GetItemsByID(int.Parse(saveData[i + 6]));
        }
    }

    //아이템 장착
    for(int j = 0; j < inventory.Length; j++)
    {
        if (int.Parse(saveData[j + 16]) != -1)
        {
            EquipItem(inventory[j]);
        }
    }
}

세이브 데이터는 모두 string 형태로 저장되어 있으며, 매개변수로 세이브 데이터를 불러온 리스트를 그대로 받아주기 때문에, 세이브에서 필요한 줄을 개별적으로 지정해주었습니다.

 

기본스텟등의 경우 이후 변경점이 없었기 때문에 직업을 고를때 기본스텟이 정해진 후, 변경점이 없었기 때문에 따로 저장하지 않았습니다.

인벤토리 내용의 경우 해당 슬롯이 비어있을경우 -1, 슬롯에 아이템이 있을경우 아이템 ID를 저장합니다.

이후 인벤토리 내용을 당담하는 10줄에서 -1이 있을경우 슬롯을 비워주고 이외의 숫자가 있을경우 해당 숫자에 맞는 아이템을 추가해줍니다.

-> 만약 아이템에 개별 스텟이 있다면 아이템 상태를 개별적으로 저장하여 불러와야 하겠지만, 지금은 아이템에 개별 스텟이 존재하지 않기때문에 이러한 방식을 선택했습니다.

 

이후 다음 10줄에서 아이템 장착 여부를 확인해줍니다.

마찬가지로 비어있을경우 -1이 저장되며 해당 슬롯의 아이템을 장착하지 않은 경우에도 -1이 저장됩니다.

만약 장착한 아이템이 있을경우 해당 아이템의 인벤토리 슬롯 번호가 저장되며 -1 이외의 값이 있으면 인벤토리의 해당 위치 슬롯의 아이템을 장착합니다.

 


Shop클래스 - 불러오기 기능

현재 상점에서 아이템을 팔면 매진상태가 되고, 플레이어가 상점에서 파는 아이템을 매각할경우 매진상태가 풀리게 됩니다.

해당 내용을 저장하기위해 상점 기능에도 불러오기 기능을 구현하였습니다.

public void LoadSoldInfo(List<string> saveData)
{
    for(int i = 0; i < isSold.Length; i++)
    {
        isSold[i] = bool.Parse(saveData[i + 26]);
    }
}

캐릭터 클래스와 마찬가지로 리스트를 받아오게되며 리스트 마지막의 trua,false 로 저장된 아이템 매진 여부를 불러와서 매진 여부를 저장해둔 배열에 저장하게 됩니다.

 

 


메인함수 변경점 - 저장과 불러오기

필드에 게임 세이브를 위한 클래스를 선언하고, 객체를 생성해주었습니다. 

public static GameSaveManager saveManager = new GameSaveManager(playerChar, shop);

게임을 시작할 때, 먼저 데이터를 확인하고 불러오도록 변경하였습니다.

if (saveManager.LoadGame())
{
    Console.WriteLine("저장된 데이터가 존재합니다.");
    Console.WriteLine("데이터를 불러오겠습니까?");
    Console.WriteLine();
    Console.WriteLine("1. 예\n2. 아니오 (2이외의 입력시 데이터를 불러옵니다.)");
    if (InputInt(Console.ReadLine()) == 2)
    {
        Console.WriteLine("새로운 캐릭터를 생성합니다.");
        Thread.Sleep(1000);
        Console.Clear();
    }
    else
    {
        Console.WriteLine("데이터를 불러옵니다.");
        playerChar.LoadPlayerData(saveManager.SaveDataList);
        Console.WriteLine("불러오기 성공");
        Thread.Sleep(1000);
        isCharCreated = true;
    }
}
else
{
    Console.WriteLine("저장된 데이터가 없습니다. 새로운 캐릭터를 생성합니다.");
    Thread.Sleep(1000);
    Console.Clear();

}

while (!isCharCreated)

데이터를 불러오는데 성공했을경우 데이터를 불러와서 게임을 시작할지, 새로운 게임을 시작할지 선택지가 발생하고 불러오기를 선택하면 캐릭터 생성을 위한 반복문을 실행하지 않습니다.

 

저장된 데이터가 없거나 새로운 게임을 시작할경우 캐릭터를 생성하게 됩니다.

 

6번과 7번 선택지, 저장하기와 게임 종료가 추가되었습니다.

case 6:
    {
        //저장
        Console.Clear();
        TextCreater("저장하시겠습니까?");
        Console.WriteLine();
        TextCreater("1. 예\n0. 아니오");
        input = InputInt(Console.ReadLine());
        if(input == 1)
        {
            SaveGame();
        }
        else
        {
            Console.Clear();
        }
        break;
    }
case 7:
    {
        Console.Clear();
        //게임 종료
        TextCreater("종료하시겠습니까?");
        Console.WriteLine();
        TextCreater("1. 예\n0. 아니오");
        input = InputInt(Console.ReadLine());
        {
            Console.Clear();
            if(input == 1)
            {
                wantToEnd = true;
                TextCreater("저장하시겠습니까?");
                Console.WriteLine();
                TextCreater("1. 예\n0. 아니오");
                input = InputInt(Console.ReadLine());
                if (input == 1)
                {
                    SaveGame();
                }
                else
                {
                    TextCreater("저장하지 않고 게임을 종료합니다");
                    Thread.Sleep(1000);
                    Console.Clear();
                }
                break;
            }

6번 선택지는 저장 여부를 확인한 뒤, 다시 메인 메뉴 선택지로 돌아가게됩니다.

7번 선택지의 경우 게임을 종료하기 전, 저장여부를 물어보며 선택은 저장 여부에만 영향을 끼칩니다.

예를 선택할 경우 게임을 저장하고 종료하며, 아니오를 선택하면 저장하지 않고 게임을 종료합니다.

 

게임 진행 반복문 마지막에 게임 종료를위한 if문이 추가되었습니다.

if(wantToEnd == true)
{
    break;
}

 

마지막, 게임을 저장하는 함수입니다.

private static void SaveGame()
{
    saveManager.SaveGame();
    TextCreater("저장이 완료되었습니다");
    Thread.Sleep(1000);
    Console.Clear();
}

GameSaveManager 클래스의 SaveGame() 함수를 호출하며, 저장이 완료되었음을 보여주는 연출이 발생합니다.

 


마치며

블로그에 글을 정리하면서 보니 이 부분에서 예외처리에 힘을 주면 더 완성도 높은 게임을 만들 수 있겠구나 하는 생각이 들게 되었습니다.