civil-and-structural-engineering
Designing Extensible Game Entities with the Prototype Pattern in Unity C#
Table of Contents
Introduction: Why Extensible Entity Design Matters
Game development often demands systems that can grow without breaking. As a project evolves, new enemy types, power‑ups, or interactive objects must be added quickly without rewriting existing logic. In Unity C#, the Prototype pattern offers a robust solution: instead of constructing objects from scratch every time, you clone a pre‑configured prototype. This approach reduces code duplication, simplifies configuration, and makes your entity system highly extensible. While Unity’s prefab system is a form of prototyping, implementing the pattern in code gives you finer control over runtime cloning, deep copying, and data inheritance.
This article explores the Prototype pattern in depth, provides practical Unity‑focused examples, and discusses how to combine it with other patterns for scalable game architectures. You will learn to design entities that are easy to create, modify, and reuse – a cornerstone of maintainable game code.
Understanding the Prototype Pattern
Core Concept
The Prototype pattern belongs to the creational design patterns family. Its intent is to create new objects by copying an existing instance, the prototype. The copying process is abstracted behind a Clone() method, which returns a duplicate. This pattern is particularly useful when:
- Object creation is expensive (e.g., involves database calls, complex calculations, or asset loading).
- You need to generate objects with varying configurations at runtime.
- You want to avoid subclasses just to change the initial state of an object.
- You have a limited number of initial configurations and need many similar instances.
In Unity, the most familiar form of prototyping is the prefab – a saved GameObject with components and data. However, code‑based prototypes extend this by allowing you to clone plain C# objects (POCOs) or even entire MonoBehaviour instances with custom deep‑copy logic.
Comparison with Other Creational Patterns
To appreciate when to use Prototype, compare it with Factory and Singleton:
- Factory Method / Abstract Factory: Focuses on creating objects through an interface but often requires subclassing to produce different object types. Prototype avoids subclassing by allowing you to clone a pre‑built instance.
- Singleton: Ensures a single instance. Prototype is the opposite – it creates many copies from a single source.
- Object Pooling: Often complements Prototype. You can have a pool of cloned prototypes that are reused, reducing garbage collection pressure.
The Prototype pattern is also a natural fit for Unity’s component‑based architecture: you can clone a prototype that contains a specific set of components and data, then attach the clone to a GameObject or compose it further.
Implementing the Prototype Pattern in Unity C#
Basic Interface and Base Class
Start by defining a generic interface that declares the clone operation:
public interface IPrototype<T> where T : IPrototype<T>
{
T Clone();
}
The generic parameter ensures that derived types return the correct concrete type when cloned. Next, create a base class for game entities:
using UnityEngine;
public abstract class GameEntity : IPrototype<GameEntity>
{
public string entityName;
public Vector3 position;
public Quaternion rotation;
// Shallow copy by default
public virtual GameEntity Clone()
{
return (GameEntity)this.MemberwiseClone();
}
}
MemberwiseClone() performs a shallow copy: value types are copied, but reference types (e.g., lists, scriptable objects, other game objects) still point to the same instance. For many game objects, shallow copy is sufficient – especially if the prototype only holds simple data or uses struct fields. When you need independent copies of reference types, you must implement deep cloning.
Deep Copying Techniques
For deep copies, you have several options:
- Manual cloning: Override
Clone()and explicitly copy each reference field. - Serialization: Serialize the object to a binary or JSON stream and deserialize it back. This is slower but works for complex object graphs.
- Reflection: Iterate over fields and recursively clone
IPrototypeinstances. Use this sparingly due to performance overhead.
Example of manual cloning for an entity with a custom component:
[System.Serializable]
public class InventoryComponent
{
public List<string> items = new List<string>();
public int gold;
}
public class PlayerEntity : GameEntity
{
public InventoryComponent inventory;
public override GameEntity Clone()
{
PlayerEntity clone = (PlayerEntity)base.Clone(); // shallow copy
// Deep copy the inventory
clone.inventory = new InventoryComponent
{
items = new List<string>(this.inventory.items),
gold = this.inventory.gold
};
return clone;
}
}
Notice that we call base.Clone() which already copies entityName, position, and rotation. We then deep copy only the mutable reference type.
Designing Extensible Entity Hierarchies
Using Inheritance to Add Specialized Behavior
One of the strengths of the Prototype pattern is that you can define a prototype for a base entity and then extend it for specific types. Consider an RPG game:
public class Enemy : GameEntity
{
public int health;
public int damage;
public string enemyType;
public virtual void Attack()
{
Debug.Log($"{enemyType} attacks for {damage} damage.");
}
public override GameEntity Clone()
{
Enemy clone = (Enemy)base.Clone();
clone.health = this.health;
clone.damage = this.damage;
clone.enemyType = this.enemyType;
return clone;
}
}
public class BossEnemy : Enemy
{
public string specialAbility;
public float rageMultiplier = 2f;
public override void Attack()
{
// Boss overrides attack behavior
int realDamage = (int)(damage * rageMultiplier);
Debug.Log($"Boss {enemyType} uses {specialAbility} for {realDamage} damage.");
}
public override GameEntity Clone()
{
BossEnemy clone = (BossEnemy)base.Clone();
clone.specialAbility = this.specialAbility;
clone.rageMultiplier = this.rageMultiplier;
return clone;
}
}
Now you can create a prototype for each enemy type and spawn clones as needed:
Enemy goblinPrototype = new Enemy
{
entityName = "Goblin",
health = 50,
damage = 10,
enemyType = "Goblin"
};
BossEnemy dragonPrototype = new BossEnemy
{
entityName = "Dragon Lord",
health = 500,
damage = 40,
enemyType = "Dragon",
specialAbility = "Fire Breath",
rageMultiplier = 3f
};
// At runtime
Enemy goblinClone = goblinPrototype.Clone();
goblinClone.position = new Vector3(10, 0, 5);
BossEnemy dragonClone = (BossEnemy)dragonPrototype.Clone();
dragonClone.position = new Vector3(0, 10, 0);
This approach avoids a sprawling constructor with many parameters and keeps the spawning logic clean. If you later add a new field to Enemy, you only update the Clone() in that class; derived classes will need to be updated only if they also use that field.
Composition Over Inheritance: Prototypes with ScriptableObjects
Unity’s ScriptableObject is an excellent container for prototype data. You can store a ScriptableObject as a prototype asset and clone its data onto runtime entities. This decouples configuration from code and allows designers to create new prototypes without programming.
Example: define a EnemyData scriptable object:
[CreateAssetMenu(fileName = "NewEnemyData", menuName = "Game/Enemy Data")]
public class EnemyData : ScriptableObject, IPrototype<EnemyData>
{
public string enemyName;
public int health;
public int damage;
public GameObject prefab; // visual prefab
public EnemyData Clone()
{
EnemyData copy = CreateInstance<EnemyData>();
copy.enemyName = this.enemyName;
copy.health = this.health;
copy.damage = this.damage;
copy.prefab = this.prefab; // shallow copy
return copy;
}
}
Then a runtime entity can hold a reference to the data:
public class EntityInstance : MonoBehaviour
{
public EnemyData prototypeData;
public int currentHealth;
public void Initialize(EnemyData data)
{
this.prototypeData = data.Clone(); // each entity gets its own data
currentHealth = data.health;
}
}
The prototype asset is never modified at runtime; clones are made before assigning values. This prevents accidental modification of the shared prototype.
Practical Usage in Unity: Spawning and Pooling
Runtime Spawning with Prototypes
When spawning enemies, power‑ups, or projectiles, using a prototype can be much more flexible than invoking new() with many constructor arguments. Store a dictionary of prototypes:
public static class EntityRegistry
{
private static Dictionary<string, GameEntity> prototypes = new Dictionary<string, GameEntity>();
public static void RegisterPrototype(string key, GameEntity prototype)
{
prototypes[key] = prototype;
}
public static GameEntity Spawn(string key, Vector3 position)
{
if (prototypes.TryGetValue(key, out GameEntity proto))
{
GameEntity clone = proto.Clone();
clone.position = position;
return clone;
}
Debug.LogError($"Prototype '{key}' not found.");
return null;
}
}
Registration can happen in a game manager or via [RuntimeInitializeOnLoadMethod].
Object Pooling + Prototype
For high‑frequency spawning (e.g., bullets or particle effects), combine the Prototype pattern with an object pool. Create a pool that stores inactive clones and reactivates them. The prototype provides the initial state.
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prototypePrefab; // visual prefab
private Queue<GameObject> pool = new Queue<GameObject>();
private GameEntity prototypeData;
public void Initialize(GameEntity data)
{
prototypeData = data;
// Optionally pre‑warm the pool
for (int i = 0; i < 10; i++)
{
GameObject obj = CreateNewObject();
obj.SetActive(false);
pool.Enqueue(obj);
}
}
private GameObject CreateNewObject()
{
GameObject go = Instantiate(prototypePrefab);
EntityInstance instance = go.GetComponent<EntityInstance>();
instance.Initialize((EnemyData)prototypeData.Clone());
return go;
}
public GameObject Get()
{
if (pool.Count == 0)
return CreateNewObject();
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
The pool reduces instantiation cost, while the clone method ensures each entity gets its own copy of data.
Handling Unity‑Specific Concerns
Cloning MonoBehaviour Components
Cloning a MonoBehaviour directly using MemberwiseClone() is dangerous because Unity components are attached to GameObjects and have internal state managed by the engine. A safer approach is to clone the data container (a ScriptableObject or plain C# object) and then apply that data to a new MonoBehaviour instance when it is created or retrieved from a pool.
If you absolutely must clone a full MonoBehaviour, consider serializing it to a JSON string and deserializing into a new component. Unity’s JsonUtility can handle public fields:
public static T DeepCopyMonoBehaviour<T>(T original) where T : MonoBehaviour
{
string json = JsonUtility.ToJson(original);
GameObject go = new GameObject();
T copy = go.AddComponent<T>();
JsonUtility.FromJsonOverwrite(json, copy);
return copy;
}
This creates a new GameObject and component – useful for editor tools but not recommended for runtime performance due to allocations.
Working with Prefabs as Prototype Visuals
Often you want a code prototype (data) paired with a UnityEngine.GameObject prefab for rendering. The EnemyData example above shows this. When spawning, you instantiate the prefab and set the data. This separation keeps data logic in plain C# and visual logic in Unity.
Performance Considerations
The Prototype pattern can improve performance by:
- Reducing expensive initialization (e.g., loading assets or computing procedural values) to once per prototype.
- Enabling object pooling, which avoids memory allocation spikes.
- Allowing lazy setup: you can clone a prototype and then modify only the fields that change.
However, deep copying can be expensive if the object graph is large. Profile your game to decide whether shallow or deep copy is needed. For many cases, shallow copy plus explicit handling of a few mutable reference fields is enough. Avoid serialization‑based cloning in hot code paths.
Advanced Extensions
Prototype Registry with Code Generation
For very large games, you can use reflection to build a registry of all IPrototype classes at startup. This reduces manual registration. Use System.Reflection to find types that implement the interface, then create instances via Activator.CreateInstance and store them.
Combining with the Factory Pattern
Sometimes you need to decide at runtime which prototype to use based on game state. A factory can encapsulate that logic:
public class EnemyFactory
{
private Dictionary<string, GameEntity> prototypes;
public GameEntity CreateEnemy(string type, Vector3 position)
{
GameEntity proto = prototypes[type];
GameEntity clone = proto.Clone();
clone.position = position;
return clone;
}
}
This factory uses prototypes internally – the best of both worlds.
Conclusion
The Prototype pattern is a powerful tool for designing extensible game entities in Unity C#. By cloning pre‑configured instances, you avoid the rigidity of constructors and the overhead of repeated initialization. Combined with Unity’s scripting and asset system, it enables a clean separation of data and visual representation. Whether you use plain classes, ScriptableObjects, or full MonoBehaviour clones, the pattern reduces complexity and improves maintainability.
Key takeaways:
- Define a generic
IPrototype<T>interface with aClone()method. - Use
MemberwiseClone()for shallow copies; implement deep copy for mutable reference types. - Store prototypes in a registry or as
ScriptableObjectassets. - Combine with object pooling for performance‑critical scenarios.
- Be cautious when cloning Unity engine objects – prefer data containers.
Apply this pattern to your next Unity project and experience how easily your entity system can adapt to new requirements without breaking existing code.
For further reading, explore the official Unity documentation on ScriptableObject and learn about MemberwiseClone in .NET. The Refactoring Guru website also provides a clear introduction to the Prototype pattern with C# examples.