Introduction
This is my first documentation/article/tutorial ever, so excuse me if some parts are not 100% clear to understand. Let me know and I will edit the parts to make the text easier to understand.
I will use my own SQL database wrapper, you can download HERE (v. 1.0.5.1)
The files of this articles are attached to this post and can be directly downloaded HERE
Background
SQL databases are used for many applications today. Every game today uses some kind of a database for easy and fast access to stored values, attributes and texts. Think of a RTS game, unit names, building times, weapon strength...everything is stored in a database. Think of a RPG, quests, items, names...stored in a database.
When having a look into the possibilities of UDK, i found nothing that could realise the things I just mentioned. For sure an .ini based database could be used, but it isn't very flexible compared to the possibilities with a database query.
So how I could use UDK and a database at the same time. The first thing that comes to my mind was DLLBind, added to UDK in November '09. With DLLBind you are able to write C++ libraries used by your UnrealScript classes.
That was the moment where my SQLProject was born.
Note: This tutorial does not cover how to create a database, a table or any other SQL related funtion like building queries or the reason for using prepared statements. Use the following links to inform about these things:
A quick tour into SQL would go beyond the scope of this tutorial.
Step: 1 - Installation
Installation of the SQLProject library is fairly easy. Just extract the folder UserCode from the zip file to your <UDK_Root>\Binaries\Win32 folder. The UDKProjectDLL.dll should be located in <UDK_Root>\Binaries\Win32\UserCode.
<UDK_Root> is the installation directory of your current UDK Version (e.g. C:\Development\UDK-2010-04).
Now you could use the library functions to work with a database. In the following chapter I will introduce a small project and will explain what the code do and what I thought when writing the sources.
Step: 2 - Setting up the API class
In the following steps I will use a package UDKDatabase where all our classes will be created. This is for demonstration only, you are free to directly implement the code into your existing project.
The next thing we have to do: Create a class which will be our interface to the UDKProjectDLL.dll. I always use a special class for this, to have an clean interface using the DLLBind feature without any other functions.
So we create a new class DB_DLLAPI.uc and add all exported functions of UDKProjectDLL.dll so we are able to use them in our scripts.
Code:
class DB_DLLAPI extends Object
DLLBind(UDKProjectDLL);
enum ESQLDriver
{
SQLDrv_None,
SQLDrv_SQLite,
};
/**
* SQL FUNCTIONS
*/
dllimport final function SQL_initSQLDriver(int aSQLDriver);
dllimport final function int SQL_createDatabase();
dllimport final function SQL_closeDatabase(int aDbIdx);
dllimport final function bool SQL_selectDatabase(int aDbIdx);
dllimport final function bool SQL_loadDatabase(string aFilename);
dllimport final function bool SQL_saveDatabase(string aFilename);
dllimport final function bool SQL_queryDatabase(string aStatement);
dllimport final function SQL_prepareStatement(string aStatement);
dllimport final function bool SQL_bindValueInt(int aParamIndex, int aValue);
dllimport final function bool SQL_bindNamedValueInt(string aParamName, int aValue);
dllimport final function bool SQL_bindValueFloat(int aParamIndex, float aValue);
dllimport final function bool SQL_bindNamedValueFloat(string aParamName, float aValue);
dllimport final function bool SQL_bindValueString(int aParamIndex, string aValue);
dllimport final function bool SQL_bindNamedValueString(string aParamName, string aValue);
dllimport final function bool SQL_executeStatement();
dllimport final function bool SQL_nextResult();
dllimport final function SQL_getIntVal(string aParamName, out int aValue);
dllimport final function SQL_getFloatVal(string aParamName, out float aValue);
dllimport final function SQL_getStringVal(string aParamName, out string aValue);
// DEPREACTED
dllimport final function SQL_getValueInt(int aColumnIdx, out int aValue);
dllimport final function SQL_getValueFloat(int aColumnIdx, out float aValue);
dllimport final function SQL_getValueString(int aColumnIdx, out string aValue);
/**
* IO FUNCTIONS
*/
dllimport final function bool IO_directoryExists(string aDirectoryPath);
dllimport final function bool IO_createDirectory(string aDirectoryPath);
dllimport final function bool IO_deleteDirectory(string aDirectoryPath, int aRecursive);
dllimport final function bool IO_fileExists(string aFilePath);
dllimport final function bool IO_deleteFile(string aFilePath);
DefaultProperties
{
}
Note the enumeration ESQLDriver which shows all available SQL drivers currently implemented in UDKProjectDLL.dll.
We created our interface class and are now able to call the DLLs function. But wait: If we want to use this class, we need an instance of it. Thats leads to the next step, the real database connection.
Step: 3 - The database connection
In the last step we only set up the interface class defining the functions we are able to call, which are exported to UDKProjectDLL.dll.
But how to use this functions now? We need an instance of this class when we run our game.
So I set up a bunch of classes which will handle all the stuff.
Code:
class DB_DatabaseMgr extends Object
dependson(DB_DLLAPI)
config(Database);
//=============================================================================
// Variables
//=============================================================================
var private DB_DLLAPI mDLLAPI;
var private GameInfo mGameInfo; // variable added to DB_DatabaseMgr...BackReference to current GameInfo
var config ESQLDriver mDatabaseDriver; // Used SQL driver set in UDKDatabase.ini
final function DB_DLLAPI getDLLAPI()
{
return mDLLAPI;
}
function initDatabase(GameInfo aGameInfo)
{
mGameInfo = aGameInfo; // set backreference to GameInfo
mDLLAPI.SQL_initSQLDriver(mDatabaseDriver); // automatically create one empty DB
}
DefaultProperties
{
Begin Object Class=DB_DLLAPI Name=DllApiInstance
End Object
mDLLAPI=DllApiInstance
}
First of all the databasemanager which owns our object instance of the interface class DB_DLLAPI. I decided to call this class DatabaseMgr, because I want to use only this class to access, select and modify the databases. To make this clear i set the reference to my interface object private. Only the databasemanager is able to access the object directly. But to make it visible for other classes i add a function that returns the objects reference.
Code:
class DB_FileMgr extends Object
dependson(DB_DLLAPI)
config(Database);
//=============================================================================
// Variables
//=============================================================================
var private DB_DatabaseMgr mDatabaseMgr;
var private DB_DLLAPI mDLLAPI;
function initFilemanager(DB_DatabaseMgr aDatabaseMgr)
{
mDatabaseMgr = aDatabaseMgr;
mDLLAPI = mDatabaseMgr.getDLLAPI();
}
DefaultProperties
{
}
When I thought about saving and loading databases from hard drive to memory and back, I come to the conclusion I will have to use some function for writing and reading files. Like the databasemanager I want to use a single class for all these things. For convenience I save the reference to the interface object (DB_DLLAPI) within the filemanager too, instead of get the reference from the databasemanager everytime when we need it (we will see this later in our BaseGameInfo).
Code:
class DB_BaseGameInfo extends GameInfo
config(Database);
//=============================================================================
// Variables
//=============================================================================
var private DB_DatabaseMgr mDatabase;
var private DB_FileMgr mFilemanager;
final function DB_DatabaseMgr getDBMgr()
{
return mDatabase;
}
final function DB_FileMgr getFileMgr()
{
return mFilemanager;
}
event InitGame( string Options, out string ErrorMessage )
{
Super.InitGame(Options, ErrorMessage);
mDatabase.initDatabase(self);
mFilemanager.initFilemanager(mDatabase);
}
DefaultProperties
{
Begin Object Class=DB_DatabaseMgr Name=DatabaseMgr
End Object
mDatabase=DatabaseMgr
Begin Object Class=DB_FileMgr Name=FileMgr
End Object
mFilemanager=FileMgr
}
At last we add the databasemanager and filemanager to our GameInfo class and create the objects in the DefaultProperties. I choose the GameInfo class due to several reasons:
- The GameInfo is created very early when we load a new map
- The GameInfo is always valid during a game session
- In a network game the GameInfo is only created on the server
The last point is very important, because we can be sure local changes on a database by the user wont affect the gameplay, because if all the content for our game is read and stored by the server, cheating the game by modifying the local database is minimized.
Step: 4 - Use .ini file for smart and flexible database content
The next step is a small thing I decided for myself to automatically load content databases into memory, so I can use their data for future SQL queries.
In the UDKDatabase.ini I add the following values and I defined the needed config variables in the specific class.
UDKDatabase.ini
Code:
[UDKDatabase.DB_DatabaseMgr]
mDatabaseDriver=SQLDrv_SQLite
mUserCodeRelPathUDKGame=..\..\..\UDKGame\
mDataRootPath=Content\Data\
mDefaultContentDBs=database.s3db
mDefaultGameplayDB=gamedb.s3db
DB_DatabaseMgr.uc
Code:
var config string mUserCodeRelPathUDKGame;
var config string mDataRootPath;
var config array<string> mDefaultContentDBs;
var config string mDefaultGameplayDB;
The databases we will need for this are located in the referenced folders you will see when unzip the attached file to your hard drive.
Note: mDefaultContentDBs is an array that will allow you to load more than one content database into memory (see Step: 5 - Loading databases).
Step: 5 - Loading databases
After setting up the .ini we can load the referenced databases into memory. I will show you my way, you are free to use your own loading routine, if you don't like mine. Just want to show you my approach.
To load my mGameContentDBs defined in the .ini I created a function in my DB_DatabaseMgr.uc and call this function in initDatabase:
DB_DatabaseMgr.uc
Code:
function initDatabase(GameInfo aGameInfo)
{
mGameInfo = aGameInfo;
mDLLAPI.SQL_initSQLDriver(mDatabaseDriver); // automatically create one empty DB
loadGameContentDatabases();
}
final function loadGameContentDatabases()
{
local int il;
local int lNewDatabase;
local string lFilePath;
if(mDefaultContentDBs.Length > 0){
lFilePath = mUserCodeRelPathUDKGame $ mDataRootPath;
// load database into in-memory database 0 (automatically created after calling SQL_initSQLDriver!!)
if(mDLLAPI.SQL_loadDatabase(lFilePath $ mDefaultContentDBs[0])){
`log("<<< GameContentDatabase loaded: " $ lFilePath $ mDefaultContentDBs[0]);
}
// for all additional default content databases create a new database, select it and load database into memory
for(il=1; il<mDefaultContentDBs.Length; ++il){
lNewDatabase = mDLLAPI.SQL_createDatabase();
if(lNewDatabase >= 0){
mDLLAPI.SQL_selectDatabase(lNewDatabase);
if(mDLLAPI.SQL_loadDatabase(lFilePath $ mDefaultContentDBs[il])){
`log("<<< GameContentDatabase loaded: " $ lFilePath $ mDefaultContentDBs[il]);
}
}
}
}
}
Now all DefaultContentDBs defined in .ini will be loaded into in-memory databases. use SQL_selectDatabase to set a specific in-memory database as active for the next queries (see Appendix A for detailed informations to the SQL_ functions of DB_DLLAPI).
After I load all my content databases into memory, I now will create my main gameplay database and store its identifier in my databasemanager, for easier access. The gameplay database is the most important, I wanna use it to store the character data for all pawns (the main character, the NPC, everything). Additionally the gameplay database will be saved to hard drive as a savegame and load back into memory when we load a previous stored savegame.
So lets do some small modification to the class and add the following function to the databasemanager. Don't forget to call the new function in initDatabase() after loading the content databases.
DB_DatabaseMgr.uc
Code:
var int mGameplayDatabaseIdx; // additional variable to store the current database index of our gameplay database
function initDatabase(GameInfo aGameInfo)
{
mGameInfo = aGameInfo;
mDLLAPI.SQL_initSQLDriver(mDatabaseDriver); // automatically create one empty DB
loadGameContentDatabases();
createGameplayDatabase();
}
function createGameplayDatabase()
{
local int lNewDatabase;
local string lFilePath;
lNewDatabase = mDLLAPI.SQL_createDatabase();
if(lNewDatabase >= 0 && mDefaultGameplayDB != ""){
lFilePath = mUserCodeRelPathUDKGame $ mDataRootPath;
mDLLAPI.SQL_selectDatabase(lNewDatabase);
if(mDLLAPI.SQL_loadDatabase(lFilePath $ mDefaultGameplayDB)){
`log("<<< GameplayTemplate loaded: " $ lFilePath $ mDefaultGameplayDB);
}
mGameplayDatabaseIdx = lNewDatabase;
}
}
As you can see I load mDefaultGameplayDB by default into the new created empty in-memory database. I use this, because my default gameplay database contains all tables i will need for my game, including an internal defines gameplayDB version, so I am able to verify the used gameplayDB version after an update for my game, where the database structure might be changed. Just to ensure the player can use an older savegame with a newer game version.
The system for filling the gameplay database with data, keep the tables up to date is fairly simple if you know how to work with a SQL database. Just execute some queries for updating, inserting and deleting table content.
Tutorials for this can be found here http://www.sql-tutorial.net/, for additional informations to the current SQL syntax available for SQLite check out http://www.sqlite.org/lang.html.
Step: 6 - Keep the gameplay database alive even after a map change
Now we have a working gameplay database, loaded our content and were able to update and query out databases with the given SQL functions. All of this is possible because the GameInfo is always alive, never being deleted. Keep in mind, the DB_BaseGameInfo owns the databasemanager object which owns the DLLAPI itself. If the BaseGameInfo will be deleted, our databasemanager and the DLLAPI will be deleted to and all our in-memory databases would be lost.
This leads to the next step: Saving our gameplay database and keep it alive even if we do a map/gameinfo change.
I created some functions which will help me with this. Maybe not the smartest way, but a working way.
First I will add two functions to my DB_FileMgr.
DB_FileMgr.uc
Code:
function loadTransition()
{
local string lFilePath;
lFilePath = mDatabaseMgr.mUserCodeRelPathUDKGame $ mDatabaseMgr.mDataRootPath;
if(mDLLAPI.IO_fileExists(lFilePath $ "transition.s3db")){
mDLLAPI.SQL_selectDatabase(mDatabaseMgr.mGameplayDatabaseIdx);
if(mDLLAPI.SQL_loadDatabase(lFilePath $ "transition.s3db")){
`log("<<< GameDatabase restored from Transition: " $ lFilePath $ "transition.s3db");
}
}
}
function saveTransition()
{
local string lFilePath;
lFilePath = mDatabaseMgr.mUserCodeRelPathUDKGame $ mDatabaseMgr.mDataRootPath;
mDLLAPI.SQL_selectDatabase(mDatabaseMgr.mGameplayDatabaseIdx);
if(mDLLAPI.SQL_saveDatabase(lFilePath $ "transition.s3db")){
`log("<<< GameDatabase saved to Transition: " $ lFilePath $ "transition.s3db");
}
}
The transition file is like a quick save of the current gameplay database. My plan is to save it, when leaving a map, and reload it, when entering the new map.
So I modified my BaseGameInfo and added this functions:
DB_BaseGameInfo.uc
Code:
function performServerTravel(string aURL)
{
if(Class == class'DB_BaseGameInfo'){
`log("<<<< GameInfo.performServerTravel(): DO NOT SAVE TRANSITION MAP");
}else{
`log("<<<< GameInfo.performServerTravel(): SAVE TRANSITION MAP");
mFilemanager.saveTransition();
}
WorldInfo.ServerTravel(aURL);
}
function performServerTravelDone(string aOptions)
{
if(Class == class'DB_BaseGameInfo'){
`log("<<<< GameInfo.performServerTravel(): DO NOT LOAD TRANSITION MAP");
return;
}
`log("<<<< GameInfo.performServerTravel(): LOAD TRANSITION MAP");
mFilemanager.loadTransition();
}
and added performServerTravelDone to the InitGame:
Code:
event InitGame( string Options, out string ErrorMessage )
{
Super.InitGame(Options, ErrorMessage);
mDatabase.initDatabase(self);
mFilemanager.initFilemanager(mDatabase);
// do enter function
performServerTravelDone(Options);
}
ATTENTION:
The last thing that should be done is calling DB_BaseGameInfo.performServerTravel(string aURL) if you want to change the map instead of a simple ConsoleCommand("open"), so you ensure the transition database is stored to hard drive before leaving the current map. This is the only thing where you have to careful when writing your scripts.
Step: 7 - Query the databases
After everything is set up now, we can finally perform some queries on our databases. The following code uses the content database to generate random names (forename, surname, gender) from a database with a simple query, gather the informations from the result and log it onto the console. The code can be placed in any class you wanna have it, I put it into the PlayerController and declared it as exec just for the tutorial now.
The only thing you have to keep in mind when you want to query the database: You need the GameInfo.
DB_PlayerController.uc
Code:
class DB_PlayerController extends UDKPlayerController;
exec function DB_generateNames(int aMaxNameCount)
{
local DB_DLLAPI lDLLAPI;
local string lForename;
local string lSurname;
local string lGender;
lDLLAPI = DB_BaseGameInfo(WorldInfo.Game).getDBMgr().getDLLAPI();
lDLLAPI.SQL_selectDatabase(0); // content database 0!
if(lDLLAPI.SQL_queryDatabase("SELECT DB1.FORENAME, DB2.SURNAME, DB1.GENDER FROM NAMES AS DB1, NAMES AS DB2 ORDER BY RANDOM() LIMIT "$aMaxNameCount$";")){
while(lDLLAPI.SQL_nextResult()){
lForename = class'DB_Defines'.static.initString(30); // prepare string for DLLBind usage
lSurname = class'DB_Defines'.static.initString(30); // prepare string for DLLBind usage
lGender = class'DB_Defines'.static.initString(1); // prepare string for DLLBind usage
lDLLAPI.SQL_getStringVal("FORENAME", lForename);
lDLLAPI.SQL_getStringVal("SURNAME", lSurname);
lDLLAPI.SQL_getStringVal("GENDER", lGender);
`log("Generated name: "$lForename$" "$lSurname$" ("$lGender$")");
}
}
}
DefaultProperties
{
}
The static function initString in class DB_Defines is a simple function used to create a string with a certain length, so we can use it in the DLL to save out a string value to.
Appendix: A - DLLAPI Interface functions
function SQL_initSQLDriver(int aSQLDriver)
- Initalize out SQL driver and create a first empty in-memory database.
function int SQL_createDatabase()
- Create a new in-memory database.
- Return an index identifier used by SQL_selectDatabase to select the database as active database.
function SQL_closeDatabase(int aDbIdx)
- Close a in-memory datbase with given aDbIdx and remove it from memory
function bool SQL_selectDatabase(int aDbIdx)
- Set an in-memory database at given index as active database. All following functions work on the selected database!
- Return true if database selection was successful.
function bool SQL_loadDatabase(string aFilename)
- Load a database from given filename into the selected in-memory database. ''Completely override previous content!''
- Return true if loading was successful.
function bool SQL_saveDatabase(string aFilename)
- Saves the current selected in-memory database to given filename.
- Return true if saving was successful.
function bool SQL_queryDatabase(string aStatement)
- Saves the current selected in-memory database to given filename.
- Same like calling SQL_prepareStatement and SQL_executeStatement.
- Return true if query execution was successful.
function SQL_prepareStatement(string aStatement)
- Prepare a statement for the selected database.
- All statements for a database will be cached internally for reuse, if aStatement wasn't changed (case-insensitive comparison)
function bool SQL_bindValueInt(int aParamIndex, int aValue)
- Binds a integer value to an parameter of the previous prepared statement for the given aParamIndex.
- Returns true if binding was successful.
function bool SQL_bindNamedValueInt(string aParamName, int aValue)
- Binds a integer value to an parameter of the previous prepared statement for the given aParamName.
- Returns true if binding was successful.
function bool SQL_bindValueFloat(int aParamIndex, float aValue)
- Binds a float value to an parameter of the previous prepared statement for the given aParamIndex.
- Returns true if binding was successful.
function bool SQL_bindNamedValueFloat(string aParamName, float aValue)
- Binds a float value to an parameter of the previous prepared statement for the given aParamName.
- Returns true if binding was successful.
function bool SQL_bindValueString(int aParamIndex, string aValue)
- Binds a string value to an parameter of the previous prepared statement for the given aParamIndex.
- Returns true if binding was successful.
function bool SQL_bindNamedValueString(string aParamName, string aValue)
- Binds a string value to an parameter of the previous prepared statement for the given aParamName.
- Returns true if binding was successful.
function bool SQL_executeStatement()
- Executes the previous prepared statement.
- Return true is execution was successful.
function bool SQL_nextResult()
- Set result pointer to the next result set.
- Return true if result pointer is valid. Use while(SQL_nextResult()) to iterate through the query result.
function SQL_getIntVal(string aParamName, out int aValue)
- Get the integer value for the given aParamName from the current result set.
- If aParamName is invalid aValue will be 0.
function SQL_getFloatVal(string aParamName, out float aValue)
- Get the float value for the given aParamName from the current result set.
- If aParamName is invalid aValue will be 0.0.
function SQL_getStringVal(string aParamName, out string aValue)
- Get the string value for the given aParamName from the current result set.
- If aParamName is invalid aValue will be an empty string.
// DEPRECATED
The following functions are still available but marked as deprecated and shouldn't be used any longer. Like the three getter functions they return the specific value for a given index within the result (means the column index of the result row):
function SQL_getValueInt(int aColumnIdx, out int aValue)
function SQL_getValueFloat(int aColumnIdx, out float aValue)
function SQL_getValueString(int aColumnIdx, out string aValue)
Appendix: B - SQL Administration tools & useful links
When working with a database it is helpful to use external tools for database administration. Means creating tables, fill tables with data, and develop queries which will be used by the UnrealScripts at runtime. Compiling and running Unreal everytime you change the database to see the results is time consuming and not necessary.
One of the tools I use for creating a database and administrate it:
Other useful links:
Bookmarks