배경
게임은 게임 내용을 저장하고 불러오는 기능이 필요하다.
저장과 불러오기는 유저의 하드디스크에 게임 파일을 저장하고 불러오는 일로서, 파일 입출력에 해당한다.
문자열로 패턴화하여 .txt 나 .csv 와 같은 문서를 작성해 게임의 내용을 저장할 수도 있으나
훨씬 더 코드를 작성하기 편하면서 유저가 함부로 세이브파일을 뜯어볼 수 없고 파일 확장자명도 내 마음대로 지정 가능하도록 하기 위해 바이너리 파일로 저장한다.
아이디어
- 저장할 정보들을 담는 구조체를 정의한다.
- 파일 경로과 저장할 정보들을 담는 구조체를 참조해 저장/불러오기 를 구현한다.
- 파일 경로에 해당하는 파일을 바이너리 모드로 연다.
- 구조체의 내용을 구조체의 크기만큼 메모리에 저장/불러오기 한다.
SaveFile 구조체
// saveload.h
#ifndef _saveload
#define _saveload
#include <fstream>
#include "keyword.h"
#include <string>
class SaveLoad
{
public:
#pragma pack(push, 1)
// 저장할 데이터 구조체
// SaveData 객체를 프록시로 코드 내의 변수들과 상호작용하면 된다.
struct SaveData
{
// 저장, 불러오기에 쓰일 변수들은 여기에서 추가하거나 제거한다.
int a;
int b;
int c;
short d;
};
#pragma pack(pop)
// ...
}
#endif
|
cs |
#pragma pack 은 컴파일러가 구조체를 저장할 때 적용하는 구조체 패딩(structure padding)을 조작한다.
일반적으로 컴파일러는 cpu 레지스터 크기( void*의 크기 )를 고려하여 cpu의 동작이 효율적이도록 구조체의 크기를 조작한다.
cpu는 한 번에 데이터를 레지스터 크기만큼 읽어들이므로, 레지스터 크기 간격으로 메모리가 배치되어 있어야 쓸데없는 주소 이동 없이 구조체를 읽어들일 수 있기 때문이다.
- 구조체 패딩이 없는 경우 이런 문제가 발생한다.
만약 32비트 시스템에서 int, char, int, char, int 순으로 구조체 멤버가 나열되어 있다면
- 1번째, int에 접근하면서 4바이트를 읽어들인다.
- 2번째, char에 접근하면서 4바이트를 읽어들인다.
- char의 크기는 1바이트이므로 3바이트만큼 되돌아간다.
- 3번째, int에 접근하면서 4바이트를 읽어들인다.
- 4번째, char에 접근하면서 4바이트를 읽어들인다.
- char의 크기는 1바이트이므로 3바이트만큼 되돌아간다.
- 5번째, int에 접근하면서 4바이트를 읽어들인다.
파일 저장/불러오기를 구현하면서는 멤버 변수를 하나하나 읽는 것이 아니라 구조체의 크기만큼 메모리를 통째로 읽어올 것이므로 구조체 패딩이 따로 필요가 없다. 따라서 위와 같은 조작을 통해 구조체 패딩을 1바이트 단위로 이루어지게 해 무효화한다.
SaveLoad 클래스 인터페이스
- class SaveLoad
설명 : 객체 생성이 불가능한, 저장/불러오기 함수 제공 목적의 클래스이다.
- struct SaveData
설명 : 저장/불러오기 로 입출력할 변수들을 모아놓은 구조체이다.
예시- SaveData 구조체 정의
struct SaveData { int hp, int level, int exp, int map_id, POINT pos }; - SaveData 구조체 정의
struct SaveData { int year, int month, int day, std::string weekday_name };
- SaveData 구조체 정의
- static void save_bin( const char* file_path, const SaveData& data )
설명 : file_path 경로에 해당하는 파일을 열어 data 의 내용을 저장한다.
예시- "MySaveFile.savedata" 파일 저장하기
SaveLoad::save_bin( "MySaveFile.savedata", SaveLoad::SaveData{ /*...*/ } ); - "저장.abcdefg" 파일 저장하기
SaveLoad::SaveData data{ /*...*/ }; // 필요한 변수들로 초기화
SaveLoad::save_bin( "저장.abcdefg", data );
- "MySaveFile.savedata" 파일 저장하기
- static void load_bin( const char* file_path, SaveData& data )
설명 : file_path 경로에 해당하는 파일을 열어 내용을 data 에 저장한다.
예시- "SaveSlot1.savedata" 파일 불러오기
SaveLoad::SaveData data;
SaveLoad::load_bin( "SaveSlot1.savedata", data ); - "저장.abcdefg" 파일 불러오기
SaveLoad::SaveData sd;
SaveLoad::load_bin( "저장.abcdefg", data );
- "SaveSlot1.savedata" 파일 불러오기
- struct SaveData
SaveData 파일은 실제 프로그램 내 존재하는 변수들을 모아 전달하는 프록시 역할을 하므로, 저장할 시에 임시 객체로써 생성될 일 또한 예상된다.
SaveData를 참조( & )로만 받으면 이러한 임시 객체에 대해 save_bin 을 호출할 수 없다.
따라서 save_bin 은 const SaveData& 대신 우측값( r-value )으로 SaveData&& 를 받는 버전도 오버로딩한다.
파일 경로도 다양한 문자열 타입이 들어올 수 있다.
const char*, const wchat_t*, const std::string&, const std::wstring& 네 가지의 경우에 대해 각각 오버로딩한다.
SaveLoad 클래스 구현
// saveload.h
#ifndef _saveload
#define _saveload
#include <fstream>
#include "keyword.h"
#include <string>
class SaveLoad NONCREATABLE
{
public:
#pragma pack(push, 1)
// 저장할 데이터 구조체
// SaveData 객체를 파일에 저장하고, (save_bin)
// 파일을 SaveData 객체에 불러온다. (load_bin)
// SaveData 객체를 프록시로 코드 내의 변수들과 상호작용하면 된다.
struct SaveData
{
// 저장, 불러오기에 쓰일 변수들은 여기에서 추가하거나 제거한다.
int a;
int b;
int c;
short d;
};
#pragma pack(pop)
static void save_bin( const char* file_path, const SaveData& data )
{
save_bin_impl( file_path, data );
}
static void save_bin( const char* file_path, SaveData&& data )
{
save_bin_impl( file_path, std::move( data ) );
}
static void save_bin( const wchar_t* file_path, const SaveData& data )
{
save_bin_impl( file_path, data );
}
static void save_bin( const wchar_t* file_path, SaveData&& data )
{
save_bin_impl( file_path, std::move( data ) );
}
static void save_bin( const std::string& file_path, const SaveData& data )
{
save_bin_impl( file_path, data );
}
static void save_bin( const std::string& file_path, SaveData&& data )
{
save_bin_impl( file_path, std::move( data ) );
}
static void save_bin( const std::wstring& file_path, const SaveData& data )
{
save_bin_impl( file_path, data );
}
static void save_bin( const std::wstring& file_path, SaveData&& data )
{
save_bin_impl( file_path, std::move( data ) );
}
static void load_bin( const char* file_path, SaveData& data )
{
load_bin_impl( file_path, data );
}
static void load_bin( const wchar_t* file_path, SaveData& data )
{
load_bin_impl( file_path, data );
}
static void load_bin( const std::string& file_path, SaveData& data )
{
load_bin_impl( file_path, data );
}
static void load_bin( const std::wstring& file_path, SaveData& data )
{
load_bin_impl( file_path, data );
}
SaveLoad() = delete;
private:
template < typename Str_t, typename SV >
HELPER static void save_bin_impl( Str_t&& file_path, SV&& data )
{
std::ofstream out{ file_path, std::ios_base::binary | std::ios_base::out };
if ( out.fail() )
throw std::ios_base::failure{ "SaveLoad::save_bin_impl : cannot open file" };
out.write( reinterpret_cast< char* >( &data ), sizeof( decltype( data ) ) );
}
template < typename Str_t, typename SV >
HELPER static void load_bin_impl( Str_t&& file_path, SV&& data )
{
std::ifstream in{ file_path, std::ios_base::binary | std::ios_base::in };
if ( in.fail() )
throw std::ios_base::failure{ "SaveLoad::load_bin_impl : cannot open file" };
in.read( reinterpret_cast< char* >( &data ), sizeof( decltype( data ) ) );
}
};
#endif
|
cs |
save_bin_impl, load_bin_impl 이 실제 구현을 가지고, save_bin, load_bin은 이 함수로 인자를 전달( forwarding ) 하는 역할밖에 하지 않는다.
save_bin_impl, load_bin_impl 은 어떤 종류의 문자열 타입도 받도록 하기 위해서 문자열을 템플릿 매개변수화, 보편참조( Scott Meyers, 2015, Effective Modern C++ )로 만들었고, 왼값과 오른값을 둘 다 받을 수 있도록 SaveFile도 템플릿 매개변수화, 보편참조로 만들었다.
굳이 사용자 코드에서 호출되는 함수들을 템플릿화 하지 않고 내부 템플릿 함수로의 포워딩을 선택한 이유가 있다.
템플릿 인자로 배열이 올 경우가 있는데, 배열은 그 크기까지 포함하여 하나의 타입이다.
템플릿은 다른 타입에 대해 다른 코드를 생성하여 인스턴스화된다.
문자열에 대해 템플릿 형식 연역이 일어날 때 const char[N] 과 같이 연역되는 경우 const char[0], const char[1], const char[2], const char[3], ... 은 모두 다른 타입이기 때문에 기하급수적으로 많은 템플릿 인스턴스가 만들어질 위험이 있다.
따라서 일단 문자열을 const char* 로 받은 후에 템플릿 함수에 전달하면 const char[N] 으로 연역될 일 없이 const char* 로 연역된다.
바이너리파일을 구조체로 읽고 쓸 때에는 std::fstream 의 read() 와 write() 를 활용한다.
첫번째 인자로 받은 char* 에 두번째 인자로 받은 std::streamsize 만큼의 char를 읽는다/쓴다.
char은 1 byte 자료형, 즉 메모리의 한 바이트를 나타낼 수 있는 자료형이다.
따라서 이 함수들에 구조체의 주소를 첫번째 인자로 주고, 구조체의 크기를 두번째 인자로 주면 메모리에서 구조체의 크기( 바이트 수 )만큼을 읽어올 수 있다.
구조체의 주소는 그 구조체의 포인터 타입으로 char* 를 받는 함수에 넘길 수 없으므로, 포인터를 재해석할 수 있는 캐스팅인 reinterpret_cast 를 활용하여 char* 로 캐스팅해 넘긴다.
이 포인터로 별다른 작업을 할 것이 아니라 단지 입출력을 위한 주소로만 쓸 것이므로 char* 로 캐스팅해도 상관없다.
주소를 나타내는 것은 어떤 타입의 포인터로도 충분히 가능하기 때문이다.
테스트
// database.h
// ...
class SaveLoad NONCREATABLE
{
public:
// ...
struct SaveData
{
int a, b, c, d;
};
// ...
};
#include <iostream>
void test_save_file()
{
SaveLoad::save_bin( "세이브 파일.savedata", SaveLoad::SaveData{ 10, 20, 30, 40 } );
SaveLoad::SaveData data;
SaveLoad::load_bin( "세이브 파일.savedata", data );
std::cout << data.a << ", " << data.b << ", " << data.c << ", " << data.d << std::endl;
// output : 10, 20, 30, 40
}
|
cs |
// database.h
// ...
struct someclass
{
char n[ 20 ];
int a;
int b;
friend std::ostream& operator<<( std::ostream& os, const someclass& my )
{
return os << my.n << ", " << my.a << ", " << my.b;
}
};
class SaveLoad NONCREATABLE
{
public:
// ...
struct SaveData
{
someclass a;
int b;
someclass c;
int d;
};
// ...
};
#include <iostream>
void test_save_file()
{
SaveLoad::save_bin( "세이브 파일.savedata",
SaveLoad::SaveData{ { "이순신", 1545, 1598 }, 1, { "삼도 수군 통제사", 1593, 1597 }, 2 } );
SaveLoad::SaveData data;
SaveLoad::load_bin( "세이브 파일.savedata", data );
someclass a = data.a;
int b = data.b;
someclass c = data.c;
int d = data.d;
std::cout << a << ", " << b << ", " << c << ", " << d << std::endl;
// output : 이순신, 1545, 1598, 삼도 수군 통제사, 1593, 1597, 2
}
|
cs |
- 10, 20, 30, 40 을 저장하고 불러오기
- { "이순신", 1545, 1598 }, 1, { "삼도 수군 통제사", 1593, 1597 }, 2 를 저장하고 불러오기
'Project > Win32API로 던그리드 모작' 카테고리의 다른 글
현재 적용할 메모리 풀 (0) | 2021.09.15 |
---|---|
메모리 풀, 오브젝트 풀 개념 (0) | 2021.08.25 |
데이터베이스 구현 (0) | 2021.08.23 |
std::iostream 으로 std::tuple 입출력하기 (0) | 2021.08.23 |
std::stringstream 을 통해 문자열을 값으로 (0) | 2021.08.23 |