The problem
Short TLDR: I am wondering what’s the simplest, cleanest and least boilerplate-heavy way to have actor components depend on other actor components and interact with them.
Longer version:
I’m posting this here because I would like to know best practices or idiomatic approaches to the sort of problem I’m repeatedly having.
Say you have an actor with various components handling logic, but a lot of those components need to interact with each other in various ways. Here are a few baseline things that made me want to split logic in components and keep characters as empty as possible:
- The vast majority of my gameplay logic should be shared between almost any character, but I don’t necessarily want all of them to share the actual character logic. I want to keep the ability to create new character blueprints that don’t inherit from some big master class that might be too inflexible
- I want to avoid code duplication in different character types as much as possible, because when this happens I have to duplicate any changes I make to multiple character blueprints.
- Composition over inheritance: Keeping the character itself as lean as possible should make it easy to create new types of characters that combine components in specific ways.
Possibly relevant detail: Most of my game engine experience is with engines based on entities that all have transforms, components and children entities, like Unity. I’m still often a bit confused by how Unreal actually wants to be used, hence this question.
How do we get the dependencies
The main problem here is simply that within components themselves, there’s no easy way to interact with the other components. Here are a few options:
- Get the owner, cast to your character type, get the component: pretty bad, creates a hard reference from component to character, probably a circular dependency, not very reusable
- Give a blueprint interface to the owner with functions to return other components: Better, no hard dependency on the character, but you have hard dependency on the other components. Cons: Every implementation of character needs boilerplate code to implement interface functions to return the components they use. Components still have hard references to each other, which can create circular dependencies.
- Same BPI idea, but instead of returning components they have functions for specific things they can do, which use components behind the scenes. Pros: No dependencies, basically event-driven code. Cons: Tedious: Characters require lots of boilerplate code which often just wraps a function in a component, which sort of breaks the benefit of splitting logic into components. This is how most of my project is done so far, and by character just has way too many BPI functions that are basically just returning what another component returns. Pros: The functions in the character can be tweaked on a per-character basis, which is sometimes useful.
- What I’ve started doing lately: A blueprint interface for each component type, then we get components by interface from the owner when we need to interact with them. Pros: No dependencies, event-driven code, components manage their own logic without having to know how the parent works, and the characters don’t need to know anything about it. This is the one that requires the least code on the character level. Cons: Doing this requires tedious calls, casts, and checking that the component array isn’t empty. It’s a lot of boilerplate whenever a component wants to use another. Also, creating BPIs for every component feels redundant.
Generally, in all the cleaner approaches using interfaces or functions to return components, there is still a common problem of boilerplate code:
The component needs to interact with various of the other components, which means on begin play finding them and setting them to variables. This is very tedious boilerplate code.
I wish there was some kind of clean way of specifying component dependencies in a component and then being able to automatically send them messages, maybe via interface, but without having to go through the whole process of manually finding them first, or having the owner actor provide functions to return them.