loading...
Cover image for MVC in Unity with Scriptable Objects Part 3

MVC in Unity with Scriptable Objects Part 3

cemuka profile image Cem Ugur Karacam ・5 min read

Here is the last part of the MVC project.

I've tried to make a schema to illustrate the workflow of MVC in unity.

Alt Text

In a unity app, MonoBehaviour objects live in a scene, so you could see what objects in the hierarchy. But these objects can't communicate with each other directly. MVC pattern in unity is a useful solution to that problem.

Let's try to write it simply:

User input --> Controller -----createView(ModelData)-----> View -----display(ModelData)

First, we wait for input from the user like a button click. Next, the Controller provides the view(as prefab in project) and what model needed to pass to be able to display in this view. Then, Controller instantiates the View and creates it in the scene with the data provided from the Controller. Now, View is ready and holds references for UI objects, passes data to those references to be displayed.

Let's make it in unity from where we left.

I'll work on the view first. I'll create a panel that contains uGUI objects.

Alt Text

We have a panel and an Image for item icon, three text objects for name, type and attack power, respectively. To hold a reference for these objects, create a class in the project, InfoView, add to InfoView object in the scene.

Alt Text

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

public class InfoView : MonoBehaviour
{
    public Image icon;
    public Text nameText;
    public Text typeText;
    public Text attackText;
}

Next, let's add an init method to pass info to those references.

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

public class InfoView : MonoBehaviour
{
    public Image icon;
    public Text nameText;
    public Text typeText;
    public Text attackText;

    public void Init(ItemData data)
    {
        icon.sprite = data.icon;
        nameText.text = data.name;
        attackText.text = "Attack Power: " + data.attack;

        switch (data.type)
        {
            case ItemType.Axe: typeText.text    = "Type: Axe"; break;
            case ItemType.Dagger: typeText.text = "Type: Dagger"; break;
            case ItemType.Hammer: typeText.text = "Type: Hammer"; break;
            case ItemType.Potion: typeText.text = "Type: Potion"; break;
        }
    }
}

Then, assign UI objects in the editor.

Alt Text

It's ready to make a prefab for this view. I'll make a folder as Resources. This is a special folder that unity has an access API to this specific folder. Place this prefab in Resources.

Alt Text

I've organized my project folders a bit. Now all prefabs in my Resources folder and I can access via API.

Since I will Instantiate view prefabs, delete InfoView from scene.--

Alt Text

Time to open Controller.

I'll make a public Transform to point who is going to be the parent of this view, and a private variable to store a reference to ViewInfo at start.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemViewController : MonoBehaviour
{
    public Inventory inventoryHolder;
    public Transform inventoryViewParent;
    public Transform infoViewParent;

    private GameObject infoViewPrefab;
    private GameObject itemViewPrefab;

    private void Start() 
    {
        itemViewPrefab = (GameObject)Resources.Load("Item");
        infoViewPrefab = (GameObject)Resources.Load("InfoView");
    }
}

Create a method to instantiate view into the scene.

private void CreateInfoView(ItemData data)
{
var infoGO = GameObject.Instantiate(infoViewPrefab, infoViewParent);
infoGO.GetComponent<InfoView>().Init(data);
}

I'll give this method to ItemView in InitItem , you guessed right thanks to Action<> in c#. Let's modify a bit ItemView to achieve that.

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

public class ItemView : MonoBehaviour
{
    public Button button;
    public Image itemIcon;

    private ItemData itemData;

    public void InitItem(ItemData item, Action<ItemData> callback)
    {
        this.itemData = item;
        itemIcon.sprite = itemData.icon;

        button.onClick.AddListener(() => callback(itemData) );
    }
}

So here, I've added a parameter more to pass a method.

Now I can wire up in the controller.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemViewController : MonoBehaviour
{
    public Inventory inventoryHolder;
    public Transform inventoryViewParent;
    public Transform infoViewParent;

    private GameObject infoViewPrefab;
    private GameObject itemViewPrefab;

    private void Start() 
    {
        itemViewPrefab = (GameObject)Resources.Load("Item");
        infoViewPrefab = (GameObject)Resources.Load("InfoView");

        foreach (var item in inventoryHolder.inventory)
        {
            var itemGO = GameObject.Instantiate(itemViewPrefab, inventoryViewParent);
            itemGO.GetComponent<ItemView>().InitItem(item, CreateInfoView);
        }
    }

    private void CreateInfoView(ItemData data)
    {
        var infoGO = GameObject.Instantiate(infoViewPrefab, infoViewParent);
        infoGO.GetComponent<InfoView>().Init(data);
    }
}

At Start, I populate inventory with items, and when you click items, CreateInfoView fires up. But before testing it out, I saw a problem here. Controller doesn't know if there is an InfoView created before we click. Let's check before Instatiate.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemViewController : MonoBehaviour
{
    public Inventory inventoryHolder;
    public Transform inventoryViewParent;
    public Transform infoViewParent;

    private GameObject infoViewPrefab;
    private GameObject itemViewPrefab;

    private GameObject infoView;

    private void Start() 
    {
        itemViewPrefab = (GameObject)Resources.Load("Item");
        infoViewPrefab = (GameObject)Resources.Load("InfoView");

        foreach (var item in inventoryHolder.inventory)
        {
            var itemGO = GameObject.Instantiate(itemViewPrefab, inventoryViewParent);
            itemGO.GetComponent<ItemView>().InitItem(item, CreateInfoView);
        }
    }

    private void CreateInfoView(ItemData data)
    {
        if (infoView != null)
        {
            Destroy(infoView);
        }

        infoView = GameObject.Instantiate(infoViewPrefab, infoViewParent);
        infoView.GetComponent<InfoView>().Init(data);
    }
}

Here, I've made a variable infoView, and when I try to make a new InfoView in the scene, first, I check if there is any.

Now time to give it a go.

Hit to play.

Alt Text

It seems we made it!

Project on github.

This was a gentle intro to MVC in unity with Scriptable Objects. But I believe it could be implemented like this way to any project. Especially, when receiving REST calls in unity, this pattern could be a life-saving pattern to keep your code organized and scaleable. In unity, it could be a challenge to maintain your objects and communicating them via code. Some may argue with the best solution could be Singletons. They're right, this is not the only way, but a good one.

I think it could be possible to live without design patterns as well, but I'm sure that would be no different from medieval :)

Anyway, since this series completed, I suggest you check my other series using MVC and Scriptable Objects as well: Making a REST service using Node and Express to use with Unity.

Happy coding unity ninjas!

Posted on by:

cemuka profile

Cem Ugur Karacam

@cemuka

Unity Developer. Loves to play rpg games. Interested in sci-fi books.

Discussion

pic
Editor guide