◀ 10.2a10.2b10.2c ▶

10.2b — ScriptableObjects: Card Data as Assets

Yesterday your deck became a List<Card_data>. Today you find out what a Card_data actually is: not a class you create in code, but a FILE — an asset you create in the Project window, fill in with the mouse, and drag into that list. That is the difference between adding a card by editing a script and adding a card by clicking. Your project has two of these assets already, and by the end of today it will have fifteen.

In this lesson you will be able to answer“how do I make game data into something I can drag?”Full walkthrough
Break It Down~10 minFull walkthrough

Your deck is a List<Card_data>, and so far the only cards in it are the two the template shipped with. Make the CARD itself an asset — a file you create in the Project window, fill in with the mouse, and drag into that list — so that adding a card is a click instead of a code change. As one problem it is not startable: "make game data draggable" names neither the class that decides what a card IS, nor the list that receives it, nor the second file a new field has to cross before anything can show it.

First, before any code: cut that into parts small enough that you could look each one up on its own.

  1. Make the shape: the class that becomes an asset[CreateAssetMenu] above the class and : ScriptableObject after its name — the two lines that turn a plain C# class into something Unity can save as a file, create from a menu, and edit in the Inspector. Every field you declare on the class becomes one editable row on every card asset in the project. It ships with this template, so this part is reading and extending one existing file, never writing a second class of the same name.Read: the [CreateAssetMenu] line and the class declaration in Assets/Scripts/Card_data.cs — quoted in this lesson's Concept Lab and taken apart line by line in the code half below
  2. Make the files: one asset per cardRight-click in the Project window ▸ Create ▸ Cards ▸ Card_data, rename the file, repeat. The menu path is the menuName string and the default file name is the fileName string, which is why a fresh asset arrives called Card_data and has to be renamed before it is useful.Read: the two assets that already ship in Assets/Cards/Card_Data/ — Orc_Data.asset filled in and Unicorn_data.asset blank in every field
  3. Fill the rows: the card, not the codeSix value fields edited with the mouse — name, description, health, cost, damage, sprite — plus one display setting, fontSize. No script is opened to author a card, which is the entire point of the exercise: the person designing the card and the person writing the code are allowed to be different people.Read: the Inspector rows on any card asset, and the field list in Assets/Scripts/Card_data.cs
  4. Put them in the deck: a list the Inspector editspublic List<Card_data> player_deck on GameManager — already declared, already exposed in the Inspector, filled slot by slot. Duplicate entries are legal, so three copies of your best card costs three slots and one asset file.Read: the six Lists you counted in 10.2a, and the player_deck field in Assets/Scripts/GameManager.cs
  5. Carry a new field to the screen: two files, not oneA field invented on Card_data is stored in the asset and goes no further. Card.Start() is what copies data.* onto the component, and something has to draw the value — so a new field means a line in each of two files and a text object in the prefab.Read: the top of Start() in Assets/Scripts/Card.cs, and steps 2 and 3 of the code half below
  6. Prove it: add a card with no codeDone when parts one through four ran with no script opened. The timed version of this test is Build Lab step 4 below.Read: the two-minute test at the end of this lesson's Build Lab

Five of these six parts are the same gesture — make a thing in the Project window, then drag it into a slot — and that gesture is the whole reason ScriptableObjects exist: data a designer can author without a programmer. The split matters because the halves fail differently. Parts one to four need no code at all, so a card with a name, a number and a picture is finished without opening a script. Part five is the only one that costs a programmer, and it only appears when you invent a field the class does not already have.

The test for a finished split: can you point at the file or the Inspector row the part lives in? "Make the data draggable" points at nothing. "The [CreateAssetMenu] line" points at one line of one file, and that is a part.

Your turn

Orc_Data.asset is filled in and Unicorn_data.asset is blank in every field. What is the blank one for — and what does keeping it in the project buy you?

Three copies of your best card in the deck is one asset file referenced three times. If a card is discarded at runtime, what did you actually remove — and what would deleting the asset file itself do differently?

The card's name is read out of the asset, but the chocolate background is written into Card.cs as a test on the string "Orc". Which of those two belongs in the data, and what would you have to change to make a second card chocolate?

Concept Lab ~15 min

The ScriptableObject, defined. It is a data class that becomes a FILE in your project (an asset, like a sprite). The template already ships one — open Assets/Scripts/Card_data.cs and read it:

[CreateAssetMenu(fileName = "Card_data", menuName = "Cards/Card_data", order = 1)]
public class Card_data : ScriptableObject
{
    public string card_name;
    public string description;
    public int health;
    public int cost;
    public int damage;
    public Sprite sprite;
    public int fontSize = 36;
}

Each field is one row of a card, and each ASSET you create from this class is one card, editable in the Inspector. Right-click in Assets → Create → Cards → Card_data and an .asset file appears. Open that file and you get fields, not code: designers edit these, and programmers build the shape once. The code half below takes every line of the class apart.

Your project already has two cards, and together they show you both ends of the pipeline. Open Assets/Cards/Card_Data/ and compare them side by side. Orc_Data.asset is filled in: a name, a description, health, cost, damage and a sprite. Unicorn_data.asset is empty in every single field — a card that was created and never written. Same class, same Inspector, two very different amounts of work.

Now find the last thing in Assets/Scripts/Card.cs:

if (card_name == "Orc")
{
    background.color = Color.chocolate;
}

That is where an asset meets the code. The card's name is not a string somebody typed into a script — it is data.card_name, copied onto the card by Start(), and "Orc" is exactly what the Orc asset says. Rename that asset in the Inspector and this branch stops running. Data you can edit with the mouse just decided what the code does.

Compare your three data homes so far:

Data lives in…Example you builtEdited by
Serialized fields on a componentthe playerTransform reference wired in the Inspector (3.1)Inspector, per scene object
Scene/prefab YAMLeverything (9.4)Unity (and you, by hand, once)
ScriptableObject assetsyour cardsInspector, in the PROJECT, reusable everywhere

Socratic

The two deck Lists hold Card_data assets; the four hand and discard Lists hold Card objects. When you drag the SAME card asset into three of those Lists, how many copies of the card exist in memory — and why is "reference, not copy" exactly what 9.4 guids do for sprites? (One card asset, many references. The exam's word is abstraction — again.)

In the Editor ~10 min

A new field can be declared in both files, stored in an asset and copied onto the card, and still be invisible — because nothing on the prefab is drawing it. This half is the object that draws it, and there is no spare one: every text object on the card is already wired to a line in the code half.

  1. Project window → Assets/Cards/Card_Data/ ▸ double-click Blank_Card to open the prefab. This is the card GameManager.Deal() clones for each hand, and its five text objects are Card_Name, Description, Health, Cost and Damage.
  2. Hierarchy → the Damage object ▸ Ctrl+D (Cmd+D on a Mac) to duplicate it. Duplicating in place keeps the TextMeshPro font, size and anchors the card is laid out with, which is faster and more consistent than adding a fresh text object from the GameObject ▸ UI menu and setting all three by hand.
  3. Inspector → rename the copy Rarity. It arrives still called Damage, and a Hierarchy holding two objects of the same name is a Hierarchy you cannot fill a slot from correctly.
  4. Inspector → the copy's TextMeshPro component ▸ Text field, which still holds the placeholder New Text. Clear it: five labels reading the same word are five labels you cannot tell apart while you are placing one of them.
  5. Scene view → drag Rarity to where the value belongs on the card, clear of the other four labels. Two values drawn on top of each other is a card that looks broken with nothing wrong in the code.
  6. Once the code half below is done — Inspector → the Card component on the prefab root ▸ Rarity Text, then drag the Rarity object in from the Hierarchy. That slot is the rarityText field you declare below, and it is the same drag-into-a-slot gesture as the sprite field and the deck list. Drag the wrong object in — the original Damage label — and two lines write to the same label, so whichever runs last wins and one of the two values quietly disappears.
    Inspector showing Blank_CardInspectorBlank_CardRectTransformCard (Script)DataNone (Card Data)Card NameDescriptionHealth0Cost0Damage0SpriteNone (Sprite)Name TextCard_NameDescription TextDescriptionHealth TextHealthCost TextCostDamage TextDamage6Rarity TextRaritySprite ImageSpriteBackgroundBackgroundBox Collider 2DDraggable UIAdd Component
    Every slot on Blank_Card, printed in the order its fields are declared. Rarity Text is not at the bottom of the list: Unity prints a field where its declaration sits, so a TextMeshProUGUI declared with the other TMP references lands between Damage Text and Sprite Image — declare it somewhere else and the slot moves with it. The six value fields above the text slots are empty on the prefab because the asset fills them.
  7. Game view → press Play. The card draws the value you typed into a file. Not one of the four lines you added was needed to author a card — they were needed to add a field, once, for every card in the game.
In the CodeFull walkthrough

The question this answers“how do I make game data into something I can drag?”

You are editing Assets/Scripts/Card_data.cs and Assets/Scripts/Card.cs. Card_data.cs is the file that makes card data into an asset, and Card.cs is the file that puts it on screen. Both already exist in this project — you are extending them. Five new lines are yours to type, one in Card_data.cs and four in Card.cs, which is what a new field costs; the text object that draws the value is the editor half above, and everything else here is to read.

Step 1Read the class that makes data into an asset

This is the whole of Assets/Scripts/Card_data.cs. Read it here before you touch it — you cannot extend a file you have not read, and every line below is load-bearing.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Card_data", menuName = "Cards/Card_data", order = 1)]
public class Card_data : ScriptableObject
{
    public string card_name;
    public string description;
    public int health;
    public int cost;
    public int damage;
    public Sprite sprite;
    public int fontSize = 36;
}
  1. 3The import that matters here. ScriptableObject, Sprite and CreateAssetMenu all live in UnityEngine, so without this line every one of them is an unknown name.Delete this line and you get three errors at once, all of them on lines you did not touch. When a file suddenly cannot find types it was finding a second ago, read the top of the file before you read anything else.
  2. 4An attribute — a note to the Unity editor, not to C#. This single line is the whole difference between a data class and an asset: it adds a Create ▸ Cards ▸ Card_data entry to the Project window's right-click menu. fileName is the name a new asset is given, menuName is the menu path, order is where it sits in that menu.The menu path is not Unity guessing from your class name — it is the menuName string, character for character. That is also why the fresh asset is called Card_data.asset: fileName is a default, and Unity will not rename it for you.
  3. 5The class declaration, with one addition to the shape you already know: : ScriptableObject names the class it inherits from. That base class is what lets Unity save an instance of this as a FILE in your project instead of as an object in a scene.Leave : ScriptableObject off and the file still compiles — you have written a perfectly good plain C# class that Unity refuses to save, cannot create from a menu, and will never show in the Inspector. No error, no asset, nothing to debug. This is the line the whole lesson is about.
  4. 7A field on the asset. public is doing real work — Unity only serializes public fields (or private ones marked [SerializeField]), so a field without it is invisible in the Inspector and silently drops whatever you type. string decides the widget: a one-line text box.
  5. 9An int — a whole number — so the Inspector draws a number box. Types choose widgets: int and float get numbers, string gets text, Sprite gets a slot you drag a picture into.
  6. 10One more number, one more box. Notice there is no value after the = — both ints start at 0, which is why a freshly created asset looks like an unfinished card rather than a broken one.
  7. 12A reference to a picture asset — not the picture itself. Sprite is a Unity type, which is one more reason the using UnityEngine; line is not optional. In the Inspector this is a slot you drag a sprite file into.This is the same idea as the deck list and the same idea as 9.4's guids: the card stores a pointer to the art, so twenty cards can share one picture and swapping the art is a drag rather than twenty edits.
  8. 13The only field in the file with a value written after the = — so it is the value a brand new card starts from instead of 0. It is also the one field here that is not a card value at all: Card.Start() reads it to set the size of the name text.A field initialiser is not decoration. Every card asset created from this class gets 36 as its starting point, and an asset made before this line existed simply has no line for it in its file — open Orc_Data.asset and Unicorn_data.asset and you can see that the two files do not even carry the same set of lines, while both show the same rows in the Inspector. The ROWS come from the class; the values come from the asset.

Step 2Add your own field to the class

One line, typed into the file that is already open. Not a new file, and not a new class: a second class Card_data anywhere in the project is a CS0101 duplicate-definition error, and Unity will not tell you which of the two it is complaining about.

    public string rarity;
  1. 1Your field, in the class body beside sprite and above fontSize, with the same public and the same trailing semicolon as its neighbours. Save the file and click any card asset: rarity is already a row in the Inspector.The row appears on assets that existed before you typed the line, which is why the two shipped cards are worth opening again now. The asset can hold the value from this moment on — but nothing on screen has changed yet, and that is the next step.

Step 3Give the field somewhere to land in Card.cs

The asset stores a value. Assets/Scripts/Card.cs is what copies values out of an asset and onto a card the player can see, so your field needs a line in each of the two places where the other six appear.

    public string rarity;
    public TextMeshProUGUI rarityText;
        sprite = data.sprite;
        rarity = data.rarity;
        damageText.text = damage.ToString();
        rarityText.text = rarity;
  1. 1In Card.cs, with the other value fields near the top of the class. Card keeps its own copies of everything Card_data holds — that is why card_name, health and damage appear in both files on purpose.
  2. 2A second addition to the same class: a slot to hold the text object that will draw the value. TextMeshProUGUI comes from the using TMPro; line at the top of Card.cs — the same component type as nameText and healthText.A value with nothing to draw it is invisible no matter how correct the copy is, and Unity's UI has no way to display a field you never gave it an object for. Leave this slot empty and Start() throws the moment a card appears.
  3. 3An anchor, not a line you add: this is the last of the six copies already inside Start(). Your copy goes on the next line, directly under it.
  4. 4The copy, beside the six that are already there. data is the pointer to the asset that Deal() assigns when it creates a card, and this line reads the value out of the asset and into the card's own field.data.rarity and rarity are two different things and both halves are needed: the left one is the copy on the component, the right one is the value stored in the asset. Writing rarity = rarity; compiles, does nothing, and leaves the card showing whatever it had before.
  5. 5The second anchor: the last of the .text assignments already in the file. Your display line goes after it.
  6. 6The line that puts it on screen, with the other .text assignments. rarity is a string, so it goes in as it is — no conversion.Order is load-bearing. This line reads rarity, so it has to run AFTER the copy above it; put the copy at the end of Start() instead and this line reads an empty string and draws a blank label with no error at all. And compare the types: health is an int and needs .ToString() on its line, rarity is a string and does not.
Check your work: the finished filebuild it as you read — then open this to check every line

Compare against your own two files and your prefab. The three things to check hardest: that rarity exists in BOTH files, that the copy line is rarity = data.rarity; and not rarity = rarity;, and that the Rarity Text slot on the Card component is not empty.

// ------------------------------------------------------------------
//  REFERENCE FILE — Assets/Scripts/Card_data.cs and Assets/Scripts/Card.cs
//  Written to be read, not pasted. Build it with the walkthrough
//  above, one step at a time, then open this and compare.
//  If it is sitting in your project unchanged, that is the thing
//  to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
// Assets/Scripts/Card_data.cs — the whole file, as it stands after this lesson.
// It SHIPS with the project. The one line marked " + " is yours; the rest was
// already here and is not retyped.

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

[CreateAssetMenu(fileName = "Card_data", menuName = "Cards/Card_data", order = 1)]
public class Card_data : ScriptableObject
{
    public string card_name;
    public string description;
    public int health;
    public int cost;
    public int damage;
    public Sprite sprite;
    public int fontSize = 36;
    public string rarity;                 // + added by you
}

// Assets/Scripts/Card.cs — additions only, and only the parts that changed.
// The other fifty lines of the file are already there. Do not retype them.

    public string rarity;                 // + with the other value copies
    public TextMeshProUGUI rarityText;    // + with the other TMP references

    void Start()
    {
        card_name = data.card_name;
        description = data.description;
        health = data.health;
        cost = data.cost;
        damage = data.damage;
        sprite = data.sprite;
        rarity = data.rarity;             // + beside the other six copies
        nameText.text = card_name;
        nameText.fontSize = data.fontSize;
        descriptionText.text = description;
        healthText.text = health.ToString();
        costText.text = cost.ToString();
        damageText.text = damage.ToString();
        spriteImage.sprite = sprite;
        rarityText.text = rarity;         // + a string, so no .ToString()
    }
Build Lab ~30 min

Step 1 — the code half above is this step's work. If you have not done it yet, do it now: the rest of this lab assumes a card can show a field you invented.

Step 2 — author five cards. In the Project window, right-click → Create → Cards → Card_data. The new file arrives called Card_data, so rename it to the card before you do anything else or your project fills up with Card_data 1, Card_data 2 and no way to tell them apart in a deck list. Fill each one in the Inspector, keep them together in Assets/Cards/Card_Data/ beside the two that ship, and give each card a number that makes it different from the others — a card that costs the same and hits the same as every other card is not a card, it is the same card wearing a different name. Five is today; a jam deck wants fifteen to twenty of them.

Step 3 — put them in the deck. No new field, and no code. GameManager.cs already declares the list the Inspector edits:

public List<Card_data> player_deck = new List<Card_data>();

Hierarchy → select Dealer — the object carrying GameManager — and player_deck is waiting: an element count, then one slot per card. Drag your assets in. Duplicate entries are legal — three copies of your best card is a design decision that costs three slots and one asset file. Read the slots that are already there before you fill them: a slot showing None is a null reference sitting in the list, and Deal() reads player_deck[0] and hands whatever it finds straight to Card.Start(), which reads data.card_name on the first frame. An empty slot is not a harmless one.

Then notice how little Deal() does with all that work: it instantiates one card per side and reads index [0]. A deck of twenty and a deck of one look identical on screen today — you are stocking a list that the shuffle in 10.2c is going to need before it can mean anything.

And read one more line while you are in there. The last line of Deal() turns DraggableUI off on the player's card and leaves it on for the AI's. The drag behaviour ships with the project, and the scene ships the Canvas and EventSystem that UI dragging needs. Which card gets to be dragged, though, is a decision somebody made — read the line and decide whether it is a bug or a rule, and say which one you would have written.

Step 4 — prove the pipeline. Add a card WITHOUT touching code: create, fill, drag into the deck, play. If that took under two minutes, your content pipeline works and the rest of the jam is content rather than plumbing. The proof is not that a new card exists — it is that you never opened a script to make it.

Close of Day ~5 min

Checkpoint — before moving on, you must be able to:

  • Explain a ScriptableObject vs a component vs a prefab
  • Create, edit, and reference card assets in a list
  • Explain why a new field needs a line in both Card_data and Card
  • Explain references vs copies when one asset sits in three lists
  • Add a card with zero code changes (the pipeline test)

Reference Docs

Need a deeper explanation? These pages cover the same concepts:

Next: 10.2c — Decks: Shuffle, Draw & Traverse

0/4 answered

Answers save on this device as you type. At the end of class, click Download and upload the file to the Canvas assignment.

Saved on this device only — the Dashboard tracks your progress.