Adding Your Own Content; Part 3.2 (Building Your Own Mod Menu)


Intro

One of the more abundant questions were related to making a mod menu for your mod in game. I'm sure there are a plethora of approaches, but when I thought of a in-game menu, this way was the easiest. I have used unity to develop games before, so it just came a bit naturally.

I'm going to use the game engine, Unity, to create my menu. Since we don't have the source code, we can't use things like prefabs or our own resources (I think). So my thought was to create the whole thing programmatically.

It is possible to create a neat looking menu using the game developer's UI, but I wanted to make a template for a menu that I can use by copy pasting for other games too. If I were to use their UI, it would be specific to that game. Sure, it might not be much of a change, but I was trying to be simple and efficient than making a beautiful menu.

You will need to have knowledge of C# and UnityEngine to understand and replicate my work.

Creating a Mod Menu

I actually developed the menu right on Unity using Visual Studio. You don't have to do this, but I'd recommend it as it is much easier to test and have things going. We're going to make a new C# script in the game and name it ModMenu.

Since we can't build our own prefabs or have anything prebuilt, we will need to built our menu when the game loads. This isn't too hard, but things could get messy.

So, the first thing I thought of doing was to create my own functions for creating buttons, toggles, and input fields.

I know that Unity has their own classes for buttons, toggles, and input fields, so I will be utilizing those classes. If you don't have any experience with Unity, you might want to keep the Unity documentation handy. Or you can copy paste and learn from that too.

Here's what I came up with (I will explain):

For Panels:
GameObject CreatePanel(string name, RectTransform parent, Vector2 anchorMax, Vector2 anchorMin, Vector2 anchoredPostition, Vector2 offsetMax, Vector2 offsetMin, Color color) {
        GameObject p = new GameObject(name);

        RectTransform rt = p.AddComponent<RectTransform>();
        rt.SetParent(parent);
        rt.anchorMax = anchorMax;
        rt.anchorMin = anchorMin;
        rt.offsetMax = offsetMax;
        rt.offsetMin = offsetMin;
        rt.pivot = new Vector2(.5f, .5f);

        Image img = p.AddComponent<Image>();
        img.color = color;
        //img.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Background.psd");
        img.fillCenter = true;
        img.type = Image.Type.Sliced;

        rt.anchoredPosition = anchoredPostition;

        return p;
    }


For Buttons:
GameObject CreateButton(string name, RectTransform parent, string text, Vector2 anchorMax, Vector2 anchorMin, Vector2 anchoredPostition, Vector2 pivot, float width, float height, Vector2 offsetMax, Vector2 offsetMin, Color color) {
        GameObject buttonRoot = new GameObject(name);
        RectTransform rt = buttonRoot.AddComponent<RectTransform>();
        rt.SetParent(parent);
        rt.anchorMax = anchorMax;
        rt.anchorMin = anchorMin;
        rt.offsetMax = offsetMax;
        rt.offsetMin = offsetMin;
        rt.pivot = pivot;
        rt.sizeDelta = new Vector2(width, height);

        GameObject childText = new GameObject("Text");
        childText.AddComponent<RectTransform>().SetParent(parent);

        Image image = buttonRoot.AddComponent<Image>();
        //image.sprite = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
        image.type = Image.Type.Sliced;
        image.color = color;

        Button bt = buttonRoot.AddComponent<Button>();

        Text txt = childText.AddComponent<Text>();
        txt.text = text;
        txt.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        txt.fontStyle = FontStyle.Normal;
        txt.fontSize = 18;
        txt.horizontalOverflow = HorizontalWrapMode.Overflow;
        txt.verticalOverflow = VerticalWrapMode.Overflow;
        txt.alignment = TextAnchor.MiddleCenter;

        RectTransform textRectTransform = childText.GetComponent<RectTransform>();
        textRectTransform.SetParent(rt);
        textRectTransform.anchorMin = Vector2.zero;
        textRectTransform.anchorMax = Vector2.one;
        textRectTransform.sizeDelta = Vector2.zero;
        textRectTransform.offsetMax = Vector2.zero;
        textRectTransform.offsetMin = Vector2.zero;

        rt.anchoredPosition = anchoredPostition;

        return buttonRoot;
    }


For Toggles:
GameObject CreateToggle(string name, RectTransform parent, string holder, Vector2 anchorMax, Vector2 anchorMin, Vector2 anchoredPostition, Vector2 offsetMax, Vector2 offsetMin, float width, float height) {
        GameObject _toggle = new GameObject(name);

        RectTransform rt = _toggle.AddComponent<RectTransform>();
        rt.SetParent(parent);
        rt.anchorMax = anchorMax;
        rt.anchorMin = anchorMin;
        rt.offsetMax = offsetMax;
        rt.offsetMin = offsetMin;
        rt.pivot = new Vector2(.5f, 1f);
        rt.sizeDelta = new Vector2(width, height);

        Toggle toggle = _toggle.AddComponent<Toggle>();
        toggle.isOn = false;

        GameObject background = new GameObject("Background");
        GameObject checkmark = new GameObject("Checkmark");
        GameObject childLabel = new GameObject("Label");

        Image bgImage = background.AddComponent<Image>();
        //bgImage.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
        bgImage.type = Image.Type.Sliced;
        bgImage.color = new Color(1f, 1f, 1f);

        Image checkmarkImage = checkmark.AddComponent<Image>();
        checkmarkImage.color = new Color(.85f, .1f, .1f);
        //checkmarkImage.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Checkmark.psd");

        Text label = childLabel.AddComponent<Text>();
        label.text = holder;
        label.fontSize = 18;
        label.color = Color.white; //new Color(.196f, .196f, .196f, 1f);
        label.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        label.horizontalOverflow = HorizontalWrapMode.Wrap;
        label.verticalOverflow = VerticalWrapMode.Truncate;

        toggle.graphic = checkmarkImage;
        toggle.targetGraphic = bgImage;

        RectTransform bgRect = background.GetComponent<RectTransform>();
        bgRect.SetParent(rt);
        bgRect.anchorMin = new Vector2(0f, 1f);
        bgRect.anchorMax = new Vector2(0f, 1f);
        bgRect.anchoredPosition = new Vector2(10f, -10f);
        bgRect.sizeDelta = new Vector2(30f, 30f);

        RectTransform checkmarkRect = checkmark.GetComponent<RectTransform>();
        checkmarkRect.SetParent(bgRect);
        checkmarkRect.anchorMin = new Vector2(0.5f, 0.5f);
        checkmarkRect.anchorMax = new Vector2(0.5f, 0.5f);
        checkmarkRect.anchoredPosition = Vector2.zero;
        checkmarkRect.sizeDelta = new Vector2(30f, 30f);

        RectTransform labelRect = childLabel.GetComponent<RectTransform>();
        labelRect.SetParent(rt);
        labelRect.anchorMin = new Vector2(0f, 0f);
        labelRect.anchorMax = new Vector2(1f, 1f);
        labelRect.offsetMin = new Vector2(30f, 1f);
        labelRect.offsetMax = new Vector2(-5f, -2f);

        rt.anchoredPosition = anchoredPostition;

        return _toggle;
    }


For Inputfields:
GameObject CreateInputField(string name, RectTransform parent, string holder, Vector2 anchorMax, Vector2 anchorMin, Vector2 anchoredPostition, Vector2 offsetMax, Vector2 offsetMin, float width, float height) {
        GameObject p = new GameObject(name);

        RectTransform rt = p.AddComponent<RectTransform>();
        rt.SetParent(parent);
        rt.anchorMax = anchorMax;
        rt.anchorMin = anchorMin;
        rt.offsetMax = offsetMax;
        rt.offsetMin = offsetMin;
        rt.pivot = new Vector2(.5f, 1f);
        rt.sizeDelta = new Vector2(width, height);

        Image img = p.AddComponent<Image>();
        //img.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/InputFieldBackground.psd");
        img.fillCenter = true;
        img.type = Image.Type.Sliced;

        InputField input = p.AddComponent<InputField>();

        GameObject placeHolder = new GameObject("Placeholder");
        RectTransform placeholderRectTransform = placeHolder.AddComponent<RectTransform>();
        placeholderRectTransform.SetParent(rt);
        placeholderRectTransform.anchorMin = Vector2.zero;
        placeholderRectTransform.anchorMax = Vector2.one;
        placeholderRectTransform.sizeDelta = Vector2.zero;
        placeholderRectTransform.offsetMin = new Vector2(10, 6);
        placeholderRectTransform.offsetMax = new Vector2(-10, -7);
        Text t = placeHolder.AddComponent<Text>();
        t.text = holder;
        t.fontSize = 20;
        t.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        t.fontStyle = FontStyle.Italic;
        t.horizontalOverflow = HorizontalWrapMode.Wrap;
        t.verticalOverflow = VerticalWrapMode.Truncate;
        t.color = new Color(.196f, .196f, .196f, .5f);

        GameObject text = new GameObject("Text");
        RectTransform textRectTransform = text.AddComponent<RectTransform>();
        textRectTransform.SetParent(rt);
        textRectTransform.anchorMin = Vector2.zero;
        textRectTransform.anchorMax = Vector2.one;
        textRectTransform.sizeDelta = Vector2.zero;
        textRectTransform.offsetMin = new Vector2(10, 6);
        textRectTransform.offsetMax = new Vector2(-10, -7);
        Text tt = text.AddComponent<Text>();
        tt.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        tt.supportRichText = false;
        tt.fontStyle = FontStyle.Normal;
        tt.fontSize = 20;
        tt.horizontalOverflow = HorizontalWrapMode.Overflow;
        tt.verticalOverflow = VerticalWrapMode.Truncate;
        tt.color = new Color(.196f, .196f, .196f, 1f);

        input.placeholder = t;
        input.textComponent = tt;

        rt.anchoredPosition = anchoredPostition;

        return p;
    }

My code is pretty messy, but you can do all the cleaning up and change it to fit your need. I based these functions off of this. Unfortunately, I found it after I already wrote most of the code, so it's still messy :0. The script is what the Unity editor uses to create default UI components. I only use these 4 types of UI elements, so I stopped there, but you can go ahead and create more like dropdown buttons or text elements.

The code is pretty self-explanatory: It instantiates a GameObject and gives it necessary components to act like what I want it to. You can see what the GameObject needs by creating a default one in the Unity editor and seeing what other GameObject and components are attached to it. For example, when I create a UI button, it creates a "Button" object with the "Button" script element + other components, and another GameObject inside of it to create the text.

Now, we will use these functions to create a panel where the user can interact with the mod.

I began by creating variables for the GameObjects for the major parts: The canvas which will show our UI, the main panel which will hold our buttons and other elements, and the button which will toggle the main panel on and off.

I'm also going to throw in the GameObjects for the eventSystem (Which is something that is needed by Canvas to work), save button, load button, and the credits string.

private GameObject _main, _eventSystem, _open_button, _panel, _save_button, _load_button, _credit;

We're going to put instantiate our elements in the Awake() function, which is called before the game starts. Notice how there's a "_" in front of each of the variable. I did this for every GameObject variable to make distinguishing it easier.

Creating the Canvas is simple (the same as creating the other UI elements): create the GameObject, _main, and give it the components it needs to act like a Canvas. Here's the documentation.

_main = new GameObject("main");
Canvas mainCanvas = _main.AddComponent<Canvas>();
mainCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
mainCanvas.sortingOrder = 16959;
CanvasScaler sc = _main.AddComponent<CanvasScaler>();
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
sc.referenceResolution = new Vector2(660f, 800f);
sc.screenMatchMode = 0;
_main.AddComponent<GraphicRaycaster>();
DontDestroyOnLoad(_main);

So I add the Canvas component to the gameObject and set the variables accordingly. The sortingOrder is on max so our canvas is the one always on top. Then I add the scaler to make it scale with any screen size. There's many ways to effectively make the UI scale with your screen using anchors and such, but you can do that on your own.

It's very important to add DontDestroyOnLoad(_main) to ensure that the menu stays intact through scene changes. We can add a check when creating the menu so that we don't create multiple instances of the menu.

Then we create an EventSystem to go along with it to allow users to interact.

_eventSystem = new GameObject("EventSystem");
_eventSystem.AddComponent<EventSystem>();
_eventSystem.AddComponent<StandaloneInputModule>();

Now that we have our canvas, we can "paint" on it. We'll add the main panel where we'll add on everything else on. We're going to use our createPanel() to create it.

_panel = CreatePanel("main_panel", _main.GetComponent<RectTransform>(), Vector2.one, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, .9f));
_panel.SetActive(false);

I set the GameObject inactive on the second line since we want it to start off as inactive so we can toggle it on with our button.

On that note, we create our button that will toggle the _panel on and off.

_open_button = CreateButton("open_button", _main.GetComponent<RectTransform>(), "W", Vector2.one, Vector2.one, Vector2.zero, Vector2.one, 50f, 50f, Vector2.zero, Vector2.zero, new Color(1f, .12f, 0f, .4f));
_open_button.GetComponent<Button>().onClick.AddListener(ToggleMenu);

The onClick.AddListener(ToggleMenu) part determines what happens when the button is pressed. I created a function called ToggleMenu() that makes the _panel active and inactive.

void ToggleMenu() {
    if (isDragged) {
        isDragged = false;
        return;
    }
    _panel.SetActive(!_panel.activeSelf);
}

I added a variable, private bool isDragged, which is set to true when the button is being dragged so that the menu doesn't open when the button is dragged.

In order to make the button draggable, we need to add a listener to the button that detects dragging and write a function that handles the drag to make the button follow the mouse or the finger. Like this:

EventTrigger trigger = _open_button.AddComponent<EventTrigger>();
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = EventTriggerType.Drag;
entry.callback.AddListener((eventData) => { OnDrag(); });
trigger.triggers.Add(entry);

void OnDrag() {
    RectTransform rt = _open_button.GetComponent<RectTransform>();
    if (!isDragged) {
        xoff = Mathf.Abs(Input.mousePosition.x - rt.position.x);
        yoff = Mathf.Abs(Input.mousePosition.y - rt.position.y);
    }
    isDragged = true;
    rt.position = new Vector2(Input.mousePosition.x + xoff, Input.mousePosition.y + yoff);
}

The OnDrag function simply sets the location of the button to the location of the mouse based on where you clicked the button (xoff and yoff).

Now that you have the menu functionality (a button that opens and closes the menu panel and the panel itself), you can add user inputs and other menu-related stuff like a credits. I'll show you an example of this, although this is the part where you add your own functionality of the mod menu for your game.

Adding the Mod Menu to the Game.

Before I show you an example of adding actual functionality to the menu, we should make sure our mod menu becomes instantiated when the game starts. I forgot to do this a few times which led me scratching my head wondering where my mod menu is.

First, we copy our code we wrote in Unity (The whole C# script) and insert a new class into the game (similar to how we created our own method in my previous post, but this time, a class). Then we paste our code and press compile and now we have our own class called ModMenu that we can use.

All you need to do to add the menu to the game is add the mod menu script as a component to some other GameObject. I'm going to add mine to the Camera element of the game. To do this, we need to find a Awake() function somewhere and add this:

Camera.main.gameObject.AddComponent<ModMenu>();

This line gets the main camera used and adds the ModMenu class as a component. However, this line may be called more than once and create multiple instances of the menu. To avoid this, in the class where we add the above line of code, we will add a boolean called isModMenuCreated or whatever you want. Then, we will add a check:

if (!isModMenuCreated) {
      Camera.main.gameObject.AddComponent<ModMenu>();
      isModMenuCreated = true;
}

That's it!

Example of Using the Mod Menu

Now that we have our mod menu, how do we make it so that it controls how the mod works? We have to create the mod so that it checks with the mod menu before changing anything. In this example, I will be using my Part 1 example about modding the item cooldowns, so you should read up on that.

Okay, so we're going to add a toggle element  (that will dictate whether this mod is active or not) and an input element (that will be the multiplier for the item cooldown; ex: 0.5 input will half the cooldown on the item). If you've used my mod menu or saw my youtube video on my mod menu, you will see a checkbox and an input field under for the item cooldown to do that.

We start off by creating public variables:
public static bool isItemCoolDown;
public static float itemCoolDown;
private GameObject _itemCoolDown, _itemCoolDownT;

The boolean isItemCoolDown will determine whether this item cooldown mod is active or not. The float itemCoolDown will be our multiplier. The GameObjects _itemCoolDown and _itemCoolDownT will hold the GameObjects of our inputfield and checkbox (toggle).

Notice how my boolean has a "is" in front. This makes it easier for me to identify which variable is a boolean and which is not, just like how "_" tells me if a variable is a GameObject or not.

I make the variables public and static so that we won't need a specific instance of the ModMenu to be able to read those variables from outside the class.

First:
We will add the toggle (checkbox) and the input field for the users onto our _panel.

RectTransform parent = _panel.GetComponent<RectTransform>();

_itemCoolDownT = CreateToggle("itemCoolDownT", parent, "Item reset time", 300f, 30f, new Vector2(0, starty - 420));
_itemCoolDownT.GetComponent<Toggle>().onValueChanged.AddListener((isOn) => { isItemCoolDown = isOn; });
_itemCoolDown = CreateInputField("itemCoolDown", parent, "Item reset time", 300f, 40f, new Vector2(0, starty - 455f));
_itemCoolDown.GetComponent<InputField>().onEndEdit.AddListener((value) => { float.TryParse(value, out itemCoolDown); });

I make a local variable called parent so that I don't have to rewrite GetComponent for every UI element I create.

In the above code, I create the toggle element first with CreateToggle() and then add a listener with a lambda function that changes the boolean value of isItemCoolDown to whatever the toggle value is.

Then I create the input element with CreateInputField() and then add a listener with a lambda function that sets the float value of itemCoolDown with whatever value the user entered. The float.TryParse() makes sure the input is a number.

Okay, now that we have that, just update the ModMenu class inside the game by right clicking on it -> edit class -> paste the whole ModMenu class again from your Unity project.

Now, we're going to go back to the modification we made to ItemExtData in Post 1.


As you can see, we hardcode the number 0 in there. We are going to change this and use the value the user entered in our mod menu.

Really simple: check if the item cooldown mod is enabled. If it is on, multiply coolTime by the itemCoolDown float value to get the new this.coolTime. If not on, use the default, this.coolTime = coolTime. We also have to change this.coolUntilAt to use the new this.coolTime instead of the parameter variable coolTime.

Edit: image is wrong. It should be
If(ModMenu.isItemCoolDown) {
        This.coolTime = coolTime * ModMenu.itemCoolDown;
} else //blahblah
Since the this.coolTime is not set, we need to use coolTime as the base with the multiplier.

And that's it! Now when you enable the cooldown mod and enter a value, the item cooldown will be modified to whatever you want!

Here's a video of it in action.

Applying This to Another Game

If you understood the making of my mod menu, you could probably do the same for any other game you want. If not, you might have trouble replicating this. If the issue is that you don't understand the programming, that's a sign you should learn how to program stuff first before attempting things like this. If the issue is that I wasn't clear, then you should read this part and ask me a question after and give me some feedback on this article.

Creating the menu is the same for any game; you can just follow what I did. Beyond that, to create your own functionality, you need to know how to mod the game (Part 1). Once you do that, it's simple converting that mod into something configurable.

First, you create the inputs that you need (in my example, it was a toggle button and an input field). If you don't know what you need, just ask yourself: is this something that you turn on or off? (toggle button), something that you press repeatedly? (a button; perhaps like giving you money when you press it), or something you change the value of? (input field)

Second, you create the necessary variables (boolean, float, int, string, or whatever) that holds the values which you will modify another ingame variable with, or check if the mod is enabled or not.

Third, you will modify the previous modification you made by checking if the mod is on, then altering the code so that if it is on, you will modify something and if not, it will just do the normal thing it would do without any mods.

That's basically it.

Thanks for reading :)

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Would you like to make an appguard bypass tutorial?

    ReplyDelete

Post a Comment

Popular Posts