diff options
| -rw-r--r-- | asset/texture/ERROR.png | bin | 0 -> 3581 bytes | |||
| -rw-r--r-- | src/crepe/Particle.cpp | 29 | ||||
| -rw-r--r-- | src/crepe/Particle.h | 65 | ||||
| -rw-r--r-- | src/crepe/api/ParticleEmitter.cpp | 39 | ||||
| -rw-r--r-- | src/crepe/api/ParticleEmitter.h | 99 | ||||
| -rw-r--r-- | src/crepe/api/Texture.cpp | 2 | ||||
| -rw-r--r-- | src/crepe/api/Vector2.cpp | 6 | ||||
| -rw-r--r-- | src/crepe/api/Vector2.h | 10 | ||||
| -rw-r--r-- | src/crepe/facade/SDLContext.cpp | 4 | ||||
| -rw-r--r-- | src/crepe/facade/SDLContext.h | 7 | ||||
| -rw-r--r-- | src/crepe/system/ParticleSystem.cpp | 157 | ||||
| -rw-r--r-- | src/crepe/system/ParticleSystem.h | 64 | ||||
| -rw-r--r-- | src/crepe/system/RenderSystem.cpp | 56 | ||||
| -rw-r--r-- | src/crepe/system/RenderSystem.h | 18 | ||||
| -rw-r--r-- | src/example/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/example/particles.cpp | 43 | ||||
| -rw-r--r-- | src/example/rendering.cpp | 1 | ||||
| -rw-r--r-- | src/example/rendering_particle.cpp | 61 | ||||
| -rw-r--r-- | src/test/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | src/test/ParticleTest.cpp | 206 | 
20 files changed, 731 insertions, 141 deletions
| diff --git a/asset/texture/ERROR.png b/asset/texture/ERROR.pngBinary files differ new file mode 100644 index 0000000..2af3548 --- /dev/null +++ b/asset/texture/ERROR.png diff --git a/src/crepe/Particle.cpp b/src/crepe/Particle.cpp index 4810e80..ab55f37 100644 --- a/src/crepe/Particle.cpp +++ b/src/crepe/Particle.cpp @@ -2,19 +2,32 @@  using namespace crepe; -Particle::Particle() { this->active = false; } - -void Particle::reset(float lifespan, Position position, Position velocity) { +void Particle::reset(uint32_t lifespan, Vector2 position, Vector2 velocity, +					 double angle) { +	// Initialize the particle state  	this->time_in_life = 0;  	this->lifespan = lifespan;  	this->position = position;  	this->velocity = velocity; +	this->angle = angle;  	this->active = true; +	// Reset force accumulation +	this->force_over_time = {0, 0}; +} + +void Particle::update() { +	// Deactivate particle if it has exceeded its lifespan +	if (++time_in_life >= lifespan) { +		this->active = false; +		return; +	} + +	// Update velocity based on accumulated force and update position +	this->velocity += force_over_time; +	this->position += velocity;  } -void Particle::update(float deltaTime) { -	time_in_life += deltaTime; -	position.x += velocity.x * deltaTime; -	position.y += velocity.y * deltaTime; -	if (time_in_life >= lifespan) this->active = false; +void Particle::stop_movement() { +	// Reset velocity to halt movement +	this->velocity = {0, 0};  } diff --git a/src/crepe/Particle.h b/src/crepe/Particle.h index 21e691d..06431bb 100644 --- a/src/crepe/Particle.h +++ b/src/crepe/Particle.h @@ -1,22 +1,65 @@  #pragma once -#include "Position.h" +#include <cstdint> + +#include "api/Vector2.h"  namespace crepe { +/** + * \brief Represents a particle in the particle emitter. + * + * This class stores information about a single particle, including its position, + * velocity, lifespan, and other properties. Particles can be updated over time + * to simulate movement and can also be reset or stopped. + */  class Particle { +	// TODO: add friend particleSsytem and rendersystem. Unit test will fail. +  public: -	Position position; -	// FIXME: `Position` is an awkward name for a 2D vector. See FIXME comment in -	// api/Transform.h for fix proposal. -	Position velocity; -	float lifespan; -	bool active; +	//! Position of the particle in 2D space. +	Vector2 position; +	//! Velocity vector indicating the speed and direction of the particle. +	Vector2 velocity; +	//! Accumulated force affecting the particle over time. +	Vector2 force_over_time; +	//! Total lifespan of the particle in milliseconds. +	uint32_t lifespan; +	//! Active state of the particle; true if it is in use, false otherwise. +	bool active = false; +	//! The time the particle has been alive, in milliseconds. +	uint32_t time_in_life = 0; +	//! The angle at which the particle is oriented or moving. +	double angle; -	Particle(); -	void reset(float lifespan, Position position, Position velocity); -	void update(float deltaTime); -	float time_in_life; +	Particle() = default; +	/** +	 * \brief Resets the particle with new properties. +	 * +	 * This method initializes the particle with a specific lifespan, position, +	 * velocity, and angle, marking it as active and resetting its life counter. +	 * +	 * \param lifespan  The lifespan of the particle in amount of updates. +	 * \param position  The starting position of the particle. +	 * \param velocity  The initial velocity of the particle. +	 * \param angle     The angle of the particle's trajectory or orientation. +	 */ +	void reset(uint32_t lifespan, Vector2 position, Vector2 velocity, +			   double angle); +	/** +	 * \brief Updates the particle's state. +	 * +	 * Advances the particle's position based on its velocity and applies accumulated forces.  +	 * Deactivates the particle if its lifespan has expired. +	 */ +	void update(); +	/** +	 * \brief Stops the particle's movement. +	 * +	 * Sets the particle's velocity to zero, effectively halting any further +	 * movement. +	 */ +	void stop_movement();  };  } // namespace crepe diff --git a/src/crepe/api/ParticleEmitter.cpp b/src/crepe/api/ParticleEmitter.cpp index 0bc2197..e7f298c 100644 --- a/src/crepe/api/ParticleEmitter.cpp +++ b/src/crepe/api/ParticleEmitter.cpp @@ -1,41 +1,18 @@ -#include <ctime> -#include <iostream> - -#include "Particle.h"  #include "ParticleEmitter.h" +#include "Particle.h"  using namespace crepe; -ParticleEmitter::ParticleEmitter(game_object_id_t id, uint32_t max_particles, -								 uint32_t emission_rate, uint32_t speed, -								 uint32_t speed_offset, uint32_t angle, -								 uint32_t angleOffset, float begin_lifespan, -								 float end_lifespan) -	: Component(id), -	  max_particles(max_particles), -	  emission_rate(emission_rate), -	  speed(speed), -	  speed_offset(speed_offset), -	  position{0, 0}, -	  begin_lifespan(begin_lifespan), -	  end_lifespan(end_lifespan) { -	std::srand( -		static_cast<uint32_t>(std::time(nullptr))); // initialize random seed -	std::cout << "Create emitter" << std::endl; -	// FIXME: Why do these expressions start with `360 +`, only to be `% 360`'d -	// right after? This does not make any sense to me. -	min_angle = (360 + angle - (angleOffset % 360)) % 360; -	max_angle = (360 + angle + (angleOffset % 360)) % 360; -	position.x = 400; // FIXME: what are these magic values? -	position.y = 400; -	for (size_t i = 0; i < max_particles; i++) { -		this->particles.emplace_back(); +ParticleEmitter::ParticleEmitter(uint32_t game_object_id, const Data & data) +	: Component(game_object_id), data(data) { +	for (size_t i = 0; i < this->data.max_particles; i++) { +		this->data.particles.emplace_back();  	}  }  ParticleEmitter::~ParticleEmitter() { -	std::vector<Particle>::iterator it = this->particles.begin(); -	while (it != this->particles.end()) { -		it = this->particles.erase(it); +	std::vector<Particle>::iterator it = this->data.particles.begin(); +	while (it != this->data.particles.end()) { +		it = this->data.particles.erase(it);  	}  } diff --git a/src/crepe/api/ParticleEmitter.h b/src/crepe/api/ParticleEmitter.h index 5939723..83a1588 100644 --- a/src/crepe/api/ParticleEmitter.h +++ b/src/crepe/api/ParticleEmitter.h @@ -5,38 +5,87 @@  #include "Component.h"  #include "Particle.h" +#include "Sprite.h" +#include "Vector2.h" + +// class Sprite;  namespace crepe { +/** + * \brief Data holder for particle emission parameters. + * + * The ParticleEmitter class stores configuration data for particle properties, + * defining the characteristics and boundaries of particle emissions. + */ +  class ParticleEmitter : public Component {  public: -	ParticleEmitter(game_object_id_t id, uint32_t max_particles, -					uint32_t emission_rate, uint32_t speed, -					uint32_t speed_offset, uint32_t angle, uint32_t angleOffset, -					float begin_lifespan, float end_lifespan); +	/** +	 * \brief Defines the boundary within which particles are constrained. +	 * +	 * This structure specifies the boundary's size and offset, as well as the +	 * behavior of particles upon reaching the boundary limits. +	 */ +	struct Boundary { +		//! boundary width (midpoint is emitter location) +		double width = 0.0; +		//! boundary height (midpoint is emitter location) +		double height = 0.0; +		//! boundary offset from particle emitter location +		Vector2 offset; +		//! reset on exit or stop velocity and set max postion +		bool reset_on_exit = false; +	}; + +	/** +	 * \brief Holds parameters that control particle emission. +	 * +	 * Contains settings for the emitter’s position, particle speed, angle, lifespan, +	 * boundary, and the sprite used for rendering particles. +	 */ +	struct Data { +		//! position of the emitter +		Vector2 position; +		//! maximum number of particles +		const uint32_t max_particles = 0; +		//! rate of particle emission per update (Lowest value = 0.001 any lower is ignored) +		double emission_rate = 0; +		//! min speed of the particles +		double min_speed = 0; +		//! min speed of the particles +		double max_speed = 0; +		//! min angle of particle emission +		double min_angle = 0; +		//! max angle of particle emission +		double max_angle = 0; +		//! begin Lifespan of particle (only visual) +		double begin_lifespan = 0.0; +		//! end Lifespan of particle +		double end_lifespan = 0.0; +		//! force over time (physics) +		Vector2 force_over_time; +		//! particle boundary +		Boundary boundary; +		//! collection of particles +		std::vector<Particle> particles; +		//! sprite reference +		const Sprite & sprite; +	}; + +public: +	/** +	 * \brief Constructs a ParticleEmitter data holder with specified settings. +	 * +	 * \param game_object_id  Identifier for the game object using this emitter. +	 * \param data            Configuration data defining particle properties. +	 */ +	ParticleEmitter(uint32_t game_object_id, const Data & data);  	~ParticleEmitter(); -	//! position of the emitter -	Position position; -	//! maximum number of particles -	uint32_t max_particles; -	//! rate of particle emission -	uint32_t emission_rate; -	//! base speed of the particles -	uint32_t speed; -	//! offset for random speed variation -	uint32_t speed_offset; -	//! min angle of particle emission -	uint32_t min_angle; -	//! max angle of particle emission -	uint32_t max_angle; -	//! begin Lifespan of particle (only visual) -	float begin_lifespan; -	//! begin Lifespan of particle -	float end_lifespan; - -	//! collection of particles -	std::vector<Particle> particles; +public: +	//! Configuration data for particle emission settings. +	Data data;  };  } // namespace crepe diff --git a/src/crepe/api/Texture.cpp b/src/crepe/api/Texture.cpp index 5ebd23d..8ce65c2 100644 --- a/src/crepe/api/Texture.cpp +++ b/src/crepe/api/Texture.cpp @@ -35,5 +35,5 @@ int Texture::get_width() const {  }  int Texture::get_height() const {  	if (this->texture == nullptr) return 0; -	return SDLContext::get_instance().get_width(*this); +	return SDLContext::get_instance().get_height(*this);  } diff --git a/src/crepe/api/Vector2.cpp b/src/crepe/api/Vector2.cpp index 09bb59b..09b3fa3 100644 --- a/src/crepe/api/Vector2.cpp +++ b/src/crepe/api/Vector2.cpp @@ -3,7 +3,7 @@  namespace crepe {  // Constructor with initial values -Vector2::Vector2(float x, float y) : x(x), y(y) {} +Vector2::Vector2(double x, double y) : x(x), y(y) {}  // Subtracts another vector from this vector and returns the result.  Vector2 Vector2::operator-(const Vector2 & other) const { @@ -16,7 +16,7 @@ Vector2 Vector2::operator+(const Vector2 & other) const {  }  // Multiplies this vector by a scalar and returns the result. -Vector2 Vector2::operator*(float scalar) const { +Vector2 Vector2::operator*(double scalar) const {  	return {x * scalar, y * scalar};  } @@ -35,7 +35,7 @@ Vector2 & Vector2::operator+=(const Vector2 & other) {  }  // Adds a scalar value to both components of this vector and updates this vector. -Vector2 & Vector2::operator+=(float other) { +Vector2 & Vector2::operator+=(double other) {  	x += other;  	y += other;  	return *this; diff --git a/src/crepe/api/Vector2.h b/src/crepe/api/Vector2.h index 741951b..5a57484 100644 --- a/src/crepe/api/Vector2.h +++ b/src/crepe/api/Vector2.h @@ -6,15 +6,15 @@ namespace crepe {  class Vector2 {  public:  	//! X component of the vector -	float x; +	double x;  	//! Y component of the vector -	float y; +	double y;  	//! Default constructor  	Vector2() = default;  	//! Constructor with initial values -	Vector2(float x, float y); +	Vector2(double x, double y);  	//! Subtracts another vector from this vector and returns the result.  	Vector2 operator-(const Vector2 & other) const; @@ -23,7 +23,7 @@ public:  	Vector2 operator+(const Vector2 & other) const;  	//! Multiplies this vector by a scalar and returns the result. -	Vector2 operator*(float scalar) const; +	Vector2 operator*(double scalar) const;  	//! Multiplies this vector by another vector element-wise and updates this vector.  	Vector2 & operator*=(const Vector2 & other); @@ -32,7 +32,7 @@ public:  	Vector2 & operator+=(const Vector2 & other);  	//! Adds a scalar value to both components of this vector and updates this vector. -	Vector2 & operator+=(float other); +	Vector2 & operator+=(double other);  	//! Returns the negation of this vector.  	Vector2 operator-() const; diff --git a/src/crepe/facade/SDLContext.cpp b/src/crepe/facade/SDLContext.cpp index 46230b4..378ccee 100644 --- a/src/crepe/facade/SDLContext.cpp +++ b/src/crepe/facade/SDLContext.cpp @@ -181,12 +181,12 @@ SDLContext::texture_from_path(const std::string & path) {  	return img_texture;  } -int SDLContext::get_width(const Texture & ctx) const { +int SDLContext::get_width(const Texture & ctx) {  	int w;  	SDL_QueryTexture(ctx.texture.get(), NULL, NULL, &w, NULL);  	return w;  } -int SDLContext::get_height(const Texture & ctx) const { +int SDLContext::get_height(const Texture & ctx) {  	int h;  	SDL_QueryTexture(ctx.texture.get(), NULL, NULL, NULL, &h);  	return h; diff --git a/src/crepe/facade/SDLContext.h b/src/crepe/facade/SDLContext.h index 536dec5..c4392bd 100644 --- a/src/crepe/facade/SDLContext.h +++ b/src/crepe/facade/SDLContext.h @@ -10,6 +10,7 @@  #include "../api/Sprite.h"  #include "../api/Transform.h"  #include "api/Camera.h" +#include "api/Vector2.h"  // FIXME: this needs to be removed  const int SCREEN_WIDTH = 640; @@ -108,14 +109,14 @@ private:  	 * \param texture Reference to the Texture object.  	 * \return Width of the texture as an integer.  	 */ -	int get_width(const Texture &) const; +	int get_width(const Texture &) ;  	/**  	 * \brief Gets the height of a texture.  	 * \param texture Reference to the Texture object.  	 * \return Height of the texture as an integer.  	 */ -	int get_height(const Texture &) const; +	int get_height(const Texture &) ;  private:  	//! Will use draw,clear_screen, present_screen, camera. @@ -130,6 +131,8 @@ private:  	void draw(const Sprite & sprite, const Transform & transform,  			  const Camera & camera); +	void draw_particle(const Vector2 & pos, const Camera & camera); +  	//! Clears the screen, preparing for a new frame.  	void clear_screen(); diff --git a/src/crepe/system/ParticleSystem.cpp b/src/crepe/system/ParticleSystem.cpp index 397b586..4a25b47 100644 --- a/src/crepe/system/ParticleSystem.cpp +++ b/src/crepe/system/ParticleSystem.cpp @@ -1,62 +1,143 @@  #include <cmath> +#include <cstdlib>  #include <ctime> -#include "../ComponentManager.h" -#include "../api/ParticleEmitter.h" +#include "api/ParticleEmitter.h" +#include "api/Transform.h" +#include "api/Vector2.h" +#include "ComponentManager.h"  #include "ParticleSystem.h"  using namespace crepe; -ParticleSystem::ParticleSystem() : elapsed_time(0.0f) {} +ParticleSystem::ParticleSystem() {}  void ParticleSystem::update() { +	// Get all emitters  	ComponentManager & mgr = ComponentManager::get_instance();  	std::vector<std::reference_wrapper<ParticleEmitter>> emitters  		= mgr.get_components_by_type<ParticleEmitter>(); -	float delta_time = 0.10; +  	for (ParticleEmitter & emitter : emitters) { -		float update_amount = 1 / static_cast<float>(emitter.emission_rate); -		for (float i = 0; i < delta_time; i += update_amount) { -			emit_particle(emitter); +		// Get transform linked to emitter +		const Transform & transform +			= mgr.get_components_by_id<Transform>(emitter.game_object_id) +				  .front() +				  .get(); + +		// Emit particles based on emission_rate +		int updates +			= calculate_update(this->update_count, emitter.data.emission_rate); +		for (size_t i = 0; i < updates; i++) { +			emit_particle(emitter, transform);  		} -		for (size_t j = 0; j < emitter.particles.size(); j++) { -			if (emitter.particles[j].active) { -				emitter.particles[j].update(delta_time); + +		// Update all particles +		for (Particle & particle : emitter.data.particles) { +			if (particle.active) { +				particle.update();  			}  		} + +		// Check if within boundary +		check_bounds(emitter, transform);  	} + +	this->update_count = (this->update_count + 1) % this->MAX_UPDATE_COUNT;  } -void ParticleSystem::emit_particle(ParticleEmitter & emitter) { -	Position initial_position = {emitter.position.x, emitter.position.y}; -	float random_angle = 0.0f; -	if (emitter.max_angle < emitter.min_angle) { -		random_angle = ((emitter.min_angle -						 + (std::rand() -							% (static_cast<uint32_t>(emitter.max_angle + 360 -													 - emitter.min_angle + 1)))) -						% 360); -	} else { -		random_angle = emitter.min_angle -					   + (std::rand() -						  % (static_cast<uint32_t>(emitter.max_angle -												   - emitter.min_angle + 1))); -	} -	float angle_in_radians = random_angle * (M_PI / 180.0f); -	float random_speed_offset = (static_cast<float>(std::rand()) / RAND_MAX) -									* (2 * emitter.speed_offset) -								- emitter.speed_offset; -	float velocity_x -		= (emitter.speed + random_speed_offset) * std::cos(angle_in_radians); -	float velocity_y -		= (emitter.speed + random_speed_offset) * std::sin(angle_in_radians); -	Position initial_velocity = {velocity_x, velocity_y}; -	for (size_t i = 0; i < emitter.particles.size(); i++) { -		if (!emitter.particles[i].active) { -			emitter.particles[i].reset(emitter.end_lifespan, initial_position, -									   initial_velocity); +void ParticleSystem::emit_particle(ParticleEmitter & emitter, +								   const Transform & transform) { +	constexpr double DEG_TO_RAD = M_PI / 180.0; + +	Vector2 initial_position = emitter.data.position + transform.position; +	double random_angle +		= generate_random_angle(emitter.data.min_angle, emitter.data.max_angle); + +	double random_speed +		= generate_random_speed(emitter.data.min_speed, emitter.data.max_speed); +	double angle_radians = random_angle * DEG_TO_RAD; + +	Vector2 velocity = {random_speed * std::cos(angle_radians), +						random_speed * std::sin(angle_radians)}; + +	for (Particle & particle : emitter.data.particles) { +		if (!particle.active) { +			particle.reset(emitter.data.end_lifespan, initial_position, +						   velocity, random_angle);  			break;  		}  	}  } + +int ParticleSystem::calculate_update(int count, double emission) const { +	double integer_part = std::floor(emission); +	double fractional_part = emission - integer_part; + +	if (fractional_part > 0) { +		int denominator = static_cast<int>(1.0 / fractional_part); +		return (count % denominator == 0) ? 1 : 0; +	} + +	return static_cast<int>(emission); +} + +void ParticleSystem::check_bounds(ParticleEmitter & emitter, +								  const Transform & transform) { +	Vector2 offset = emitter.data.boundary.offset + transform.position +					 + emitter.data.position; +	double half_width = emitter.data.boundary.width / 2.0; +	double half_height = emitter.data.boundary.height / 2.0; + +	const double LEFT = offset.x - half_width; +	const double RIGHT = offset.x + half_width; +	const double TOP = offset.y - half_height; +	const double BOTTOM = offset.y + half_height; + +	for (Particle & particle : emitter.data.particles) { +		const Vector2 & position = particle.position; +		bool within_bounds = (position.x >= LEFT && position.x <= RIGHT +							  && position.y >= TOP && position.y <= BOTTOM); + +		if (!within_bounds) { +			if (emitter.data.boundary.reset_on_exit) { +				particle.active = false; +			} else { +				particle.velocity = {0, 0}; +				if (position.x < LEFT) particle.position.x = LEFT; +				else if (position.x > RIGHT) particle.position.x = RIGHT; +				if (position.y < TOP) particle.position.y = TOP; +				else if (position.y > BOTTOM) particle.position.y = BOTTOM; +			} +		} +	} +} + +double ParticleSystem::generate_random_angle(double min_angle, +											 double max_angle) const { +	if (min_angle == max_angle) { +		return min_angle; +	} else if (min_angle < max_angle) { +		return min_angle +			   + static_cast<double>(std::rand() +									 % static_cast<int>(max_angle - min_angle)); +	} else { +		double angle_offset = (360 - min_angle) + max_angle; +		double random_angle = min_angle +							  + static_cast<double>( +								  std::rand() % static_cast<int>(angle_offset)); +		return (random_angle >= 360) ? random_angle - 360 : random_angle; +	} +} + +double ParticleSystem::generate_random_speed(double min_speed, +											 double max_speed) const { +	if (min_speed == max_speed) { +		return min_speed; +	} else { +		return min_speed +			   + static_cast<double>(std::rand() +									 % static_cast<int>(max_speed - min_speed)); +	} +} diff --git a/src/crepe/system/ParticleSystem.h b/src/crepe/system/ParticleSystem.h index 3ac1d3f..3155df1 100644 --- a/src/crepe/system/ParticleSystem.h +++ b/src/crepe/system/ParticleSystem.h @@ -1,18 +1,74 @@  #pragma once -#include "../api/ParticleEmitter.h" +#include <cstdint>  namespace crepe { - +class ParticleEmitter; +class Transform; +/** + 	* \brief ParticleSystem class responsible for managing particle emission, updates, and bounds checking. + 	*/  class ParticleSystem {  public: +	/** +		* \brief Default constructor. +		*/  	ParticleSystem(); + +	/** +		* \brief Updates all particle emitters by emitting particles, updating particle states, and checking bounds. +		*/  	void update();  private: -	void emit_particle(ParticleEmitter & emitter); //emits a new particle +	/** +		* \brief Emits a particle from the specified emitter based on its emission properties. +		*  +		* \param emitter Reference to the ParticleEmitter. +		* \param transform Const reference to the Transform component associated with the emitter. +		*/ +	void emit_particle(ParticleEmitter & emitter, const Transform & transform); + +	/** +		* \brief Calculates the number of times particles should be emitted based on emission rate and update count. +		*  +		* \param count Current update count. +		* \param emission Emission rate. +		* \return The number of particles to emit. +		*/ +	int calculate_update(int count, double emission) const; + +	/** +		* \brief Checks whether particles are within the emitter’s boundary, resets or stops particles if they exit. +		*  +		* \param emitter Reference to the ParticleEmitter. +		* \param transform Const reference to the Transform component associated with the emitter. +		*/ +	void check_bounds(ParticleEmitter & emitter, const Transform & transform); -	float elapsed_time; //elapsed time since the last emission +	/** +		* \brief Generates a random angle for particle emission within the specified range. +		*  +		* \param min_angle Minimum emission angle in degrees. +		* \param max_angle Maximum emission angle in degrees. +		* \return Random angle in degrees. +		*/ +	double generate_random_angle(double min_angle, double max_angle) const; + +	/** +		* \brief Generates a random speed for particle emission within the specified range. +		*  +		* \param min_speed Minimum emission speed. +		* \param max_speed Maximum emission speed. +		* \return Random speed. +		*/ +	double generate_random_speed(double min_speed, double max_speed) const; + +private: +	//! Counter to count updates to determine how many times emit_particle is called. +	uint32_t update_count = 0; +	//! Determines the lowest amount of emission rate (1000 = 0.001 = 1 particle per 1000 updates). +	static constexpr uint32_t MAX_UPDATE_COUNT = 100;  };  } // namespace crepe diff --git a/src/crepe/system/RenderSystem.cpp b/src/crepe/system/RenderSystem.cpp index 10211a3..17a2337 100644 --- a/src/crepe/system/RenderSystem.cpp +++ b/src/crepe/system/RenderSystem.cpp @@ -1,3 +1,4 @@ +#include <cmath>  #include <functional>  #include <vector> @@ -6,7 +7,10 @@  #include "../api/Transform.h"  #include "../facade/SDLContext.h"  #include "../util/log.h" +#include "../api/ParticleEmitter.h" +#include "../api/Vector2.h" +#include "Particle.h"  #include "RenderSystem.h"  using namespace crepe; @@ -38,24 +42,62 @@ void RenderSystem::update_camera() {  		this->curr_cam = &cam;  	}  } -void RenderSystem::render_sprites() const { + +bool RenderSystem::render_particle(const Sprite & sprite, +								   const Transform & tm) {  	ComponentManager & mgr = ComponentManager::get_instance(); +	SDLContext & render = SDLContext::get_instance(); + +	auto emitters = mgr.get_components_by_id<ParticleEmitter>(sprite.game_object_id); + +	bool rendering_particles = false; + +	Transform tmp(0, Vector2{0, 0}, 0, 0); +	tmp.scale = tm.scale; +	for (const ParticleEmitter & em : emitters) { +		if (!em.active) continue; +		if (!(em.data.sprite.game_object_id == sprite.game_object_id)) continue; -	std::vector<std::reference_wrapper<Sprite>> sprites -		= mgr.get_components_by_type<Sprite>(); +		rendering_particles = true; +		for (const Particle & p : em.data.particles) { +			if (!p.active) continue; +			tmp.position = p.position; +			tmp.rotation = p.angle; +			render.draw(em.data.sprite, tmp, *curr_cam); +		} +	} +	return rendering_particles; +} +void RenderSystem::render_normal(const Sprite & sprite, const Transform & tm) { + +	ComponentManager & mgr = ComponentManager::get_instance();  	SDLContext & render = SDLContext::get_instance(); +	 +	render.draw(sprite, tm, *curr_cam); +} + +void RenderSystem::render() { + +	ComponentManager & mgr = ComponentManager::get_instance(); + +	auto sprites = mgr.get_components_by_type<Sprite>();  	for (const Sprite & sprite : sprites) { -		auto transforms -			= mgr.get_components_by_id<Transform>(sprite.game_object_id); -		render.draw(sprite, transforms[0], *curr_cam); +		if (!sprite.active) continue; +		auto transform = mgr.get_components_by_id<Transform>(sprite.game_object_id); + +		bool rendered_particles = this->render_particle(sprite, transform[0].get()); + +		if (rendered_particles) continue; + +		this->render_normal(sprite, transform[0].get());  	}  }  void RenderSystem::update() {  	this->clear_screen();  	this->update_camera(); -	this->render_sprites(); +	this->render();  	this->present_screen();  } diff --git a/src/crepe/system/RenderSystem.h b/src/crepe/system/RenderSystem.h index 70db21a..468f79b 100644 --- a/src/crepe/system/RenderSystem.h +++ b/src/crepe/system/RenderSystem.h @@ -1,6 +1,8 @@  #pragma once  #include "api/Camera.h" +#include "api/Sprite.h" +#include "api/Transform.h"  #include "System.h" @@ -43,12 +45,22 @@ private:  	//! Updates the active camera used for rendering.  	void update_camera(); -	//! Renders all active sprites to the screen. -	void render_sprites() const; +	//! Renders the whole screen +	void render(); + +	/** +	 * \brief Renders all the particles on the screen from a given sprite. +	 * +	 * \param sprite renders the particles with given texture +	 * \param tm the Transform component for scale +	 * \return true if particles have been rendered +	 */ +	bool render_particle(const Sprite &, const Transform & tm); + +	void render_normal(const Sprite &, const Transform & tm);  	/**  	 * \todo Include color handling for sprites. -	 * \todo Implement particle emitter rendering with sprites.  	 * \todo Add text rendering using SDL_ttf for text components.  	 * \todo Implement a text component and a button component.  	 * \todo Ensure each sprite is checked for active status before rendering. diff --git a/src/example/CMakeLists.txt b/src/example/CMakeLists.txt index c8a4b85..ec08c08 100644 --- a/src/example/CMakeLists.txt +++ b/src/example/CMakeLists.txt @@ -28,5 +28,7 @@ add_example(proxy)  add_example(db)  add_example(ecs)  add_example(scene_manager) +add_example(particles) +add_example(rendering_particle)  add_example(gameloop) diff --git a/src/example/particles.cpp b/src/example/particles.cpp new file mode 100644 index 0000000..6eab046 --- /dev/null +++ b/src/example/particles.cpp @@ -0,0 +1,43 @@ +#include <crepe/ComponentManager.h> +#include <crepe/api/AssetManager.h> + +#include <crepe/Component.h> +#include <crepe/api/Color.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/ParticleEmitter.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Sprite.h> +#include <crepe/api/Texture.h> +#include <crepe/api/Transform.h> + +using namespace crepe; +using namespace std; + +int main(int argc, char * argv[]) { +	GameObject game_object(0, "", "", Vector2{0, 0}, 0, 0); +	Color color(0, 0, 0, 0); +	Sprite test_sprite = game_object.add_component<Sprite>( +		make_shared<Texture>("../asset/texture/img.png"), color, +		FlipSettings{true, true}); +	game_object.add_component<ParticleEmitter>(ParticleEmitter::Data{ +		.position = {0, 0}, +		.max_particles = 100, +		.emission_rate = 0, +		.min_speed = 0, +		.max_speed = 0, +		.min_angle = 0, +		.max_angle = 0, +		.begin_lifespan = 0, +		.end_lifespan = 0, +		.force_over_time = Vector2{0, 0}, +		.boundary{ +			.width = 0, +			.height = 0, +			.offset = Vector2{0, 0}, +			.reset_on_exit = false, +		}, +		.sprite = test_sprite, +	}); + +	return 0; +} diff --git a/src/example/rendering.cpp b/src/example/rendering.cpp index e02f6a3..a7cb5d6 100644 --- a/src/example/rendering.cpp +++ b/src/example/rendering.cpp @@ -35,6 +35,7 @@ int main() {  	{  		Color color(0, 0, 0, 0);  		obj1.add_component<Sprite>(make_shared<Texture>("../asset/texture/second.png"), color, FlipSettings{true, true}); +		obj1.add_component<Sprite>(make_shared<Texture>("../asset/texture/second.png"), color, FlipSettings{true, true});  	}  	/* diff --git a/src/example/rendering_particle.cpp b/src/example/rendering_particle.cpp new file mode 100644 index 0000000..dd08354 --- /dev/null +++ b/src/example/rendering_particle.cpp @@ -0,0 +1,61 @@ +#include "api/Camera.h" +#include "system/ParticleSystem.h" +#include <SDL2/SDL_timer.h> +#include <crepe/ComponentManager.h> + +#include <crepe/Component.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/ParticleEmitter.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Texture.h> +#include <crepe/api/Transform.h> +#include <crepe/system/RenderSystem.h> +#include <crepe/util/log.h> +#include <crepe/api/Color.h> +#include <crepe/api/Sprite.h> +#include <crepe/api/Vector2.h> + +#include <chrono> + +using namespace crepe; +using namespace std; + +int main(int argc, char * argv[]) { +	GameObject game_object(0, "", "", Vector2{100, 100}, 0, 0.1); +	Color color(0, 0, 0, 0); +	Sprite test_sprite = game_object.add_component<Sprite>( +		make_shared<Texture>("../asset/texture/img.png"), color, +		FlipSettings{false, false}); +	game_object.add_component<ParticleEmitter>(ParticleEmitter::Data{ +		.position = {0, 0}, +		.max_particles = 10, +		.emission_rate = 0.5, +		.min_speed = 6, +		.max_speed = 20, +		.min_angle = -20, +		.max_angle = 20, +		.begin_lifespan = 0, +		.end_lifespan = 60, +		.force_over_time = Vector2{0, 0}, +		.boundary{ +			.width = 1000, +			.height = 1000, +			.offset = Vector2{0, 0}, +			.reset_on_exit = false, +		}, +		.sprite = test_sprite, +	}); +	game_object.add_component<Camera>(Color::get_white()); + +	auto & sys = crepe::RenderSystem::get_instance(); +	auto sys_part = crepe::ParticleSystem(); +	auto start = std::chrono::steady_clock::now(); +	while (std::chrono::steady_clock::now() - start < std::chrono::seconds(5)) { +		sys_part.update(); +		sys.update(); +		SDL_Delay(10 ); +	} + +	return 0; +} + diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 0e4eaed..acab388 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,6 +1,7 @@  target_sources(test_main PUBLIC  	dummy.cpp  	# audio.cpp -	PhysicsTest.cpp +	# PhysicsTest.cpp +	ParticleTest.cpp  ) diff --git a/src/test/ParticleTest.cpp b/src/test/ParticleTest.cpp new file mode 100644 index 0000000..6fe3133 --- /dev/null +++ b/src/test/ParticleTest.cpp @@ -0,0 +1,206 @@ +#include "api/Vector2.h" +#include <crepe/ComponentManager.h> +#include <crepe/Particle.h> +#include <crepe/api/Config.h> +#include <crepe/api/GameObject.h> +#include <crepe/api/ParticleEmitter.h> +#include <crepe/api/Rigidbody.h> +#include <crepe/api/Sprite.h> +#include <crepe/api/Transform.h> +#include <crepe/system/ParticleSystem.h> +#include <gtest/gtest.h> +#include <math.h> + +using namespace std; +using namespace std::chrono_literals; +using namespace crepe; + +class ParticlesTest : public ::testing::Test { +protected: +	ParticleSystem particle_system; +	void SetUp() override { +		ComponentManager & mgr = ComponentManager::get_instance(); +		std::vector<std::reference_wrapper<Transform>> transforms +			= mgr.get_components_by_id<Transform>(0); +		if (transforms.empty()) { + +			GameObject game_object(0, "", "", Vector2{0, 0}, 0, 0); + +			Color color(0, 0, 0, 0); +			Sprite test_sprite = game_object.add_component<Sprite>( +				make_shared<Texture>("../asset/texture/img.png"), color, +				FlipSettings{true, true}); + +			game_object.add_component<ParticleEmitter>(ParticleEmitter::Data{ +				.position = {0, 0}, +				.max_particles = 100, +				.emission_rate = 0, +				.min_speed = 0, +				.max_speed = 0, +				.min_angle = 0, +				.max_angle = 0, +				.begin_lifespan = 0, +				.end_lifespan = 0, +				.force_over_time = Vector2{0, 0}, +				.boundary{ +					.width = 0, +					.height = 0, +					.offset = Vector2{0, 0}, +					.reset_on_exit = false, +				}, +				.sprite = test_sprite, +			}); +		} +		transforms = mgr.get_components_by_id<Transform>(0); +		Transform & transform = transforms.front().get(); +		transform.position.x = 0.0; +		transform.position.y = 0.0; +		transform.rotation = 0.0; +		std::vector<std::reference_wrapper<ParticleEmitter>> rigidbodies +			= mgr.get_components_by_id<ParticleEmitter>(0); +		ParticleEmitter & emitter = rigidbodies.front().get(); +		emitter.data.position = {0, 0}; +		emitter.data.emission_rate = 0; +		emitter.data.min_speed = 0; +		emitter.data.max_speed = 0; +		emitter.data.min_angle = 0; +		emitter.data.max_angle = 0; +		emitter.data.begin_lifespan = 0; +		emitter.data.end_lifespan = 0; +		emitter.data.force_over_time = Vector2{0, 0}; +		emitter.data.boundary = {0, 0, Vector2{0, 0}, false}; +		for (auto & particle : emitter.data.particles) { +			particle.active = false; +		} +	} +}; + +TEST_F(ParticlesTest, spawnParticle) { +	Config::get_instance().physics.gravity = 1; +	ComponentManager & mgr = ComponentManager::get_instance(); +	ParticleEmitter & emitter +		= mgr.get_components_by_id<ParticleEmitter>(0).front().get(); +	emitter.data.end_lifespan = 5; +	emitter.data.boundary.height = 100; +	emitter.data.boundary.width = 100; +	emitter.data.max_speed = 0.1; +	emitter.data.max_angle = 0.1; +	emitter.data.max_speed = 10; +	emitter.data.max_angle = 10; +	particle_system.update(); +	//check if nothing happend +	EXPECT_EQ(emitter.data.particles[0].active, false); +	emitter.data.emission_rate = 1; +	//check particle spawnes +	particle_system.update(); +	EXPECT_EQ(emitter.data.particles[0].active, true); +	particle_system.update(); +	EXPECT_EQ(emitter.data.particles[1].active, true); +	particle_system.update(); +	EXPECT_EQ(emitter.data.particles[2].active, true); +	particle_system.update(); +	EXPECT_EQ(emitter.data.particles[3].active, true); + +	for (auto & particle : emitter.data.particles) { +		// Check velocity range +		EXPECT_GE(particle.velocity.x, emitter.data.min_speed); +		// Speed should be greater than or equal to min_speed +		EXPECT_LE(particle.velocity.x, emitter.data.max_speed); +		// Speed should be less than or equal to max_speed +		EXPECT_GE(particle.velocity.y, emitter.data.min_speed); +		// Speed should be greater than or equal to min_speed +		EXPECT_LE(particle.velocity.y, emitter.data.max_speed); +		// Speed should be less than or equal to max_speed + +		// Check angle range +		EXPECT_GE(particle.angle, emitter.data.min_angle); +		// Angle should be greater than or equal to min_angle +		EXPECT_LE(particle.angle, emitter.data.max_angle); +		// Angle should be less than or equal to max_angle +	} +} + +TEST_F(ParticlesTest, moveParticleHorizontal) { +	Config::get_instance().physics.gravity = 1; +	ComponentManager & mgr = ComponentManager::get_instance(); +	ParticleEmitter & emitter +		= mgr.get_components_by_id<ParticleEmitter>(0).front().get(); +	emitter.data.end_lifespan = 100; +	emitter.data.boundary.height = 100; +	emitter.data.boundary.width = 100; +	emitter.data.min_speed = 1; +	emitter.data.max_speed = 1; +	emitter.data.max_angle = 0; +	emitter.data.emission_rate = 1; +	for (int a = 1; a < emitter.data.boundary.width / 2; a++) { +		particle_system.update(); +		EXPECT_EQ(emitter.data.particles[0].position.x, a); +	} +} + +TEST_F(ParticlesTest, moveParticleVertical) { +	Config::get_instance().physics.gravity = 1; +	ComponentManager & mgr = ComponentManager::get_instance(); +	ParticleEmitter & emitter +		= mgr.get_components_by_id<ParticleEmitter>(0).front().get(); +	emitter.data.end_lifespan = 100; +	emitter.data.boundary.height = 100; +	emitter.data.boundary.width = 100; +	emitter.data.min_speed = 1; +	emitter.data.max_speed = 1; +	emitter.data.min_angle = 90; +	emitter.data.max_angle = 90; +	emitter.data.emission_rate = 1; +	for (int a = 1; a < emitter.data.boundary.width / 2; a++) { +		particle_system.update(); +		EXPECT_EQ(emitter.data.particles[0].position.y, a); +	} +} + +TEST_F(ParticlesTest, boundaryParticleReset) { +	Config::get_instance().physics.gravity = 1; +	ComponentManager & mgr = ComponentManager::get_instance(); +	ParticleEmitter & emitter +		= mgr.get_components_by_id<ParticleEmitter>(0).front().get(); +	emitter.data.end_lifespan = 100; +	emitter.data.boundary.height = 10; +	emitter.data.boundary.width = 10; +	emitter.data.boundary.reset_on_exit = true; +	emitter.data.min_speed = 1; +	emitter.data.max_speed = 1; +	emitter.data.min_angle = 90; +	emitter.data.max_angle = 90; +	emitter.data.emission_rate = 1; +	for (int a = 0; a < emitter.data.boundary.width / 2 + 1; a++) { +		particle_system.update(); +	} +	EXPECT_EQ(emitter.data.particles[0].active, false); +} + +TEST_F(ParticlesTest, boundaryParticleStop) { +	Config::get_instance().physics.gravity = 1; +	ComponentManager & mgr = ComponentManager::get_instance(); +	ParticleEmitter & emitter +		= mgr.get_components_by_id<ParticleEmitter>(0).front().get(); +	emitter.data.end_lifespan = 100; +	emitter.data.boundary.height = 10; +	emitter.data.boundary.width = 10; +	emitter.data.boundary.reset_on_exit = false; +	emitter.data.min_speed = 1; +	emitter.data.max_speed = 1; +	emitter.data.min_angle = 90; +	emitter.data.max_angle = 90; +	emitter.data.emission_rate = 1; +	for (int a = 0; a < emitter.data.boundary.width / 2 + 1; a++) { +		particle_system.update(); +	} +	const double TOLERANCE = 0.01; +	EXPECT_NEAR(emitter.data.particles[0].velocity.x, 0, TOLERANCE); +	EXPECT_NEAR(emitter.data.particles[0].velocity.y, 0, TOLERANCE); +	if (emitter.data.particles[0].velocity.x != 0) +		EXPECT_NEAR(std::abs(emitter.data.particles[0].position.x), +					emitter.data.boundary.height / 2, TOLERANCE); +	if (emitter.data.particles[0].velocity.y != 0) +		EXPECT_NEAR(std::abs(emitter.data.particles[0].position.y), +					emitter.data.boundary.width / 2, TOLERANCE); +} |