Prefab management
Unity can do inheritance of game objects by using prefab variants. A prefab variant inherits all the components and sub-objects from its original. When you change the original, then all the variants get changed too. But you can add additional components and sub-objects to the variants and you can override individual properties from the parent by changing them in the variant.
That means you can create on generic "Enemy" prefab which has everything all enemies have in common, and then create multiple variant prefabs inheriting from it which represent more specialized enemies. When you now do a change to the generic enemy, then that change gets automatically applied to all the enemies (unless that enemy overrides that specific property).
Spawning
When you have spawners which are supposed to spawn enemies randomly, then you can give it a public GameObject[] enemyPrefabs
(or [SerializeField] private GameObject[] enemyPrefabs
if you want to be thorough). You can then maintain the palette of enemies in the editor. At runtime, the spawner would then pick from that palette of prefabs.
You might perhaps want some enemies to spawn more frequently than others. You can do that with multiple spawners with different settings. For example, when you want more zombies than skeletons, you can have one spawner which spawns skeletons and zombies, and another which spawns only zombies. Or you can do that using the techniques from the question "How do I create a weighted collection and then pick a random element from it?".
Code architecture
On the script side, Unity's software architecture is based on the principle of Composition over Inheritance. Which means that instead of having one abstract base class "Enemy" with other more specialized inheriting classes, you usually have multiple classes which each implement one mechanic and which can be mixed and matched to create new enemies with very little effort.
For example, when you have enemies which move, enemies which attack and enemies which do both, then you create two MonoBehaviours "Attacker" and "Mover". Each enemy might have the first, the second or both behaviours. Enemy-specific details (How fast does it move? How strong does it hit?) can then be configured via serialized fields on those behaviours.
If you want to add some unique enemy-specific programming to control where these enemies moves and when they attack, then you can add a separate controller script to each prefab which makes high-level decisions, but references the "Attacker" and "Mover" components on the same game object (via GetComponent<T>()
) and delegates the low level execution of those decisions to them by calling public methods.
One great advantage of this architecture is that it allows you to experiment in the editor. You can add and remove components and experiment with values without having to switch back to the IDE. For example, you can see how enemy A works if you give it the controller script of enemy B. Or if it is still challenging if you take away its Mover component, turning it into a stationary enemy. You can in many cases even perform such experiments while the game is running in the editor.