How to Add a New Component
This tutorial shows how to create and use a new component in the ECS architecture.
📋 What is a Component?
Components are pure data structures that hold state. They don't contain logic - that belongs in Systems. A component represents a single aspect of an entity's behavior or properties.
Examples:
Position: X, Y coordinatesHealth: Current HP, max HPVelocity: Speed and directionSprite: Visual representation
🎯 Step 1: Define the Component
Components live in lib/common/src/components/ or game-specific directories like src/games/rtype/shared/components/.
Example: Create a Shield Component
Create lib/common/src/components/ShieldComponent.hpp:
#ifndef RTYPE_COMMON_COMPONENTS_SHIELDCOMPONENT_HPP_
#define RTYPE_COMMON_COMPONENTS_SHIELDCOMPONENT_HPP_
namespace rtype::component {
/**
* @brief Shield component providing temporary invincibility
*
* When active, the entity is invulnerable to damage.
* The shield depletes over time or when absorbing hits.
*/
struct Shield {
float maxCapacity; ///< Maximum shield strength
float currentCapacity; ///< Current shield strength
float rechargeRate; ///< Shield recharge per second
float rechargeDelay; ///< Delay before recharge starts (seconds)
float timeSinceHit; ///< Time since last hit
bool isActive; ///< Whether shield is currently active
/**
* @brief Construct a new Shield with default values
*/
Shield(float capacity = 100.0f, float recharge = 20.0f)
: maxCapacity(capacity)
, currentCapacity(capacity)
, rechargeRate(recharge)
, rechargeDelay(2.0f)
, timeSinceHit(0.0f)
, isActive(true) {}
};
} // namespace rtype::component
#endif // RTYPE_COMMON_COMPONENTS_SHIELDCOMPONENT_HPP_
Component Design Guidelines
✅ Good Components (Data Only)
struct Health {
int current;
int maximum;
};
struct Position {
float x;
float y;
};
struct Velocity {
float dx;
float dy;
};
❌ Bad Components (Logic Included)
// DON'T DO THIS - Logic belongs in Systems
struct BadHealth {
int health;
void takeDamage(int amount) { // ❌ No methods
health -= amount;
}
bool isDead() const { // ❌ No logic
return health <= 0;
}
};
🎯 Step 2: Register the Component
Components must be registered with the ECS registry before use.
In Your Game Initialization
Edit src/games/rtype/server/GameEngine.cpp (or client equivalent):
#include "common/src/components/ShieldComponent.hpp"
void GameEngine::initialize() {
// Register all components
registry_.registerComponent<rtype::component::Position>();
registry_.registerComponent<rtype::component::Health>();
registry_.registerComponent<rtype::component::Shield>(); // New component
// ... other components
}
🎯 Step 3: Create a System to Use the Component
Create a system that operates on entities with the component.
Example: ShieldSystem
Create src/games/rtype/server/Systems/ShieldSystem.hpp:
#ifndef RTYPE_SERVER_SYSTEMS_SHIELDSYSTEM_HPP_
#define RTYPE_SERVER_SYSTEMS_SHIELDSYSTEM_HPP_
#include "engine/src/ASystem.hpp"
#include "ecs/src/Registry.hpp"
namespace rtype::server {
/**
* @brief System that manages shield recharge and depletion
*/
class ShieldSystem : public engine::ASystem {
public:
ShieldSystem() : ASystem("ShieldSystem") {}
void update(ECS::Registry& registry, float dt) override;
};
} // namespace rtype::server
#endif // RTYPE_SERVER_SYSTEMS_SHIELDSYSTEM_HPP_
Create src/games/rtype/server/Systems/ShieldSystem.cpp:
#include "ShieldSystem.hpp"
#include "common/src/components/ShieldComponent.hpp"
#include <algorithm>
namespace rtype::server {
void ShieldSystem::update(ECS::Registry& registry, float dt) {
// View all entities with Shield component
auto view = registry.view<component::Shield>();
view.each([dt](auto entity, component::Shield& shield) {
// Increment time since last hit
shield.timeSinceHit += dt;
// Recharge shield if delay has passed
if (shield.timeSinceHit >= shield.rechargeDelay) {
if (shield.currentCapacity < shield.maxCapacity) {
shield.currentCapacity += shield.rechargeRate * dt;
shield.currentCapacity = std::min(
shield.currentCapacity,
shield.maxCapacity
);
}
}
// Deactivate shield if depleted
if (shield.currentCapacity <= 0.0f) {
shield.isActive = false;
}
// Reactivate shield if recharged
if (shield.currentCapacity > 0.0f && !shield.isActive) {
shield.isActive = true;
}
});
}
} // namespace rtype::server
🎯 Step 4: Use the Component in Game Logic
Adding Components to Entities
#include "common/src/components/ShieldComponent.hpp"
// Create entity with shield
auto player = registry.spawnEntity();
registry.emplaceComponent<component::Position>(player, 100.0f, 200.0f);
registry.emplaceComponent<component::Health>(player, 100, 100);
registry.emplaceComponent<component::Shield>(player, 100.0f, 20.0f);
Checking for Components
// Check if entity has shield
if (registry.hasComponent<component::Shield>(entity)) {
auto& shield = registry.getComponent<component::Shield>(entity);
if (shield.isActive) {
// Shield is protecting entity
}
}
Removing Components
// Remove shield when power-up expires
registry.removeComponent<component::Shield>(entity);
🎯 Step 5: Integrate with Other Systems
Example: Damage System Integration
Modify your damage system to respect shields:
void DamageSystem::update(ECS::Registry& registry, float dt) {
auto view = registry.view<component::Health>();
view.each([®istry](auto entity, component::Health& health) {
// Check for incoming damage (from collision system, etc.)
if (hasPendingDamage(entity)) {
int damage = getPendingDamage(entity);
// Check if entity has active shield
if (registry.hasComponent<component::Shield>(entity)) {
auto& shield = registry.getComponent<component::Shield>(entity);
if (shield.isActive && shield.currentCapacity > 0.0f) {
// Shield absorbs damage
shield.currentCapacity -= static_cast<float>(damage);
shield.timeSinceHit = 0.0f; // Reset recharge delay
// Only damage health if shield was depleted
if (shield.currentCapacity < 0.0f) {
health.current += static_cast<int>(shield.currentCapacity);
shield.currentCapacity = 0.0f;
}
} else {
// No shield, apply damage directly
health.current -= damage;
}
} else {
// No shield component, apply damage directly
health.current -= damage;
}
clearPendingDamage(entity);
}
});
}
🎯 Step 6: Add to CMakeLists
For Header-Only Components
No CMakeLists changes needed if your component is header-only.
For Components with Implementation
If your component has a .cpp file:
Edit lib/common/CMakeLists.txt:
target_sources(rtype_common PRIVATE
# ... existing files
src/components/ShieldComponent.cpp
)
🎯 Step 7: Write Tests
Create tests/common/test_shield_component.cpp:
#include <gtest/gtest.h>
#include "ecs/src/Registry.hpp"
#include "common/src/components/ShieldComponent.hpp"
TEST(ShieldComponentTest, DefaultConstruction) {
rtype::component::Shield shield;
EXPECT_FLOAT_EQ(shield.maxCapacity, 100.0f);
EXPECT_FLOAT_EQ(shield.currentCapacity, 100.0f);
EXPECT_TRUE(shield.isActive);
}
TEST(ShieldComponentTest, CustomCapacity) {
rtype::component::Shield shield(200.0f, 50.0f);
EXPECT_FLOAT_EQ(shield.maxCapacity, 200.0f);
EXPECT_FLOAT_EQ(shield.rechargeRate, 50.0f);
}
TEST(ShieldComponentTest, AddToEntity) {
ECS::Registry registry;
registry.registerComponent<rtype::component::Shield>();
auto entity = registry.spawnEntity();
registry.emplaceComponent<rtype::component::Shield>(entity, 100.0f, 20.0f);
EXPECT_TRUE(registry.hasComponent<rtype::component::Shield>(entity));
auto& shield = registry.getComponent<rtype::component::Shield>(entity);
EXPECT_FLOAT_EQ(shield.maxCapacity, 100.0f);
}
Add to tests/CMakeLists.txt:
add_executable(test_shield_component
common/test_shield_component.cpp
)
target_link_libraries(test_shield_component
rtype_common
rtype_ecs
GTest::gtest
GTest::gtest_main
)
add_test(NAME ShieldComponent COMMAND test_shield_component)
🎯 Step 8: Document Your Component
Add Doxygen comments:
/**
* @brief Shield component providing temporary invincibility
*
* The Shield component grants entities temporary protection from damage.
* It features:
* - Automatic recharge after taking damage
* - Configurable capacity and recharge rate
* - Deactivation when depleted
*
* ## Usage Example
*
* @code
* // Create entity with shield
* auto entity = registry.spawnEntity();
* registry.emplaceComponent<Shield>(entity, 100.0f, 20.0f);
*
* // Check shield status
* if (shield.isActive && shield.currentCapacity > 0.0f) {
* // Entity is protected
* }
* @endcode
*
* ## Related Systems
* - ShieldSystem: Handles recharge logic
* - DamageSystem: Consumes shield capacity
* - RenderSystem: May render shield visual effect
*
* @see ShieldSystem
* @see component::Health
*/
struct Shield {
// ...
};
📊 Component Architecture Patterns
Tag Components (Marker)
Empty components used for filtering:
// Mark entities as enemies
struct EnemyTag {};
// Mark entities as players
struct PlayerTag {};
// Usage: View only enemies
auto enemyView = registry.view<EnemyTag, Position>();
State Components
Components that track state machines:
enum class MovementState {
Idle,
Walking,
Running,
Jumping
};
struct MovementStateComponent {
MovementState currentState = MovementState::Idle;
MovementState previousState = MovementState::Idle;
float stateTimer = 0.0f;
};
Relationship Components
Components that reference other entities:
struct Parent {
ECS::Entity entity;
};
struct Children {
std::vector<ECS::Entity> entities;
};
🚀 Advanced Tips
Component Pooling
The ECS automatically pools components for performance. No manual management needed.
Component Dependencies
If a component requires other components, document it:
/**
* @brief Weapon component (requires Position)
*
* @warning This component requires Position component to function correctly
*/
struct Weapon {
// Weapon fires from entity's position
};
Enforce dependencies in system:
void WeaponSystem::update(ECS::Registry& registry, float dt) {
// View requires both Weapon AND Position
auto view = registry.view<Weapon, Position>();
view.each([](auto entity, Weapon& weapon, Position& pos) {
// System only processes entities with both components
});
}
Component Serialization
For save/load functionality:
#include <nlohmann/json.hpp>
struct Shield {
// ... members
nlohmann::json toJson() const {
return {
{"maxCapacity", maxCapacity},
{"currentCapacity", currentCapacity},
{"rechargeRate", rechargeRate}
};
}
static Shield fromJson(const nlohmann::json& j) {
Shield shield;
shield.maxCapacity = j["maxCapacity"];
shield.currentCapacity = j["currentCapacity"];
shield.rechargeRate = j["rechargeRate"];
return shield;
}
};
🎓 Best Practices
- Keep components simple: One responsibility per component
- No logic in components: Logic belongs in Systems
- Use plain data: Avoid pointers, use indices/IDs instead
- Document dependencies: Clearly state required components
- Write tests: Test component behavior in isolation
- Consider performance: Small components are cached better
- Namespace correctly: Use
rtype::component:: - Follow naming: Clear, descriptive names ending in "Component" or short names for common types
📚 Related Tutorials
Happy coding! 🚀