diff options
-rwxr-xr-x | asset/texture/green_square.png | bin | 0 -> 135 bytes | |||
-rwxr-xr-x | asset/texture/red_square.png | bin | 0 -> 135 bytes | |||
-rw-r--r-- | src/crepe/Collider.cpp | 2 | ||||
-rw-r--r-- | src/crepe/Collider.h | 9 | ||||
-rw-r--r-- | src/crepe/api/BoxCollider.cpp | 7 | ||||
-rw-r--r-- | src/crepe/api/BoxCollider.h | 24 | ||||
-rw-r--r-- | src/crepe/api/CMakeLists.txt | 4 | ||||
-rw-r--r-- | src/crepe/api/CircleCollider.cpp | 6 | ||||
-rw-r--r-- | src/crepe/api/CircleCollider.h | 17 | ||||
-rw-r--r-- | src/crepe/api/Event.h | 9 | ||||
-rw-r--r-- | src/crepe/api/LoopManager.cpp | 9 | ||||
-rw-r--r-- | src/crepe/api/Rigidbody.h | 10 | ||||
-rw-r--r-- | src/crepe/system/CollisionSystem.cpp | 390 | ||||
-rw-r--r-- | src/crepe/system/CollisionSystem.h | 233 | ||||
-rw-r--r-- | src/example/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/example/game.cpp | 92 | ||||
-rw-r--r-- | src/test/CMakeLists.txt | 26 | ||||
-rw-r--r-- | src/test/CollisionTest.cpp | 347 | ||||
-rw-r--r-- | src/test/Profiling.cpp | 230 |
19 files changed, 1392 insertions, 24 deletions
diff --git a/asset/texture/green_square.png b/asset/texture/green_square.png Binary files differnew file mode 100755 index 0000000..7772c87 --- /dev/null +++ b/asset/texture/green_square.png diff --git a/asset/texture/red_square.png b/asset/texture/red_square.png Binary files differnew file mode 100755 index 0000000..6ffbbec --- /dev/null +++ b/asset/texture/red_square.png diff --git a/src/crepe/Collider.cpp b/src/crepe/Collider.cpp index bbec488..80a944d 100644 --- a/src/crepe/Collider.cpp +++ b/src/crepe/Collider.cpp @@ -2,4 +2,4 @@ using namespace crepe; -Collider::Collider(game_object_id_t id) : Component(id) {} +Collider::Collider(game_object_id_t id, vec2 offset) : Component(id), offset(offset) {} diff --git a/src/crepe/Collider.h b/src/crepe/Collider.h index 827f83d..5b26af5 100644 --- a/src/crepe/Collider.h +++ b/src/crepe/Collider.h @@ -1,14 +1,19 @@ #pragma once +#include "api/Vector2.h" + #include "Component.h" +#include "types.h" namespace crepe { class Collider : public Component { public: - Collider(game_object_id_t id); + Collider(game_object_id_t id, vec2 offset); - int size; +public: + //! Offset of the collider relative to rigidbody position + vec2 offset; }; } // namespace crepe diff --git a/src/crepe/api/BoxCollider.cpp b/src/crepe/api/BoxCollider.cpp new file mode 100644 index 0000000..4c767c8 --- /dev/null +++ b/src/crepe/api/BoxCollider.cpp @@ -0,0 +1,7 @@ +#include "BoxCollider.h" + +#include "../Collider.h" + +using namespace crepe; + +BoxCollider::BoxCollider(game_object_id_t game_object_id,vec2 offset, double width, double height) : Collider(game_object_id,offset), width(width), height(height) {} diff --git a/src/crepe/api/BoxCollider.h b/src/crepe/api/BoxCollider.h new file mode 100644 index 0000000..6135954 --- /dev/null +++ b/src/crepe/api/BoxCollider.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Vector2.h" +#include "../Collider.h" + +namespace crepe { + +/** + * \brief A class representing a box-shaped collider. + * + * This class is used for collision detection with other colliders (e.g., CircleCollider). + */ +class BoxCollider : public Collider { +public: + BoxCollider(game_object_id_t game_object_id,vec2 offset, double width, double height); + + //! Width of box collider + double width; + + //! Height of box collider + double height; +}; + +} // namespace crepe diff --git a/src/crepe/api/CMakeLists.txt b/src/crepe/api/CMakeLists.txt index 50c51ed..d8215b9 100644 --- a/src/crepe/api/CMakeLists.txt +++ b/src/crepe/api/CMakeLists.txt @@ -15,6 +15,8 @@ target_sources(crepe PUBLIC SceneManager.cpp Camera.cpp Animator.cpp + BoxCollider.cpp + CircleCollider.cpp EventManager.cpp IKeyListener.cpp IMouseListener.cpp @@ -48,6 +50,8 @@ target_sources(crepe PUBLIC FILE_SET HEADERS FILES SceneManager.hpp Camera.h Animator.h + BoxCollider.h + CircleCollider.h EventManager.h EventManager.hpp EventHandler.h diff --git a/src/crepe/api/CircleCollider.cpp b/src/crepe/api/CircleCollider.cpp new file mode 100644 index 0000000..43de991 --- /dev/null +++ b/src/crepe/api/CircleCollider.cpp @@ -0,0 +1,6 @@ +#include "CircleCollider.h" + +using namespace crepe; + + +CircleCollider::CircleCollider(game_object_id_t game_object_id,vec2 offset, int radius) : Collider(game_object_id,offset), radius(radius) {} diff --git a/src/crepe/api/CircleCollider.h b/src/crepe/api/CircleCollider.h index e77a592..843547f 100644 --- a/src/crepe/api/CircleCollider.h +++ b/src/crepe/api/CircleCollider.h @@ -1,14 +1,23 @@ #pragma once + +#include "Vector2.h" + #include "../Collider.h" namespace crepe { +/** + * \brief A class representing a circle-shaped collider. + * + * This class is used for collision detection with other colliders (e.g., BoxCollider). + */ class CircleCollider : public Collider { public: - CircleCollider(game_object_id_t game_object_id, int radius) - : Collider(game_object_id), - radius(radius) {} - int radius; + + CircleCollider(game_object_id_t game_object_id,vec2 offset, int radius); + + //! Radius of the circle collider. + double radius; }; } // namespace crepe diff --git a/src/crepe/api/Event.h b/src/crepe/api/Event.h index b267e3e..259acba 100644 --- a/src/crepe/api/Event.h +++ b/src/crepe/api/Event.h @@ -3,6 +3,8 @@ #include <string> +#include "system/CollisionSystem.h" + #include "KeyCodes.h" namespace crepe { @@ -93,7 +95,12 @@ public: /** * \brief Event triggered during a collision between objects. */ -class CollisionEvent : public Event {}; +class CollisionEvent : public Event { +public: + crepe::CollisionSystem::CollisionInfo info; + CollisionEvent(const crepe::CollisionSystem::CollisionInfo& collisionInfo) + : info(collisionInfo) {} +}; /** * \brief Event triggered when text is submitted, e.g., from a text input. diff --git a/src/crepe/api/LoopManager.cpp b/src/crepe/api/LoopManager.cpp index 7edf4d1..feb1338 100644 --- a/src/crepe/api/LoopManager.cpp +++ b/src/crepe/api/LoopManager.cpp @@ -6,6 +6,8 @@ #include "../system/PhysicsSystem.h" #include "../system/RenderSystem.h" #include "../system/ScriptSystem.h" +#include "..//system/PhysicsSystem.h" +#include "../system/CollisionSystem.h" #include "LoopManager.h" #include "LoopTimer.h" @@ -32,7 +34,11 @@ void LoopManager::start() { } void LoopManager::set_running(bool running) { this->game_running = running; } -void LoopManager::fixed_update() {} +void LoopManager::fixed_update() { + this->get_system<ScriptSystem>().update(); + this->get_system<PhysicsSystem>().update(); + this->get_system<CollisionSystem>().update(); +} void LoopManager::loop() { LoopTimer & timer = LoopTimer::get_instance(); @@ -58,6 +64,7 @@ void LoopManager::setup() { this->game_running = true; LoopTimer::get_instance().start(); LoopTimer::get_instance().set_fps(200); + this->scene_manager.load_next_scene(); } void LoopManager::render() { diff --git a/src/crepe/api/Rigidbody.h b/src/crepe/api/Rigidbody.h index 3b0588f..7939563 100644 --- a/src/crepe/api/Rigidbody.h +++ b/src/crepe/api/Rigidbody.h @@ -1,5 +1,7 @@ #pragma once +#include <cmath> + #include "../Component.h" #include "types.h" @@ -58,13 +60,13 @@ public: //! linear velocity of object vec2 linear_velocity; //! maximum linear velocity of object - vec2 max_linear_velocity; + vec2 max_linear_velocity = {INFINITY ,INFINITY}; //! linear damping of object vec2 linear_damping; //! angular velocity of object double angular_velocity = 0.0; //! max angular velocity of object - double max_angular_velocity = 0.0; + double max_angular_velocity = INFINITY; //! angular damping of object double angular_damping = 0.0; //! movements constraints of object @@ -73,6 +75,10 @@ public: bool use_gravity = true; //! if object bounces bool bounce = false; + //! bounce factor of material + double elastisity = 0.0; + //! offset of all colliders relative to transform position + vec2 offset; }; public: diff --git a/src/crepe/system/CollisionSystem.cpp b/src/crepe/system/CollisionSystem.cpp index c74ca1d..e0c6d03 100644 --- a/src/crepe/system/CollisionSystem.cpp +++ b/src/crepe/system/CollisionSystem.cpp @@ -1,5 +1,393 @@ +#include <cmath> +#include <algorithm> +#include <cstddef> +#include <functional> +#include <utility> +#include <variant> +#include <optional> + +#include "api/Event.h" +#include "api/EventManager.h" +#include "api/BoxCollider.h" +#include "api/CircleCollider.h" +#include "api/Vector2.h" +#include "api/Rigidbody.h" +#include "api/Transform.h" + +#include "ComponentManager.h" #include "CollisionSystem.h" +#include "Collider.h" +#include "types.h" +#include "util/OptionalRef.h" using namespace crepe; -void CollisionSystem::update() {} +void CollisionSystem::update() { + // Get collider components and keep them seperate + ComponentManager & mgr = this->component_manager; + std::vector<std::reference_wrapper<BoxCollider>> boxcolliders = mgr.get_components_by_type<BoxCollider>(); + std::vector<std::reference_wrapper<CircleCollider>> circlecolliders = mgr.get_components_by_type<CircleCollider>(); + + std::vector<collider_variant> all_colliders; + // Add BoxCollider references + for (auto& box : boxcolliders) { + all_colliders.push_back(collider_variant{box}); + } + + // Add CircleCollider references + for (auto& circle : circlecolliders) { + all_colliders.push_back(collider_variant{circle}); + } + + // Check between all colliders if there is a collision + std::vector<std::pair<CollisionInternal,CollisionInternal>> collided = check_collisions(all_colliders); + + // For both objects call the collision handler + for (auto& collision_pair : collided) { + collision_handler_request(collision_pair.first,collision_pair.second); + collision_handler_request(collision_pair.second,collision_pair.first); + } +} + +void CollisionSystem::collision_handler_request(CollisionInternal& data1,CollisionInternal& data2){ + + CollisionInternalType type = check_collider_type(data1.collider,data2.collider); + std::pair<vec2,CollisionSystem::Direction> resolution_data = collision_handler(data1,data2,type); + + OptionalRef<Collider> collider1; + OptionalRef<Collider> collider2; + switch (type) { + case CollisionInternalType::BOX_BOX:{ + collider1 = std::get<std::reference_wrapper<BoxCollider>>(data1.collider); + collider2 = std::get<std::reference_wrapper<BoxCollider>>(data2.collider); + break; + } + case CollisionInternalType::BOX_CIRCLE:{ + collider1 = std::get<std::reference_wrapper<BoxCollider>>(data1.collider); + collider2 = std::get<std::reference_wrapper<CircleCollider>>(data2.collider); + break; + } + case CollisionInternalType::CIRCLE_BOX:{ + collider1 = std::get<std::reference_wrapper<CircleCollider>>(data1.collider); + collider2 = std::get<std::reference_wrapper<BoxCollider>>(data2.collider); + break; + } + case CollisionInternalType::CIRCLE_CIRCLE:{ + collider1 = std::get<std::reference_wrapper<CircleCollider>>(data1.collider); + collider2 = std::get<std::reference_wrapper<CircleCollider>>(data2.collider); + break; + } + } + + // collision info + crepe::CollisionSystem::CollisionInfo collision_info{ + .first_collider = collider1, + .first_transform = data1.transform, + .first_rigidbody = data1.rigidbody, + .second_collider = collider2, + .second_transform = data2.transform, + .second_rigidbody = data2.rigidbody, + .resolution = resolution_data.first, + .resolution_direction = resolution_data.second, + }; + + // Determine if static needs to be called + determine_collision_handler(collision_info); +} + + +std::pair<vec2,CollisionSystem::Direction> CollisionSystem::collision_handler(CollisionInternal& data1,CollisionInternal& data2,CollisionInternalType type) { + vec2 move_back; + switch (type) { + case CollisionInternalType::BOX_BOX: { + const BoxCollider & collider1 = std::get<std::reference_wrapper<BoxCollider>>(data1.collider); + const BoxCollider & collider2 = std::get<std::reference_wrapper<BoxCollider>>(data2.collider); + vec2 collider_pos1 = current_position(collider1.offset, data1.transform, data1.rigidbody); + vec2 collider_pos2 = current_position(collider2.offset, data2.transform, data2.rigidbody); + move_back = box_box_resolution(collider1,collider2,collider_pos1,collider_pos2); + } + case CollisionInternalType::BOX_CIRCLE: { + + } + case CollisionInternalType::CIRCLE_CIRCLE: { + + } + case CollisionInternalType::CIRCLE_BOX: { + + } + } + + Direction move_back_direction = Direction::NONE; + if(move_back.x != 0 && move_back.y > 0) { + move_back_direction = Direction::BOTH; + } else if (move_back.x != 0) { + move_back_direction = Direction::X_DIRECTION; + if(data1.rigidbody.data.linear_velocity.y != 0) + move_back.y = data1.rigidbody.data.linear_velocity.y * (move_back.x/data1.rigidbody.data.linear_velocity.x); + } else if (move_back.y != 0) { + move_back_direction = Direction::Y_DIRECTION; + if(data1.rigidbody.data.linear_velocity.x != 0) + move_back.x = data1.rigidbody.data.linear_velocity.x * (move_back.y/data1.rigidbody.data.linear_velocity.y); + } + + return {move_back,move_back_direction}; +} + +vec2 CollisionSystem::box_box_resolution(const BoxCollider& box_collider1,const BoxCollider& box_collider2,vec2 final_position1,vec2 final_position2) +{ + vec2 resolution; // Default resolution vector + vec2 delta = final_position2 - final_position1; + + // Compute half-dimensions of the boxes + float half_width1 = box_collider1.width / 2.0; + float half_height1 = box_collider1.height / 2.0; + float half_width2 = box_collider2.width / 2.0; + float half_height2 = box_collider2.height / 2.0; + + // Calculate overlaps along X and Y axes + float overlap_x = (half_width1 + half_width2) - std::abs(delta.x); + float overlap_y = (half_height1 + half_height2) - std::abs(delta.y); + + // Check if there is a collision + if (overlap_x > 0 && overlap_y > 0) {//should always be true check if this can be removed + // Determine the direction of resolution + if (overlap_x < overlap_y) { + // Resolve along the X-axis (smallest overlap) + resolution.x = (delta.x > 0) ? -overlap_x : overlap_x; + } else if (overlap_y < overlap_x) { + // Resolve along the Y-axis (smallest overlap) + resolution.y = (delta.y > 0) ? -overlap_y : overlap_y; + } else { + // Equal overlap, resolve both directions with preference + resolution.x = (delta.x > 0) ? -overlap_x : overlap_x; + resolution.y = (delta.y > 0) ? -overlap_y : overlap_y; + } + } + + return resolution; +} + +void CollisionSystem::determine_collision_handler(CollisionInfo& info){ + // Check rigidbody type for static + if(info.first_rigidbody.data.body_type != Rigidbody::BodyType::STATIC) + { + // If second body is static perform the static collision handler in this system + if(info.second_rigidbody.data.body_type == Rigidbody::BodyType::STATIC){ + static_collision_handler(info); + }; + // Call collision event for user + CollisionEvent data(info); + EventManager::get_instance().trigger_event<CollisionEvent>(data, info.first_collider.game_object_id); + } +} + +void CollisionSystem::static_collision_handler(CollisionInfo& info){ + // Move object back using calculate move back value + info.first_transform.position += info.resolution; + + // If bounce is enabled mirror velocity + if(info.first_rigidbody.data.bounce) { + if(info.resolution_direction == Direction::BOTH) + { + info.first_rigidbody.data.linear_velocity.y = -info.first_rigidbody.data.linear_velocity.y * info.first_rigidbody.data.elastisity; + info.first_rigidbody.data.linear_velocity.x = -info.first_rigidbody.data.linear_velocity.x * info.first_rigidbody.data.elastisity; + } + else if(info.resolution_direction == Direction::Y_DIRECTION) { + info.first_rigidbody.data.linear_velocity.y = -info.first_rigidbody.data.linear_velocity.y * info.first_rigidbody.data.elastisity; + } + else if(info.resolution_direction == Direction::X_DIRECTION){ + info.first_rigidbody.data.linear_velocity.x = -info.first_rigidbody.data.linear_velocity.x * info.first_rigidbody.data.elastisity; + } + } + // Stop movement if bounce is disabled + else { + info.first_rigidbody.data.linear_velocity = {0,0}; + } +} + +std::vector<std::pair<CollisionSystem::CollisionInternal,CollisionSystem::CollisionInternal>> CollisionSystem::check_collisions(std::vector<collider_variant> & colliders) { + + + // TODO: + // If no colliders skip + // Check if colliders has rigidbody if not skip + + // TODO: + // If amount is higer than lets say 16 for now use quadtree otwerwise skip + // Quadtree code + // Quadtree is placed over the input vector + + std::vector<std::pair<CollisionInternal,CollisionInternal>> collisions_ret; + for (size_t i = 0; i < colliders.size(); ++i) { + std::visit([&](auto& inner_collider_ref) { + if (!inner_collider_ref.get().active) return; + auto inner_components = get_active_transform_and_rigidbody(inner_collider_ref.get().game_object_id); + if (!inner_components) return; + for (size_t j = i + 1; j < colliders.size(); ++j) { + std::visit([&](auto& outer_collider_ref) { + if (!outer_collider_ref.get().active) return; + if (inner_collider_ref.get().game_object_id == outer_collider_ref.get().game_object_id) return; + auto outer_components = get_active_transform_and_rigidbody(outer_collider_ref.get().game_object_id); + if (!outer_components) return; + CollisionInternalType type = check_collider_type(colliders[i],colliders[j]); + if(!check_collision({ + .collider = colliders[i], + .transform = inner_components->first, + .rigidbody = inner_components->second, + }, + { + .collider = colliders[j], + .transform = outer_components->first, + .rigidbody = outer_components->second, + }, + type)) return; + collisions_ret.emplace_back( + CollisionInternal{colliders[i], inner_components->first.get(), inner_components->second.get()}, + CollisionInternal{colliders[j], outer_components->first.get(), outer_components->second.get()} + ); + }, colliders[j]); + } + }, colliders[i]); + } + + return collisions_ret; +} + +std::optional<std::pair<std::reference_wrapper<Transform>, std::reference_wrapper<Rigidbody>>> +CollisionSystem::get_active_transform_and_rigidbody(game_object_id_t game_object_id) { + RefVector<Transform> transforms = this->component_manager.get_components_by_id<Transform>(game_object_id); + if (transforms.empty()) return std::nullopt; + + RefVector<Rigidbody> rigidbodies = this->component_manager.get_components_by_id<Rigidbody>(game_object_id); + if (rigidbodies.empty()) return std::nullopt; + + Transform& transform = transforms.front().get(); + if (!transform.active) return std::nullopt; + + Rigidbody& rigidbody = rigidbodies.front().get(); + if (!rigidbody.active) return std::nullopt; + + // Return the active components + return std::make_pair(std::ref(transform), std::ref(rigidbody)); +} + +CollisionSystem::CollisionInternalType CollisionSystem::check_collider_type(const collider_variant& collider1,const collider_variant& collider2){ + if(std::holds_alternative<std::reference_wrapper<CircleCollider>>(collider1)){ + if(std::holds_alternative<std::reference_wrapper<CircleCollider>>(collider2)) + { + return CollisionInternalType::CIRCLE_CIRCLE; + } + else { + return CollisionInternalType::CIRCLE_BOX; + } + } + else { + if(std::holds_alternative<std::reference_wrapper<CircleCollider>>(collider2)) + { + return CollisionInternalType::BOX_CIRCLE; + } + else { + return CollisionInternalType::BOX_BOX; + } + } +} + +bool CollisionSystem::check_collision(const CollisionInternal& first_info,const CollisionInternal& second_info, CollisionInternalType type){ + switch (type) { + case CollisionInternalType::BOX_BOX: { + const BoxCollider & box_collider1 = std::get<std::reference_wrapper<BoxCollider>>(first_info.collider); + const BoxCollider & box_collider2 = std::get<std::reference_wrapper<BoxCollider>>(second_info.collider); + return check_box_box_collision(box_collider1,box_collider2,first_info.transform,second_info.transform,second_info.rigidbody,second_info.rigidbody); + } + case CollisionInternalType::BOX_CIRCLE: { + const BoxCollider & box_collider = std::get<std::reference_wrapper<BoxCollider>>(first_info.collider); + const CircleCollider & circle_collider = std::get<std::reference_wrapper<CircleCollider>>(second_info.collider); + return check_box_circle_collision(box_collider,circle_collider,first_info.transform,second_info.transform,second_info.rigidbody,second_info.rigidbody); + } + case CollisionInternalType::CIRCLE_CIRCLE: { + const CircleCollider & circle_collider1 = std::get<std::reference_wrapper<CircleCollider>>(first_info.collider); + const CircleCollider & circle_collider2 = std::get<std::reference_wrapper<CircleCollider>>(second_info.collider); + return check_circle_circle_collision(circle_collider1,circle_collider2,first_info.transform,second_info.transform,second_info.rigidbody,second_info.rigidbody); + } + case CollisionInternalType::CIRCLE_BOX: { + const CircleCollider & circle_collider = std::get<std::reference_wrapper<CircleCollider>>(first_info.collider); + const BoxCollider & box_collider = std::get<std::reference_wrapper<BoxCollider>>(second_info.collider); + return check_box_circle_collision(box_collider,circle_collider,first_info.transform,second_info.transform,second_info.rigidbody,second_info.rigidbody); + } + } + return false; +} + + +bool CollisionSystem::check_box_box_collision(const BoxCollider& box1, const BoxCollider& box2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2) +{ + // Get current positions of colliders + vec2 final_position1 = current_position(box1.offset,transform1,rigidbody1); + vec2 final_position2 = current_position(box2.offset,transform2,rigidbody2); + + // Calculate half-extents (half width and half height) + float half_width1 = box1.width / 2.0; + float half_height1 = box1.height / 2.0; + float half_width2 = box2.width / 2.0; + float half_height2 = box2.height / 2.0; + + // Check if the boxes overlap along the X and Y axes + return (final_position1.x + half_width1 > final_position2.x - half_width2 && // not left + final_position1.x - half_width1 < final_position2.x + half_width2 && // not right + final_position1.y + half_height1 > final_position2.y - half_height2 && // not above + final_position1.y - half_height1 < final_position2.y + half_height2); // not below +} + +bool CollisionSystem::check_box_circle_collision(const BoxCollider& box1, const CircleCollider& circle2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2) { + // Get current positions of colliders + vec2 final_position1 = current_position(box1.offset, transform1, rigidbody1); + vec2 final_position2 = current_position(circle2.offset, transform2, rigidbody2); + + // Calculate box half-extents + float half_width = box1.width / 2.0; + float half_height = box1.height / 2.0; + + // Find the closest point on the box to the circle's center + float closest_x = std::max(final_position1.x - half_width, std::min(final_position2.x, final_position1.x + half_width)); + float closest_y = std::max(final_position1.y - half_height, std::min(final_position2.y, final_position1.y + half_height)); + + // Calculate the distance squared between the circle's center and the closest point on the box + float distance_x = final_position2.x - closest_x; + float distance_y = final_position2.y - closest_y; + float distance_squared = distance_x * distance_x + distance_y * distance_y; + + // Compare distance squared with the square of the circle's radius + return distance_squared <= circle2.radius * circle2.radius; +} + +bool CollisionSystem::check_circle_circle_collision(const CircleCollider& circle1, const CircleCollider& circle2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2) { + // Get current positions of colliders + vec2 final_position1 = current_position(circle1.offset,transform1,rigidbody1); + vec2 final_position2 = current_position(circle2.offset,transform2,rigidbody2); + + float distance_x = final_position1.x - final_position2.x; + float distance_y = final_position1.y - final_position2.y; + float distance_squared = distance_x * distance_x + distance_y * distance_y; + + // Calculate the sum of the radii + float radius_sum = circle1.radius + circle2.radius; + + // Check if the distance between the centers is less than or equal to the sum of the radii + return distance_squared <= radius_sum * radius_sum; +} + +vec2 CollisionSystem::current_position(vec2 collider_offset, const Transform& transform, const Rigidbody& rigidbody) { + // Get the rotation in radians + float radians1 = transform.rotation * (M_PI / 180.0); + + // Calculate total offset with scale + vec2 total_offset = (rigidbody.data.offset + collider_offset) * transform.scale; + + // Rotate + float rotated_total_offset_x1 = total_offset.x * cos(radians1) - total_offset.y * sin(radians1); + float rotated_total_offset_y1 = total_offset.x * sin(radians1) + total_offset.y * cos(radians1); + + // Final positions considering scaling and rotation + return(transform.position + vec2(rotated_total_offset_x1, rotated_total_offset_y1)); + +} diff --git a/src/crepe/system/CollisionSystem.h b/src/crepe/system/CollisionSystem.h index c1a70d8..ea8c1e1 100644 --- a/src/crepe/system/CollisionSystem.h +++ b/src/crepe/system/CollisionSystem.h @@ -1,13 +1,246 @@ #pragma once +#include <vector> +#include <variant> +#include <optional> + +#include "api/Rigidbody.h" +#include "api/Transform.h" +#include "api/BoxCollider.h" +#include "api/CircleCollider.h" +#include "api/Vector2.h" + +#include "Collider.h" #include "System.h" + namespace crepe { + +//! A system responsible for detecting and handling collisions between colliders. class CollisionSystem : public System { public: using System::System; +private: + + //! A variant type that can hold either a BoxCollider or a CircleCollider. + using collider_variant = std::variant<std::reference_wrapper<BoxCollider>, std::reference_wrapper<CircleCollider>>; + + //! Enum representing the types of collider pairs for collision detection. + enum class CollisionInternalType { + BOX_BOX, + CIRCLE_CIRCLE, + BOX_CIRCLE, + CIRCLE_BOX, + }; + + /** + * \brief A structure to store the collision data of a single collider. + * + * This structure stores the collider type, its associated transform, and its rigidbody. + */ + struct CollisionInternal { + //! Store either BoxCollider or CircleCollider + collider_variant& collider; + Transform& transform; + Rigidbody& rigidbody; + }; + + //! Enum representing movement directions during collision resolution. + enum class Direction { + //! No movement required. + NONE, + //! Movement in the X direction. + X_DIRECTION, + //! Movement in the Y direction. + Y_DIRECTION, + //! Movement in both X and Y directions. + BOTH + }; + +public: + /** + * \brief Structure representing detailed collision information between two colliders. + * + * Includes information about the colliding objects and the resolution data for handling the collision. + */ + struct CollisionInfo{ + Collider& first_collider; + Transform& first_transform; + Rigidbody& first_rigidbody; + Collider& second_collider; + Transform& second_transform; + Rigidbody& second_rigidbody; + //! The resolution vector for the collision. + vec2 resolution; + //! The direction of movement for resolving the collision. + Direction resolution_direction = Direction::NONE; + }; + +public: + + //! Updates the collision system by checking for collisions between colliders and handling them. void update() override; +private: + + /** + * \brief Determines the type of collider pair from two colliders. + * + * Uses std::holds_alternative to identify the types of the provided colliders. + * + * \param collider1 First collider variant (BoxCollider or CircleCollider). + * \param collider2 Second collider variant (BoxCollider or CircleCollider). + * \return The combined type of the two colliders. + */ + CollisionInternalType check_collider_type(const collider_variant& collider1,const collider_variant& collider2); + + /** + * \brief Calculates the current position of a collider. + * + * Combines the Collider offset, Transform position, and Rigidbody offset to compute the position of the collider. + * + * \param collider_offset The offset of the collider. + * \param transform The Transform of the associated game object. + * \param rigidbody The Rigidbody of the associated game object. + * \return The calculated position of the collider. + */ + vec2 current_position(vec2 collider_offset, const Transform& transform, const Rigidbody& rigidbody); + +private: + + /** + * \brief Handles collision resolution between two colliders. + * + * Processes collision data and adjusts objects to resolve collisions and/or calls the user oncollision script function. + * + * \param data1 Collision data for the first collider. + * \param data2 Collision data for the second collider. + */ + void collision_handler_request(CollisionInternal& data1,CollisionInternal& data2); + + /** + * \brief Resolves collision between two colliders and calculates the movement required. + * + * Determines the displacement and direction needed to separate colliders based on their types. + * + * \param data1 Collision data for the first collider. + * \param data2 Collision data for the second collider. + * \param type The type of collider pair. + * \return A pair containing the resolution vector and direction for the first collider. + */ + std::pair<vec2,Direction> collision_handler(CollisionInternal& data1,CollisionInternal& data2 ,CollisionInternalType type); + + /** + * \brief Calculates the resolution vector for two BoxColliders. + * + * Computes the displacement required to separate two overlapping BoxColliders. + * + * \param box_collider1 The first BoxCollider. + * \param box_collider2 The second BoxCollider. + * \param position1 The position of the first BoxCollider. + * \param position2 The position of the second BoxCollider. + * \return The resolution vector for the first BoxCollider. + */ + vec2 box_box_resolution(const BoxCollider& box_collider1,const BoxCollider& box_collider2,vec2 position1,vec2 position2); + + /** + * \brief Determines the appropriate collision handler for a collision. + * + * Decides the correct resolution process based on the dynamic or static nature of the colliders involved. + * + * \param info Collision information containing data about both colliders. + */ + void determine_collision_handler(CollisionInfo& info); + + /** + * \brief Handles collisions involving static objects. + * + * Resolves collisions by adjusting positions and modifying velocities if bounce is enabled. + * + * \param info Collision information containing data about both colliders. + */ + void static_collision_handler(CollisionInfo& info); +private: + + /** + * \brief Checks for collisions between colliders. + * + * Identifies collisions and generates pairs of colliding objects for further processing. + * + * \param colliders A collection of all active colliders. + * \return A list of collision pairs with their associated data. + */ + std::vector<std::pair<CollisionInternal,CollisionInternal>> check_collisions(std::vector<collider_variant> & colliders); + + /** + * \brief Checks for collision between two colliders. + * + * Calls the appropriate collision detection function based on the collider types. + * + * \param first_info Collision data for the first collider. + * \param second_info Collision data for the second collider. + * \param type The type of collider pair. + * \return True if a collision is detected, otherwise false. + */ + bool check_collision(const CollisionInternal& first_info,const CollisionInternal& second_info, CollisionInternalType type); + + /** + * \brief Retrieves the active Transform and Rigidbody components for a given game object. + * + * This function looks up the Transform and Rigidbody components associated with the specified + * game object ID. It checks if both components are present and active. If both are found + * to be active, they are returned wrapped in reference wrappers; otherwise, an empty + * optional is returned. + * + * \param game_object_id The ID of the game object for which to retrieve the components. + * + * \return A std::optional containing a pair of reference wrappers to the active Transform + * and Rigidbody components, or std::nullopt if either component is not found + * or not active. + */ + std::optional<std::pair<std::reference_wrapper<Transform>, std::reference_wrapper<Rigidbody>>> get_active_transform_and_rigidbody(game_object_id_t game_object_id); + + + /** + * \brief Detects collisions between two BoxColliders. + * + * \param box1 The first BoxCollider. + * \param box2 The second BoxCollider. + * \param transform1 Transform of the first object. + * \param transform2 Transform of the second object. + * \param rigidbody1 Rigidbody of the first object. + * \param rigidbody2 Rigidbody of the second object. + * \return True if a collision is detected, otherwise false. + */ + bool check_box_box_collision(const BoxCollider& box1, const BoxCollider& box2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2); + + /** + * \brief Check collision for box on circle collider + * + * \param box1 The BoxCollider + * \param circle2 The CircleCollider + * \param transform1 Transform of the first object. + * \param transform2 Transform of the second object. + * \param rigidbody1 Rigidbody of the first object. + * \param rigidbody2 Rigidbody of the second object. + * \return True if a collision is detected, otherwise false. + */ + bool check_box_circle_collision(const BoxCollider& box1, const CircleCollider& circle2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2); + + /** + * \brief Check collision for circle on circle collider + * + * \param circle1 First CircleCollider + * \param circle2 Second CircleCollider + * \param transform1 Transform of the first object. + * \param transform2 Transform of the second object. + * \param rigidbody1 Rigidbody of the first object. + * \param rigidbody2 Rigidbody of the second object. + * \return True if a collision is detected, otherwise false. + * + * \return status of collision + */ + bool check_circle_circle_collision(const CircleCollider& circle1, const CircleCollider& circle2, const Transform& transform1, const Transform& transform2, const Rigidbody& rigidbody1, const Rigidbody& rigidbody2); }; } // namespace crepe diff --git a/src/example/CMakeLists.txt b/src/example/CMakeLists.txt index 560e2bc..3ec5e43 100644 --- a/src/example/CMakeLists.txt +++ b/src/example/CMakeLists.txt @@ -20,4 +20,5 @@ add_example(asset_manager) add_example(savemgr) add_example(rendering_particle) add_example(gameloop) +add_example(game) diff --git a/src/example/game.cpp b/src/example/game.cpp new file mode 100644 index 0000000..e851526 --- /dev/null +++ b/src/example/game.cpp @@ -0,0 +1,92 @@ +#include <crepe/api/GameObject.h> +#include <crepe/api/Transform.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/BoxCollider.h> +#include <crepe/api/Camera.h> +#include <crepe/api/Script.h> +#include <crepe/api/Color.h> +#include <crepe/api/Sprite.h> +#include <crepe/api/Texture.h> +#include <crepe/api/Vector2.h> +#include <crepe/api/Event.h> +#include <crepe/api/EventManager.h> +#include <crepe/api/LoopManager.h> + +using namespace crepe; + +using namespace std; + +class MyScript : public Script { + bool oncollision(const CollisionEvent& test) { + Log::logf("Box {} script on_collision()", test.info.first.collider.game_object_id); + return true; + } + void init() { + subscribe<CollisionEvent>([this](const CollisionEvent& ev) -> bool { + return this->oncollision(ev); + }); + } + void update() { + // Retrieve component from the same GameObject this script is on + } +}; + + +class ConcreteScene1 : public Scene { +public: + using Scene::Scene; + + void load_scene() { + ComponentManager & mgr = this->component_manager; + Color color(0, 0, 0, 0); + + float screen_size_width = 640; + float screen_size_height = 480; + float world_collider = 1000; + //define playable world + GameObject world = mgr.new_object("Name", "Tag", vec2{screen_size_width/2, screen_size_height/2}, 0, 1); + world.add_component<Rigidbody>(Rigidbody::Data{ + .mass = 0, + .gravity_scale = 0, + .body_type = Rigidbody::BodyType::STATIC, + .constraints = {0, 0, 0}, + .use_gravity = false, + .bounce = false, + .offset = {0,0} + }); + world.add_component<BoxCollider>(vec2{0, 0-(screen_size_height/2+world_collider/2)}, world_collider, world_collider);; // Top + world.add_component<BoxCollider>(vec2{0, screen_size_height/2+world_collider/2}, world_collider, world_collider); // Bottom + world.add_component<BoxCollider>(vec2{0-(screen_size_width/2+world_collider/2), 0}, world_collider, world_collider); // Left + world.add_component<BoxCollider>(vec2{screen_size_width/2+world_collider/2, 0}, world_collider, world_collider); // right + + + GameObject game_object1 = mgr.new_object("Name", "Tag", vec2{screen_size_width/2, screen_size_height/2}, 0, 1); + game_object1.add_component<Rigidbody>(Rigidbody::Data{ + .mass = 1, + .gravity_scale = 0.01, + .body_type = Rigidbody::BodyType::DYNAMIC, + .linear_velocity = {1,1}, + .constraints = {0, 0, 0}, + .use_gravity = true, + .bounce = true, + .elastisity = 1, + .offset = {0,0}, + }); + game_object1.add_component<BoxCollider>(vec2{0, 0}, 20, 20); + game_object1.add_component<BehaviorScript>().set_script<MyScript>(); + game_object1.add_component<Sprite>( + make_shared<Texture>("/home/jaro/crepe/asset/texture/green_square.png"), color, + FlipSettings{true, true}); + game_object1.add_component<Camera>(Color::WHITE); + } + + string get_name() const { return "scene1"; } +}; + +int main(int argc, char * argv[]) { + + LoopManager gameloop; + gameloop.add_scene<ConcreteScene1>(); + gameloop.start(); + return 0; +} diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d310f6a..616e238 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,15 +1,17 @@ target_sources(test_main PUBLIC + CollisionTest.cpp main.cpp - PhysicsTest.cpp - ScriptTest.cpp - ParticleTest.cpp - AssetTest.cpp - OptionalRefTest.cpp - RenderSystemTest.cpp - EventTest.cpp - ECSTest.cpp - SceneManagerTest.cpp - ValueBrokerTest.cpp - DBTest.cpp - Vector2Test.cpp + # PhysicsTest.cpp + # ScriptTest.cpp + # ParticleTest.cpp + # AssetTest.cpp + # OptionalRefTest.cpp + # RenderSystemTest.cpp + # EventTest.cpp + # ECSTest.cpp + # SceneManagerTest.cpp + # ValueBrokerTest.cpp + # DBTest.cpp + # Vector2Test.cpp + # Profiling.cpp ) diff --git a/src/test/CollisionTest.cpp b/src/test/CollisionTest.cpp new file mode 100644 index 0000000..8daf77f --- /dev/null +++ b/src/test/CollisionTest.cpp @@ -0,0 +1,347 @@ +#include <cmath> +#include <cstddef> +#include <gtest/gtest.h> + +#define private public +#define protected public + +#include <crepe/ComponentManager.h> +#include <crepe/api/Event.h> +#include <crepe/api/EventManager.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Script.h> +#include <crepe/api/Transform.h> +#include <crepe/system/CollisionSystem.h> +#include <crepe/system/ScriptSystem.h> +#include <crepe/types.h> +#include <crepe/util/Log.h> + +using namespace std; +using namespace std::chrono_literals; +using namespace crepe; +using namespace testing; + +class CollisionHandler : public Script { +public: + int box_id; + function<void(const CollisionEvent& ev)> test_fn = [](const CollisionEvent & ev) { }; + + CollisionHandler(int box_id) { + this->box_id = box_id; + } + + bool on_collision(const CollisionEvent& ev) { + //Log::logf("Box {} script on_collision()", box_id); + test_fn(ev); + return true; + } + + void init() { + subscribe<CollisionEvent>([this](const CollisionEvent& ev) -> bool { + return this->on_collision(ev); + }); + } + void update() { + // Retrieve component from the same GameObject this script is on + } +}; + +class CollisionTest : public Test { +public: + ComponentManager mgr; + CollisionSystem collision_sys{mgr}; + ScriptSystem script_sys{mgr}; + + GameObject world = mgr.new_object("world","",{50,50}); + GameObject game_object1 = mgr.new_object("object1", "", { 50, 50}); + GameObject game_object2 = mgr.new_object("object2", "", { 50, 30}); + + CollisionHandler * script_object1_ref = nullptr; + CollisionHandler * script_object2_ref = nullptr; + + void SetUp() override { + world.add_component<Rigidbody>(Rigidbody::Data{ + // TODO: remove unrelated properties: + .body_type = Rigidbody::BodyType::STATIC, + .bounce = false, + .offset = {0,0}, + }); + // Create a box with an inner size of 10x10 units + world.add_component<BoxCollider>(vec2{0, -100}, 100, 100); // Top + world.add_component<BoxCollider>(vec2{0, 100}, 100, 100); // Bottom + world.add_component<BoxCollider>(vec2{-100, 0}, 100, 100); // Left + world.add_component<BoxCollider>(vec2{100, 0}, 100, 100); // right + + game_object1.add_component<Rigidbody>(Rigidbody::Data{ + .mass = 1, + .gravity_scale = 0.01, + .body_type = Rigidbody::BodyType::DYNAMIC, + .linear_velocity = {0,0}, + .constraints = {0, 0, 0}, + .use_gravity = true, + .bounce = true, + .elastisity = 1, + .offset = {0,0}, + }); + game_object1.add_component<BoxCollider>(vec2{0, 0}, 10, 10); + BehaviorScript & script_object1 = game_object1.add_component<BehaviorScript>().set_script<CollisionHandler>(1); + script_object1_ref = static_cast<CollisionHandler*>(script_object1.script.get()); + ASSERT_NE(script_object1_ref, nullptr); + + game_object2.add_component<Rigidbody>(Rigidbody::Data{ + .mass = 1, + .gravity_scale = 0.01, + .body_type = Rigidbody::BodyType::DYNAMIC, + .linear_velocity = {0,0}, + .constraints = {0, 0, 0}, + .use_gravity = true, + .bounce = true, + .elastisity = 1, + .offset = {0,0}, + }); + game_object2.add_component<BoxCollider>(vec2{0, 0}, 10, 10); + BehaviorScript & script_object2 = game_object2.add_component<BehaviorScript>().set_script<CollisionHandler>(2); + script_object2_ref = static_cast<CollisionHandler*>(script_object2.script.get()); + ASSERT_NE(script_object2_ref, nullptr); + + // Ensure Script::init() is called on all BehaviorScript instances + script_sys.update(); + } +}; + +TEST_F(CollisionTest, collision_example) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + }; + EXPECT_FALSE(collision_happend); + collision_sys.update(); + EXPECT_FALSE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_both_no_velocity) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, 10); + EXPECT_EQ(ev.info.resolution.y, 10); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 10); + EXPECT_EQ(ev.info.resolution.y, 10); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,30}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_x_direction_no_velocity) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, -5); + EXPECT_EQ(ev.info.resolution.y, 0); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::X_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 5); + EXPECT_EQ(ev.info.resolution.y, 0); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::X_DIRECTION); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {45,30}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_y_direction_no_velocity) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, 0); + EXPECT_EQ(ev.info.resolution.y, -5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::Y_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 0); + EXPECT_EQ(ev.info.resolution.y, 5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::Y_DIRECTION); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,25}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_both) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, 10); + EXPECT_EQ(ev.info.resolution.y, 10); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 10); + EXPECT_EQ(ev.info.resolution.y, 10); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,30}; + Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get(); + rg1.data.linear_velocity = {10,10}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.linear_velocity = {10,10}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_x_direction) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, -5); + EXPECT_EQ(ev.info.resolution.y, -5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::X_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 5); + EXPECT_EQ(ev.info.resolution.y, 5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::X_DIRECTION); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {45,30}; + Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get(); + rg1.data.linear_velocity = {10,10}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.linear_velocity = {10,10}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_dynamic_y_direction) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, -5); + EXPECT_EQ(ev.info.resolution.y, -5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::Y_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 2); + EXPECT_EQ(ev.info.resolution.x, 5); + EXPECT_EQ(ev.info.resolution.y, 5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::Y_DIRECTION); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,25}; + Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get(); + rg1.data.linear_velocity = {10,10}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.linear_velocity = {10,10}; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + + +TEST_F(CollisionTest, collision_box_box_static_both) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, 10); + EXPECT_EQ(ev.info.resolution.y, 10); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + // is static should not be called + FAIL(); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,30}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_static_x_direction) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, -5); + EXPECT_EQ(ev.info.resolution.y, -5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::X_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + // is static should not be called + FAIL(); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {45,30}; + Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get(); + rg1.data.linear_velocity = {10,10}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} + +TEST_F(CollisionTest, collision_box_box_static_y_direction) { + bool collision_happend = false; + script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + collision_happend = true; + EXPECT_EQ(ev.info.first_collider.game_object_id, 1); + EXPECT_EQ(ev.info.resolution.x, -5); + EXPECT_EQ(ev.info.resolution.y, -5); + EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::Y_DIRECTION); + }; + script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) { + // is static should not be called + FAIL(); + }; + EXPECT_FALSE(collision_happend); + Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get(); + tf.position = {50,25}; + Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get(); + rg1.data.linear_velocity = {10,10}; + Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get(); + rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC; + collision_sys.update(); + EXPECT_TRUE(collision_happend); +} diff --git a/src/test/Profiling.cpp b/src/test/Profiling.cpp new file mode 100644 index 0000000..2549c57 --- /dev/null +++ b/src/test/Profiling.cpp @@ -0,0 +1,230 @@ +#include "system/ParticleSystem.h" +#include "system/PhysicsSystem.h" +#include "system/RenderSystem.h" +#include <cmath> +#include <chrono> +#include <gtest/gtest.h> + +#define private public +#define protected public + +#include <crepe/ComponentManager.h> +#include <crepe/api/Event.h> +#include <crepe/api/EventManager.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Script.h> +#include <crepe/api/Transform.h> +#include <crepe/api/ParticleEmitter.h> +#include <crepe/system/CollisionSystem.h> +#include <crepe/system/ScriptSystem.h> +#include <crepe/types.h> +#include <crepe/util/Log.h> + +using namespace std; +using namespace std::chrono_literals; +using namespace crepe; +using namespace testing; + + +/* +List of test cases with component settings/details +1. Minimal test creates gameobject without additonal components +2. Minimal 'Complex' gameobject. Has dynamic body without bounce and no collision handler +3. Minimal 'Complex' gameobject. Same as test 2 but with particle emitter +*/ + +class TestScript : public Script { + bool oncollision(const CollisionEvent& test) { + Log::logf("Box {} script on_collision()", test.info.first_collider.game_object_id); + return true; + } + void init() { + subscribe<CollisionEvent>([this](const CollisionEvent& ev) -> bool { + return this->oncollision(ev); + }); + } + void update() { + // Retrieve component from the same GameObject this script is on + } +}; + +class Profiling : public Test { +public: + // Config for test + // Minimum amount to let test pass + const int min_gameobject_count = 100; + // Maximum amount to stop test + const int max_gameobject_count = 200; + // Amount of times a test runs to calculate average + const int average = 5; + // Maximum duration to stop test + const std::chrono::microseconds duration = 16000us; + + + ComponentManager mgr; + // Add system used for profling tests + CollisionSystem collision_sys{mgr}; + PhysicsSystem physics_sys{mgr}; + ParticleSystem particle_sys{mgr}; + RenderSystem render_sys{mgr}; + ScriptSystem script_sys{mgr}; + + // Test data + std::map<std::string, std::chrono::microseconds> timings; + int game_object_count = 0; + std::chrono::microseconds total_time = 0us; + + + void SetUp() override { + + GameObject do_not_use = mgr.new_object("DO_NOT_USE","",{0,0}); + do_not_use.add_component<Camera>(Color::WHITE); + // initialize systems here: + //calls init + script_sys.update(); + //creates window + render_sys.update(); + } + + // Helper function to time an update call and store its duration + template <typename Func> + std::chrono::microseconds time_function(const std::string& name, Func&& func) { + auto start = std::chrono::steady_clock::now(); + func(); + auto end = std::chrono::steady_clock::now(); + std::chrono::microseconds duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start); + timings[name] += duration; + return duration; + } + + // Run and profile all systems, return the total time in milliseconds + std::chrono::microseconds run_all_systems() { + std::chrono::microseconds total_microseconds = 0us; + total_microseconds += time_function("PhysicsSystem", [&]() { physics_sys.update(); }); + total_microseconds += time_function("CollisionSystem", [&]() { collision_sys.update(); }); + total_microseconds += time_function("ParticleSystem", [&]() { particle_sys.update(); }); + total_microseconds += time_function("RenderSystem", [&]() { render_sys.update(); }); + return total_microseconds; + } + + // Print timings of all functions + void log_timings() const { + std::stringstream ss; + ss << "\nFunction timings:\n"; + for (const auto& [name, duration] : timings) { + ss << name << " took " << duration.count() / 1000.0 / average << " ms (" << duration.count() / average << " µs).\n"; + } + ss << "Total time: " << this->total_time.count() / 1000.0 / average << " ms (" << this->total_time.count() / average << " µs)\n"; + ss << "Amount of gameobjects: " << game_object_count << "\n"; + GTEST_LOG_(INFO) << ss.str(); + } + + void clear_timings() { + for (auto& [key, value] : timings) { + value = std::chrono::microseconds(0); + } + } +}; + +TEST_F(Profiling, Profiling_1) { + while (this->total_time/this->average < this->duration) { + + + { + //define gameobject used for testing + GameObject gameobject = mgr.new_object("gameobject","",{0,0}); + } + + this->game_object_count++; + + this->total_time = 0us; + clear_timings(); + + for (int amount = 0; amount < this->average; amount++) { + this->total_time += run_all_systems(); + } + + if(this->game_object_count >= this->max_gameobject_count) break; + } + log_timings(); + EXPECT_GE(this->game_object_count, this->min_gameobject_count); +} + +TEST_F(Profiling, Profiling_2) { + while (this->total_time/this->average < this->duration) { + + { + //define gameobject used for testing + GameObject gameobject = mgr.new_object("gameobject","",{static_cast<float>(game_object_count*2),0}); + gameobject.add_component<Rigidbody>(Rigidbody::Data{ + .body_type = Rigidbody::BodyType::STATIC, + .use_gravity = false, + }); + gameobject.add_component<BoxCollider>(vec2{0, 0}, 1, 1); + gameobject.add_component<BehaviorScript>().set_script<TestScript>(); + Color color(0, 0, 0, 0); + gameobject.add_component<Sprite>( + make_shared<Texture>("asset/texture/green_square.png"), color, + FlipSettings{true, true}); + } + + this->game_object_count++; + + this->total_time = 0us; + clear_timings(); + for (int amount = 0; amount < this->average; amount++) { + this->total_time += run_all_systems(); + } + + if(this->game_object_count >= this->max_gameobject_count) break; + } + log_timings(); + EXPECT_GE(this->game_object_count, this->min_gameobject_count); +} + +TEST_F(Profiling, Profiling_3) { + while (this->total_time/this->average < this->duration) { + + { + //define gameobject used for testing + GameObject gameobject = mgr.new_object("gameobject","",{static_cast<float>(game_object_count*2),0}); + gameobject.add_component<Rigidbody>(Rigidbody::Data{ + .body_type = Rigidbody::BodyType::STATIC, + .use_gravity = false, + }); + gameobject.add_component<BoxCollider>(vec2{0, 0}, 1, 1); + gameobject.add_component<BehaviorScript>().set_script<TestScript>(); + Color color(0, 0, 0, 0); + gameobject.add_component<Sprite>( + make_shared<Texture>("asset/texture/green_square.png"), color, + FlipSettings{true, true}); + Sprite & test_sprite = gameobject.add_component<Sprite>( + make_shared<Texture>("asset/texture/img.png"), color, FlipSettings{false, false}); + auto & test = gameobject.add_component<ParticleEmitter>(ParticleEmitter::Data{ + .max_particles = 10, + .emission_rate = 100, + .end_lifespan = 100000, + .boundary{ + .width = 1000, + .height = 1000, + .offset = vec2{0, 0}, + .reset_on_exit = false, + }, + .sprite = test_sprite, + }); + } + render_sys.update(); + this->game_object_count++; + + this->total_time = 0us; + clear_timings(); + for (int amount = 0; amount < this->average; amount++) { + this->total_time += run_all_systems(); + } + + if(this->game_object_count >= this->max_gameobject_count) break; + } + log_timings(); + EXPECT_GE(this->game_object_count, this->min_gameobject_count); +} |