Loading...

EC2019 VISUALIZER (PART 4)

The Blueprint Utility Functions

Previously...

In Part 3: Starting the UI I covered the initial setup and UI for the match select screen. I ended that post with the outline of the new Blueprint Utility Library C++ class.

In this post, I'll cover the selecting and verification of the match directory, as well as how the UI fits together with the utility class.

Get the match directory

The first bit of C++ I added was the method that calls the native file dialog of the operating system, to let the user select the match directory.

In the generated class definition that I discussed in the previous post, I declared the following function:


UFUNCTION(BlueprintCallable, Category = "EC2019Utility")
static void GetMatchRootDirectory(const FString& DialogTitle,
        const FString& DefaultPath, FString& OutRootDir);

Functions in Unreal Engine can be declared in standard C++ syntax, or by preceding the declaration with the UFUNCTION() macro. This macro allows additional information about the function to be provided to the engine by means of Function Specifiers.

For this (and most of the utility functions) I needed the ability to call the function from Blueprints. I added the BlueprintCallable function specifier to achieve this.

The Category function specifier is just a label under which the editor will group the functions. Any C++ function that is available to Blueprints, are available as Blueprint nodes.

The function takes a title for the dialog and a default path to open the dialog on, and a reference to an output path. The parameters are all fo type FString. FString's are just dynamically resizable text objects.

Finally, I prefer using references to variable to capture output, as that leaves the functions return type available to return control codes, or status values in addition to populating the output reference.

Let's look at the implementation of the get match root directory function.


 0: void UEC2019Utilities::GetMatchRootDirectory(const FString& DialogTitle, 
        const FString &DefaultPath, 
        FString& OutRootDir) 
1: { 2: if(GEngine) { 3: if(GEngine->GameViewport) { 4: void *ParentWindowHandle = GEngine->GameViewport-> GetWindow()->GetNativeWindow()->GetOSWindowHandle(); 5: 6: IDesktopPlatform* DesktopPlatform =
FDesktopPlatformModule::Get(); 7: if(DesktopPlatform) { 8: DesktopPlatform->OpenDirectoryDialog(ParentWindowHandle, "Choose Match Directory", "", OutRootDir);
9: } 10: } 11: } 12: }

The GetMatchRootDirectory function is pretty simple. First, I checked if GEngine exists. This is a global pointer to the game engine, but can be null in some circumstances, so it must be checked.

Then I used GEngine to get a reference the currently active viewport client, GameViewPort. This can also be null.

Using the reference to the viewport client I could request a window handle that is native to the running operating system (line 4).

After this, I retrieved an instance of FDesktopPlatformModule which gave me access to some native OS functionality. Checking that the instance is not null I finished the method with a simple call to OpenDirectoryDialog (line 8) which does just that - opens the native OS open directory dialog.

Since the output reference is populated whenever the dialog is dismissed I had to check that it is not empty (in the event that the dialog was cancelled).  

Using the function

Once I completed the first utility function, I need to call it from the blueprint. Going back to the event graph of the WBP_SPLASH_MENU blueprint I create in Part 3 I added a few nodes and the event graph now looks like this:

EC2019_P4_IMG_1

Node 1 is the C++ function I built. Any C++ function tagged with the BlueprintCallable function specifier is made available as a blueprint node by the engine. Note that the inputs, Dialog Title and Default Path specified in the function definition are available as pins on the blueprint node. The FString reference OutRootDir in the function definition is available as an output pin on the node.

Node 2, is a built in node called Branch. This is a simple if statement, that I used to check if the match directory was set successfully. If this is not the case (such as when the user hits cancel on the open file dialog) I use the previous reference to the media player and make another call to Play (Node 3) to resume the video.

If the directory was set correctly, the output is set to a variable Match Directory. I tend to create variables to set output values for later use. While this is not necessary (you can use the output directly from the pin) it tends to keep the blueprint graphs cleaner. After the variable was set I made a call to RemoveFromParent (Node 4). This is another built-in node, instructing the engine to remove the current UI widget from the screen.

As a final step, I used the event DidRemoveFromParent with the newly selected match directory as input, to signal to the GameMode blueprint that the widget has been removed from the view.

GameMode blueprint?

The last step in calling the Get Match Root Directory node, is informing the GameMode blueprint of the widgets dismissal. So what is the GameMode  blueprint?

In Unreal Engine there are two main classes that handle information about the game being played. The GameMode and the GameState. The GameMode is usually used to manage how players enter the game, how many players are allowed, is the game pause-able etc.

The GameMode also determines how the game is launched, and because of this, I decided to use the GameMode class to manage the loading and unloading of different UI components.

I created a GameMode subclass blueprint, very cleverly named GM_EC2019. The event graph looks like this

EC2019_P4_IMG_2

The blueprint kicks off with the built-in event Event BeginPlay that fires automatically once the engine loads the visualizer. The first node I placed (Node 1) is a call to create an instance of the WBP_SPLASH_SCREEN UI Widget that I built (See Part 3). This is a built in engine method. The class pin was set manually to the correct class, so the engine knows which widget to instantiate. 

Once a reference to the newly instantiated widget is retrieved, it has to be added to the screen. Node 2 is another built in method that places the widget onto the current viewport. As a final step, the DidRemoveFromParent event listener is bound to the widget. The event (Node 4) created here, will listen for the built in DidRemoveFromParent in the widget. Once the widget is removed from the viewport, this event listener will trigger and give an entry point for any logic that I need to execute after the removal from the viewport. Note that in the widget event graph, I added the selected match directory as an input into the DidRemoveFromParent event. That variable is now available as an output from the event listener. 

Node 5 in the graph is where I set the selected directory to a variable in the GameMode blueprint for later use.
At this point, the user has selected the match logs directory, and the game mode is aware of that selection. The next step is to verify that it is a valid directory that contain the match logs. In Node 6, I again instantiated an instance of a second UI widget (WBP_LOADING_SCREEN). This loading screen widget has a variable for the selected output directory, which is set to the selected directory from the game mode. (Node 7).

As a last step, another call was made to add the newly instantiated loading screen widget to the viewport.

The Loading Screen

The loading screen widget displays a progress bar to the user, while some new utility functions verify that the selected directory is valid and contain the necessary match logs.
SPOILER - The progress bar does not indicate actual progress... I cheated here a little bit, as I didn't want to spend too much time trying to bind a C++ variable and monitor it in realtime. The progress for the progress bar is set manually to arbitrary values in between verification steps. Quick and dirty, but it serves the purpose. The user doesn't have to stare at a static screen while verification runs in the background.
The designer layout for the widget looks like this:

EC2019_P4_IMG_3

A very simple layout. Two labels (with placeholder text) and two progress bars, as well as the same background and logo as in the first UI widget.

The Event Graph

The (not quite finished) event graph for this widget looks like this:

EC2019_P4_IMG_4

A little more going on here than in the previous widget... Let's break it down a little. The first cluster of nodes is where I set up the progress bars and verify the match directory. That first cluster looks like this:
EC2019_P4_IMG_5
I won't discuss every node, since we'll never get anything done if I do. But as always, if you want information on anything, please feel free to reach out using the Contact Form. Remember how I mentioned some fakery with the progress bar? Well, that's right there in block 1. I started by setting an arbitrary percentage in the percentage bar. I also updated the label and set a short delay to give the user just enough time to read the label. In block two, I called another of the utility functions, to check that the folder contains the two CSV files with the player move summaries. I also saved the filenames of the CSV files into variables. Block 3 updates the percentage to an arbitrary number again, and updates the label. 
If the directory does not contain the two expected CSV files, I print an error to the logs. At the moment, this simply stops the process, with no way to recover. This may be addressed in the future.

The second part of the event graph looks like this:
EC2019_P4_IMG_6
In block one, I called the Get Player Moves utility function twice, once for each CSV filename I saved as variables in the previous step. (Note that the CSV filenames were saved in an array, so we use the GET node to pull out a filename for a given index.) The Get Player Moves function, reads each line of the CSV file and adds it to an array of player move objects. These arrays are set as variables again for later use. In block two I continued the cheating and set an arbitrary value to the progress bar and update the status label.

The last part of the event graph (as far as it is completed) looks like this:
EC2019_P4_IMG_7
Node 1 is a call I made to Verify Match Directory Rounds. This utility function does two things. First, it reads all the round directory names into an array. Second, it compares the size of that array, with the number of lines in the CSV (player move) files. In case a player wins in round x and the match doesn't write the losing players last move, I take the smallest of the two for comparison, using the built in MIN node (node 2). The inputs to the MIN function is the length of the player move arrays read in the previous step.
Once the number of directories is verified, the list of directory paths are saved. Here is an example of using a reference parameter to save output, and keeping the function return value for the purpose of a subsequent check.
If the directory count check passes, the function returns a TRUE boolean, otherwise it returns a FALSE. This boolean was then used in the branch condition (node 3) to check if the process can continue or not.

The final part of the graph is pretty straightforward:
EC2019_P4_IMG_8
Here I set another value for the progress bar and updated the label. I also set the visibility on the second label (that starts hidden) as well as the second progress bar (also initially hidden). The second progress bar and label is there to break up the process between the short steps mentioned already, and the very long step of parsing the Console.txt files from every round directory. The method to do this parsing will be added to the event graph at a later stage.

Back to the code

A quick look at the utility methods used in this UI widget, starts with the Verify Match Directory CSV method.

bool UEC2019Utilities::VerifyMatchDirectoryCSV(const FString directory,
TArray& csvFiles) {
    UEC2019Utilities::GetAllFilesInDirectory(csvFiles, directory, 
        false, "", "csv", ""); 
        return true; 
}

This method is a simple one liner that reads the filenames of all CSV files in the match folder. I didn't bother check the number of files present, as this will implicitly be checked in a subsequent step.
The next method, Get Player Moves is listed below:


void UEC2019Utilities::GetPlayerMoves(TArray<UPlayerMove*>& Moves, const FString directory, const FString fileName) {
    FString finalPath = directory + "/" + fileName;
    
    TArray lines;
    FFileHelper::LoadANSITextFileToStrings(*finalPath, &IFileManager::Get(), lines);
    
    for(int i = 0; i < lines.Num() - 1; i++) {
        if(i == 0) { continue; }
        FString line = lines[i];
        TArray lineArray = {};
        
        line.ParseIntoArray(lineArray, TEXT(","), false);
        
        UPlayerMove* newMove = NewObject();
        newMove->RoundNumber = FCString::Atoi(*lineArray[0]);
        newMove->commandType = *lineArray[1];
        newMove->commandStr = *lineArray[2];
        newMove->activeWormId = FCString::Atoi(*lineArray[3]);
        newMove->playerScore = FCString::Atoi(*lineArray[4]);
        newMove->playerHealth = FCString::Atoi(*lineArray[5]);
        
        Moves.Add(newMove);
    }
}

For this method I created a simple data class PlayerMove, (inheriting from UObject) to store the information provided in each line of the CSV containing the player move summaries.

Using the built in file manager's method, LoadANSITextFileToStrings, I loaded the CSV file into an array of CSV rows. Then I took each line one by one, split it into an array of elements and used the built in parsing tools to assign each value to the right container on the PlayerMove object, and then adding the PLayerMove to an array of moves.
This method is called twice, once for each player, and the result is that I have two arrays consisting of PlayerMove objects for each move each player made during the match. 

The next method I used, was the VerifyMatchDirectoryRounds method.

bool UEC2019Utilities::VerifyMatchDirectoryRounds(const FString directory, const int roundCheck, 
TArray& RoundDirectories) {
    TArray Rounds;
    UEC2019Utilities::GetAllRoundDirectories(RoundDirectories, directory);
   
    if(roundCheck == RoundDirectories.Num()) {
        return true;
    }

    return false;
}

bool UEC2019Utilities::GetAllRoundDirectories(TArray& Rounds, FString rootFolder) {
    FPaths::NormalizeDirectoryName(rootFolder);
    IFileManager &FileManager = IFileManager::Get();
    
    FString finalPath = rootFolder + "/*";
    FileManager.FindFiles(Rounds, *finalPath, false, true);
    
    Rounds.Sort();
    
    return true;
}

This method is fairly straightforward as well. It calls another utility method, GetAllRoundDirectories, to get the directory paths of all the rounds, and then compares the number of round directories found, to the input roundCheck (the number of moves in the player CSV files).

The listing for GetAllRoundDirectories is also very straightforward. I first ask the engine for an instance of the active implementation of IFileManager and then use the file manager's FindFiles method. Note the last to input parameters to this method. The parameters are boolean values indicating wether we are looking for files or directories respectively - which I set to false (for files) and true (for directories). One small thing, if you are looking for directories, setting the booleans correctly will not work, unless you append "/*" to your input path as well. GetAllRoundDirectories uses the FindFiles methods to get the file paths for the round directories, then sorts them, and returns.

That's it...

This is as far as I've progressed. The next step is going to be the ReadMapStateFromText method, that will read the Console.txt file from each round directory, and build up a snapshot of the map state for each point in time (round) over the course of the match.
Once that method is built, I'll get into the actual map generation and assets creation for laying out the tiles that the visualiser will be using.

As always, feel free to drop a comment with any questions, critique or suggestions below, or contact me using the Contact Form.