Modular Character Behavior Archetecture in Construct, best practices

0 favourites
From the Asset Store
Casino? money? who knows? but the target is the same!
  • Hey all,

    I know this is a topic that I have brought up before, but I wanted some recent input on design. Consider the topic of creating a platformer project, like mario, but where mario's abilities and behaviors are dynamically altered based on various factors, such as equipment.

    For those who program, (For me, c# in unity or monogame), I would typically create a character handler that would handle states and delegate which particular behavior should have control over the character (run, walk, jumping, moving in air, wall sliding, etc...). When a a new ability is added to the character, it would register its various functions on actions/callbacks that are triggered by the handler. Some people use state machines to handle the transitions, but I preffered a softer machine where multiple states run at the same time and can share control (for example, sometimes you want the "horizontal movement" ability to execute while "rocket jump" is executing.

    Its nice, because then adding a "power jump that charges while ducking" allows the code for that ability to live in one place, instead of having to be distributed around a giant if/else tree in various locations. You just register into the appropriate action/callback. In this case, the handler would call any ability that has registered with the particular input/state conditions that result in duck being true, and the power jump would charge.

    Ideally, these abilities all inherit from some base class that handles the boilerplate registration and callbacks. This is easier to manage with polymorphism and class imo, but I suspect there is an elegant way to handle this in c3.

    Elegance = Easily scalable, maintainable, and changeable code that is the easiest to read and work with.

    Building such a system out with c3 function maps doesn't seem the right way to go, as it clutters the global name space (Not ELEGANTO!) and has some performance issues. Custom actions is a better solution, but without dynamic routing, you have to wrap up abilities in objects. So even though you have a mario characterHandler object, his jump logic is actually being processed by a custom action on a "BehaviorJump" object that is in a family of CharacterAbilities. The CharacterHandler familily handles a list of UIDs to its abilities , and simply calls the type of action wanted.

    This current solution has got a lot of UID picking, and is verbose. Alot of boilerplate. But it is as elegant as I can imagine atm.

    You can easily build up a giant spaghetti mess of character abilities all being togled on and off by boolean trees, but that gets to be a nightmare once you start trying to add dynamic abilities that can be picked up with an item or activated in specific situations. I know some famous gamemakers have said, screw it, and then just made a mess (celeste's character controler is a grand example of this), but the fact is, a working mess is only a problem if you have to go in and change things.

    AND the whole point of the project is to provide a reference for design patterns for those building projects along SOLID principles, so the matter is somewhat academic.

  • In my experience most elegant and readable is less abstractions for me. A tree of conditions and some variables should be enough. But we all have our own styles of what we consider elegant code.

  • R0J0hound Do you tend to mix all abilities in the same conditional tree, or do you keep seperate trees for non-intersecting behaviors? For simple things, I totally agree, abstraction can be an added difficulty that solves no issue, but... for slightly more complicated issues...

    For example, if pressing down normally causes a character to duck while on ground, but a digger upgraded character digs into the ground, and perhaps another ability causes some jump activated power to charge.... would you simply prefer to have those all under the condition tree as such, or do I misunderstand? For example:

    	if (onGround) {
    		if (isFirstTickonGround) {
    			if(isRecoveringFromSpecial) //Do special landing thing
    			elseif (hasJumpAbility & 
    				input.jumpLastpressedTime < forgivness)
    				//Do regular jump
    
    		}
    		
    		elseif (input.down) {
    			if(hasChargeAbility) //do charge stuff;
    			if(hasDigUpgrade) //do dig stuff;
    			else //do normal duck;
    		}
    		elseif (input.jump) {
    			if (hasChargeAbility & isChargeComplete) //Do Special
    			elseif (hasJumpAbility) //Do Regular Jump;
    		}
    		
    		
    	}
    
    	//Somewhere else in the tree
    	if(isInWater)
    		if(isRecoveringFromSpecial) //do specialstuff
    		else etc... // do water stuff
    	
    

    Basically, the logic for the special gets mixed in various places under various input or state conditions. Do you find it difficult to maintain such code into the future; Removing, or adding abilities, etc, forgettign how many different places the special charge ability touches? I have found such trees are easy to make initially, and the process is quite organic. When making a game like mario, where mario is the most complicated character, but has a limited and static set of behaviors, and most enemies are simplified subsets of mario abilities, I think the ease of a conditional trees is probably the best approach. But I canʻt see how this wouldnt become a tangled nightmare for a game where abilities are dynamically added and removed in any combination, or where all characters are more complicated than mario but at the same time use his behavior as a base framework to overide.

    As opposed to something more like:

    	//in class for ability dig
    
    	CanDig() { return this.state.OnGround & this.input.down }
    	DoDig() {
    		//Code for digging behavior
    	}
    
    	//in class for Charge Ability
    
    	CanCharge() { return this.state.OnGround & this.input.down }
    	DoCharge() { //code for handling the charging }
    
    	CanDoSpecial {return this.state.OnGround & this.input.Onjump }
    	DoSpecial() { //code for special charge ability }
    
    	//in class for jump ability
    
    	CanJump() {
    		return
    			this.state.ground & this.input.Onjump ||
    			this.state.ground & this.input.lastJumpPressedtime< f||
    			this.state.lastexitgroundTime < f & input.OnJump }
    	doJump() { //Code for handling jump }
    
    	
    	//Somewhere else in characterHandler
    	if (noActiveAbility) {
    		//iterate through dynamic list of registered "CanChecks()"
    		//Compare priorities and set Active ability to the ability
    		if non, set to default
    	}
    	DoActivateAbility();
    	
    

    In the above, the code itself is more cleanly contained imo, but there isnʻt a good way to acomplish this on construct as far as I have found. Not easily, or performantly. Which I think we can all agree, trading 1 problem for 2 more is particularly elegant lol.

  • What do you think about activating/deactivating groups? Not elegant in terms of number of events/lines but super simple and easy to maintain.

    Got digger gear? Disable the normal duck movement group and enable the digger variant group.

    Got the double jump? Enable that sub section of the jump folder that allows for it.. or for simplicity just disable the normal jump group and enable the double jump group.

    Your character is any combination of enabled and disabled groups/behaviours this way.

    I have no idea how this compares in terms of performance but I would imagine it would work fairly well.

  • Yeah I’d do something like your first example and I’d refactor it as I go. Any abstractions would come organically as they made sense to simplify things. Basically I figure it out as I go.

    The second example is less readable to me.

  • What do you think about activating/deactivating groups? Not elegant in terms of number of events/lines but super simple and easy to maintain.

    Got digger gear? Disable the normal duck movement group and enable the digger variant group.

    Got the double jump? Enable that sub section of the jump folder that allows for it.. or for simplicity just disable the normal jump group and enable the double jump group.

    Your character is any combination of enabled and disabled groups/behaviours this way.

    I have no idea how this compares in terms of performance but I would imagine it would work fairly well.

    I may be stupid here, so correct me if I am wrong, but groups don't work on a per instance basis. If a group is disabled, its disabled. In the end, the group functions as a simple conditional anyway, I believe, so it is basically the same cost as have a condition. I actually made my first platformer ever in c2 using such a method.

    The biggest issues I face comes down to performance, so there is a real possibility that brute forcing all possible ability checks on every active character will be costly. At least, based on preliminary tests I did a few months ago

  • Absolutely, I assumed you meant single player. If you are planning for coop then it's not going to be an elegant option.

    A shame too if you are really looking for performance. The difference between the group and a conditional as I understand it is (forgive me if I am wrong here), disabling the group means the code isn't run at all so it's less work by not having to run through the tree for the same result.

  • Absolutely, I assumed you meant single player. If you are planning for coop then it's not going to be an elegant option.

    A shame too if you are really looking for performance. The difference between the group and a conditional as I understand it is (forgive me if I am wrong here), disabling the group means the code isn't run at all so it's less work by not having to run through the tree for the same result.

    Well... it is single player at the moment... But enemies aren't any different than the player (they pick up gear, have abilities, and in some cases, the player can control them). So they all need the same abstract hook ins for control and handling. In the end, the underlying core behaviors (custom platformer/8direction) is applicable to most characters, even if they have some of it stripped out.

    As far as the group goes, The performance of group checks wouldn't make a big deal at all. I use groups for game state, and other big "Only this or that" type behaviors, like UI, for ex.

    The performance that negatively impacts my projects is usually always because a.) I want a 1000x , and b.) I need those x-objects to have dozens of functions calls, usually to vector math or other trig functions, and a variety of hookins for dynamic ability routing. Construct3 doesn't inline function calls, so a simple function to calculate the dot product will be 5x slower in a function call than if you just create an action for it. Functions with a single ACE and a few parameters have terrible performance/convenience tradeoffs, if you are going for volume. For dynamic routing, conditional trees are okay, but the larger the number of conditions, the more easily it is to justify dynamic callbacks and other routing patterns.

    In the end, I have to end up making behaviors to contain alot of those simple functions. But once you get higher up the food chain, for example creating a platformer behavior that is modular, you can't extend existing behaviors and you either suffer creating behaviors that essentially duplicate function - or you have to find a way to abstract from within construct, which usually requires dynamic routing (ie, function calls).

    imo, there isn't a good solution in c3 for creating abstract, scalable patterns. Not in projects that have heavy object counts. They do work perfectly fine in games that keep such patterns limited to a few dozen objects. This is totally doable.

    But it always comes down to the stress test. I mean, I fit into that camp of people that gets annoyed if I have to do anything where you have to repeat yourself, but abstraction comes at a cost in c3.

    As far as groups go... I think ( knowledge based on c2 circa 2015) that groups were the exact same as system condition evaluating a bool... likely one picked from a dictionary with key "groupName" and it still gets checked everytick. I might very well be wrong on that but I think that's what Ashley said.

  • Unless I am interpreting the manual wrong, I think conditions still have to be checked whereas disabled groups are just skipped altogether.

    But yeah, when you talk about the scale I understand the dilemma. Depending how smart you want the AI it gets out of hand quickly. I know it's not a particular strength of construct but would any engine be able to handle that really? 1000+ dynamic characters on screen at once.

    With that number it seems essential to have super basic AI for those too far away to meaningfully interact with the player and then switch them into the more advanced tree when they get closer. I would love to see an example that shows otherwise though.

  • 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

  • And I should make clear: Wrapping up functionality from the event sheet and putting it into a single Action call in a behavior can save alot of performance, especially where eliminating event based for each loops and functions calls.

    But what I don't like is that creating behaviors to do these sort of things often involve duplicating portions of pre-existing behaviors.

    My stat system is basically a dictionary, etc...

    Worst part is, the interfacing between c3 events and complex behaviors will also end up with overhead. For example, there is no way to dynamically trigger an Action from an action without passing in a token and parsing it. In c#, you could simply save any function into an action variable, and the line actionVariable?.Invoke(); would essentially call any functions assigned to it via actionVariable += anyObject.someFunction(); Sure, there are also methods in javascript for doing this to, but communicating from the event sheet into the world of javascript has limitations.

    Now, delegates in c# do have overhead as well, but its negligible at the scale I am talking about. My unity project has a dynamic system with 20+ action variables that all invoke however many functions are set to them for every object.

  • Thanks Ruskul. I really appreciate you taking the time to explain it like that. I had no idea you could achieve something like that in unity. It honestly sounds amazing.

    There are still a lot of things I don't understand about the limitations of events vs scripting and what stops bridges being made between the two (not for lack of trying!). As someone who is relying on the event system being as powerful as possible, I wish that more resources were delegated into making these kind of projects easier. Either through solid examples or more official behaviours better suited to the task - or both!

    Your game sounds great too btw, I hope you can find a solution in c3. Would be happy to do some testing for you when you need it!

  • Thanks for your interest! I'll let you know.

    I fully committed my current project to c3, so one way or another, I intend to find a solution or compromise. I headed into this knowing that. :D

    Part of the reason to stay in construct for me was that I wanted the scope of the project to stay limited, even if that was because construct was the limitation. I make WAY too many frameworks and abstrations in unity, instead of just making the blasted game. You can go down some deep, deep, rabbit holes in programming if you aren't careful... and while those deep dives are fun, and would be useful in some constexts, they can be time sucking over-reaches for solo devs.... cab be and usually are usually every time (for me at least).

  • Try Construct 3

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

    Try Now Construct 3 users don't see these ads
  • Since you're already writing custom behaviors, what about just doing it in js directly? Surely the stuff you're looking for can be done with some js. Arguably there are cases that the event engine will not be able to handle as elegantly, but most games don't actually require this amount of complexity.

    Doing it via events, I'd think of adding an array per instance, with strings that then call mapped functions per string. If a string doesn't exist, nothing is called or acted upon, so no further overhead.

  • Doing it via events, I'd think of adding an array per instance, with strings that then call mapped functions per string. If a string doesn't exist, nothing is called or acted upon, so no further overhead.

    That does seem to be the only way. In vanilla c3, there are 2 concerns for a project with high object count:

    1.) The simple act of calling a Function has overhead. It does in any programming context, but the overhead in construct is significant at scale, (a function call that simply sets a variable from a parameter will take 3-5x longer than simply running that event outside of a function. Since you can't call a mapped function without a router function, (if you want to use parameters), every dynamic function calls becomes 2 function calls (6x-10x) the time of simply running the simple event, requires a way to manage tokens, etc... So the net result is that dynamic function routing when used liberally comes at a huge cost.

    2.) Managing per instance arrays, or dictionaries requires an amount of repetitive boiler plate every time you use it. For each object, pick array by uid, for each element, function call .... This is itself a perfect thing to abstract away, and you could use functions to do it, but now you have functions inside of foreach loops. The performance loss of foreach loops is sizable when object counts are high, as it duplicates the overhead of simply running a blank event. So things get messy where you prefer the for each loop inside of the function and not the other way around.

    The solution is as you say, put as much of the nec looping in javascript and eliminate all that you can editor side. Overboy's UID to anything is helpful, but it can only go straight to common expressions (like variables, position, etc, and can't hook into a behavior. I often create simple behaviors simply to hold data, so that iterating with that data, etc can happen in in the behavior instead of events. You can also create getters and setters that run at a tiny fraction the cost of doing that with a eventsheet function. Overboy's Data+ essentially does that as well and is a fine tool - much better than authoring it yourself.

    Overboy's Advanced signals, also function like the old c2 functions, so you don't need to call two functions to route a signal. They also function per instance in lovely fashion, allow for polymorphism with out setting up boilerplate code. And he has custom expressions. Working in c3 with Overboy's tools makes it much more scalable and easy to work with. But, if performance is still the issue, Js is probably the only solution, either plugin sdk or the editor side JS.

    I have a post about it, but the takeaway is that using editor js has a lot of restrictions in how you use it, or you might end up with worse performance than not using it at all. For example, I see many people say calling a function from js is easier, than using mapped functions. I agree. But the overhead of a JS block is huge compared to basic actions or c3 function calls. Like 300x worse. Again, this isn't a problem if your JS contains more than one line of code. But putting JS blocks in any kind of loop destroys any performance gained from the logic being JS instead of ACES.

    Basically, every ACE has an overhead. But a JS block does too, just way more. So in a way it's much like the function. If the JS block is eliminating the need for editor side loops via event sheets, then you are winning, but if you are using JS more like fancy functions to wrap utilities in, you are probably going to have worse performance. Behaviors on the other hand, for whatever reason, do not invoke the same expensive overhead when they are called. Ashley seemingly brushed off the concern with the usual, "but no real game..." He isn't wrong, so long as the valid selection of real games are only those that aren't object heavy in the way a vampire survivor game is. He also isn't particularly wrong to be unconcerned about it, as its not a bottleneck many users will face. But imo, using eventside js, is worse than behavior side, though it is easier to author.

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