feat: rethought behaviour separation and states

This commit is contained in:
Sara Gerretsen 2025-12-26 00:35:36 +01:00
parent 54a887dbf3
commit c3ff41e1e6
12 changed files with 274 additions and 68 deletions

View file

@ -20,7 +20,7 @@ void Character::physics_process(double delta) {
}
void Character::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
if (Engine::get_singleton()->is_editor_hint() || !this->data.is_valid()) {
return;
}
switch (what) {
@ -38,3 +38,80 @@ void Character::_notification(int what) {
void Character::set_movement(Vector2 movement) {
this->world_movement_direction = movement;
}
bool Character::is_moving() const {
return !this->world_movement_direction.is_zero_approx();
}
void CharacterState::_bind_methods() {
BIND_PROPERTY(Variant::BOOL, start_active);
}
void CharacterState::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
this->character = cast_to<Character>(get_parent());
return;
case NOTIFICATION_READY:
if (start_active) {
set_state_active(true);
}
return;
}
}
void CharacterState::switch_to_state(String value) {
if (this->state_active) {
print_error(vformat("Attempt to switch from inactive state %s to new state %s", get_path(), value));
return;
}
set_state_active(false);
stack_state_independent(value);
}
void CharacterState::stack_state_dependent(String value) {
if (this->state_active) {
print_error(vformat("Attempt to stack dependent state %s from inactive state %s", value, get_path()));
return;
}
Node *node{ get_parent()->get_node(value) };
if (CharacterState * state{ cast_to<CharacterState>(node) }) {
state->depending_state = this;
this->dependent_states.insert(state);
state->set_state_active(true);
}
}
void CharacterState::stack_state_independent(String value) {
if (this->state_active) {
print_error(vformat("Attempt to stack state %s from inactive state %s", value, get_path()));
return;
}
Node *node{ get_parent()->get_node(value) };
if (CharacterState * state{ cast_to<CharacterState>(node) }) {
state->set_state_active(true);
} else {
print_error(vformat("Attempt to stack nonexistent state %s from %s", value, get_path()));
}
}
void CharacterState::set_state_active(bool active) {
if (this->state_active == active) {
this->state_active = active;
if (active) {
state_entered();
} else {
state_exited();
this->depending_state = nullptr;
for (CharacterState *state : this->dependent_states) {
state->set_state_active(false);
}
this->dependent_states.clear();
}
}
}

View file

@ -2,6 +2,7 @@
#include "authority/macros.h"
#include "core/io/resource.h"
#include "core/templates/hash_set.h"
#include "scene/3d/physics/character_body_3d.h"
class CharacterData : public Resource {
@ -25,6 +26,7 @@ protected:
public:
void set_movement(Vector2 movement);
bool is_moving() const;
private:
Ref<CharacterData> data{};
@ -33,3 +35,32 @@ private:
public:
GET_SET_FNS(Ref<CharacterData>, data);
};
class CharacterState : public Node {
GDCLASS(CharacterState, Node);
static void _bind_methods();
protected:
void _notification(int what);
void switch_to_state(String state);
void stack_state_dependent(String state);
void stack_state_independent(String state);
virtual void state_entered() {}
virtual void state_exited() {}
public:
void set_state_active(bool active);
bool get_state_active() const;
Character *get_character() const;
private:
bool start_active{ false };
bool state_active{ false };
Character *character{ nullptr };
HashSet<CharacterState *> dependent_states{};
CharacterState *depending_state{ nullptr };
public:
GET_SET_FNS(bool, start_active);
};

View file

@ -1,43 +0,0 @@
#include "player_character.h"
#include "core/input/input.h"
#include "scene/main/viewport.h"
void PlayerCharacter::_bind_methods() {}
void PlayerCharacter::process(double delta) {
Basis const basis{ get_viewport()->get_camera_3d()->get_global_basis() };
Vector2 backward{ basis.get_column(2).x, basis.get_column(2).z };
Vector2 right{ basis.get_column(0).x, basis.get_column(0).z };
set_movement({ backward.normalized() * this->last_movement_input.x + right.normalized() * this->last_movement_input });
}
void PlayerCharacter::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
set_process(true);
set_process_unhandled_input(true);
return;
case NOTIFICATION_PROCESS:
process(get_process_delta_time());
}
}
void PlayerCharacter::unhandled_input(Ref<InputEvent> const &what) {
if (what->is_action(input_move_left) || what->is_action(input_move_forward) || what->is_action(input_move_right) || what->is_action(input_move_backward)) {
this->last_movement_input = {
Input::get_singleton()->get_axis(input_move_left, input_move_right),
Input::get_singleton()->get_axis(input_move_backward, input_move_forward)
};
get_viewport()->set_input_as_handled();
}
}
String const PlayerCharacter::input_move_left{ "move_left" };
String const PlayerCharacter::input_move_right{ "move_right" };
String const PlayerCharacter::input_move_forward{ "move_forward" };
String const PlayerCharacter::input_move_backward{ "move_backward" };

View file

@ -1,21 +0,0 @@
#pragma once
#include "authority/character.h"
class PlayerCharacter : public Character {
GDCLASS(PlayerCharacter, Character);
static void _bind_methods();
protected:
void process(double delta);
void _notification(int what);
virtual void unhandled_input(Ref<InputEvent> const &what) override;
private:
Vector2 last_movement_input{ 0, 0 };
static String const input_move_left;
static String const input_move_right;
static String const input_move_forward;
static String const input_move_backward;
};

View file

@ -0,0 +1,26 @@
#include "player_states.h"
void PlayerInputState::_bind_methods() {}
void PlayerInputState::process(double delta) {}
void PlayerInputState::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_PROCESS:
process(get_process_delta_time());
return;
}
}
void PlayerInputState::state_entered() {
set_process(true);
}
void PlayerInputState::state_exited() {
set_process(false);
}

View file

@ -0,0 +1,32 @@
#pragma once
#include "authority/character.h"
class PlayerInputState : public CharacterState {
GDCLASS(PlayerInputState, CharacterState);
static void _bind_methods();
void process(double delta);
protected:
void _notification(int what);
void unhandled_input(Ref<InputEvent> const &event) override;
void state_entered() override;
void state_exited() override;
public:
Vector2 input{};
};
class PlayerMovementState : public CharacterState {
GDCLASS(PlayerMovementState, CharacterState);
static void _bind_methods();
void ready();
void process(double delta);
protected:
void state_entered() override;
void state_exited() override;
private:
Vector2 movement{};
};

View file

@ -1,7 +1,6 @@
#include "register_types.h"
#include "authority/character.h"
#include "authority/player_character.h"
#include "core/object/class_db.h"
void initialize_authority_module(ModuleInitializationLevel p_level) {
@ -10,7 +9,7 @@ void initialize_authority_module(ModuleInitializationLevel p_level) {
}
ClassDB::register_class<CharacterData>();
ClassDB::register_class<Character>();
ClassDB::register_class<PlayerCharacter>();
ClassDB::register_class<CharacterState>();
}
void uninitialize_authority_module(ModuleInitializationLevel p_level) {