Mobile FPS Game Controls Customization in Unity

In this article, we will be creating a similar system to PUBG controls customization in Unity where users will be able to replace, resize and change the opacity of the buttons and then save it for later sessions.

A lot of mobile games allow you to change your button layouts which will be preferable for your playing style. This system is hugely adapted by popular games like PUBG and Call of Duty to extend support to their players to play the game with more than two fingers. In this article, we will be creating a similar system to PUBG controls customization in Unity where users will be able to replace, resize and change the opacity of the buttons and then save it for later sessions.

After you complete this article step-by-step, your end result should look like below:

Final Demo 

Getting Started with the basics

To start off with the project I created a basic UI with a bunch of buttons and images and placed them in a UI canvas. I wrote a script called GameButton.cs

public class GameButton: MonoBehaviour, IDragHandler, IPointerDownHandler {
    public string id;
    private Vector2 _buttonPosition;
    private Vector3 _movedPosition;
    private RectTransform _rectTransform;
    private UIManager _uiManager;
    private CanvasGroup _canvasGroup;

    public Vector2 DefaultSize;
    public Vector2 DefaultPosition;

    void Start() {
      id = gameObject.name;
      _canvasGroup = GetComponent<CanvasGroup> ();
      _rectTransform = GetComponent<RectTransform > ();
      _uiManager = FindObjectOfType<UIManager > ();
      LoadData();
    }

    // Update is called once per frame
    void Update() {
      CalculatePosition();
    }

    void CalculatePosition() {
      _buttonPosition = RectTransformUtility.WorldToScreenPoint(new Camera(), transform.position);
      _buttonPosition.x = Mathf.Clamp(_buttonPosition.x, 0, Screen.width);
      _buttonPosition.y = Mathf.Clamp(_buttonPosition.y, 0, Screen.height);
      RectTransformUtility.ScreenPointToWorldPointInRectangle(_rectTransform, _buttonPosition, new Camera(),
        out _movedPosition);
      transform.position = _movedPosition;
    }

    public void OnDrag(PointerEventData eventData) {
      transform.position = Input.mousePosition;
    }

    public void SetSizeAndAlpha(float size, float alpha) {
      _rectTransform.transform.localScale = DefaultSize * size;
      _canvasGroup.alpha = alpha;
    }

    public void OnPointerDown(PointerEventData eventData) {
      _uiManager.gameButton = this;
      _uiManager.SetButtonData(transform.localScale.x, _canvasGroup.alpha);
    }
gameButton.cs

As you can see the script above implements two interfaces IDragHandler and IPointerDownHandler, we are using these two interfaces so that we can get OnDrag and OnPointerDown functions. In the OnDrag the function we set the position of the transform to the position of our mouse cursor that is input.mousePosition, this is done to move the dragged object with our cursor as OnDrag is called by events system every time a pointer is dragging something. For more information about OnDrag and IDragHandler check Unity's official documentation.

After doing this you need to assign this script to the object you want to move and then you should be able to move your dragged object around the screen. In the update function, we call calculate position every frame in order to clamp the object's movement to the ends of the device's screen.  After that is done we will create a function called SetSizeAndAlpha that takes two floats size and alpha, this will later be used with the uiManager to set the button's size and transparency.

After the implementation of the Drag, we also implement another interface IPointerDownHandler which is later used to assign our button's instance and set its data every time we end our drag and leave the button on the screen.

Handling the data with UIManager

In order to change the data like size and transparency of the buttons, we will be using a UIManager the class that handles all the references related to the data.

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

public class UIManager: MonoBehaviour {
    public Slider SizeSlider;
    public Slider TransparencySlider;
    private List < GameButton > GameButtons;
    public GameButton gameButton {
      get;
      set;
    }

    private void Start() {
      GameButtons = FindObjectsOfType<GameButton> ().ToList();
      SizeSlider.onValueChanged.AddListener(OnUpdate);
      TransparencySlider.onValueChanged.AddListener(OnUpdate);
    }

    void OnUpdate(float value) {
      gameButton.SetSizeAndAlpha(SizeSlider.value, TransparencySlider.value);
    }

    public void SetButtonData(float size, float alpha) {
      Debug.Log("Size set to " + size);
      TransparencySlider.value = alpha;
      SizeSlider.value = size;
    }
UIManager

We will now be creating an empty gameObject and assigning this UIManager script to it, then assign our sliders references through the inspector. Make sure to limit your slider values according to your wish. In this script, you can see we have two references to the slider SizeSlider and TransparencySlider, these sliders are used to set our size and transparency values. You can also see a list of type gameButtons we have created this is where we will be saving all our GameButton instances throughout our game runtime. In the start initialization, we have used FindObjectsOfType<GameButton> which returns us an array of the game button instances in our current runtime and then we change the array into a list and assign it to the GameButtons list. After that, we have added a listener to our size and transparency sliders OnUpdate(float value) which updates every single time we change our slider's value or use it. In  OnUpdate we access our current gameButton and set its size and alpha to their respective slider's value. If you remember we did this in our gameButton script every time the pointer was down.

 //Reference of GameButton Script
 public void OnPointerDown(PointerEventData eventData) {
   _uiManager.gameButton = this;
   _uiManager.SetButtonData(transform.localScale.x, _canvasGroup.alpha);
 }
gameButton

So what we are doing is we are telling the UIManager that every time we drag on a GameButton and release it our current edited button is this current gameobject. This helps us find our reference to the dragged button and know its edited value. We also have called _uiManager SetButtonData and sent the button's scale on the X-axis and it's canvas group alpha so that we can set the current values of the button to the slider.

Saving and loading the data

After doing all this stuff you should be able to move, change opacity and scale the buttons as per your wish but after you quit out from your unity play mode you might notice it getting back to its default position, so for that, we will have to create a save and load system. For this save and load system we will be using PlayerPrefs and JSONUtility. So to get started we first create a class with all the necessary data types that need to be saved.

[Serializable]
public class ButtonData
{
    public string id="";
    public float Size=1f;
    public float Alpha=1f;
    public float PosX=0f;
    public float PosY=0f;
}

Here, we have created a class called ButtonData that is serializable and contains the necessary data that needs to be saved. id is our unique button identifier, Size is our button's size, Alpha is our button's transparency and PosX and PosY are our rect transforms anchoredPosition values. In order to save the data for each button, we then create a function called SaveData in our GameButton script.

 public void SaveData() {
 
   ButtonData btnData = new ButtonData();
   btnData.id = gameObject.name; 
   btnData.Size = gameObject.transform.localScale.x; 
   btnData.Alpha = _canvasGroup.alpha; 
   btnData.PosX = _rectTransform.anchoredPosition.x; 
   btnData.PosY = _rectTransform.anchoredPosition.y;

  PlayerPrefs.SetString(id + Constants.BTN_DATA,JsonUtility.ToJson(btnData));
}
GameButton.cs SaveData Function

In this SaveData the function we create a new object of our class ButtonData and then assign its variables with our data values. Here we assign id with our gameObject's name, Size with the gameObject current localScaleX value, Alpha with the gameObject Canvas Group's alpha value, and its PosX and PosY with the button's recttransform anchored position. After assigning the value we use playerprefs to save the class into a JSON with its unique id. You can see we are packing the values into a JSON and saving it as a string in prefs so that we will not have to assign all those data individually and it also makes the data managed. You might notice that we have added the  – id – to our constant string, we are doing it in order to save our button's data individually and access the unique data with our button's id. After the function is written we go to our UIManager and add a function.

 //Reference of UIManager Script
   public void Save() {
     foreach(var btn in GameButtons) {
       btn.SaveData();
     }
   }
UIManager.cs Save Function

You can later assign this to a button's event and this will loop through the list of all our GameButtons and call their SaveData Function.

Now after the Save Part is done, we need to find a way to load the data so that we will create a new function in our GameButton.cs called LoadData().

public void LoadData() {
  ButtonData btnData = JsonUtility.FromJson <ButtonData> (PlayerPrefs.GetString(id + Constants.BTN_DATA, ""));
  
  if (btnData == null) {
    return;
  }
  
  SetSizeAndAlpha(btnData.Size, btnData.Alpha);
  _rectTransform.anchoredPosition = new Vector2(btnData.PosX, btnData.PosY);
}
LoadData

Here in this function, we do the same thing as SaveData but in reverse, we create a local scope variable with type ButtonData where we use JsonUtility.FromJSON to unpack the data that we previously saved in our playerprefs. Then we check if the btnData is null and if it is we return from there but it isn't null we set the button's size and alpha to btnData.Size and btnData.Alpha which is accessed through the unpacked JSON to object data, we also set the rectTransfrom's anchored position to a new Vector2 data with the saved PosX and PosY. We call the LoadData function in the start so that the data is automatically loaded in the first init.

Resetting the data

 public void ResetData()
    {
        SetSizeAndAlpha(1f,1f);
        _rectTransform.anchoredPosition = DefaultPosition;
    }
ResetData function of GameButton.cs
    public void Reset() {
      foreach(var btn in GameButtons) {
        btn.ResetData();
      }
    }
Reset function of UIManager.cs

The above function is used to reset the button to its default state, you can see we have set the size and alpha to 1f which is our default size and opacity and we have set the rectTransform anchoredPosition to the DefaultPosition which can be assigned through the inspector. InOrder to call this function on every button on the reset button click you can add the function below to the UIManager.cs and assign it to the reset button.