The classic example is the abstract class with overrides for unique behaviors. Yet, the situation presented in Unity doesn’t fit 100%. In the classic example there is a common, neutral base class from which derived classes provide unique behaviors. The storage of these objects is the base class, which is to say that code using this class does not know (and shouldn’t have to know) what the derived type actually is. Code using such objects rely upon the virtual behavior defined by abstract functions to handle mutation of the behavior.
In Unity, however, these classes are usually components attached to a GameObject. For those to instantiate correctly, the framework creates them, and therefore must know the derived type. It doesn’t work easily to limit Unity’s framework to only knowledge of the base type. If I assumed ‘monster’ is the base, while ‘troll’ and ‘imp’ are derived from ‘monster’, the objects you attach these scripts to must select an ‘imp’ or a ‘troll’. They can’t instantiate a ‘monster’ because it is an abstract class, and there is no factory easily accessible (that I’ve seen) for the framework to instantiate.
This isn’t a problem, you can instantiate trolls and imps without issue. My point is that the design does not 100% fit into the entire model of abstract base classes, and my summary message by detailing this observation is that, as your post hints, it may be that an abstract base class, the classic design example, has a better alternative for the situation presented in Unity.
Your thought about delegates is exactly on point in this line of thinking. Delegates have lots of potential uses, but among them is to mutate the behavior of a class similar to that of abstract base classes, but it can work better in the kind of scenario presented by Unity, where the class is applied as a component from the viewpoint of the derived object, not the base object.
Put another way, virtual functions (a C++ term) or abstract functions (the C# name for the same thing) are, under the hood, implemented as pointers to functions (or methods as C# names them). Virtual (or abstract) base classes are fashioned with an “internal” or “invisible” table of function pointers which are assigned when the derived object is configured. You can do nearly the same thing with delegates, without having to create abstract classes (in a system like Unity where they are not as easily or natively supported).
When using delegates for this design, you’d write your code to call methods using variables declared as delegates which are configured during initialization so that the class behaves in a fashion fairly identical to that expected of a virtual descendent (the derived class that overrides abstract methods in C#). The missing part, however, is the fact that a derived class can introduce new and unique member variables and methods applicable only to that derivative, so the result isn’t quite as organized and clean when those differences are complex.
Therein comes a determining factor. If the differences between your monsters are not large, and do not involve significant unique member variables, the delegate oriented approach may do quite well and offer some net gain because Unity’s design doesn’t quite lend itself to abstract base components perfectly. On the other hand, if the differences between monsters is vast, and there are a considerable number of methods and member variables applicable only to certain types of monsters, you may need to consider a different design, possibly including the abstract base.
Another alternative, however, is to consider Unity’s own use of components, and mimic that. You could establish a central monster class which is configured by attaching behavioral component classes which ultimately configure the unique behaviors defining each character. This can be quite flexible, bypasses those issue about abstract bases and does organize code well. It is also a bit more complicated, and you’ll end up with a puzzle about where methods do work (the components may often have to operate by method calls from the owning Update function, but are operating in a scope that doesn’t have access to the underlying MonoBehaviour class, and thus need a reference to it, or will be calling back to the MonoBehviour class for some certain work (like access to the gameObject or transform members).