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:
So, to create a cheat panel, you need these elements:
UI Tool
Panel appearance-hide logic
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.
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.
usingSystem;usingSystem.Collections;usingUnityEngine;usingUnityEngine.UI;publicclassCoinsHandler:MonoBehaviour{// UI elements
[SerializeField]publicTextcoinsTitle=null; [SerializeField]publicTextcountdownTitle=null; [SerializeField]publicButtonspendButton=null;// Characteristics for the coin mechanic
publicconstintMaxCoinsCount=500;privateconstintSpendCoinsCount=100;privateint_coinsCount;publicintCoinsCount=>_coinsCount;// Coin recovering timer
privatereadonlyTimeSpan_timeToRestore=newTimeSpan(0,0,3);privateDateTime_restoreCoinsTimeMark;publicDateTimeRestoreCoinsTimeMark=>_restoreCoinsTimeMark;// Separate redrawing of UI elements
privatebool_updateUi=true;voidStart(){// 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
GameObjectcheatsHandler=newGameObject{name="CheatsHandler"};cheatsHandler.AddComponent<CheatsHandler>();#endif
}// Update UI
privatevoidLateUpdate(){if(_updateUi){_updateUi=false;UpdateUi();}}// Update UI
privatevoidUpdateUi(){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
TimeSpantimeDiff=_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
IEnumeratorUpdateTimer(){while(true){CheckCoinsTimer();_updateUi=true;yieldreturnnewWaitForSecondsRealtime(1);}}// Function decrease coins
publicvoidSpendCoins(){if(CanSpendCoins()){SetCoinsTo(_coinsCount-SpendCoinsCount);}}// Check for enough coins
publicboolCanSpendCoins(){return_coinsCount>=SpendCoinsCount;}// Coin calculation function
publicvoidGetCoins(){if(_coinsCount<MaxCoinsCount){SetCoinsTo(_coinsCount+SpendCoinsCount);}}// The function of checking the coin timer
privatevoidCheckCoinsTimer(){// Current time
DateTimetimeNow=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
publicvoidSetCoinsTo(intcount){if(_coinsCount>=MaxCoinsCount){DateTimetimeNow=DateTime.UtcNow;_restoreCoinsTimeMark=timeNow+_timeToRestore;}_coinsCount=count;_updateUi=true;}}
#if DEBUG_CHEATS
usingSystem;usingSystem.Globalization;usingUnityEngine;publicclassCheatsHandler:MonoBehaviour{// The rectangle for the cheat panel
privatereadonlyRect_cheatsBoxRect=newRect(10,10,Screen.width-20,200);// String for entering the number of coins
privatestring_coinsInput="0";// Object With Coin Mechanics
privateCoinsHandler_coinsHandler=null;// Flag of cheat panels displaying
privatebool_showPanel=false;// Panel activation timer
privateconstfloatTimeToActivate=3;privatefloat_activationTimeCounter=0;// Rectangle to check activation
privateRect_checkRect=newRect(0,0,50,50);privatebool_buttonPressed=false;voidStart(){// Informing about the presence of cheats
Debug.LogWarning("Debug cheats enabled");_coinsHandler=FindObjectOfType<CoinsHandler>();}voidUpdate(){// 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
privatevoidOnGUI(){// 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(newRect(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
TimeSpantimeDiff=_coinsHandler.RestoreCoinsTimeMark-DateTime.UtcNow;// Add minus sign if the accrual time has already passed
stringminus=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
stringtestStr=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,outintlives)){_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
privatestaticboolIsDigitsString(strings){if(s==null||s=="")returnfalse;for(inti=0;i<s.Length;i++)if(s[i]<'0'||s[i]>'9')returnfalse;returntrue;}}#endif
Add Scripting Define Symbol into Project Settings
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.
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! =)