🠔 Back

R-Type (Epitech B-CPP-501)


Project Status
Completed
Project Type
School Project
Project Duration
~2 months
Team Size
4
Software Used
Visual Studio 2022
Languages Used
C++ 17, SFML, Protobuf, Conan

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++.

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;
	}
};