빌드파일 내부가 아니라, 외부의 경로에서 게임의 데이터를 관리하는 방법을 알아본다. 파일로 만들어 데이터베이스에 보관하기 위해서 쓰는 CSV, JSON을 알아보자

  • 게임 같은 프로그램에는 로직 뿐만 아니라 기초 데이터도 존재한다. 개별 오브젝트의 능력치 등을 저장하는 능력치 데이터, 혹은 이어하기 같은 기능을 위한 세이브 데이터 등이 있다
  • CSV, JSON데이터베이스 구성과 게임 저장 기능 구현을 해보자

CSV

  • Comma-Separated Values
  • 쉼표를 기준으로 구분된 값들 (으로 구분 된 것은 TSV)
  • 기초 데이터들의 대표적인 예시로 몬스터나 플레이어, 아이템의 기본 능력치가 있다. 이 데이터들을 모두 스크립트로 작성하게 된다면, 프로그래머가 할 일이 너무 많다. 단순히 데이터를 수정할 뿐인데 그때마다 빌드를 해야 한다!
  • 이 문제를 해결하기 위해 별도의 데이터베이스를 구성하고, 게임 구동시 데이터베이스의 정보를 바탕으로 각 오브젝트들의 능력치를 정해주는 구조를 도입할 수 있다. 데이터베이스를 이룰 수 있는 가장 기본적인 구조인 CSV파일을 알아보자

주의점

  • 게임 자체가 원본데이터를 들고 있지 않는다. 해킹, 핵 등에 악용될 수 있기 때문
  • 게임 속 대사 같은것들을 CSV로 다룬다 했을때, 대사에 쉼표가 들어가면 쉼포를 기준으로 나뉜다. "대사"와 같이 큰따옴표 안에 넣으면 나뉘지 않음

CSV 파일 만들기

  • 위와 같이 데이터 시트를 작성 하자(엑셀 또는 VS코드로 작성)
  • 엑셀의 경우에는 위와같이 저장하면 된다. CSV 파일 형식을 지정하자

스크립트 작성

  • 스크립트들은 유니티 패키지 안에 있는 소스 코드들을 보면서 복습하자(너무 길다)
  • 여기서는 짚어볼 만한 내용들을 기록한다

CSV 클래스

  • CSV 구조를 위한 클래스다. CSV TableCSV Dictionary를 위한 파일 경로, 구분 기호를 처리하기 위한 공통적인 속성과 메서드를 가진다. MonoBehaviour를 상속받지 않는다
[SerializeField] private string _filePath;
public string FilePath => Path.Combine(CsvReader.BasePath, _filePath);

[field: SerializeField] public char SplitSymbol { get; private set; }

// 생성자를 통해 생성 가능
protected Csv(string path, char splitSymbol)
{
    _filePath = path;
    SplitSymbol = splitSymbol;
}

CSV Reader

  • CSV 파일을 읽어서 정해진 데이터 구조(현재는 이차원 배열 또는 Dictionary 자료구조)로 로드를 한다
  • CSV Table 또는 CSV Dictionary 와 같이 CSV 클래스를 상속받은 클래스의 인스턴스를 생성
  • Read()를 통해 CSV파일에서 데이터를 로드
// 전달받은 Csv 객체를 기준으로 CSV 파일을 읽고 데이터 구조에 맞게 로드하는 메서드
public static void Read(Csv csv)
{
    // 파일 경로 유효성 검사 및 내용이 비어있는지 검사, 실패하면 리턴
    if (!IsValidPath(csv) || 
        !IsValidEmpty(csv, out string[] lines))
        return;

    bool isReadSuccessful; // 로드 성공 여부 저장용 변수

    // Csv 객체의 실제 타입에 따라 다르게 처리
    switch (csv)
    {
        case CsvTable table: // CsvTable 타입인 경우
            isReadSuccessful = ReadToTable(table, lines); // 2D 배열 형태로 로드
            break;
        case CsvDictionary dictionary: // CsvDictionary 타입인 경우
            isReadSuccessful = ReadToDictionary(dictionary, lines); // Dictionary 형태로 로드
            break;
        default: // 그 외의 타입은 실패 처리
            isReadSuccessful = false;
            break;
    }
}

CSV Table

  • CSV 파일 데이터를 2차원 배열로 표현하는 클래스. 행,렬 인덱스를 통해 접근한다
public string GetData(Vector2Int vector2)
{
    (int, int) rc = GetClampTableIndex(vector2.y, vector2.x);
    return Table[rc.Item1, rc.Item2];
}
  • 튜플 형식으로 데이터를 반환한다. 가독성을 높여서 쓰면 다음과 같다
public string GetData(Vector2Int vector2)
{
    (int row, int col) rc = GetClampTableIndex(vector2.y, vector2.x);
    return Table[rc.row, rc.col];
}
  • 단, C# 7 버전 이상에서만 가능

Tuple types

System.Serializable

  • Attribute중 하나다. 사용하면, 시스템에 의해 직접적으로 데이터가 직렬화 될 수 있음을 나타낸다. 에디터 상에서 데이터를 직접 편집 할 수 있다. ClassStruct 위에 써준다

CSV Dictionary

  • 딕셔너리 자료구조 방식으로 데이터를 불러온다
public string GetData(string row, string column)
{
    if (Dict.TryGetValue(row, out var columns) && columns.TryGetValue(column, out var value))
    {
        return value;
    }

    Debug.LogError($"Data not found for row '{row}' and column '{column}'.");
    return null;
}
  • 딕셔너리에 담겨있는 자료 데이터를 불러오는 함수다. 조건문 if 안쪽에서 순서대로 보자
    1. Dict 딕셔너리에서 row 키를 넣어서 나오는 값을 columns에 담는다. 여기선 string으로 담긴다 없으면 에러 로그를 띄우고 null 반환
    2. && 논리 연산자 이후, columns 딕셔너리에서 column 키 값을 넣어서 나오는 값을 value에 담는다. 없으면 에러 로그를 띄우고 null반환
    3. 조건문을 만족하면 value가 반환된다

Test

  • 앞서 만든 CSV 파일을 가지고 테스트를 진행해본다
public class Tester : MonoBehaviour
{
    // 유니티 인스펙터 및 생성자에서 초기화
    [SerializeField] private CsvTable _tableCSV;
    [SerializeField] private CsvDictionary _dictCsv = new("DataTable/CsvTemp.csv", ',');
    
    public Vector2Int dataTablePos;
    
    private void Start()
    {
        // 읽기
        CsvReader.Read(_dictCsv);
        CsvReader.Read(_tableCSV);
        
        // Table 구조는 int타입 및 Vector2Int를 사용해 데이터 참조
        Debug.Log(_tableCSV.GetData(2, 3));
        Debug.Log(_tableCSV.GetData(dataTablePos));
        
        // Dictionary 구조는 string 두 개를 사용해 행, 열 데이터 참조
        Debug.Log(_dictCsv.GetData("주황버섯", "설명"));
        
        // 두 형태 모두 하나의 행을 통째로 string배열로 받아오는 GetLine기능 지원
        string[] strArr1 = _tableCSV.GetLine(4);
        string[] strArr2 = _dictCsv.GetLine("옥토퍼스");
    }
}
  • 테스트 스크립트다. 빈 오브젝트에 컴포넌트로 추가하고, 인스펙터 창에서 설정한다
  • File Path : Assets 폴더를 기준으로 기입하면 된다
  • Split Symbol : 각 데이터를 쪼개는 기준 부호. CSV,을 기준으로 쪼갠다
  • Data Table Pos : 찾고자 하는 데이터의 열, 행을 입력한다

실행결과

ID레벨공격력설명
파란슬라임11570젤리같이 생겼다. 탄산젤리
빨간달팽이2610느리다. 느림. 모험가들에게 좋은 경제 수급원이다
주황버섯3815평생을 뛰어다닌다. 그 고행을 이겨내면 머쉬맘이 된다는 전설이 있다
옥토퍼스41025공중에 떠있는것 같지만, 사실 다리로 걸어다닌다
핑크빈51502000귀여운 외모와 달리 최강
  • CSV 자료 데이터를 가지고 수행한다
  • 먼저 Table로 된 CSV만 수행했다
  • (2,3) -> 2행3열 -> 10
  • dataTablePos -> 클램프 되어서 15를 넣어도 최대가 5가 된다 -> (y = 5, x = 2) -> 150
  • 그 아래는 4번째 줄(옥토퍼스)의 출력 결과다

    주의

    • 만약에 대사나 설명 안에 ,가 있을 경우 그 뒤의 내용은 짤린다. 이번에는 옥토퍼스의 설명 뒷내용이 짤렸다

  • 이번에는 딕셔너리 방식의 CSV 내용들만 수행했다. string을 키 값으로 데이터를 불러온다는 것만 다르다
  • 라인을 불러오는 함수의 경우, 구현 방식의 차이 때문에 첫 열의 내용(이름)이 빠져있다. 이는 키 값이 첫 열이기 때문이다. 원한다면 키 값을 첫 열로 지정해서 구현하면 된다

#if UNITY_EDITOR

#if UNITY_EDITOR                                       // 유니티 에디터 환경인 경우
                    return Application.dataPath + Path.DirectorySeparatorChar;  // Assets 폴더 경로 반환
#else                                                  // 빌드된 애플리케이션 환경인 경우
                    return Application.persistentDataPath + Path.DirectorySeparatorChar; // 해당 플랫폼 저장 경로 반환
#endif
  • 빌드 전까지만 작동하는 기능이다. 빌드 후에는 제거된다

JSON

  • JsonUtilityToJson, FromJson이 메인이 되는 기능들이고, 이들을 통해 세이브로드를 구현한다

ToJson, FromJson

  • ToJson: json 파일로 바꿔준 다는 의미다.
  • FromJson: json 파일을 불러와 사용할 수 있게 파싱한다

세이브 파일

.json 확장자명으로 지정한 경로에 저장된다. 데이터들이 직렬화 되어 저장된다

  • 요런 느낌으로 저장된다

Binary Save

  • 이진형태로 저장 가능. 세이브 데이터의 변형을 막는 데 도움이 됨
 public override void Save<T>(T target)
 {
     Directory.CreateDirectory(BasePath);
     
     string filePath = GetFilePath(target.FileName);
     string jsonString = JsonUtility.ToJson(target);
     
     using (FileStream fs = new FileStream(filePath, FileMode.Create))
     {
         byte[] bytes = Encoding.Default.GetBytes(jsonString);
         
         BinaryFormatter bf = new BinaryFormatter();
         bf.Serialize(fs, bytes);
     }
     
     IsFileAccessible(filePath);
 }

왜 using을 쓰는가?

  • 파일, 네트워크, 데이터베이스 연결 등 외부 리소스는 자동으로 정리하지 않음
  • using으로 묶어서 쓰면, 구문을 벗어날 때 자동으로 Dispose()를 해서 메모리 해제를 함 -> 메모리 누수 방지

CSV와 JSON 차이

  • CSVDB와 같이 데이터들을 한데 모아놓고 뽑아 쓸 때 쓰기 좋다
  • JSON은 세이브파일과 같이 파일 형태로 데이터들을 모아두고 가져다 쓸 때 좋다

실시간 저장

  • 다크소울, 항아리 게임과 같이 게임의 상태가 실시간으로 자동 저장되는 기능
  • 일정한 시간마다 Json에 저장하거나, 특정한 동작에 옵저버 패턴으로 저장하는 기능 수행을 하면 가능
  • 강제 종료시에도 저장되는 것은 모르겠음
profile
개발 박살내자

0개의 댓글