diff --git a/src/state.cpp b/src/state.cpp
new file mode 100644
index 0000000..b877058
--- /dev/null
+++ b/src/state.cpp
@@ -0,0 +1,5 @@
+#include "state.hpp"
+#include "state_machine.hpp"
+
+namespace godot {
+}
diff --git a/src/state.hpp b/src/state.hpp
new file mode 100644
index 0000000..90a5a8e
--- /dev/null
+++ b/src/state.hpp
@@ -0,0 +1,19 @@
+#ifndef STATE_HPP
+#define STATE_HPP
+
+#include "godot_cpp/core/defs.hpp"
+#include <godot_cpp/classes/node.hpp>
+
+namespace godot {
+class StateMachine;
+
+class IState {
+    friend class StateMachine;
+public:
+    virtual void init(Node *target, StateMachine *machine) = 0;
+    virtual StringName get_next() const = 0;
+    _ALWAYS_INLINE_ Node *as_node() { return dynamic_cast<Node*>(this); }
+};
+}
+
+#endif // !STATE_HPP
diff --git a/src/state_machine.cpp b/src/state_machine.cpp
new file mode 100644
index 0000000..15e9d89
--- /dev/null
+++ b/src/state_machine.cpp
@@ -0,0 +1,63 @@
+#include "state_machine.hpp"
+#include "state.hpp"
+#include "utils/godot_macros.h"
+#include <godot_cpp/classes/scene_tree.hpp>
+#include <godot_cpp/variant/string_name.hpp>
+#include <godot_cpp/variant/utility_functions.hpp>
+
+namespace godot {
+void StateMachine::_bind_methods() {
+#define CLASSNAME StateMachine
+    GDPROPERTY(initial_state, Variant::STRING_NAME);
+}
+
+void StateMachine::_enter_tree() { GDGAMEONLY();
+    this->override_state(this->initial_state);
+}
+
+void StateMachine::_exit_tree() { GDGAMEONLY();
+    for(KeyValue<StringName, IState*> kvp : available)
+        if(!kvp.value->as_node()->is_inside_tree())
+            kvp.value->as_node()->queue_free();
+}
+
+void StateMachine::_process(double delta_time) { GDGAMEONLY();
+    UtilityFunctions::print(this->get_path(), " _process");
+    if(this->state)
+        this->override_state(this->state->get_next());
+}
+
+void StateMachine::override_state(StringName next) {
+    // don't change state if target is current
+    if(this->state != nullptr && next == this->state->as_node()->get_class())
+        return;
+    if(this->state)
+        this->remove_child(this->state->as_node());
+    if(next.is_empty()) {
+        this->state = nullptr;
+        return;
+    }
+    // instantiate new state if this type is not yet available
+    if(!this->available.has(next)) {
+        IState *instance = dynamic_cast<IState*>(Object::cast_to<Node>(ClassDB::instantiate(next)));
+        if(instance == nullptr) {
+            UtilityFunctions::push_error("Failure to instantiate state with class '", next, "'");
+            return;
+        }
+        this->available.insert(next, instance);
+        instance->init(this->get_parent(), this);
+        instance->as_node()->set_process_mode(ProcessMode::PROCESS_MODE_INHERIT);
+    }
+    // set the new state and add it to the tree
+    this->state = this->available.get(next);
+    this->add_child(this->state->as_node());
+}
+
+void StateMachine::set_initial_state(StringName name) {
+    this->initial_state = name;
+}
+
+StringName StateMachine::get_initial_state() const {
+    return this->initial_state;
+}
+}
diff --git a/src/state_machine.hpp b/src/state_machine.hpp
new file mode 100644
index 0000000..aa9f335
--- /dev/null
+++ b/src/state_machine.hpp
@@ -0,0 +1,30 @@
+#ifndef STATE_MACHINE_HPP
+#define STATE_MACHINE_HPP
+
+#include <godot_cpp/variant/string_name.hpp>
+#include <godot_cpp/classes/node.hpp>
+#include <godot_cpp/templates/hash_map.hpp>
+
+namespace godot {
+class IState;
+
+class StateMachine : public Node {
+    GDCLASS(StateMachine, Node);
+    static void _bind_methods();
+public:
+    virtual void _enter_tree() override;
+    virtual void _exit_tree() override;
+    virtual void _process(double delta_time) override;
+
+    void override_state(StringName new_state_class);
+
+    void set_initial_state(StringName name);
+    StringName get_initial_state() const;
+private:
+    String initial_state{};
+    IState *state{nullptr};
+    HashMap<StringName, IState*> available{};
+};
+}
+
+#endif // !STATE_MACHINE_HPP