ugh, I wrote a novel.
Honestly, it's cool, I find this all interesting to learn about and understand, and took the time to read this. My replies come from someone with no experience with JS or traditional programming languages, and simply an event-sheet guy lol.
The stacking buffs example is actually a good one. If you know in advance the types of buffs that can exist, and the number of stats they affect, then making a system for that is easy. When things become difficult is when you don't know how many types of effects will exist before hand(or the number is high), nor the objects that can be affected. Some a problem is best solved by a completely abstract system to accommodate designers future ambitions.
If designed right, couldn't this be future-proof? 2 different ways pop to mind: A data-driven way to handle buffs (i.e. you have an array of "CurrentBuffs" that grows/shrinks, and stores a Dictionary/JSON string within the array that has a list of stats to alter, maybe another dimension for timer so 10x2x1 size array, 10 being 10 different active buffs, Y0 being the JSON string, Y1 being the timer), a loop to iterate and subtract from the countdown timers in the array, but you'd only "apply" that JSON string in a function, so not needing to constantly load the JSON data every tick (e.g. players have instance vars for "Speed_Default", "Speed_Current", then can always mathematically get the final buff result when this function runs by taking the default vars and applying a buff equation). The JSON data can have keys like "Speed : 10", "Damage : 120", "ParticleType : Smoke" and whatnot, to allow you to apply these values to the player upon a buff changing. Overall being the idea of not constantly reading from the array/JSON/Dictionary, unless required such as a buff change/add/end, and a hefty yet tightly designed function to apply all stats found.
2nd idea being same thing but likely worse idea as you later mention conditional branches - An Array but with the name of the buff, and then within events, applying whatever effects based on the buff. This is also future proof as you can always add a new buff name like "Poison" and then add a new line in a function for your "AddBuff" function that checks "Array.at() = "Poison"". Again though, I get this would quickly balloon up into a huge list of subevents.
Take mario as an example: We have a character that can walk/run, duck slide, wall jump, and a few other things. Now lets give mario an inventory for items that can alter his stats, and even add completely new abilities. Without deep oop, or ecs architecture, the easiest way to solve this problem is to embed conditional branches in mario's behavior logic that checks for items containing abilities. Not too bad... so far... especially if the number is low.
But now extend that to all characters and allow any character to have any ability. Now every character needs conditional branches. Now keep adding abilities into the game. With new abilities might come the need for new stats, etc... and the base class for characters is getting hefty.
This, depending on the type of game, could be redesigned - Does every enemy really need to have all the base functionality of the player? Perhaps a fair chunk of it, yes, say you made a platformer behaviour within events and want to apply this to both Mario and Goomba enemies, then yeah I follow the paradigm of wanting to keep it all under one roof - However couldn't a single boolean to say "IsPlayer" with much of the enemy-only code be done within the loop, to dramatically lower event checks? This would of course mean you'd have to pick and choose what works for enemies and what works for players, maybe not ideal in a specific type of project.
Again I'm an event guy, but from what I've gleaned from looking at source code of C++ games, there's so sooo so many booleans checking things, state of player, state of A B C - Could be similar to what could be done here, despite the desire to lower conditional branches, one way or another you have to check some sort of value to determine whether the entity can do an action or not.
The issue at this point isn't that you can't make the game, Its that you've created a situation where adding a new behavior to a single character requires a deep understanding of all characters and all abilities in the game so far, typically requires modyifying the code base in multiple places, and if you want to remove an ability, you have to remember all the places it has code for it. This becomes unmaintainable. A single developer likely can do this if well organized- but only to a point; and as part of a team - this is nearly impossible if you have two people working on the same system.
Would the question be "why would abilities be stretching far across your event sheets"? If it was abilities such as wall jump, double jump, wouldn't this all be in the same area/group/event block sub events? I understand where maybe you need to alter the camera or something for a fast "dash" ability, but then designing this neatly is doable, functions in the general player events to affect the camera (then C3 lets us right-click the function and jump to the function event block, making this smooth to navigate).
If some features do need to stretch everywhere, then yeah it's difficult to navigate. One trick is to have a unique comment that is text that is never used anywhere else, like "@@@WallJump" then pasting this in all areas of all event sheets that are relevant, and then any dev can do CTRL+F, find this string, and the "find" dialogue presents itself as a neat list of all areas to check if you need to edit the walljump code.
The second issue is that every character, from a performance standpoint, no matter how simple, is as complicated as the most complicated character, as every character needs to have a conditional tree for every ability it could posses and the stats to go with it. Every object is bloated with data it doesn't need and this can hurt performance. We haven't even gotten to input support, ai support, or flexible and dynamic stats, etc... which also has to balloon to fit the conditionals. If you take a game like vampire survivors, and require bullets to also have abilities or dynamic effectors, you now have a code structure that kills performance so completely you have to do something about it.
I agree with the balloon moment here, lets take multiplayer game example rather than "players and enemies" as you'd expect to have exact same abilities for multiple players, it does start to impact performance when you have all these checks to do for each player. This is where I feel, lol, even more booleans to "break it down" could help. I'm not a "group disable/enable" type of user, but I often see folks use this and find there's a lot of benefit to performance - I feel booleans give you that little bit more control over things rather than groups, and utilising the booleans as much as you want for each player is a great aid, can reuse whatever you've found from collision checks such as "IsOnGround" across your ability events, even if these checks are later on and such.
Even if it's ballooned into a hectic mess of abilities, all those conditional checks don't need to be checked if a player is standing still/doing this and that. Breaking things down so that a typical tick is just going "Ok, for each player, first player, moving around, so gotta check X Y Z, do this do that, ok done... Next player, only moving right, not near walls so no wall jump checks (and wall collision is baseline check so get the data from the wall collision to help lower addition checks later)... 3rd player, they're in air, against a wall, are they pressing a key, they are, do this, do that".
In either system, the goal is to create a framework that efficiently facilitates adding functionality to objects arbitrarily at runtime, and reduces the complexity of maintaining and creating new functionality in this environment. Good for the game, good for the developer.
(Also replying to general "family" stuff here).
I agree that at face value, you cannot arbitrarily add functionality to objects at runtime - You have to know in advance, yes. And I agree with the family thing, it's not like you can make some families such as "PlayerMovement" "PlayerWallJump", etc, and then dynamically throw any object into these families at runtime, PLUS the downside of having "PlayerMovement" family with all it's instance vars like XSpeed and YSpeed, so then the "PlayerWallJump" family comes in, you cannot affect the vars from PlayerMovement family, meaning UID picking, which I agree needs to be minimal or else you're cycling through many player objects for seemingly no reason just to change a single var. Nested families would be great here.
Dictionaries almost solve this, esp with containers, but yep, cannot use containers with families so you cannot add a dictionary to a family, and definitely we all want to avoid doing "For each player, pick dictionary with var "PlayerUID" that matches player.uid", as then this has to cycle through 100's of dictionaries to pick the correct one, not ideal, almost like doing a 2nd for each loop but worse since the 31st player in the "for each player" loop, sure, it's cycling the players, but the dictionary UID check would be checking ALL dictionaries each time until it finds the correct one (or if it's based on order, then sure the first few players will find the matching dictionary quickly, but then the later players in the loop will be checking essentially all dictionaries).
Definitely room for improvement, feels so close to being attainable!!
At the end of the day, for ECS, you will have to use a Foreach loop for every system, and then loop through the components, running various custom actions like "Check" "Enter" "exit" "update" etc... The overhead of the small scale OOP to pick related families, dictionary lists of components, etc... and then dynamic routing of state/input through components, means that the performance overhead of each system scales with its complexity and its possible to achieve better performance than the simple/basic branching conditionals if the project is complicated. Also, adding functionality is as easy as the system is designed for, and allows the functionality to be self contained. apart from the fact that you still have to boiler plate every object.
I am not so fearful of "for each" loops. I tend to have a base For Each loop that contains all Player functionality, so in my mind, this is cycling each player once per tick - Of course this gets hectic and bad for performance when you need to pick a dictionary or something like I mentioned in last paragraph, in my case I haven't used families so that I can take advantage of containers (Only in recent year, after 12 or so years of Construct lol), having a base "Player" object with a dictionary in container, kinda works like an adaptable runtime-editable version of instance vars, add and remove at any time, keep it small if needed but can expand into a huge list if needed. Again though I do wish families could utilise this, hope we get to see this.
For a more "trigger-based" project, I find I use a lot of for each loops and doesn't ruin performance, mostly because of filtering down the picked objects list and rarely putting "for each" at the top of the event block - Just checked my project and I have apparently 600 for each loops (it's an 8k event project), but few are running "every tick", perhaps I've added them in some events that weren't necessary, but overall has no impact on performance. If I did have many for each loops at the base of event blocks, then yeah this would be hell on earth.
The takeaway? Construct isn't the right solution for a sufficiently complex project with high dynamic object counts. It literally becomes a case of, this isn't possible, and better off in a different engine.
I humbly disagree with "this isn't possible", could maybe agree with "better off in different engine" although for my case, what engine has a god-tier event sheet editor, GDevelop I revisit, but I cannot shake off the opinion of "clunky" and working against me (not to say it's just awful, there's cool features within the workflow), but, I have no completed project to show for this. I have made some great strides being event-only, performant platformer behaviour with custom jump-thrus and Sonic-style physics supporting up to approx 12 active online multiplayer players with equipment that both "applies buffs if holding weapon" and "applies buffs if equipped even if not currently holding weapon". I fear to come across as arrogant, but I note this as more of an explanation as to why I get a bit passionate when folks say performance is bad in C3 and such - I do see the other side though, I imagine if I recreated whatever I've done into Unity/Godot, it would let me have 24 or more players, but the project's goal wasn't to support 100 players, just 8 players was ideal! I'm making a game, not benchmarking (although it's fun to benchmark!). If I wanted a gamemode with a lot of Bot players for the 8 players online to battle, then those bots could be the same "Player" objects, but then can think outside the box to save on CPU, lesser collision checks (always the biggest CPU eater in my experience), hiding abilities that the bots never use in a boolean, or in an online multiplayer case, letting the host be the more heavier collision handler whilst players see a more broken collision bot but gets updated due to syncing from host.
For my own project, I really wanted to finish a game with construct, so I went from a bullet hell style game with high numbers to a game with fewer but much more impactful threats. It actually turned out to be good for game feel, as it reduces noise and makes choices more meaningful. Each enemy is an intimate threat, instead of one of a 1000.
Both projects sound cool - I bet the bullet hell one was a tricky moment, can't utilise Collsion Cels due to being all onscreen and such - I suppose a bullet hell is always a tricky one due to collision checking, almost feels like the type of project where you need to solve it in a minimal way, once you get that right combination of events, suddenly it will all just perform wonderfully, but cracking that sounds like an interesting challenge.