diff --git a/Server/CMakeLists.txt b/Server/CMakeLists.txt index 57855d3b..a851fa43 100644 --- a/Server/CMakeLists.txt +++ b/Server/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(${PROJECT_NAME} src/systems/apply_movement.cpp src/systems/apply_enemy_movement.cpp src/systems/broadcast_updated_movements.cpp + src/systems/broadcast_dead_entities.cpp ) target_include_directories(${PROJECT_NAME} PRIVATE diff --git a/Server/src/handlers/handle_user_input.cpp b/Server/src/handlers/handle_user_input.cpp index 10322b24..54480245 100644 --- a/Server/src/handlers/handle_user_input.cpp +++ b/Server/src/handlers/handle_user_input.cpp @@ -1,6 +1,7 @@ #include "components/hitbox.hpp" #include "components/position.hpp" #include "enums/input.hpp" +#include "enums/player_state.hpp" #include "handlers.hpp" #include "lobby/lobby.hpp" #include "packets/client/user_input.hpp" @@ -14,8 +15,14 @@ static void spawnBullet(const rtecs::types::EntityID& id, { if (packet.input_mask & static_cast(game::Input::kShoot)) { using namespace components; - lobby.spawnEntity( - {entity::Type::kBullet}, {pos.x, pos.y}, {id}, {20, 0}, {true, 75, 35}, {}); + lobby.spawnEntity( + {entity::Type::kBullet}, + {pos.x, pos.y}, + {id}, + {20, 0}, + {true, 75, 35}, + {entity::state::EntityAlive}, + {}); } } @@ -34,7 +41,8 @@ void handleUserInput(const SessionPtr& session, if (!velocity) { LOG_WARN( "This should not happen, check for component velocity on this entity or for this " - "entity presence."); + "entity presence. (entity id: {})", + lobby.getPlayerId(session).value()); return; } auto& [vx, vy, max_vx, max_vy] = velocity.value().get(); diff --git a/Server/src/level_director/level_director.cpp b/Server/src/level_director/level_director.cpp index 46661209..801c675a 100644 --- a/Server/src/level_director/level_director.cpp +++ b/Server/src/level_director/level_director.cpp @@ -115,13 +115,14 @@ void Director::update(const float dt, x, y); using namespace components; - lobby.spawnEntity( + lobby.spawnEntity( {x, y}, {group.type}, typeToHitbox(group.type), typeToVelocity(group.type), {}, typeToValue(group.type), + {entity::state::EntityAlive}, {nameToMoveSet(group.patternName)}); wave.spawnedInGroup++; diff --git a/Server/src/lobby/lobby.cpp b/Server/src/lobby/lobby.cpp index 0b56ae80..bac4a8f9 100644 --- a/Server/src/lobby/lobby.cpp +++ b/Server/src/lobby/lobby.cpp @@ -14,6 +14,7 @@ #include "logger/Thread.h" #include "systems/apply_enemy_movement.hpp" #include "systems/apply_movement.hpp" +#include "systems/broadcast_dead_entities.hpp" #include "systems/broadcast_updated_movements.hpp" Lobby::Lobby(const lobby::Id id, @@ -34,6 +35,7 @@ void Lobby::registerAllSystems() _engine.registerSystem(std::make_shared()); _engine.registerSystem(std::make_shared()); _engine.registerSystem(std::make_shared(*this)); + _engine.registerSystem(std::make_shared(*this)); } lobby::Id Lobby::getRoomId() const { return _roomId; } @@ -68,6 +70,21 @@ void Lobby::broadcast(const packet::server::Variant& packet) const _outGoing.push({getAllSessions(), packet}); } +rtecs::types::EntityID Lobby::killEntity(const rtecs::types::EntityID id, + const packet::server::SessionPtr& session) +{ + std::cout << "Killing entity " << id << std::endl; + _engine.destroyEntity(id); + if (session) { + _players.erase(session); + } + packet::Destroy p{}; + p.id = id; + p.earned_points = 0; + broadcast(p); + return id; +} + rteng::GameEngine& Lobby::getEngine() { return _engine; } void Lobby::changeGameState(const uint64_t& gameState) @@ -95,7 +112,7 @@ bool Lobby::hasPlayerAlive() } return std::ranges::any_of(_players | std::views::values, [&](const auto& playerId) { const auto& stateOpt = _engine.getEntityFromGroup(playerId); - return stateOpt && stateOpt->get().state == player::state::PlayerAlive; + return stateOpt && stateOpt->get().state == entity::state::EntityAlive; }); } @@ -104,7 +121,7 @@ void Lobby::restart() _levelDirector.restart(); for (const auto& playerId : _players | std::views::values) { _engine.getEntityFromGroup(playerId).value().get().state = - player::state::PlayerAlive; + entity::state::EntityAlive; } } @@ -130,7 +147,7 @@ void Lobby::join(const packet::server::SessionPtr& session) {true, 150, 75}, {}, {}, - {player::state::PlayerWaiting}, + {entity::state::PlayerWaiting}, session); packet::JoinAck j = {_players.at(session), _roomId, _engine.getGameState(), true}; send(session, j); diff --git a/Server/src/lobby/lobby.hpp b/Server/src/lobby/lobby.hpp index 19af1bb7..716eae94 100644 --- a/Server/src/lobby/lobby.hpp +++ b/Server/src/lobby/lobby.hpp @@ -168,6 +168,14 @@ class Lobby return id; } + /** + * @brief Remove an entity and broadcasts it's deletion. + * @param id The entity's ID. + * @param session The session triggering the deletion (nullptr if none). + */ + rtecs::types::EntityID killEntity(rtecs::types::EntityID id, + const packet::server::SessionPtr& session = nullptr); + /** * @brief Getter for the gameEngine. * @return A reference to the used gameEngine. diff --git a/Server/src/systems/apply_movement.cpp b/Server/src/systems/apply_movement.cpp index 9ff896b0..11766445 100644 --- a/Server/src/systems/apply_movement.cpp +++ b/Server/src/systems/apply_movement.cpp @@ -1,9 +1,9 @@ #include "apply_movement.hpp" -#include "components/collision.hpp" #include "components/factory.hpp" #include "components/position.hpp" #include "components/velocity.hpp" +#include "enums/player_state.hpp" #include "rtecs/ECS.hpp" using namespace server::systems; @@ -14,48 +14,104 @@ ApplyMovement::ApplyMovement() { } -void ApplyMovement::apply(rtecs::ECS &ecs) +void ApplyMovement::apply(rtecs::ECS& ecs) { using namespace components; - auto movable = ecs.group(); - auto colliders = ecs.group(); + auto movable = ecs.group(); + auto colliders = ecs.group(); - movable.apply( - [&](const rtecs::types::EntityID id, Velocity &vel, Position &pos, const Hitbox &box) { - std::optional> collider = std::nullopt; + movable.apply([&](const rtecs::types::EntityID id, + const Type& type, + Velocity& vel, + Position& pos, + const Hitbox& box, + State& state) { + pos.isUpdated = vel.vx != 0 || vel.vy != 0; + if (type.type == entity::Type::kPlayer) { + const Position nextHorizontalPos = {pos.x + vel.vx, pos.y}; + const std::optional horizontalCollider = + findCollider(id, nextHorizontalPos, box, colliders, entity::Type::kPlayer); + handlePlayerHorizontalMovement(pos, vel, box, horizontalCollider); - pos.isUpdated = true; - colliders.apply([&](const rtecs::types::EntityID otherId, - const Position &otherPos, - const Hitbox &otherBox) { - if (id == otherId || collider.has_value()) { - return; - } + const Position nextVerticalPos = {pos.x, pos.y + vel.vy}; + const std::optional verticalCollider = + findCollider(id, nextVerticalPos, box, colliders, entity::Type::kPlayer); + handlePlayerVerticalMovement(pos, vel, box, verticalCollider); + } else { + const Position nextPos = {pos.x + vel.vx, pos.y + vel.vy}; + const entity::Type expectedType = + type.type == entity::Type::kBullet ? entity::Type::kEnemy : entity::Type::kPlayer; + const std::optional collider = + findCollider(id, nextPos, box, colliders, expectedType); + handleEntityMovement(pos, vel, box, state, collider); + } + }); +} + +std::optional ApplyMovement::findCollider( + const rtecs::types::EntityID id, + const Position& pos, + const Hitbox& box, + rtecs::sparse::SparseGroup& colliders, + const entity::Type expectedColliderType) +{ + bool isCollisionDetected = false; + std::optional collider = std::nullopt; - const Position newRefPos = {pos.x + vel.vx, pos.y + vel.vy}; - bool isColliding = collide(newRefPos.x, box.width, otherPos.x, otherBox.width); - isColliding &= collide(newRefPos.y, box.height, otherPos.y, otherBox.height); + colliders.apply([&](const rtecs::types::EntityID colliderId, + Position& colliderPos, + Hitbox& colliderBox, + State& colliderState, + Type& colliderType) { + if (colliderType.type != expectedColliderType || colliderId == id || isCollisionDetected) { + return; + } + if (collide(pos, box, colliderPos, colliderBox)) { + isCollisionDetected = true; + collider = std::tuple( + colliderPos, colliderBox, colliderState, colliderType); + } + }); + return collider; +} - if (isColliding) { - collider = {otherPos, otherBox}; - } - }); - applyHorizontalMovement(pos, vel, box, collider); - applyVerticalMovement(pos, vel, box, collider); - }); +void ApplyMovement::handleEntityMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + State& state, + const std::optional& collider) +{ + if (collider.has_value()) { + // Stop the entity movement, probably useless, but we never know + vel.vx = 0; + vel.vy = 0; + // Kill the movable entity and the collided entity + std::get(collider.value()).state = entity::state::EntityDead; + state.state = entity::state::EntityDead; + } else { + pos.x += vel.vx; + pos.y += vel.vy; + } + if (pos.x - box.width < 0 || pos.x > 1920 || pos.y - box.height < 0 || pos.y > 1080) { + state.state = entity::state::EntityDead; + } } -void ApplyMovement::applyHorizontalMovement(Position &pos, - Velocity &vel, - const Hitbox &box, - const std::optional> &collider) +void ApplyMovement::handlePlayerHorizontalMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + const std::optional& collider) { - if (vel.vx != 0 && collider.has_value()) { - if (vel.vx > 0) { // Going right - pos.x = collider.value().first.x - box.width; - } else { // Going left - pos.x = collider.value().first.x + collider.value().second.width; + if (collider.has_value()) { + const Position& colliderPos = std::get(collider.value()); + const Hitbox& colliderBox = std::get(collider.value()); + if (vel.vx > 0) { // Right + pos.x = colliderPos.x - box.width; + } else if (vel.vx < 0) { // Left + pos.x = colliderPos.x + colliderBox.width; } } else { pos.x += vel.vx; @@ -63,17 +119,18 @@ void ApplyMovement::applyHorizontalMovement(Position &pos, vel.vx = 0; } -void ApplyMovement::applyVerticalMovement(Position &pos, - Velocity &vel, - const Hitbox &box, - const std::optional> &collider) +void ApplyMovement::handlePlayerVerticalMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + const std::optional& collider) { - if (vel.vy != 0 && collider.has_value()) { - if (vel.vy > 0) { // Going down - pos.y = collider.value().first.y - box.height; - } else { // Going up - pos.y = collider.value().first.y + collider.value().second.height; + if (collider.has_value()) { + const Position& colliderPos = std::get(collider.value()); + const Hitbox& colliderBox = std::get(collider.value()); + if (vel.vy > 0) { // Down + pos.y = colliderPos.y - box.height; + } else if (vel.vy < 0) { // Up + pos.y = colliderPos.y + colliderBox.height; } } else { pos.y += vel.vy; @@ -81,11 +138,11 @@ void ApplyMovement::applyVerticalMovement(Position &pos, vel.vy = 0; } -bool ApplyMovement::collide(const float refPos, - const float refSize, - const float otherPos, - const float otherSize) +bool ApplyMovement::collide(const Position& refPos, + const Hitbox& refBox, + const Position& otherPos, + const Hitbox& otherBox) { - return (refPos >= otherPos && refPos < (otherPos + otherSize)) || - (refPos + refSize > otherPos && refPos + refSize <= otherPos + otherSize); + return (refPos.x < otherPos.x + otherBox.width && refPos.x + refBox.width > otherPos.x && + refPos.y < otherPos.y + otherBox.height && refPos.y + refBox.height > otherPos.y); } diff --git a/Server/src/systems/apply_movement.hpp b/Server/src/systems/apply_movement.hpp index ab643ba2..fbbf9f24 100644 --- a/Server/src/systems/apply_movement.hpp +++ b/Server/src/systems/apply_movement.hpp @@ -4,39 +4,94 @@ #include "components/hitbox.hpp" #include "components/position.hpp" +#include "components/state.hpp" +#include "components/type.hpp" #include "components/velocity.hpp" +#include "rtecs/sparse/group/SparseGroup.hpp" #include "rtecs/systems/ASystem.hpp" +#include "rtecs/types/types.hpp" + +using namespace components; namespace server::systems { class ApplyMovement final : public rtecs::systems::ASystem { private: + using Collider = std::tuple; /** * @brief Check if a horizontal/vertical collision is detected. * - * @param refPos The reference X/Y position - * @param refSize The reference box's width/height - * @param otherPos The other X/Y position - * @param otherSize The other box's width/height - * @return `true` if a horizontal/vertical collision is detected, `false` otherwise. + * @param refPos The reference position + * @param refBox The reference hitbox + * @param otherPos The other position + * @param otherBox The other hitbox + * @return `true` if a collision is detected, `false` otherwise. + */ + static bool collide(const Position& refPos, + const Hitbox& refBox, + const Position& otherPos, + const Hitbox& otherBox); + + /** + * @bief Find a collider + * + * @param id The id of the moving entity + * @param pos The position of the moving entity + * @param box The box of the moving entity + * @param colliders The list of colliders + * @param expectedColliderType The excpected collider type + * @return An optional collider if any. + */ + static std::optional findCollider(rtecs::types::EntityID id, + const Position& pos, + const Hitbox& box, + rtecs::sparse::SparseGroup& colliders, + entity::Type expectedColliderType); + + /** + * @brief Handle the movement of the bullets and enemies + * + * @param pos The position of the moving entity + * @param vel The velocity of the moving entity + * @param box The hitbox of the moving entity + * @param state The state of the moving entity + * @param collider The optional collider if any found + */ + static void handleEntityMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + State& state, + const std::optional& collider); + + /** + * @brief Handle the horizontal movement of the player + * + * @param pos The position of the player + * @param vel The velocity of the player + * @param box The hitbox of the player + * @param collider The optional collider (which is a player) if any found + */ + static void handlePlayerHorizontalMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + const std::optional& collider); + + /** + * @brief Handle the vertical movement of the player + * + * @param pos The position of the player + * @param vel The velocity of the player + * @param box The hitbox of the player + * @param collider The optional collider (which is a player) if any found */ - static bool collide(float refPos, - float refSize, - float otherPos, - float otherSize); - - static void applyHorizontalMovement( - components::Position& pos, - components::Velocity& vel, - const components::Hitbox& box, - const std::optional>& collider); - static void applyVerticalMovement(components::Position& pos, - components::Velocity& vel, - const components::Hitbox& box, - const std::optional>& collider); + static void handlePlayerVerticalMovement(Position& pos, + Velocity& vel, + const Hitbox& box, + const std::optional& collider); public: explicit ApplyMovement(); diff --git a/Server/src/systems/broadcast_dead_entities.cpp b/Server/src/systems/broadcast_dead_entities.cpp new file mode 100644 index 00000000..25dc5582 --- /dev/null +++ b/Server/src/systems/broadcast_dead_entities.cpp @@ -0,0 +1,25 @@ +#include "broadcast_dead_entities.hpp" + +#include "enums/player_state.hpp" + +using namespace server::systems; +using namespace components; + +BroadcastDeadEntities::BroadcastDeadEntities(Lobby& lobby) + : ASystem("BroadcastDeadEntities"), + _lobby(lobby) +{ +} + +void BroadcastDeadEntities::apply(rtecs::ECS& ecs) +{ + auto group = ecs.group(); + + group.apply([&](const rtecs::types::EntityID id, const State& state) { + if (state.state == entity::state::EntityDead) { + LOG_INFO("Killing entity {}.", id); + _lobby.killEntity(id); + } + }); +} + diff --git a/Server/src/systems/broadcast_dead_entities.hpp b/Server/src/systems/broadcast_dead_entities.hpp new file mode 100644 index 00000000..47e7c53e --- /dev/null +++ b/Server/src/systems/broadcast_dead_entities.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "lobby/lobby.hpp" +#include "rtecs/ECS.hpp" +#include "rtecs/systems/ASystem.hpp" + +namespace server::systems { + +class BroadcastDeadEntities final : public rtecs::systems::ASystem +{ +private: + Lobby &_lobby; + +public: + explicit BroadcastDeadEntities(Lobby &lobby); + void apply(rtecs::ECS &ecs) override; +}; + +} // namespace server::systems diff --git a/common/enums/player_state.hpp b/common/enums/player_state.hpp index 407d731c..a12a7352 100644 --- a/common/enums/player_state.hpp +++ b/common/enums/player_state.hpp @@ -2,11 +2,11 @@ #include -namespace player::state { +namespace entity::state { static constexpr uint8_t PlayerWaiting = 1; ///< The player waiting for players to join static constexpr uint8_t PlayerReady = 2; ///< The player is ready to start the game -static constexpr uint8_t PlayerAlive = 2; ///< The player is alive -static constexpr uint8_t PlayerDead = 2; ///< The player is dead +static constexpr uint8_t EntityAlive = 3; ///< The entity is alive +static constexpr uint8_t EntityDead = 4; ///< The entity is dead -} // namespace player::state +} // namespace entity::state diff --git a/lib/rtecs/include/rtecs/ECS.hpp b/lib/rtecs/include/rtecs/ECS.hpp index 52743a24..b472887d 100644 --- a/lib/rtecs/include/rtecs/ECS.hpp +++ b/lib/rtecs/include/rtecs/ECS.hpp @@ -272,6 +272,24 @@ class ECS final (insertComponentInstance(entity, instances), ...); } + /** + * @brief Get the component's instance of an entity. + * + * @tparam T The component type + * @param entityId The entity's ID + * @return An optional reference of the component instance. + */ + template + types::OptionalRef getEntityComponent(const types::EntityID entityId) + { + types::OptionalRef> optSet = getComponent(); + + if (!optSet) { + return std::nullopt; + } + return optSet.value().get().get(entityId); + } + /** * @brief Update multiple components instances of an entity. *