From bbe71bb37837819109366fcc372a91754099e49b Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Tue, 30 Jul 2024 12:31:25 +0200 Subject: [PATCH 1/3] Add transition guards --- hsm/limbo_hsm.cpp | 17 +++++++++++------ hsm/limbo_hsm.h | 5 ++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/hsm/limbo_hsm.cpp b/hsm/limbo_hsm.cpp index 52730d2..abc3182 100644 --- a/hsm/limbo_hsm.cpp +++ b/hsm/limbo_hsm.cpp @@ -100,7 +100,7 @@ void LimboHSM::update(double p_delta) { } } -void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event) { +void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard) { ERR_FAIL_COND_MSG(p_from_state != nullptr && p_from_state->get_parent() != this, "LimboHSM: Unable to add a transition from a state that is not an immediate child of mine."); ERR_FAIL_COND_MSG(p_to_state == nullptr, "LimboHSM: Unable to add a transition to a null state."); ERR_FAIL_COND_MSG(p_to_state->get_parent() != this, "LimboHSM: Unable to add a transition to a state that is not an immediate child of mine."); @@ -108,8 +108,13 @@ void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, TransitionKey key = Transition::make_key(p_from_state, p_event); ERR_FAIL_COND_MSG(transitions.has(key), "LimboHSM: Unable to add another transition with the same event and origin."); - // Note: Explicit casting needed for GDExtension. - transitions[key] = { p_from_state != nullptr ? ObjectID(p_from_state->get_instance_id()) : ObjectID(), ObjectID(p_to_state->get_instance_id()), p_event }; + // Note: Explicit ObjectID casting needed for GDExtension. + transitions[key] = { + p_from_state != nullptr ? ObjectID(p_from_state->get_instance_id()) : ObjectID(), + ObjectID(p_to_state->get_instance_id()), + p_event, + p_guard + }; } void LimboHSM::remove_transition(LimboState *p_from_state, const StringName &p_event) { @@ -166,13 +171,13 @@ bool LimboHSM::_dispatch(const StringName &p_event, const Variant &p_cargo) { Transition transition; _get_transition(active_state, p_event, transition); - if (transition.is_valid()) { + if (transition.is_valid() && transition.is_allowed()) { to_state = Object::cast_to(ObjectDB::get_instance(transition.to_state)); } if (to_state == nullptr) { // Get ANYSTATE transition. _get_transition(nullptr, p_event, transition); - if (transition.is_valid()) { + if (transition.is_valid() && transition.is_allowed()) { to_state = Object::cast_to(ObjectDB::get_instance(transition.to_state)); if (to_state == active_state) { // Transitions to self are not allowed with ANYSTATE. @@ -300,7 +305,7 @@ void LimboHSM::_bind_methods() { ClassDB::bind_method(D_METHOD("get_leaf_state"), &LimboHSM::get_leaf_state); ClassDB::bind_method(D_METHOD("set_active", "active"), &LimboHSM::set_active); ClassDB::bind_method(D_METHOD("update", "delta"), &LimboHSM::update); - ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event"), &LimboHSM::add_transition); + ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event", "guard"), &LimboHSM::add_transition, DEFVAL(Callable())); ClassDB::bind_method(D_METHOD("remove_transition", "from_state", "event"), &LimboHSM::remove_transition); ClassDB::bind_method(D_METHOD("has_transition", "from_state", "event"), &LimboHSM::has_transition); ClassDB::bind_method(D_METHOD("anystate"), &LimboHSM::anystate); diff --git a/hsm/limbo_hsm.h b/hsm/limbo_hsm.h index f7890c1..4f446f3 100644 --- a/hsm/limbo_hsm.h +++ b/hsm/limbo_hsm.h @@ -39,9 +39,12 @@ private: ObjectID from_state; ObjectID to_state; StringName event; + Callable guard; inline bool is_valid() const { return to_state != ObjectID(); } + inline bool is_allowed() const { return guard.is_null() || guard.call(); } + static _FORCE_INLINE_ TransitionKey make_key(LimboState *p_from_state, const StringName &p_event) { return TransitionKey( p_from_state != nullptr ? uint64_t(p_from_state->get_instance_id()) : 0, @@ -93,7 +96,7 @@ public: void update(double p_delta); - void add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event); + void add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard = Callable()); void remove_transition(LimboState *p_from_state, const StringName &p_event); bool has_transition(LimboState *p_from_state, const StringName &p_event) const { return transitions.has(Transition::make_key(p_from_state, p_event)); } From 85eda3c804ff04528e45b667f84a4ddf2a9b1b2e Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 1 Nov 2024 14:59:10 +0100 Subject: [PATCH 2/3] Tests for per-transition guards --- tests/test_hsm.h | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_hsm.h b/tests/test_hsm.h index 83388d2..741c6a2 100644 --- a/tests/test_hsm.h +++ b/tests/test_hsm.h @@ -215,7 +215,7 @@ TEST_CASE("[Modules][LimboAI] HSM") { CHECK(beta_updates->num_callbacks == 0); CHECK(beta_exits->num_callbacks == 1); // * exited } - SUBCASE("Test transition with guard") { + SUBCASE("Test transition with state-wide guard") { Ref guard = memnew(TestGuard); state_beta->set_guard(callable_mp(guard.ptr(), &TestGuard::can_enter)); @@ -234,6 +234,25 @@ TEST_CASE("[Modules][LimboAI] HSM") { CHECK(beta_entries->num_callbacks == 0); } } + SUBCASE("Test transition with transition-scoped guard") { + Ref guard = memnew(TestGuard); + hsm->add_transition(state_alpha, state_beta, "guarded_transition", callable_mp(guard.ptr(), &TestGuard::can_enter)); + + SUBCASE("When entry is permitted") { + guard->permitted_to_enter = true; + hsm->dispatch("guarded_transition"); + CHECK(hsm->get_active_state() == state_beta); + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + } + SUBCASE("When entry is not permitted") { + guard->permitted_to_enter = false; + hsm->dispatch("guarded_transition"); + CHECK(hsm->get_active_state() == state_alpha); + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + } + } SUBCASE("When there is no transition for given event") { hsm->dispatch("not_found"); CHECK(alpha_exits->num_callbacks == 0); From a5d59a0a5185f101fa6e2fbe34002a4ebb0abcc2 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 1 Nov 2024 15:24:32 +0100 Subject: [PATCH 3/3] Update docs --- doc/source/classes/class_limbohsm.rst | 53 +++++++++++++++------------ doc_classes/LimboHSM.xml | 8 +++- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/doc/source/classes/class_limbohsm.rst b/doc/source/classes/class_limbohsm.rst index bed9f79..4c25f3a 100644 --- a/doc/source/classes/class_limbohsm.rst +++ b/doc/source/classes/class_limbohsm.rst @@ -45,27 +45,27 @@ Methods .. table:: :widths: auto - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`add_transition`\ (\ from_state\: :ref:`LimboState`, to_state\: :ref:`LimboState`, event\: ``StringName``\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`change_active_state`\ (\ state\: :ref:`LimboState`\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | :ref:`LimboState` | :ref:`get_active_state`\ (\ ) |const| | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | :ref:`LimboState` | :ref:`get_leaf_state`\ (\ ) |const| | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | :ref:`LimboState` | :ref:`get_previous_active_state`\ (\ ) |const| | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | ``bool`` | :ref:`has_transition`\ (\ from_state\: :ref:`LimboState`, event\: ``StringName``\ ) |const| | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`initialize`\ (\ agent\: ``Node``, parent_scope\: :ref:`Blackboard` = null\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`remove_transition`\ (\ from_state\: :ref:`LimboState`, event\: ``StringName``\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`set_active`\ (\ active\: ``bool``\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | |void| | :ref:`update`\ (\ delta\: ``float``\ ) | - +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`add_transition`\ (\ from_state\: :ref:`LimboState`, to_state\: :ref:`LimboState`, event\: ``StringName``, guard\: ``Callable`` = Callable()\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`change_active_state`\ (\ state\: :ref:`LimboState`\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | :ref:`LimboState` | :ref:`get_active_state`\ (\ ) |const| | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | :ref:`LimboState` | :ref:`get_leaf_state`\ (\ ) |const| | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | :ref:`LimboState` | :ref:`get_previous_active_state`\ (\ ) |const| | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | ``bool`` | :ref:`has_transition`\ (\ from_state\: :ref:`LimboState`, event\: ``StringName``\ ) |const| | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`initialize`\ (\ agent\: ``Node``, parent_scope\: :ref:`Blackboard` = null\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`remove_transition`\ (\ from_state\: :ref:`LimboState`, event\: ``StringName``\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`set_active`\ (\ active\: ``bool``\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | |void| | :ref:`update`\ (\ delta\: ``float``\ ) | + +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. rst-class:: classref-section-separator @@ -191,9 +191,16 @@ Method Descriptions .. rst-class:: classref-method -|void| **add_transition**\ (\ from_state\: :ref:`LimboState`, to_state\: :ref:`LimboState`, event\: ``StringName``\ ) :ref:`🔗` +|void| **add_transition**\ (\ from_state\: :ref:`LimboState`, to_state\: :ref:`LimboState`, event\: ``StringName``, guard\: ``Callable`` = Callable()\ ) :ref:`🔗` -Establishes a transition from one state to another when ``event`` is dispatched. Both ``from_state`` and ``to_state`` must be immediate children of this state. +Establishes a transition from one state to another when ``event`` is dispatched. Both ``from_state`` and ``to_state`` must be immediate children of this **LimboHSM**. + +Optionally, a ``guard`` function can be specified, which must return a boolean value. If the guard function returns ``false``, the transition will not occur. The guard function is called immediately before the transition is considered. For a state-wide guard function, check out :ref:`LimboState.set_guard`. + +:: + + func my_guard() -> bool: + return is_some_condition_met() .. rst-class:: classref-item-separator diff --git a/doc_classes/LimboHSM.xml b/doc_classes/LimboHSM.xml index fc6652f..6efd5ee 100644 --- a/doc_classes/LimboHSM.xml +++ b/doc_classes/LimboHSM.xml @@ -14,8 +14,14 @@ + - Establishes a transition from one state to another when [param event] is dispatched. Both [param from_state] and [param to_state] must be immediate children of this state. + Establishes a transition from one state to another when [param event] is dispatched. Both [param from_state] and [param to_state] must be immediate children of this [LimboHSM]. + Optionally, a [param guard] function can be specified, which must return a boolean value. If the guard function returns [code]false[/code], the transition will not occur. The guard function is called immediately before the transition is considered. For a state-wide guard function, check out [method LimboState.set_guard]. + [codeblock] + func my_guard() -> bool: + return is_some_condition_met() + [/codeblock]