How would i make a state machine modular? I want an ability system that can add or remove the abilities whenever.
Is it possible using a state machine such as the one i have now?

  • xyz replied to this.

    SuperMatCat24 What benefit would you get with "modularity"?
    You can simply make all possible states and just enable/disable entering them according to abilities.

      xyz I'd like to have multiple player characters share the same base scene, with the main node handling the creation of character-specific abilities, changing textures and such. Adding a new node for each character in once scene, each with multiple possible abilities would be quite wasteful and might make the player scene unnecessarily messy.

      • xyz replied to this.

        SuperMatCat24 would be quite wasteful and might make the player scene unnecessarily messy.

        How would that be wasteful? Godot can handle hundreds, thousands of nodes without problems. In the case of your setup, only one state node is processed at a time. So not much waste there either. Best to try it out and see how it runs. Unlikely there'll be any bottlenecks.

        The state machine implementation you're using is already relatively messy and wasteful, requiring an instantiated node per state per entity and a new class per state per entity type. In 4.x I'd go with node-less implementation based on Callables. Much more lightweight and flexible.

          AMKAMH Cry. Shall never be fixed for the gods hath spoken. All textures shalt have lines in them and that is how it will be for eons to come.

          xyz That seems great! Are there any tutorials for callables I could use? I've never really given them much thought.

          • xyz replied to this.

            SuperMatCat24 Don't know about tutorials because I don't use them. But there should be something around the web.

            Callables are just references to methods. So you can immediately see how a state machine can make good use of them, ditching the requirement for dedicated nodes/classes to host each state's functionality.

            SuperMatCat24 Since we're talking state machines, let me just say that guys at Gdquest should do something about that tutorial. Although the implementation shown there does its job, it really pulls it off in a clunky overcomplicated way, using many things that should really not be in an introductory state machine tutorial for beginners. Among those are: dogmatic oo "everything is a class" approach, virtual calls, awaits/yields, misuse of node's owner property, parent node references, using strings for state identifiers... I could go on.

            The biggest problem is that this tutorial is extremely popular. Every single beginner posting here about their problems with state machines copied that approach. Many people think that this is the way to do state management in Godot. On top of that, it's for 3.x and for some reason everybody does it in 4.x, leading to additional frustration.

            With introduction of callables in 4.x, this type of state machine implementation becomes utterly obsolete. Note that even without callables a state machine can be implemented in much less convoluted way.

            So here's my implementation of a simple state machine:

            class_name SM
            
            enum {ENTER, EXIT, PROCESS}
            var states; var current
            
            func _init(s):
            	states = s
            
            func switch(new):
            	var old = current
            	current = states[new] if states.has(new) else null
            	if current != old:
            		if old and old.has(EXIT): old[EXIT].call()
            		if current and current.has(ENTER): current[ENTER].call()
            	
            func process(delta):
            	if current and current.has(PROCESS): current[PROCESS].call(delta)

            That's all there is to it. It can handle any number of states with 3 basic callbacks per state (enter, exit, process). The functionality can be easily extended. The state callbacks are held in a dictionary which can be indexed with any type of key we choose. The usage is simple as well:

            extends Node
            
            enum {IDLE, WALK}
            var sm:= SM.new({	IDLE: {SM.PROCESS: _idle_process},
            			WALK: {SM.PROCESS: _walk_process, SM.ENTER: _walk_enter, SM.EXIT: _walk_exit}})
            func _ready():
            	sm.switch(IDLE)
            
            func _process(delta):
            	sm.process(delta)
            
            func _idle_process(delta): print("IDLE PROCESS")
            func _walk_enter(): print("WALK ENTER")
            func _walk_exit(): print("WALK EXIT")
            func _walk_process(delta): print("WALK PROCESS")

            Just initialize the SM object with a dictionary of state callbacks and call SM::process() each frame. Switch the state by calling SM::switch().

            All that is left to do is writing actual state callbacks. Each of those can be placed in any script or even passed as lambda. In most cases the best place obviously is the script of an object whose state we're managing so all its properties are directly available.

            There you have it. Only 15 lines of state machine class code, no additional nodes or convoluted setups, easily extended. Just instantiate the SM object, give it the callbacks and switch as needed.

              xyz i've been there with GDquest. they really drop the ball on "for beginners" at times. hard.
              i'm pretty convinced await and yield exist solely as a prank on newbies. in the 4-5 years i've committed to gdscript, i have never used either outside of a GDquest tutorial. to this day, i've only found more reasons to find any alternative possible.
              the same goes for using string identifiers instead of enumerations. even as a lil fledgling i thought something wasn't right with that.

              • Toxe replied to this.

                packrat await is pretty useful, for example waiting for timeouts, signals in general or the next frame.

                  Toxe i've seen timeouts the way they're normally done in the Godotsphere. i normally do it something like this:

                  extends Something
                  
                  var timeout:int = 10
                  
                  func _process(_delta) -> void:
                      if timeout < 0:
                          do_some_stuff()
                          timeout = 10
                          return
                      timeout -= 1
                      return

                  no good reason for me to think about await much. i am aware some lag could bug up my method, but as of yet, it hasn't been a problem.
                  i'm intermediate at best, so call me dumb if it's appropriate and i'll look at await a bit more closely.

                  • xyz replied to this.

                    One good example of using await is waiting for a Tween to finish before doing something else.

                    tween = create_tween()
                    tween.tween_property(object, "property", value, duration)
                    await tween.finished

                      DaveTheCoder ah. i see. i haven't used a tween in a project where connecting a finished signal wasn't good enough. i also haven't used tweens since Godot 3.X.

                      packrat await is the main tool for implementing coroutines in Godot. Those are very useful in realtime and asynchronous applications. For example in games there is a quite frequent need for lazy or infinite generators that can be elegantly implemented as coroutines:

                      signal more_orcs_needed(how_many: int)
                      
                      func generate_orc_horde(orc_count: int):
                      	while orc_count > 0:
                      		for i in await more_orcs_needed:
                      			print("Here comes a ", ["boldog", "uruk", "goblin"].pick_random() )
                      			orc_count -= 1
                      
                      func _ready():
                      	generate_orc_horde(1000)
                      
                      func _process(delta):
                      	if not_enough_orcs_on_level:
                      		more_orcs_needed.emit( 5 )

                        xyz This is a really smart use of await. I would have never thought of using it this way.