Announcement

Collapse
No announcement yet.

How do you separate concerns of your classes when interfacing with blueprints?

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    How do you separate concerns of your classes when interfacing with blueprints?

    Hi,

    The game I am working on is starting to have an increasing number of god classes, most about 1000-2000 lines but some going higher.
    They are mainly actors and components.
    How do you break them up while keeping them exposed to be customizable with blueprints but also allowing them to be properly tested?


    Lets say I have a component that exposes some data to blueprint and I decide to break it into 3 classes.
    Some of these classes have references to each other and now parts of the data exposed to blueprint are needed in different classes.

    One solution would be to have a class being made into a component, expose all the data needed for all of them and create the other classes.
    This makes it difficult to test the logic of only the component because it instanciates the objects.

    Another solution would be to make all three classes into components, each exposing the data each needs and having references to each other.
    The actor would be responsible to wire them.
    This allow the logic to be independently tested but has the overhead of more components in the actor.
    Optionally you could have each component also implement an interface and have the other classes depend on the interface to reduce coupling, again with the actor wiring them.
    This would make the actor now hard to test independently.

    And there is no easy way to remove the wiring from the actors because it is Unreal who creates them originally when the level starts and Unreal does not do the wiring for you.
    If it is not the actor, you would need a callback after level creation to initialize the actors, but then you might as well create a dependency injection framework for Unreal and it is not priority right now.

    The only way I see to reduce actor responsibility is to create more component classes to execute the logic and have the actors being merely delegators to these components.
    To allow more flexibility perhaps you could instead of creating the components in the actor, have the components be BlueprintSpawnableComponent and create them in blueprint.
    Inside BeginPlay you could use one of the GetComponentsBy methods to retrieve the components needed to do the wiring.
    This would increase the overhead of creating the actor.


    How do you keep class responsibility low in your projects while still interfacing with blueprints?

    Refactoring these classes might not be done now but would be nice to have these options in mind when introducing new code.

    #2
    1,000 lines isn't so bad. I think the worst file I've seen was 36,000 lines; that was pretty bad. (A full Java VM implementation, in a single file, if memory serves.)
    Thousands of lines seems to work for Unreal, too: UObject.cpp is 4600 lines.

    Large classes are a concern, of course, especially when the INTERFACE for those objects becomes large (hundreds of functions to keep track of; some of them virtual, others not? Not good.)
    But implementations sometimes need to be wordy. If code isn't used by more than one code path, re-factoring it to a separate function in the same namespace/class/file, only called from one place, doesn't really give much improvement to the flow, unless it's actually a clearly defined unit that can also have its own unit test written for it.

    But sometimes, you really do need most of the features for most of the users, and trying to separate every little fraction into a separate component may be technically possible, but it also may add runtime overhead, it may add cognitive overhead (especially when the components aren't as independent as you'd want,) and it may add time overhead working with it.
    Your end goal is to ship a game that's fun and doesn't have bugs. If you find that browsing through a 2,000 line source file causes you to work a lot slower and causes many more bugs, then by all means, find a better factoring! But if you can still develop with good velocity, and there aren't any lurking bugs in the code, it's probably fine the way it is.

    If you end up wanting to re-use the code, you can always factor it AT THE TIME YOU NEED TO -- because you'll have better knowledge about the shared use cases for the code once you actually have the shared use cases! Premature factoring often gets in the way more than it helps.

    there is no easy way to remove the wiring from the actors because it is Unreal who creates them originally
    You can easily create subobjects in your actor subclass in the constructor. You could then create many actor C++ classes that each create the set of sub-components they need. If this ends up being cumbersome, then you should take that as a hint that, actually, the bigger shared class might be the right choice in this instance.

    Note: I'm not saying that you have a blank check to write your entire game in a single ball of mud. That will end up with bugs and be very hard to diagnose to make it run fast. What I'm saying is that you should apply the right amount of factoring to your particular project, to increase the chance of shipping a fun game that doesn't have bugs. Exactly what level that is, is up to you, but you should LISTEN TO THE CODE -- the code will tell you the amount of factoring needed will. Don't listen to random strangers on the internet who have opinions on software engineering :-D

    Comment


      #3
      I have a UStruct over 7000 lines of code (operators overloading) and a header file over 20,000 lines, but you didn't read this from me...
      | Savior | USQLite | FSM | Object Pool | Sound Occlusion | Property Transfer | Magic Nodes | MORE |

      Comment


        #4
        Thanks for sharing your experiences.

        You can easily create subobjects in your actor subclass in the constructor. You could then create many actor C++ classes that each create the set of sub-components they need.
        Let me try to explain what I was trying to say.
        Unreal creates the initial actors of the level with default constructor and does nothing more. The actor has to initialize itself.
        That is not the same thing the actor is doing with its components, the actor creates the components and initializes them by wiring their dependencies so they do not have to.
        This makes it easier to test the components because you can easily mock their dependencies and initialize them.
        It is even easier if the components depend on an interface that another component implements.

        In the actor's case, because it has to initialize itself, it creates unnecessary objects when testing with its dependencies mocked. Which is not too bad per se.
        But because components can not be nested, actors end up with dependencies with a lot components and having to wire them. Mocking all of their dependencies becomes too much work.
        That is why making actors into delegators may be interesting. If they are just a delegator they do not need to be unit tested.

        Also, if classes do not create their dependencies they become more extendable. Actors can let the blueprint create their components for them.
        For example if instead of creating a UMyMovementComponent, the actor checked to see it had any component extending UMyMovementComponent,
        you can modify the actor's behavior by creating a different UMyMovementComponent in the blueprint, without touching the actor's code or creating a child actor.
        That of course comes at a performance cost of checking if it has the component in its BeginPlay.

        Comment

        Working...
        X