Well, first thing I should mention is that 99% of my coding has been done in unreal, and most of that using visual scripting. So I am not doing “real” ECS but I take the principles of it as far as I can.
Some things are easier to do in OOP way simply because the engine is setup that way. For instance, if I am working on a team and I need to have some system isolated from others to prevent merge conflicts (thats more of a visual scripting issue because they are binary assets), then I put that system into an component and it can attach to some actor/gameobject.
So at a low level that isn’t accomplishing anything that ECS is meant to, but at high level the same principles of how systems and components communicate can still be accomplished.
For UI I have been able to run it all the same as most other code. Basically a system just listens for events and then feeds data into widgets. Same thing for input - for instance Unreal has several ways to handle input, but I only read input in a single place and then that directs it whereever it needs to go via events.
Sound gets a big benefit from ECS architecture. Before I just had calls for SFX to happen strewn all throughout code. A complete nightmare to solve any issues with that. Now I just make systems like “Player SFX System”, “Enemy SFX System”, “Ambient SFX System”. Of course you can make systems smaller or broader as you need, its totally arbitrary. Usually I try to keep them as large as possible and only split up if really necessary.
These systems simply listen for events. So there may be a “projectiles system” and whenever a projectile is fired, it puts out an event. The SFX systems may be listening for that, and then they can react.
So the major benefit here is that, lets say some months from now I notice some bugs related to SFX when a missile fires… I just open the system associated with that and I can be sure that they problem will be in there. I dont have to think about a nearly infinite number of ways a missile game object might be interacting with other things, and if any other class and have to jump throughout codebase like a detective, wasting all my precious brain juices.
I keep all of the events in an abstract, globally accessible class. I guess you might call that a singleton? In unreal its either the game mode or play controller - I am certain unity has some equivalent. Basically any class can easily find this global class and then request to call one of its events. Then any system anywhere is going to receive that, but because all any class knows about is this one singleton, no systems know about each other. So systems can be added or dropped and its never going to break any code.
One area where I mix in some OOP is with animations. There is a lot of animation focused features that you can only get if you work within specific animation focused classes, and those are associated with an actor (aka gameobject). So it is just simpler to use that, though as much as possible I try to have my systems pump data and events into the animation handling class. The reason I prefer it that way is so that if I have a bug or I need to refamiliarize how things work with the animations, I dont want to have to jump around 50 places. I want to view one place that was easy to find with text search and be done.
I dont know a lot about 3rd party databases - i looked briefly at SQLite but ended up not needing it. But I dont expect that would be any different - just a system which has the responsibility to get/send data to and from the database, right?
I probably use a lot of terminology wierdly- when I saw OOP, what I mean is that both logic and state (e.g. logic = code that calculates stuff, and state = variables) are housed within a class and that class it mostly responsible for itself. Then if classes need to interact in some way, thats where my problems occur. Becomes too complicated fast. So I like a more top down approach where I can just direct what I want to happen and I can be sure that if there is something happening with, say, Missiles, I will see everything about that in the Missiles System. If it happens that having a bit of code actually happen in some gameobject is easier, I ofcourse just do that, its not the end of the world to break protocol occassionally.
An example is in an enemy AI system I’m working on. 90% of the code happens within two systems: Enemy Lifespan System and Enemy Behavior System.
But for each enemy, I use a few timers that makes them change behavior at different rates. For instance, every 3 seconds I just flip a bool and enemies that rush player use that to alternate which side they flank around.
In a System, I’d need an array or map (aka dictionary) to associate each enemy actor/gameobject with that bool, or more like, a struct of various data. That gets a bit unwiedly, so just for that timer I let it run in the enemy class, that way each instance can hold a bit of unique state and I dont have to do unwieldy data management so much. This is just convenience though and I think the wonkiness is mostly an issue of visual scripting - it’s just not great for maps, arrays, and struct stuff.
So, TLDR: Yes I still use a bit of OOP - basically whenever it is more convenient to do so, and that is usually when not using OOP means that I wouldn’t be able to use some feature of the engine easily.
I think this system makes most sense to me, because my young adult life was spent in the miitary. I think this has a lot in common with military communication methods. Basically, i dont want for privates in the field to make decisions. They should report what is happening, and then somebody in central command who is listening to all the goings-ons will tell the soldiers in the field what to do. That’s pretty much the gist of it really. If soldier A bumps into soldier B, I never want for them to make a decision what to do. They should only report that a bump has occured, and then the Commander of Bumps is going to decide what happens next.
For me as the programmer, this seems to make it simply a lot easier to read the code and know how it works with the least amount of effort.