[00:00:00] The 3D Game Kit, similar to the 2D Game Kit, actually follows the same character, but in a different world, if you will. And so you can again, like 2D Game Kit, download the 3D Game Kit from the Unity Asset Store and that's available here. You add this to your profile and then you can add it to the project simply by going into the package manager and under My Assets, adding the 3D Game Kit from there. That will completely rewrite any project that you add it to with its own input settings, etc. once you have that installed into a project. You can add GameDriver like we did in the 2D Game Kit example I've already done so here, so we don't need to do that again. And I've already added the empty object with the GDIO Agent script here. I've opened all my tools and let's take a quick look at this game in action when I hit play. You'll see it looks very similar to the 2D Game Kit, same character, same sort of environment, but 3D instead of 2D. Now, the purpose of these projects is to provide an easy way to learn the different ways of developing on these platforms. So if you're interested, there are tutorials available from Unity in the development of these games. Once this game loads, you can see that I'm in a very sort of different environment, although, it looks similar in a lot of ways. In this case, I'm moving around in three dimensions. I can see different activities here. I can jump across ravines, get my weapon and perform inputs like breaking boxes, killing enemies, etc. It's a little more dynamic of an environment than the 2D version, but we can learn a lot about how to test in this environment from here. I'm going to stop my test here. And then I want to show you the recording agent.
[00:02:04] So the recorder will allow us to capture our inputs during replay so that I know what my inputs are. I can see what objects that I'm interacting with and where I clicked on the scene. And I can also add these sorts of checkpoints or insert functions into my test so that I can have a baseline for which to start with. So let's hit record here and this is going to put the editor into play mode. And you can see the agent is connected and there's a little bit of information that's already being populated into the recorder here. Let's expand that a little bit so we can see what happens there. So first off, I want to click on the start button. So similar to the previous game where I'm searching for the start button, I would insert the click object here and you can see in this viewport in the editor of the api.ClickObject and it shows the MouseButton.LEFT the start button, and then it was just a 10-frame click, very simple. Now when I actually click on the start button here, you're going to see the corresponding X-Y coordinates for that click as well. And that's intentional because these things might be different. We use the insert function of highlighting the object in the scene and hitting the click object in order to get this line of code. But then I perform the click using my X-Y coordinates here so that you can see the exact coordinates that we clicked on. And that might come in handy when we're comparing things later. We then can move around the scene and you can see the key processes that are occurring there, the key code plus the amount of time that is being spent on each of those things. If I dragged the mouse, you can see the mouse or click action that's taking place there and then I can jump over my space and go get this weapon. As I mentioned initially during this course, the recording is meant to provide as a starting point an example of what was done. And then we can take the same sort of concepts that we used in the 2D Game Kit, where we isolate features such as clicking on the object and then checking that the desired result occurred. And we can wrap those around those steps in order to make the test more reusable, much more resilient to change. But this gives us a nice starting place so we don't have to code from scratch. I hit stopping the recording there and then we're going to switch over to the actual tests and you can see what some of this looks like. You can see that I performed a bunch of different actions here, things like pressing space to jump escape, to get to the menu, stuff like that. And this is the type of information that would inform my test definitions, but I'm going to remove that for now.
[00:04:54] In this test, again, based on our sample that's available on our website, you have the same sort of startup examples where we're deciding whether we're running in the IDE or standalone on a local host or in another machine and what the path to that executable is if indeed there is one. And then we're going to instantiate the API client again. In this case, we're just doing a single-player test, so we just have one. And then we've got the logic below for where do we run this test. And this is all part of the one-time setup. One time setup, as it sounds is executed once. There is also a setup tag within NUnit that allows me to perform a setup before each of the tests. Let's say I have five tests. I think in this case we have seven that we have seven tests within this. But let's say there's a setup portion between each of these tests that we want to reset the data or load some value or load some saved file or something like that. You can put that in the setup tag as a method here, and I won't set this permanently, but set-up is also a valid tag. But unlike the one-time setup that's going to happen for each one of the tests, one-time setup happens once at the beginning of the run. In this case, we're checking the executable path, if it's not standalone or not null and equal standalone, we're using the path. And then we're going to connect to the editor or to the agent running on standalone as we normally would enable our keyboard mouse hooks. And then we're going to do some logging here. Now, one thing that I didn't mention in the previous one is that we can actually capture on handled exceptions from the Unity editor, from the Unreal editor, and from the engine itself during replay, and those will be passed back through the agent to the client so that we can correlate between what was I doing in my test and what happened in the engine during replay? And so that's a useful addition to your tests here as well. And you can find, like all of our other commands, more information on the unity log message on our API docs.
[00:06:59] Now in this case, we want to start the scene. We want to start ourselves and do a scene one. So again, we're going to check if the SceneName equals start. We're in the menu at the initial scene there. We're going to wait for the start button to appear and then we're going to click on it. Very similar to the previous one. And then we're going to wait for Ellen to appear in our scene. Now, before we even start anything, we're checking our Level1. If we are Level1, great, if not, fail the test. If this didn't happen, if Level1 didn't load, something's broken. And we don't want to waste any of our time sending this to testers to do their testing and they load it up on their consoles or their mobile devices or what have you, just to find that it won't even load past the initial scene.
[00:07:46] Next is we didn't talk about in the previous example with the 2D Game Kit is we're doing a one-time teardown as well. Just like set up there's a teardown and a one-time setup is a one-time teardown. And the one-time teardown is what happens when the test is done. So we're waiting a little bit of time here for things to catch up. We're disabling the mouse hooks in case we want to continue testing manually afterward, and then we're disconnecting from the agent. And then if TestMode equals IDE, we're stopping the editor play. Otherwise, if the test were standalone, we're actually terminating the game. And this is just like a cleanup step. We can perform any sort of cleanup in here that we want. Reset the game status, reset the character, and reset whatever we want as part of each of these steps to maybe set us up for the next run. This is just another one of those best practice things. And then this test taking our lessons learned from the 2D Game Kit, we're going to do some very isolated feature testing here. We're going to test movement inputs, for example. And in the first test we're going to say is the SceneName Level 1 otherwise fail. And then we're going to wait for Ellen to appear. And then we're going to get Ellen's position. The initial starting position which we're also going to log to the console. And this is useful for a number of reasons, but if we're doing some troubleshooting, we want to understand if this test failed, where did it fail? And we can go through the output and say, okay, the original position was with 0.0 or whatever it is, and the final position was 0.0. The inputs didn't take, Ellen didn't move. And so something's wrong with the input handler, for example. And then you remember I used to GetLastFrames per second in the previous tests that we were showing. In this case, I'm just going to get the value once. I want to know what's the base frames per second, and then I'm going to use that throughout.
[00:09:45] Now, there are any number of ways to skin a cat. We're probably not the best analogy, but to solve the problem where we don't want the inputs to be static from one machine to the next. In this case, we're going to use frames per second. But we're not so concerned in this case that there frame perfect inputs, right? This isn't a speed-running tool. And we would use very specific number of frames for each of these inputs. In this case, I'm just testing are the inputs working? I'm going to do my horizontal AxisPress using the 1f axis. We're saying positive input on the horizontal axis and then positive input on the vertical axis. And this is all defined, by the way, by the input manager on the project itself. And you may have noticed in the recorder we were capturing keystrokes, but those keystrokes are also mapped to these accesses. We're using the axis in this case because that's what the input manager is expecting. And then we're providing a negative horizontal input followed by a negative vertical input and then we're checking the final position. We have Ellen's position before the movement and Ellen's position after the movement, and then we compare the two to see that they're not equal. We're asserting that the test, before position, and after position has changed. Otherwise, something's wrong with inputs. Now, next, we do something similar with the camera. In this case, you may have seen that when I move the mouse around, the camera moves. So in 3D space, we need to make sure that the camera is working. In this case, it's called the camera brain. And so we're testing very similarly that those inputs affect the position of the camera brain. We're checking the object position before, we're performing some AxisPresses on camera X and camera Y, and then we're checking the position after. These are very basic tests, but these are critical for us to make sure that all of the core functionality is working.
[00:11:37] Lastly, before we get into actual gameplay, we're checking that when I hit the escape key that the menu appears. Now, I could get a little more adventurous here, and I probably would in this case to check that when I hit the pause button, does the menu appear correctly? Are the texts correct? Maybe there's a language translation to check there. If there are different options available in different scenes, do I want to make sure that those are appearing appropriately in the different scenes that I load up the pause menu? Or I could go deeper into the menu and say, Pause, check options, set resolution, and then check the screen balance to make sure that that took effect. Next, we get into actual gameplay.
[00:12:21] The next few tests we're making sure that when I perform certain activities that I'm getting the actual desired outcome from the gameplay perspective. So there's a little bit more tricky behavior going on here. Now, one of the things you may have noticed when I was navigating through the scene is that I had to jump across a ravine and then get the weapon. Now, I've already tested movement, camera movement, etc. so I'm not going to risk jumping off the ledge and doing it manually, although I could. In this case, I'm just going to go get the weapon directly. First, we're going to check is the weapon active in the scene, the weapon load? Right. And so we check for the staff and to see that there are actually a couple of stats here, but this is the second one. Is it set to active? And if it's true, we're going to set Ellen's position. Set ObjectFieldValue of Ellen using her UnityEngine.Transform component, which is where the position is stored. And then that's the position value. And then the next argument is what we're setting it to, and we're just getting the position of the staff directly.
[00:13:34] In one line of code, we're saying move this character to the position of that object, right? And then where the last argument here is the coordinate conversion, which is not anywhere in the 3D space. So we're just whatever the raw position value is, that's what we're moving to. And then we're going to check. Using this assertion. GetObjectFieldValue, which is a boolean. True or false? For Ellen, Game Kit 3D player controller can attack. Now this is what happens based on some experimentation. This is what happens when I get the stuff, the can attack field becomes true. Now, if this failed, this means that when I moved Ellen to the staff, either that didn't work or when I did move on to the staff, it did not trigger the can attack property to be enabled. And this is going to be controlled by a method attached to the player controller, which we'll see in a second. This whole test is just to make sure getting the weapon enables the attacks. Right. Long winded explanation, but about 1 to 3 or 4 lines of actual code. Very simple. And then the next test is actually going to depend on the previous one. But in order to avoid that, we're going to take a different approach. In the next test, I'm actually going to go out into the world and defeat all of the enemies. They're called Chompers, and there's a bunch of them. But before I can do that, I need to have the weapon. I'm going to again check whether or not the can attack field is set to true. But if it's set to false, in this case, we're checking if good GetObjectFieldValue for the Ellen character 3D or Game Kit 3D player controller. If the can attack field is set to false, we're actually going to call the method directly. And the reason why we're doing that is I don't want to repeat the last test. Let's assume that the last test failed, when I moved the character to the staff, it did not enable attacks. But there is a method attached to the player character called set can attack that I will set to true. Now, if this fails, this test will fail. And that's fine. That's wanted behavior. But we want to make sure that what we're doing here is actually affecting the gameplay the way that it should without depending on that gameplay. And so the next step is a little more complicated. Now, in this next section, we're actually going to move around the scene, as I mentioned, and find all of the chompers and then hit them. We're going to destroy them. And to do that, we need to first check that the chompers exist, and then we're going to do something a little more complicated. You'll see some methods here, like close to object. Now, this is not a GameDriver command. This is what we call the helper function. Control-click on this and you can see that this is a command or a method that I created as a little bit of a helper to use throughout my tests. So CloseToObject is going to take the path of an object in the Hierarchy, right? Using the Hierarchy Path. And then we're going to set the initial position to that of the object in question, and then we're going to return not that exact position, but something close to it. Let's take a little bit of trial and error, basically saying that we want to be one on the X position away from the target object and one on the Z position. So it's X and Z, Y going this way and then we're going to return this new value. This saves me from having to write these three lines every time that I want to go through and go near an object such as the Chomper or one of those destructible boxes that we saw. And then you can see there's another helper function here above, which is to set object position and that of the Hierarchy Path of the object. We're just going to pass that along with the position. And it's going to set the path of the object's unity engine transform component to that of the position that you send it. Again, very simple but very useful in that we're actually able to cut down the amount of code that we're writing by use or creating these helper functions. With the chompers, we've got a bunch of chompers. I don't know how many in this case. We're just going to find the next chomper in the scene. And when I have a bunch if there's zero through 26 or whatever the number is, this search for just slash, slash @name equals chomper is going to return the first one each and every time. And then we're going to use the CloseToObject to that Chomper. And then we're going to get the position of the Chomper, turn Ellen towards that position. You see the set object position, Ellen? That's the other helper function that I mentioned to that of the destination, which is close to the Chomper. And then we're going to do something really cool here. We're going to call it internal method that this is used by the Unity engine itself. And this is on the Unity engine transform again, same place where we saw a position rotation, etc. And there's a method called LookAt which accepts a Vector3 value. And LookAt is actually going to turn the Ellen character pointing towards that object that we're passing it or that value the Vector3. And the benefit here is that if we were to perform this logic ourselves, what we need to do is determine the position and rotation of both objects so that we could set our character position to that, to be looking at the one that we're trying to get to, which is complicated. And so we make use of Unity's internal behavior in order to be able to do that. And we can do it in a single line of code. And then after we've done all that, we've moved to Ellen, close to the object, we're getting the position of the object and setting Ellen towards that. And then we're pressing the fire button and then we move on to the next one. We do this over and over again to make sure that there are no more Chompers. We do this until assert is false, that wait for object, which normally returns true when we find the object, we're actually waiting for it to return false to say there are no more Chompers.
[00:20:14] That's quite a long explanation, but not a very complicated test. Maybe took less time to write initially then than it did for me to explain it here. Once I understood what was happening in the scene. Let's go back to the game for a second and let's look at those Chompers in a little more detail before I run this. I'll start my scene, my running game. And then we'll go to the first scene here. And I'm just going to pause for a second. We're going to look at the enemies in the scene. So I know from poking around that they're all stored under the enemy's container here. This is an empty object. It just contains for the developer's sake, all of the Chompers. Very clean.
[00:21:04] Now, if I just search for @Name equals Chompers, it will get me the first one in the list and it will just go down the list until they're all gone. If I wanted, for example, this empty Chomper in the list, I could right-click and say, Give me the hierarchy path of this Chomper, which I can see logged down below, it's the instance predicate of 11, which means it's the 12th chopper in the list. And if I run that, it shows me highlights the exact one. If I delete 11 and run that, I'll see that there are 26 Chompers that are returned. And so you can see how the Hierarchy Path is used to identify unique objects in a scene. We can go to the Hierarchy Path Debugger, and we're in the same thing here. Run, it'll show me that there's a bunch of game objects. These are Chompers UnityEngine.GameObjects that are returned when I look for that @Name equals. If I go to the game object Explorer for any one of these, let's say I'm looking for you know again this let's say the 11th one that I initially had 12 sorry and I want to get the specific transform and properties of this Chomper. And so I would navigate to the UnityEngine.Transform using the GameDriver Object Explorer, Game Object Explorer. And I can look for things like child count. How many children does it have? What are the eulerAngles of the rotation of that object which way is forward? What is the local position? What is the local rotation? The parent object of this? The position. This is all useful information. A lot of times we're really only looking for something basic like position, rotation, etc. because these are the things that are going to inform our gameplay. And so the position is something that I can just take directly from this. I can copy out the Hierarchy Path for this and let's put this into the hierarchy path debugger and you can see Chomper with instance, Predicate 11 has a function component of Unity engine transform and the @position field, which is what we're using in our test. I can step into this and see what's returned at each step along the way. As I run this, or I can just hit run. And it shows me the precise Vector3 value of that object's position. So you can see the usefulness of the tools as we're going through this. Almost all of these properties and values will return something valid. Be aware that there are some that are protected and unavailable and might return an error message, but it's usually something that you don't have to worry about. Generally speaking, you can traverse the objects and properties, and methods without concern. Now there are methods attached to components and methods attached to base objects. There's a lot of information there. You can explore this and get a sense of how things are implemented before asking the developer, how do I interact with this object in this scene? Now we're ready to run our test. We're going to press play in the test class, and we're going to take hands off keyboards to see how things run it. That's going to put the editor into play mode and it's going to start running through each of those tests. We click on the start menu there. That's the one-time setup. There are going to be movements where we're testing user inputs. You can see Ellen moving around the scene a bit. Followed by some camera inputs. Looks like we went a little bit too far. That's okay. Followed by opening up the menu. And then we're going to see the gameplay. We're going to move directly to the staff and start moving around the scene, finding each of those chompers, doing the LookAt and then attacking those chompers.
[00:25:07] Now this is live gameplay, so the chompers are actually trying to attack me as well. If we wanted to isolate the behavior of the Chompers from our ability to destroy them, we would have to insert another command into the test to disable the chompers somehow, to say, disable attacks or whatever the method is that they're using to identify me. And you can see them running halfway across the scene in order to get me and set that the false so that we can isolate the behavior of the destruction of the chomper from their AI and Pathing. But that's not the case here. And also see that they're using the terrain in order to move around, which is not something that we did. But we could use the Navmesh agent here to navigate from one part of the scene to the next. Now, the chompers have enough mesh agent, whereas, the player character does not. And so we would need to modify our gameplay in order to facilitate that, which is something that we generally try and avoid. But let's say that we wanted to, we could tell one of the Chompers to move from one place on the scene to the other, and it would use that Navmesh in order to get there. In this case, we're just concerned about, can I destroy the Chompers and do they destroy when I do so? And one thing worth mentioning here is that there is a bug in this project besides being terribly optimized for execution. The death source from each of the chompers actually increases over time. This is not a game-breaking bug by any means, but something that we noticed in execution is that a death source audio source appears when a chomper dies, but it never gets destroyed. This is something we could check for. Either by searching for them explicitly and counting up the number of death sources as they compared to the number of Chompers. Or we could pull a full list of all of the objects in the scene and count the number of death sources that appear, which should be zero at the end of this test. But that wasn't the focus of the task. We really just wanted to see, can we move to the Chomper and destroy it, and does it get destroyed? I can verify that by expanding the enemies container here and seeing that we went from 26 Chompers down to just a few and that number gradually continues to drop. Now, there is another test that runs after this where we break boxes in the scene of the boxes, unlike the chompers, do not destroy themselves. We took a different approach with that, where we do the full list of objects in the scene, count how many of them are named destructible box, and then iterate over that list until all the boxes have been destroyed. But the actual destructible box containers under the destructible here is static. It doesn't decrease as we perform those actions. What they do is they change from active to inactive after we destroy them. We can see the enemies test or the Chomper test is finished. It's going to move on to the destructible boxes here. But we've already seen Ellen jump around the scene a bunch and destroy a bunch of boxes. Instead of watching that directly, we're going to move on. What have we learned from the 3D Game Kit example? We're going to make use of built-in methods here for working in 3D space because 3D can be kind of tricky things like the transform that look at where we're going to turn the character towards another object in the scene in order to perform some action. We could make the use of Navmesh agents to navigate around a more complex environment with terrain and obstacles. But in this case, the own character doesn't have one. So without modifying the game, we would be limited to using that for the chompers alone. We need to understand the positioning and how it affects gameplay loop. In this case, the distance and timing from where we would teleport to a Chomper, turn in its direction, and then press the attack button, we needed to know how close and what's the relative timing in order to be able to achieve that without getting killed too many times. And as you saw, it was still kind of a challenge.
[00:29:36] And then lastly, to make the use of helper methods. Now, the number of helper methods in a given project is going to grow almost exponentially over time. You'll see that as you continue to write code over and over again to satisfy your testing needs, it just makes sense to move some of that into a helper function and then call it the next natural sort of progression that is to say, I don't want to set object position, I just want to say, move left or move right or whatever, and then become very kind of natural language in how we form our tests. And that's an abstraction layer that we'll talk about later in the course.