Course Reference:Udemy-Niraj Vishwakarma
Asset Sources:
-
Unity Asset Store-HobiSoLoved(Background)
-
Unity Asset Store-Hippo (Tanks)
-
Unity Asset Store-Crehera(Drones)
Introduction of SOLID Principles
Spaghetti Code
This kind of code is basically a tangled mess where everything is tightly coupled. For example, in a script like PlayerController.cs, you might see the player’s Move(), Attack(), Jump()… all crammed together, along with extra functions like PlayAudio.
Code written this way is fragile and hard to extend. It’s like spaghetti — once everything is tangled up, pulling on one strand can easily cause multiple bugs to appear.
Solution:
Ditch the spaghetti‑style structure and switch to a modular design.
Spaghetti code can be fine when you’re quickly prototyping a demo or building a small game to test ideas. But if you expect the project to grow, with more game objects and features being added, setting up a solid architecture and modular design early on becomes crucial.
What Are the SOLID Principles?
In short, there are five main principles:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
Core Idea
One script, one function. SRP breaks down big classes into smaller ones, each with its own independent responsibility. These small classes can be reused and don’t interfere with each other. Think of them like Lego bricks — you can combine them to build all kinds of larger models.
Benefits
When every script has a clear responsibility, you can swap things out freely without worrying about breaking other parts of the game.
Open/Closed Principle (OCP)
Core Idea
Classes, modules, and functions should be open for extension but closed for modification.
Benefits
You can add new features by creating new classes, without messing with the independence of existing ones. This makes your code more modular and easier to maintain.
How to Apply
- Create new classes for new features.
- Define interfaces or abstract classes, then implement them in your feature classes.
- Interact with features through abstract methods instead of calling them directly.
- Make good use of polymorphism.
Liskov Substitution Principle (LSP)
Core Idea
A superclass (or parent class) should be replaceable by its subclasses without breaking functionality.
Benefits
This allows you to use subclasses in place of the parent class without directly referencing each one. It ensures flexibility and scalability when new variables or behaviors are introduced.
How to Apply
- Create a base class and declare all the methods.
- Build subclasses for objects with similar behaviors.
- In other scripts, reference the parent class instead of the subclasses directly.
Interface Segregation Principle (ISP)
Core Idea
A class shouldn’t depend on methods it doesn’t use.
Summary
Write only what’s needed, and keep it relevant.
Dependency Inversion Principle (DIP)
Core Idea
High‑level modules shouldn’t be coupled to low‑level modules. Both should depend on abstractions.
Summary
Depend on abstract functions, not concrete implementations.
How to Apply
- Use interfaces to define abstract functions.
- Implement those interfaces in concrete classes.
- Interact with concrete classes through the interfaces.
Practice Project
We’ll use a 2D Tank vs. Drone War game to show how the SOLID principles can be applied in Unity game development.
Core Gameplay::
- On the left side of the screen, there are two tanks:
- A light tank, controlled by pressing L
- A heavy tank, controlled by pressing H
- The light tank can move left and right, rotate its cannon, and fire light shells.
- The heavy tank cannot move, but it can rotate its cannon and fire heavy shells.
- On the right side of the screen, drones move leftward. Hitting them increases the score.
- Lose condition: If a drone collides with a tank, that tank is destroyed and becomes unresponsive to player input. When both of the tanks are unresponsive, the player loses.
Game / Feature Breakdown
Content
-
Background image
-
Tanks (light and heavy)
-
Bullets (light and heavy)
-
Drones
-
UI (score, start screen, gameplay screen, end screen)
-
Sound effects (firing, drone hit, tank hit)
Logic
- When the game starts, pressing L activates the light tank (move, rotate, fire shells).
- Pressing H activates the heavy tank (rotate, fire shells).
- Drones fly from right to left — entering from the right side of the screen and exiting on the left.
- If a tank is hit by a drone, it becomes inactive.
- If both tanks are inactive, the game ends.
Module Organization
Development Steps
-
Scene Setup
-
Tank Development
- Movement and rotation
- Firing
- Switching between light and heavy tanks
- Health system
-
Drone Development
- Drone prefab
- Drone spawning
- Health system
-
Game Management
- Configure game events
- AudioManager
- UIManager
- GameManager
Feature Implementation and Principles Followed
1. Tank Movement
In the tank design, we follow ISP (Interface Segregation Principle) and DIP (Dependency Inversion Principle).
2. Tank Firing
When designing the firing system, the two types of projectile follow the Open/Closed Principle (OCP). Each time we add a new type of projectile, we should extend the system rather than modify TankFire.cs. Inside TankFire.cs, the firing logic should interact through an interface instead of directly calling the specific shell prefab.
It’s important to note that both types of projectiles interact with TankFire.cs through an interface. However, since the interface does not inherit from MonoBehaviour, it cannot be instantiated directly. To solve this, you need to implicitly cast it to a Component.
|
|
3. Tank Selector Implementation
This part is designed following the Liskov Substitution Principle (LSP).
For smaller projects, you don’t necessarily need separate
LightEngine.csandHeavyEngine.csfiles — you can simply put everything inside theTankEngineclass.
4. Tank Health Implementation
Following the Dependency Inversion Principle (DIP), we use an IDamageable interface to interact with concrete classes. These concrete classes could be drones or other colliding objects such as bullets, missiles, and so on.
5. Airplane Movement
This part reuses the ImoveUp.cs code in the same way as tank movement. It’s also implemented through an interface, leaving room for adding new types of drones in the future.
6. Automatic Destroy of Drones and Projectiles
Here I use a single script, DestroyByTime.cs, following the Single Responsibility Principle (SRP). Unlike the course approach of handling each case separately, this unified script keeps the design simpler and cleaner.
7. Collision Detection and Health System Integration (Tanks, Projectiles, Drones)
This is the final application of the Dependency Inversion Principle (DIP) mentioned earlier. Since both projectiles and drones are destroyed upon collision, they don’t directly implement the IDamageable interface. Instead, the tank’s health system implements the interface. This way, even if new projectile types are added in the future with different damage values, they can be easily integrated. For drones or other enemy types, if different damage behaviors are needed, they can also be flexibly designed by inheriting from the interface.
|
|
8. Managing UI, Audio, and Game State with the Observer Pattern
Register the following events so that, through a unified update interface, observers can be notified and respond whenever changes occur. At its core, this is a listen‑and‑respond system.
By following the Single Responsibility Principle (SRP), scripts like AudioManager.cs and UIManager.cs only handle their specific tasks — playing sounds or managing the UI — without mixing in unrelated logic.
Suggestions on Applying SOLID Principles
- Start with the easier solution
- Never force yourself to use SOLID principles — apply them only when needed (this is my takeaway)
- Avoid over‑designing, especially when your design goes beyond the scope of the project itself
- Always try to use a modular approach, so each independent module can be reused like LEGO blocks (this is one of the most useful methods I’ve learned from other courses — it really saves time and effort)
- At first, applying SOLID principles in code design will definitely take more time, but for projects with long‑term scalability, the investment is absolutely worth it
- Apply → Fail → Learn → Apply → and you’ll be surprised by the results!
Project Expansion & Retrospective
Expansion
The following features were added based on the original project:
- Added Medium Tank
- Controlled by pressing the “M” key
- Can move up and down, but slower and with a smaller range than the Light Tank
- Different Bullet Types Based on Tank Type: Laser, Bullet, Bomb
- Light Tank: Fires lasers (shortest cooldown), deals the least damage, shoots 5 beams in a fan shape
- Medium Tank: Fires bullets (medium cooldown), deals moderate damage
- Heavy Tank: Fires rockets (longest cooldown), deals the highest damage
- Added Heavy Drone
- Moves slightly up and down randomly while flying
- Adjusted Drone Spawn Rules to Increase Difficulty Over Time
- At the start, heavy drone have a low spawn rate, which increases as time goes on
- Different Tanks Take Different Amounts of Damage
- Heavy tanks are the most durable
Project Architecture Overview
The project uses a hybrid design combining Component-Based Architecture + Event-Driven Programming + Strategy Pattern.
Folder Structure
|
|
Architecture Overview
| Module | Responsibility | Core Classes |
|---|---|---|
| Tank/Engine | Tank engine switching system; player can switch tank types using L/M/H keys | TankEngineBase → TankEngineLight/Medium/Heavy, TankEngineSelector |
| Tank/Move | Tank movement system; each tank type has its own movement logic | TankMoveBase → LightTankMove/MediumTankMove/HeavyTankMove, IMoveY, TankRotate |
| Tank/Fire | Firing system; different projectile types implemented via IProjectile interface |
TankFire, IProjectile → LightShell/MediumShell/RocketShell/LaserShellProjectile |
| Tank/Health | Health system; unified damage handling via IDamageable interface |
HealthSystem, IDamageable |
| Drone | Enemy drone system, including spawning, movement, and damage handling | DroneMoveBase → LightDroneMove/HeavyDroneMove, DroneHealth, DroneSpawner |
| Mgr | Global managers | GameMgr (game flow), UIMgr (UI panels/score), AudioMgr (sound effects) |
| GameUtil | Global event bus | GameEvents (static event class) |
Architecture diagram
Code Reuse
Observer Pattern
The Observer pattern usually follows the same routine. Take AudioManager as an example:
|
|
Event Bus System — GameEvents
Create a similar static event class in any project to enable decoupled communication between modules. Simply update the event names to adapt it to different games.
|
|
Interface-Driven Damage System — IDamageable
All objects that can take damage (players, enemies, destructibles) implement this interface. Attackers simply call GetComponent<IDamageable>()?.TakeDamage(), achieving complete decoupling.
|
|
Analysis of SOLID Principles in the Project
S — Single Responsibility Principle (SRP)
Tank functionalities like movement, shooting, rotation, and health are split into independent components instead of being lumped into a massive “Tank” class. This is a classic example of SRP in practice.
| Class | Responsibility |
|---|---|
TankMoveBase |
Solely responsible for movement logic |
TankFire |
Solely responsible for triggering shots |
TankRotate |
Solely responsible for rotation and aiming |
HealthSystem |
Solely responsible for health management |
AudioMgr |
Solely responsible for playing sound effects |
UIMgr |
Solely responsible for UI display and scoring |
GameMgr |
Solely responsible for game flow logic |
DroneSpawner |
Solely responsible for spawning drones |
DroneHealth |
Solely responsible for drone damage logic |
O — Open/Closed Principle (OCP)
- Ammo System: The
IProjectileinterface allows adding new ammo types (e.g.,LaserShellProjectile,RocketShellProjectile) without modifyingTankFire. You simply create a new class implementingIProjectile. - Movement System: The combination of the
IMoveYinterface andTankMoveBaseenables adding new tank movement styles without altering the base class. - Drone Movement:
DroneMoveBaseallows extending new flight behaviors via inheritance (e.g.,HeavyDroneMovewith random offset movements).
L — Liskov Substitution Principle (LSP)
- The three subclasses of
TankEngineBase(TankEngineLight,TankEngineMedium,TankEngineHeavy) can be used interchangeably withinTankEngineSelectorandGameMgr. GameMgr.CheckEngine()iterates through aList<TankEngineBase>and callsGetStatus(), remaining unaware of the specific engine type.- Similarly, subclasses of
DroneMoveBasecan be swapped wherever aDroneMoveBaseis required.
I — Interface Segregation Principle (ISP)
IMoveY: Defines only a single method,MoveY(float), forcing implementers to ignore unrelated functions like rotation or shooting.IProjectile: Defines onlyFire(Transform), allowing each ammo type to focus solely on “how to fire.”IDamageable: Defines onlyTakeDamage(int), enabling any object capable of taking damage to implement it easily.
The interfaces are extremely granular, each containing just one method, perfectly adhering to ISP.
D — Dependency Inversion Principle (DIP)
- Damage handling in
DroneHealthrelies on theIDamageableinterface rather than the concreteHealthSystemclass:
|
|
TankFiredepends on theIProjectileinterface to launch ammo, not on specific ammo classes.TankMoveBasecasts itself to an interface (this as IMoveY) for calls, ensuring high-level logic depends on abstractions rather than concrete implementations.
Retrospective
While SOLID principles greatly enhance scalability, they also significantly increase the number of scripts. For instance, implementing the Observer Pattern for the UI took nearly a full day, and debugging a single issue consumed an entire afternoon. For beginners, pitfalls often arise from where listener code is placed and the order of execution, which requires careful attention.
Additionally, this was my first systematic dive into game architecture. At times, the concepts felt quite abstract and difficult to grasp immediately. However, the most tangible takeaway from this project is its excellent extensibility: adding new features requires writing new scripts without touching old ones, effectively avoiding the scenario where one new feature introduces three new bugs. This is crucial for large-scale projects or online mobile games.
I hope that as my skills grow, my exposure to excellent complex/large-scale projects increases, and the complexity of my personal projects rises, my understanding and practical application of software architecture will continue to improve.