Jump to content
  • Advertisement
  • 09/24/19 07:38 PM
    Sign in to follow this  

    Unity Immediate and the Art of Automating Playtests

    General and Gameplay Programming

    Ruben Torres

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

    Unity Immediate Window: Sample Project

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

     

    Unity Immediate Window Level 1: Grind

    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,
      "SkipAds": true,
      "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;
    }
    var cheats = JsonUtility.FromJson(File.ReadAllText(Path.Combine(Application.persistentDataPath, "cheats.txt")));
    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.

    Smiley - Furious

    UPSET

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

    So I asked myself: can we do better?

    Link to the summary

     
    Unity Immediate Window Thumbnail

    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

    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

    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

    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

    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.

    Download the project in ZIP format directly from GitHub

    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!

    Link to the summary

     

    Unity Immediate Window: Sample Project

     

    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;
            });
            Debug.Log($"[C] Jump finished");
        }
    }

     

    Disclaimer: for this article, I probably care less than you do about how pretty the code looks like. The point is to get my message across.

    Lines 4-7 adds the possibility of invoking C.Execute() from the Unity Immediate Window, passing it a coroutine containing a sequence of code for asynchronous execution. Neat!
    Lines 9-12 and lines 20-30 gives us the chance to move the character to the left (-1) or right (1) for a specific amount of seconds.  We can wait on this move to finish before proceeding further in our script.
    Lines 14-18 and lines 32-50 do the actual jumping. Similarly to moving, this accepts a weighted direction (negative means left, positive means right). Duration is implicit, as this function will return only when the floor is reached, which is cool. A slight delay of 0.2s was added to prevent the raycasting from activating too soon.

    Let's put all of this together into something useful. We are going to create our first automated playthrough script for the Unity Immediate Window. Finally, we will be able to keep our hands free for whatever we want while this test runs!

    Switch to multi-line input (one of the top-right buttons) and hide the bulky assembly window.

    Are you ready?

    Give this a brief read, and then copy it in the input section of the Unity Immediate Window:

    IEnumerator SkipToFinalJump()
    {
        // Fast-forward till the final jump section
        Time.timeScale = 2f;
        yield return new WaitForSeconds(3);
    
        yield return C.Move(1, 1);
        yield return C.Jump(1);         // Onto first platform
        yield return C.Jump(0.5F);      // Onto second platform
        yield return C.Jump(0.7F);      // Onto third platform
        yield return C.Move(1, 1);
        yield return C.Jump(1);         // Over flame
        yield return C.Move(1, 1.75f);
        yield return C.Move(-1, 0.75f); // Descend to second platform.
        yield return C.Move(1, 0.65f);  // Descend to ground level.
        yield return C.Jump(1);         // First sugar block
    
        yield return C.Move(1, 0.5f);
        yield return C.Jump(1.0f);      // Stomp over enemy head
    
        yield return C.Jump(0.8f);      // Stomp over second enemy head!
    
        yield return C.Jump(0.8f);      // Second batch of stacked sugar cubes
    
        yield return C.Move(1, 1.0f);   // Approach edge of next platform area
    
        // Testing the final jump section
        Time.timeScale = 1f;
    
        C.Gravity = 0.3f;               // Let's cheat a bit
        yield return C.Jump(0.3f);      // Super high jump to a new platform
        C.Gravity = 1.5f;               // Nothing happened here
    
        yield return C.Move(1, 2.5f);   // Walk into...
    
    
    //    ScenesManager.Instance.CallNextScene();   // Wanna go back?
    }
    
    
    C.Execute(SkipToFinalJump());

     

    What you and I are doing here is to create an asynchronous coroutine that will perform a series of gameplay steps. Moving and jumping are our foundation building blocks for this game, so we want to time them well, one after another.

    Since your and my time are valuable, we will increase the time scale during the initial stage of the game and slow it down back to a normal pace when we reach the middle stage we are working on. It could be the final boss.

    No more waiting. Start the game, type in the playthrough script and press the Unity Immediate's execute button!

    Level 2 Developer: Unity Immediate Window - Playthrough Automation

    Level 2 Developer: Unity Immediate Window - Playthrough Automation

    I love it.

    Now that you created a functional playthrough script, make it permanent by converting it to a plain C# file. A meaningful name could be PlayMission1.cs.

    The point of using Immediate was to prototype our test as we played, avoiding, therefore, our stop-compile-play iteration times.

    Now that we have a playthrough test in place...

    Link to Summary

     
    Level 3 Unity Developer (Unity Immediate): Sample Output Table

     Level 3 Unity Developer (Unity Immediate): Sample Output Table

    Level 3 Unity Developer: Extended Playthroughs

    One of my all-time favorites with Unity Immediate is just this: to quickly prototype your playthrough scripts. Then, you make those permanent and enjoy the results over and over again.

    However, by reading on this, you'll probably have realized the possibilities that Unity Immediate offers you.

    Can you guess what is the next level after automated playtests?

    Since we already have a playthrough script in place, wouldn't it be GREAT to extend it so we record useful data useful for further analysis?

    What kind of data?

    I'll start with performance metrics. But that's not the end, nor the middle.

    For my game, I want to record gameplay metrics too! Gathering stats such as remaining health, energy level, remaining monsters, ship status and so on at all points during our game is EXTREMELY useful.

    How happy will you make your designers by giving them access to these stats automatically?

    Imagine how easy is now to tweak difficulty levels, for instance.

    Even better, this is not an exclusive feature of automated playtests. The point is to gather this data in the background from real human players! And then, you bring this information to the front-end of your designers.

    Answer me this question: how many negative reviews will you avoid if you had access to accurate data at every point during your playtests?

     

    Popcorn-Smiley.png  Level 3 Top Game Metrics

    Time
    • What's the time since we started this specific play session?
    • Record the variability of the different play styles from your target players.
     
    Markers
    • What is your player doing right now? Are they in level 3 of our dungeon? Are we in a shop, or fighting a disgruntled boss?
    • By analyzing how long it took our friend, the playtester Jamie, to reach the final boss, we can estimate whether our playing sessions are too long/short for the general public.
    • Casual games should have relatively short play cycles, for instance. You'll want to make sure of this when analyzing your data to get your well-deserved money.

    Gameplay variables

    • How many remaining enemies are there now? What is the player's score at the beginning, middle, and end of the game?
    • What is the time left at this point? Is the player progressing as fast as we expect? Maybe the initial section is too hard.
    • How many lives does our player Tobbi have at level 8 of our dungeon?
      If your players nearly die when you open that door, add a terrifying Sound FX to scare the shit out of your players.
      Create memorable moments to receive memorable reviews
    Performance metrics.
    Prevent your players from flipping a table when they start lagging at a final boss.
    • At this point in the game, what's our average FPS and consumed memory? Respect the specified minimum system requirements!
    • What's the vertex and poly count in the current player's view?

    Did I mention you can export this as CSV files? Now we are talking business. Do your numbers in Excel any time you want. Apply all the fancy math we all learned but forgot in school. Find boring areas, tweak them.

     

    You see, I learned the Importance of Analytics in games after years of programming casual games with over 30 million players. You do not get this data back ever if you didn't give a shit about it. It's gone. Finito.

    Do not skip this chance to make intelligent decisions in your game.

    Here, I am offering you a system to record all the data you need to make your game GREAT. And once you gathered your stats, take action by applying the Strategies I also send you.

     

    Get it now, regret it never <

     

     

    the-gamedev-guru-logo-full.jpg

    Conclusion

    In this article, I gave you one of the latest tricks I discovered in Unity.

    The Unity Immediate Window package allows you to run C# scripts in run-time, which effectively cuts your iteration times during prototyping phases.

    You saw some of the possibilities of Unity Immediate. The main take-away of this entry was to create automated playtests that you would later extend to collect gameplay data. Afterward, you will use this precious information to make intelligent decisions about your game, tweak your gameplay and optimize it.

    As you might have noticed, the release rate of new packages is steadily increasing. It's so easy to miss packages! But that's no problem, that's why I'm here.

    I am here to remind you of things you don't want to miss out on. So make sure to subscribe to stay up to date in the industry.

    If you have enjoyed the article, give me a hug. You know where to find me.
    Rubén

    Edited by Ruben Torres



      Report Article
    Sign in to follow this  


    User Feedback


    at all its quite a simple concept but a great idea.

    i might try to create automated playtests that runs ony my jenkins, automated after each git push 😮

    Share this comment


    Link to comment
    Share on other sites
    On 10/16/2019 at 9:00 AM, Salmakis said:

    at all its quite a simple concept but a great idea.

    i might try to create automated playtests that runs ony my jenkins, automated after each git push 😮

    I'd love to see that. Will you run the tests on an attached device or in a standalone player?

    Share this comment


    Link to comment
    Share on other sites


    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!