limbonaut / limboai

LimboAI - Behavior Trees and State Machines for Godot 4
https://limboai.readthedocs.io/
MIT License
1.2k stars 44 forks source link

Re-parenting as an action shouldn't affect agent's states and interrupt its logic at all #231

Open andmish opened 3 weeks ago

andmish commented 3 weeks ago

Godot version

v4.3.stable.official [77dcf97d8]

LimboAI version

[60a7670]

LimboAI variant

GDExtension / AssetLib

Issue description

There are a lot of gameplay situations (especially in heavily animation driven games, with animation blends) where re-parenting need to be done during(in the middle) animation or tween and then current state need to be switched to new state at the end of that animation/tween (examples, AI approaching and getting into the vehicle, character controller and ladder states, or some tricks you will want to do using re-parenting for ledge grabbing/moving along states), so re-parenting there now will triggers _exit() every time, interrupts the animations/tweens and resets state to initial state (which is also can be completely undesirable, as sometimes initial states are here just for init/reset parameters/variables, I know you can redefine initial_state before re-parenting, but it's not about it and will just adds more complications). Or even simpler example: character Walking forward on platforms and you re-parent character from one platform to another when it steps on new one for some gameplay reason, if you have some logic in the Walking _exit(), it will be triggered now every time (interrupting character walking animation, etc. depends what you do here, and also resetting hsm to initial state), it's just weird behavior for a state machine.

How to reproduce

After reverts in https://github.com/limbonaut/limboai/pull/226 this example won't work anymore (works in 1.2.2), reparent will trigger _exit(), then hsm will be set_active(true) in NOTIFICATION_POST_ENTER_TREE again, meaning we will just return to initial_state idle instead of going to crawl (obviously, in this example on second loop reparent will be a failure, and we will proceed to crawl as needed, but it's unrelated)

extends Node3D

@onready var new_parent = $"../NewParent"
@onready var hsm: LimboHSM = $LimboHSM
@onready var idle: LimboState = $LimboHSM/Idle
@onready var walk: LimboState = $LimboHSM/Walk
@onready var crawl: LimboState = $LimboHSM/Crawl

const WALK = &"WALK"
const CRAWL = &"CRAWL"

func _ready():
    hsm.initial_state = idle
    hsm.add_transition(idle, walk, WALK)
    hsm.add_transition(walk, crawl, CRAWL)

    idle.call_on_enter(func():
        print("IDLE ENTER")
        await get_tree().create_timer(2.0).timeout
        hsm.dispatch(WALK)
        )

    idle.call_on_exit(func():
        print("IDLE EXIT")
        )   

    walk.call_on_enter(func():
        print("WALK ENTER")
        await get_tree().create_timer(2.0).timeout
        reparent(new_parent, false)
        await get_tree().create_timer(2.0).timeout
        hsm.dispatch(CRAWL)
        )

    walk.call_on_exit(func():
        print("WALK EXIT")
        )

    crawl.call_on_enter(func():
        print("CRAWL ENTER")
        )

    hsm.initialize(self)
    hsm.set_active(true)
limbonaut commented 2 weeks ago

Thanks for bringing up this issue. These are valid concerns, and perhaps we can find a solution that satisfies us here. The reason why _exit is called after a node leaves the tree is that, quite often, a state (or a behavior tree) gets a hold unto some resources in _enter that it needs to release (as well as signal connections). The problem with re-parenting is that there is no easy way to detect why the node leaves the tree. Godot doesn't differentiate between reparent and remove_child. Is it going to be freed? Stashed away back into a pool of available objects? Or re-parented? We don't have an answer to those questions at the time NOTIFICATION_TREE_EXIT arrives. Historically, we're releasing resources in the _exit callback in the behavior trees. I was considering overriding reparent implementation, but sadly in GDExtension it is not currently virtual.