Creating cheat panel in Unity

Tulenber 1 May, 2020 ⸱ Intermediate ⸱ 13 min ⸱ 2019.3.11f1 ⸱

Oddly enough but cheats are an essential part of the game, which significantly helps in its development.

Konami code is probably the most famous cheat code in the history of video games. It was created by Kazuhisa Hashimoto while porting the Gradius game to the NES console. He adds the code because of the consideration that the game was too hard. After entering the sequence using the controller, the player received all available power-ups. Later it was implemented in hundreds of games for a wide variety of purposes.

In your new game, after the addition of the first mechanics, you can get a situation similar to Gradius. Her testing will take a reasonably large amount of time. As an example, if your game consists of dozens of levels, then it is unlikely that for the sake of testing level 52, you will go through the entire game first to find out that some element on the map is 5 pixels to the left of the place. So adding cheats to the game becomes a usual practice to solve such problems. It helps cut corners and create the necessary conditions for checking mechanics or other game elements.

Cheat panel

If not taken this part very seriously, the embedding of cheats will start with simple mechanisms, for example, restore lives entirely by clicking on the icon with lives or, add one hundred coins when you press “Ctrl + M.” Subsequently, this translates into the fact that it will be quite tricky to remember all these non-obvious combinations and places, and new people on the project simply will not know about them. It will be even more challenging to clean and check such places on release assembling.

An elegant solution to this issue is cheating panels, which become the standard for such mechanics. They provide the ability to inform about the state of the game and manage it, excluded from regular interaction with interfaces. And all you need is a bunch of little things like make them cheap to develop, easily eliminated from the final assembly and well integrated into the game mechanics.

Cheat panel in Conan Exiles:
Conan admin panel

So, to create a cheat panel, you need these elements:

  1. UI Tool
  2. Panel appearance-hide logic
  3. Final assembly exclusion mechanism

Immediate Mode GUI (IMGUI)

The answer to the first requirement will be the primary tool for creating service interfaces in Unity, named Immediate Mode GUI (IMGUI). The same mechanism used to create custom components for the Unity editor itself, which we talked about in one of our previous articles. It is a pretty well documented and not very big package. Familiarization, with its capabilities, significantly facilitates your life with custom editor tool creation.

Panel appearance logic

Given that the panel should not interact with the main UI system, since this is not the most obvious behavior and can affect the final assembly of the product, this requirement becomes an interesting task. On devices with a keyboard, the most obvious way is to add a combination of keys that show the panel; however, on a mobile device, the solution does not immediately catch the eye. One of the simplest options is to add an area that tracking a long click or tap to the screen.

Conditional compilation

Depending on the project size and the team working on it, the project may be at different levels of its development. At the initial level, commenting parts of code seem like an acceptable method to exclude it from the final assembly. Of course, this is very dangerous, ugly, time-consuming. So never do like this if it goes to any kind of production.

An interesting suggestion from the community is to add scripts to the Editor folder. Availability can be checked by Type.GetType(“type_name”) method and the final assembly will not include it. Of course, this mechanism is designed for other tasks and does not provide the opportunity to test the game on the device. Still, it is the simplest and fastest available method.

The main method of excluding cheats from the assembly remains the good old conditional compilation with using of preprocessor symbols. There are two ways to add them:

  • Platform dependent - through the project settings Edit > Project Settings... > Other Settings > Scripting Define Symbols
  • Global - by adding mcs.rsp/csc.rsp (depending on the compiler) file to the Assets folder with the parameter -define:CUSTOM_NAME

The priority method is through project settings. The biggest disadvantage of which is the copying of all the values to each of the used platforms. If we talk about the possibility of automation, it will be necessary to add a static method to the project, which during assembly will be passed through the argument -executeMethod and, if necessary, add symbols through a call to PlayerSettings.SetScriptingDefineSymbolsForGroup (BuildTargetGroup.iOS, “DEBUG_CHEATS”);.

A global setting will work for all scripts except contained in the Editor folders and is also a valid method. However, it poses a greater danger since the mcs.rsp and csc.rsp files may contain other compiler configuration parameters. Also, it may be necessary to create additional tools for changing parameters in the case of automatic assembly.

Preprocessor-based code exception also can be divided into two methods:

  • Using the directives #if define_name, #elif define_name, #else and #endif.
  • Using the Assembly Definitions

Directives are the easiest and most affordable option, which is easiest to apply on an existing codebase.

Assembly Definitions introduces more requirements for the code architecture and usage of various patterns since the division into libraries requires a more responsible approach to the code. However, their use is valuable in itself. It can greatly simplify life on large projects, speed up the reassembly of the project, and make testing more convenient, as in our article about unit tests. And one more thing that deserves mention is that for different assemblies, you can use different mcs/csc files, which can simplify automation.

Test project

To demonstrate cheat panel creation, we build a small project with a coin mechanic.

Initially, there is a maximum of five hundred coins. Clicking on the button decrease coins by a hundred. Coins increased on a hundred every three seconds.
Conan admin panel

Implementation

Given all of the above, the implementation will look like a panel written with IMGUI, which will appear by a long click (3 seconds) in the lower-left part of the screen, and the code will use preprocessor directives #if and #endif, which we will enable by using the project settings.

Class managing mechanics and UI:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class CoinsHandler : MonoBehaviour
{
    // UI elements
    [SerializeField] public Text coinsTitle = null;
    [SerializeField] public Text countdownTitle = null;
    [SerializeField] public Button spendButton = null;

    // Characteristics for the coin mechanic
    public const int MaxCoinsCount = 500;
    private const int SpendCoinsCount = 100;
    private int _coinsCount;

    public int CoinsCount => _coinsCount;

    // Coin recovering timer
    private readonly TimeSpan _timeToRestore = new TimeSpan(0,0,3);
    private DateTime _restoreCoinsTimeMark;

    public DateTime RestoreCoinsTimeMark => _restoreCoinsTimeMark;

    // Separate redrawing of UI elements
    private bool _updateUi = true;

    void Start()
    {
        // Set the initial value for coins
        _coinsCount = MaxCoinsCount;

        // Start the timer to check the coins
        StartCoroutine(UpdateTimer());

        // Creation of object responsible for cheats if DEBUG_CHEATS flag enabled
#if DEBUG_CHEATS
        GameObject cheatsHandler = new GameObject {name = "CheatsHandler"};
        cheatsHandler.AddComponent<CheatsHandler>();
#endif
    }

    // Update UI
    private void LateUpdate()
    {
        if (_updateUi)
        {
            _updateUi = false;
            UpdateUi();    
        }
    }

    // Update UI
    private void UpdateUi()
    {
        coinsTitle.text = "Coins: " + _coinsCount;

        // The timer is displayed only when the coins count are less than the maximum
        countdownTitle.gameObject.SetActive(_coinsCount < MaxCoinsCount);
        if (_coinsCount < MaxCoinsCount)
        {
            // The difference between the future time of coins increasing and the current moment
            TimeSpan timeDiff = _restoreCoinsTimeMark - DateTime.UtcNow;
            countdownTitle.text = "Countdown: " + timeDiff.ToString("mm\\:ss");    
        }

        // The button is active when it is possible to decrease coins
        spendButton.interactable = CanSpendCoins();
    }

    // UI update timer
    IEnumerator UpdateTimer()
    {
        while (true)
        {
            CheckCoinsTimer();
            _updateUi = true;
            yield return new WaitForSecondsRealtime(1);
        }
    }

    // Function decrease coins
    public void SpendCoins()
    {
        if (CanSpendCoins())
        {
            SetCoinsTo(_coinsCount - SpendCoinsCount);
        }
    }

    // Check for enough coins
    public bool CanSpendCoins()
    {
        return _coinsCount >= SpendCoinsCount;
    }

    // Coin calculation function
    public void GetCoins()
    {
        if (_coinsCount < MaxCoinsCount)
        {
            SetCoinsTo(_coinsCount + SpendCoinsCount);
        }
    }

    // The function of checking the coin timer
    private void CheckCoinsTimer()
    {
        // Current time
        DateTime timeNow = DateTime.UtcNow;
        // If the current time is more than the mark for charging and coins are less than the maximum
        if (timeNow >= _restoreCoinsTimeMark && _coinsCount < MaxCoinsCount)
        {
            GetCoins();
            // Move the coin increase time mark
            _restoreCoinsTimeMark += _timeToRestore;
        }
    }

    // Unified function for setting the number of coins
    public void SetCoinsTo(int count)
    {
        if (_coinsCount >= MaxCoinsCount)
        {
            DateTime timeNow = DateTime.UtcNow;
            _restoreCoinsTimeMark = timeNow + _timeToRestore;
        }
        
        _coinsCount = count;

        _updateUi = true;
    }
}

Cheat Control Class:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#if DEBUG_CHEATS

using System;
using System.Globalization;
using UnityEngine;

public class CheatsHandler : MonoBehaviour
{
    // The rectangle for the cheat panel
    private readonly Rect _cheatsBoxRect = new Rect (10, 10, Screen.width - 20, 200);
    // String for entering the number of coins
    private string _coinsInput = "0";

    // Object With Coin Mechanics
    private CoinsHandler _coinsHandler = null;

    // Flag of cheat panels displaying
    private bool _showPanel = false;

    // Panel activation timer
    private const float TimeToActivate = 3;
    private float _activationTimeCounter = 0;
    
    // Rectangle to check activation
    private Rect _checkRect = new Rect(0,0,50,50);
    private bool _buttonPressed = false;

    void Start()
    {
        // Informing about the presence of cheats
        Debug.LogWarning("Debug cheats enabled");
        _coinsHandler = FindObjectOfType<CoinsHandler>();
    }

    void Update()
    {
        // Increase the click timer on the panel activation zone
        if (_buttonPressed)
        {
            _activationTimeCounter += Time.deltaTime;
        }

        // With a long enough click, show the panel
        if (_activationTimeCounter >= TimeToActivate)
        {
            _showPanel = true;
        }

        // Keyboard shortcut to show the panel
        if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKey(KeyCode.C))
        {
            _showPanel = true;
        }

        // Track the start of the click in the panel activation zone
        if (Input.GetMouseButtonDown(0) && _checkRect.Contains(Input.mousePosition))
        {
            _buttonPressed = true;
        }

        // Reset clicking information
        if (Input.GetMouseButtonUp(0))
        {
            _buttonPressed = false;
            _activationTimeCounter = 0;
        }

        /*
        // Track the start of a tap in the panel activation zone
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);
            if (_checkRect.Contains(touch.position))
            {
                _buttonPressed = true;
            }
        }

        // Reset tap information
        if (Input.touchCount == 0 && _buttonPressed)
        {
            _buttonPressed = false;
            _activationTimeCounter = 0;
        }*/
    }

    // The main function of rendering the cheat panel
    private void OnGUI()
    {
        // Do not draw the panel without activation
        if (!_showPanel)
        {
            return;
        }

        // The main object of the panel
        GUI.Box(_cheatsBoxRect, "Cheats panel");

        // The beginning of the auto layout area
        GUILayout.BeginArea (new Rect(20, 35, Screen.width - 40, 180));
        // Start vertical placement of elements
        GUILayout.BeginVertical();
        // Displays the number of lives
        GUILayout.Label("Lives count: " + _coinsHandler.CoinsCount + (_coinsHandler.CoinsCount >= CoinsHandler.MaxCoinsCount ? " (Max)" : ""));
        // Display current time
        GUILayout.Label("Current time: " + DateTime.UtcNow.ToString("G", CultureInfo.InvariantCulture));
        // Calculate the difference between the current time and the time of charging coins
        TimeSpan timeDiff = _coinsHandler.RestoreCoinsTimeMark - DateTime.UtcNow;
        // Add minus sign if the accrual time has already passed
        string minus = timeDiff < TimeSpan.Zero ? "-" : "";
        // Display the exact accrual time and the difference in minutes and seconds
        GUILayout.Label("Restore time: " + _coinsHandler.RestoreCoinsTimeMark.ToString("G", CultureInfo.InvariantCulture) + " (" + minus + timeDiff.ToString("mm\\:ss") + ")");

        // Start horizontal placement of elements
        GUILayout.BeginHorizontal();
        // Input field for the number of coins to be credited
        string testStr = GUILayout.TextField(_coinsInput);
        // Updating the text in the input field with validation
        if (IsDigitsString(testStr))
        {
            _coinsInput = testStr;
        }

        // Crediting the number of coins from the input field with validation
        if (GUILayout.Button("Set") && Int32.TryParse(_coinsInput, out int lives))
        {
            _coinsHandler.SetCoinsTo(lives);
        }
        // The end of the horizontal placement of elements
        GUILayout.EndHorizontal();

        // Start horizontal placement of elements
        GUILayout.BeginHorizontal();
        // Button to set coins to zero
        if (GUILayout.Button("0"))
        {
            _coinsHandler.SetCoinsTo(0);
        }
        // Button to withdraw a hundred coins
        if (GUILayout.Button("-100"))
        {
            _coinsHandler.SpendCoins();
        }
        // Button to add a hundred coins
        if (GUILayout.Button("+100"))
        {
            _coinsHandler.GetCoins();
        }
        // Button to set coins to maximum
        if (GUILayout.Button("Max"))
        {
            _coinsHandler.SetCoinsTo(CoinsHandler.MaxCoinsCount);
        }
        // The end of the horizontal placement of elements
        GUILayout.EndHorizontal();

        // Button to close the panel
        if (GUILayout.Button("Close panel"))
        {
            _showPanel = false;
        }

        // The end of the vertical placement of elements
        GUILayout.EndVertical();
        // End of the auto layout area
        GUILayout.EndArea();
    }

    // Validate coins input on decimals only
    private static bool IsDigitsString(string s)
    {
        if (s == null || s == "") 
            return false;
        
        for (int i = 0; i < s.Length; i++) 
            if (s[i] < '0' || s[i] > '9') 
                return false; 
        return true;
    }
}

#endif
Add Scripting Define Symbol into Project Settings
Scripting define symbols

Result

In addition to opening by a long click, we also add a key combination. The tap option is commented because it has not been tested.
Result

Conclusion

Cheat panels are one of the main things that help to test the game, and the more complex systems and mechanics are introduced into the game, the more attention will be paid to the creation of such panels. At first glance, those panels do not look as complicated tasks. However, sometimes embedding a cheat may require not only a well-built architecture but even an extension of the game mechanics themselves, so this may not be the most trivial solution. If you include here the requirement to breed builds for testing and production, this goes beyond just programming and falls into the dev_ops responsibility zone with CI settings and all other things which will help to reduce manual labor. Also, new projects are simply obliged to use the capabilities of Assembly Definitions both for organizing the code and for other features, but this is beyond the scope of the cheat panels, and we will talk about this separately. Cheat less! See you next time! =)



Privacy policyCookie policyTerms of service
Tulenber 2020