Search code examples
databaseiniunreal-engine4

How to change game data on the fly in a packaged UE4 project?


My question seems to be pretty straight forward but, I haven't been able to find any solutions to this online. I've looked at a number of different types of objects like DataTables and DataAssets only to realize they are for static data alone.

The goal of my project is to have data-driven configurable assets where we can choose different configurations for our different objects. I have been able to successfully pull JSON data down from the database at run-time but, I would like to save said data to something like a Data Asset or something similar that I can read and write to. So when we pull from said database later we only pull updates to our different configurations and not the entire database (every time at start-up).

On a side note: would this be possible/feasible using an .ini file or is this kind of thing considered too big for something like that (i.e 1000+ json objects)?

Any solutions to this problem would be greatly appreciated.


Solution

  • Like you say, DataTable isn't really usable here. You'll need to utilize UE4's various File IO API utilities.


    Obtaining a Local Path

    This function converts a path relative to your intended save directory, into one that's relative to the UE4 executable, which is the format expected throughout UE4's File IO.

    //DataUtilities.cpp
    
    FString DataUtilities::FullSavePath(const FString& SavePath) {
        return FPaths::Combine(FPaths::ProjectSavedDir(), SavePath);
    }
    

    "Campaign/profile1.json" as input would result in something like:

    "<game.exe>/game/Saved/Campaign/profile1.json".

    Before you write anything locally, you should find the appropriate place to do it. Using ProjectSaveDir() results in saving files to <your_game.exe>/your_game/Saved/ in packaged builds, or in your project's Saved folder in development builds. Additionally, FPaths has other named Dir functions if ProjectSavedDir() doesn't suit your purpose.

    Using FPaths::Combine to concatenate paths is less error-prone than trying to append strings with '/'.


    Storing generated JSON Text Data on Disk

    I'll assume you have a valid JSON-filled FString (as opposed to a FJSONObject), since generating valid JSON is fairly trivial.

    You could just try to write directly to the location of the full path given by the above function, but if the directory tree to it doesn't exist (i.e., first-run), it'll fail. So, to generate that path tree, there's some path processing and PlatformFile usage.

    //DataUtilities.cpp
    
    void DataUtilities::WriteSaveFile(const FString& SavePath, const FString& Data) {
        auto FullPath = FullSavePath(SavePath);
        FString PathPart, Disregard;
        FPaths::Split(FullPath, PathPart, Disregard, Disregard);
        IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
        if (PlaftormFile.CreateDirectoryTree(*PathPart)){
            FFileHelper::SaveStringToFile(Data, *FullPath);
        }
    }
    

    If you're unsure what any of this does, read up on FPaths and FPlatformFileManager in the documentation section below.

    As for generating a JSON string: Instead of using the Json module's DOM, I generate JSON strings directly from my FStructs when needed, so I don't have experience with using the Json module's serialization functionality. This answer seems to cover that pretty well, however, if you go that route.


    Pulling Textual Data off the Disk

    // DataUtilities.cpp
    
    bool DataUtilities::SaveFileExists(const FString& SavePath) {
        return IFileManager::Get().FileExists(*FullSavePath(SavePath));
    }
    
    FString DataUtilities::ReadSaveFile(const FString& SavePath) {
        FString Contents;
        if(SaveFileExists(SavePath)) {
            FFileHelper::LoadFileToString(Contents, *FullSavePath(SavePath));
        }
        return Contents;
    }
    

    As is fairly obvious, this only works for string or string-like data, of which JSON qualifies.

    You could consolidate SaveFileExists into ReadSaveFile, but I found benefit in having a simple "does-this-exist" probe for other methods. YMMV.

    I assume if you're already pulling JSON off a server, you have a means of deserializing it into some form of traversable container. If you don't, this is an example from the UE4 Answer Hub of using the Json module to do so.


    Relevant Documentation


    To address your side note: I would suggest using an extension that matches the type of content saved, if for nothing other than clarity of intention. I.e., descriptive_name.json for files containing JSON. If you know ahead of time that you will be reading/needing all hundreds or thousands of JSON objects at once, it would likely be better to group as many as possible into fewer files, to minimize overhead.