• Unity Immediate and the Art of Automating Playtests

General and Gameplay Programming

If you love developing hard games but aren't a good player yourself, you have a challenge ahead.

Unity Immediate Window: Sample Project

Last week, while working on a game, I noticed that I could not beat my own levels anymore. I wanted to reach the final boss, so to speak, and that single thingy took me longer than I'd be happy to admit. It might be the age, or that the game is simply too hard.

So I started looking for different ways to save time. As always, cheating was an option. I also thought of making a special, shorter version of my gameplay to avoid playing through the same hard spots every single time. After all, I just wanted to test the final boss!

As you know, there are many ways of approaching this. But, in my case, I came up with an alternative approach that ended up being surprisingly useful. This strategy helped me shortening the testing times of my games plus cutting the design iteration times.

If you are a programmer or designer, this article will give you insight into a second approach for handling these challenges. If you have any other role, well, share this article with the right person.

To present the use case, I looked for an open-source game on GitHub. And so I found a cute little game called Popcorn that I borrowed from TheAislan (full credits to him!). It's a basic 2d platform game that features simple 2d physics mechanics, perfect for our use case. Check the screenshot above, isn't it adorable?

I'll start off by telling you how I used to deal with testing...

Level 1 Developer: The (Infamous) Grind

Level 1 Unity Developer: The Grind

A decade ago, grinding was my preferred way of working (oh dear). All the testing  I did was manual, and it was all fun.

Then, weeks passed and the game got more and more complex. I started growing frustrated, since now playing every time took me almost 20 minutes. Tweaking the final boss scene transformed from being a pleasant moment to sucking all my mana and burning the entire QA's yearly budget.

Something had to be done, and so I searched for solutions.

One workaround was to split each level in multiple, even finer sub-levels. That way, we could just load the specific sub-level we want to test. This solution might work well for games whose level areas are highly independent of each other, such as platform games. This has some overhead on the programmer when it comes to scene management, but it's quite alright.

Another possibility is to add custom in-game panels or UI inspectors to enabling cheating and special actions. Those could allow the developer to skip certain parts, increase stats or just disable annoying ads. These work just well, but some time has to be spent on preparing and maintaining the front-end.

And here comes the classic: a configuration file to be loaded in run-time. That sneaky file can be used to choose which features or cheats to enable. That may look like below:

{
"GodMode": true,
"AllLevelsUnlocked": true,
"Gold": 500,
"UnlimitedTime": true
}

You can load such a configuration file through a C# script like below:

[Serializable]
public class Cheats
{
public bool GodMode;
public bool AllLevelsUnlocked;
public int Gold;
public bool UnlimitedTime;
}
player.Gold = cheats.Gold;

Of course, make sure not to enable that feature in release versions

These options have one common problem: we have to pre-define what is possible in advance. We get what we programmed; we don't get what we didn't.

That can quickly become a time sink:

During testing, we decide we need X. Therefore, we stop playing and wait for a programmer to implement X.
We then replay till that point and perform X, only to realize we actually wanted to do Y.

UPSET

In some situations, we could clearly profit from having more flexibility.

So I asked myself: can we do better?

Level 2 Unity Developer: the Unity Immediate Window Package

You and I want to shorten our iteration times. We want to avoid pressing the stop button as often as we can.

What if we could invoke whichever behavior we wished for during run-time? How less frustrated would you be if you didn't have to start a new play iteration so often?

We want to stop depending on pre-defined behavior. The bribery cookies we offer to our colleague the programmer are getting way too expensive.

It's our lucky day. It turns out that Unity released a package called Immediate Window. That sneaky beast of a package gives you just that: it allows you to run your DIY C# code in run-time.

This might sound a bit too abstract. The benefits might be hidden. But trust me on this:

Unity Immediate Window will cut your iteration times SIGNIFICANTLY (if used correctly ™)

Remember that I wanted some kind of fast-forward feature to reach the final boss faster? That's exactly what we're gonna do just now using Unity Immediate Window.

What we are going to do is to implement Automated Playtests. In other words: in this blog post we are making the machine play for us. No more human mistakes made. And not only that, we wanted to save time, so we will do that at 2x the speed!

Here comes a very quick section on how to install this Unity Immediate Window Package. If you're a pro, skip it.

A. Unity Immediate Window Installation

The first step is to add the Unity Immediate Window package from Window ► Package Manager. There, just search for Immediate Window and install it.

I take you are using some sort of Version Control, so remember to keep track of the changes made in the Packages/manifest.json file.

Level 2 Developer: Unity Immediate Window Installation

Once installed, you can easily open your new Unity Immediate Window panel by navigating to Window ► Analysis ► Immediate Window.

That's all to it, really.

How do you feel about a quick guided tour on this new toy? Let's get started.

B. Unity Immediate Window Overview

Level 2 Developer: Unity Immediate Window Overview

In the above capture, you see the main and only existing window in Unity Immediate.

The pink-highlighted area is the input command window. This is your bread-and-butter section where you and I enter commands just like we would in a C# script.  Only that we will do it in run-time (while playing in the editor). Note that the C# syntax has been slightly simplified; for instance, typing C.Player in this project will let you see the contents of that variable in the output log. No need for Log, no need for a semicolon.

In the yellow section, we see what we call the object inspector section. Here, you'll be able to inspect the contents of in-game objects and variables. You may expand the JSON-like view by clicking the arrow located on the left, as seen below.

Level 2 Developer: Unity Immediate Window - Object Inspection

Finally, the blue highlighted area lists the registered assemblies. You might find it helpful to explore the available types, especially their static properties. An example is shown below, where we inspect the global gravity property.

Level 2 Developer: Unity Immediate Window - Assembly Inspector

C. Unity Immediate Window: The Art of Making Cheats

Now that I gave you a brief theoretical grasp on the Unity Immediate Window, let's apply it to our case. This is based on the Unity Immediate Level 2 GitHub repository (see below) with Unity 2019.2.X.

Or alternatively, visit the GitHub Repository

Coming back to our cutie, we wanted to be able to fast-forward to the final boss without dying in the process. So...

If you cannot beat the house, do some sneaky cheating

Here's my first suggestion for you: create a static class that wraps the functionality you want to invoke. That way, all developers have centralized access to all functionality and you don't have to memorize as much. Let me announce our lucky winner! It is...

GamedevGuruHelpers.cs!!!!

public static class C
{
public static Player Player
{
get  { return Player.Instance;}
}

public static string GodMode
{
get  { return string.Format($"Godmode: {Player.Instance.godMode = !Player.Instance.godMode}");} } public static float TimeScale { get { return Time.timeScale; } set { Time.timeScale = value; } } public static float Gravity { get { return Player.Instance.rb.gravityScale; } set { Player.Instance.rb.gravityScale = value; } } public static void Win() { Player.Instance.Win(); } } Is it redundant? Yes. Is it helpful? Yes. If you don't like it, don't keep it. Line 1 declares a static class with a mysterious name of C. This can be thought of Cheat, CVar (Console Variable), Cockatrice or whatever meaning you'd like to give it. A single letter keeps it easy for people to remember it. Lines 3-6 define a static shortcut property to allow quick inspection and acting on the Player class. As you saw in the previous images, we only had to type C.Player to inspect its contents. Lines 8-11 offer an interesting way of toggling the God Mode cheat on or off. Instead of typing C.GodMode = true, it'll be enough to type C.GodMode so switch between modes. This syntax is less verbose for the developers using the Unity Immediate Window. Lines 13-17 allows you to change the Time.timeScale of your game. This will become handy if you are doing automated tests or want to increase the speed of your iterations, as you will see later. The same is done for the global Gravity property. Lines 25-28 will give you the option to type C.Win() in the Unity Immediate Window to trigger an instant win in the scenario. Play the game. Open the Unity Immediate Window. Type any of the commands previously mentioned. Now you can beat any game Cheating alone will save you only so much time. You still have to play manually. And THAT SUCKS. Can we do better? Aw yes! D. Unity Immediate Window: The Art of Automating Playthroughs I initially confessed you the difficulty we had in repeatedly testing scenarios from start to finish. It was prone to errors, to game overs and we were spending way too much time. And yet, I have seen this so often in so many projects. Having a QA department is not an excuse for making their lives harder! I live in Amsterdam and I definitely tell you I've seen increased weed consumption as game levels increased in complexity. Avoid frustration from everyone and as often as you can The idea here is simple: we want the machine to play for us. The freed time can now be spent profiling (while playing), debugging or eating the bribery cookies artists and designers gave us. In this project, we have a limited amount of input: arrow keys and jump key. This will make things easier for us, programmers. We'll start by extending our well-named class C to add basic gameplay functionality to it: 𝓬𝓸𝓹𝔂 𝓪𝓷𝓭 𝓹𝓪𝓼𝓽𝓮 baby! public static class C { // ... public static Coroutine Execute(IEnumerator function) { return Player.Instance.StartCoroutine(function); } public static Coroutine Move(float direction, float duration) { return Player.Instance.StartCoroutine(Move_Internal(direction, duration)); } public static Coroutine Jump(float direction, float force = 0) { if (Mathf.Approximately(force, 0f)) force = Player.Instance.jumpForce; return Player.Instance.StartCoroutine(Jump_Internal(direction, force)); } public static IEnumerator Move_Internal(float direction, float duration) { Debug.Log($"[C] Dashing in direction {direction} for {duration} seconds");
var startTime = Time.time;
yield return new WaitUntil(() =>
{
Player.Instance.ExecuteMove(direction);
return Time.time - startTime > duration;
});
Debug.Log($"[C] Dash finished"); } private static IEnumerator Jump_Internal(float direction, float force) { var groundLayer = LayerMask.GetMask("Ground"); var player = Player.Instance; Debug.Log($"[C] Jumping with force {force}");
Player.Instance.ExecuteJump(force);
var time0 = Time.time;
yield return new WaitUntil(() =>
{
player.ExecuteMove(direction);

float raycastDistance = 0.8f;
var hit = Physics2D.Raycast(player.rb.position, Vector2.down, raycastDistance, groundLayer);
var onPlatform = (hit.collider != null && hit.collider.CompareTag("Platform"));
return (Time.time - time0 > 0.2f) && onPlatform;
});

• 0
• 0
• 4
• 3

• 16
• 11
• 23
• 42
• 75
×