I code my games exactly like this as well. It becomes pretty easy to drop in all sorts of behavior into any actor. I first realized games in Unity are all built this way since that’s just how game objects are put together. I now hardly ever have any actor code aside from setting up interactions between the components.
I actually use interfaces a ton mostly to help find the component in O(1) time instead of calling, FindComponentByClass on the actor. All the interface does is it returns the appropriate component. A lot of my code just tries to call interface methods on different objects and if that interface is implemented and returns non-null, my code continues. Otherwise it assumes that behavior doesn’t exist on the object I’m calling the interface on. Only issue is if I forget to make an actor implement an interface and just add a component of some type, my behavior doesn’t work.
One great example is an inventory manager component that can exist in a character, treasure chest, dead body, random tree in the world. My character can walk up to any object and interact with its inventory component and see its contents. It’s super easy to just drop any actor into the world, and configure what inventory items it has in its inventory.
I can also very easily make interactable “Usable” objects by having a User component and a Usable component. User components keep track of Usable components that can currently be “Used”. This is done by knowing which ones are currently overlapping with collision geometry and priority is given to the “Usable” if the owner actor’s GetActorEyesViewpoint is aiming at it, otherwise it choses the closest one. I can make my player hit the “Interact” button and it’ll take care of calling OnUsed on some appropriate UsableComponent. The Usable component just has a OnUsed callback that the actor can implement however it sees fit. It’s very easy to build a level, throw in a random actor that has a usable component, and hook up a level blueprint to do something when that Usable is triggered by a User.
I even do this for tiny things like controlling how my first person character’s hands animate. I used to put things like that into the ACharacter subclass, but now I really put most behavior into components. I try to isolate specific behaviors into components and have that component only do that thing. It does get a bit hard though since sometimes components need to talk to other components, or it’s actually not that obvious how to split the behavior up and I end up doing a somewhat bigger component that does more things than it should. After a while I find ways to refactor it.
My code is also currently not built for multiplayer, but I think it’ll be much easier to go through component by component and add proper replication and other things when the time comes, since I have all of my behavior already split up into tiny manageable chunks.
One thing I used an interface for is an idea I got from Unreal Tournament source. There’s a Reset interface. The start of a match or round fetches all instances of things that implement the reset interface to put the game back to normal. This can help with games like Counterstrike where you don’t want to reload a map every round. Things like bullets, shell casings, impacts, can kill themselves on reset so they disappear. Respawnable items, etc… can ensure they’re back in their initial state.