diff --git a/include/Nazara/Core.hpp b/include/Nazara/Core.hpp index ea152ff61..02ab1c133 100644 --- a/include/Nazara/Core.hpp +++ b/include/Nazara/Core.hpp @@ -82,6 +82,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/include/Nazara/Core/State.hpp b/include/Nazara/Core/State.hpp new file mode 100644 index 000000000..12e61f33f --- /dev/null +++ b/include/Nazara/Core/State.hpp @@ -0,0 +1,31 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_STATE_HPP +#define NAZARA_CORE_STATE_HPP + +#include +#include +#include + +namespace Nz +{ + class StateMachine; + + class NAZARA_CORE_API State + { + public: + State() = default; + virtual ~State(); + + virtual void Enter(StateMachine& fsm) = 0; + virtual void Leave(StateMachine& fsm) = 0; + + virtual bool Update(StateMachine& fsm, Time elapsedTime) = 0; + }; +} + +#endif // NAZARA_CORE_STATE_HPP diff --git a/include/Nazara/Core/StateMachine.hpp b/include/Nazara/Core/StateMachine.hpp new file mode 100644 index 000000000..b86b27e27 --- /dev/null +++ b/include/Nazara/Core/StateMachine.hpp @@ -0,0 +1,62 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_STATEMACHINE_HPP +#define NAZARA_CORE_STATEMACHINE_HPP + +#include +#include +#include +#include +#include + +namespace Nz +{ + class StateMachine + { + public: + inline StateMachine(std::shared_ptr originalState); + StateMachine(const StateMachine&) = delete; + inline StateMachine(StateMachine&& fsm) = default; + inline ~StateMachine(); + + inline void ChangeState(std::shared_ptr state); + + inline bool IsTopState(const State* state) const; + + inline void PopState(); + inline void PopStatesUntil(std::shared_ptr state); + inline void PushState(std::shared_ptr state); + + inline void ResetState(std::shared_ptr state); + + inline bool Update(Time elapsedTime); + + inline StateMachine& operator=(StateMachine&& fsm) = default; + StateMachine& operator=(const StateMachine&) = delete; + + private: + enum class TransitionType + { + Pop, + PopUntil, + Push, + }; + + struct StateTransition + { + TransitionType type; + std::shared_ptr state; + }; + + std::vector> m_states; + std::vector m_transitions; + }; +} + +#include + +#endif // NAZARA_CORE_STATEMACHINE_HPP diff --git a/include/Nazara/Core/StateMachine.inl b/include/Nazara/Core/StateMachine.inl new file mode 100644 index 000000000..c9a3b0888 --- /dev/null +++ b/include/Nazara/Core/StateMachine.inl @@ -0,0 +1,206 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include + +namespace Nz +{ + /*! + * \ingroup Core + * \class Nz::StateMachine + * \brief Core class that represents a state machine, to represent the multiple states of your program as a stack + */ + + /*! + * \brief Constructs a StateMachine object with an original state + * + * \param originalState State which is the entry point of the application, a nullptr will create an empty state machine + */ + inline StateMachine::StateMachine(std::shared_ptr originalState) + { + if (originalState) + PushState(std::move(originalState)); + } + + /*! + * \brief Destructs the object + * + * \remark Calls "Leave" on all the states from top to bottom + */ + inline StateMachine::~StateMachine() + { + // Leave state from top to bottom (as if states were popped out) + for (auto it = m_states.rbegin(); it != m_states.rend(); ++it) + (*it)->Leave(*this); + } + + /*! + * \brief Replaces the current state on the top of the machine + * + * \param state State to replace the top one if it is nullptr, no action is performed + * + * \remark It is forbidden for a state machine to have (at any moment) the same state in its list multiple times + * \remark Like all actions popping or pushing a state, this is not immediate and will only take effect when state machine is updated + */ + inline void StateMachine::ChangeState(std::shared_ptr state) + { + if (state) + { + // Change state is just a pop followed by a push + StateTransition transition; + transition.type = TransitionType::Pop; + m_transitions.emplace_back(std::move(transition)); + + transition.state = std::move(state); + transition.type = TransitionType::Push; + m_transitions.emplace_back(std::move(transition)); + } + } + + /*! + * \brief Checks whether the state is on the top of the machine + * \return true If it is the case + * + * \param state State to compare the top with + * \remark Because all actions popping or pushing a state don't take effect until next state machine update, this can return false on a just pushed state + */ + inline bool StateMachine::IsTopState(const State* state) const + { + if (m_states.empty()) + return false; + + return m_states.back().get() == state; + } + + /*! + * \brief Pops the state on the top of the machine + * + * \remark This method can completely empty the stack + * \remark Like all actions popping or pushing a state, this is not immediate and will only take effect when state machine is updated + */ + inline void StateMachine::PopState() + { + StateTransition transition; + transition.type = TransitionType::Pop; + + m_transitions.emplace_back(std::move(transition)); + } + + /*! + * \brief Pops all states of the machine until a specific one is reached + * + * \param state State to find on the stack. If nullptr is passed, no action is performed + * + * \remark This method will completely empty the stack if state is not present + * \remark Like all actions popping or pushing a state, this is not immediate and will only take effect when state machine is updated + */ + inline void StateMachine::PopStatesUntil(std::shared_ptr state) + { + if (state) + { + StateTransition transition; + transition.state = std::move(state); + transition.type = TransitionType::PopUntil; + + m_transitions.emplace_back(std::move(transition)); + } + } + + /*! + * \brief Pushes a new state on the top of the machine + * + * \param state Next state to represent if it is nullptr, it performs no action + * + * \remark It is forbidden for a state machine to have (at any moment) the same state in its list multiple times + * \remark Like all actions popping or pushing a state, this is not immediate and will only take effect when state machine is updated + */ + inline void StateMachine::PushState(std::shared_ptr state) + { + if (state) + { + StateTransition transition; + transition.state = std::move(state); + transition.type = TransitionType::Push; + + m_transitions.emplace_back(std::move(transition)); + } + } + + /*! + * \brief Pops every states of the machine to put a new one + * + * \param state State to reset the stack with. If state is invalid, this will clear the state machine + * + * \remark It is forbidden for a state machine to have (at any moment) the same state in its list multiple times + * \remark Like all actions popping or pushing a state, this is not immediate and will only take effect when state machine is updated + */ + inline void StateMachine::ResetState(std::shared_ptr state) + { + StateTransition transition; + transition.type = TransitionType::PopUntil; //< Pop until nullptr, which basically clears the state machine + m_transitions.emplace_back(std::move(transition)); + + if (state) + { + transition.state = std::move(state); + transition.type = TransitionType::Push; + m_transitions.emplace_back(std::move(transition)); + } + } + + /*! + * \brief Updates all the states + * \return true If update is successful for everyone of them + * + * \param elapsedTime Delta time used for the update + */ + inline bool StateMachine::Update(Time elapsedTime) + { + // Use a classic for instead of a range-for because some state may push/pop on enter/leave, adding new transitions as we iterate + // (range-for is a problem here because it doesn't handle mutable containers) + + for (std::size_t i = 0; i < m_transitions.size(); ++i) + { + StateTransition& transition = m_transitions[i]; + + switch (transition.type) + { + case TransitionType::Pop: + { + std::shared_ptr& topState = m_states.back(); + topState->Leave(*this); //< Call leave before popping to ensure consistent IsTopState behavior + + m_states.pop_back(); + break; + } + + case TransitionType::PopUntil: + { + while (!m_states.empty() && m_states.back() != transition.state) + { + m_states.back()->Leave(*this); + m_states.pop_back(); + } + break; + } + + case TransitionType::Push: + { + m_states.emplace_back(std::move(transition.state)); + m_states.back()->Enter(*this); + break; + } + } + } + m_transitions.clear(); + + return std::all_of(m_states.begin(), m_states.end(), [=](std::shared_ptr& state) { + return state->Update(*this, elapsedTime); + }); + } +} + +#include diff --git a/src/Nazara/Core/State.cpp b/src/Nazara/Core/State.cpp new file mode 100644 index 000000000..613bde482 --- /dev/null +++ b/src/Nazara/Core/State.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +namespace Nz +{ + /*! + * \ingroup Core + * \class Nz::State + * \brief Core class that represents a state of your application + */ + + State::~State() = default; +}