Unreal에 SQLite 붙이기 – Windows

개요

언리얼 프로젝트를 진행하면서 데이터베이스를 붙일 일이 생겼습니다. 그래서 마켓플레이스를 둘러보는데 무료로 사용할 수 있는 데이터베이스 플러그인들은 유료가 많았고, 무료로 사용할 수 있는 플러그인은 4버전을 마지막으로 개발자가 사라져서 요원해졌죠.

그래서 직접 데이터베이스를 붙여보기로 하고 일단 Windows를 대상으로 하는 프로젝트이기 때문에 Windows를 중점으로 작업했습니다.

Storage Singleton

데이터베이스를 참조하는 멤버 변수를 유지하는 매우 간단한 싱글톤 클래스를 만드는 것부터 시작해 보겠습니다.

지금 구현하는 싱글톤 클래스는 매우 간단하게 구현한 버전입니다.

Header

#pragma once

#include "CoreMinimal.h"

DECLARE_LOG_CATEGORY_EXTERN(LogDatabase, Log, All);

class StorageManager {
private:
	static StorageManager *Instance;
	
private:
	StorageManager();

public:
	~StorageManager();
	
public:
	static StorageManager *GetInstance();

	bool Initialize();
};

Source

#include "StorageManager.h"

DEFINE_LOG_CATEGORY(LogDatabase)

StorageManager *StorageManager::Instance = nullptr;

StorageManager::StorageManager() {
	//
}

StorageManager::~StorageManager() {
    //
}

StorageManager * StorageManager::GetInstance() {
	if (Instance == nullptr) {
		Instance = new StorageManager();
	}
	
	return Instance;
}

bool StorageManager::Initialize() {
	return true;
}

이를 통해 이제 전달 된 데이터를 저장하는 요청을 처리하는 데 필요한 싱글톤 클래스가 생겼습니다. 이제 필요할 때 GetInstance 를 통해 인스턴스를 생성함과 동시에 사용할 수 있습니다.

데이터베이스 및 테이블 생성

먼저 SQLite3 라이브러리를 가져오고 필요한 헤더를 포함하는 것부터 시작해 보겠습니다. 우리는 윈도우에서 SQLite3 를 사용하기 위해 winsqlite3 라이브러리를 사용할 예정이고 이를 위해 헤더를 추가해보겠습니다.

#pragma once

#include "CoreMinimal.h"

#if WIN32
// 윈도우 10 이상에서만 작동하도록 설정
#define WINVER 0x0A00
#define _WIN32_WINNT 0x0A00
#endif

#include <winsqlite/winsqlite3.h>
#pragma comment(lib, "winsqlite3")

DECLARE_LOG_CATEGORY_EXTERN(LogDatabase, Log, All);

다음으로 데이터베이스에 대한 handle 이 필요합니다. 이를 위해 sqlite3 변수를 생성해 줍니다.

class StorageManager {
private:
	static StorageManager *Instance;

private:
	sqlite3 *DBSession;
	
private:
	StorageManager();

이 글에서는 Windows 유저의 AppData/Local 폴더에 데이터를 저장할 것입니다. AppData/Local 를 가져오기 위한 API를 정의하고 있는 헤더를 포함해줍시다.

#include <winsqlite/winsqlite3.h>
#pragma comment(lib, "winsqlite3")

#include <ShlObj.h>
#include <Shlwapi.h>
#pragma comment(lib, "shlwapi")

class StorageManager {

이제 SQLite 를 초기화하고 필요한 테이블로 데이터베이스를 생성하는 private 함수를 추가해보겠습니다.

bool StorageManager::InitializeSQLTable() {
	TCHAR Path[MAX_PATH] = TEXT("");

	if (FAILED(SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, Path))) {
		UE_LOG(LogDatabase, Error, TEXT("Unable to find DB Path"));
		return false;
	}

	PathCombine(Path, Path, *DatabaseName);

	if (sqlite3_enable_shared_cache(1) != SQLITE_OK) {
		UE_LOG(LogDatabase, Error, TEXT("Disabling logging since DB could not be opened in shared cache mode"));
		return false;
	}

	if (sqlite3_open16(Path, &DBSession) != SQLITE_OK) {
		const char* ErrorMsg = sqlite3_errmsg(DBSession);
		UE_LOG(LogDatabase, Error, TEXT("Failed to open database: %hs"), ErrorMsg);
		return false;
	}

	return true;
}

위의 InitializeSQLTable 함수에서는 먼저 데이터베이스를 생성해야 하는 AppData/Local 폴더의 경로를 가져온 다음 공유 캐시 모드에서 SQLite3 을 초기화합니다. SQLite3 파일을 연 뒤, 아래 CheckAndCreateTable 함수를 호출합니다. 이를 사용하여 게임에 필요한 만큼 테이블을 만들 수 있습니다.

bool StorageManager::Execute(const FString &SQLCommand) const {
	if (!bInitialized) return false;

	const char * ANSISQLCommand = TCHAR_TO_ANSI(*SQLCommand);
	
	sqlite3_stmt *CreateStmt;
	int Result = sqlite3_prepare_v2(DBSession, ANSISQLCommand, SQLCommand.Len(), &CreateStmt, nullptr);
	if (Result != SQLITE_OK) {
		UE_LOG(LogDatabase, Error, TEXT("Failed to prepare sql command: '%ls'. code: %d, message: '%hs'"),
			*SQLCommand, Result, sqlite3_errmsg(DBSession));
		return false;
	}

	Result = sqlite3_step(CreateStmt);
	if (Result != SQLITE_DONE) {
		UE_LOG(LogDatabase, Error, TEXT("Failed to execute sql command: '%ls'. code: %d, message: '%hs'"),
			*SQLCommand, Result, sqlite3_errmsg(DBSession));
        return false;
	}
	
	return false;
}

bool StorageManager::CheckAndCreateTable() {
	TStringBuilder<1024> SqlCommandBuilder;
	SqlCommandBuilder.Append(TEXT("CREATE TABLE IF NOT EXISTS SaveData ("));
	SqlCommandBuilder.Append(TEXT("id INTEGER PRIMARY KEY, "));
	SqlCommandBuilder.Append(TEXT("user_id INTEGER NOT NULL, "));
	SqlCommandBuilder.Append(TEXT("score INTEGER NOT NULL"));
	SqlCommandBuilder.Append(TEXT(")"));

	return Execute(SqlCommandBuilder.ToString());
}

이제 Initialize 함수에서 InitializeSQLTable 함수 및 CheckAndCreateTable 를 호출하여 데이터베이스 파일 및 필요한 테이블을 생성해줍니다.

bool StorageManager::Initialize() {
	bInitialized = false;

	if (!InitializeSQLTable()) return false;

	bInitialized = true;
	
	if (!CheckAndCreateTable()) return false;

	return bInitialized;
}

마지막으로 StorageManager 가 삭제될 때 Session 을 종료하도록 만들어줍니다.

StorageManager::~StorageManager() {
	if (bInitialized) {
		sqlite3_close_v2(DBSession);
	} 
}

저같은 경우는 GameMode 의 BeginPlay 함수에서 아래와 같은 함수를 호출하여 추후에 사용할 수 있도록 초기화 및 데이터베이스 생성을 진행합니다.

void AMyGameModeBase::BeginPlay() {
	Super::BeginPlay();

	if (!StorageManager::GetInstance()->Initialize()) {
		/** TODO: */
	}

	...
}

이렇게 SQLite3 Windows 버전을 추가하고 필요한 테이블을 생성하면서 SQLCommand 를 실행하는 방법에 대하여 알아봤습니다.

비록 Thread 에서 안전하지 않고 게임을 실행하면 데이터베이스 파일을 점유하여 외부에서 접근 불가능하게 만들며 되게 간단하게 만들었지만, 튜토리얼의 범위를 벗어나기도 하고 너무 방대한 요소라서 이번에는 간단하게 소개하는 것으로 글을 마치겠습니다.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *