So I am currently splitting up some prototype code in order to create a good architecture. And I am standing before a classic dilemma situation.
Let's say I decided to create one module, which contains all the stuff for my game creatures. It contains health, damage calculations, keeps track of effects, etc. In addition, I have a module, which contains all the magic stuff. It contains how spells are built, executed, what effects they have and what kind of magical stuff is going on in the world.
Now, at some point, a spell hits a creature, and the spell should inflict damage. Before, I would have queried the target and just called some kind of doDamage() method on it. However, this time, I am using a highly concurrent ECS, and I want all the different modules (actually Amethyst bundles) to be as independent as possible.
In order to solve the situation, I came up with different solutions, but they all don't really sit right, so maybe someone else (with more experience?) could give me a hint on how to go about this situation.
Idea 0: Of course, there is always the option that I just let the magic system depend on the creature module. I also have a weather system, which would have to depend on the creature, in order to simulate heat, wetness, coldness (stiffness?), damage by hail and thunderstruck, etc. And there is a quest system, which might inflict conditions as part of a quest. Lot's of dependencies and I don't know which future dependencies I might have to create. Sounds a little scary, tbh (though that is already widely practiced in package managers, like npm and cargo). If nothing else works, that's what I will roll with and hope that I can keep the dependencies to a minimum... however self-contained modules good-bye~
Idea 1: Create a messaging module, which everything else depends on. So instead of depending on various modules, the magic system just attaches an event-component to an entity and be done with it. The event may or may not be handled, which is nice for various reasons. However, such a messaging system would have several drawbacks. For one, components would have to be created and destroyed all the time and I don't think that would perform very well (might be wrong about that, though). I don't really think it would be practical, either, to just attach a messaging object to all entities and query them all the time, even when there are no messages (that might even be a lot worse than creating and destroying the components). Also, what would a messaging component contain in order to cater to not only the magic system, but also other systems, which I don't even know about, yet? How would it store the data for damage, translations, effects, etc.?
Idea 2: Putting the logic and data together into the individual bundles might be a flawed design by itself in this case. So maybe I should instead put the logic into a central place (or, alternatively, the data?), which has access to all modules with their data. This approach would instantly solve a lot (all?) of my problems, however I would be unable to have separated drop-in modules. Whenever I want to use a module, I would have to write some glue, which is ok for now, but might be annoying when I want to re-use the bundles or make them available to other people, who also need creatures and a magic system like mine. It just does not feel right. Well, this might be more of a problem with the idealist inside me than a practical problem, however I would love to have options and then decide for the least unsettling solution :D
Idea 3: Create "glue modules". If you want to use the magic system with creatures, you need to also have a magic-creature-bundle, which contains all the interaction logic between those two. Hell, this one sounds wrong just thinking about it, but at the same time also solves the problem. Well, the catch is: What if I want to also have a third bundle interact with the first two? I would have to create a new glue module. For every possible interaction, I would have to create a new glue module. Oof! That would increase the boilerplate by an order of scary, so I don't wanna do it.
And all that with only 2-3 bundles. Remember, that I also need a bundle for other stuff, like structures, terrain,... which might depend on one another, too. Any input on this?
For context, I am talking about Amethyst, a data-driven game engine implemented in Rust using an Entity Component System (ECS)
Do some research on modeling languages for behaviors and behavior trees. You are on the right track with idea 1.
Don’t destroy actions, cache them for later use.
I don't think I have very much to contribute for the architecture part, but I know people who work in games and animations and things where the types of things you're describing (characters, weather, properties) need to be changed by modular code in a larger environment, and they can talk at length about Finite State Machines (FSM), rather than OOP-style for this sort of thing.
So I don't know if you know about FSMs, or if they would be of help here. I'm sorry I can't understand enough to recommend anything myself, but I hope you can find a model that works for you :D
(I think it would mean you are storing the state of everything in the environment as data somewhere and all your modular code acts on that data, modifying the state - rather than speaking to other code)
Do you by chance have a dependency diagram of your model?
What language? The only programming thing called "ECS" I'm familiar with involves cryptocurrency on Amazon's cloud; which seems utterly unrelated to what you're doing. Not sure what "Amethyst bundles" even are... and my Google-fu failed me on both.
Oh wait, you said NPM and Cargo, so this is JavaScript, right? Well there's your problem.
What you are trying to do is a prime candidate for objects that inherit... If you could implement proper object inheritance using conventional includes you'd just register the new objects having them inherit off old ones... but with modules isolating their own scope forcing you to require inside your require to require inside that require with ZERO way to just flat include a file... well, I'm just going to come right out and say it -- it's a really dumbass system in usage scenarios like this.
Because of the limitations, I'd be half-tempted to create my own build process to chain together separate easy to maintain files into a single monolithic .js to just skip the ENTIRE module/export/require thing altogether. A "linker" for lack of a better term... just have it parse the file line-by-line looking for lines starting with #include to implement them just like a C compiler would.
That may sound a bit strange, but then you could use proper/normal objects that can inherit off each-other, register themselves into lists on parent objects, and so forth. You go from dependency hell to namespace hell, but that's why just bothering to use proper/unique names or maybe maintain a cross-reference is important. Oh for the days when a proper cross-referencing utility was at the TOP of the toolbox.
I actually used this approach recently on a client's back-end to fix some SEVERE overhead issues introduced by all of their require() needing to require() resulting in multiple loads and copies being made over and over, the resulting system not just a pain to maintain, but also chewing on RAM like candy and dragging processing time under as well. The only practical solution since everything was always required() (no conditional requires thankfully) was to switch to a single monolithic file since throwing more hardware at it wasn't a viable answer.
Though if there are modules being required() that are only done so conditionally, said approach isn't an option... well, unless you can 'afford' to load dead code.
There are times that node.js modules are a godsend, then there are times where you end up screaming at the display "Oh for Pete's sake just let me do an include like every other blasted programming language in current use!"
I have the nasty feeling you're butting heads with the latter. You reach this point where the require of a require of a require just results in this bloated convoluted mess... one that makes you end up being afraid to tug on anything because you lose track of where it is connected. When it's one require() of a parent prototype it's bloated methodology, but not the end of the world and often the easiest way to 'think' about it... when you're doing 200 of them you really have to question the wisdom of the approach.