Learn how to build tests for the Cropout demo game for Unreal Engine using GameDriver
Cropout is the casual real-time strategy game from Unreal. In the game, players collect resources and can build houses, farms, trees, and shrubs. The landscape is dynamically generated, as are the resources. The game was released to showcase 5.2 features like Enhanced Input and CommonUI and presents a more realistic use case to demonstrate automated testing with Gamedriver. You can download Cropout Here:
https://www.unrealengine.com/en-US/blog/cropout-casual-rts-game-sample-project
In this article, we will walk you through writing some simple tests to demonstrate GameDriver usage. We will learn to explore the user interface, investigate AActors properties, and execute methods, as well as simulate mouse and touch input which can be tested on Android and iOS.
Before you get started, you should follow our basic tutorial for getting gamedriver installed and set up. Once you have the plugin installed and the API downloaded and set up (we suggest a C# NUnit test) you can follow along here.
Referencing Actors with HierarchyPath
AActors are the main objects in Unreal Games. They are shown in the Outliner tab of the Unreal Editor. Through this view, you can right-click on any AActor to generate a HierarchyPath (or HPath) for that object. The “Relative” HPath below is one way to quickly select objects by name.
//BP_Villager_C_0
You can generate a Relative, Absolute, or Wildcard Path, and edit them into powerful expressions once you get comfortable with what HPath can do. You can also select objects by matching a part of the name, the class, or tags. You can even match objects based on field values. For more information on using the HierarchyPath language, see this article.
Calling methods on Actors.
Now that we can select AActors objects we can move on to show how we can access and manipulate those objects in the game. In this example, our villager AActors can be assigned tasks so they collect a particular resource. They can also consume food, and build new structures.
A simple method call
We will start with a simple test to confirm that the Eat() function is working correctly. To illustrate this simple test case we call Eat() on a villager and then validate that the food has gone down by the right amount (3). Note: Getting the value of the Food is covered a bit later when we show how to extract values from the user interface.
public void Test11_EatWorksCorrectly()
{
int foodBefore = GetFood();
api.CallMethod("/*[@name = 'BP_Villager_C_0']", "Eat");
int foodAfter = GetFood();
Assert.AreEqual(foodBefore - 3, foodAfter);
}
Changing Job - A more complex method example
You can use Gamedriver’s Object Explorer tool to see the fields and methods available to be called. By selecting a villager in the Outliner we see the available methods and fields. If we right-click on “Change job” we can generate a sample method call.
Sample usage:
api.CallMethod("/*[@name = 'BP_Villager_C_0']", "Change Job", new object[]{string}); //replace types with objects
The sample usage shows that we need to pass in a String. In this game “Food”, “Wood”, or “Stone” are the potential valid parameters. We use the following code in our test to change the villager’s job to collecting food.
api.CallMethod("/*[@name = 'BP_Villager_C_0']", "Change Job", new object[] { "Food"});
We can visually confirm that the Villager is collecting food, but how do we validate that the villager is correctly assigned? There are several ways, and it may depend on what you’re testing.
Validate by searching for a tag on the Villager.
To confirm that the villager has changed jobs we need to look at some field or value of it. In this game, tags are used by the designers to determine the Villager’s job, so we can call the ActorHasTag method on the object to see if a tag is present.
api.CallMethod("/*[@name = 'BP_Villager_C_0']", "Change Job", new object[] { "Food" });
bool var = api.CallMethod<bool>("/*[@name = 'BP_Villager_C_0']", "ActorHasTag", new object[] { "Food" });
Assert.IsTrue(var);
If we wanted to see how much food the Villager was carrying we could access the field directly using GetObjectFieldValue
int quantity = api.GetObjectFieldValue<int>("//BP_Villager_C_0/@Quantity");
Validate by looking at a visual asset (the gatherer hat)
Although the simple tests above work perfectly, there are times when you want to inspect the visual elements of a game to ensure users are getting the visual feedback they expect. In Cropout, the villager wears a different hat depending on their assigned job.
This is a little more complex, but with Gamedriver tools is still relatively easy. The Villagers all have a hat component, and the SkeletalMeshAsset of that component changes depending on their role. Using the Object Explorer we can explore and right-click for HPath values.
With an HPath reference to the SkeletalMeshAsset we can get the name of the SkeletalMesh using GameDriver’s CallMethod with the object method “GetName”. In this case, I know the food task is associated with the SKM_Gatherer hat so I can add an assert to my NUnit test, automating the validation that change job works and changes hats correctly.
api.CallMethod("/*[@name = 'BP_Villager_C_0']", "Change Job", new object[] { "Food" });
string hatName = api.CallMethod<string>("//*[contains(@name,'BP_Villager_C')][0]/fn:component('Hat')/@SkeletalMeshAsset","GetName");
Assert.IsTrue("SKM_Gatherer".CompareTo(hatName) == 0);
Getting values from your User interface (UMG/Slate)
GameDriver provides editor tools and HPath syntax to help you grab and manipulate UI items easily.
Since Unreal UMG elements are not in-scene objects, they do not appear within Unreal’s Outliner the same way as in-game objects. To get a reference to a particular button or text field we open the Slate Explorer that comes with Gamedriver to find the interface items we’re interested in.
In this case, the resource items are laid out within the UI_Layer_Game_C_0 widget, and the items themselves are laid out as instantiated UIE_Resource_C objects, a user-defined widget. By filtering on the word “Resource” we get a good sense of the hierarchy involved in the screenshot.
Since the actual Text value will reside within the CommonTextBlock nested within each UIE_Resource_C object we right-click one of the CommonTextBlocks and get an Absolute HierarchyPath.
Note: The relative HPath will rely on the number in the name of the instantiated object. In standalone builds that number can change from editor builds so we recommend starting with the Absolute HPath which better accounts for these differences.
/*[contains(@name,'UI_Layer_Game_C')]/Overlay_28/SafeZone_2/Overlay_0/CommonBorder_0/ResourceContainer/*[contains(@name,'UIE_Resource_C')][0]/SizeBox_74/Overlay_0/HorizontalBox_26/Txt_ResourceValue
The Absolute path contains the complete hierarchy of Slate objects. You can use the // notation to get “any descendant” from a particular place, including the root, making far cleaner HPaths. By removing unwanted items we can simplify complex HPaths above as follows:
/*[contains(@name,'UI_Layer_Game_C')]/Overlay_28/SafeZone_2/Overlay_0/CommonBorder_0/ResourceContainer/*[contains(@name,'UIE_Resource_C')][0]/SizeBox_74/Overlay_0/HorizontalBox_26/Txt_ResourceValue
Into simple and intuitive HPaths as below.
//*[contains(@name,'UIE_Resource_C')][0]//Txt_ResourceValue
However, this is a UTextBlock, which means we’ll need to call a method to get its string value.
The Unreal docs show that to get the text value you call GetText(). Since FText return values are cast to strings for the API, you can put it all together as follows.
string food = api.CallMethod<string>("//*[contains(@name,'UIE_Resource_C')][0]//Txt_ResourceValue", "GetText", null);
string wood = api.CallMethod<string>("//*[contains(@name,'UIE_Resource_C')][1]//Txt_ResourceValue", "GetText", null);
string stone = api.CallMethod<string>("//*[contains(@name,'UIE_Resource_C')][2]//Txt_ResourceValue", "GetText", null);
To finish up the GetFood method alluded to earlier, you can get an integer from the string as follows:
food = food.Replace(",", "");//for values > 1,000
int foodCount = Int32.Parse(food);
We can now easily access the string value in the user interface which opens the door to a huge range of validation tests for UI elements.
You can access the villager count using a similar approach.
population = api.CallMethod<string>("//VillagerCounter", "GetText", null);
Note: There is a field in Unreal’s UICommonTextBlock named Text. It sometimes contains the displayed value(if the value is static), but depending on the game you may need to call GetText().
The villageCounter happens to hold the displayed value in the Text field, so you could also access the field using GetObjectField Value as follows.
string population = api.GetObjectFieldValue<string>("//VillagerCounter/@Text");
Setting Resource Values
Often when testing you need to set specific values so you can test certain scenarios. For example, we may want to set the wood, food, and stone values. In Croupout, there’s even a cheat mode, evidence that programmers/testers want to skip ahead sometimes. While we could utilize cheat codes 1, 2, and 3 with the KeyPress command, we will show how the cheat becomes unnecessary and how we can call methods directly.
The cheat (and the villager, if you want to look yourself) calls the “Add Resource” method in the GameMode class. This method takes an enum (we can pass the int directly) and an amount.
To call this method we first determine the HPath of the GameMode object. In this case, it’s //BP_GM_C_0. We then use the CallMethod API to call the “Add Resource“ method and pass an int representing the type and an int representing the amount.
When we look at the enum in blueprints, we see the values “None”, “Food”, “Wood”, and “Stone” in that order, so we can use the corresponding order when defining our enum in our testing app as follows:
enum E_ResourceType
{
None, Food, Wood, Stone
}
Now we can add any amount we want, using the object reference and the enum. To add 100 food we type :
api.CallMethod("//BP_GM_C_0", "Add Resource", new object[] { E_ResourceType.Food, 100 });
By looking at the existing values we can write a function to set the values to whatever you want, by adding the appropriate amount to get the value you desire. Now we can initialize the world however we need to do certain tests.
Dragging a character to a Resource/House
One common use case is to grab a Villager and assign it to a task by dragging it to the nearest resource/house/etc. This could be used to test the functionality of the drag, the assignment of the character to a task, or within a larger test. In addition, there are two ways villagers can be dragged: by a mouse event or by a touch event on mobile platforms.
Simulating a Mouse drag event
The code below has multiple calls to move the mouse to allow for the animated interface to catch up with the moving villager. The bolded line is the actual drag call.
Vector3 foodSpot = api.GetObjectPosition(GetClosestBushPath(),
CoordinateConversion.WorldToScreenPoint);
Vector3 villagerSpot = api.GetObjectPosition("//*[contains(@name,'BP_Villager_C')][0]",
CoordinateConversion.WorldToScreenPoint);
api.MouseMoveToObject("//*[contains(@name,'BP_Villager_C')][0]", 30);
api.Wait(500);
api.MouseDrag(MouseButtons.LEFT, new Vector2(foodSpot.x, foodSpot.y), 60,
new Vector2(villagerSpot.x, villagerSpot.y));
api.Wait(100);
//Validate it worked
LiteGameObject hat = api.GetGameObject("//*[contains(@name,'BP_Villager_C')][0]/fn:component('Hat')/@SkeletalMeshAsset");
Assert.IsTrue("SKM_Gatherer".CompareTo(hat.name) == 0);
Simulating a Touch Drag Event
When writing tests for iOS and Android devices you won’t be able to use Mouse events and instead have to simulate touch events. GameDriver supports touch inputs, and the code to drag is very similar, with the final drag happening through a TouchInput API method call.
//stop walking so I can touch
api.CallMethod("//*[contains(@name,'BP_Villager_C')][0]", "Return To Idle");
Vector3 villagerSpot = api.GetObjectPosition("//*[contains(@name,'BP_Villager_C')][0]",
CoordinateConversion.WorldToScreenPoint);
Vector3 foodSpot = api.GetObjectPosition(GetClosestBushPath(),
CoordinateConversion.WorldToScreenPoint);
api.TouchInput(new Vector2(villagerSpot.x, villagerSpot.y), new Vector2(foodSpot.x, foodSpot.y), 0, 1, 30);
Clicking buttons (Building new resources)
To build new resources you have to have the right amount of resources, and then navigate a menu to click the item you desire.
The sequence in particular is to
-
Click the “Build” button, and wait for the Build menu to appear.
-
Click a resource (that we have the resources for from among the 8 buttons presented)
-
Move the mouse so the item is not colliding with another item on the map.
-
Click “Proceed”
-
Determine if the new item was added (or was colliding and failed)
-
If successful, click “Cancel” to close the menu
-
Click “back” on the former “build” button to close the build UI.
It’s easy to get the HPath for the following buttons from our Slate Explorer:
-
//CUI_Button_55 is the “Build” button
-
//BTN_Pos is the “Proceed” button (for confirming a build)
-
//BTN_Neg is the “Cancel” button (for closing the confirmation menu)
To start building, we can call “ClickObject” using the HPath for the Build button
api.ClickObject(MouseButtons.LEFT, "//CUI_Button_55",10);
This will bring up the 8 button choices.
These buttons all contain names of the form CUI_BuildItem_C**** where the trailing numbers are generated at run time. To select these buttons we use the contains() function in the HPath query with GetObjectList as follows:
List<LiteGameObject> buildButtons = api.GetObjectList("//*[contains(@name,'CUI_BuildItem_C')]", true);
Now we can loop through all the buttons and for each one make inquiries about values within it. In particular, we’re interested in whether the button is activated, although you could also access other information if you wanted to make your selection based on other things. For instance, to grab whether a button is enabled we call the IsInteractionEnabled method from Unreal
bool canIBuild = api.CallMethod<bool>(hierarchyPath, "IsInteractionEnabled", null);
string displayName = api.CallMethod<string>(hierarchyPath + "//Txt_Title", "GetText", null);
If the button is clickable that means we have resources and can build. So we click the “Proceed” button to confirm our build.
api.ClickObject(MouseButtons.LEFT, "//BTN_Pos",100);
After that, it’s a matter of closing the Interfaces down by clicking on buttons (identified through the Slate Explorer) and checking that the new object has been spawned. This latter problem can be solved by checking the count of a certain class of objects before and after, for instance.
Conclusion
In this tutorial, we explored how to select AActors and slate Widgets using HPath and how to get and set the properties of those objects. We demonstrated the use of the Slate and Object Explorer tools in the Editor, and we showed how to perform a simulated click, simulated drag, and simulated touch (for mobile). We showed how to call methods on objects and how to pass parameters from your test to those game methods.
The full sample code discussed can be found here. We hope these Cropout examples give you some guidance you can apply to testing your own games and apps.