Modular Character Behavior Archetecture in Construct, best practices

0 favourites
From the Asset Store
Casino? money? who knows? but the target is the same!
  • To add, in a practical setting, if the code for collision handling and movement is completely authored in the event sheet, using no functions or custom actions, Lets say it uses an arbitrary 20%cpu time per tick.

    Wrapping it up into custom actions might increase that to 25%. Which imo is worth it because it is creating good architecture for adding complexity. If you wrap everything that was written more than once into a function (as the rule typically goes in any other dev environment), then the cpu time can be as high as 40%. But thats wrapping things like calculating the dot product or other one-off math functions not covered by construct.

    The function wrapping does nothing except to make it easier for the programmer to make changes and adjustments, and to avoid repetition. So I don't use them in this project, though I do in others where performance isn't so constrained.

  • Yeah I've read your posts about js blocks but I'm not sure if there's a solution for it other than offloading more and more work into js directly. Keep js blocks minimal and handle the rest outside the eventsheet. Sure, Ashley could start fixing this somehow but it's one of these things that is straight up not relevant for a vast majority of users, so I can understand why he brushes it off.

    Managing per instance arrays, or dictionaries requires an amount of repetitive boiler plate every time you use it.

    This is one of the cases why I really dislike working with data structures in eventsheet context. Overboys addon does solve this exactly how I would envision it should be solved, like instance variables being able to hold arrays/json/data. But I'm at the point where I'm very comfortable in js so I'd just subclass the instance, give it a this.array and some methods to handle the data and call it a day. (If I ever manage to push out my current game, my next game will probably be 95% js)

    Anyway, I think this is fine. That's exactly why there's javascript, to handle the cases that the event engine can't. Advanced problems require advanced solutions.

  • Tbh there's lots to read here, but I'd love to see some c3p files with measurements, just for some visual feedback like "woah, functions really are X times slower" and such.

    I haven't done a vampire survivor style of game, but I've done an elaborate platformer system, where, depending on the weapons you have equipped, it can change your characters movement set, such as granting you a double jump if any of your weapon slots specifically have a weapon that grants you this, but also some weapons only grant you a buff if you are wielding it, etc.

    This worked very well with up to 12 players, all behaving independently (altho I also did a custom platformer movement that was quite collision-heavy, which is the crux of performance for me).

    What I've learned is, arrays are my best friend. I personally don't feel the use of arrays/dictionaries require much boilerplate stuff, especially if you use containers, as containers save on picking and such and only leave you needing the occasional "for 0 to array.width" (I use "for" when cycling an array, as I like to be able to use "stop loop", which you cannot use in an array's "for each element".

    Tho I may be completely off topic since not focusing on huge numbers of enemies and such, but overall, I am definitely a fan of an abundance of arrays attached to things, feels like you can manipulate and bend everything super smoothly, getting the result I desire.

  • This is one of the cases why I really dislike working with data structures in eventsheet context. Overboys addon does solve this exactly how I would envision it should be solved, like instance variables being able to hold arrays/json/data. But I'm at the point where I'm very comfortable in js so I'd just subclass the instance, give it a this.array and some methods to handle the data and call it a day. (If I ever manage to push out my current game, my next game will probably be 95% js)

    Tbh, I need to learn to do this. Ashley told me about subclassing and I was like ermergosh. The problem is that, if I wanted to use that feature, I would need to move more into JS, while the behaviors let me still use the event sheet. I'm not totally opposed, just as I mentioned, a huge appeal for me is the event sheet. Also, there is the overhead of becoming actually competent at js. I stutter and stumble, and typescript helps, but I've been using c# too long and have always disliked js. A total me problem.

    I have a hunch I am going to "compromise" on this project by going from chaos and pandemonium to stark and minimalist conflict, which imo fits the games style better anyway which is why it isn't really a "compromise" at all and a new "design decision", or at least, thats what I tell myself. lol.

  • Jase00 I like containers, but how do you use them? I mean to say, how do you afford to not have everything in families?

    Many times I want to drop things in containers, but then you lose the benefits of a family.

    Common problems for me would be something like, All entities need collision handling so they go in a family. Then I can put particular entityA with the appropriate view model in a container, but all the big logic is in the family and has no access to that relationship. Then I realize I need a family for the view models because they all need to handle animations and frame settings and effects, and now my container is broken. I haven't checked if hierarchies can side step the picking problem, as so far I have only used them for compound environments or view models.

    ---

    As far as function overhead goes, it really isn't a big deal in 90% of cases. A simple test where you compare setting a variable in a function compared to doing the same outside of one gives you the basic overhead difference. Adding parameters increases the overhead as well. Again, not a big deal if you wrap 10 events in a function, as the per event cost decreases, but loops obviously multiple this cost.

    Again, probably not a reasonable concern for most projects.

    But if you structure a project to have everything in functions, and have a lot of needed utility functions for basic stuff (like vector math or other stuff), you pay dearly for it. Which again is tolerable in alot of projects. If I recreated Nes mario3, with copious functions and custom actions, down to simulating specific nes style subpixel movement and screenline rendering, I would still be cruising at a perfectly comfortable performance with room to run the game multiple times over and tipple the on screen badguy count.

    So like Ashley said, it isn't actually a big deal as most projects have other limitations long before function overhead is a concern, and the only improvement that could happen to functions that I can figure, is that we could have a special type that inlines it's contents at runtime (basically copy pasting its contents). That improvement would be useful for utility functions (like dot product, or add score), but it wouldn't work for dynamically routed functions- meaning half of my function calls still wouldn't benefit. You could think of inline functions as simply as putting a CONST place holder for a specific set of subevents that get placed there instead of a function call when you run the project.

    Since I rely on alot of trig functions that aren't included in c3 math expressions, these functions get called thousands of times per tick. The overhead of the function call itself, typically with a minimum of 4 parameters, ends up being more costly than all the sqrts being calculated. Moving these utilities to a plugin/behavior improves performance by more than 7x in that category; Which is alot in the end, but only because my project spends a huge amount of cpu on 3 things: 1. collisions, 2. function wrapped math, 3. function driven dynamic routing for modular behavior.

    Since a good 20% of my performance is based on shoving numbers around, taking it out of functions saved me around 17% cpu time, which is definitely worth it, for me, but only because I am trying to really push the object count.

    I've done other dumb tests for things like character input buffers - like if I store most data for an instance in an array, I simplify my event sheet and logic, but I have to use foreach loops to iterate the data. If I manually go over instance variables, I can save maybe 50% the processing time, but as this accounts for a much smaller percentage of overall performance, do I care? Well, unfortunately yes, but only because I am literally trying to see just how many thousands of complex entities I can have. But, so far those are all dumb entities. Adding in ai that requires basic information about its surroundings will quickly ellipse the performance gained by avoiding an array. Pretty soon that 2.5 overall performance increase becomes 0.025, and you wasted alot of time being an idiot about performance that didn't matter (this is me 90% of the time).

    TLDR The only issue I have had with function overhead is while trying to simulate dozens of characters with 100s of bullets all needing to calculate vector normals and projections, all looping 1000s of times over small functions that could be inlined.

  • Jase00 Here is a simple but dumb test to simply compare the "cost" of doing things a particular way. You can set the loopcount to whatever makes sense given your cpu.

    drive.google.com/file/d/1MAxcL6abf63VCZxdU4dBWbthdFzSFFIk/view

    In this, you can see routed functions have, of course, the highest overhead. I use tests like this to try and anticipate my "budget" for how many function calls I can have, if I am worried about performance. If I have a 100 objects and they each call 20 routed function calls. Who cares, no problem. But a 1000 calls is non-negligible in terms of performance impact over avoiding the routing.

    At the global scope, and not looped, I totally advocate using as many functions as you need to make life easy on yourself. But in performance critical areas, you have to avoid them. If you take a setup where you have routed dynamic functions invoking behaviors, and then you also use function maps for utilities that are sprinkled throughout, you can end up calling a mapped function 20 times as the result of one behavior. Multiple that by several other active behaviors, and per object, having a function call count of over 100 is not uncommon. That makes such object types crucial to either keep counts low, or... go the sdk route for their logic. Utility functions go into a plugin, and behavior logic can either get expanded into conditional tree event sheets, or a behavior sdk/js scripts.

  • Well... that depends...

    It isn't so much about complex ai at this point, its about the need to abstract the complexity of the character handlers.

    I'll lay it out as best I can.

    In monogame with c# or in unity, you can create a framework of components that interact with each other with no performance concern. The framework that allows you to create a component based, dynamic set of character handlers in c# will involve very little overhead and running 1000 objects with that behavior with be nothing if more than a fraction of 1 percent of the cpu usage. But the same thing in c3 has tremendous overhead, without even considering adding the actual gameplay functionality and logic (like collision solving). You can already have a project running at 60-80% load with just an empty framework. As great as the event sheet/sol paradigm is, it is aweful for handing instance specific logic, when those instances must refer dynamically to other instances of unknown type. It also doesn't hadnle functions very well either. You end up with alot of ForEach Object loops, and pick another object by UID, loops. Each blank event in the event sheet has an overhead to simply iterate. Doing a forEach (1000 objects -> set variable) will basically have the overhead of processing 1000 events. While a pickAll (1000 objects -> set variable), will only have the overhead of processing one event, with an internal engine side loop to iterate the objects. The latter event is WAY faster than the former, but because we have no way of dynamically referencing other objects other than UID picking, you have to run for each loops whereever. You can create hierarchies, but if the dynamic reference is to external objects, again, you have to end up picking by uid. Sorry for the block, hopefully that all makes sense.

    The only viable way I have found is to take as much of the framework and put it into behaviors. this way you can minimize the need to refer to uid compound objects and foreachloops. But, behaviors don't take to each other very well. Scirra made behaviors like Solid and Platformer "Cheat" by having an internal engine link between the two, that you can't create because you can't extend the engine. And, since behaviors can't be extended things can get tricky there as well.

    Every Character has a number of required components. They need to know their physical state (on the ground, touching a wall, in the water, etc...), handle collisions and resolve physical overlaps, handle command inputs (move up, do thingA, etc).

    The most complex character would be something like this:

    State data: Up, down, left, right, surounding surfaceangles, water(full submerged, surface, ankledeep), edgeleft, edgeright, and a few others for testing passthrough.

    Colliders: head, foot, sides, high sides, collection, hit, + a few

    Each one of those colliders or state checks is going to require a collision check. From there, the most complicated characters with have 20+ input commands to track. Each of those needs to be buffered from frame to frame for allowing combos, etc...

    Now comes the list of potential "Characterhandlers" (routines that control how the character moves). These are conditionally applied based on input/commands and the particular state of the character. these Handlers are basically treated as abilities, with their data being stored as modifiable stats (such as lateralAcceleration)

    Ontop of that, each character has an inventory of abilities/items. With those abilities come stats that can be dynamically altered by other characters, weapons, etc...

    If you make every entity a family that handles all of the above, you get 95% more features than any entity needs. A character doesn't need the stat "acceleration", if it never accelerates. It doesn't need a state check for bonking its head, if it never can bonk its head. And so on... If you put it all in one behavior, that one behavior is also overkill for 90% of objects- and also requires a large amount of back and forth with ACES to communicate effectively with other interacting behaviors.

    The ai for all these guys is pretty dumb and basic. The issue is that a rocket, dumb as it is, is carrying fuel, is flammable, has a steering behavior with a homing ai, a number of stats such as armor, fuel capacity, etc... If the rocket was launched from a weapon that confers other abilities or stats. So a rocket, isn't always the same rocket in the game.

    And the reason to have a 1000? Because... its like vampire survivor. Not like the game, just the fact that there are alot of baddies and bullets

    Overboy 's addons help alot with instance picking. Check his itch for more info

  • RafaelMatos Thanks, I have his plugins and totally forgot he has some nice family pick sliding, etc... I was using his signals and data+ but forgot about the utilities. thanks for reminding me. Totally awesome tools.

  • Tbh, I need to learn to do this. Ashley told me about subclassing and I was like ermergosh. The problem is that, if I wanted to use that feature, I would need to move more into JS, while the behaviors let me still use the event sheet.

    It's easily one of the best features out there and I have a hunch it's woefully underutilized for how easy it is to set up. Subclassing and mixins should be high up on your list if you want to make modular stuff imo.

  • Welp, I didn't know about mixins. Anything I know about JS is merely because I program in c# and c++. Which means engine specific quirks or features are often the bane of my existence. I still forget whether I need == or === .

    Sigh... Maybe I should shutup and go learn this stuff.

  • I still forget whether I need == or ===

    Haha, most cases will be ===, you rarely need == imo

    Yes go and learn, it's probably worth it! Should be easy to pick up if you have experience with c# and c++

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • One way c# can hold your hand is access modifiers.

    Correctly used, nobody using the code can possibly misuse any variable or function. JS lets you seemingly use anything however you please. Don't want to be bothered with get/set functions? Just grab the variable and change it as you see fit. lol

    Its a real potential to shoot self in foot. Of course... c3 editor side does the same thing, since there is no way to protect variables or limit them to only some uses or scripts.

    Its a thing that doesn't make the language itself harder, just code one is working with.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)