I’ve been trying to integrate MySQL into my game’s server-side code, but I feel the way I’m doing it is lacking. I’m pretty sure some mutex/threading is needed here, but I don’t know if UE4 has such classes or if I should use std::thread/mutex. Additionally, for replicated variables that issue function calls, I want the client and the server to both execute different code from each other (ie. when a character levels up, the client displays a visual effect while the server updates a record in the database.
Although lengthy, I’m not posting my entire code here, but I need advice on all of this that I’m posting below. First, I started off modifying <project>.Build.cs to link MySQL if the server code is built:
public MyProject(TargetInfo Target)
{
// default module add code is here, snipped it out for irrelevancy to this thread post
if (UEBuildConfiguration.bWithServerCode)
LoadMySQL(Target);
}
public bool LoadMySQL(TargetInfo Target)
{
bool supported = false;
if ((Target.Platform == UnrealTargetPlatform.Win64) || (Target.Platform == UnrealTargetPlatform.Win32)
{
supported = true;
string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32";
string LibrariesPath = Path.Combine(ThirdPartyPath, "MySQL", "Libraries", PlatformString);
PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, PlatformString, "libmysql.lib"));
PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, PlatformString, "mysqlclient.lib"));
}
if (supported)
PublicIncludePaths.Add( Path.Combine( ThirdPartyPath, "MySQL", "Includes" ) );
Definitions.Add(string.Format( "WITH_MYSQL={0}", supported ? 1 : 0 ));
return supported;
}
Then, I override the default game module implementation with my own if the server code is compiled, otherwise, FDefaultGameModuleImpl will do:
MyProject.h:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Engine.h"
#if WITH_SERVER_CODE
#include "my_global.h"
#include "mysql.h"
class FMyProjectGameModule : FDefaultGameModuleImpl
{
MYSQL *world_db, *save_db;
public:
MYSQL* GetWorldDB() const { return world_db; }
MYSQL* GetSaveDB() const { return save_db; }
virtual void StartupModule() override;
virtual void ShutdownModule();
};
#endif // WITH_SERVER_CODE
MyProject.cpp:
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyProject.h"
#if WITH_SERVER_CODE
#include <cstdio>
void FMyProjectGameModule::StartupModule() override
{
std::FILE *configfile;
char wdb_addr[255], wdb_user[32], wdb_pw[32], wdb_name[32];
char sdb_addr[255], sdb_user[32], sdb_pw[32], sdb_name[32];
unsigned short wdb_port, sdb_port;
if (!(configfile = std::fopen("server.cfg", "r"))) // unsure of where 'server.cfg' will be found yet
{
UE_LOG(LogTemp, Fatal, TEXT("Unable to read server.cfg"));
std::fclose(configfile);
}
std::fscanf("WorldDatabase=%s;%s%s;%s;%hu", wdb_addr, wdb_user, wdb_pw, wdb_name, &wdb_port);
std::fscanf("SavesDatabase=%s;%s%s;%s;%hu", sdb_addr, sdb_user, sdb_pw, sdb_name, &sdb_port);
std::fclose(configfile);
if (!(world_db = mysql_init(NULL)))
{
UE_LOG(LogTemp, Fatal, TEXT("Unable to initialise the world database connection"));
mysql_close(world_db);
}
if (!(save_db = mysql_init(NULL)))
{
UE_LOG(LogTemp, Fatal, TEXT("Unable to initialise the saves database connection"));
mysql_close(save_db);
}
if (!(mysql_real_connect(world_db, wdb_addr, wdb_user, wdb_pw, wdb_name, wdb_port, NULL, 0)))
{
UE_LOG(LogTemp, Fatal, TEXT("Unable to connect to world database connection at %s:%hu"), wdb_addr, &wdb_port);
mysql_close(world_db);
}
if (!(mysql_real_connect(save_db, sdb_addr, sdb_user, sdb_pw, sdb_name, sdb_port, NULL, 0)))
{
UE_LOG(LogTemp, Fatal, TEXT("Unable to connect to saves database connection at %s:%hu"), sdb_addr, &sdb_port);
mysql_close(save_db);
}
}
void FMyProjectGameModule::ShutdownModule()
{
mysql_close(world_db);
mysql_close(save_db);
}
IMPLEMENT_PRIMARY_GAME_MODULE(FMyProjectGameModule, MyProject, "MyProject");
#else
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, MyProject, "MyProject");
#endif // WITH_SERVER_CODE
and lastly, I’m unsure of how to write a function that does somethiung different on client/server. I’m thinking “#if WITH_SERVER_CODE … #else … #endif” but I have a feeling that’s not correct:
MyPlayerState.h (snippet):
...
UPROPERTY(replicatedusing = OnRep_Level, blueprintreadwrite, category = CharacterData)
uint8 Level;
UFUNCTION(server)
void OnRep_Level();
...
MyPlayerState.cpp (snippet):
...
void AMyPlayerState::OnRep_Level_Implementation()
{
if (mysql_query(MyProject->GetSaveDB(), TCHAR_TO_UTF8(*FString.Printf("UPDATE `characters` SET `level`=%hhu WHERE `id`=%d", Level, PlayerId))))
UE_LOG(LogTemp, Warning, TEXT("Failed to update level for character ID %d!"), PlayerId);
}
...
Also, I know I’m using FILE and fscanf as opposed to std::ifstream here. It’s probably not recommended, but I find reading formatted input from a config file preferrable over streamed input, given that I want something reminiscent of an .INI file.