limboai/hsm/limbo_hsm.cpp

359 lines
13 KiB
C++
Raw Normal View History

/**
* limbo_hsm.cpp
* =============================================================================
* Copyright 2021-2024 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
2022-09-29 10:54:07 +00:00
#include "limbo_hsm.h"
VARIANT_ENUM_CAST(LimboHSM::UpdateMode);
void LimboHSM::set_active(bool p_active) {
ERR_FAIL_COND_MSG(agent == nullptr, "LimboHSM is not initialized.");
ERR_FAIL_COND_MSG(p_active && initial_state == nullptr, "LimboHSM has no initial substate candidate.");
if (active == p_active) {
return;
}
active = p_active;
switch (update_mode) {
case UpdateMode::IDLE: {
set_process(p_active);
set_physics_process(false);
} break;
case UpdateMode::PHYSICS: {
set_process(false);
set_physics_process(p_active);
} break;
case UpdateMode::MANUAL: {
set_process(false);
set_physics_process(false);
} break;
}
set_process_input(p_active);
if (active) {
_enter();
2022-09-29 10:54:07 +00:00
} else {
_exit();
2022-09-29 10:54:07 +00:00
}
}
void LimboHSM::change_active_state(LimboState *p_state) {
ERR_FAIL_NULL(p_state);
ERR_FAIL_COND_MSG(!is_active(), "LimboHSM: Unable to change active state when HSM is not active.");
ERR_FAIL_COND_MSG(p_state->get_parent() != this, "LimboHSM: Unable to perform transition to a state that is not a child of this HSM.");
2022-09-29 10:54:07 +00:00
if (active_state) {
active_state->_exit();
active_state->set_process_input(false);
previous_active = active_state;
2022-09-29 10:54:07 +00:00
}
active_state = p_state;
active_state->_enter();
active_state->set_process_input(true);
2022-12-16 10:56:41 +00:00
2024-08-21 18:04:35 +00:00
emit_signal(LW_NAME(active_state_changed), active_state, previous_active);
2022-09-29 10:54:07 +00:00
}
void LimboHSM::_enter() {
2022-09-29 10:54:07 +00:00
ERR_FAIL_COND_MSG(get_child_count() == 0, "LimboHSM has no candidate for initial substate.");
ERR_FAIL_COND(active_state != nullptr);
ERR_FAIL_COND_MSG(initial_state == nullptr, "LimboHSM: Initial state is not set.");
2022-09-29 10:54:07 +00:00
LimboState::_enter();
change_active_state(initial_state);
2022-09-29 10:54:07 +00:00
}
void LimboHSM::_exit() {
2022-09-29 10:54:07 +00:00
ERR_FAIL_COND(active_state == nullptr);
active_state->_exit();
2022-09-29 10:54:07 +00:00
active_state = nullptr;
LimboState::_exit();
2022-09-29 10:54:07 +00:00
}
void LimboHSM::_update(double p_delta) {
2022-09-29 10:54:07 +00:00
if (active) {
ERR_FAIL_NULL(active_state);
LimboState *last_active_state = active_state;
LimboState::_update(p_delta);
if (last_active_state == active_state) {
active_state->_update(p_delta);
}
2022-09-29 10:54:07 +00:00
}
}
void LimboHSM::update(double p_delta) {
updating = true;
_update(p_delta);
updating = false;
if (next_active) {
change_active_state(next_active);
next_active = nullptr;
}
}
2024-07-30 10:31:25 +00:00
void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard) {
2024-01-28 10:56:53 +00:00
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.");
ERR_FAIL_COND_MSG(p_event == StringName(), "LimboHSM: Failed to add transition due to empty event string.");
2022-09-29 10:54:07 +00:00
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.");
2024-07-30 10:31:25 +00:00
// 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
};
2022-09-29 10:54:07 +00:00
}
2024-07-20 15:23:13 +00:00
void LimboHSM::remove_transition(LimboState *p_from_state, const StringName &p_event) {
ERR_FAIL_COND_MSG(p_from_state != nullptr && p_from_state->get_parent() != this, "LimboHSM: Unable to remove a transition from a state that is not an immediate child of mine.");
ERR_FAIL_COND_MSG(p_event == StringName(), "LimboHSM: Unable to remove a transition due to empty event string.");
TransitionKey key = Transition::make_key(p_from_state, p_event);
2024-07-20 15:23:13 +00:00
ERR_FAIL_COND_MSG(!transitions.has(key), "LimboHSM: Unable to remove a transition that does not exist.");
transitions.erase(key);
}
2024-07-21 11:45:36 +00:00
void LimboHSM::_get_transition(LimboState *p_from_state, const StringName &p_event, Transition &r_transition) const {
ERR_FAIL_COND_MSG(p_from_state != nullptr && p_from_state->get_parent() != this, "LimboHSM: Unable to get a transition from a state that is not an immediate child of this HSM.");
ERR_FAIL_COND_MSG(p_event == StringName(), "LimboHSM: Unable to get a transition with an empty event string.");
TransitionKey key = Transition::make_key(p_from_state, p_event);
if (transitions.has(key)) {
r_transition = transitions[key];
}
}
2022-09-29 10:54:07 +00:00
LimboState *LimboHSM::get_leaf_state() const {
LimboHSM *hsm = const_cast<LimboHSM *>(this);
while (hsm->active_state != nullptr && hsm->active_state->is_class("LimboHSM")) {
hsm = Object::cast_to<LimboHSM>(hsm->active_state);
}
if (hsm->active_state) {
return hsm->active_state;
} else {
return hsm;
}
}
void LimboHSM::set_initial_state(LimboState *p_state) {
2022-10-26 21:12:29 +00:00
ERR_FAIL_COND(p_state == nullptr || !p_state->is_class("LimboState"));
initial_state = Object::cast_to<LimboState>(p_state);
}
bool LimboHSM::_dispatch(const StringName &p_event, const Variant &p_cargo) {
ERR_FAIL_COND_V(p_event == StringName(), false);
2022-09-29 10:54:07 +00:00
bool event_consumed = false;
if (active_state) {
event_consumed = active_state->_dispatch(p_event, p_cargo);
2022-09-29 10:54:07 +00:00
}
if (!event_consumed) {
event_consumed = LimboState::_dispatch(p_event, p_cargo);
2022-09-29 10:54:07 +00:00
}
if (!event_consumed && active_state) {
2022-10-12 12:02:39 +00:00
LimboState *to_state = nullptr;
Transition transition;
2024-07-21 11:45:36 +00:00
_get_transition(active_state, p_event, transition);
2024-07-30 10:31:25 +00:00
if (transition.is_valid() && transition.is_allowed()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
2022-10-12 12:02:39 +00:00
}
if (to_state == nullptr) {
// Get ANYSTATE transition.
2024-07-21 11:45:36 +00:00
_get_transition(nullptr, p_event, transition);
2024-07-30 10:31:25 +00:00
if (transition.is_valid() && transition.is_allowed()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
if (to_state == active_state) {
// Transitions to self are not allowed with ANYSTATE.
to_state = nullptr;
}
2022-10-12 12:02:39 +00:00
}
}
if (to_state != nullptr) {
2022-09-29 20:44:51 +00:00
bool permitted = true;
2022-12-16 10:56:41 +00:00
if (to_state->guard_callable.is_valid()) {
Variant ret;
2024-01-07 04:54:17 +00:00
#ifdef LIMBOAI_MODULE
Callable::CallError ce;
2022-12-16 10:56:41 +00:00
to_state->guard_callable.callp(nullptr, 0, ret, ce);
if (unlikely(ce.error != Callable::CallError::CALL_OK)) {
ERR_PRINT_ONCE("LimboHSM: Error calling substate's guard callable: " + Variant::get_callable_error_text(to_state->guard_callable, nullptr, 0, ce));
}
2024-01-13 16:10:42 +00:00
#elif LIMBOAI_GDEXTENSION
2024-01-07 04:54:17 +00:00
ret = to_state->guard_callable.call();
2024-01-13 16:10:42 +00:00
#endif
2024-01-07 04:54:17 +00:00
2022-12-16 10:56:41 +00:00
if (unlikely(ret.get_type() != Variant::BOOL)) {
ERR_PRINT_ONCE(vformat("State guard callable %s returned non-boolean value (%s).", to_state->guard_callable, to_state));
2022-09-29 20:44:51 +00:00
} else {
2022-12-16 10:56:41 +00:00
permitted = bool(ret);
2022-09-29 20:44:51 +00:00
}
}
if (permitted) {
if (!updating) {
change_active_state(to_state);
} else if (!next_active) {
// Only set next_active if we are not already in the process of changing states.
next_active = to_state;
}
2022-09-29 20:44:51 +00:00
event_consumed = true;
}
2022-09-29 10:54:07 +00:00
}
}
2024-01-09 12:42:54 +00:00
if (!event_consumed && p_event == LW_NAME(EVENT_FINISHED) && !(get_parent() && get_parent()->is_class("LimboState"))) {
_exit();
2022-09-29 10:54:07 +00:00
}
return event_consumed;
}
2022-12-16 10:56:41 +00:00
void LimboHSM::initialize(Node *p_agent, const Ref<Blackboard> &p_parent_scope) {
2022-09-29 10:54:07 +00:00
ERR_FAIL_COND(p_agent == nullptr);
ERR_FAIL_COND_MSG(!is_root(), "LimboHSM: initialize() must be called on the root HSM.");
_initialize(p_agent, p_parent_scope);
if (initial_state == nullptr) {
initial_state = Object::cast_to<LimboState>(get_child(0));
}
2022-10-31 20:30:32 +00:00
}
2022-12-16 10:56:41 +00:00
void LimboHSM::_initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) {
2022-10-31 20:30:32 +00:00
ERR_FAIL_COND(p_agent == nullptr);
ERR_FAIL_COND_MSG(agent != nullptr, "LimboAI: HSM already initialized.");
2022-09-29 10:54:07 +00:00
ERR_FAIL_COND_MSG(get_child_count() == 0, "Cannot initialize LimboHSM: no candidate for initial substate.");
if (initial_state == nullptr) {
initial_state = Object::cast_to<LimboState>(get_child(0));
ERR_FAIL_COND_MSG(initial_state == nullptr, "LimboHSM: Child at index 0 is not a LimboState.");
}
LimboState::_initialize(p_agent, p_blackboard);
2022-09-29 10:54:07 +00:00
for (int i = 0; i < get_child_count(); i++) {
LimboState *c = Object::cast_to<LimboState>(get_child(i));
if (unlikely(c == nullptr)) {
ERR_PRINT(vformat("LimboHSM: Child at index %d is not a LimboState.", i));
} else {
c->_initialize(agent, blackboard);
2022-09-29 10:54:07 +00:00
}
}
}
2024-07-31 12:40:30 +00:00
void LimboHSM::_validate_property(PropertyInfo &p_property) const {
if (p_property.name == LW_NAME(update_mode) && !is_root()) {
// Hide update_mode for non-root HSMs.
p_property.usage = PROPERTY_USAGE_NONE;
}
}
void LimboHSM::_exit_if_not_inside_tree() {
if (is_active() && !is_inside_tree()) {
_exit();
}
}
2022-09-29 10:54:07 +00:00
void LimboHSM::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POST_ENTER_TREE: {
if (was_active && is_root()) {
// Re-activate the root HSM if it was previously active.
// Typically, this happens when the node is re-entered scene repeatedly (such as with object pooling).
set_active(true);
}
} break;
case NOTIFICATION_EXIT_TREE: {
if (is_root()) {
// Exit the state machine if the root HSM is no longer in the scene tree (except when being reparented).
// This ensures that resources and signal connections are released if active.
was_active = is_active();
if (is_active()) {
// Check if the HSM node is being deleted.
bool is_being_deleted = false;
Node *node = this;
while (node) {
if (node->is_queued_for_deletion()) {
is_being_deleted = true;
break;
}
node = node->get_parent();
}
if (is_being_deleted) {
// Exit the state machine immediately if the HSM is being deleted.
_exit();
} else {
// Use deferred mode to prevent exiting during Node re-parenting.
// This allows the HSM to remain active when it (or one of its parents) is reparented.
callable_mp(this, &LimboHSM::_exit_if_not_inside_tree).call_deferred();
}
}
}
2022-09-29 10:54:07 +00:00
} break;
case NOTIFICATION_PROCESS: {
_update(get_process_delta_time());
2022-09-29 10:54:07 +00:00
} break;
case NOTIFICATION_PHYSICS_PROCESS: {
_update(get_physics_process_delta_time());
2022-09-29 10:54:07 +00:00
} break;
}
}
void LimboHSM::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_update_mode", "mode"), &LimboHSM::set_update_mode);
2022-09-29 10:54:07 +00:00
ClassDB::bind_method(D_METHOD("get_update_mode"), &LimboHSM::get_update_mode);
2022-10-26 21:12:29 +00:00
ClassDB::bind_method(D_METHOD("set_initial_state", "state"), &LimboHSM::set_initial_state);
2022-10-26 21:12:29 +00:00
ClassDB::bind_method(D_METHOD("get_initial_state"), &LimboHSM::get_initial_state);
2022-09-29 10:54:07 +00:00
ClassDB::bind_method(D_METHOD("get_active_state"), &LimboHSM::get_active_state);
ClassDB::bind_method(D_METHOD("get_previous_active_state"), &LimboHSM::get_previous_active_state);
2022-09-29 10:54:07 +00:00
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);
2024-07-30 10:31:25 +00:00
ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event", "guard"), &LimboHSM::add_transition, DEFVAL(Callable()));
2024-07-20 15:23:13 +00:00
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);
2022-10-12 12:02:39 +00:00
ClassDB::bind_method(D_METHOD("anystate"), &LimboHSM::anystate);
ClassDB::bind_method(D_METHOD("initialize", "agent", "parent_scope"), &LimboHSM::initialize, Variant());
ClassDB::bind_method(D_METHOD("change_active_state", "state"), &LimboHSM::change_active_state);
2022-10-31 20:30:32 +00:00
2022-09-29 10:54:07 +00:00
BIND_ENUM_CONSTANT(IDLE);
BIND_ENUM_CONSTANT(PHYSICS);
BIND_ENUM_CONSTANT(MANUAL);
ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM, "Idle, Physics, Manual"), "set_update_mode", "get_update_mode");
2022-12-16 10:56:41 +00:00
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "ANYSTATE", PROPERTY_HINT_RESOURCE_TYPE, "LimboState", 0), "", "anystate");
2022-10-26 21:12:29 +00:00
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "initial_state", PROPERTY_HINT_RESOURCE_TYPE, "LimboState", 0), "set_initial_state", "get_initial_state");
2022-09-29 10:54:07 +00:00
ADD_SIGNAL(MethodInfo("active_state_changed",
PropertyInfo(Variant::OBJECT, "current", PROPERTY_HINT_RESOURCE_TYPE, "LimboState"),
PropertyInfo(Variant::OBJECT, "previous", PROPERTY_HINT_RESOURCE_TYPE, "LimboState")));
2022-09-29 10:54:07 +00:00
}
LimboHSM::LimboHSM() {
2022-12-16 10:56:41 +00:00
update_mode = UpdateMode::PHYSICS;
2022-09-29 10:54:07 +00:00
active_state = nullptr;
previous_active = nullptr;
next_active = nullptr;
2022-09-29 10:54:07 +00:00
initial_state = nullptr;
2022-10-26 21:12:29 +00:00
}