Unity ScriptableObjects for Beginners: Creating a Hero Selection Interface Step by Step

ScriptableObjects in Unity: A Formal Overview

ScriptableObjects in Unity are versatile data containers that provide a structured and efficient means of organizing and managing data outside of MonoBehaviour instances. Derived from the ScriptableObject base class, these objects serve as assets within the Unity Editor, capable of encapsulating serialized data.

Key Characteristics of ScriptableObjects:

  1. Data Encapsulation: ScriptableObjects allow developers to encapsulate data in a reusable and modular form. This is particularly beneficial when dealing with information that doesn’t necessarily belong to specific GameObjects within a scene.
  2. Asset-Based: Unlike MonoBehaviour scripts, ScriptableObjects are not attached to GameObjects in the scene. Instead, they are created and manipulated as assets within the Unity Editor. This asset-based nature makes ScriptableObjects highly versatile for storing configurations, settings, and other static data.
  3. Modularity and Extensibility: ScriptableObjects promote modularity in design by facilitating the encapsulation of discrete units of data. Modifications or additions to data can be made independently of code, contributing to a more extensible and maintainable architecture.
  4. Decoupling Logic: Storing data in ScriptableObjects allows for the separation of data from logic. This decoupling simplifies the implementation of systems, as logic can dynamically reference and interact with ScriptableObjects without direct dependencies.
  5. Serialization and Sharing: ScriptableObjects seamlessly integrate with Unity’s serialization system. Serialized instances can be saved as asset files, facilitating easy sharing of data across scenes, projects, or even with other developers.
  6. Run-Time Efficiency: ScriptableObjects are particularly efficient during runtime. They lack MonoBehaviour functions, making them suitable for holding static data used consistently throughout the game.

Now lets check how we can create a simple hero selection interface. It will be very simple, Simple hero stats like health, stamina, attack and damage. And the hero image..
We will need two panel in our canvas , you can make the interface design as you like. I’m just giving a simple overview. In the left panel we ill show the selected hero’s image, name, health,stamina, attack and defense.
And in another panel on the right we will load all heroes dynamically. There I have used a grid to show the heroes. User will be able to click on the slots and select the hero and it will be shown in the left panel with detailed stats.

Creating a Simple Hero Selection Interface in Unity

In this guide, we will walk through the process of designing a basic hero selection interface in Unity. Our interface will consist of two panels: one for displaying detailed information about the selected hero and another for dynamically loading and selecting heroes.

Interface Overview:

  1. Left Panel (Details Panel):
    • This panel will showcase detailed information about the selected hero.
    • Information displayed includes the hero’s image, name, health, stamina, attack, and defense.
  2. Right Panel (Heroes Panel):
    • A grid-based layout will be used to dynamically load and display all available heroes.
    • Users can click on hero slots to select a hero, and the detailed information will be updated in the left panel.

Step-by-Step Implementation:

1. Create Canvas and Panels:

  • Start by creating a Canvas in your Unity scene.
  • Add two panels: one for the left (Details Panel) and one for the right (Heroes Panel).
  • Design the interface layout according to your preference.

2. Create Hero ScriptableObject:

  • Define a ScriptableObject class to represent a hero. Include attributes such as name, health, stamina, attack, defense, and a reference to the hero’s image.

3. Design Hero Slots:

  • Create prefabs for hero slots to be used in the grid layout of the Heroes Panel.
  • Each slot should display the hero’s image and possibly their name.

4. Implement Hero Selection Logic:

  • Write a script to manage hero selection.
  • Dynamically instantiate hero slots in the grid based on the available heroes.
  • Allow users to click on a hero slot, triggering an event to update the Details Panel with the selected hero’s information.

5. Update Details Panel:

  • Implement logic to update the Details Panel with the selected hero’s information.
  • Display the hero’s image, name, health, stamina, attack, and defense.

Code Example (Overview):

HeroClass.cs (Scriptable Object class for creating heroes)
using UnityEngine;

[CreateAssetMenu(fileName = "NewHero", menuName = "Hero/Create a new hero")]
public class HeroClass : ScriptableObject
{
    [Header("Hero Information")]
    public string heroName;
    public Sprite heroImage;

    [Header("Stats")]
    public float maxHealth;
    public float maxStamina;
    public float attack;
    public float defence;
}
HeroInfo.cs (Slot prefab, will hold all information for checking purpose)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class HeroInfo : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    [Header("Hero Information")]
    public HeroData heroData;

    public Button button;
    public Image imageSlot;

    public GameObject bg;
    public GameObject selectionStateImage;

    public bool isSelected;

    private void Start()
    {
        button.onClick.AddListener(() => HeroSelectionPanelManager.Instance.SetHeroData(heroData));
        button.onClick.AddListener(SelectionState);
    }
    
    // Set UI elements with HeroClass information
    public void SetHeroInfo(HeroClass heroClass)
    {
        if (heroClass != null)
        {
            heroData.heroName = heroClass.heroName;
            heroData.heroImage = heroClass.heroImage;
            heroData.maxHealth = heroClass.maxHealth;
            heroData.maxStamina = heroClass.maxStamina;
            heroData.attack = heroClass.attack;
            heroData.defence = heroClass.defence;
        }

        gameObject.name = heroData.heroName;
        imageSlot.sprite = heroData.heroImage;
    }

    public void SelectionState()
    {
        for (int i=0;i<HeroSelectionPanelManager.Instance.heroList.Length;i++)
        {
            if (HeroSelectionPanelManager.Instance.heroList[i].GetComponent<HeroInfo>()!=null)
            {
                HeroSelectionPanelManager.Instance.heroList[i].GetComponent<HeroInfo>().isSelected = false;
                HeroSelectionPanelManager.Instance.heroList[i].GetComponent<HeroInfo>().bg.SetActive(false);
                
            }
        }
        bg.SetActive(true);
        isSelected = true;
    }

    public void SetInitialObjectState()
    {
        bg.SetActive(true);
        isSelected = true;
    }
    
    public void OnPointerEnter(PointerEventData eventData)
    {
        bg.SetActive(true);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        if (!isSelected)
        {
            bg.SetActive(false);
        }
    }
}

[System.Serializable]
public class HeroData
{
    public string heroName;
    public Sprite heroImage;
    public float maxHealth;
    public float maxStamina;
    public float attack;
    public float defence;
}
HeroSelectionPanelManager.cs (An singleton class for referencing all slots and all other ui objects)
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class HeroSelectionPanelManager : MonoBehaviour
{
    private static HeroSelectionPanelManager instance;

    [Header("Basic UI References")] 
    public Image heroImage;
    public TextMeshProUGUI heroName;
    public Slider healthSlider;
    public Slider staminaSlider;
    public Slider attackSlider;
    public Slider defenceSlider;
    
    [Header("Spawned Hero list")]
    public GameObject[] heroList;
    
    
    public static HeroSelectionPanelManager Instance
    {
        get
        {
            if (instance == null)
            {
                // If the instance is not yet set, find it in the scene
                instance = FindObjectOfType<HeroSelectionPanelManager>();

                // If it still cannot be found, create a new GameObject with the script attached
                if (instance == null)
                {
                    GameObject singletonObject = new GameObject("HeroSelectionPanelManager");
                    instance = singletonObject.AddComponent<HeroSelectionPanelManager>();
                }
            }

            return instance;
        }
    }
    private void Awake()
    {
        // Ensure there is only one instance of this class
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
        }

        // Set the instance to this object
        instance = this;

        // Keep the object alive between scenes
        DontDestroyOnLoad(gameObject);
    }
    
    // Method to update UI elements with hero data
    public void SetHeroData(HeroData heroData)
    {
        if (heroData != null)
        {
            // Update UI elements with hero data
            heroImage.sprite = heroData.heroImage;
            heroName.text = heroData.heroName;
            healthSlider.value = MapToSliderRange(heroData.maxHealth);
            staminaSlider.value = MapToSliderRange(heroData.maxStamina);
            attackSlider.value = MapToSliderRange(heroData.attack);
            defenceSlider.value = MapToSliderRange(heroData.defence);  // Assuming you have a 'defence' property in HeroData
        }
    }

    // Helper method to map a value from the original range to the slider's 0 to 1 range
    private float MapToSliderRange(float value)
    {
        // Assuming original range is 1 to 100
        return (value - 1) / 99;  // Map to the slider range (0 to 1)
    }
}
SlotLoader.cs (For dynamically loading slots by checking the list from the singleton class)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SlotLoader : MonoBehaviour
{
    public HeroClass[] heroes; // Array of your HeroClass scriptable objects
    public GameObject slotPrefab; // The prefab for displaying heroes
    public Transform contentPanel; // The panel where heroes will be instantiated
    private bool _hasCalledMethod = false;

    private void Awake()
    {
        // Instantiate heroes on Start
        InstantiateHeroes();
    }
    
    void InstantiateHeroes()
    {
        HeroSelectionPanelManager.Instance.heroList = new GameObject[heroes.Length];
        int index = 0;
        foreach (HeroClass heroClass in heroes)
        {
            
            // Instantiate the hero prefab
            GameObject heroInstance = Instantiate(slotPrefab, contentPanel);
            HeroSelectionPanelManager.Instance.heroList[index] = heroInstance;
            index++;

            // Get the HeroClass script from the prefab
            HeroInfo heroScriptInPrefab = heroInstance.GetComponent<HeroInfo>();

            if (heroScriptInPrefab != null)
            {
                // Set the HeroClass instance in the prefab script
                heroScriptInPrefab.SetHeroInfo(heroClass);
                if (!_hasCalledMethod)
                {
                    HeroSelectionPanelManager.Instance.SetHeroData(heroScriptInPrefab.heroData);
                    heroScriptInPrefab.SetInitialObjectState();
                    _hasCalledMethod = true; // Set the flag to true after the method is called
                }
            }
        }
    }
}

I deliberately set many variables as public for increased transparency during debugging and learning. This decision allows for easy inspection of variables, aiding in identifying and resolving issues quickly.

Recognizing that the code isn’t flawless and may not adhere to the strictest coding practices. However, this intentional simplicity serves the purpose of focusing on ScriptableObjects and showcasing their practical application.

The way Scriptable object is helping us here:

  • ScriptableObjects allow us to centralize hero data. Each hero is represented as a ScriptableObject, encapsulating essential attributes such as name, image, health, stamina, attack, and defense.
  • Hero slots in the grid layout are instantiated dynamically using the hero ScriptableObjects. This modular approach ensures that each hero’s representation is consistent and easily expandable.
  • When a hero slot is clicked, the associated ScriptableObject is used to update the Details Panel. This dynamic linking allows for immediate and seamless updates, showcasing the selected hero’s attributes without the need for complex logic.
  • The modular nature of ScriptableObjects promotes a scalable design. It becomes easy to add new heroes to the roster simply by creating new ScriptableObjects, minimizing the need for extensive code modifications.

If you need the full project then please comment your email address. I will send you.

Endless Existence
Endless Existence
Articles: 74

Leave a Reply

Your email address will not be published. Required fields are marked *