~ 3.5 Minute Read.
I recently came across a use case for having a small local database to store some statistics in an Unreal Engine 4 project. I found CISQLite3, a plugin by conflict.industries, who unfortunately seem to have gone out of business. The plugin was released under MIT license, though, which allows me to a. use and b. update the plugin in any use case I may have. Many thanks go out to them for choosing this license.
You can find my fork here.
Updating the Plugin
This was a matter of merely updating a single build file. As sqlite hadn’t been updated since 2016, I did that as well.
Other Improvements
There were a couple of minor things that were easy to clean up, e.g. using const FString& instead of const FString or FString for method parameters, since this avoids copying the string.
Other cleanups I made include updating use of now deprecated UE4 API, expose some private methods that turned out to be useful during development, some repository maintanence (adding a .gitignore) and a performance optimization that works by allowing you to leave the sqlite3 database open instead of opening and closing it for every operation. In single connection use cases this works great.
Installation
Either download a zip and extract it into a subdirectory of your Plugins dir in your Unreal Engine
project (e.g. <MyGame>/Plugins/unreal-sqlite3/
) or—if you already use git—add it as a submodule:
cd Plugins/ git submodule add https://github.com/Squareys/unreal-sqlite3
Then enable it in the plugin settings of your Unreal Engine project.
Basic Use
While the plugin can do some fancy stuff like populate properties of an Actor, the main thing you’ll need is to run some SQL.
A simple example for a highscores table could look like this:
const FString DB_NAME = "DB"; /* Connect to the database */ bool connect() { const FString databaseFile = FPaths::ConvertRelativePathToFull( FPaths::Combine(FPaths::ProjectSavedDir(), FString("my.db"))); if (!USQLiteDatabase::IsValidDatabase(databaseFile, false)) { if (!USQLiteDatabase::CreateDatabase(databaseFile, false)) { UE_LOG(LogTemp, Error, TEXT("Couldn't create database %s"), *databaseFile); return false; } else { UE_LOG(LogTemp, Log, TEXT("Created database %s"), *databaseFile); } /* Register database, cannot be registered yet, because file did not exist. */ bool success = USQLiteDatabase::RegisterDatabase(DB_NAME, databaseFile, false, true); if(!success) { UE_LOG(LogTemp, Error, TEXT("Couldn't register database %s"), *databaseFile); return false; } else { UE_LOG(LogTemp, Log, TEXT("Registered database %s"), *databaseFile); } /* Run some SQL without return value! */ success = USQLiteDatabase::ExecSql(DB_NAME, TEXT( "CREATE TABLE Highscores(" "id INTEGER PRIMARY KEY ASC," "score INTEGER," "player TEXT" ")")); if(!success) { UE_LOG(LogTemp, Error, TEXT("Couldn't create table")); return false; } } return true; }
Where the following line is one of the more important:
bool success = USQLiteDatabase::RegisterDatabase(DB_NAME, databaseFile, false, true);
Which registers (stores the filename for given database name and checks if the file can be opened) the database with given file, false indicating that the path is not relative to the project and true keeping it open for performance—which you may not need, though, especially in the case of highscores.
If you do keep the database open, you should close it sometime:
USQLiteDatabase::UnregisterDatabase(DB_NAME);
Running a Query with Result
Running SQL without return values can still be very useful if you just want to store statistics and later evaluate them “offline” (as in with a different application 1 and not during game runtime). If we want to show some statistics to the user—and I bet he’d be interested in that highscore list—we will need to be able to query some data, though:
const FString playerName = "Squareys"; int64 out = -1; TUniquePtr<SQLiteQueryResult> result = USQLiteDatabase::RunQueryAndGetResults(DB_NAME, FString::Printf( TEXT("SELECT score FROM Highscores WHERE player='%s'"), playerName)); if (result->Success && result->Results.Num() > 0 && result->Results[0].Fields.Num() > 0) { out = result->Results[0].Fields[0].IntValue; } else { UE_LOG(LogTemp, Error, TEXT("Could not get integer output: %s"), *(result->ErrorMessage)); }
You will notice that I pass parameters to SQL using FString::Printf
.
Underneath a prepared statement is created. So in case this is not sufficient for your use case and you need that, you can check the code there and possibly expose prepared statements through the plugins API. (If you do so, I would appreciate a pullrequest to my fork :) )
Written in 40 minutes, edited in 15 minutes.