Search code examples
c#unity-game-enginemodularity

How to create modular self contained components without repeat code in each?


I'm looking for a bit of direction on an issue I keep running into. I've recently challenged myself to code very granularly, so that small pieces of functionality can exist in a single component and operate independently of any other components.

The issue that I am running into is that some components use the same functions, functions that will likely never need to differ from each other. For example, I have a generic function that you can pass a string, and it executes an animation with that name. It seems silly to copy and paste this function into two separate components that need to trigger an animation, creating two versions of the function. The only other idea I have though is creating a separate modular piece that handles all animation. The only issue is, if I do that, now my modular components require the existence of this new component to fully function. Obviously I could make it "function" without it and throw debug warnings, but ultimately it makes objects more difficult to properly configure.

I am curious if anyone has any insight into these situations, as I am imagining there's got to be a technique to it.

Thanks


Solution

  • Questions like this are kind of the heart of object-oriented programming. I'd highly recommend the "Gang of Four" book on Design Patterns.

    It is a whole textbook, but a few key approaches to your particular problem might be:

    1. Favor composition over inheritance
    2. Code to interfaces instead of classes, and
    3. Use factories to instantiate and initialize objects.

    So you could have an enum that defines the kind of animator you get:

    public enum AnimationType
    {
        Knight,
        Peasant
    }
    

    then you can have an interface that defines the animation states:

    public interface IAnimator
    {
        void Idle();
        void Walk();
        void Attack();
    } 
    

    With an interface it doesn't matter what the actual class is that's doing the work, all you care about is that it does the actions listed in the interface.

    This means that you can now have anything inherit the interface, like:

    public class AnimatorA : IAnimator
    {
        public AnimatorA(string filepath) 
        {
            // load whatever you need to
        } 
        public void Walk() 
        {
            // do the walk animation
        } 
        // also all the other things required by IAnimator
    } 
    

    You can create as many or few animator classes as you want now, and they're all interchangeable because the class that needs it is only going to refer to it as an IAnimator.

    Almost finally, now you can make a factory to get the particular things you want:

    public static class IAnimatorFactory
    {
        private const string knightPath = "/path/to/knight";
        private const string peasantPath = "/path/to/peasant";
        
        public IAnimator GetIAnimator(AnimationType animationType) 
        {
            switch(animationType) 
            {
                case AnimationType.Knight:
                    return new AnimatorA(knightPath);
                case AnimationType.Peasant:
                    return new AnimatorA(peasantPath);
             } 
        } 
    } 
    

    and then finally you can have your class use the enum, which shows as a dropdown in the inspector, and a private IAnimator, and just get the AnimationType on Start():

    public class YourActor
    {
        public AnimationType animationType;
        private IAnimator animator;
        
        void Start() 
        {
            animator = IAnimatorFactory.GetIAnimator(animationType);
        } 
        
        void Update() 
        {
            if(walkCondition) 
            {
                animator.Walk();
            } 
            // etc. 
        } 
    } 
    

    The factory is a static class, so you don't need an instance of it. Want to change the animator type? Make a new class, modify when the factory should use that new class, and you don't touch any of the user classes. Want to change the thing the class is using? Change the enum and don't touch the factory or any of the animators.

    This can add a fair bit of overhead, but paying the price for adding that infrastructure buys you easier maintenance down the road.