Working with HierarchyPath in Unreal

Learn how to leverage the HierarchyPath (HPath) query language for testing applications built with the Unreal Engine

Introduction

GameDriver uses a proprietary but familiar method to identify objects within a project that we call “HierarchyPath” (or HPath). This approach is similar to XPath, which is an industry-standard XML query language and is designed to make XML queries simple and flexible.

In this guide, we will give several examples of common uses of HierarchyPath, and some more complex scenarios to give you an idea of how to work with your project more effectively. The goal of HierarchyPath is to provide a simple yet flexible interface that can enable resilient object tree traversal.

Resolving Objects by Name

In this example, we will reference an object that has been pre-placed into a scene. A pressure plate. In the Outliner our object has a label of BP_PressurePlate11, but at runtime, this object will have a generated name that we can grab by right-clicking on the object, either in the Outliner, or the game viewport. 

In our case, the HPath is:

/*[@name = 'BP_PressurePlate_C_99']

Or

//BP_PressurePlate_C_99

Inside these pressurePlate objects are components of type BP_InteractionComponent (a user type). To reference these components we add the following syntax to the Hierarchy Path. 

//BP_PressurePlate_C_99/fn:component('BP_InteractionComponent')

If within that component is a boolean named Activate, we can add the field with @ at the end of the query. Alternatively, we can generate the entire path using a right click from the GameDriver Object Explorer, which provides an alternate view into all fields and methods of the currently selected object reflected through Unreal. 

//BP_PressurePlate_C_99/fn:component('BP_Interactable')/@Activated

Empowered with that HPath value we can now use the gamedriver API to get (or set) that field. 

Using the GetObjectFieldValue function( which requires only an HPath argument), we can use the following line of code to query the bool property attached to the BP_InteractionComponent component. GetObjectFieldValue returns the type <T> object defined in the last portion of the HierarchyPath. If no object is found at any point in the lookup, NULL is returned.

Unreal_HPathStructure

Resolving Objects by Class

Just like how you use @name, you can use @class to resolve objects by class. To get all the platforms (of class BP_PressurePlate_C) in our game we can therefore use. 

//*[@class='BP_PressurePlate_C']

Resolving Objects by Tag

Resolving tags is another powerful way to identify objects. The syntax is similar but instead of using @name, or @class you use @tag

In our case, all completed platforms are given a tag of  “Done” if they have been triggered correctly according to some other game logic. So we can query them with:

//*[@tag='Done']

Unfortunately, our game also uses that tag inside of other objects, so in addition to returning completed pressure plates it also returns other objects. To get only the completed pressure plates we can combine elements using boolean operators…

Using Boolean Operators

You can combine any of the name, tag, class, and other characteristics into boolean operators. To identify the objects that contain a ‘Done’ tag and that are also of a certain class we can combine them as so:

//*[@tag='Done'and @class='BP_PressurePlate_C']

To return those not of a certain class with the tag we use

//*[@tag='Done'and @class!='BP_PressurePlate_C']

You can also use OR boolean operators and combine them all with name, class, and tag selectors as required. 

Using Self, Parent, and Descendant Axis

The self axis ('.') can be used to reference the currently resolving object from within a predicate, as can be seen in the following example:

api.WaitForObject("//*[./*/@name='Child']");

Here we are looking for an object that has a child object (since '.' references the current object, './*' would therefore reference its child) with the name "Child".

It is also possible to find objects using references to their ancestor objects by utilizing the parent axis ('..'). The below will look for an object with the name “ChildActor” that has a parent object with the tag “Destroyed”.

api.GetObjectPosition(“//ChildActor[../@tag= 'Destroyed']”);

The parent axis can be used multiple times in sequence to reference objects that are not immediate relatives (i.e. not a direct parent or child) of the object. If, for example, we wanted to find an object that we had no information about, other than that it had a grandchild object (a child object of one of its own child objects) with the name "ButtonText", we could find it via repeated uses of the parent axis such as the following:

api.GetObjectPosition(“//*[@name='ButtonText']/../..”);

The first usage of the parent axis elevates the reference to the "ButtonText" object's immediate parent, and then the second use elevates us to its parent's parent (i.e. the grandparent).

To access any descendant object, regardless of the distance of the relationship, you can use the descendant axis ('//').

api.GetGameObject("/GrandParent/Parent/Child/GrandChild"); api.GetGameObject("/GrandParent//GrandChild");

The two queries will find the same object, as the second call will look for any descendant of the GrandParent object for an object tagged as GrandChild, regardless of how far down in the object tree it may be.

Using "contains" to locate objects by sub-properties

HierarchyPath can locate objects using any sub-property value associated with that object, using the syntax contains(haystack, needle). This can be used with several types of properties, including simple values as seen with the following:

api.WaitForObject("(//*[contains(@name, 'Text')])[0]");

The above would return the first object that had the substring "Text" somewhere in its name. The same can be applied to any simple value property a component might have.

Contains can be used for more complex sub-properties as well, such as searching for objects with specific components.

Regex Matching

String values can be matched and verified with regular expressions in a HierarchyPath's predicate by use of the 'match' function. For instance, the following call would get any object that had a name that consisted of either the word 'Enemy' or 'NPC', followed by an underscore and either the word 'Attack' or 'Talk', and then followed by any number of other characters.

api.WaitForObject("//*[match(@name, '(Enemy|NPC)_(Attack|Talk).+')]")

Accessing Lists

In cases where it is necessary to read the values of a list-based component, such as a dropdown menu, or access items within a TArray. HierarchyPath functions are provided for that purpose.

Count

Consider the following property definition of an example Weapon class, which contains exposed items id, damage, life, and offset. 

UCLASS()
class UWeapon: public UObject {
...
UPROPERTY()
FString Text;
UPROPERTY()
int id;
UPROPERTY()
float damage;
UPROPERTY()
double ;
UPROPERTY()
FVector offset;
...

If our hero contains a TArray of such Weapons in a variable named MyWeapons then we can access the length of that array using fn:count()

//*[contains(@name,'MyHero_')]/@MyWeapons/fn:count()

Similarly, if we have a ComboBox named WeaponBox in our user interface we can find out how many options are in that list using a similar HPath:

int count = api.GetObjectFieldValue<int>("//*[contains(@name,'userHUD_C')]/WeaponBox/@DefaultOptions/fn:count()");

ElementAt

We can also access an element at a specific position in the list, such as with the following example in which we access the text value of the first (index 0) element in the list by utilizing the elementat function which takes the desired index as a parameter:

string itemZero = api.GetObjectFieldValue<string>("//*[contains(@name,'userHUD_C')]/WeaponBox/@DefaultOptions/fn:elementAt(0)");

Additionally, it is possible to iterate through every element's value by taking advantage of the foreach function which takes the name of the desired value (such as the 'Text' value) and returns an array of those values in order of their corresponding element. These values can then be easily iterated over, as demonstrated below.

string[] results = api.GetObjectFieldValue<string[]> ("//*[contains(@name,'MyPawn_')]/@TestItems/fn:foreach('Text')");

for (int i = 0; i < stringResults.Length; i++)
{
Console.WriteLine(stringResults[i]);
}

Order of Operations

It is important to remember the order of operations of HierarchyPath parsing, particularly when it comes to the use of index predicates.

api.WaitForObject("/*[@name='Button']/*[1]"); api.WaitForObject("(/*[@name='Button']/*)[1]");

The above two lines of code seem nearly indistinct, and in many cases, they might even produce the same result, but the distinction can make a significant difference.

In both cases, it starts by finding objects with the name "Button" but then diverges due to the difference in what the index predicate ('[1]') is considered to be modifying. The first statement considers the index predicate to be modifying the wild card tag, and will therefore return all objects that are the second child of an object named "Button". The second statement considers the index predicate to be modifying the entire path (due to enclosing the path in parenthesis), and will therefore first find all objects that are children of an object called 'Button', and then return only the second object in the list of objects found.

Putting it All Together

In this document, we covered the basics of using HierarchyPath in your GameDriver tests. There are many ways to combine the concepts presented here to build resilient automation for your Unreal projects. You can locate objects by any combination of the following properties:

  • Relative object path using the //* notation

  • The full path to the object, such as “/ActorIdName[@class=’BP_MyActor_C_0’]/ChildActorIdName

  • Search predicates within square brackets,e.g. “//*[@name=’MyObject’ and @tag=’MyTag’]

  • An object instance number is used for locating also using the predicate notation, e.g. [3]. Remember, indexes start with 0, e.g. "/Grandparent/*[1]"

  • Field properties of the object, or component of that object, such as “//MyActor/fn:component(‘MyActorComponent’)/@color

The HierarchyPath structure is designed to be flexible, allowing you to handle a wide range of scenarios in your GameDriver tests. If you find a scenario that you are unfamiliar with, start with the basics and refine from there. For example, you might start by searching for an object using the output from the GameDriver plugin for Unreal, and printing the output to the console. Once you have refined your object search (perhaps using HPath Debugger and Object Explorer) to return the necessary properties, you can incorporate them into your tests.

Tutorial

For an interactive tutorial on how to use the HierarchyPath query language, visit https://hierarchypath.gamedriver.io/ in a desktop browser.