Stop Copy-Pasting the Same Code: The Unity Generics Tutorial That Changed How I Build Games
Here's the thing—I spent a good afternoon at CMU writing three nearly identical inventory systems: one for weapons, one for potions, and one for armor. Each had the same add, remove, and sort logic. Just different item types. By the third copy-paste, I knew something was wrong. That's when my professor introduced me to C# generics in Unity, and honestly, it felt like discovering a cheat code for programming.
Generics solve the major problem of code duplication by allowing you to write flexible, reusable code that can work with any data type. Instead of writing separate Inventory classes for Swords, Potions, and Armor, you write a single Inventory class that works for all of them. This Unity generics tutorial will show you exactly how to do that, saving you significant time and reducing the chance of errors in your projects.
The Vending Machine Analogy That Makes Generics Click
Let me give you the simplest explanation I've found. Think of a generic component as a vending machine. You don't need a separate machine for Coke, Pepsi, and water. You have one machine (the generic class) with slots. You can stock those slots with any type of drink (the specific type, like Potion or EnemyData). The machine's logic for accepting money and dispensing a product is the same regardless of what's inside, making it incredibly efficient and reusable.
This is exactly what generics do in your code. You create powerful, type-safe systems like object pools, state machines, event managers, and data containers that are not tied to a specific class. This means you can build a robust Unity object pooling system once and use it for bullets, enemies, and particle effects without changing a single line of its code.
Breaking Down the Jargon: What These Angle Brackets Actually Mean
Before we dive into code, let me explain the terms you'll see everywhere in this Unity generics tutorial:
- Generics: A feature of C# that allows you to define classes, methods, and interfaces with a placeholder for a data type. This placeholder is called a "type parameter."
- Type Parameter (
<T>): The placeholder for the type, denoted by angle brackets. By convention,Tis used, but it can be any name (e.g.,<TItem>,<TState>). This is the part you replace with a concrete type likeint,string, orPlayer. - Generic Class: A class defined with a type parameter, such as
public class MyList<T> { ... }. You cannot attach a genericMonoBehaviourdirectly to a GameObject in the Inspector. - Generic Method: A method defined with its own type parameter, like
public T GetItem<T>() { ... }. These are commonly used for factory methods or utility functions. - Type Constraint (
where T : ...): A rule you apply to a type parameter to limit what types can be used with it. For example,where T : Componentmeans thatTmust be a Unity Component, giving you access to things liketransformandgameObject. default(T): A keyword that returns the default value for a given typeT. For reference types (like classes), it'snull; for numeric types, it's0; forbool, it'sfalse.
Your First Generic Class: Building Something You Can Actually Use
Let me show you how I approach defining a generic class. You add <T> after the class name to make it generic. This T can then be used as a type for variables, method parameters, and return values within the class.
// A generic list that can hold items of any type T.
public class GenericList<T>
{
private T[] items;
public void Add(T newItem)
{
// ... logic to add the item ...
}
}
Source: Microsoft Docs - Generic Classes
When you create an instance of a generic class, you must provide a specific, concrete type in the angle brackets. Here's exactly how I do it:
// Creates a list that can only hold integers.
GenericList<int> numberList = new GenericList<int>();
numberList.Add(5);
// Creates a list that can only hold GameObjects.
GenericList<GameObject> objectList = new GenericList<GameObject>();
The beauty here is that you write the GenericList logic once, and it works for any type. No copy-pasting, no maintenance headaches.
When Unity Says "No" to Generic MonoBehaviours
Here's something that tripped me up early on. Unity's serialization system does not support generic MonoBehaviour classes. You cannot write public class MyGenericMonoBehaviour<T> : MonoBehaviour and drag it onto a GameObject. The solution is to use a non-generic base class and concrete subclasses.
// This will NOT show up in the "Add Component" menu.
public class GenericComponent<T> : MonoBehaviour where T : class
{
public T data;
}
This limitation exists because Unity needs to serialize your components to save them in scenes and prefabs. The Unity component system can't serialize a generic type that isn't specified at edit time. I'll show you the workaround I use in every project in just a bit.
Type Constraints: Teaching Your Generic Code What It Can Do
Constraints are essential for making generic code useful in Unity. By constraining T to be a Component, you can use Unity's component-based functions on it. These Unity type constraints give your generic classes real power.
// This method will only accept types that are Unity Components.
public T FindComponentOnObject<T>(GameObject target) where T : Component
{
T component = target.GetComponent<T>();
return component;
}
Source: Microsoft Docs - Constraints on Type Parameters
Without the constraint, you couldn't call GetComponent<T>() because the compiler wouldn't know that T has the properties of a Component. The constraint tells the compiler: "Trust me, T will always be a Component, so allow me to use Component methods on it."
Generic Classes vs. Inheritance: When I Use Each Approach
After working on multiple Unity projects, I've developed clear guidelines for when to use each pattern. Here's the comparison that guides my decisions:
| Criteria | Approach A: Generic Classes (Non-MonoBehaviour) | Approach B: Inheritance (Base Class Pattern) |
|---|---|---|
| Best For | Pure C# systems that manage data or logic behind the scenes, such as inventories, event systems, or object pools. | Creating Unity reusable components where the core logic is the same but the specific data type changes (e.g., a stat system for Health, Mana, Stamina). |
| Performance | Very high performance. The C# compiler generates specialized classes for each type at compile time, resulting in no runtime overhead. | High performance. Involves a small virtual function call overhead if you override methods, but this is generally negligible. |
| Complexity | Simple for backend systems. Can be tricky to integrate with the Inspector since you can't serialize generic fields directly. | More boilerplate code is required (you need a base class and a concrete subclass for each type), but it integrates perfectly with the Unity editor. |
| Code Example | public class ObjectPool<T> { ... } |
public abstract class Stat : MonoBehaviour { ... } public class Health : Stat { ... } |
I default to generic classes C# for pure data structures and backend systems. I use inheritance when I need Inspector integration and Unity-specific lifecycle methods.
Why This Changed Everything About How I Write Unity Code
Let me tell you why mastering generics matters more than you think:
- Drastically Reduced Code Duplication: It allows you to follow the "Don't Repeat Yourself" (DRY) principle (a guideline that says you should avoid duplicating code), which is a cornerstone of professional software development.
- Type Safety: Generics provide compile-time type checking. If you have an
Inventory<Potion>, the compiler will give you an error if you try to add aSwordto it, catching bugs before you even run the game. - Increased Reusability: You can write a powerful, generic system once and reuse it across multiple projects. A Unity state machine or Unity object pooling system is a tool you can carry with you for years.
- Cleaner and More Maintainable Code: With less duplicated code, your projects become smaller, easier to read, and much simpler to update or debug. A change to your generic
ObjectPool<T>class instantly applies to all pools in your game.
From my time at CMU through my years at KIXEYE, these patterns have saved me countless hours and made my codebases exponentially more maintainable.
The MonoBehaviour Workaround I Use in Every Project
To work around Unity's serialization limits for Unity generic MonoBehaviour scripts, I create a concrete subclass for each type I need to use in the Inspector. This is the exact method I use:
// The generic base class with all the logic.
public abstract class GenericPool<T> : MonoBehaviour where T : Component
{
// ... pool logic using type T ...
}
// The concrete, non-generic subclass you attach in the Inspector.
public class BulletPool : GenericPool<Bullet> { }
The generic base class holds all your logic. The concrete subclass is just an empty shell that specifies the type. You attach the concrete class to GameObjects in your scene, and it inherits all the functionality from the generic base. This pattern lets you get the benefits of reusable code Unity while still working within Unity's limitations.
Building a Generic Singleton Pattern That Works Everywhere
A generic singleton is a powerful pattern for creating a single, globally accessible manager for any class type. I've configured this dozens of times:
public class Singleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
// ... singleton logic to find or create the instance ...
return _instance;
}
}
}
This single piece of code can turn any manager class into a singleton. Just inherit from Singleton<YourManagerClass> and you instantly get global access without rewriting the singleton logic every time. I use this pattern for GameManager, AudioManager, UIManager—basically any manager that should only exist once in a scene.
The Value Type Constraint That Prevents Boxing
If your generic class should only work with value types like int, float, or custom structs, use the struct constraint for clarity and to prevent boxing (the process of converting a value type to a reference type, which creates performance overhead).
public class ValueWrapper<T> where T : struct
{
public T value;
}
This constraint ensures that T is always a value type. It prevents someone from accidentally using ValueWrapper<string> (since string is a reference type), and it helps the compiler optimize your code better. For game development, this matters when you're dealing with large amounts of data like positions, rotations, or custom stat structures.
How Vampire Survivors Manages Dozens of Items with One System
I've seen this technique used brilliantly in Vampire Survivors. The player collects dozens of different weapons and passive items. The game needs to track the level and stats for each of these unique items.
The Implementation: The developers could use a generic StatTracker<T> class. They could then have instances like StatTracker<FireWand> and StatTracker<Garlic> to manage the specific data for each item, while the core logic for leveling up and applying stat boosts is shared.
The Player Experience: The game supports a massive variety of items that all behave according to a consistent set of rules. This consistency is a direct result of a reusable, data-driven backend, likely powered by generics. What I find fascinating about this approach is how it scales—adding a new weapon doesn't require rewriting any core systems.
Diablo IV's Inventory: A Masterclass in Generic Type Safety
One of my favorite implementations of generics is in Diablo IV. The player has an inventory that can hold various types of items: equipment, consumables, and quest items.
The Implementation: A generic Inventory<T> system is a perfect fit here. The main inventory might be an Inventory<Item>, but special containers could be more specific, like a GemPouch being an Inventory<Gem> that uses the same core logic but is constrained to only accept Gem types.
The Player Experience: The inventory system is robust and intuitive. Players can easily manage different categories of items, and the system prevents invalid actions (like trying to equip a potion), which is a key benefit of generic type safety. After analyzing dozens of games, this stands out because it allows for complex item management without fragile code.
Stardew Valley's Storage System Architecture
Let me tell you about how Stardew Valley solved this exact problem. The game features chests, which are containers that can store any type of item in the game, from seeds to fish to minerals.
The Implementation: A chest is a real-world example of a generic component. The developer could have a single Chest<T> class, and for the standard wooden chest, it would be instantiated as Chest<Item>. This allows the same code to handle storing, retrieving, and stacking for any item type imaginable.
The Player Experience: Players can organize their farm and belongings with a reliable and flexible storage system. The consistency of how chests work, regardless of their contents, makes the game feel polished and bug-free. This is why I always recommend studying this game's approach to data management.
Blueprint 1: A Reusable Unity Object Pooling System for Bullets, Enemies, and Everything
Let me show you how I approach building an object pool. We're going to create a reusable object pooling system that can manage any type of GameObject (like bullets, enemies, or effects) to improve performance by avoiding frequent instantiation and destruction.
Unity Editor Setup:
- Create an empty GameObject named "BulletPool".
- Create a simple "Bullet" prefab with a
Rigidbodyand a script namedBullet. - Create new C# scripts for
Bullet,GenericObjectPool,BulletPool, andPlayerShooting.
Step 1: Create the Bullet Component
First, we need a component to identify our bullet prefab. This script can be minimal; its main purpose is to provide a type for our generic pool.
// In Bullet.cs
using UnityEngine;
// This component identifies the bullet prefab.
public class Bullet : MonoBehaviour
{
// In a real game, you'd add logic for movement, damage, etc. here.
}
Step 2: Create the Generic Pool Logic
This script will handle creating, storing, and reusing objects of type T. These are the exact settings I use:
// In GenericObjectPool.cs
using UnityEngine;
using System.Collections.Generic;
// The generic constraint ensures that T is a Component we can work with.
public class GenericObjectPool<T> : MonoBehaviour where T : Component
{
[SerializeField] private T prefab;
private Queue<T> objects = new Queue<T>();
public T Get()
{
if (objects.Count == 0)
{
AddObjects(1);
}
return objects.Dequeue();
}
public void ReturnToPool(T objectToReturn)
{
objectToReturn.gameObject.SetActive(false);
objects.Enqueue(objectToReturn);
}
private void AddObjects(int count)
{
var newObject = GameObject.Instantiate(prefab);
newObject.gameObject.SetActive(false);
objects.Enqueue(newObject);
}
}
Step 3: Create a Concrete Subclass for Bullets
This is the non-generic script you will actually attach in the Inspector. It's empty because it just inherits all the logic:
// In BulletPool.cs
public class BulletPool : GenericObjectPool<Bullet> { }
Step 4: Use the Pool in a PlayerShooting script
Here's how you can adapt this for your own game:
// In PlayerShooting.cs
using UnityEngine;
public class PlayerShooting : MonoBehaviour
{
// Assign this in the Inspector by dragging the "BulletPool" GameObject.
[SerializeField] private BulletPool bulletPool;
void Update()
{
if (Input.GetButtonDown("Fire1"))
{
// Get a bullet from the pool instead of instantiating.
Bullet bullet = bulletPool.Get();
bullet.transform.position = transform.position;
bullet.gameObject.SetActive(true);
// After some time, the bullet would return itself to the pool.
}
}
}
Source: Unity Learn - Object Pooling
The beauty of this system is that you write the pool logic once, and then you can create an EnemyPool, ParticlePool, or any other pool by just creating a one-line concrete subclass. No code duplication whatsoever.
Blueprint 2: The Generic Singleton I Copy to Every Project
I've configured this pattern dozens of times, and here's my go-to setup. We're going to create a reusable Singleton<T> base class that any manager-type script (like GameManager, AudioManager, UIManager) can inherit from to ensure there is only one instance of it in the scene.
Unity Editor Setup:
- Create an empty GameObject named "GameManager".
- Create a new C# script named
Singleton. - Create a new C# script named
GameManager.
Step 1: Create the Generic Singleton<T> Base Class
This holds all the core logic for finding and maintaining a single instance. I've specifically removed the slow FindObjectOfType call from the Instance getter to make it highly performant. The singleton now relies entirely on Awake for initialization.
// In Singleton.cs
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
// This error indicates a setup problem in your scene.
Debug.LogError($"An instance of {typeof(T).Name} is needed in the scene, but there is none.");
}
return _instance;
}
}
public virtual void Awake()
{
if (_instance == null)
{
// First instance becomes the singleton.
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
// A singleton already exists, so destroy this duplicate.
Destroy(gameObject);
}
}
}
Step 2: Inherit from the Singleton in GameManager
Let's tackle this together. Your GameManager now becomes a singleton by simply changing its base class:
// In GameManager.cs
using UnityEngine;
public class GameManager : Singleton<GameManager>
{
// All your game management logic goes here.
public void StartGame()
{
Debug.Log("Game has started!");
}
}
Step 3: Access the Singleton from any other script
// In any other script, e.g., MainMenu.cs
using UnityEngine;
public class MainMenu : MonoBehaviour
{
public void OnPlayButtonClick()
{
// Easily access the single instance of GameManager from anywhere.
GameManager.Instance.StartGame();
}
}
This generic singleton pattern has saved me hundreds of lines of duplicated code across projects. I write it once and use it for years.
Blueprint 3: A State Machine That Works for Players and Enemies
Here's the exact method I use when building state machines. We're going to create a simple but reusable state machine that can be used to control the behavior of different entities like a Player (Idle, Walk, Jump states) or an Enemy (Patrol, Chase, Attack states).
Unity Editor Setup:
- Create a "Player" GameObject.
- Create new C# scripts for
IState,PlayerIdleState,StateMachine, andPlayerController.
Step 1: Define the IState Interface
This ensures all states have the necessary methods. This is the contract that all our state classes will follow.
// In IState.cs
public interface IState
{
void Enter();
void Execute();
void Exit();
}
Step 2: Create a Concrete State
Now, let's create a simple state. This PlayerIdleState will represent the player's idle behavior.
// In PlayerIdleState.cs
using UnityEngine;
public class PlayerIdleState : IState
{
public void Enter()
{
Debug.Log("Entering Idle State.");
}
public void Execute()
{
// In a real game, you'd check for input here to transition to another state.
// For example: if (Input.GetAxis("Horizontal") != 0) { /* transition to walk */ }
}
public void Exit()
{
Debug.Log("Exiting Idle State.");
}
}
Step 3: Create the Generic StateMachine
This class will manage the current state and transitions. It is a plain C# class, not a MonoBehaviour:
// In StateMachine.cs
public class StateMachine<T> where T : IState
{
public T CurrentState { get; private set; }
public void Initialize(T startState)
{
CurrentState = startState;
startState.Enter();
}
public void ChangeState(T newState)
{
CurrentState.Exit();
CurrentState = newState;
newState.Enter();
}
}
Step 4: Create a PlayerController to run the state machine
In my projects, I always structure the controller this way. This MonoBehaviour will create and run our state machine.
// In PlayerController.cs
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// Use the generic state machine with the base IState type.
public StateMachine<IState> stateMachine;
void Start()
{
stateMachine = new StateMachine<IState>();
stateMachine.Initialize(new PlayerIdleState());
}
void Update()
{
// Execute the current state's logic every frame.
stateMachine.CurrentState.Execute();
}
}
Source: Game Dev Beginner - State Machine
This generic state machine works for any entity in your game. Create different state classes for different behaviors, and the state machine handles all the transitions and lifecycle management for you.
Wrapping Up: Write Once, Use Everywhere
C# generics in Unity transformed how I write code. Instead of copy-pasting inventory systems, object pools, and singleton managers, I write each system once with a type parameter and reuse it everywhere. The code is cleaner, the bugs are fewer, and when I need to fix something, I fix it in one place.
Every time you find yourself copying and pasting code to work with different types, ask yourself: "Could this be a generic class?" Answer that question with this Unity generics tutorial in mind, and you'll build systems that last across projects and years.
Master this, and you're writing production-quality, maintainable code that professional studios use every day.
Ready to Start Building Your First Game?
Now that you understand how to write reusable code Unity with generics, object pools, singletons, and state machines, it's time to put that knowledge into action.
At Outscal, we take students from zero experience to building professional-quality games. Our Mr. Blocks course teaches you these exact principles in a hands-on project where you'll implement reusable systems, component architecture, and professional code patterns from scratch.
You'll go from basic Unity setup to implementing object pooling, manager systems, state machines, and all the essential patterns covered in this guide. Stop copying code and start building systems that scale.
Key Takeaways
- Generics eliminate code duplication by letting you write one system that works for any data type—instead of three inventory classes, you write one
Inventory<T>. - Unity cannot serialize generic MonoBehaviours directly—the workaround is to create a generic base class and concrete, non-generic subclasses for each type you need.
- Type constraints (
where T : Component) give your generics real power—they let you use Unity-specific methods likeGetComponentinside your generic classes. - Generic classes provide compile-time type safety—an
Inventory<Potion>won't accept aSword, catching bugs before you even run the game. - The generic singleton pattern eliminates boilerplate—write the singleton logic once as
Singleton<T>, then any manager just inherits from it. - Generic object pooling systems work for any Component type—one pool implementation works for bullets, enemies, particles, and anything else.
- Use
where T : structfor value-only generics—this prevents boxing overhead and makes your intent clear when working with value types. - Generic state machines work for players, enemies, and any entity—write the state management logic once, create different state classes for different behaviors.
Common Questions About Generics in Unity
What are generics in C# and why should I use them in Unity?
Generics are a C# feature that lets you define classes, methods, and interfaces with a placeholder for a data type (called a type parameter). You should use them in Unity because they eliminate code duplication—instead of writing separate inventory systems for weapons, potions, and armor, you write one Inventory<T> that works for all of them. This saves time, reduces bugs, and makes your code more maintainable.
Why can't I attach a generic MonoBehaviour to a GameObject in Unity?
Unity's serialization system cannot serialize generic types at edit time. When you create a component in the editor, Unity needs to know the exact type to save it in the scene file. A generic type like GenericPool<T> doesn't specify what T is until runtime, so Unity can't serialize it. The solution is to create a non-generic concrete subclass like BulletPool : GenericPool<Bullet> that specifies the type.
What is a type parameter and what does <T> mean?
A type parameter is a placeholder for a data type in a generic class or method. The <T> syntax (where T stands for "Type") tells the compiler: "I don't know what specific type this will be yet, but when someone uses this class, they'll tell me." By convention, single letters like T are used, but you can use any name like <TItem> or <TState> to make the purpose clearer.
What are type constraints and when should I use them?
Type constraints limit what types can be used with your generic class. For example, where T : Component means T must be a Unity Component. You should use constraints when you need to call specific methods on T—without where T : Component, you couldn't call GetComponent<T>() because the compiler wouldn't know that T has component properties. Constraints make your generic code more powerful and specific.
How do I create a generic object pool in Unity?
Create a generic base class like GenericObjectPool<T> : MonoBehaviour where T : Component with methods for Get() and ReturnToPool(). Use a Queue<T> to store inactive objects. Then create a concrete subclass like BulletPool : GenericObjectPool<Bullet> that you can attach to a GameObject in your scene. The concrete class inherits all the pooling logic but specifies the exact type to pool.
What's the difference between generic classes and inheritance?
Generic classes use a type parameter to work with any type, while inheritance creates a base class with shared logic that subclasses override. Use generics for pure data structures and backend systems (like object pools or inventories) that don't need Inspector integration. Use inheritance for MonoBehaviour components where you need Unity editor integration and lifecycle methods. Generics have zero runtime overhead, while inheritance has a tiny virtual method call overhead.
How do I make a generic singleton pattern in Unity?
Create a generic Singleton<T> base class with a static Instance property and logic to ensure only one instance exists. Use where T : Component as the type constraint. In Awake(), check if _instance is null—if it is, set it to this and call DontDestroyOnLoad(). Then any manager class can inherit from Singleton<GameManager> to become a singleton instantly.
What does default(T) return for different types?
default(T) returns the default value for type T. For reference types (classes), it returns null. For numeric types like int or float, it returns 0. For bool, it returns false. For structs, it returns a new instance with all fields set to their default values. This is useful in generic code when you need to return a "nothing" value but don't know what type T is.
When should I use where T : struct instead of where T : class?
Use where T : struct when your generic should only work with value types like int, float, or custom structs. This prevents boxing (converting value types to reference types, which creates garbage collection overhead) and makes your intent clear. Use where T : class when you specifically need reference types (classes) and want to ensure T can be null. For Unity Components, use where T : Component instead.
Can I create generic methods in Unity, not just generic classes?
Yes, you can create generic methods with their own type parameters, like public T GetItem<T>() where T : Component. These are commonly used for utility functions or factory methods. The type parameter can be inferred from the context (like GetComponent<Rigidbody>()) or explicitly specified. Generic methods are perfect for one-off operations that need to work with multiple types without creating an entire generic class.