Version 2023.05



Legal Notices

Warranty

The only warranties for products and services of GameDriver and its affiliates and licensors (“GameDriver”) are set forth in the express warranty statements accompanying such products and services. Nothing herein should be construed as constituting an additional warranty. GameDriver shall not be liable for technical or editorial errors or omissions contained herein. The information contained herein is subject to change without notice.


Restricted Rights

Contains Confidential Information. Except as specifically indicated otherwise, a valid license is required for possession, use, or copying.


Copyright Notice

© Copyright 2023 GameDriver®, Inc.

Trademark Notices

Unity™ is a trademark of Unity Technologies AsP.

Microsoft® and Visual Studio® are U.S. registered trademarks of Microsoft Corporation.


The only warranties for products and services of GameDriver® and its affiliates and licensors (“GameDriver”)




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.


Take the following example. We have an object in a scene that will display some text when certain criteria are met. The object is under MoveObjectScene > Canvas > TestStatus.




Resolving Objects by Name

In this example, we’re specifically looking for the value of the text property of a UnityEngine.UI.Text component, which is attached to our TestStatus object as shown here.


Initially, we expect this field to be blank, but in our test, the field will change to “Test Complete” if we meet certain criteria. We will not be covering the test case leading up to this scenario, only how to identify the objects involved in the test.




First, we will want to query this object for the value of this text field.


There are a few ways to resolve the object, depending on whether more than one copy of it exists. For now, let’s assume this is the only copy of the object.


Using the GetObjectFieldValue function which requires only an HPath argument, we can use the following line of code to query the text property attached to the Text 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, a NullReferenceException is returned.


Command          Type                            Object                                               Component            Property

       v                     v                                    v                                                            v                            v

api.GetObjectFieldValue<string>("//*[@name='TestStatus']/fn:component('UnityEngine.UI.Text')/@text");


Note that this approach can be used to query any component property as long as the type serialization is supported. Currently supported types include string, int, float, bool, and color. For the example above, we see:

  • The type of parameter being returned is a string.

  • First, the "//*[@name='TestStatus']” portion of the query refers to the relative path to the object, as indicated by the // notation. The asterisk “*” notation refers to a wildcard match for the object’s Tag. The path will return the first object with the name field of “TestStatus

  • Once found, we are looking for the child component of the returned object above, which is specifically a UnityEngine.UI.Text component. We perform this search with an internal function call fn:component containing the argument ('UnityEngine.UI.Text') and then the property of that Component we want, which is the @text property.


Another approach might be to use the absolute path to the TestStatus object, including the name or tags of each object in the path, or both.


          Parent to object                Object                                            Component            Property

                     v                                 v                                                            v                        v

(“/*[@name='Canvas']/*[@name='TestStatus']/fn:component('UnityEngine.UI.Text')/@text")


This might be useful if there is more than one TestStatus object, and the one we want to work with isn’t the first. However, there is an alternative approach for working with multiple instances of an object which is particularly useful for working with cloned objects, which are often created at runtime. For example, take the following scene hierarchy:




The above examples will work for the first instance of TestStatus, but not the second. If that’s the instance we’re looking for, we need to add an instance number to the object query in predicate form [ ], such as:


            Parent(s)                    Object         Instance                            Component               Property

                    v                            v                     v                                            v                        v

(“/*[@name='Canvas']/*[@name='TestStatus'][0]/fn:component('UnityEngine.UI.Text')/@text")


The above will find the first instance of an object with the name TestStatus, with the parent object named Canvas. This may require a different instance number than the absolute path depending on whether there are other objects with the same name in other parts of the object tree.


Additional functions are provided to find the first or last instance of an object, which can be used as follows:


Parent(s)                                Object         Instance

    v                                           v                     v

(“/*[@name='Canvas']/*[@name='TestStatus'][first()]/fn:component('UnityEngine.UI.Text')/@text");



Resolving Objects by Tag

Using object tags to resolve objects is similar to using their name, but can be useful when there are groups of objects with the same name but different tags, or groups of objects with different names and the same tag. This combination of tag and name allows us to fine-tune out object identification so that tests are more resilient to change. Using the same object in our first example, we can use the following for an absolute path:

api.GetObjectPosition(“/Untagged/Untagged[3]”);


This represents the 4th untagged object (arrays start at 0) that is the child of a single object with no duplicates. Not very intuitive, and there is a likelihood these will change at runtime and break our tests. However but if we add the names to that path, it starts to look more clear:


                                                                        Tag                        Name              Tag                          Name

                                                                          v                              v                    v                                v

api.GetObjectPosition(“/Untagged[@name='Canvas']/Untagged[@name='TestStatus']”);


That’s better. Now if the developers add more descriptive tags to these objects, we start to see something more useful. Take the example above where there were two child objects to Canvas named TestStatus. Only this time, the second one is tagged with Popup.



Using the absolute HPath to the second object, we get the following:

api.GetObjectPosition(“/Untagged[@name='Canvas']/Popup[@name='TestStatus']”);


Note the /Popup in the HPath, which will be able to locate that object even if another “TestStatus” object is inserted between the two unless it also uses the Popup tag. Then we would need to add the instance predicate (i.e. [1]) before the end quote.


If your project is undergoing a lot of change and the relative position of the “TestStatus” object changes, it might be better to use a combination of the Relative Path covered in the first section, and the use of tags covered here. Using both, we end up with the following search:

api.GetObjectPosition(“//Popup[@name='TestStatus']”);


The above will resolve to the first instance of an object with the name “TestStatus” and the tag of “Popup”, regardless of where it exists in the scene hierarchy.



Using fn:components() to Resolve multiple Components of the same Type

In situations where selecting and returning field values or calling methods from a Component among a list of components of the same type on an object, fn:components() can be handy.


api.GetObjectFieldValue<int>("//*[@name='TestObject']/fn:components('TestScript')[2]/@intVal");

Notice the index '[2]' which selects the third component on the GameObject named 'TestObject' and then returns the 'intVal' property. 


Using fn:type() to access Static methods and properties

It is possible to access Static methods and functions using fn:type() with the hierarchy path.

api.CallMethod<Quaternion>("fn:type('UnityEngine.Quaternion')", "RotateTowards", new object[] { new Quaternion(), new Quaternion(), maxDegreesDelta });

Using Boolean Operators

GameDriver HierarchyPath also supports any combination of object names or tags using the boolean operators "and" or "or" contained within a predicate clause such as:

api.GetObjectPosition(“//*[@name='Button' and ./fn:component('UnityEngine.UI.Text')/@text = 'Default String Value']”);


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 tag “TagButton” that has a parent object with the name “Canvas”.

api.GetObjectPosition(“//TagButton[../@name = 'Canvas']”);


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.WaitForObject("/GrandParent/Parent/Child/GrandChild");
api.WaitForObject("/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 a number of 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.

api.WaitForObject("//*[contains(.,fn:type('ParentPointer'))]");

Note the usage of 'type' instead of 'component', since we are only checking to see if that component type is extant on the object, and are not accessing any of the component's attributes.



HierarchyPath References as Arguments

Many HierarchyPath functions take various arguments, such as 'component' and 'type' taking strings representing the name of the component type, or the 'float' or 'double' functions which take a string representing a floating point value, for example. While a string can be passed directly to these functions, you can also instead pass a HierarchyPath reference that points to a valid attribute for the expected argument.


DateTime date = api.GetObjectFieldValue<DateTime>("datetime(//*[@name='DateTime']/fn:component('DateTimeTest')/@dateTime, //*[@name='DateTime']/fn:component('DateTimeTest')/@dateFormat)");

In the above example, we are generating a DateTime struct using the HierarchyPath function 'datetime', which takes two strings, the first of which holds the information representing the date and time, and the second of which represents the format that the first argument should be interpreted using. Instead of directly passing two strings as arguments, we are instead passing HierarchyPath references that point to two different string attributes that hold valid string data that can be passed to the 'datetime' function.



Regex Matching

String values can be matched and verified with regular expression in a HierarchyPath's predicate by use of the 'match' function. For instance, the following call would find and wait for 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).+')]")


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.



Accessing Lists

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


int count = api.GetObjectFieldValue<int>("//*[@name='DropdownList']/fn:component('TMPro.TMP_Dropdown')/@m_Options/@m_Options/fn:count()");

Here we are grabbing the current count of elements that make up the list of options on a dropdown component on an object called "DropdownList" by using the 'count' function.


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>("//*[@name='DropdownList']/fn:component('TMPro.TMP_Dropdown')/@m_Options/@m_Options/fn:elementat(0)/@text");


Additionally, it is possible to iterate through every elements' 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.

object[] results = api.GetObjectFieldValue<object[]>("//*[@name='DropdownList']/fn:component('TMPro.TMP_Dropdown')/@m_Options/@m_Options/fn:foreach('text')");
for (int i = 0; i < count; i++)
{
    Assert.AreEqual(results[i], "ListItem" + i);
}



Working with the HierarchyPath Plugin for Unity

The HierarchyPath plugin for Unity allows you to build tests simply by right-click selecting an object in the game and outputting the object’s HPath to the console using either the Relative or Absolute value and for absolute values using either the object Tag, Name, or both.



The output will look something like this:

//*[@name='GameObject']
UnityEngine.Debug:Log(Object)
UnityHPathPlugin.HierarchyPathPlugin:RelativePath()
UnityEditor.GenericMenu:CatchMenu(Object, String[], Int32) (at C:/buildslave/unity/build/Editor/Mono/GUI/GenericMenu.cs:119)


The above reflects the Relative path to an object with the name “GameObject”. We can copy and paste this value into a test query, ignoring the rest of the output above. For example:

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


For the same object, the following appears when we select the “Tag and Name” option (ignoring the unnecessary output):

/Untagged[@name='GameObject']


We can use either in the object query, i.e. api.GetObjectPosition(“/Untagged[@name='GameObject']”); will still be valid.


The HierarchyPath plugin can also be used in Play mode, to help identify objects that are created at run-time.



Working with the HierarchyPath tools for GameDriver

Object Explorer

Another option for capturing HierarchyPath for a given object, including component and property names, is the Object Explorer that was introduced in GameDriver 2.0. The Object Explorer can be found under the Window > GameDriver > Object Explorer menu in the editor.



Once opened, you can anchor the Object Explorer the same as any native Unity editor panel or window. The GameDriver Object Explorer allows you to traverse every object in the active Scene, and export the HierarchyPath from any supported Object, Component, or Property to either the console log or directly to the clipboard.



This makes writing tests simpler, ensuring proper HierarchyPath syntax for all object interactions.


Note: Not all object properties or types are supported at this time and will sometimes throw errors indicating a property cannot be serialized. These errors can be safely ignored.


HPath REPL

The HPath REPL allows you to quickly validate whether a given HierarchyPath resolves to the desired object. This feature was introduced with the Recorder, and more information on how to use it can be found in the Recorder documentation.


HierarchyPath Debugger

The HierarchyPath Debugger is an advanced tool for troubleshooting and working with HPath. It allows you to validate the format of your HPath queries, as well as see the value(s) returned by the search. For more information on how to use the HierarchyPath Debugger, see the documentation here.




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 Unity 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 “/Untagged[@name=’ParentObject’]/MyTag[@name=’MyObject’]

  • 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.

  • Component types, such as fn:component(‘UnityEngine.UI.Text’) or fn:component(‘UnityEngine.MeshRenderer’)

  • Field properties of the object, or component of that object, such as “//*Untagged[@name=’MyObject’]/fn:component(‘UnityEngine.UI.Image’)/@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 Unity, and printing the output to the console. Once you have refined your object search to return the necessary properties, you can incorporate them into your tests.


For additional news and information regarding the use of GameDriver and HierarchyPath, please visit the knowledge base at support.gamedriver.io



Advanced Configuration

You can choose between tag and name as the primary attribute for HPath, simply by modifying the setting in GDIO\Resources\config\gdio.unity_agent.config.txt file:


<hierarchypath primaryattribute="name" />  or <hierarchypath primaryattribute="tag" />


For example, if the configuration is set to <hierarchypath primaryattribute="name" /> then //MyObject is same as //*[@name='MyObject'].


The default is set to tag.