Have Unity Support Your Custom File! (Part 3/6)

Miijii
Miijii’s Unified Works
9 min readMar 4, 2023

Learn a feature of Unity you may have not heard of before that’ll boost your productivity.

A Script Template that creates a templated class that derives from SequenceEventScript.

This is perhaps a feature of Unity that you haven’t heard of. If you have, power to you king! If you haven’t, this is a perfect time to introduce you to them!

A Script Template is a file that defines a skeleton or structure of a file that Unity can generate. It’s a really neat feature for things like creating multiple Pawns and Controllers, or in the picture provided, a data model of a stage. There’s no need to repeatedly type a script deriving from another class. Script Templates gives your a level of reusablity whenever you’re desiging or programming your game.

I tried searching up “Script Templates” inside Unity’s Documentation. However, there doesn’t seem to be any results in the search query. I had learned of it through an few forums on it, making it a “Secret Feature”. Kind of defeats the purpose of it being a “Secret” since their’s forums of it, which would be nice if Unity included it with their documentation. Feel free to confirm if there’s actually no mention of Scripted Templates inside Unity’s documentation.

Otherwise, I’m here to say that creating a Script Template is actually very easy, and it’ll make your development workflow much simpler. Go ahead and create a folder called “ScriptTemplates” inside your Assets folder. (This is an important first step. The folder must be in the Assets folder, or Unity will not find it).

The ScriptTemplates folder must be inside the Assets Folder.

Inside your ScriptTemplates folder, go ahead and create a new Text file. When it comes to naming the text file, there’s a specific naming convention that you must follow:

  • ##-[Menu]-[Item].[Extension].txt
  • ##-[Menu]__[SubMenu]-[Item].[Extension].txt

The number (##) can be any number you want (I fact-checked this beforehand). This tells Unity where to place it inside the context menu. For example, having the number as 00 “00-CustomFile_New File.cfile.txt” will place your menu item at the very top of your context menu when you right-click in your Project tab.

A Script Template Item selected at the very top of the Context Menu.
Selecting the menu item gives us a “New File.cfile” 📁

I normally set the number around the 80s (usually starting with 81), so that it’s right next to the C# Script option. You can make a sub menu of your item by using double underscores. This gives you the ability to categorize your templated scripts. We went ahead and renamed our previous file to “00-Custom File__Blank-New File.cfile.txt”, and created another file named “00-Custom File__Scaffold-New Scaffold File.cfile.txt”

Now we have 2 different types of Custom Files to chose from. ✌️

Now, the most common use of Script Templates is to create C# scripts that derives from an abstract class or some base class. For example, you can have a template for creating multiple Pawn scripts for the player and the different types of enemies for your game. For that, you would name your script template something like “81-Pawn-New Pawn Script.cs.txt”

A new menu item for creating a Pawn. 👾

As our file names suggest, they are simply Text files. This is how they are at first. However, Unity will take this, and which ever extension comes before the .txt extension will be generated. In this case, we’ll be give a new .cs script.

The last component to Script Templates is (you guessed it), the text itself. The only thing that you need when structuring your template inside your text file is the #SCRIPTNAME# flag. The flag will be replaced with whatever you had named your newly created file.

Typing out our Script Template structure, and creating a new Pawn called PlayerPawn. 🎮

Here’s a quick tidbit; whenever you create a new Script Template (with all of its structure content), you have to close Unity and re-open it again. Only then will it acknowledge it as a Script Template, and it’ll be viewed on your Context Menu when you right-click in your Projects Tab. From there, you are free to restructure it’s content, and Unity will reflect that change immediately. However, if you rename the .txt file, you’ll need to close Unity again and reopen it. Slightly tedious, especially if you have a lot of content inside your project that would cause Unity to take long to open.

Congratulations! You’re now amongst the people who knows this little “secret feature” Now… What do we use this for? Well, if you have read the previous part to this series where we’ve gone over ScriptedObjects, we made an example where you could create a Scriptable Object of a Trading Card Game, and how you could instead a file format of a Card and generate it. Well, we can do that using ScriptedTemplates as well.

But first, we have to create that Digicard asset that we were talking about. I went ahead and provided a script that has everything we need in preparation for creating our Digicard file. Refactor this script as needed.

using System;
using System.IO;
using UnityEditor;
using UnityEngine;

public sealed class DigicardAsset : ScriptableObject
{
private int m_instanceID;
public int InstanceID
{
get
{
if (m_instanceID == 0)
m_instanceID = GetInstanceID();
return m_instanceID;
}
}
private int m_hashCode;
public int HashCode => InstanceID.GetHashCode();

// Imagine we have field and properties representing our file type
[HideInInspector] public string filePath;

// Exposible Field
[SerializeField] private string cardName;
[SerializeField] private CardType cardType;
[SerializeField] private CardProps[] cardProperties;
[SerializeField] private Sprite cardCoverArt;
[SerializeField] private string cardDescription;

private DigicardData data = new();

private void OnEnable()
{
cardName = name;
}

public override int GetHashCode()
{
return HashCode + data.GetHashCode();
}

public void Save()
{
GenerateNewCard();

var jsonString = JsonUtility.ToJson(data, true);

if (filePath == null)
{
Debug.LogError("Failed to save Digicard");
return;
}

using StreamWriter streamWriter = new(filePath);
streamWriter.Write(jsonString);
}

private void GenerateNewCard()
{
// Only run this if our data
// object is void. This prevents
// unnecessary memory allocation.
data ??= new DigicardData()
{
name = cardName,
type = cardType,
properties = cardProperties,
description = cardDescription,
coverArtPath = AssetDatabase.GetAssetPath(cardCoverArt)
};

// We then reuse our data, and change the field
// accordingly.
data.name = cardName;
data.type = cardType;
data.properties = cardProperties;
data.description = cardDescription;
data.coverArtPath = AssetDatabase.GetAssetPath(cardCoverArt);
}

public void Load()
{
using StreamReader streamReader = new StreamReader(filePath);
var jsonString = streamReader.ReadToEnd();
data = JsonUtility.FromJson<DigicardData>(jsonString);

cardName = data.name;
cardType = data.type;
cardProperties = data.properties;
cardDescription = data.description;

if (data.coverArtPath == string.Empty) return;
RequestingImage(data.coverArtPath);
}

private bool RequestingImage(string path)
{
Sprite image = ConvertTextureToSprite(LoadTexture(path), 100f, SpriteMeshType.Tight);

cardCoverArt = image;

return true;
}

private Sprite ConvertTextureToSprite(Texture2D _texture, float _pixelPerUnit = 100.0f, SpriteMeshType _spriteType = SpriteMeshType.Tight)
{

Sprite newSprite = null;
//Converts a Texture2D to a sprite, assign this texture to a new sprite and return its reference
newSprite = Sprite.Create(_texture, new Rect(0, 0, _texture.width, _texture.height), new Vector2(0, 0), _pixelPerUnit, 0, _spriteType);

return newSprite;
}

private Texture2D LoadTexture(string _filePath)
{
/*We'll load a png or jpg file from disk to a Texture2D
If we fail at doing so, return null.*/

Texture2D tex2D;

//We want to read the binary data of this file,
//in order to know the formatting. With the formatting,
//we'll use the data to create the image that we requested
byte[] fileData;

if (File.Exists(_filePath))
{
fileData = File.ReadAllBytes(_filePath);
tex2D = new Texture2D(2, 2);
if (tex2D.LoadImage(fileData))
return tex2D;
}

return null;
}
}

public enum CardType
{
Normal,
Spell,
Summons
}

[Serializable]
public sealed class DigicardData
{
public string name;
public CardType type;
public CardProps[] properties;
public string description;
public string coverArtPath;
}

[Serializable]
public sealed class CardProps
{
public string name;
public int value;
public string description;
}

If you went ahead and created the Digicard.cs, you should be able to right-click, and got Create>Digicard. Go ahead and populate the Scriptable Object with data. Also feel free to change anything within the script to make it more accurate to what an actual card would represent.

Creating our Digicard called “NormalCard”

As stated before, if we were just using a Scriptable Object, this would be fine. However, we want to be able to create a .digicard file instead. The first problem that we encounter is that our Digicard Asset’s file name has the “.asset” extension. This is normal. Scriptable Objects use the extension “.asset” by default. We can remedy this by creating a Script Template.

Go ahead and create a text file. Name it:

  • “81-Digicard-New Digicard.digicard.txt”

Be sure that you remove the CreateAssetMenu attribute from our DigicardAsset class. Inside your new Script Template, insert the follow JSON format:

{
"name": "#SCRIPTNAME#",
"type": 0,
"properties": [
{
"name": "Health",
"value": 10,
"description": "Health"
},
{
"name": "Mana",
"value": 10,
"description": "Mana"
}
],
"description": "New Card #SCRIPTNAME#",
"coverArtPath": ""
}

To test that our Script Template works, be sure you close Unity and re-open it. You should be able to see you Digicard Option again (though it may be located at a different location). Click on it, and name it whatever you want. Once you’ve created your new Digicard file, click on it, and make sure the #SCRIPTNAME# is replaced with your file name.

Note: If your new file type shows a blank file, be sure to right-click, and select “Show In Explorer”, and in the File Explorer, right-click on your created file again, and select “Open-With”. Select any text editor you want.

Creating a Digicard file called “EmpireStride” (cool name. I know.)

Wow! That was a lot of steps. If you made it this far, I’m proud of you! Power to you king!

Now, you may have noticed our Save() and Load() methods inside our DigicardAsset class. More particularly our Save() method. “Couldn’t we just have generated our JSON file using the Save() method?” The answer to that my friend is “Yes!” We can use the method to generate our JSON file instead of typing it in manually.

However, how are we going to call our Save() method? How will we know the “filePath” that we want to write to? Even if you’ve answered these questions, “How will we be able to write to our .digicard file using our DigicardAsset Scriptable Object (with the extension of .asset)”. There’s a very interesting trick were we can kind of “fuse” our DigicardAsset object to our file type, and it all has to do with ScriptedImporters. This is where all the magic happens, and how we can tell Unity that “you’ll accept my Custom File Format, and you’re going to like it!” We’ll learn what ScriptedImporters are in Part 4 of our series.

This ended up being a very long chapter in our Custom Files journey. Hopefully, I was able to provide you with enough information to understand the capabilities that Script Templates have. If you decide to leave off from here, I hope this knowledge helps you improve your development workflow. You can do so many things with Script Templates as much as you can with Scriptable Objects.

If you like what you read, be sure to like this, and leave a comment if you have any questions. It would also help if you become a follower, so that you can be provided with more Unity and Game Development content. With that said, コーディング諦めないぞ!Don't Stop Coding! I hope you have a great rest of your day!

Click here for Part 4: Scripted Importers!

--

--

Miijii
Miijii’s Unified Works

Miijii (Pen-Name) | Game Developer | Developing XVNML, XVNML2U, and Tomakunihahaji