diff options
40 files changed, 1264 insertions, 140 deletions
diff --git a/mwe/events/include/event.h b/mwe/events/include/event.h index ee1bf52..e1b220b 100644 --- a/mwe/events/include/event.h +++ b/mwe/events/include/event.h @@ -148,7 +148,7 @@ private: }; class ShutDownEvent : public Event { public: - ShutDownEvent() : Event("ShutDownEvent") {}; + ShutDownEvent() : Event("ShutDownEvent"){}; REGISTER_EVENT_TYPE(ShutDownEvent) diff --git a/src/crepe/Component.h b/src/crepe/Component.h index 060ff00..eff5a58 100644 --- a/src/crepe/Component.h +++ b/src/crepe/Component.h @@ -50,8 +50,6 @@ public: * \return The maximum number of instances for this component */ virtual int get_instances_max() const { return -1; } - //! Only ComponentManager needs to know the max instance count - friend class ComponentManager; }; } // namespace crepe diff --git a/src/crepe/api/AI.cpp b/src/crepe/api/AI.cpp new file mode 100644 index 0000000..2195249 --- /dev/null +++ b/src/crepe/api/AI.cpp @@ -0,0 +1,89 @@ +#include <stdexcept> +#include <type_traits> + +#include "AI.h" +#include "types.h" + +namespace crepe { + +AI::AI(game_object_id_t id, float max_force) : Component(id), max_force(max_force) {} + +void AI::make_circle_path(float radius, const vec2 & center, float start_angle, + bool clockwise) { + if (radius <= 0) { + throw std::runtime_error("Radius must be greater than 0"); + } + + // The step size is determined by the radius (step size is in radians) + float step = RADIUS_TO_STEP / radius; + // Force at least MIN_STEP steps (in case of a small radius) + if (step > 2 * M_PI / MIN_STEP) { + step = 2 * M_PI / MIN_STEP; + } + // The path node distance is determined by the step size and the radius + this->path_node_distance = radius * step * PATH_NODE_DISTANCE_FACTOR; + + if (clockwise) { + for (float i = start_angle; i < 2 * M_PI + start_angle; i += step) { + path.push_back(vec2{static_cast<float>(center.x + radius * cos(i)), + static_cast<float>(center.y + radius * sin(i))}); + } + } else { + for (float i = start_angle; i > start_angle - 2 * M_PI; i -= step) { + path.push_back(vec2{static_cast<float>(center.x + radius * cos(i)), + static_cast<float>(center.y + radius * sin(i))}); + } + } +} + +void AI::make_oval_path(float radius_x, float radius_y, const vec2 & center, float start_angle, + bool clockwise, float rotation) { + if (radius_x <= 0 && radius_y <= 0) { + throw std::runtime_error("Radius must be greater than 0"); + } + + float max_radius = std::max(radius_x, radius_y); + // The step size is determined by the radius (step size is in radians) + float step = RADIUS_TO_STEP / max_radius; + // Force at least MIN_STEP steps (in case of a small radius) + if (step > 2 * M_PI / MIN_STEP) { + step = 2 * M_PI / MIN_STEP; + } + // The path node distance is determined by the step size and the radius + this->path_node_distance = max_radius * step * PATH_NODE_DISTANCE_FACTOR; + + std::function<vec2(vec2, vec2)> rotate_point = [rotation](vec2 point, vec2 center) { + float s = sin(rotation); + float c = cos(rotation); + + // Translate point back to origin + point.x -= center.x; + point.y -= center.y; + + // Rotate point + float xnew = point.x * c - point.y * s; + float ynew = point.x * s + point.y * c; + + // Translate point back + point.x = xnew + center.x; + point.y = ynew + center.y; + + return point; + }; + + if (clockwise) { + for (float i = start_angle; i < 2 * M_PI + start_angle; i += step) { + vec2 point = {static_cast<float>(center.x + radius_x * cos(i)), + static_cast<float>(center.y + radius_y * sin(i))}; + path.push_back(rotate_point(point, center)); + } + } else { + for (float i = start_angle; i > start_angle - 2 * M_PI; i -= step) { + vec2 point = {static_cast<float>(center.x + radius_x * cos(i)), + static_cast<float>(center.y + radius_y * sin(i))}; + path.push_back(rotate_point(point, center)); + } + } +} + +} // namespace crepe diff --git a/src/crepe/api/AI.h b/src/crepe/api/AI.h new file mode 100644 index 0000000..c780a91 --- /dev/null +++ b/src/crepe/api/AI.h @@ -0,0 +1,128 @@ +#pragma once + +#include "Component.h" +#include "types.h" + +namespace crepe { + +/** + * \brief The AI component is used to control the movement of an entity using AI. + * + * The AI component can be used to control the movement of an entity. The AI component can be used + * to implement different behaviors such as seeking, fleeing, arriving, and path following. + */ +class AI : public Component { +public: + //! The different types of behaviors that can be used + enum BehaviorTypeMask { + SEEK = 0x00002, + FLEE = 0x00004, + ARRIVE = 0x00008, + PATH_FOLLOW = 0x00010, + }; + +public: + /** + * \param id The id of the game object + * \param max_force The maximum force that can be applied to the entity + */ + AI(game_object_id_t id, float max_force); + + /** + * \brief Check if a behavior is on (aka activated) + * + * \param behavior The behavior to check + * \return true if the behavior is on, false otherwise + */ + bool on(BehaviorTypeMask behavior) const { return (flags & behavior); } + //! Turn on the seek behavior + void seek_on() { flags |= SEEK; } + //! Turn off the seek behavior + void seek_off() { flags &= ~SEEK; } + //! Turn on the flee behavior + void flee_on() { flags |= FLEE; } + //! Turn off the flee behavior + void flee_off() { flags &= ~FLEE; } + //! Turn on the arrive behavior + void arrive_on() { flags |= ARRIVE; } + //! Turn off the arrive behavior + void arrive_off() { flags &= ~ARRIVE; } + //! Turn on the path follow behavior + void path_follow_on() { flags |= PATH_FOLLOW; } + //! Turn off the path follow behavior + void path_follow_off() { flags &= ~PATH_FOLLOW; } + + /** + * \brief Add a path node (for the path following behavior) + * + * \note The path is not relative to the entity's position (it is an absolute path) + * + * \param node The path node to add + */ + void add_path_node(const vec2 & node) { path.push_back(node); } + /** + * \brief Make a circle path (for the path following behavior) + * + * \note The path is not relative to the entity's position (it is an absolute path) + * + * \param radius The radius of the circle (in game units) + * \param center The center of the circle (in game units) + * \param start_angle The start angle of the circle (in radians) + * \param clockwise The direction of the circle + */ + void make_circle_path(float radius, const vec2 & center = {0, 0}, float start_angle = 0, + bool clockwise = true); + /** + * \brief Make an oval path (for the path following behavior) + * + * \note The path is not relative to the entity's position (it is an absolute path) + * + * \param radius_x The x radius of the oval (in game units) + * \param radius_y The y radius of the oval (in game units) + * \param center The center of the oval (in game units) + * \param start_angle The start angle of the oval (in radians) + * \param clockwise The direction of the oval + * \param rotation The rotation of the oval (in radians) + */ + void make_oval_path(float radius_x, float radius_y, const vec2 & center = {0, 0}, + float start_angle = 0, bool clockwise = true, float rotation = 0); + +public: + //! The maximum force that can be applied to the entity (higher values will make the entity adjust faster) + float max_force; + + //! The target to seek at + vec2 seek_target; + //! The target to arrive at + vec2 arrive_target; + //! The target to flee from + vec2 flee_target; + //! The distance at which the entity will start to flee from the target + float square_flee_panic_distance = 200.0f * 200.0f; + //! The deceleration rate for the arrive behavior (higher values will make the entity decelerate faster (less overshoot)) + float arrive_deceleration = 40.0f; + //! The path to follow (for the path following behavior) + std::vector<vec2> path; + //! The distance from the path node at which the entity will move to the next node (automatically set by make_circle_path()) + float path_node_distance = 400.0f; + //! Looping behavior for the path + bool path_loop = true; + +private: + //! The flags for the behaviors + int flags = 0; + //! The current path index + size_t path_index = 0; + + //! The AISystem is the only class that should access the flags and path_index variables + friend class AISystem; + + //! The minimum amount of steps for the path following behavior + static constexpr int MIN_STEP = 16; + //! The radius to step size ratio for the path following behavior + static constexpr float RADIUS_TO_STEP = 400.0f; + //! The path node distance factor for the path following behavior + static constexpr float PATH_NODE_DISTANCE_FACTOR = 0.75f; +}; + +} // namespace crepe diff --git a/src/crepe/api/CMakeLists.txt b/src/crepe/api/CMakeLists.txt index 718e497..46deb67 100644 --- a/src/crepe/api/CMakeLists.txt +++ b/src/crepe/api/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(crepe PUBLIC Script.cpp Button.cpp UIObject.cpp + AI.cpp ) target_sources(crepe PUBLIC FILE_SET HEADERS FILES @@ -53,4 +54,5 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES Asset.h Button.h UIObject.h + AI.h ) diff --git a/src/crepe/api/LoopManager.cpp b/src/crepe/api/LoopManager.cpp index 88243c4..1f0ba72 100644 --- a/src/crepe/api/LoopManager.cpp +++ b/src/crepe/api/LoopManager.cpp @@ -1,4 +1,6 @@ +#include "../system/AISystem.h" #include "../system/AnimatorSystem.h" +#include "../system/AudioSystem.h" #include "../system/CollisionSystem.h" #include "../system/InputSystem.h" #include "../system/ParticleSystem.h" @@ -20,6 +22,8 @@ LoopManager::LoopManager() { this->load_system<RenderSystem>(); this->load_system<ScriptSystem>(); this->load_system<InputSystem>(); + this->load_system<AudioSystem>(); + this->load_system<AISystem>(); } void LoopManager::process_input() { this->get_system<InputSystem>().update(); } @@ -35,8 +39,10 @@ void LoopManager::fixed_update() { EventManager & ev = this->mediator.event_manager; ev.dispatch_events(); this->get_system<ScriptSystem>().update(); + this->get_system<AISystem>().update(); this->get_system<PhysicsSystem>().update(); this->get_system<CollisionSystem>().update(); + this->get_system<AudioSystem>().update(); } void LoopManager::loop() { diff --git a/src/crepe/api/LoopManager.h b/src/crepe/api/LoopManager.h index b800f5b..7389124 100644 --- a/src/crepe/api/LoopManager.h +++ b/src/crepe/api/LoopManager.h @@ -4,6 +4,8 @@ #include "../facade/SDLContext.h" #include "../manager/ComponentManager.h" +#include "../manager/ResourceManager.h" +#include "../manager/SaveManager.h" #include "../manager/SceneManager.h" #include "../system/System.h" @@ -96,6 +98,10 @@ private: ComponentManager component_manager{mediator}; //! Scene manager instance SceneManager scene_manager{mediator}; + //! Resource manager instance + ResourceManager resource_manager{mediator}; + //! Save manager instance + SaveManager save_manager{mediator}; SDLContext sdl_context {mediator}; diff --git a/src/crepe/api/Script.cpp b/src/crepe/api/Script.cpp index 4091fd4..753a9e3 100644 --- a/src/crepe/api/Script.cpp +++ b/src/crepe/api/Script.cpp @@ -8,8 +8,7 @@ using namespace crepe; using namespace std; Script::~Script() { - Mediator & mediator = this->mediator; - EventManager & mgr = mediator.event_manager; + EventManager & mgr = this->mediator->event_manager; for (auto id : this->listeners) { mgr.unsubscribe(id); } @@ -21,7 +20,8 @@ void Script::subscribe(const EventHandler<CollisionEvent> & callback) { } void Script::set_next_scene(const string & name) { - Mediator & mediator = this->mediator; - SceneManager & mgr = mediator.scene_manager; + SceneManager & mgr = this->mediator->scene_manager; mgr.set_next_scene(name); } + +SaveManager & Script::get_save_manager() const { return this->mediator->save_manager; } diff --git a/src/crepe/api/Script.h b/src/crepe/api/Script.h index d99ab0e..668e5d1 100644 --- a/src/crepe/api/Script.h +++ b/src/crepe/api/Script.h @@ -86,6 +86,25 @@ protected: RefVector<T> get_components() const; /** + * \copydoc ComponentManager::get_components_by_id + * \see ComponentManager::get_components_by_id + */ + template <typename T> + RefVector<T> get_components_by_id(game_object_id_t id) const; + /** + * \copydoc ComponentManager::get_components_by_name + * \see ComponentManager::get_components_by_name + */ + template <typename T> + RefVector<T> get_components_by_name(const std::string & name) const; + /** + * \copydoc ComponentManager::get_components_by_tag + * \see ComponentManager::get_components_by_tag + */ + template <typename T> + RefVector<T> get_components_by_tag(const std::string & tag) const; + + /** * \brief Log a message using Log::logf * * \tparam Args Log::logf parameters @@ -113,6 +132,9 @@ protected: */ void set_next_scene(const std::string & name); + //! Retrieve SaveManager reference + SaveManager & get_save_manager() const; + //! \} private: diff --git a/src/crepe/api/Script.hpp b/src/crepe/api/Script.hpp index 45f1ff1..225a51c 100644 --- a/src/crepe/api/Script.hpp +++ b/src/crepe/api/Script.hpp @@ -20,10 +20,7 @@ T & Script::get_component() const { template <typename T> RefVector<T> Script::get_components() const { - Mediator & mediator = this->mediator; - ComponentManager & mgr = mediator.component_manager; - - return mgr.get_components_by_id<T>(this->game_object_id); + return this->get_components_by_id<T>(this->game_object_id); } template <typename... Args> @@ -34,8 +31,7 @@ void Script::logf(Args &&... args) { template <typename EventType> void Script::subscribe_internal(const EventHandler<EventType> & callback, event_channel_t channel) { - Mediator & mediator = this->mediator; - EventManager & mgr = mediator.event_manager; + EventManager & mgr = this->mediator->event_manager; subscription_t listener = mgr.subscribe<EventType>( [this, callback](const EventType & data) -> bool { bool & active = this->active; @@ -56,4 +52,26 @@ void Script::subscribe(const EventHandler<EventType> & callback) { this->subscribe_internal(callback, EventManager::CHANNEL_ALL); } +template <typename T> +RefVector<T> Script::get_components_by_id(game_object_id_t id) const { + Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + + return mgr.get_components_by_id<T>(id); +} +template <typename T> +RefVector<T> Script::get_components_by_name(const std::string & name) const { + Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + + return mgr.get_components_by_name<T>(name); +} +template <typename T> +RefVector<T> Script::get_components_by_tag(const std::string & tag) const { + Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + + return mgr.get_components_by_tag<T>(tag); +} + } // namespace crepe diff --git a/src/crepe/api/Vector2.h b/src/crepe/api/Vector2.h index c278c87..bf9d124 100644 --- a/src/crepe/api/Vector2.h +++ b/src/crepe/api/Vector2.h @@ -66,6 +66,30 @@ struct Vector2 { //! Checks if this vector is not equal to another vector. bool operator!=(const Vector2<T> & other) const; + + //! Truncates the vector to a maximum length. + void truncate(T max); + + //! Normalizes the vector (resulting in vector with a length of 1). + void normalize(); + + //! Returns the length of the vector. + T length() const; + + //! Returns the squared length of the vector. + T length_squared() const; + + //! Returns the dot product (inwendig product) of this vector and another vector. + T dot(const Vector2<T> & other) const; + + //! Returns the distance between this vector and another vector. + T distance(const Vector2<T> & other) const; + + //! Returns the squared distance between this vector and another vector. + T distance_squared(const Vector2<T> & other) const; + + //! Returns the perpendicular vector to this vector. + Vector2 perpendicular() const; }; } // namespace crepe diff --git a/src/crepe/api/Vector2.hpp b/src/crepe/api/Vector2.hpp index cad15f8..ff53cb0 100644 --- a/src/crepe/api/Vector2.hpp +++ b/src/crepe/api/Vector2.hpp @@ -1,5 +1,7 @@ #pragma once +#include <cmath> + #include "Vector2.h" namespace crepe { @@ -115,4 +117,50 @@ bool Vector2<T>::operator!=(const Vector2<T> & other) const { return !(*this == other); } +template <class T> +void Vector2<T>::truncate(T max) { + if (length() > max) { + normalize(); + *this *= max; + } +} + +template <class T> +void Vector2<T>::normalize() { + T len = length(); + if (len > 0) { + *this /= len; + } +} + +template <class T> +T Vector2<T>::length() const { + return std::sqrt(x * x + y * y); +} + +template <class T> +T Vector2<T>::length_squared() const { + return x * x + y * y; +} + +template <class T> +T Vector2<T>::dot(const Vector2<T> & other) const { + return x * other.x + y * other.y; +} + +template <class T> +T Vector2<T>::distance(const Vector2<T> & other) const { + return (*this - other).length(); +} + +template <class T> +T Vector2<T>::distance_squared(const Vector2<T> & other) const { + return (*this - other).length_squared(); +} + +template <class T> +Vector2<T> Vector2<T>::perpendicular() const { + return {-y, x}; +} + } // namespace crepe diff --git a/src/crepe/facade/SDLContext.cpp b/src/crepe/facade/SDLContext.cpp index a92ec8b..a0d7f04 100644 --- a/src/crepe/facade/SDLContext.cpp +++ b/src/crepe/facade/SDLContext.cpp @@ -233,7 +233,7 @@ SDL_FRect SDLContext::get_dst_rect(const DestinationRectangleData & ctx) const { const Sprite::Data & data = ctx.sprite.data; - vec2 size = {data.size.x, data.size.y}; + vec2 size = data.size; if (data.size.x == 0 && data.size.y != 0) { size.x = data.size.y * ctx.texture.get_ratio(); } diff --git a/src/crepe/manager/ComponentManager.cpp b/src/crepe/manager/ComponentManager.cpp index 80cf8b4..df30d27 100644 --- a/src/crepe/manager/ComponentManager.cpp +++ b/src/crepe/manager/ComponentManager.cpp @@ -1,4 +1,5 @@ #include "../api/GameObject.h" +#include "../api/Metadata.h" #include "../types.h" #include "../util/Log.h" @@ -61,3 +62,13 @@ GameObject ComponentManager::new_object(const string & name, const string & tag, void ComponentManager::set_persistent(game_object_id_t id, bool persistent) { this->persistent[id] = persistent; } + +set<game_object_id_t> ComponentManager::get_objects_by_name(const string & name) const { + return this->get_objects_by_predicate<Metadata>( + [name](const Metadata & data) { return data.name == name; }); +} + +set<game_object_id_t> ComponentManager::get_objects_by_tag(const string & tag) const { + return this->get_objects_by_predicate<Metadata>( + [tag](const Metadata & data) { return data.tag == tag; }); +} diff --git a/src/crepe/manager/ComponentManager.h b/src/crepe/manager/ComponentManager.h index 44429d9..19a8e81 100644 --- a/src/crepe/manager/ComponentManager.h +++ b/src/crepe/manager/ComponentManager.h @@ -1,6 +1,7 @@ #pragma once #include <memory> +#include <set> #include <typeindex> #include <unordered_map> #include <vector> @@ -134,8 +135,77 @@ public: */ template <typename T> RefVector<T> get_components_by_type() const; + /** + * \brief Get all components of a specific type on a GameObject with name \c name + * + * \tparam T The type of the component + * \param name Metadata::name for the same game_object_id as the returned components + * \return Components matching criteria + */ + template <typename T> + RefVector<T> get_components_by_name(const std::string & name) const; + /** + * \brief Get all components of a specific type on a GameObject with tag \c tag + * + * \tparam T The type of the component + * \param name Metadata::tag for the same game_object_id as the returned components + * \return Components matching criteria + */ + template <typename T> + RefVector<T> get_components_by_tag(const std::string & tag) const; + +private: + /** + * \brief Get object IDs by predicate function + * + * This function calls the predicate function \c pred for all components matching type \c T, + * and adds their parent game_object_id to a \c std::set if the predicate returns true. + * + * \tparam T The type of the component to check the predicate against + * \param pred Predicate function + * + * \note The predicate function may be called for multiple components with the same \c + * game_object_id. In this case, the ID is added if *any* call returns \c true. + * + * \returns game_object_id for all components where the predicate returned true + */ + template <typename T> + std::set<game_object_id_t> + get_objects_by_predicate(const std::function<bool(const T &)> & pred) const; + + /** + * \brief Get components of type \c T for multiple game object IDs + * + * \tparam T The type of the components to return + * \param ids The object IDs + * + * \return All components matching type \c T and one of the IDs in \c ids + */ + template <typename T> + RefVector<T> get_components_by_ids(const std::set<game_object_id_t> & ids) const; + + /** + * \brief Get object IDs for objects with name \c name + * + * \param name Object name to match + * \returns Object IDs where Metadata::name is equal to \c name + */ + std::set<game_object_id_t> get_objects_by_name(const std::string & name) const; + /** + * \brief Get object IDs for objects with tag \c tag + * + * \param tag Object tag to match + * \returns Object IDs where Metadata::tag is equal to \c tag + */ + std::set<game_object_id_t> get_objects_by_tag(const std::string & tag) const; private: + //! By Component \c std::type_index (readability helper type) + template <typename T> + using by_type = std::unordered_map<std::type_index, T>; + //! By \c game_object_id index (readability helper type) + template <typename T> + using by_id_index = std::vector<T>; /** * \brief The components * @@ -146,8 +216,7 @@ private: * The first vector is for the ids of the GameObjects and the second vector is for the * components (because a GameObject might have multiple components). */ - std::unordered_map<std::type_index, std::vector<std::vector<std::unique_ptr<Component>>>> - components; + by_type<by_id_index<std::vector<std::unique_ptr<Component>>>> components; //! Persistent flag for each GameObject std::unordered_map<game_object_id_t, bool> persistent; diff --git a/src/crepe/manager/ComponentManager.hpp b/src/crepe/manager/ComponentManager.hpp index ffb38ec..9e70865 100644 --- a/src/crepe/manager/ComponentManager.hpp +++ b/src/crepe/manager/ComponentManager.hpp @@ -95,32 +95,25 @@ template <typename T> RefVector<T> ComponentManager::get_components_by_id(game_object_id_t id) const { using namespace std; - // Determine the type of T (this is used as the key of the unordered_map<>) - type_index type = typeid(T); - - // Create an empty vector<> - RefVector<T> component_vector; - - if (this->components.find(type) == this->components.end()) return component_vector; - - // Get the correct vector<> - const vector<vector<unique_ptr<Component>>> & component_array = this->components.at(type); - - // Make sure that the id (that we are looking for) is within the boundaries of the vector<> - if (id >= component_array.size()) return component_vector; - - // Loop trough the whole vector<> - for (const unique_ptr<Component> & component_ptr : component_array[id]) { - // Cast the unique_ptr to a raw pointer - T * casted_component = static_cast<T *>(component_ptr.get()); - - if (casted_component == nullptr) continue; + static_assert(is_base_of<Component, T>::value, + "get_components_by_id must recieve a derivative class of Component"); - // Add the dereferenced raw pointer to the vector<> - component_vector.push_back(*casted_component); + type_index type = typeid(T); + if (!this->components.contains(type)) return {}; + + const by_id_index<vector<unique_ptr<Component>>> & components_by_id + = this->components.at(type); + if (id >= components_by_id.size()) return {}; + + RefVector<T> out = {}; + const vector<unique_ptr<Component>> & components = components_by_id.at(id); + for (auto & component_ptr : components) { + if (component_ptr == nullptr) continue; + Component & component = *component_ptr.get(); + out.push_back(static_cast<T &>(component)); } - return component_vector; + return out; } template <typename T> @@ -158,4 +151,46 @@ RefVector<T> ComponentManager::get_components_by_type() const { return component_vector; } +template <typename T> +std::set<game_object_id_t> +ComponentManager::get_objects_by_predicate(const std::function<bool(const T &)> & pred) const { + using namespace std; + + set<game_object_id_t> objects = {}; + RefVector<T> components = this->get_components_by_type<T>(); + + for (const T & component : components) { + game_object_id_t id = dynamic_cast<const Component &>(component).game_object_id; + if (objects.contains(id)) continue; + if (!pred(component)) continue; + objects.insert(id); + } + + return objects; +} + +template <typename T> +RefVector<T> +ComponentManager::get_components_by_ids(const std::set<game_object_id_t> & ids) const { + using namespace std; + + RefVector<T> out = {}; + for (game_object_id_t id : ids) { + RefVector<T> components = get_components_by_id<T>(id); + out.insert(out.end(), components.begin(), components.end()); + } + + return out; +} + +template <typename T> +RefVector<T> ComponentManager::get_components_by_name(const std::string & name) const { + return this->get_components_by_ids<T>(this->get_objects_by_name(name)); +} + +template <typename T> +RefVector<T> ComponentManager::get_components_by_tag(const std::string & tag) const { + return this->get_components_by_ids<T>(this->get_objects_by_tag(tag)); +} + } // namespace crepe diff --git a/src/crepe/manager/Mediator.h b/src/crepe/manager/Mediator.h index bb51d6b..d5f7e00 100644 --- a/src/crepe/manager/Mediator.h +++ b/src/crepe/manager/Mediator.h @@ -5,11 +5,13 @@ // TODO: remove these singletons: #include "EventManager.h" #include "SaveManager.h" +#include "api/LoopTimer.h" namespace crepe { class ComponentManager; class SceneManager; +class SaveManager; class ResourceManager; class SDLContext; class LoopTimer; @@ -30,7 +32,7 @@ struct Mediator { OptionalRef<SDLContext> sdl_context; OptionalRef<ComponentManager> component_manager; OptionalRef<SceneManager> scene_manager; - OptionalRef<SaveManager> save_manager = SaveManager::get_instance(); + OptionalRef<SaveManager> save_manager; OptionalRef<EventManager> event_manager = EventManager::get_instance(); OptionalRef<ResourceManager> resource_manager; OptionalRef<LoopTimer> timer; diff --git a/src/crepe/manager/SaveManager.cpp b/src/crepe/manager/SaveManager.cpp index d4ed1c1..691ea2f 100644 --- a/src/crepe/manager/SaveManager.cpp +++ b/src/crepe/manager/SaveManager.cpp @@ -1,13 +1,25 @@ #include "../ValueBroker.h" #include "../api/Config.h" #include "../facade/DB.h" -#include "../util/Log.h" #include "SaveManager.h" using namespace std; using namespace crepe; +SaveManager::SaveManager(Mediator & mediator) : Manager(mediator) { + mediator.save_manager = *this; +} + +DB & SaveManager::get_db() { + if (this->db == nullptr) { + Config & cfg = Config::get_instance(); + this->db + = {new DB(cfg.savemgr.location), [](void * db) { delete static_cast<DB *>(db); }}; + } + return *static_cast<DB *>(this->db.get()); +} + template <> string SaveManager::serialize(const string & value) const noexcept { return value; @@ -90,22 +102,6 @@ int32_t SaveManager::deserialize(const string & value) const noexcept { return deserialize<int64_t>(value); } -SaveManager::SaveManager() { dbg_trace(); } - -SaveManager & SaveManager::get_instance() { - dbg_trace(); - static SaveManager instance; - return instance; -} - -DB & SaveManager::get_db() { - Config & cfg = Config::get_instance(); - // TODO: make this path relative to XDG_DATA_HOME on Linux and whatever the - // default equivalent is on Windows using some third party library - static DB db(cfg.savemgr.location); - return db; -} - bool SaveManager::has(const string & key) { DB & db = this->get_db(); return db.has(key); @@ -155,7 +151,8 @@ ValueBroker<T> SaveManager::get(const string & key) { return { [this, key](const T & target) { this->set<T>(key, target); }, [this, key, value]() mutable -> const T & { - value = this->deserialize<T>(this->get_db().get(key)); + DB & db = this->get_db(); + value = this->deserialize<T>(db.get(key)); return value; }, }; diff --git a/src/crepe/manager/SaveManager.h b/src/crepe/manager/SaveManager.h index 3d8c852..61a978d 100644 --- a/src/crepe/manager/SaveManager.h +++ b/src/crepe/manager/SaveManager.h @@ -1,9 +1,12 @@ #pragma once +#include <functional> #include <memory> #include "../ValueBroker.h" +#include "Manager.h" + namespace crepe { class DB; @@ -18,7 +21,7 @@ class DB; * * The underlying database is a key-value store. */ -class SaveManager { +class SaveManager : public Manager { public: /** * \brief Get a read/write reference to a value and initialize it if it does not yet exist @@ -63,8 +66,8 @@ public: */ bool has(const std::string & key); -private: - SaveManager(); +public: + SaveManager(Mediator & mediator); virtual ~SaveManager() = default; private: @@ -89,26 +92,13 @@ private: template <typename T> T deserialize(const std::string & value) const noexcept; -public: - // singleton - static SaveManager & get_instance(); - SaveManager(const SaveManager &) = delete; - SaveManager(SaveManager &&) = delete; - SaveManager & operator=(const SaveManager &) = delete; - SaveManager & operator=(SaveManager &&) = delete; +protected: + //! Create or return DB + virtual DB & get_db(); private: - /** - * \brief Create an instance of DB and return its reference - * - * \returns DB instance - * - * This function exists because DB is a facade class, which can't directly be used in the API - * without workarounds - * - * TODO: better solution - */ - static DB & get_db(); + //! Database + std::unique_ptr<void, std::function<void(void *)>> db = nullptr; }; } // namespace crepe diff --git a/src/crepe/system/AISystem.cpp b/src/crepe/system/AISystem.cpp new file mode 100644 index 0000000..e2e36a5 --- /dev/null +++ b/src/crepe/system/AISystem.cpp @@ -0,0 +1,185 @@ +#include <algorithm> +#include <cmath> + +#include "api/LoopTimer.h" +#include "manager/ComponentManager.h" +#include "manager/Mediator.h" + +#include "AISystem.h" + +using namespace crepe; + +void AISystem::update() { + const Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + RefVector<AI> ai_components = mgr.get_components_by_type<AI>(); + + //TODO: Use fixed loop dt (this is not available at master at the moment) + double dt = LoopTimer::get_instance().get_delta_time(); + + // Loop through all AI components + for (AI & ai : ai_components) { + if (!ai.active) { + continue; + } + + RefVector<Rigidbody> rigidbodies + = mgr.get_components_by_id<Rigidbody>(ai.game_object_id); + if (rigidbodies.empty()) { + throw std::runtime_error( + "AI component must be attached to a GameObject with a Rigidbody component"); + } + Rigidbody & rigidbody = rigidbodies.front().get(); + if (!rigidbody.active) { + continue; + } + if (rigidbody.data.mass <= 0) { + throw std::runtime_error("Mass must be greater than 0"); + } + + // Calculate the force to apply to the entity + vec2 force = this->calculate(ai, rigidbody); + // Calculate the acceleration (using the above calculated force) + vec2 acceleration = force / rigidbody.data.mass; + // Finally, update Rigidbody's velocity + rigidbody.data.linear_velocity += acceleration * dt; + } +} + +vec2 AISystem::calculate(AI & ai, const Rigidbody & rigidbody) { + const Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + RefVector<Transform> transforms = mgr.get_components_by_id<Transform>(ai.game_object_id); + Transform & transform = transforms.front().get(); + + vec2 force; + + // Run all the behaviors that are on, and stop if the force gets too high + if (ai.on(AI::BehaviorTypeMask::FLEE)) { + vec2 force_to_add = this->flee(ai, rigidbody, transform); + + if (!this->accumulate_force(ai, force, force_to_add)) { + return force; + } + } + if (ai.on(AI::BehaviorTypeMask::ARRIVE)) { + vec2 force_to_add = this->arrive(ai, rigidbody, transform); + + if (!this->accumulate_force(ai, force, force_to_add)) { + return force; + } + } + if (ai.on(AI::BehaviorTypeMask::SEEK)) { + vec2 force_to_add = this->seek(ai, rigidbody, transform); + + if (!this->accumulate_force(ai, force, force_to_add)) { + return force; + } + } + if (ai.on(AI::BehaviorTypeMask::PATH_FOLLOW)) { + vec2 force_to_add = this->path_follow(ai, rigidbody, transform); + + if (!this->accumulate_force(ai, force, force_to_add)) { + return force; + } + } + + return force; +} + +bool AISystem::accumulate_force(const AI & ai, vec2 & running_total, vec2 & force_to_add) { + float magnitude = running_total.length(); + float magnitude_remaining = ai.max_force - magnitude; + + if (magnitude_remaining <= 0.0f) { + // If the force is already at/above the max force, return false + return false; + } + + float magnitude_to_add = force_to_add.length(); + if (magnitude_to_add < magnitude_remaining) { + // If the force to add is less than the remaining force, add it + running_total += force_to_add; + } else { + // If the force to add is greater than the remaining force, add a fraction of it + force_to_add.normalize(); + running_total += force_to_add * magnitude_remaining; + } + + return true; +} + +vec2 AISystem::seek(const AI & ai, const Rigidbody & rigidbody, + const Transform & transform) const { + // Calculate the desired velocity + vec2 desired_velocity = ai.seek_target - transform.position; + desired_velocity.normalize(); + desired_velocity *= rigidbody.data.max_linear_velocity; + + return desired_velocity - rigidbody.data.linear_velocity; +} + +vec2 AISystem::flee(const AI & ai, const Rigidbody & rigidbody, + const Transform & transform) const { + // Calculate the desired velocity if the entity is within the panic distance + vec2 desired_velocity = transform.position - ai.flee_target; + if (desired_velocity.length_squared() > ai.square_flee_panic_distance) { + return vec2{0, 0}; + } + desired_velocity.normalize(); + desired_velocity *= rigidbody.data.max_linear_velocity; + + return desired_velocity - rigidbody.data.linear_velocity; +} + +vec2 AISystem::arrive(const AI & ai, const Rigidbody & rigidbody, + const Transform & transform) const { + // Calculate the desired velocity (taking into account the deceleration rate) + vec2 to_target = ai.arrive_target - transform.position; + float distance = to_target.length(); + if (distance > 0.0f) { + if (ai.arrive_deceleration <= 0.0f) { + throw std::runtime_error("Deceleration rate must be greater than 0"); + } + + float speed = distance / ai.arrive_deceleration; + speed = std::min(speed, rigidbody.data.max_linear_velocity.length()); + vec2 desired_velocity = to_target * (speed / distance); + + return desired_velocity - rigidbody.data.linear_velocity; + } + + return vec2{0, 0}; +} + +vec2 AISystem::path_follow(AI & ai, const Rigidbody & rigidbody, const Transform & transform) { + if (ai.path.empty()) { + return vec2{0, 0}; + } + + // Get the target node + vec2 target = ai.path.at(ai.path_index); + // Calculate the force to apply to the entity + vec2 to_target = target - transform.position; + if (to_target.length_squared() > ai.path_node_distance * ai.path_node_distance) { + // If the entity is not close enough to the target node, seek it + ai.seek_target = target; + ai.arrive_target = target; + } else { + // If the entity is close enough to the target node, move to the next node + ai.path_index++; + if (ai.path_index >= ai.path.size()) { + if (ai.path_loop) { + // If the path is looping, reset the path index + ai.path_index = 0; + } else { + // If the path is not looping, arrive at the last node + ai.path_index = ai.path.size() - 1; + return this->arrive(ai, rigidbody, transform); + } + } + } + + // Seek the target node + return this->seek(ai, rigidbody, transform); +} diff --git a/src/crepe/system/AISystem.h b/src/crepe/system/AISystem.h new file mode 100644 index 0000000..d5f8a8e --- /dev/null +++ b/src/crepe/system/AISystem.h @@ -0,0 +1,81 @@ +#pragma once + +#include "api/AI.h" +#include "api/Rigidbody.h" + +#include "System.h" +#include "api/Transform.h" +#include "types.h" + +namespace crepe { + +/** + * \brief The AISystem is used to control the movement of entities using AI. + * + * The AISystem is used to control the movement of entities using AI. The AISystem can be used to + * implement different behaviors such as seeking, fleeing, arriving, and path following. + */ +class AISystem : public System { +public: + using System::System; + + //! Update the AI system + void update() override; + +private: + /** + * \brief Calculate the total force to apply to the entity + * + * \param ai The AI component + * \param rigidbody The Rigidbody component + */ + vec2 calculate(AI & ai, const Rigidbody & rigidbody); + /** + * \brief Accumulate the force to apply to the entity + * + * \param ai The AI component + * \param running_total The running total of the force + * \param force_to_add The force to add + * \return true if the force was added, false otherwise + */ + bool accumulate_force(const AI & ai, vec2 & running_total, vec2 & force_to_add); + + /** + * \brief Calculate the seek force + * + * \param ai The AI component + * \param rigidbody The Rigidbody component + * \param transform The Transform component + * \return The seek force + */ + vec2 seek(const AI & ai, const Rigidbody & rigidbody, const Transform & transform) const; + /** + * \brief Calculate the flee force + * + * \param ai The AI component + * \param rigidbody The Rigidbody component + * \param transform The Transform component + * \return The flee force + */ + vec2 flee(const AI & ai, const Rigidbody & rigidbody, const Transform & transform) const; + /** + * \brief Calculate the arrive force + * + * \param ai The AI component + * \param rigidbody The Rigidbody component + * \param transform The Transform component + * \return The arrive force + */ + vec2 arrive(const AI & ai, const Rigidbody & rigidbody, const Transform & transform) const; + /** + * \brief Calculate the path follow force + * + * \param ai The AI component + * \param rigidbody The Rigidbody component + * \param transform The Transform component + * \return The path follow force + */ + vec2 path_follow(AI & ai, const Rigidbody & rigidbody, const Transform & transform); +}; + +} // namespace crepe diff --git a/src/crepe/system/AudioSystem.cpp b/src/crepe/system/AudioSystem.cpp index b2c1dc6..b1aa0f8 100644 --- a/src/crepe/system/AudioSystem.cpp +++ b/src/crepe/system/AudioSystem.cpp @@ -26,12 +26,11 @@ void AudioSystem::diff_update(AudioSource & component, Sound & resource) { SoundContext & context = this->get_context(); if (component.active != component.last_active) { - if (component.active) { - component.oneshot_play = component.play_on_awake; - } else { + if (!component.active) { context.stop(component.voice); return; } + if (component.play_on_awake) component.oneshot_play = true; } if (!component.active) return; diff --git a/src/crepe/system/CMakeLists.txt b/src/crepe/system/CMakeLists.txt index 6b2e099..0e2db76 100644 --- a/src/crepe/system/CMakeLists.txt +++ b/src/crepe/system/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(crepe PUBLIC AudioSystem.cpp AnimatorSystem.cpp InputSystem.cpp + AISystem.cpp ) target_sources(crepe PUBLIC FILE_SET HEADERS FILES @@ -19,4 +20,5 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES AudioSystem.h AnimatorSystem.h InputSystem.h + AISystem.h ) diff --git a/src/crepe/util/OptionalRef.h b/src/crepe/util/OptionalRef.h index 3201667..1b2cb3f 100644 --- a/src/crepe/util/OptionalRef.h +++ b/src/crepe/util/OptionalRef.h @@ -25,7 +25,7 @@ public: */ OptionalRef<T> & operator=(T & ref); /** - * \brief Retrieve this reference + * \brief Retrieve this reference (cast) * * \returns Internal reference if it is set * @@ -33,6 +33,14 @@ public: */ operator T &() const; /** + * \brief Retrieve this reference (member access) + * + * \returns Internal reference if it is set + * + * \throws std::runtime_error if this function is called while the reference it not set + */ + T * operator->() const; + /** * \brief Check if this reference is not empty * * \returns `true` if reference is set, or `false` if it is not diff --git a/src/crepe/util/OptionalRef.hpp b/src/crepe/util/OptionalRef.hpp index 4608c9e..5e36b3a 100644 --- a/src/crepe/util/OptionalRef.hpp +++ b/src/crepe/util/OptionalRef.hpp @@ -19,6 +19,13 @@ OptionalRef<T>::operator T &() const { } template <typename T> +T * OptionalRef<T>::operator->() const { + if (this->ref == nullptr) + throw std::runtime_error("OptionalRef: attempt to dereference nullptr"); + return this->ref; +} + +template <typename T> OptionalRef<T> & OptionalRef<T>::operator=(T & ref) { this->ref = &ref; return *this; diff --git a/src/example/AITest.cpp b/src/example/AITest.cpp new file mode 100644 index 0000000..f4efc9f --- /dev/null +++ b/src/example/AITest.cpp @@ -0,0 +1,86 @@ +#include <crepe/api/AI.h> +#include <crepe/api/BehaviorScript.h> +#include <crepe/api/Camera.h> +#include <crepe/api/Color.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/LoopManager.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Scene.h> +#include <crepe/api/Script.h> +#include <crepe/api/Sprite.h> +#include <crepe/api/Texture.h> +#include <crepe/manager/Mediator.h> +#include <crepe/types.h> + +using namespace crepe; +using namespace std; + +class Script1 : public Script { + bool shutdown(const ShutDownEvent & event) { + // Very dirty way of shutting down the game + throw "ShutDownEvent"; + return true; + } + + bool mousemove(const MouseMoveEvent & event) { + /*RefVector<AI> aivec = this->get_components<AI>(); + AI & ai = aivec.front().get(); + ai.flee_target + = vec2{static_cast<float>(event.mouse_x), static_cast<float>(event.mouse_y)};*/ + return true; + } + + void init() { + subscribe<ShutDownEvent>( + [this](const ShutDownEvent & ev) -> bool { return this->shutdown(ev); }); + subscribe<MouseMoveEvent>( + [this](const MouseMoveEvent & ev) -> bool { return this->mousemove(ev); }); + } +}; + +class Scene1 : public Scene { +public: + void load_scene() override { + Mediator & mediator = this->mediator; + ComponentManager & mgr = mediator.component_manager; + + GameObject game_object1 = mgr.new_object("", "", vec2{0, 0}, 0, 1); + GameObject game_object2 = mgr.new_object("", "", vec2{0, 0}, 0, 1); + + Texture img = Texture("asset/texture/test_ap43.png"); + game_object1.add_component<Sprite>(img, Sprite::Data{ + .color = Color::MAGENTA, + .flip = Sprite::FlipSettings{false, false}, + .sorting_in_layer = 1, + .order_in_layer = 1, + .size = {0, 195}, + }); + AI & ai = game_object1.add_component<AI>(3000); + // ai.arrive_on(); + // ai.flee_on(); + ai.path_follow_on(); + ai.make_oval_path(500, 1000, {0, -1000}, 1.5708, true); + ai.make_oval_path(1000, 500, {0, 500}, 4.7124, false); + game_object1.add_component<Rigidbody>(Rigidbody::Data{ + .mass = 0.1f, + .max_linear_velocity = {40, 40}, + }); + game_object1.add_component<BehaviorScript>().set_script<Script1>(); + + game_object2.add_component<Camera>(ivec2{1080, 720}, vec2{5000, 5000}, + Camera::Data{ + .bg_color = Color::WHITE, + .zoom = 1, + }); + } + + string get_name() const override { return "Scene1"; } +}; + +int main() { + LoopManager engine; + engine.add_scene<Scene1>(); + engine.start(); + + return 0; +} diff --git a/src/example/CMakeLists.txt b/src/example/CMakeLists.txt index 73fc512..187ed46 100644 --- a/src/example/CMakeLists.txt +++ b/src/example/CMakeLists.txt @@ -16,7 +16,7 @@ function(add_example target_name) add_dependencies(examples ${target_name}) endfunction() -add_example(savemgr) add_example(rendering_particle) add_example(game) add_example(button) +add_example(AITest) diff --git a/src/example/savemgr.cpp b/src/example/savemgr.cpp deleted file mode 100644 index 65c4a34..0000000 --- a/src/example/savemgr.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/** \file - * - * Standalone example for usage of the save manager - */ - -#include <cassert> -#include <crepe/api/Config.h> -#include <crepe/api/SaveManager.h> -#include <crepe/util/Log.h> -#include <crepe/util/Proxy.h> - -using namespace crepe; - -// unrelated setup code -int _ = []() { - // make sure all log messages get printed - auto & cfg = Config::get_instance(); - cfg.log.level = Log::Level::TRACE; - - return 0; // satisfy compiler -}(); - -int main() { - const char * key = "mygame.test"; - - SaveManager & mgr = SaveManager::get_instance(); - - dbg_logf("has key = {}", mgr.has(key)); - ValueBroker<int> prop = mgr.get<int>(key, 0); - Proxy<int> val = mgr.get<int>(key, 0); - - dbg_logf("val = {}", mgr.get<int>(key).get()); - prop.set(1); - dbg_logf("val = {}", mgr.get<int>(key).get()); - val = 2; - dbg_logf("val = {}", mgr.get<int>(key).get()); - mgr.set<int>(key, 3); - dbg_logf("val = {}", mgr.get<int>(key).get()); - - dbg_logf("has key = {}", mgr.has(key)); - assert(true == mgr.has(key)); - - return 0; -} diff --git a/src/test/AudioTest.cpp b/src/test/AudioTest.cpp index 774fdb8..48bba1b 100644 --- a/src/test/AudioTest.cpp +++ b/src/test/AudioTest.cpp @@ -150,3 +150,12 @@ TEST_F(AudioTest, PlayOnActive) { system.update(); } } + +TEST_F(AudioTest, PlayImmediately) { + component.play_on_awake = false; + component.play(); + + EXPECT_CALL(context, play(_)).Times(1); + + system.update(); +} diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 9d08767..15937af 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -19,4 +19,7 @@ target_sources(test_main PUBLIC ScriptEventTest.cpp ScriptSceneTest.cpp Profiling.cpp + SaveManagerTest.cpp + ScriptSaveManagerTest.cpp + ScriptECSTest.cpp ) diff --git a/src/test/ECSTest.cpp b/src/test/ECSTest.cpp index 3e6c61c..af2b7b0 100644 --- a/src/test/ECSTest.cpp +++ b/src/test/ECSTest.cpp @@ -1,6 +1,7 @@ #include <gtest/gtest.h> #define protected public +#define private public #include <crepe/api/GameObject.h> #include <crepe/api/Metadata.h> @@ -16,6 +17,10 @@ class ECSTest : public ::testing::Test { public: ComponentManager mgr{m}; + + class TestComponent : public Component { + using Component::Component; + }; }; TEST_F(ECSTest, createGameObject) { @@ -387,3 +392,77 @@ TEST_F(ECSTest, resetPersistent) { EXPECT_EQ(metadata.size(), 0); EXPECT_EQ(transform.size(), 0); } + +TEST_F(ECSTest, IDByName) { + GameObject foo = mgr.new_object("foo"); + GameObject bar = mgr.new_object("bar"); + + { + auto objects = mgr.get_objects_by_name(""); + EXPECT_EQ(objects.size(), 0); + } + + { + auto objects = mgr.get_objects_by_name("foo"); + EXPECT_EQ(objects.size(), 1); + EXPECT_TRUE(objects.contains(foo.id)); + } +} + +TEST_F(ECSTest, IDByTag) { + GameObject foo = mgr.new_object("foo", "common tag"); + GameObject bar = mgr.new_object("bar", "common tag"); + + { + auto objects = mgr.get_objects_by_tag(""); + EXPECT_EQ(objects.size(), 0); + } + + { + auto objects = mgr.get_objects_by_tag("common tag"); + EXPECT_EQ(objects.size(), 2); + EXPECT_TRUE(objects.contains(foo.id)); + EXPECT_TRUE(objects.contains(bar.id)); + } +} + +TEST_F(ECSTest, ComponentsByName) { + GameObject foo = mgr.new_object("foo"); + foo.add_component<TestComponent>(); + GameObject bar = mgr.new_object("bar"); + bar.add_component<TestComponent>(); + bar.add_component<TestComponent>(); + + { + auto objects = mgr.get_components_by_name<TestComponent>(""); + EXPECT_EQ(objects.size(), 0); + } + + { + auto objects = mgr.get_components_by_name<TestComponent>("foo"); + EXPECT_EQ(objects.size(), 1); + } + + { + auto objects = mgr.get_components_by_name<TestComponent>("bar"); + EXPECT_EQ(objects.size(), 2); + } +} + +TEST_F(ECSTest, ComponentsByTag) { + GameObject foo = mgr.new_object("foo", "common tag"); + foo.add_component<TestComponent>(); + GameObject bar = mgr.new_object("bar", "common tag"); + bar.add_component<TestComponent>(); + bar.add_component<TestComponent>(); + + { + auto objects = mgr.get_components_by_tag<TestComponent>(""); + EXPECT_EQ(objects.size(), 0); + } + + { + auto objects = mgr.get_components_by_tag<TestComponent>("common tag"); + EXPECT_EQ(objects.size(), 3); + } +} diff --git a/src/test/InputTest.cpp b/src/test/InputTest.cpp index a7c0157..73eaab3 100644 --- a/src/test/InputTest.cpp +++ b/src/test/InputTest.cpp @@ -61,7 +61,7 @@ protected: TEST_F(InputTest, MouseDown) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool mouse_triggered = false; EventHandler<MousePressEvent> on_mouse_down = [&](const MousePressEvent & event) { @@ -91,7 +91,7 @@ TEST_F(InputTest, MouseDown) { TEST_F(InputTest, MouseUp) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool function_triggered = false; EventHandler<MouseReleaseEvent> on_mouse_release = [&](const MouseReleaseEvent & e) { @@ -119,7 +119,7 @@ TEST_F(InputTest, MouseUp) { TEST_F(InputTest, MouseMove) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool function_triggered = false; EventHandler<MouseMoveEvent> on_mouse_move = [&](const MouseMoveEvent & e) { @@ -149,7 +149,7 @@ TEST_F(InputTest, MouseMove) { TEST_F(InputTest, KeyDown) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool function_triggered = false; @@ -180,7 +180,7 @@ TEST_F(InputTest, KeyDown) { TEST_F(InputTest, KeyUp) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool function_triggered = false; EventHandler<KeyReleaseEvent> on_key_release = [&](const KeyReleaseEvent & event) { @@ -204,7 +204,7 @@ TEST_F(InputTest, KeyUp) { TEST_F(InputTest, MouseClick) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; bool on_click_triggered = false; EventHandler<MouseClickEvent> on_mouse_click = [&](const MouseClickEvent & event) { @@ -225,7 +225,7 @@ TEST_F(InputTest, MouseClick) { TEST_F(InputTest, testButtonClick) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; GameObject button_obj = mgr.new_object("body", "person", vec2{0, 0}, 0, 1); bool button_clicked = false; @@ -252,7 +252,7 @@ TEST_F(InputTest, testButtonClick) { TEST_F(InputTest, testButtonHover) { GameObject obj = mgr.new_object("camera", "camera", vec2{0, 0}, 0, 1); auto & camera = obj.add_component<Camera>( - ivec2{100, 100}, vec2{100, 100}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); + ivec2{0, 0}, vec2{500, 500}, Camera::Data{.bg_color = Color::WHITE, .zoom = 1.0f}); camera.active = true; GameObject button_obj = mgr.new_object("body", "person", vec2{0, 0}, 0, 1); bool button_clicked = false; diff --git a/src/test/SaveManagerTest.cpp b/src/test/SaveManagerTest.cpp new file mode 100644 index 0000000..e9b0c29 --- /dev/null +++ b/src/test/SaveManagerTest.cpp @@ -0,0 +1,40 @@ +#include <gtest/gtest.h> + +#include <crepe/ValueBroker.h> +#include <crepe/facade/DB.h> +#include <crepe/manager/SaveManager.h> + +using namespace std; +using namespace crepe; +using namespace testing; + +class SaveManagerTest : public Test { + Mediator m; + class TestSaveManager : public SaveManager { + using SaveManager::SaveManager; + + // in-memory database for testing + DB db{}; + virtual DB & get_db() override { return this->db; } + }; + +public: + TestSaveManager mgr{m}; +}; + +TEST_F(SaveManagerTest, ReadWrite) { + ASSERT_FALSE(mgr.has("foo")); + mgr.set<string>("foo", "bar"); + ASSERT_TRUE(mgr.has("foo")); + + ValueBroker value = mgr.get<string>("foo"); + EXPECT_EQ(value.get(), "bar"); +} + +TEST_F(SaveManagerTest, DefaultValue) { + ValueBroker value = mgr.get<int>("foo", 3); + + ASSERT_EQ(value.get(), 3); + value.set(5); + ASSERT_EQ(value.get(), 5); +} diff --git a/src/test/ScriptECSTest.cpp b/src/test/ScriptECSTest.cpp new file mode 100644 index 0000000..1ec33ba --- /dev/null +++ b/src/test/ScriptECSTest.cpp @@ -0,0 +1,41 @@ +#include <gtest/gtest.h> + +#define protected public + +#include <crepe/api/BehaviorScript.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/Metadata.h> +#include <crepe/api/Script.h> +#include <crepe/manager/ComponentManager.h> +#include <crepe/system/ScriptSystem.h> + +#include "ScriptTest.h" + +using namespace std; +using namespace crepe; +using namespace testing; + +class ScriptECSTest : public ScriptTest { +public: + class TestComponent : public Component { + using Component::Component; + }; +}; + +TEST_F(ScriptECSTest, GetOwnComponent) { + MyScript & script = this->script; + Metadata & metadata = script.get_component<Metadata>(); + + EXPECT_EQ(metadata.name, OBJ_NAME); +} + +TEST_F(ScriptECSTest, GetOwnComponents) { + const unsigned COUNT = 4; + + for (unsigned i = 0; i < COUNT; i++) entity.add_component<TestComponent>(); + + MyScript & script = this->script; + RefVector<TestComponent> components = script.get_components<TestComponent>(); + + EXPECT_EQ(components.size(), COUNT); +} diff --git a/src/test/ScriptEventTest.cpp b/src/test/ScriptEventTest.cpp index 5da31e7..c1b4028 100644 --- a/src/test/ScriptEventTest.cpp +++ b/src/test/ScriptEventTest.cpp @@ -26,7 +26,7 @@ public: class MyEvent : public Event {}; }; -TEST_F(ScriptEventTest, Inactive) { +TEST_F(ScriptEventTest, Default) { BehaviorScript & behaviorscript = this->behaviorscript; MyScript & script = this->script; EventManager & evmgr = this->event_manager; diff --git a/src/test/ScriptSaveManagerTest.cpp b/src/test/ScriptSaveManagerTest.cpp new file mode 100644 index 0000000..64403c4 --- /dev/null +++ b/src/test/ScriptSaveManagerTest.cpp @@ -0,0 +1,35 @@ +#include <gtest/gtest.h> + +// stupid hack to allow access to private/protected members under test +#define private public +#define protected public + +#include <crepe/facade/DB.h> +#include <crepe/manager/SaveManager.h> + +#include "ScriptTest.h" + +using namespace std; +using namespace crepe; +using namespace testing; + +class ScriptSaveManagerTest : public ScriptTest { +public: + class TestSaveManager : public SaveManager { + using SaveManager::SaveManager; + + // in-memory database for testing + DB db{}; + virtual DB & get_db() override { return this->db; } + }; + + TestSaveManager save_mgr{mediator}; +}; + +TEST_F(ScriptSaveManagerTest, GetSaveManager) { + MyScript & script = this->script; + + SaveManager & mgr = script.get_save_manager(); + + EXPECT_EQ(&mgr, &save_mgr); +} diff --git a/src/test/ScriptSceneTest.cpp b/src/test/ScriptSceneTest.cpp index 9ee1e52..2568049 100644 --- a/src/test/ScriptSceneTest.cpp +++ b/src/test/ScriptSceneTest.cpp @@ -18,7 +18,7 @@ public: class MyScene : public Scene {}; }; -TEST_F(ScriptSceneTest, Inactive) { +TEST_F(ScriptSceneTest, Default) { BehaviorScript & behaviorscript = this->behaviorscript; MyScript & script = this->script; diff --git a/src/test/ScriptTest.cpp b/src/test/ScriptTest.cpp index 1d2d6dd..acdae70 100644 --- a/src/test/ScriptTest.cpp +++ b/src/test/ScriptTest.cpp @@ -6,7 +6,6 @@ #define protected public #include "ScriptTest.h" -#include <crepe/api/GameObject.h> using namespace std; using namespace crepe; @@ -14,7 +13,6 @@ using namespace testing; void ScriptTest::SetUp() { auto & mgr = this->component_manager; - GameObject entity = mgr.new_object("name"); BehaviorScript & component = entity.add_component<BehaviorScript>(); this->behaviorscript = component; diff --git a/src/test/ScriptTest.h b/src/test/ScriptTest.h index 1bbfdd3..309e016 100644 --- a/src/test/ScriptTest.h +++ b/src/test/ScriptTest.h @@ -11,10 +11,12 @@ class ScriptTest : public testing::Test { protected: crepe::Mediator mediator; + static constexpr const char * OBJ_NAME = "foo"; public: crepe::ComponentManager component_manager{mediator}; crepe::ScriptSystem system{mediator}; + crepe::GameObject entity = component_manager.new_object(OBJ_NAME); class MyScript : public crepe::Script { // NOTE: explicitly stating `public:` is not required on actual scripts diff --git a/src/test/Vector2Test.cpp b/src/test/Vector2Test.cpp index 17bca41..1e21af9 100644 --- a/src/test/Vector2Test.cpp +++ b/src/test/Vector2Test.cpp @@ -382,3 +382,151 @@ TEST_F(Vector2Test, NotEquals) { EXPECT_FALSE(long_vec1 != long_vec1); EXPECT_TRUE(long_vec1 != long_vec2); } + +TEST_F(Vector2Test, Truncate) { + Vector2<int> vec = {3, 4}; + vec.truncate(3); + EXPECT_EQ(vec.x, 0); + EXPECT_EQ(vec.y, 0); + + Vector2<double> vec2 = {3.0, 4.0}; + vec2.truncate(3.0); + EXPECT_FLOAT_EQ(vec2.x, 1.8); + EXPECT_FLOAT_EQ(vec2.y, 2.4); + + Vector2<long> vec3 = {3, 4}; + vec3.truncate(3); + EXPECT_EQ(vec3.x, 0); + EXPECT_EQ(vec3.y, 0); + + Vector2<float> vec4 = {3.0f, 4.0f}; + vec4.truncate(3.0f); + EXPECT_FLOAT_EQ(vec4.x, 1.8f); + EXPECT_FLOAT_EQ(vec4.y, 2.4f); +} + +TEST_F(Vector2Test, Normalize) { + Vector2<int> vec = {3, 4}; + vec.normalize(); + EXPECT_EQ(vec.x, 0); + EXPECT_EQ(vec.y, 0); + + Vector2<double> vec2 = {3.0, 4.0}; + vec2.normalize(); + EXPECT_FLOAT_EQ(vec2.x, 0.6); + EXPECT_FLOAT_EQ(vec2.y, 0.8); + + Vector2<long> vec3 = {3, 4}; + vec3.normalize(); + EXPECT_EQ(vec3.x, 0); + EXPECT_EQ(vec3.y, 0); + + Vector2<float> vec4 = {3.0f, 4.0f}; + vec4.normalize(); + EXPECT_FLOAT_EQ(vec4.x, 0.6f); + EXPECT_FLOAT_EQ(vec4.y, 0.8f); +} + +TEST_F(Vector2Test, Length) { + Vector2<int> vec = {3, 4}; + EXPECT_EQ(vec.length(), 5); + + Vector2<double> vec2 = {3.0, 4.0}; + EXPECT_FLOAT_EQ(vec2.length(), 5.0); + + Vector2<long> vec3 = {3, 4}; + EXPECT_EQ(vec3.length(), 5); + + Vector2<float> vec4 = {3.0f, 4.0f}; + EXPECT_FLOAT_EQ(vec4.length(), 5.0f); +} + +TEST_F(Vector2Test, LengthSquared) { + Vector2<int> vec = {3, 4}; + EXPECT_EQ(vec.length_squared(), 25); + + Vector2<double> vec2 = {3.0, 4.0}; + EXPECT_FLOAT_EQ(vec2.length_squared(), 25.0); + + Vector2<long> vec3 = {3, 4}; + EXPECT_EQ(vec3.length_squared(), 25); + + Vector2<float> vec4 = {3.0f, 4.0f}; + EXPECT_FLOAT_EQ(vec4.length_squared(), 25.0f); +} + +TEST_F(Vector2Test, Dot) { + Vector2<int> vec1 = {3, 4}; + Vector2<int> vec2 = {5, 6}; + EXPECT_EQ(vec1.dot(vec2), 39); + + Vector2<double> vec3 = {3.0, 4.0}; + Vector2<double> vec4 = {5.0, 6.0}; + EXPECT_FLOAT_EQ(vec3.dot(vec4), 39.0); + + Vector2<long> vec5 = {3, 4}; + Vector2<long> vec6 = {5, 6}; + EXPECT_EQ(vec5.dot(vec6), 39); + + Vector2<float> vec7 = {3.0f, 4.0f}; + Vector2<float> vec8 = {5.0f, 6.0f}; + EXPECT_FLOAT_EQ(vec7.dot(vec8), 39.0f); +} + +TEST_F(Vector2Test, Distance) { + Vector2<int> vec1 = {1, 1}; + Vector2<int> vec2 = {4, 5}; + EXPECT_EQ(vec1.distance(vec2), 5); + + Vector2<double> vec3 = {1.0, 1.0}; + Vector2<double> vec4 = {4.0, 5.0}; + EXPECT_FLOAT_EQ(vec3.distance(vec4), 5.0); + + Vector2<long> vec5 = {1, 1}; + Vector2<long> vec6 = {4, 5}; + EXPECT_EQ(vec5.distance(vec6), 5); + + Vector2<float> vec7 = {1.0f, 1.0f}; + Vector2<float> vec8 = {4.0f, 5.0f}; + EXPECT_FLOAT_EQ(vec7.distance(vec8), 5.0f); +} + +TEST_F(Vector2Test, DistanceSquared) { + Vector2<int> vec1 = {3, 4}; + Vector2<int> vec2 = {5, 6}; + EXPECT_EQ(vec1.distance_squared(vec2), 8); + + Vector2<double> vec3 = {3.0, 4.0}; + Vector2<double> vec4 = {5.0, 6.0}; + EXPECT_FLOAT_EQ(vec3.distance_squared(vec4), 8.0); + + Vector2<long> vec5 = {3, 4}; + Vector2<long> vec6 = {5, 6}; + EXPECT_EQ(vec5.distance_squared(vec6), 8); + + Vector2<float> vec7 = {3.0f, 4.0f}; + Vector2<float> vec8 = {5.0f, 6.0f}; + EXPECT_FLOAT_EQ(vec7.distance_squared(vec8), 8.0f); +} + +TEST_F(Vector2Test, Perpendicular) { + Vector2<int> vec = {3, 4}; + Vector2<int> result = vec.perpendicular(); + EXPECT_EQ(result.x, -4); + EXPECT_EQ(result.y, 3); + + Vector2<double> vec2 = {3.0, 4.0}; + Vector2<double> result2 = vec2.perpendicular(); + EXPECT_FLOAT_EQ(result2.x, -4.0); + EXPECT_FLOAT_EQ(result2.y, 3.0); + + Vector2<long> vec3 = {3, 4}; + Vector2<long> result3 = vec3.perpendicular(); + EXPECT_EQ(result3.x, -4); + EXPECT_EQ(result3.y, 3); + + Vector2<float> vec4 = {3.0f, 4.0f}; + Vector2<float> result4 = vec4.perpendicular(); + EXPECT_FLOAT_EQ(result4.x, -4.0f); + EXPECT_FLOAT_EQ(result4.y, 3.0f); +} |