The SOLID Principles (City Planning)
The Spilled Spaghetti (The Problem)
Welcome to the big leagues. You've graduated from training a single Robot Dog to managing an entire metropolis of 10,000 dogs.
If every dog in this massive city creates its own toys, cooks its own food, and acts as its own mayor, the city becomes absolute chaos. If you try to update how food is cooked, you have to find and rewrite the cooking code inside every single dog. Your game will crash, updates will take weeks, and your codebase will turn into unreadable "spaghetti code."
The City Planner's Solution
The SOLID Principles are the Zoning Laws of your Robot Dog City. They are 5 rules designed to keep the city organized, scalable, and easy to manage.
S - Single Responsibility (SRP)
A Fetch Dog should only fetch. It shouldn't also be the City Chef. Give every script exactly one job.
O - Open/Closed (OCP)
Don't tear down the entire factory just to build a new Fire Dog. Your code should be open to adding extensions, but closed to modifying the core foundation.
L - Liskov Substitution (LSP)
Any dog with a "Dog License" should be able to safely enter the dog park without crashing the game. Don't create subclasses that break the rules of their parent.
I - Interface Segregation (ISP)
Don't force a blind dog to carry a "How to Read" manual. Give dogs only the specific rules and interfaces they actually need.
D - Dependency Inversion (DIP)
The dogs shouldn't rely on a specific brand of tennis ball. They should just rely on the idea of a "Throwable Object."
The Blueprint (The Code)
Here is how a Senior Developer strictly enforces the Single Responsibility Principle (SRP) in Unity:
// BAD WAY: The Dog does everything (Violates SRP)
public class BadDog : MonoBehaviour {
public void Fetch() { /* fetch logic */ }
public void CookFood() { /* cooking logic */ }
public void PayTaxes() { /* tax logic */ }
}
// GOOD WAY: Single Responsibility (SRP)
public class GoodDog : MonoBehaviour {
public void Fetch() { /* fetch logic */ }
}
public class CityChef : MonoBehaviour {
public void CookFood() { /* cooking logic */ }
}
Where to Use This in a Real Game
- ✦Player Controllers: Splitting
PlayerMovement,PlayerHealth, andPlayerInputinto separate scripts instead of one massivePlayer.csfile. - ✦Weapons Systems: Using Dependency Inversion so a gun fires any
IDamageableobject, not just a specificGoblinEnemy. - ✦Inventory Systems: Using Open/Closed so you can add new item types without completely rewriting the Backpack class.
Object Pooling (The Recycling Center)
The Spilled Spaghetti (The Problem)
Imagine your city needs tennis balls. Whenever a dog wants to play, you build a brand new tennis ball from scratch using Unity's Instantiate() command. When the dog is done playing, you set the ball on fire using Destroy().
Doing this 60 times a second creates a massive mess in the computer's memory. Eventually, the city garbage collectors (the C# Garbage Collector) have to stop all traffic in the city just to clean up the ashes. This causes massive lag spikes and stutters in your game.
The City Planner's Solution
Instead of setting toys on fire, we build a Toy Recycling Center. This is called an Object Pool.
We create 50 tennis balls when the game starts. When a dog wants a ball, they borrow one from the shelf. When they are done, they wash it and put it back on the shelf for the next dog. No fires, no garbage collection, and absolutely zero lag.
The Blueprint (The Code)
Here is a simple, highly efficient Object Pool in Unity:
public class BulletPool : MonoBehaviour {
public GameObject bulletPrefab;
private Queue<GameObject> pool = new Queue<GameObject>();
void Start() {
// Create 50 bullets when the game starts
for(int i = 0; i < 50; i++) {
GameObject bullet = Instantiate(bulletPrefab);
bullet.SetActive(false); // Hide it on the shelf
pool.Enqueue(bullet);
}
}
public GameObject BorrowBullet() {
if(pool.Count > 0) {
GameObject bullet = pool.Dequeue();
bullet.SetActive(true); // Give it to the dog
return bullet;
}
return Instantiate(bulletPrefab); // Emergency backup
}
public void ReturnBullet(GameObject bullet) {
bullet.SetActive(false); // Wash it and put it back
pool.Enqueue(bullet);
}
}
Where to Use This in a Real Game
- ✦Bullet Hell Shooters: Reusing hundreds of bullets instead of constantly destroying them when they go off-screen.
- ✦Particle Effects: Reusing explosion effects or blood splatters so the game doesn't stutter during intense combat.
- ✦Endless Runners: Reusing the platform chunks and obstacles as the player runs, moving them from behind the player to in front.
The Factory Pattern (The Assembly Line)
The Spilled Spaghetti (The Problem)
Imagine your Robot Dog City needs different types of dogs: Police Dogs, Fire Dogs, and Construction Dogs. If you just use standard instantiation (new PoliceDog()), every script in your game needs to know exactly how to build every type of dog.
If you ever change how a Police Dog is built (e.g., adding a siren component), you have to hunt down and update every single script that spawns one. Your code becomes a tangled mess of dependencies.
The City Planner's Solution
Instead of every building in the city making its own dogs, we build a Factory (The Assembly Line). The Factory is the only building in the city that knows how a dog is built.
If the Police Station needs a dog, it doesn't build it; it just sends a work order to the Factory: "Give me one Police Dog." The Factory handles all the messy assembly and delivers the finished product.
The Blueprint (The Code)
public enum DogType { Police, Fire, Construction }
// 1. The Factory Class (The Assembly Line)
public class RobotDogFactory : MonoBehaviour
{
[SerializeField] private GameObject _policeDogPrefab;
[SerializeField] private GameObject _fireDogPrefab;
// The single method the rest of the city calls
public GameObject CreateDog(DogType type, Vector3 spawnLocation)
{
GameObject newDog = null;
// The Factory hides all the messy assembly logic in here
switch (type)
{
case DogType.Police:
newDog = Instantiate(_policeDogPrefab, spawnLocation, Quaternion.identity);
newDog.AddComponent<Siren>(); // Add specific parts
break;
case DogType.Fire:
newDog = Instantiate(_fireDogPrefab, spawnLocation, Quaternion.identity);
newDog.AddComponent<WaterHose>();
break;
}
return newDog;
}
}
// 2. How the city uses it (Clean and simple)
public class PoliceStation : MonoBehaviour
{
[SerializeField] private RobotDogFactory _factory;
public void OrderBackup()
{
// The station doesn't know HOW the dog is built, it just orders one!
_factory.CreateDog(DogType.Police, transform.position);
}
}
- ✦Where to Use This: Spawning different enemy variants, generating random loot drops, or building complex UI popups dynamically.
The State Pattern (The Brain Modes)
The Spilled Spaghetti (The Problem)
If you want your Robot Dog to Patrol, Chase, and Sleep, you might be tempted to put all of that inside one massive Update() loop using endless if/else statements.
What happens if it tries to Sleep while Chasing? You add more boolean checks: if (isChasing && !isSleeping). Before long, your Dog.cs script is 3,000 lines long, and fixing a bug in the Sleep logic accidentally breaks the Chase logic.
The City Planner's Solution
We use a State Machine (The Brain Modes). Instead of one giant brain trying to do everything, the dog's brain uses interchangeable chips.
There is a "Sleep Chip," a "Patrol Chip," and a "Chase Chip." The dog can only plug in one chip at a time. When the dog is sleeping, the Chase logic literally does not exist in its active memory.
The Blueprint (The Code)
// 1. The Blueprint for a Brain Chip
public interface IDogState
{
void EnterState(RobotDog dog);
void UpdateState(RobotDog dog);
void ExitState(RobotDog dog);
}
// 2. A Specific Brain Chip (Patrol Mode)
public class PatrolState : IDogState
{
public void EnterState(RobotDog dog) { dog.PlayAnimation("Walk"); }
public void UpdateState(RobotDog dog)
{
dog.MoveForward();
if (dog.SeesIntruder)
{
// Unplug the Patrol Chip, plug in the Chase Chip!
dog.ChangeState(new ChaseState());
}
}
public void ExitState(RobotDog dog) { dog.StopMoving(); }
}
// 3. The Dog's Brain (The State Machine)
public class RobotDog : MonoBehaviour
{
private IDogState _currentState;
private void Start()
{
ChangeState(new PatrolState()); // Plug in the first chip
}
private void Update()
{
_currentState?.UpdateState(this); // Run whatever chip is plugged in
}
public void ChangeState(IDogState newState)
{
_currentState?.ExitState(this); // Unplug old chip
_currentState = newState;
_currentState?.EnterState(this); // Plug in new chip
}
}
- ✦Where to Use This: Enemy AI behaviors, Player Locomotion (Idle, Run, Jump, Fall), and UI Menus (MainMenu, Settings, LoadingScreen).
The Observer Pattern (The City Siren)
The Spilled Spaghetti (The Problem)
Imagine it starts raining in your Robot City. If you don't use the Observer pattern, every single one of your 10,000 dogs has to run an Update() loop that constantly asks the sky: "Is it raining? Is it raining now? How about now?"
This is called "polling," and it will absolutely destroy your game's CPU performance.
The City Planner's Solution
We install a City Siren (The Observer Pattern). The dogs stop asking the sky about the weather. They just go about their day.
When it finally rains, the Weather System hits the giant City Siren: "ATTENTION EVERYONE, IT IS RAINING!" Any dog that cares about the rain hears the siren and puts on a raincoat. The dogs are observing the event, waiting to be notified.
The Blueprint (The Code)
using System;
// 1. The City Siren (The Subject)
public class WeatherSystem : MonoBehaviour
{
// The Siren button
public static event Action OnRained;
public void StartRain()
{
// Hit the Siren! (Notify all dogs)
OnRained?.Invoke();
}
}
// 2. The Listener (The Observer)
public class RobotDog : MonoBehaviour
{
private void OnEnable()
{
// Subscribe: "Hey Siren, let me know when it rains!"
WeatherSystem.OnRained += PutOnRaincoat;
}
private void OnDisable()
{
// Unsubscribe: Crucial to prevent memory leaks!
WeatherSystem.OnRained -= PutOnRaincoat;
}
private void PutOnRaincoat()
{
Debug.Log("Equipping Raincoat!");
}
}
- ✦Where to Use This: Achievements systems, updating UI Health Bars when taking damage, and decoupling audio managers from gameplay logic.
The Flyweight Pattern (The Shared Library Card)
The Spilled Spaghetti (The Problem)
Let's say every Robot Dog has statistics: Max Health (100), Run Speed (15f), and Jump Height (5f). If you spawn 10,000 dogs, Unity will allocate memory to store the number "100", "15", and "5" ten thousand separate times. You are copying the exact same static data over and over again, bloating your RAM.
The City Planner's Solution
We use a Shared Library Card (The Flyweight Pattern). Instead of every dog carrying a heavy book containing all the rules and stats, we put one master copy of the rulebook in City Hall.
We then give every dog a lightweight "Library Card" that points to that single book. In Unity, we do this using ScriptableObjects.
The Blueprint (The Code)
// 1. The Master Rulebook (The Flyweight)
// This is saved as an asset file in your Unity project. Memory is only allocated ONCE.
[CreateAssetMenu(fileName = "NewDogStats", menuName = "Stats/DogStats")]
public class DogStats : ScriptableObject
{
public float maxHealth = 100f;
public float runSpeed = 15f;
public float jumpHeight = 5f;
}
// 2. The Dog (Carrying the Library Card)
public class RobotDog : MonoBehaviour
{
// This is the Library Card. It just points to the ScriptableObject.
[SerializeField] private DogStats _myStats;
private float _currentHealth;
private void Start()
{
// We read the shared data to set up our personal current data
_currentHealth = _myStats.maxHealth;
}
public void Move()
{
transform.Translate(Vector3.forward * _myStats.runSpeed * Time.deltaTime);
}
}
- ✦Where to Use This: RPG Item databases, enemy base stats, grid-based tile data (e.g., sharing the "Grass" texture and friction data across 10,000 map tiles).
The Singleton Pattern (The Mayor)
The Spilled Spaghetti (The Problem)
Imagine your Robot Dog City is hosting a massive parade. You want background music playing for all the dogs to hear. If you just attach a "Music Player" script to random dogs, you will suddenly have five different songs playing at the exact same time, creating a horrible wall of noise.
Alternatively, imagine you want to keep track of the total number of treats eaten by the whole city. If Dog A eats a treat, how does he tell Dog B? If every dog has to manually find every other dog to share the score, your game's performance will crawl to a halt. You need a central authority.
The City Planner's Solution
We build City Hall (The Singleton Pattern). City Hall is unique because of two very strict zoning laws:
- ✦There can only be ONE City Hall. If someone tries to build a second City Hall, the city immediately bulldozes the fake one.
- ✦Everyone knows the address. Any dog, anywhere in the city, can walk up to City Hall to ask a question or drop off information without needing a map.
In code, a Singleton is just a class that guarantees only one instance of itself exists, and provides a simple, global way for any other script to talk to it.
The Blueprint (The Code)
using UnityEngine;
// 1. Building City Hall (The Singleton)
public class GameManager : MonoBehaviour
{
// The "Address" that everyone in the city knows.
// It is 'static', meaning it belongs to the class itself, not any one specific object.
public static GameManager Instance { get; private set; }
public int totalCityTreats = 0;
private void Awake()
{
// Law #1: There can be only ONE.
if (Instance != null && Instance != this)
{
// A fake City Hall tried to spawn! Bulldoze it immediately!
Debug.LogWarning("Found a duplicate GameManager. Destroying it!");
Destroy(this.gameObject);
return;
}
// We are the one true City Hall. Set the address.
Instance = this;
// Optional: Keep City Hall around even if we load a new scene/city district
DontDestroyOnLoad(this.gameObject);
}
public void AddTreat()
{
totalCityTreats++;
Debug.Log("Total Treats: " + totalCityTreats);
}
}
How the City Uses It: Now, if a random Robot Dog finds a treat, it doesn't need to search the whole game for the scorekeeper. It just uses the global address:
public class RobotDog : MonoBehaviour
{
public void EatTreat()
{
// Go straight to City Hall and tell the Mayor!
GameManager.Instance.AddTreat();
}
}
The Danger: The "Dictator" Problem
Because Singletons are so easy to talk to, junior developers tend to use them for everything. They make the Player a Singleton, the Enemies Singletons, and the UI Singletons.
If City Hall handles the music, the score, the dog's health, the weather, and the physics... City Hall becomes a God Object. It knows too much, does too much, and if City Hall breaks, the entire game crashes. Only use a Singleton for true managers that the whole game needs to share.
Where to Use This
- ✦Audio Managers: To ensure background music persists smoothly between level loads.
- ✦Pool Managers: The recycling center that every dog can easily access to get a new toy.
- ✦Game/State Managers: Tracking high-level states like
IsGameOverorCurrentScore.
Enterprise Folder Structure (The City Districts)
The Spilled Spaghetti (The Problem)
If you dump every single Robot Dog, toy, and building into one massive pile (the standard Unity Assets/Scripts folder), your city becomes a disorganized landfill.
When you want to find the specific blueprints for the Police Dog, you have to dig through 500 unrelated files. Eventually, dogs start accidentally relying on buildings from completely different neighborhoods. It makes finding bugs impossible and turns your game into a nightmare to update.
The City Planner's Solution
We divide our city into highly organized Districts. We strictly separate the core "City Infrastructure" (your foundational architecture) from the "City Residents" (the specific game features like Players or Enemies). In software engineering, this is called Domain-Driven Design.
Having an enterprise-grade folder structure is the ultimate trust signal that you are a Senior Developer.
The Blueprint
Here is the ultimate enterprise-level folder structure for a Unity project:
Assets/
├── App/ // The main entry points
│ ├── Bootstrapper.cs // Initializes the game (Dependency Injection, Services)
│ └── GameSettings.asset // Global ScriptableObject configurations
│
├── Core/ // The "City Infrastructure" (Your Architecture)
│ ├── DesignPatterns/ // Generic implementations (Singleton, Factory bases)
│ ├── ObjectPooling/ // The PoolManager and IPoolable interfaces
│ ├── StateMachine/ // Base State, StateMachine, and transitions
│ └── EventBus/ // The global event system for decoupled messaging
│
├── Features/ // The "City Residents" (Actual Game Logic)
│ ├── Player/ // Everything specific to the player
│ │ ├── Scripts/
│ │ ├── Prefabs/
│ │ └── Data/ // Player ScriptableObjects (Stats, Inventory)
│ ├── Enemies/ // Enemy AI, state machines, and visual prefabs
│ └── Weapons/ // Weapon logic, object-pooled bullets, FX
│
├── ArtAssets/ // Visuals (Strictly separated from logic)
│ ├── Materials/
│ ├── Models/
│ └── UI/
│
├── Scenes/
│ ├── Boot.unity // The empty initialization scene
│ ├── MainMenu.unity
│ └── Gameplay.unity
│
└── ThirdParty/ // External tools
├── DOTween/
└── UniTask/
Why This Sells Your Expertise
1. It Prevents Spaghetti Dependencies
By grouping things by Feature rather than by Type, you visually enforce clean architecture. If you completely delete the Features/Weapons folder, your game will no longer have guns, but the Player and the Core systems will still compile perfectly without throwing errors.
2. The "Core" Folder is Your Goldmine
Your Core folder is completely generic. You can literally copy and paste your Core/ObjectPooling folder from this game into your next 5 commercial games without changing a single line of code.
3. The Boot Scene Architecture
Having an explicit Boot.unity scene signals seniority. It shows you understand that a game needs a dedicated, lightweight space to initialize services and load save data before attempting to load heavy 3D geometry.