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.
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.
- Make the shape: the class that becomes an asset
[CreateAssetMenu]above the class and: ScriptableObjectafter 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 inAssets/Scripts/Card_data.cs— quoted in this lesson's Concept Lab and taken apart line by line in the code half below - 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
menuNamestring and the default file name is thefileNamestring, which is why a fresh asset arrives calledCard_dataand has to be renamed before it is useful.Read: the two assets that already ship inAssets/Cards/Card_Data/—Orc_Data.assetfilled in andUnicorn_data.assetblank in every field - 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 inAssets/Scripts/Card_data.cs - Put them in the deck: a list the Inspector edits
public List<Card_data> player_deckonGameManager— 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 theplayer_deckfield inAssets/Scripts/GameManager.cs - Carry a new field to the screen: two files, not oneA field invented on
Card_datais stored in the asset and goes no further.Card.Start()is what copiesdata.*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 ofStart()inAssets/Scripts/Card.cs, and steps 2 and 3 of the code half below - 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?
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 built | Edited by |
|---|---|---|
| Serialized fields on a component | the playerTransform reference wired in the Inspector (3.1) | Inspector, per scene object |
| Scene/prefab YAML | everything (9.4) | Unity (and you, by hand, once) |
| ScriptableObject assets | your cards | Inspector, 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.)
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.
-
Project window →
Assets/Cards/Card_Data/▸ double-click Blank_Card to open the prefab. This is the cardGameManager.Deal()clones for each hand, and its five text objects are Card_Name, Description, Health, Cost and Damage. - 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.
- 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.
-
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. - 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.
-
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
rarityTextfield 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.Every slot on Blank_Card, printed in the order its fields are declared.Rarity Textis not at the bottom of the list: Unity prints a field where its declaration sits, so aTextMeshProUGUIdeclared with the other TMP references lands betweenDamage TextandSprite 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. - 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.
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;
}- 3The import that matters here.
ScriptableObject,SpriteandCreateAssetMenuall live inUnityEngine, 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. - 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.
fileNameis the name a new asset is given,menuNameis the menu path,orderis where it sits in that menu.The menu path is not Unity guessing from your class name — it is themenuNamestring, character for character. That is also why the fresh asset is calledCard_data.asset:fileNameis a default, and Unity will not rename it for you. - 5The class declaration, with one addition to the shape you already know:
: ScriptableObjectnames 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: ScriptableObjectoff 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. - 7A field on the asset.
publicis 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.stringdecides the widget: a one-line text box. - 9An
int— a whole number — so the Inspector draws a number box. Types choose widgets:intandfloatget numbers,stringgets text,Spritegets a slot you drag a picture into. - 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. - 12A reference to a picture asset — not the picture itself.
Spriteis a Unity type, which is one more reason theusing 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. - 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 — openOrc_Data.assetandUnicorn_data.assetand 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;- 1Your field, in the class body beside
spriteand abovefontSize, with the samepublicand the same trailing semicolon as its neighbours. Save the file and click any card asset:rarityis 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;- 1In
Card.cs, with the other value fields near the top of the class.Cardkeeps its own copies of everythingCard_dataholds — that is whycard_name,healthanddamageappear in both files on purpose. - 2A second addition to the same class: a slot to hold the text object that will draw the value.
TextMeshProUGUIcomes from theusing TMPro;line at the top ofCard.cs— the same component type asnameTextandhealthText.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 andStart()throws the moment a card appears. - 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. - 4The copy, beside the six that are already there.
datais the pointer to the asset thatDeal()assigns when it creates a card, and this line reads the value out of the asset and into the card's own field.data.rarityandrarityare 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. Writingrarity = rarity;compiles, does nothing, and leaves the card showing whatever it had before. - 5The second anchor: the last of the
.textassignments already in the file. Your display line goes after it. - 6The line that puts it on screen, with the other
.textassignments.rarityis astring, so it goes in as it is — no conversion.Order is load-bearing. This line readsrarity, so it has to run AFTER the copy above it; put the copy at the end ofStart()instead and this line reads an empty string and draws a blank label with no error at all. And compare the types:healthis anintand needs.ToString()on its line,rarityis astringand 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()
}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.
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:
- ScriptableObjects — the full reference
- Lists & Arrays — the lists holding them
Saved on this device only — the Dashboard tracks your progress.