aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--asset/texture/ERROR.pngbin0 -> 3581 bytes
-rw-r--r--src/crepe/Particle.cpp29
-rw-r--r--src/crepe/Particle.h65
-rw-r--r--src/crepe/api/ParticleEmitter.cpp39
-rw-r--r--src/crepe/api/ParticleEmitter.h99
-rw-r--r--src/crepe/api/Texture.cpp2
-rw-r--r--src/crepe/api/Vector2.cpp6
-rw-r--r--src/crepe/api/Vector2.h10
-rw-r--r--src/crepe/facade/SDLContext.cpp4
-rw-r--r--src/crepe/facade/SDLContext.h7
-rw-r--r--src/crepe/system/ParticleSystem.cpp157
-rw-r--r--src/crepe/system/ParticleSystem.h64
-rw-r--r--src/crepe/system/RenderSystem.cpp56
-rw-r--r--src/crepe/system/RenderSystem.h18
-rw-r--r--src/example/CMakeLists.txt2
-rw-r--r--src/example/particles.cpp43
-rw-r--r--src/example/rendering.cpp1
-rw-r--r--src/example/rendering_particle.cpp61
-rw-r--r--src/test/CMakeLists.txt3
-rw-r--r--src/test/ParticleTest.cpp206
20 files changed, 731 insertions, 141 deletions
diff --git a/asset/texture/ERROR.png b/asset/texture/ERROR.png
new file mode 100644
index 0000000..2af3548
--- /dev/null
+++ b/asset/texture/ERROR.png
Binary files differ
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);
+}