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.pngBinary files differ new 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.pngBinary files differ new 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); +} |