R-Type (Epitech B-CPP-501)
About the Project
The R-Type is a project I did during my 2nd year at Epitech. The purpose of this project is to create a one-to-four player game, using a client/server architecture.
We got an A Grade with a mark of 44/49. Best grade and mark of our promotion.
Project Architecture
The project is divided into several parts:
RType.Assets contains all the assets used by the game, such as images, sounds, fonts, ...
RType.Common contains the elements shared between the client and the server. It is built as a static library.
RType.Game is the game client, used by players to connect to the server and start a game.
RType.Server contains the server code that handles the game logic and player connections.
Entity Component System
We decided to implement an Entity Component System (ECS) for our project. The reason is quite simple: the subject strongly recommended us to use it, but we also wanted to learn more about this type of architecture.
So, I was in charge of the ECS implementation.
To implement the ECS, I took advantage of the templates in C++.
There is a lot of interfaces in this project. Honestly, there's no reason to inherit each class from an interface, it doesn't add anything except a loss of performance due to virtual methods. However it was an obligation of our subject: All classes must inherit from an interface or abstract class.
Component
The Component
class is a relatively simple class that defines methods common to all components that inherit from it, such as the Destroy()
method which allows you to delete a Component
.
class RTYPECOMMON_API Component : public IComponent
{
public:
Component();
Component(const Component&) = default;
Component(Component&&) noexcept = default;
~Component() override = default;
public:
Component& operator=(const Component&) = default;
Component& operator=(Component&&) noexcept = default;
public:
void Destroy() override;
public:
void SetEntityId(const std::size_t& id) final;
public:
[[nodiscard]] const std::size_t& GetEntityId() const final;
protected:
std::size_t m_EntityId;
};
In addition, each Component
which inherits from the class Component
must define a static and public variable Id
. It must be unique for each Component
otherwise an error occurs during compilation, in particular thanks to type checking with templates.
static constexpr std::size_t Id = 0x0001;
Entity
The Entity class inherits from IEntity.
class RTYPECOMMON_API Entity final : public IEntity
The constructor takes the Id
of the entity to create as a parameter. This is the only parameter required for creating an entity.
public:
[[deprecated("Use Entity::Entity(const std::size_t& id) instead.")]]
Entity();
explicit Entity(const std::size_t& id);
~Entity() override = default;
public:
Entity(const Entity&);
Entity(const Entity&&) noexcept;
public:
Entity& operator=(const Entity&);
Entity& operator=(Entity&&) noexcept;
The components are stored in a std::unordered_map
because we have no particular order to respect. In addition, it allows us to easily retrieve them thanks to their Id
which is used as a key for the map.
std::unordered_map<std::size_t, IComponent*, std::hash<std::size_t>, std::equal_to<>> m_Components;
Thus, adding a component is very simple. Just assign it the Id
of the entity it is associated with and add it to the map.
void Entity::AddComponent(IComponent* component, const std::size_t& id)
{
component->SetEntityId(m_Id);
m_Components.emplace(id, component);
}
For getting a component from an entity, I used the same logic as Unity. We have a templated method with a constraint on the type that must be an IComponent
or a class that inherits from it. This allows me to use the Id
member variable to search the unordered map.
The GetComponent
method returns a std::optional
in order to be able to return an empty value in case the entity does not have the requested component. If the component is found, it is cast to the targeted type using a dynamic_cast<T*>
.
template<typename T, typename = std::enable_if_t<std::is_base_of<IComponent, T>::value>>
std::optional<T*> GetComponent() const
{
auto it = m_Components.find(T::Id);
if (it == m_Components.end())
return {};
return dynamic_cast<T*>(it->second);
}
The same logic is applied for deleting a Component
.
template<typename T, typename = std::enable_if_t<std::is_base_of<IComponent, T>::value>>
void RemoveComponent()
{
auto it = m_Components.find(T::Id);
if (it == m_Components.end())
return;
m_Components.erase(it);
}
Voici un exemple d'utilisation de la classe Entity
pour un Button
.
Entity e(1);
auto* button = new Button();
e.AddComponent(button, Button::Id);
Et pour récupérer le Component
.
const auto c = e.GetComponent<Button>();
// Do something with the Button component.
System
La partie système de l'architecture est probablement la plus complexe et celle qui a pris le plus de temps. Une fois de plus les templates sont utilisées afin d'avoir une gestion des entités et components plus poussée.
La classe System
est donc une classe templaté qui peut pendre en type autant de classe que nécessaire. Cela permet de définir les components géré par le système. Ainsi si une Entity
possèdent tous les components requis par le système elle sera automatiquement gérée par ce dernier.
template<class ... Components>
class System : public ISystem
Les components sont stockés dans un std::tuple
variadique de Components*
(qui est le nom donné au type de la template et non la classe Component
).
using CompTuple = std::tuple<Components*...>;
std::vector<CompTuple> m_Components;
template<class ... Components>
class System : public ISystem
{
public:
explicit System(std::string id) : m_Id(std::move(id)) {}
System(const System&) = default;
System(System&&) noexcept = default;
~System() override = default;
public:
System& operator=(const System&) = default;
System& operator=(System&&) noexcept = default;
protected:
using CompTuple = std::tuple<Components*...>;
const std::string m_Id;
std::unordered_map<std::size_t, std::size_t> m_EntityIdToIndex;
std::vector<CompTuple> m_Components;
public:
void OnEntityCreated(const IEntity& e) final
{
CompTuple compTuple;
std::size_t matchingComps = 0;
for (const auto &[id, component] : e.GetComponents())
{
if (ProcessEntityComponent<0, Components...>(id, component, compTuple))
{
matchingComps += 1;
}
}
if (matchingComps == sizeof...(Components))
{
m_Components.emplace_back(std::move(compTuple));
m_EntityIdToIndex.emplace(e.GetId(), m_Components.size() - 1);
}
}
void OnEntityDestroyed(const std::size_t& id) final
{
const auto& it = m_EntityIdToIndex.find(id);
if (it == m_EntityIdToIndex.end())
return;
m_Components[it->second] = std::move(m_Components.back());
m_Components.pop_back();
if (m_Components.empty() || m_Components.size() <= it->second)
return;
const auto movedComp = std::get<0>(m_Components[it->second]);
const auto movedTupleIt = m_EntityIdToIndex.find(movedComp->GetEntityId());
if (movedTupleIt == m_EntityIdToIndex.end())
throw RTypeException(typeid(System).name(), "Error during Entity destruction.");
movedTupleIt->second = it->second;
}
void Update(const double& deltaTime) override {}
void OnEvent(sf::Event& event) override {}
void OnPacket(const RType::RGamePack& pack) override {}
public:
[[nodiscard]] const std::string& GetId() const final { return m_Id; }
[[nodiscard]] const auto& GetComponents() const { return m_Components; }
private:
template <size_t Index, class CompType, class ... CompArgs>
bool ProcessEntityComponent(const std::size_t& id, IComponent* component, CompTuple& components)
{
if (CompType::Id == id)
{
std::get<Index>(components) = dynamic_cast<CompType*>(component);
return true;
}
return ProcessEntityComponent<Index + 1, CompArgs...>(id, component, components);
}
template <size_t Index>
static bool ProcessEntityComponent(const std::size_t& id, IComponent* component, CompTuple& components)
{
return false;
}
};