• 0 Posts
  • 73 Comments
Joined 3 years ago
cake
Cake day: July 1st, 2023

help-circle
  • There is a trick I learned from Firebelley Games (a youtube channel) that is just as simple to spin up and use as the Enum + match strategy but without sacrificing any versatility.

    I actually like it better than the Node-based pattern because you don’t have to set up much boilerplate, and you really don’t need to think about how different state classes might share data. Plus, none of it will clog up your scene tree or need to be pointlessly instantiated by the engine.

    Tap for code

    If you’re on mobile, I would recommend reading this in horizontal view.

    This is all it takes to spin one up:

    class_name Player2D extends Node2D
    
    var _state_machine := CallableStateMachine.new();
    
    func _ready() -> void:
      _state_machine.add_state(
        _state_idle_update,
        Callable(),
        Callable()
      );
      _state_machine.add_state(
        _state_jump_update,
        _state_jump_enter,
        Callable()
      );
      # Set first state
      _state_machine.switch_to(_state_idle_update);
    
    func _process(_delta: float) -> void:
      _state_machine.update();
    
    # These are your state functions.
    func _state_idle_update() -> void;
    func _state_jump_update() -> void;
    func _state_jump_enter() -> void;
    

    The only thing your state machine actually needs to know is which functions are paired together. You can use Callable() to fill in any steps you’re not actually using.

    func _ready() -> void:
      _state_machine.add_state(
        _state_idle_update, # update
        _state_idle_enter,  # enter
        Callable(),         # exit
      );
    

    You call update() yourself, so its timing is completely under your control.

    func _process(delta: float) -> void:
      velocity.y += 9.8 * delta;
      _state_machine.update();
      move_and_slide();
    

    States are keyed by their own update step, so there’s no extra overhead for string names or Enums or the like, and you still get your IDE’s tab autocomplete to help you with 'em.

    func _state_idle_update() -> void:
      if Input.is_action_pressed('jump'):
        _state_machine.switch_to(_state_jump_update);
    

    All state functions exist within the Player2D script, so you have complete access to any shared data or component that Player2D does.

    var _anim: AnimatedSprite2D = $An...;
    var _jump_times := 0;
    
    func _state_idle_enter() -> void:
      _anim.play('idle');
      _jump_times = 0;
    
    func _state_jump_enter() -> void:
      _anim.play('jump');
      _jump_times += 1;
    

    A basic implementation of CallableStateMachine is none too complicated, and you can reuse it anywhere.

    class_name CallableStateMachine extends RefCounted
    
    var _states_map := {} as Dictionary[Callable, CallableState];
    var _current_state: CallableState = null;
    
    func add_state(update: Callable, enter: Callable, exit: Callable) -> void:
      _states_map.set(update, CallableState.new(update, enter, exit));
    
    func switch_to(update: Callable) -> void:
      if not _states_map.has(update):
        return;
      exit();
      _current_state = _states_map.get(update);
      enter();
    
    func update() -> void:
      if _current_state:
        _current_state.update.call();
    
    func enter() -> void:
      if _current_state:
        _current_state.enter.call();
    
    func exit() -> void:
      if _current_state:
        _current_state.exit.call();
    
    # This is just a struct to package the set of functions.
    class CallableState extends RefCounted:
      var update: Callable;
      var enter: Callable;
      var exit: Callable;
      func _init(update: Callable, enter: Callable, exit: Callable) -> void:
        self.update = update;
        self.enter = enter;
        self.exit = exit;
    

    You can do a lot from this base setup, too. I have mine setup such that if I name my functions like this:

    func _state_idle() -> void;
    func _state_idle__update(delta: float) -> void;
    func _state_idle__unhandled_input(event: InputEvent) -> void;
    func _state_idle__exit() -> void;
    

    My state machine automatically knows which step each function is for by the keyword after the double-unders (e.g. ‘__update’), as well as that the nameless _state_idle() is the enter step and the key that I use to switch_to().














  • I’m not entirely sure what conversation you’re wrapping people into. I don’t know what they say about battered wife syndrome, you would have to tell us.

    That said, I think you’re missing the push and pull nature of spousal abuse. It’s not just punching your wife, it’s creating an unstable environment that the victim believes they can overcome and that it is valuable to overcome it.

    The negative reinforcement side, that is the abuse and the removal of it for “good behavior”, is often paired with shame. The wife is not just ducking an uppercut, she is made to believe that she deserved it. Would you divorce your husband if you thought you were at fault for all of your marital problems? Abuse victims often think that they are lucky someone is even willing to put up with them.

    The positive reinforcement side, that is the honeymoon-like love-bombing that happens between abusive episodes, is what the spouse actually wants. But it’s given only intermittently, like a skinner box (another concept you should look up), which creates a dynamic very much like gambling to make an addict of the victim. They spend most of their time trying to figure out how to create those good times without realizing that it’s being deliberately withheld from them like a dangling carrot on stick.

    Both of these contribute to why the spouse stays.

    If your contention has more to do with operant conditioning not being inherently evil, uh, that would be true. It’s a normal psychological function. Abusers… abuse it, but there are other reasons why it might be useful to associate a sound with food, for instance.





  • Hey, Shin!

    You seem pretty excited about your weighted selection strategy for choosing enemy actions, and I thought I’d share with you some other cool tricks.

    I’m going to hide them in a spoiler, though, in case you’re like me and you really love figuring things out on your own.

    Tap for spoiler

    If you get your action weight from a function call, something like:

    action_heal.get_weight(battle_context) => float;

    You can use the information in battle_context (remaining HP, for instance) to dynamically change how likely each action is to be chosen, which seemed to be something you wanted to look into.

    You can even return a weight of 0 if the battle_context would suggest the action would be useless.

    For something much more complicated, you might want to look into a concept called a Behavior Tree. Even if you don’t use one, you might learn a lot from how they’re constructed. Video game AI, robotics, and all kinds of other stuff use them pretty often.

    If you want to see an example of a what a really simple behavior tree is capable of (so simple it might be more accurately called a Decision Tree), you should look into Final Fantasy 12’s Gambit system. This game actually lets players program their own party member AI to do certain things when certain conditions are met, and it ends up being a really useful if barebones model that I’ve based a few AI systems on. If you wanted to combine that with the weighted random-choice trick to add a little more decision variety, I’m sure there’s a cool way to do that.