Unity: Simple Cheat System

August 15, 2025

Testing a game can be really tedious without cheats, nobody wants to grind through half an hour of levels just to test a specific feature.

The simplest way to go about this is to hard-code a cheat, perhaps using hotkeys or a special menu, but that can become quite tedious too.

A more flexible, modular method would be to implement some sort of command system or cheat console, kinda like Minecraft's "give" or "tp" commands.

Semi-Declarative Programming

Implementing commands can be as trivial as storing the user's input, then matching it with a bunch of strings, like in a big switch case or if else chain.

Such a method is extremely ugly and inelegant however, in my humble opinion. So instead, I'll opt to write my cheats in a more "declarative" way, using Dictionaries and Actions to do so.

Example Cheats Enum

We'll start with an enum describing all the Cheats we want to have:

enum CheatKey
{
    None,
    ToggleCheats,
    SkipBattle,
    InstantWin,
    InstantLose,
    GiveMoney,
    GiveItem
};

Example Cheat Name Table

We'll map the enum to a cheat name table:

static readonly ReadOnlyDictionary<CheatKey, string> CheatNameTable = new(new Dictionary<CheatKey, string>() {
    { CheatKey.ToggleCheats, "godmode" },
    { CheatKey.SkipBattle, "skip" },
    { CheatKey.InstantWin, "allyourbasearebelongtous" },
    { CheatKey.InstantLose, "somebodysetusupthebomb" },
    { CheatKey.AddMoney, "greedisgood" },
    { CheatKey.GiveItem, "give" },
});

Add a helper to fetch cheat names from enum values:

const string INVALID_CHEAT_NAME = "INVALID";

static string CheatName(CheatKey key)
{
    try
    {
        return CheatNameTable[key];
    }
    catch
    {
        return INVALID_CHEAT_NAME;
    }
}

Example Cheat Class

Then maybe we can have a Cheat class to store the behaviour we want:

class Cheat
{
    public CheatKey key;
    public string name;
    public Action<object> action;
    public Predicate<Cheat> predicate;

    public Cheat(CheatKey key, Action<object> action, Predicate<Cheat> predicate = null)
    {
        this.key = key;
        this.name = CheatName(key);
        this.action = action;
        this.predicate = predicate;
    }
}

This can be extended to use the Command pattern later (for Undo/Redo/Etc.), but I'll keep things simple for now.

Cheat Table

Putting it all together, we can register cheats in a dictionary, using the cheat key, cheat name, actions and predicates to define the cheat's logic, behaviour and possible preconditions.

static bool cheatMode = false;

static Tuple<string, Cheat> RegisterCheat(CheatKey key, Action<object> action, Predicate<Cheat> predicate = null)
{
    return new(CheatName(key), new(key, action, predicate));
}

static readonly ReadOnlyDictionary<string, Cheat> CheatTable = new (new DictionaryTupleInitializer<string, Cheat>() {
    RegisterCheat(CheatKey.ToggleCheats, (_) => {
        cheatMode = !cheatMode;
        UISFXPlayer.Play_SomeSound();

        Debug.Log("CHEATS TOGGLED!");
    }),

    RegisterCheat(CheatKey.SkipBattle, (_) => {
        GameManager.SkipBattle();
        UISFXPlayer.Play_SomeSound();
    }),

    RegisterCheat(CheatKey.InstantWin, (_) => {
        GameManager.currentBattle.enemy.TakeDamage(1000000); 
    }),

    RegisterCheat(CheatKey.InstantLose, (_) => {
        GameManager.player.TakeDamage(1000000);
    }),

    RegisterCheat(CheatKey.AddMoney, (_) => {
        GameManager.player.money += 1000;
        UISFXPlayer.Play_Buy(); 
    }),
});

Cheats with Arguments

Here's an example of a cheat using multiple arguments (with helper):

static List<string> GetCheatArgsOrDefault(object data, int expectedCount, string defaultValue)
{
    List<string> argsList = new(expectedCount);
    for (int i = 0; i < expectedCount; ++i)
    {
        argsList.Add(defaultValue);
    }

    if (data is string[])
    {
        var args= (string[])data;
        int argCount= Math.Min(expectedCount, args.Length);

        for (var i= 0; i < argCount; i++)
        {
            argsList[i]= args[i];
        }
    }

    return argsList;
}

...
RegisterCheat(CheatKey.GiveItem, (data)=> {
    var args= GetCheatArgsOrDefault(data, 4, "");
    var (cheatName, itemName, quantity, target)= (args[0], args[1], args[2], args[3]);

    if (itemName= "")
        return;

    UISFXPlayer.Play_SomeSound();
    GameManager.GiveItem(target, new Item(itemName, quantity));
}),
...

Update Loop

Finally, we need to have an update loop to check and store the user's input, as well as to dispatch the corresponding cheat.

void CheckCheat(string enteredText)
{
    enteredText = enteredText.ToLower().Trim();
    var args = enteredText.Split(' ');

    TryExecuteCheat(args[0], args);

    commandHistory.Add(enteredText);
    selectedCommandIndex = commandHistory.Count;
}

static bool TryExecuteCheat(string name, string[] args)
{
    var success = CheatTable.TryGetValue(name, out var cheat);
    if (success && name == cheat.name)
    {
        if (cheat.predicate == null)
        {
            cheat.action.Invoke(args);
            return success;
        }

        if (cheat.predicate.Invoke(cheat))
            cheat.action.Invoke(args);
    }

    return success;
}

void Update()
{
    if (UnityEngine.EventSystems.EventSystem.current != null &&
        UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject != null)
    {
        return;
    }

    foreach (char c in Input.inputString)
    {
        if (c == '\b')
        {
            if (inputBuffer.Length > 0)
                inputBuffer = inputBuffer.Substring(0, inputBuffer.Length - 1);
        }
        else if (c == '\n' || c == '\r')
        {
            CheckCheat(inputBuffer);
            inputBuffer = "";
        }
        else
        {
            inputBuffer += c;
            // Optional: limit buffer length
            if (inputBuffer.Length > 64)
                inputBuffer = inputBuffer.Substring(inputBuffer.Length - 64);
        }
    }

    if (cheatMode)
    {
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            selectedCommandIndex = Math.Clamp(selectedCommandIndex - 1, 0, commandHistory.Count - 1);
            inputBuffer = (selectedCommandIndex) < commandHistory.Count ? commandHistory[selectedCommandIndex] : inputBuffer;
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            selectedCommandIndex = Math.Clamp(selectedCommandIndex + 1, 0, commandHistory.Count - 1);
            inputBuffer = (selectedCommandIndex) >= 0 ? commandHistory[selectedCommandIndex] : inputBuffer;
        }
    }
}

Cheat Console

A simple way to render a cheat console of some sorts would be to use IMGUI:

private void OnGUI()
{
    if (!(cheatMode))
        return;

    int windowX = 64;
    int windowY = 64;

    int windowWidth = 512;
    int windowHeight = 512;

    int inputHeight = 32;

    int itemWidth = windowWidth;
    int itemHeight = 32;
    int itemOffsetX = 4;
    int itemOffsetY = itemHeight / 2;

    GUI.Box(new Rect(windowX, windowY, windowWidth, windowHeight), "");
    for (int i = 0; i < commandHistory.Count; i++)
    {
        var command = commandHistory[i];
        GUI.Label(new Rect(windowX + itemOffsetX, windowY + itemOffsetY * i, itemWidth, itemHeight), command);
    }
    GUI.TextField(new Rect(windowX, windowHeight + inputHeight / 2, windowWidth, inputHeight), inputBuffer);
}

This renders a simple window, showing the current input and previous commands.

Final Boilerplate Class

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using UnityEngine;

public class CheatController : MonoBehaviour
{
	public static bool cheatMode = false;

	string inputBuffer = "";
	static Dictionary<string, Action> closures = new();
	static List<string> commandHistory = new();
	static int selectedCommandIndex = 0;

    //Cheat MAPPING START
    enum CheatKey
	{
		None,
		ToggleCheats
	};

	const string INVALID_CHEAT_NAME = "INVALID";

	static string CheatName(CheatKey key)
	{
		try
		{
			return CheatNameTable[key];
        }
		catch
		{
			return INVALID_CHEAT_NAME;
		}
	}

	static bool TryGetCheatArg(object data, out string cheatName, out string arg)
	{
        cheatName = "";
		arg = "";

        if (data is string[])
		{
			var args = (string[])data;
			if (args.Length < 2)
				return false;

			cheatName= args[0];
            arg= args[1];

			return true;
		}

		return false;
	}

	static List<string> GetCheatArgsOrDefault(object data, int expectedCount, string defaultValue)
	{
		List<string> argsList = new(expectedCount);
		for (int i = 0; i < expectedCount; ++i)
		{
			argsList.Add(defaultValue);
		}

		if (data is string[])
		{
			var args= (string[])data;
            int argCount= Math.Min(expectedCount, args.Length);

            for (var i= 0; i < argCount; i++)
            {
				argsList[i]= args[i];
            }
        }

		return argsList;
	}

    class Cheat
	{
		public CheatKey key;
		public string name;
		public Action<object> action;
		public Predicate<Cheat> predicate;

		public Cheat(CheatKey key, Action<object> action, Predicate<Cheat> predicate = null)
		{
			this.key = key;
			this.name = CheatName(key);
			this.action = action;
			this.predicate = predicate;
		}
	}

	static readonly ReadOnlyDictionary<CheatKey, string> CheatNameTable = new(new Dictionary<CheatKey, string>() {
		{ CheatKey.ToggleCheats, "godmode" }
	});

    static Tuple<string, Cheat> RegisterCheat(CheatKey key, Action<object> action, Predicate<Cheat> predicate = null)
	{
		return new(CheatName(key), new(key, action, predicate));
	}

	static readonly ReadOnlyDictionary<string, Cheat> CheatTable = new (new DictionaryTupleInitializer<string, Cheat>() {
        RegisterCheat(CheatKey.ToggleCheats, (_) => {
			cheatMode = !cheatMode;
			UISFXPlayer.Play_SomeSound();

			Debug.Log("CHEATS TOGGLED!");
        }),
    });
	static readonly DictionaryBindingValidator<CheatKey, Cheat, string> cheatTableBindingValidator = new(CheatNameTable, CheatTable);

    //CHEAT MAPPING END

	//helpers
	static bool TryGetEnumValueFromName<T>(string name, out T value) where T : Enum
	{
        var names = Enum.GetNames(typeof(T));
        var runeNames = Enum.GetNames(typeof(Rune));

		value = default;

        for (int i = 0; i < names.Count(); ++i)
        {
            if (names[i].ToLower()= name)
            {
				value= (T)((object)i);
				return true;
            }
        }

		return false;
    }

    private void Awake()
    {
        inputBuffer= "";
    }

    static bool TryExecuteCheat(string name, string[] args)
	{
		var success= CheatTable.TryGetValue(name, out var cheat);
		if (success && name= cheat.name)
		{
			if (cheat.predicate= null)
			{
                cheat.action.Invoke(args);
				return success;
            }

			if (cheat.predicate.Invoke(cheat))
				cheat.action.Invoke(args);
        }

		return success;
    }

	void Update()
	{
		if (UnityEngine.EventSystems.EventSystem.current = null &&
			UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject = null)
		{
			return;
		}

		foreach (char c in Input.inputString)
		{
			if (c= '\b')
			{
				if (inputBuffer.Length > 0)
					inputBuffer = inputBuffer.Substring(0, inputBuffer.Length - 1);
			}
			else if (c == '\n' || c == '\r')
			{
				CheckCheat(inputBuffer);
				inputBuffer = "";
			}
			else
			{
				inputBuffer += c;
				// Optional: limit buffer length
				if (inputBuffer.Length > 64)
					inputBuffer = inputBuffer.Substring(inputBuffer.Length - 64);
			}
		}

		if (cheatMode)
		{
			if (Input.GetKeyDown(KeyCode.UpArrow))
			{
				selectedCommandIndex = Math.Clamp(selectedCommandIndex - 1, 0, commandHistory.Count - 1);
                inputBuffer = (selectedCommandIndex) < commandHistory.Count ? commandHistory[selectedCommandIndex] : inputBuffer;
			}
            else if (Input.GetKeyDown(KeyCode.DownArrow))
			{
                selectedCommandIndex= Math.Clamp(selectedCommandIndex + 1, 0, commandHistory.Count - 1);
                inputBuffer= (selectedCommandIndex) >= 0 ? commandHistory[selectedCommandIndex] : inputBuffer;
            }
        }
	}

	void CheckCheat(string enteredText)
	{
        enteredText = enteredText.ToLower().Trim();
		var args = enteredText.Split(' ');

		TryExecuteCheat(args[0], args);

		commandHistory.Add(enteredText);
		selectedCommandIndex = commandHistory.Count;
    }

    private void OnGUI()
    {
		if (!(cheatMode))
			return;

		int windowX = 64;
		int windowY = 64;

		int windowWidth = 512;
		int windowHeight = 512;

		int inputHeight = 32;

		int itemWidth = windowWidth;
		int itemHeight = 32;
		int itemOffsetX = 4;
		int itemOffsetY = itemHeight / 2;

        GUI.Box(new Rect(windowX, windowY, windowWidth, windowHeight), "");
        for (int i = 0; i < commandHistory.Count; i++)
        {
			var command= commandHistory[i];
			GUI.Label(new Rect(windowX + itemOffsetX, windowY + itemOffsetY * i, itemWidth, itemHeight), command);
		}
		GUI.TextField(new Rect(windowX, windowHeight + inputHeight / 2, windowWidth, inputHeight), inputBuffer);
    }
}

Auxiliary/Dependency classes

DictionaryTupleInitializer

using System;
using System.Collections.Generic;

public class DictionaryTupleInitializer<KeyType, ValueType> : Dictionary<KeyType, ValueType>
{
    public void Add(Tuple<KeyType, ValueType> values)
    {
        Add(values.Item1, values.Item2);
    }
};

DictionaryBindingValidator

using System;
using System.Collections.Generic;

public struct DictionaryBindingValidator<KeyType, ValueType, BinderType>
{
    bool valid;

    public DictionaryBindingValidator(IDictionary<KeyType, BinderType> bindingTable, IDictionary<BinderType, ValueType> valueTable)
    {
        valid = false;
        foreach (var (key, binder) in bindingTable)
        {
            if (!valueTable.ContainsKey(binder))
                throw new Exception($"Not all values have been bound between {bindingTable} and {valueTable}!");
        }

        valid = true;
    }

    public bool IsValid { get { return valid; } }
}