[DirectX] Save와 Load의 구현
in Study on WinAPI&DirectX
리소스를 UI를 이용해서 편집을 하고 Material이나 PreFab을 만들어줬다면 수정한 내역을 파일로 저장하고 로드할 수 있어야한다.
그래야 프로그램을 껐다가 다시 켜도 수정 내역이 유지가 된다.
이를 구현해보자.
Material Save 기능의 구현
우선, Material은 엔진에서 생성된 리소스인지, 사용자가 생성한 리소스인지를 판별해주어야한다.
엔진에서 생성된 리소스는 저장을 하지 못하도록 막아두어야 하니까.
그래서 CRes에서 m_bEngine이라고 하여, 엔진에서 생성된 리소스인지 여부를 저장하는 멤버변수를 추가하고, CMaterial클래스에서 생성자로 상속을 받는다.
그리고 CMaterial클래스에 Save함수와 Load함수를 만들어주면된다.
Save 함수를 통해 아래와 같은 자료가 저장된다.
- Material이 어떤 텍스쳐를 참조하고 있었는가? (파일 경로와 Key값)
- 어느 경로에 저장될 것인가?
- 어떤 쉐이더를 사용하고 있는가?
- 어떤 Key값을 사용하고 있는가?
문자열의 저장
wstring을 저장할 때의 주의할 점은 문자열의 길이를 먼저 저장한 다음에 wchar_t 크기의 자료가 몇 개 있는지를 저장해주어야 한다.
그래야 데이터를 읽어올 때 오류가 발생하지 않는다.
// SaveWString 함수, func.cpp 중에서
void SaveWString(const wstring& _str, FILE* _File)
{
UINT iLen = (UINT)_str.length();
fwrite(&iLen, sizeof(UINT), 1, _File);
fwrite(_str.c_str(), sizeof(wchar_t), _str.length(), _File);
}
문자열의 로드
그리고 wstring을 로드할 때에는 아래의 함수를 이용한다.
// LoadWString 함수, func.cpp 중에서
void LoadWString(wstring& _str, FILE* _File)
{
wchar_t szBuffer[256] = {};
UINT iLen = 0;
fread(&iLen, sizeof(UINT), 1, _File);
fread(szBuffer, sizeof(wchar_t), iLen, _File);
_str = szBuffer;
}
Save함수와 Load함수
그래서 구현한 Save함수는 아래와 같다.
// CMaterial의 Save 함수
int CMaterial::Save(const wstring& _strRelativePath)
{
if (IsEngineRes())
return E_FAIL;
wstring strFilePath = CPathMgr::GetInst()->GetContentPath();
strFilePath += _strRelativePath;
FILE* pFile = nullptr;
_wfopen_s(&pFile, strFilePath.c_str(), L"wb");
// Entity
SaveWString(GetName(), pFile);
// Res
SaveWString(GetKey(), pFile);
// Shader
SaveResRef(m_pShader.Get(), pFile);
// Constant
fwrite(&m_Const, sizeof(tMtrlConst), 1, pFile);
// Texture
for (UINT i = 0; i < (UINT)TEX_PARAM::TEX_END; ++i)
{
SaveResRef(m_arrTex[i].Get(), pFile);
}
fclose(pFile);
return S_OK;
}
그리고 Load함수는 아래와 같다.
// CMaterial의 Load 함수
int CMaterial::Load(const wstring& _strFilePath)
{
FILE* pFile = nullptr;
_wfopen_s(&pFile, _strFilePath.c_str(), L"rb");
// Entity
wstring strName;
LoadWString(strName, pFile);
SetName(strName);
// Res
wstring strKey;
LoadWString(strKey, pFile);
// Shader
LoadResRef(m_pShader, pFile);
// Constant
fread(&m_Const, sizeof(tMtrlConst), 1, pFile);
// Texture
for (UINT i = 0; i < (UINT)TEX_PARAM::TEX_END; ++i)
{
LoadResRef(m_arrTex[i], pFile);
}
fclose(pFile);
return S_OK;
}
CMaterial 클래스의 재구성
// CMaterial.h
#pragma once
#include "CRes.h"
#include "ptr.h"
#include "CGraphicsShader.h"
#include "CTexture.h"
class CMaterial :
public CRes
{
private:
Ptr<CGraphicsShader> m_pShader;
tMtrlConst m_Const;
Ptr<CTexture> m_arrTex[TEX_END];
public:
void SetScalarParam(SCALAR_PARAM _Param, const void* _Src);
void SetTexParam(TEX_PARAM _Param, const Ptr<CTexture>& _Tex);
void GetScalarParam(SCALAR_PARAM _param, void* _pData);
Ptr<CTexture> GetTexParam(TEX_PARAM _param) { return m_arrTex[(UINT)_param]; }
void SetShader(Ptr<CGraphicsShader> _Shader) { m_pShader = _Shader; }
Ptr<CGraphicsShader> GetShader() { return m_pShader; }
virtual void UpdateData() override;
private:
virtual int Load(const wstring& _strFilePath);
public:
virtual int Save(const wstring& _strRelativePath);
public:
CLONE(CMaterial);
public:
CMaterial(bool _bEngine = false);
~CMaterial();
};
// CMaterial.cpp
#include "pch.h"
#include "CMaterial.h"
#include "CDevice.h"
#include "CConstBuffer.h"
#include "CResMgr.h"
#include "CPathMgr.h"
CMaterial::CMaterial(bool _bEngine)
: CRes(RES_TYPE::MATERIAL, _bEngine)
, m_Const{}
, m_arrTex{}
{
}
CMaterial::~CMaterial()
{
}
void CMaterial::UpdateData()
{
if (nullptr == m_pShader)
return;
m_pShader->UpdateData();
// Texture Update
for (UINT i = 0; i < TEX_END; ++i)
{
if (nullptr == m_arrTex[i])
{
m_Const.arrTex[i] = 0;
CTexture::Clear(i);
continue;
}
else
{
m_Const.arrTex[i] = 1;
m_arrTex[i]->UpdateData(i, PIPELINE_STAGE::PS_PIXEL);
}
}
// Constant Update
CConstBuffer* pMtrlBuffer = CDevice::GetInst()->GetConstBuffer(CB_TYPE::MATERIAL);
pMtrlBuffer->SetData(&m_Const);
pMtrlBuffer->UpdateData();
}
void CMaterial::SetScalarParam(SCALAR_PARAM _Param, const void* _Src)
{
switch (_Param)
{
case INT_0:
case INT_1:
case INT_2:
case INT_3:
m_Const.arrInt[_Param] = *((int*)_Src);
break;
case FLOAT_0:
case FLOAT_1:
case FLOAT_2:
case FLOAT_3:
m_Const.arrFloat[_Param - FLOAT_0] = *((float*)_Src);
break;
case VEC2_0:
case VEC2_1:
case VEC2_2:
case VEC2_3:
m_Const.arrV2[_Param - VEC2_0] = *((Vec2*)_Src);
break;
case VEC4_0:
case VEC4_1:
case VEC4_2:
case VEC4_3:
m_Const.arrV4[_Param - VEC4_0] = *((Vec4*)_Src);
break;
case MAT_0:
case MAT_1:
case MAT_2:
case MAT_3:
m_Const.arrMat[_Param - MAT_0] = *((Matrix*)_Src);
break;
}
}
void CMaterial::SetTexParam(TEX_PARAM _Param, const Ptr<CTexture>& _Tex)
{
m_arrTex[_Param] = _Tex;
}
void CMaterial::GetScalarParam(SCALAR_PARAM _param, void* _pData)
{
switch (_param)
{
case INT_0:
case INT_1:
case INT_2:
case INT_3:
{
int idx = (UINT)_param - (UINT)INT_0;
*((int*)_pData) = m_Const.arrInt[idx];
}
break;
case FLOAT_0:
case FLOAT_1:
case FLOAT_2:
case FLOAT_3:
{
int idx = (UINT)_param - (UINT)FLOAT_0;
*((float*)_pData) = m_Const.arrFloat[idx];
}
break;
case VEC2_0:
case VEC2_1:
case VEC2_2:
case VEC2_3:
{
int idx = (UINT)_param - (UINT)VEC2_0;
*((Vec2*)_pData) = m_Const.arrV2[idx];
}
break;
case VEC4_0:
case VEC4_1:
case VEC4_2:
case VEC4_3:
{
int idx = (UINT)_param - (UINT)VEC4_0;
*((Vec4*)_pData) = m_Const.arrV4[idx];
}
break;
case MAT_0:
case MAT_1:
case MAT_2:
case MAT_3:
{
int idx = (UINT)_param - (UINT)MAT_0;
*((Matrix*)_pData) = m_Const.arrMat[idx];
}
break;
}
}
// ================
// File Save / Load
// ================
int CMaterial::Save(const wstring& _strRelativePath)
{
if (IsEngineRes())
return E_FAIL;
wstring strFilePath = CPathMgr::GetInst()->GetContentPath();
strFilePath += _strRelativePath;
FILE* pFile = nullptr;
_wfopen_s(&pFile, strFilePath.c_str(), L"wb");
// Entity
SaveWString(GetName(), pFile);
// Res
SaveWString(GetKey(), pFile);
// Shader
SaveResRef(m_pShader.Get(), pFile);
// Constant
fwrite(&m_Const, sizeof(tMtrlConst), 1, pFile);
// Texture
for (UINT i = 0; i < (UINT)TEX_PARAM::TEX_END; ++i)
{
SaveResRef(m_arrTex[i].Get(), pFile);
}
fclose(pFile);
return S_OK;
}
int CMaterial::Load(const wstring& _strFilePath)
{
FILE* pFile = nullptr;
_wfopen_s(&pFile, _strFilePath.c_str(), L"rb");
// Entity
wstring strName;
LoadWString(strName, pFile);
SetName(strName);
// Res
wstring strKey;
LoadWString(strKey, pFile);
// Shader
LoadResRef(m_pShader, pFile);
// Constant
fread(&m_Const, sizeof(tMtrlConst), 1, pFile);
// Texture
for (UINT i = 0; i < (UINT)TEX_PARAM::TEX_END; ++i)
{
LoadResRef(m_arrTex[i], pFile);
}
fclose(pFile);
return S_OK;
}
그리고 test 재질을 만들고, Save하고 Load를 해보는 코드를 작성해보자.
// TestLevel.cpp 중에서
Ptr<CMaterial> pNewMtrl = new CMaterial;
pNewMtrl->SetShader(CResMgr::GetInst()->FindRes<CGraphicsShader>(L"Std2DLightShader"));
int a = 101;
pNewMtrl->SetScalarParam(INT_1, &a);
pNewMtrl->SetTexParam(TEX_0, CResMgr::GetInst()->FindRes<CTexture>(L"PlayerTex"));
CResMgr::GetInst()->AddRes<CMaterial>(L"New Material", pNewMtrl);
pNewMtrl->Save(L"material\\test.mtrl");
GameObject의 Save / Load 기능의 구현
GameObject에는 Script가 들어간다.
하지만 Script는 Engine 프로젝트가 아니라 Script 프로젝트에 구현하기 때문에 Engine 내에 있는 세이브, 로드 기능으로는 구현할 수 없다.
그렇기 때문에 Client 프로젝트에서 새로이 구현을 해줄 것이다.
Client 프로젝트의 CLevelSaveLoad 클래스의 구성
// CLevelSaveLoad.h
#pragma once
class CLevel;
class CLayer;
class CGameObject;
class CLevelSaveLoad
{
public:
static int SaveLevel(const wstring& _LevelPath, CLevel* _Level);
static int SaveGameObject(CGameObject* _Object, FILE* _File);
static CLevel* LoadLevel(const wstring& _LevelPath);
static CGameObject* LoadGameObject(FILE* _File);
};
// CLevelSaveLoad.cpp
#include "pch.h"
#include "CLevelSaveLoad.h"
#include <Engine\CPathMgr.h>
#include <Engine\CLevelMgr.h>
#include <Engine\CLevel.h>
#include <Engine\CLayer.h>
#include <Engine\CGameObject.h>
#include <Engine\components.h>
#include <Engine\CScript.h>
#include <Script\CScriptMgr.h>
int CLevelSaveLoad::SaveLevel(const wstring& _LevelPath, CLevel* _Level)
{
if (_Level->GetState() != LEVEL_STATE::STOP)
return E_FAIL;
wstring strPath = CPathMgr::GetInst()->GetContentPath();
strPath += _LevelPath;
FILE* pFile = nullptr;
_wfopen_s(&pFile, strPath.c_str(), L"wb");
if (nullptr == pFile)
return E_FAIL;
// 레벨 이름 저장
SaveWString(_Level->GetName(), pFile);
// 레벨의 레이어들을 저장
for (UINT i = 0; i < MAX_LAYER; ++i)
{
CLayer* pLayer = _Level->GetLayer(i);
// 레이어 이름 저장
SaveWString(pLayer->GetName(), pFile);
// 레이어의 게임오브젝트들 저장
const vector<CGameObject*>& vecParent = pLayer->GetParentObject();
// 오브젝트 개수 저장
size_t objCount = vecParent.size();
fwrite(&objCount, sizeof(size_t), 1, pFile);
// 각 게임오브젝트
for (size_t i = 0; i < objCount; ++i)
{
SaveGameObject(vecParent[i], pFile);
}
}
fclose(pFile);
return S_OK;
}
int CLevelSaveLoad::SaveGameObject(CGameObject* _Object, FILE* _File)
{
// 이름
SaveWString(_Object->GetName(), _File);
// 컴포넌트
for (UINT i = 0; i <= (UINT)COMPONENT_TYPE::END; ++i)
{
if (i == (UINT)COMPONENT_TYPE::END)
{
// 컴포넌트 타입 저장
fwrite(&i, sizeof(UINT), 1, _File);
break;
}
CComponent* Com = _Object->GetComponent((COMPONENT_TYPE)i);
if (nullptr == Com)
continue;
// 컴포넌트 타입 저장
fwrite(&i, sizeof(UINT), 1, _File);
// 컴포넌트 정보 저장
Com->SaveToLevelFile(_File);
}
// 스크립트
const vector<CScript*>& vecScript = _Object->GetScripts();
size_t ScriptCount = vecScript.size();
fwrite(&ScriptCount, sizeof(size_t), 1, _File);
for (size_t i = 0; i < vecScript.size(); ++i)
{
wstring ScriptName = CScriptMgr::GetScriptName(vecScript[i]);
SaveWString(ScriptName, _File);
vecScript[i]->SaveToLevelFile(_File);
}
// 자식 오브젝트
const vector<CGameObject*>& vecChild = _Object->GetChild();
size_t ChildCount = vecChild.size();
fwrite(&ChildCount, sizeof(size_t), 1, _File);
for (size_t i = 0; i < ChildCount; ++i)
{
SaveGameObject(vecChild[i], _File);
}
return 0;
}
CLevel* CLevelSaveLoad::LoadLevel(const wstring& _LevelPath)
{
wstring strPath = CPathMgr::GetInst()->GetContentPath();
strPath += _LevelPath;
FILE* pFile = nullptr;
_wfopen_s(&pFile, strPath.c_str(), L"rb");
if (nullptr == pFile)
return nullptr;
CLevel* NewLevel = new CLevel;
// 레벨 이름
wstring strLevelName;
LoadWString(strLevelName, pFile);
NewLevel->SetName(strLevelName);
for (UINT i = 0; i < MAX_LAYER; ++i)
{
CLayer* pLayer = NewLevel->GetLayer(i);
// 레이어 이름
wstring LayerName;
LoadWString(LayerName, pFile);
pLayer->SetName(strLevelName);
// 게임 오브젝트 개수
size_t objCount = 0;
fread(&objCount, sizeof(size_t), 1, pFile);
// 각 게임오브젝트
for (size_t j = 0; j < objCount; ++j)
{
CGameObject* pNewObj = LoadGameObject(pFile);
NewLevel->AddGameObject(pNewObj, i, false);
}
}
fclose(pFile);
NewLevel->ChangeState(LEVEL_STATE::STOP);
return NewLevel;
}
CGameObject* CLevelSaveLoad::LoadGameObject(FILE* _File)
{
CGameObject* pObject = new CGameObject;
// 이름
wstring Name;
LoadWString(Name, _File);
pObject->SetName(Name);
// 컴포넌트
while (true)
{
UINT ComponentType = 0;
fread(&ComponentType, sizeof(UINT), 1, _File);
// 컴포넌트 정보의 끝을 확인
if ((UINT)COMPONENT_TYPE::END == ComponentType)
break;
CComponent* Component = nullptr;
switch ((COMPONENT_TYPE)ComponentType)
{
case COMPONENT_TYPE::TRANSFORM:
Component = new CTransform;
break;
case COMPONENT_TYPE::COLLIDER2D:
Component = new CCollider2D;
break;
case COMPONENT_TYPE::COLLIDER3D:
//Component = new CCollider2D;
break;
case COMPONENT_TYPE::ANIMATOR2D:
Component = new CAnimator2D;
break;
case COMPONENT_TYPE::ANIMATOR3D:
break;
case COMPONENT_TYPE::LIGHT2D:
Component = new CLight2D;
break;
case COMPONENT_TYPE::LIGHT3D:
break;
case COMPONENT_TYPE::CAMERA:
Component = new CCamera;
break;
case COMPONENT_TYPE::MESHRENDER:
Component = new CMeshRender;
break;
case COMPONENT_TYPE::PARTICLESYSTEM:
Component = new CParticleSystem;
break;
case COMPONENT_TYPE::TILEMAP:
Component = new CTileMap;
break;
case COMPONENT_TYPE::LANDSCAPE:
break;
case COMPONENT_TYPE::DECAL:
break;
}
Component->LoadFromLevelFile(_File);
pObject->AddComponent(Component);
}
// 스크립트
size_t ScriptCount = 0;
fread(&ScriptCount, sizeof(size_t), 1, _File);
for (size_t i = 0; i < ScriptCount; ++i)
{
wstring ScriptName;
LoadWString(ScriptName, _File);
CScript* pScript = CScriptMgr::GetScript(ScriptName);
pObject->AddComponent(pScript);
pScript->LoadFromLevelFile(_File);
}
// 자식 오브젝트
size_t ChildCount = 0;
fread(&ChildCount, sizeof(size_t), 1, _File);
for (size_t i = 0; i < ChildCount; ++i)
{
CGameObject* ChildObject = LoadGameObject(_File);
pObject->AddChild(ChildObject);
}
return pObject;
}