Demystifying Attributes, C#'s Under-Utilized Superpower for Game Development

May 18, 2020
protect

This post was originally published on my personal blog

Introduction

If you've used Unity, you've likely done something like this in your code:


public class MyClass : MonoBehaviour {
  [Range(0,10)]
  public int Test;
}

Specifically, you've put that [Range] thing above an int or float field in your class and, like magic, enjoyed that fact that in the inspector you now have a slider to set Test's value instead of needing to enter its value in a text field:

[Range] in the above code is an Attribute, a way of associating metadata with code. Unity offers a handful of other attributes (you'll likely run into [Header] and [ExecuteInEditMode]) that help you specifically work with Unity things in your code and aren't really generic and applicable more broadly to scripting work for your specific game.

Beyond the attributes mentioned above, it's honestly easy to forget they exist. I'd even argue that Unity's own tutorial's on the topic imply that attributes are specifically a Unity thing, not a C# thing, their functionality limited to only what Unity provides (one commenter even mentions "Please, more attributes!!!").

This specific misconception is what I aim to correct in this post, and shine a light on all the amazing, crazy things you (not just Unity!) can do with attributes.

Background

There's a specific gap when it comes to attributes that is similar to the gap that exists around learning game development at all. An aspiring game developer sees a game like Dark Souls and decides they want to make a game. So they open up a tutorial on getting started in game development and are able to... make a ball roll around. The path from Roll-a-Ball to Dark Souls is unclear at best and dispiriting at worst.

Attributes are similar. Looking at the official C# documentation, attributes are raised as a great way to... add author metadata to a method. Reading even what I've typed so far here and then jumping into the documentation could invoke whiplash — "Where is the radical potential here?"

But, like the developer looking for a way to Dark Souls from Roll-a-Ball, there is a path — you just need a little guidance and some imagination. Attributes are exciting, and let me be your guide to tell (and show!) you why.

Attributes

Attributes allow you to attach metadata to basically any code you write. This means an attribute could be attached to a class, field, method, etc. Unity's (and I mean this as a C# attribute Unity defines for us to use) [Range] attribute, for example, attaches to a float/int field while [ExecuteInEditMode] attaches to a class. This metadata (defined by either the parameters of the attribute of the attribute itself) can be retrieved at runtime via additional code and, as a byproduct, can give you access to the element they are applied to.

That last part is the key to what makes attributes special. Not only can you get a list of fields/classes/etc. where an attribute is applied, you're able to essentially mark your code in a way that lets you retrieve any element from it at any time, without needing a direct reference to the element. It's hard to talk about the usefulness of this without getting into specific examples, so let's get into it!

Custom Attributes

Unity's [Header], [Range], etc. are all custom attributes defined inside the Unity source code. C# itself does come with a fair amount of built-in attributes, but for the most part if you're working with attributes you'll be working with custom ones that you define.

Because we're talking about metadata, and because of the slightly strange syntax, it may be easy to think that custom attributes themselves are hard to define. However, the opposite it true! When you call something like [Header("My Header Text")], you're effectively calling the following code:


Header header = new Header("My Header Text");

You're just creating an object that is bound to the element the attribute is attached to. Creating a custom attribute is about as simple as the above code:


[System.AttributeUsage(System.AttributeTargets.Class |  System.AttributeTargets.Struct)]
public class CoolAttribute : System.Attribute {  
    public string Value;
    public CoolAttribute(string value)  
    {  
        Value = value;
    }  
}  

You extend System.Attribute, and use the attribute System.AttributeUsage to declare what your custom attribute can be applied to (for more options see here). In the above code I'm making it so that CoolAttribute accepts and stores a string parameter in its constructor.

After defining the attribute above, you are immediately able to use it like so:


[CoolAttribute("some value")]
public class CoolClass {}

Retrieving Attributes

When you declare something like [Header("Cool Header")] in a Monobehaviour, you see some text in the inspector that says the thing you typed in ("Cool Header", for example). It makes sense, but also.... what? How does that happen?

Part of the difficulty in learning attributes, especially for newcomers, is that to effectively use them, you need to know a bit about reflection. Reflection can be a strange concept — just saying it sounds like you're tapping into the secret meta world of code. This is true, but it's also misleading. All reflection does is get information about the code you wrote. This doesn't mean 1s and 0s. It means that, for example, imagine this line:


public GameObject SuperAwesomeObject;

Inside your code you may call SuperAwesomeObject.transform or whatever, but imagine you actually want to get the name "SuperAwesomeObject". How do you find that? Reflection! The actual variable name, SuperAwesomeObject is data stored inside your compiled code. Most of the time, the actual name of this variable is irrelevant — we only care about what it's referencing. However, you can use a similar method of retrieval to not grab the name of the variable, but to instead grab any attributes that are attached to it.

Imagine we had this class:


[CoolAttribute("foobar")]
public class CoolClass : MonoBehaviour {
  public int Number;
}

I want to get the attributes attached to CoolClass. I can do that like so:


// Get the "Type" of CoolClass as a variable by using typeof()
// Note: this does not create an instance of CoolClass. It instead gets its Type as a System.Type object.
var t = typeof(CoolClass); 

// After you have the Type information of a class, you can get the attributes attached to the class by calling code like this:
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t);
// Note here as well that I'm getting the attributes of the class, not its fields/methods.
// I show examples of getting attributes from those in later examples

// attrs is now an array filled with all the attributes attached to the class CoolClass.
// To retreive the information stored in that attribute, we can loop over all the attributes in attrs (because there may be more than just the one we attached), and find our custom attribute:  
foreach (System.Attribute attr in attrs) {  // loop through the attribute array
  if (attr is CoolAttribute) {  // if the attribute is the type we're looking for
    // We need to cast to our attribute type to the target type
    CoolAttribute a = (CoolAttribute)attr; 
    Debug.Log(a.Value); // Would log "foobar"
  }  
}

Working with attributes for more advance cases still starts with some version of the above code:

  1. Get a Type

  2. Get which parts of that type you want to look for attributes on (the class itself, methods, fields, structs, etc.)

  3. Use some version of System.Attribute.GetCustomAttributes() to fetch the attributes you want.

Knowing that, let's move on to more concrete use cases.

Attribute Use Cases

Value Replacement (Localization, Data Management, Modding, etc.)

It's often the case in game development, especially in localization, that you will want to replace some value with another value. Programming is generally good at this, but where this becomes complicated is that the ways those values are used, even if they all use the same source data, can vary greatly.

Traditional Method

Consider a string of text that you want to be localized to some language. To change that string for the proper language, you'd need to find any instance of that string. Not only that, but I'd need to find every version of that string — it could be hardcoded into a class, set in the inspector via TextMeshPro or legacy UI Text, set in some other asset's inspectors settings, set via a function, and so on. Being able to simply change this value then becomes a messy knot not only of tracking where all these references to the text are, but then trying to contextually call the proper function to change these strings.

Attributes don't make this problem go away, but they can make it far more manageable. Let's imagine we have some class that looks like this:


public class TextClass {
    public TextMeshProUGUI TMPAsset;
}

An attribute-less way to change the text used by

JikGuard.com, a high-tech security service provider focusing on game protection and anti-cheat, is committed to helping game companies solve the problem of cheats and hacks, and providing deeply integrated encryption protection solutions for games.

Read More>>