diff --git a/CMakeLists.txt b/CMakeLists.txt index af072a7..9ddcbec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,3 +38,7 @@ add_subdirectory ("lesson_22") add_subdirectory ("lesson_23") add_subdirectory ("lesson_26") add_subdirectory ("lesson_27") + +add_subdirectory ("battle_tanks/common_logic") +add_subdirectory ("battle_tanks/client") +add_subdirectory ("battle_tanks/server") diff --git a/CMakePresets.json b/CMakePresets.json index be4ee24..47e75f1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -85,7 +85,10 @@ "binaryDir": "${sourceDir}/out/build/${presetName}", "installDir": "${sourceDir}/out/install/${presetName}", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_TOOLCHAIN_FILE": "vcpkg_r_d_cpp_gamedev/scripts/buildsystems/vcpkg.cmake" }, "condition": { "type": "equals", diff --git a/battle_tanks/client/CMakeLists.txt b/battle_tanks/client/CMakeLists.txt new file mode 100644 index 0000000..2e19140 --- /dev/null +++ b/battle_tanks/client/CMakeLists.txt @@ -0,0 +1,28 @@ +project(battle_tanks_client) + +file(GLOB_RECURSE SOURCES "./*.cpp") + +include_directories(${PROJECT_SOURCE_DIR}/headers) + +find_package(SFML COMPONENTS system window graphics CONFIG REQUIRED) + +add_executable (${PROJECT_NAME} ${SOURCES}) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/game_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +if (CMAKE_VERSION VERSION_GREATER 3.12) + set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 20) +endif() + +target_include_directories(${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../external/headers + ${CMAKE_CURRENT_SOURCE_DIR}/../common_logic/headers +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + bt-common + sfml-graphics + sfml-window +) \ No newline at end of file diff --git a/battle_tanks/client/battle_tanks.cpp b/battle_tanks/client/battle_tanks.cpp new file mode 100644 index 0000000..701595a --- /dev/null +++ b/battle_tanks/client/battle_tanks.cpp @@ -0,0 +1,49 @@ +#include +#include +#include + +#include "SFML/System/Clock.hpp" +#include "SFML/Graphics.hpp" + +#include "game/game_objects/player_game_object.hpp" +#include "game/sfml_game.hpp" +#include "network/player_actions.hpp" + + +int main() +{ + auto window = std::make_shared( + sf::VideoMode( + bt::game_entity_consts::game_field_size.x, + bt::game_entity_consts::game_field_size.y + ), + "Battle Tanks" + ); + //window->setFramerateLimit(60); + + sf::Clock clock; + + const auto game = std::make_unique( + window, + bt::physics_consts::pixels_per_meters + ); + + while (window->isOpen()) + { + const float delta_time = clock.restart().asSeconds(); + sf::Event event; + while (window->pollEvent(event)) + { + if (event.type == sf::Event::Closed || + (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Key::Escape)) + { + window->close(); + } + game->handle_event(event); + } + + game->update(delta_time); + } + + return 0; +} diff --git a/battle_tanks/client/game_data/atlases/bullet_1.png b/battle_tanks/client/game_data/atlases/bullet_1.png new file mode 100644 index 0000000..ac6687f Binary files /dev/null and b/battle_tanks/client/game_data/atlases/bullet_1.png differ diff --git a/battle_tanks/client/game_data/atlases/map_forest_tileset.json b/battle_tanks/client/game_data/atlases/map_forest_tileset.json new file mode 100644 index 0000000..9ada74d --- /dev/null +++ b/battle_tanks/client/game_data/atlases/map_forest_tileset.json @@ -0,0 +1,196 @@ +{ + "id": 1, + "name": "forest tileset", + "atlas_path": "game_data/atlases/map_forest_tileset.png", + "textures": [ + { + "id": 4, + "x": 1, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 5, + "x": 18, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 6, + "x": 35, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 7, + "x": 1, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 8, + "x": 18, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 9, + "x": 35, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 10, + "x": 1, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 11, + "x": 18, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 12, + "x": 35, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 13, + "x": 52, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 14, + "x": 69, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 15, + "x": 86, + "y": 1, + "w": 16, + "h": 16 + }, + { + "id": 16, + "x": 52, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 17, + "x": 69, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 18, + "x": 86, + "y": 18, + "w": 16, + "h": 16 + }, + { + "id": 19, + "x": 52, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 20, + "x": 69, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 21, + "x": 86, + "y": 35, + "w": 16, + "h": 16 + }, + { + "id": 22, + "x": 1, + "y": 52, + "w": 16, + "h": 16 + }, + { + "id": 23, + "x": 18, + "y": 52, + "w": 16, + "h": 16 + }, + { + "id": 24, + "x": 35, + "y": 52, + "w": 16, + "h": 16 + }, + { + "id": 25, + "x": 1, + "y": 69, + "w": 16, + "h": 16 + }, + { + "id": 26, + "x": 18, + "y": 69, + "w": 16, + "h": 16 + }, + { + "id": 27, + "x": 35, + "y": 69, + "w": 16, + "h": 16 + }, + { + "id": 28, + "x": 1, + "y": 86, + "w": 16, + "h": 16 + }, + { + "id": 29, + "x": 18, + "y": 86, + "w": 16, + "h": 16 + }, + { + "id": 30, + "x": 35, + "y": 86, + "w": 16, + "h": 16 + } + ] +} \ No newline at end of file diff --git a/battle_tanks/client/game_data/atlases/map_forest_tileset.png b/battle_tanks/client/game_data/atlases/map_forest_tileset.png new file mode 100644 index 0000000..5618770 Binary files /dev/null and b/battle_tanks/client/game_data/atlases/map_forest_tileset.png differ diff --git a/battle_tanks/client/game_data/atlases/rock_1.png b/battle_tanks/client/game_data/atlases/rock_1.png new file mode 100644 index 0000000..0880f6a Binary files /dev/null and b/battle_tanks/client/game_data/atlases/rock_1.png differ diff --git a/battle_tanks/client/game_data/atlases/tank.png b/battle_tanks/client/game_data/atlases/tank.png new file mode 100644 index 0000000..6f6b814 Binary files /dev/null and b/battle_tanks/client/game_data/atlases/tank.png differ diff --git a/battle_tanks/client/game_data/atlases/tower.png b/battle_tanks/client/game_data/atlases/tower.png new file mode 100644 index 0000000..f02bacb Binary files /dev/null and b/battle_tanks/client/game_data/atlases/tower.png differ diff --git a/battle_tanks/client/game_data/fonts/wheaton capitals.otf b/battle_tanks/client/game_data/fonts/wheaton capitals.otf new file mode 100644 index 0000000..b32c8bc Binary files /dev/null and b/battle_tanks/client/game_data/fonts/wheaton capitals.otf differ diff --git a/battle_tanks/client/game_data/screens/game_screen_config.json b/battle_tanks/client/game_data/screens/game_screen_config.json new file mode 100644 index 0000000..9452372 --- /dev/null +++ b/battle_tanks/client/game_data/screens/game_screen_config.json @@ -0,0 +1,6 @@ +{ + "hp_text_color_thresholds": { + "wounded": 50, + "dying": 25 + } +} \ No newline at end of file diff --git a/battle_tanks/client/headers/dev/debug_draw.hpp b/battle_tanks/client/headers/dev/debug_draw.hpp new file mode 100644 index 0000000..55db2db --- /dev/null +++ b/battle_tanks/client/headers/dev/debug_draw.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include + +class SFMLDebugDraw : public b2Draw +{ +private: + std::shared_ptr m_window; + //float m_pixels_per_meter; + static float pixels_per_meter; + +public: + SFMLDebugDraw(const std::shared_ptr& window, float pixels_per_meters); + + /// Convert Box2D's OpenGL style color definition[0-1] to SFML's color definition[0-255], with optional alpha byte[Default - opaque] + static sf::Color GLColorToSFML(const b2Color& color, sf::Uint8 alpha = 255) + { + return sf::Color(static_cast(color.r * 255), static_cast(color.g * 255), static_cast(color.b * 255), alpha); + } + + /// Convert Box2D's vector to SFML vector [Default - scales the vector up by SCALE constants amount] + static sf::Vector2f B2VecToSFVec(const b2Vec2& vector, bool scaleToPixels = true) + { + return sf::Vector2f(vector.x * (scaleToPixels ? pixels_per_meter : 1.f), vector.y * (scaleToPixels ? pixels_per_meter : 1.f)); + } + + /// Draw a closed polygon provided in CCW order. + void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color); + + /// Draw a solid closed polygon provided in CCW order. + void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color); + + /// Draw a circle. + void DrawCircle(const b2Vec2& center, float radius, const b2Color& color); + + /// Draw a solid circle. + void DrawSolidCircle(const b2Vec2& center, float radius, const b2Vec2& axis, const b2Color& color); + + /// Draw a line segment. + void DrawSegment(const b2Vec2& p1, const b2Vec2& p2, const b2Color& color); + + /// Draw a transform. Choose your own length scale. + void DrawTransform(const b2Transform& xf); + + void DrawPoint(const b2Vec2& p, float size, const b2Color& color) override; +}; diff --git a/battle_tanks/client/headers/game/game_objects/bullet_game_object.hpp b/battle_tanks/client/headers/game/game_objects/bullet_game_object.hpp new file mode 100644 index 0000000..f674603 --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/bullet_game_object.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "game/game_objects/game_object.hpp" +#include "physics/physics_body_factory.hpp" +#include "renderer/textures.hpp" +#include "utils/uuid.hpp" + + +namespace bt +{ + //TODO:: create object poll for such objects + class bullet_game_object : public bt::game_object + { + public: + bullet_game_object( + const bt::uuid id, + const std::weak_ptr& ph_body_factory, + const std::shared_ptr& render_data + ); + + virtual ~bullet_game_object() override = default; + + public: + virtual void restore_from_frame(const bt::game_object_frame_restorer& restorer) override; + virtual void update(float delta_time) override; + + protected: + virtual void create_render_object() override; + virtual void create_game_object_entity() override; + + private: + + std::shared_ptr render_data_{ nullptr }; + const std::weak_ptr ph_body_factory_; + }; +} diff --git a/battle_tanks/client/headers/game/game_objects/forest_map_game_object.hpp b/battle_tanks/client/headers/game/game_objects/forest_map_game_object.hpp new file mode 100644 index 0000000..1620b1c --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/forest_map_game_object.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "game/entity/game_map_entity.hpp" +#include "renderer/textures.hpp" + + +namespace bt +{ + class forest_map_game_object : public bt::game_object + { + public: + explicit forest_map_game_object( + const bt::uuid id, + const std::shared_ptr& map_textures_pack, + const sf::Vector2u& game_field_size, + const std::weak_ptr& ph_body_factory + ) + : game_object(id), map_textures_pack_(map_textures_pack), field_size_{ game_field_size }, + ph_body_factory_(ph_body_factory) + { + } + + virtual ~forest_map_game_object() override = default; + public: + void restore_from_frame(const bt::game_object_frame_restorer& restorer) override + { + } + + void update(float delta_time) override + { + } + public: + void create_rock(const sf::Vector2f position, const std::shared_ptr& texture_data) const + { + const auto main_cont = dynamic_cast(render_object_.get()); + const auto rock_sprite = std::make_shared(texture_data); + rock_sprite->setPosition(position); + const auto size = texture_data->get_size(); + rock_sprite->setOrigin(static_cast(size.x) / 2.0f, static_cast(size.y) / 2.0f); + main_cont->add_child(rock_sprite); + + const auto entity = dynamic_cast(game_object_entity_.get()); + entity->create_rock( + { + position.x / physics_consts::pixels_per_meters, + position.y / physics_consts::pixels_per_meters + }, + { + static_cast(size.x) / physics_consts::pixels_per_meters, + static_cast(size.y) / physics_consts::pixels_per_meters, + } + ); + } + + protected: + void create_render_object() override + { + auto main_cont = std::make_shared(); + + auto bg_texture = map_textures_pack_->get_texture(texture_id::grass_dark_m); + bg_texture->get_texture()->setRepeated(true); + + //TODO:: build tiles map from config file + //auto tile_size = sf::Vector2i{ bg_texture->get_texture_rect().width, bg_texture->get_texture_rect().height }; + //const int tiles_count_x = field_size_.x / bg_texture->get_texture_rect().width + 1; + //const int tiles_count_y = field_size_.y / bg_texture->get_texture_rect().height + 1; + + //for (int i = 0; i < tiles_count_x; ++i) + //{ + // for (int j = 0; j < tiles_count_y; ++j) + // { + // auto bg_tile = std::make_shared(bg_texture); + // bg_tile->setPosition + // (sf::Vector2f{ static_cast(i * bg_texture->get_texture_rect().width), + // static_cast(j * bg_texture->get_texture_rect().height) } + // ); + // main_cont->add_child(bg_tile); + // } + //} + + render_object_ = main_cont; + } + + void create_game_object_entity() override + { + auto map_entity = std::make_unique( + 0, + b2Vec2{ + static_cast(field_size_.x) / physics_consts::pixels_per_meters, + static_cast(field_size_.y) / physics_consts::pixels_per_meters + }, + ph_body_factory_ + ); + map_entity->build_map(); + game_object_entity_ = std::move(map_entity); + } + + private: + std::shared_ptr map_textures_pack_; + sf::Vector2u field_size_; + std::weak_ptr ph_body_factory_; + }; +} diff --git a/battle_tanks/client/headers/game/game_objects/game_object.hpp b/battle_tanks/client/headers/game/game_objects/game_object.hpp new file mode 100644 index 0000000..8359ba1 --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/game_object.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "game/entity/game_object_entity.hpp" +#include "game/game_objects/game_object_frame_restorer.hpp" +#include "utils/uuid.hpp" +#include "renderer/render_object.hpp" + + +namespace bt +{ + class game_object + { + public: + explicit game_object(const bt::uuid id) : id_{ id } + { + } + + virtual ~game_object() = default; + + public: + virtual void restore_from_frame(const bt::game_object_frame_restorer& restorer) = 0; + virtual void update(float delta_time) = 0; + + public: + bt::uuid get_id() const + { + return id_; + } + + const std::shared_ptr& get_render_object() const + { + return render_object_; + } + + void initialize() + { + create_game_object_entity(); + create_render_object(); + } + + protected: + virtual void create_render_object() = 0; + virtual void create_game_object_entity() = 0; + + protected: + bt::uuid id_; + std::shared_ptr render_object_{ nullptr }; + std::unique_ptr game_object_entity_{ nullptr }; + }; + +} diff --git a/battle_tanks/client/headers/game/game_objects/game_objects.hpp b/battle_tanks/client/headers/game/game_objects/game_objects.hpp new file mode 100644 index 0000000..bab1e5d --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/game_objects.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "game/entity/game_object_type.hpp" +#include "game/game_objects/game_object.hpp" +#include "game/game_objects/bullet.hpp" +#include "game/game_objects/game_scene.hpp" \ No newline at end of file diff --git a/battle_tanks/client/headers/game/game_objects/game_scene.hpp b/battle_tanks/client/headers/game/game_objects/game_scene.hpp new file mode 100644 index 0000000..8277424 --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/game_scene.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + + +namespace bt +{ + class game_scene + { + public: + void update(const float delta_time) const + { + for (const auto& game_object : game_objects_ | std::views::values) + { + game_object->update(delta_time); + } + } + + void add_game_object(const std::shared_ptr& game_object) + { + game_objects_.emplace(game_object->get_id(), game_object); + } + + void remove_game_object(const std::shared_ptr& game_object) + { + game_objects_.erase(game_object->get_id()); + } + + std::shared_ptr get_game_object(const bt::uuid id) const + { + return game_objects_.at(id); + } + + private: + std::unordered_map> game_objects_{}; + }; +} diff --git a/battle_tanks/client/headers/game/game_objects/player_game_object.hpp b/battle_tanks/client/headers/game/game_objects/player_game_object.hpp new file mode 100644 index 0000000..9a42ca2 --- /dev/null +++ b/battle_tanks/client/headers/game/game_objects/player_game_object.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include + +#include "game/entity/player_game_object_entity.hpp" +#include "game/game_objects/game_object.hpp" +#include "renderer/container.hpp" +#include "renderer/sprite.hpp" +#include "renderer/textures.hpp" +#include "utils/uuid.hpp" + + +namespace bt +{ + class player_game_object : public bt::game_object + { + public: + player_game_object( + const bt::uuid id, + const std::shared_ptr& textures_pack, + const std::weak_ptr& ph_body_factory) + : game_object(id), textures_pack_{ textures_pack }, ph_body_factory_{ ph_body_factory } + { + } + + virtual ~player_game_object() override = default; + + public: + virtual void restore_from_frame(const bt::game_object_frame_restorer& restorer) override + { + game_object_entity_->restore_frame(restorer); + } + + virtual void update(float delta_time) override + { + const auto frame = std::make_shared(); + game_object_entity_->write_to_frame(frame); + const auto& ph_body_pos = frame->position; + const float ph_body_rot = frame->rotation; + + render_object_->setPosition({ph_body_pos.x * physics_consts::pixels_per_meters, ph_body_pos.y * physics_consts::pixels_per_meters}); + render_object_->setRotation(std::fmod(bt::rad_to_deg(ph_body_rot), 360.0f)); + + if (!turret_render_object_.expired()) + { + turret_render_object_.lock()->setRotation(frame->turret_rotation); + } + } + + public: + sf::Int32 get_health() const + { + return dynamic_cast(game_object_entity_.get())->get_health(); + } + + protected: + virtual void create_render_object() override + { + auto tank_texture = textures_pack_->get_texture(texture_id::tank_base); + auto& texture_size = tank_texture->get_size(); + const auto tank_container = std::make_shared(); + //tank_container->setPosition(100.0, 100.0); + tank_container->setOrigin(static_cast(texture_size.x) / 2.0f, static_cast(texture_size.y) / 2.0f); + + const auto tank_base_sprite = std::make_shared(tank_texture); + tank_container->add_child(tank_base_sprite); + + auto turret_texture = textures_pack_->get_texture(texture_id::tank_turret); + const auto turret_sprite = std::make_shared(turret_texture); + turret_sprite->setPosition(16.5f, 27.0f); + turret_sprite->setOrigin(10.0f, 67.0f); + tank_container->add_child(turret_sprite); + + turret_render_object_ = turret_sprite; + + render_object_ = tank_container; + } + + virtual void create_game_object_entity() override + { + auto player_entity = std::make_unique(id_, ph_body_factory_); + player_entity->create_phy_body(); + game_object_entity_ = std::move(player_entity); + } + + private: + float velocity_{ 0.0f }; + sf::Vector2f position_{}; + sf::Vector2u field_size_{}; + + std::shared_ptr textures_pack_; + std::weak_ptr ph_body_factory_; + + std::weak_ptr turret_render_object_{}; + }; +} diff --git a/battle_tanks/client/headers/game/game_session.hpp b/battle_tanks/client/headers/game/game_session.hpp new file mode 100644 index 0000000..746ecd9 --- /dev/null +++ b/battle_tanks/client/headers/game/game_session.hpp @@ -0,0 +1,31 @@ +#pragma once + + +namespace bt +{ + class player_connecting_state; + + class game_session + { + friend class bt::player_connecting_state; + public: + game_session() = default; + + ~game_session() = default; + + public: + bt::uuid get_player_id() const + { + return player_id_; + } + + sf::Uint32 get_session_id() const + { + return session_id_; + } + + private: + bt::uuid player_id_{}; + sf::Uint32 session_id_{}; + }; +} diff --git a/battle_tanks/client/headers/game/game_states/game_connection_states.hpp b/battle_tanks/client/headers/game/game_states/game_connection_states.hpp new file mode 100644 index 0000000..4fc222c --- /dev/null +++ b/battle_tanks/client/headers/game/game_states/game_connection_states.hpp @@ -0,0 +1,551 @@ +#pragma once + +#include + +#include "game/game_session.hpp" +#include "game/game_objects/bullet_game_object.hpp" +#include "network/connection_service.hpp" + + +namespace bt +{ + enum class player_connection_state : unsigned int + { + unknown = 0, + disconnected, + connecting, + main_lobby, + session_lobby, + game_session, + }; + + class connection_state + { + public: + explicit connection_state(const player_connection_state connection_state) + : connection_state_(connection_state) + { + } + + virtual ~connection_state() = default; + + public: + virtual void start() = 0; + virtual player_connection_state handle_event(const sf::Event& event) = 0; + virtual player_connection_state process_packet( + command_id_server command_id, + sf::Uint32 connection_id, + sf::Uint32 session_id, + const std::shared_ptr& packet) = 0; + + public: + player_connection_state get_connection_state() const { return connection_state_; } + + private: + player_connection_state connection_state_; + }; + + + //TODO:: handle lost connection and reconnect to session if exists + class player_disconnected_state final : public connection_state + { + public: + player_disconnected_state() + : connection_state(player_connection_state::disconnected) + { + } + + virtual ~player_disconnected_state() override = default; + + public: + virtual void start() override + { + } + + virtual player_connection_state handle_event(const sf::Event& event) override + { + return get_connection_state(); + } + + virtual player_connection_state process_packet( + const command_id_server command_id, + const sf::Uint32 connection_id, + const sf::Uint32 session_id, + const std::shared_ptr& packet) override + { + return get_connection_state(); + } + }; + + class player_connecting_state final : public connection_state + { + public: + explicit player_connecting_state(const std::shared_ptr& game_session) + : connection_state(player_connection_state::connecting), game_session_{ game_session } + { + } + + virtual ~player_connecting_state() override = default; + public: + virtual void start() override + { + std::cout << "connecting..." << std::endl; + } + + virtual player_connection_state handle_event(const sf::Event& event) override + { + return get_connection_state(); + } + + virtual player_connection_state process_packet( + const command_id_server command_id, + const sf::Uint32 connection_id, + const sf::Uint32 session_id, + const std::shared_ptr& packet) override + { + if (command_id == command_id_server::connection_established) + { + + bt::uuid player_id; + *packet >> player_id; + game_session_->player_id_ = player_id; + std::cout << "connected" << std::endl; + return player_connection_state::main_lobby; + } + + return get_connection_state(); + } + + private: + std::shared_ptr game_session_{ nullptr }; + }; + + class main_lobby_state final : public connection_state + { + public: + explicit main_lobby_state(std::shared_ptr connection_service) + : connection_state(player_connection_state::main_lobby), connection_service_(std::move(connection_service)) + { + } + + virtual ~main_lobby_state() override = default; + + public: + virtual void start() override + { + } + + virtual player_connection_state handle_event(const sf::Event& event) override + { + //TODO:: don't allow to repeat it until server response + if (event.type == sf::Event::KeyReleased) + { + if (event.key.code == sf::Keyboard::C) + { + std::cout << "creating session... " << std::endl; + connection_service_->send(client_command{ command_id_client::create_session }); + } + else if (event.key.code == sf::Keyboard::J) + { + std::cout << "joining to session... id: " << 2 << std::endl; + connection_service_->send(join_session_command{ 2 }); + + return bt::player_connection_state::session_lobby; + } + } + + return get_connection_state(); + } + + virtual player_connection_state process_packet( + const command_id_server command_id, + const sf::Uint32 connection_id, + const sf::Uint32 session_id, + const std::shared_ptr& packet) override + { + if (command_id == command_id_server::session_created) + { + sf::Uint32 current_session; + *packet >> current_session; + std::cout << "session created, id: " << current_session << std::endl; + + //TODO:: parse session_id and connection_id in connection layer + connection_service_->get_connection().set_session_id(current_session); + std::cout << "joining to session... id: " << current_session << std::endl; + connection_service_->send(join_session_command{ current_session }); + + return bt::player_connection_state::session_lobby; + } + + return get_connection_state(); + } + + private: + std::shared_ptr connection_service_{ nullptr }; + }; + + class session_lobby_state final : public connection_state + { + public: + session_lobby_state( + std::shared_ptr texture_warehouse, + std::weak_ptr physics_body_factory, + std::function& player)> player_created_callback + ) + : connection_state(player_connection_state::session_lobby), texture_warehouse_(std::move(texture_warehouse)), + physics_body_factory_(std::move(physics_body_factory)), + player_created_callback_(std::move(player_created_callback)) + { + } + + virtual ~session_lobby_state() override = default; + + public: + virtual void start() override + { + } + + virtual player_connection_state handle_event(const sf::Event& event) override + { + return get_connection_state(); + } + + virtual player_connection_state process_packet( + const command_id_server command_id, + const sf::Uint32 connection_id, + const sf::Uint32 session_id, + const std::shared_ptr& packet) override + { + if (command_id == command_id_server::player_joined_to_session) + { + std::cout << "joined to session, id: " << session_id << std::endl; + //TODO:: add player to game session and display in session lobby connected players + } + else if (command_id == command_id_server::session_started) + { + sf::Uint32 started_session_id; + *packet >> started_session_id; + std::cout << "game session started, id: " << session_id << std::endl; + //TODO:: start game world rendering and physics simulation + sf::Uint32 game_objects_count; + *packet >> game_objects_count; + for (sf::Uint32 i = 0; i < game_objects_count; ++i) + { + bt::uuid game_object_id; + *packet >> game_object_id; + bt::game_object_frame_restorer_packet restorer_packet{ packet }; + auto player = std::make_shared( + game_object_id, + texture_warehouse_->load_pack(bt::textures_pack_id::tank), + physics_body_factory_ + ); + player->initialize(); + player->restore_from_frame(restorer_packet); + player_created_callback_(player); + } + + return bt::player_connection_state::game_session; + } + return get_connection_state(); + } + + private: + std::shared_ptr texture_warehouse_; + std::weak_ptr physics_body_factory_; + std::function& player)> player_created_callback_; + }; + + class game_session_state final : public connection_state + { + public: + game_session_state( + std::shared_ptr connection_service, + std::shared_ptr texture_warehouse, + std::weak_ptr physics_body_factory, + std::function& game_object)> game_object_created_callback, + std::function game_object_deleted_callback, + std::function restore_game_object_callback + ) + : connection_state(player_connection_state::game_session), + physics_body_factory_(std::move(physics_body_factory)), texture_warehouse_(std::move(texture_warehouse)), + game_object_created_callback_(std::move(game_object_created_callback)), + game_object_deleted_callback_(std::move(game_object_deleted_callback)), + restore_game_object_callback_(std::move(restore_game_object_callback)), + connection_service_(std::move(connection_service)) + { + } + virtual ~game_session_state() override = default; + + public: + virtual void start() override + { + bullet_creation_time_ = std::chrono::high_resolution_clock::now(); + } + + virtual player_connection_state handle_event(const sf::Event& event) override + { + //TODO:: start move object immediately after key pressed. don't wait server to move + //player_action tank_move_action{}; + //player_action tank_rotate_action{}; + //player_action tower_rotate_action{}; + //bool tank_fire{ false }; + // /////////////////////////// + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::W)) + { + connection_service_->send(player_action_command{ player_action::move_forward }); + } + else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::S)) + { + connection_service_->send(player_action_command{ player_action::move_backward }); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::A)) + { + connection_service_->send(player_action_command{ player_action::turn_left }); + } + else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::D)) + { + connection_service_->send(player_action_command{ player_action::turn_right }); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) + { + connection_service_->send(player_action_command{ player_action::turn_turret_left }); + } + else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) + { + connection_service_->send(player_action_command{ player_action::turn_turret_right }); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Space)) + { + const auto bullet_current_time = std::chrono::high_resolution_clock::now(); + if (std::chrono::duration_cast(bullet_current_time - bullet_creation_time_).count() >= 1000) + { + bullet_creation_time_ = bullet_current_time; + connection_service_->send(player_action_command{ player_action::turret_fire }); + } + } + + if (event.type == sf::Event::KeyReleased) + { + if (event.key.code == sf::Keyboard::W || event.key.code == sf::Keyboard::S) + { + connection_service_->send(player_action_command{ player_action::stop_move }); + } + + if (event.key.code == sf::Keyboard::A || event.key.code == sf::Keyboard::D) + { + connection_service_->send(player_action_command{ player_action::stop_turn }); + } + + if (event.key.code == sf::Keyboard::Left || event.key.code == sf::Keyboard::Right) + { + connection_service_->send(player_action_command{ player_action::stop_turn_turret }); + } + + if (event.key.code == sf::Keyboard::Space) + { + connection_service_->send(player_action_command{ player_action::turret_stop_fire }); + } + } + + return get_connection_state(); + } + + virtual player_connection_state process_packet( + const command_id_server command_id, + const sf::Uint32 connection_id, + const sf::Uint32 session_id, + const std::shared_ptr& packet) override + { + if (command_id == command_id_server::update_game_frame) + { + sf::Uint32 game_objects_count; + *packet >> game_objects_count; + for (sf::Uint32 i = 0; i < game_objects_count; ++i) + { + bt::uuid game_object_id; + *packet >> game_object_id; + + bt::game_object_frame_restorer_packet restorer_packet{ packet }; + restore_game_object_callback_(game_object_id, restorer_packet); + } + } + else if (command_id == command_id_server::player_shoot) + { + bt::uuid player_id; + *packet >> player_id; + bt::uuid bullet_id; + *packet >> bullet_id; + const bt::game_object_frame_restorer_packet restorer_packet{ packet }; + const auto bullet = std::make_shared( + bullet_id, + physics_body_factory_, + texture_warehouse_->load_texture(bt::texture_id::bullet) + ); + bullet->initialize(); + bullet->restore_from_frame(restorer_packet); + game_object_created_callback_(bullet); + + } + else if (command_id == command_id_server::delete_game_object) + { + bt::uuid game_object_id; + *packet >> game_object_id; + game_object_deleted_callback_(game_object_id); + } + + return get_connection_state(); + } + + private: + std::weak_ptr physics_body_factory_; + std::shared_ptr texture_warehouse_; + std::function& game_object)> game_object_created_callback_; + std::function game_object_deleted_callback_; + std::function restore_game_object_callback_; + std::shared_ptr connection_service_; + + std::chrono::time_point bullet_creation_time_{}; + }; + + class connection_state_manager + { + public: + explicit connection_state_manager( + std::shared_ptr game_session, + std::shared_ptr texture_warehouse, + std::weak_ptr physics_body_factory, + std::function& player)> player_created_callback, + std::function& game_object)> game_object_created_callback, + std::function game_object_deleted_callback, + std::function restore_game_object_callback + ) + //TODO:: add signals or events library to substitute callbacks + : game_session_(std::move(game_session)), texture_warehouse_(std::move(texture_warehouse)), + physics_body_factory_(std::move(physics_body_factory)), + player_created_callback_(std::move(player_created_callback)), + game_object_created_callback_(std::move(game_object_created_callback)), + game_object_deleted_callback_(std::move(game_object_deleted_callback)), + restore_game_object_callback_(std::move(restore_game_object_callback)) + { + } + + ~connection_state_manager() = default; + + public: + player_connection_state get_connection_state() const + { + if (current_state_ == nullptr) + { + return player_connection_state::unknown; + } + + return current_state_->get_connection_state(); + } + + void start() + { + if (current_state_ != nullptr) + { + return; + } + commands_in_ = std::make_shared>(); + connection_service_ = std::make_unique(commands_in_); + + current_state_ = std::make_unique(game_session_); + current_state_->start(); + connection_service_->get_connection().connect({ "localhost" }, 52000); + } + + void update(const float delta_time) + { + if (current_state_ == nullptr) + { + return; + } + + connection_service_->get_connection().update(); + + if (commands_in_->empty()) + { + return; + } + + auto [connection_id, session_id, packet_ref] = commands_in_->pop_front(); + sf::Uint32 command_id; + *packet_ref >> command_id; + + const auto server_command_id = static_cast(command_id); + + const auto state = current_state_->process_packet(server_command_id, connection_id, session_id, packet_ref); + change_state(state); + } + + void handle_event(const sf::Event& event) + { + const auto state = current_state_->handle_event(event); + change_state(state); + } + + private: + void change_state(const player_connection_state new_state) + { + if (new_state == current_state_->get_connection_state()) + { + return; + } + //TODO:: move to factory or DI + switch (new_state) + { + case player_connection_state::disconnected: + current_state_ = std::make_unique(); + break; + case player_connection_state::connecting: + current_state_ = std::make_unique(game_session_); + break; + case player_connection_state::main_lobby: + current_state_ = std::make_unique(connection_service_); + break; + case player_connection_state::session_lobby: + current_state_ = std::make_unique( + texture_warehouse_, + physics_body_factory_, + player_created_callback_ + ); + break; + case player_connection_state::game_session: + current_state_ = std::make_unique( + connection_service_, + texture_warehouse_, + physics_body_factory_, + game_object_created_callback_, + game_object_deleted_callback_, + restore_game_object_callback_ + ); + break; + case player_connection_state::unknown: + //TODO:: handle state switch error + break; + } + + current_state_->start(); + } + + private: + std::unique_ptr current_state_{ nullptr }; + + std::shared_ptr> commands_in_{ nullptr }; + std::shared_ptr connection_service_{ nullptr }; + + std::shared_ptr game_session_{ nullptr }; + std::shared_ptr texture_warehouse_; + std::weak_ptr physics_body_factory_; + std::function& player)> player_created_callback_; + std::function& game_object)> game_object_created_callback_; + std::function game_object_deleted_callback_; + std::function restore_game_object_callback_; + }; +} diff --git a/battle_tanks/client/headers/game/sfml_game.hpp b/battle_tanks/client/headers/game/sfml_game.hpp new file mode 100644 index 0000000..10eb193 --- /dev/null +++ b/battle_tanks/client/headers/game/sfml_game.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include + +#include "unordered_set" +#include "dev/debug_draw.hpp" +#include "game/game_objects/game_object_frame_restorer_packet.hpp" +#include "game_objects/forest_map_game_object.hpp" + +#include "physics/collision_listener.hpp" +#include "game_objects/game_scene.hpp" +#include "game_states/game_connection_states.hpp" +#include "physics/physics_body_factory.hpp" +#include "renderer/render_scene.hpp" +#include "ui/game_screen.hpp" + + +namespace bt +{ + class sfml_game + { + public: + sfml_game(const std::shared_ptr& render_target, const float pixels_per_meters) + : pixels_per_meters_{ pixels_per_meters }, debug_draw_{ render_target, pixels_per_meters_ }, window_{ render_target } + { + game_world_ = std::make_unique(); + render_scene_ = std::make_unique(render_target); + game_screen_ = std::make_unique(render_target); + physics_world_.SetAllowSleeping(true); + //physics_world.SetContinuousPhysics(true); + contact_listener_ = std::make_unique(); + physics_world_.SetContactListener(contact_listener_.get()); + debug_draw_.AppendFlags(b2Draw::e_shapeBit); + physics_world_.SetDebugDraw(&debug_draw_); + + contact_listener_->add_collision_handler([this]( + const bt::game_object_b2d_link game_object_a, + const bt::game_object_b2d_link game_object_b + ) + { + if (game_object_a.type == bt::game_object_type::bullet) + { + //objects_to_delete_.insert(game_object_a.id); + } + else if (game_object_b.type == bt::game_object_type::bullet) + { + //objects_to_delete_.insert(game_object_b.id); + } + }); + + physics_body_factory_ = std::make_shared(&physics_world_); + + texture_warehouse_ = std::make_shared(); + const auto map_atlas = bt::atlas_data{ "game_data/atlases/map_forest_tileset.json" }; + + texture_warehouse_->pre_load_atlas(map_atlas); + + const auto forest_game_map = std::make_shared( + 0, + texture_warehouse_->load_pack(bt::textures_pack_id::map_forest), + bt::game_entity_consts::game_field_size, + physics_body_factory_ + ); + forest_game_map->initialize(); + add_game_object(forest_game_map); + + const auto rock_texture_render_data = std::make_shared("game_data/atlases/rock_1.png"); + forest_game_map->create_rock({ 200, 200 }, rock_texture_render_data); + + game_session_ = std::make_shared(); + + connection_state_manager_ = std::make_unique( + game_session_, + texture_warehouse_, + physics_body_factory_, + [this](const std::shared_ptr& player_game_object) + { + add_game_object(player_game_object); + players_.emplace(player_game_object->get_id(), player_game_object); + }, + [this](const std::shared_ptr& game_object) + { + add_game_object(game_object); + }, + [this](const bt::uuid& game_object_id) + { + delete_game_object(game_object_id); + if (players_.contains(game_object_id)) + { + players_.erase(game_object_id); + } + }, + [this](const bt::uuid game_object_id, const bt::game_object_frame_restorer_packet& restorer_packet) + { + game_world_->get_game_object(game_object_id)->restore_from_frame(restorer_packet); + } + ); + connection_state_manager_->start(); + } + + ~sfml_game() = default; + + public: + void add_game_object(const std::shared_ptr& game_object) const + { + game_world_->add_game_object(game_object); + render_scene_->add_child(game_object->get_render_object()); + } + + void delete_game_object(const bt::uuid game_object_id) + { + objects_to_delete_.insert(game_object_id); + } + + void update(const float delta_time) + { + for (const auto& player : players_ | std::views::values) + { + if (player->get_id() == game_session_->get_player_id()) + { + game_screen_->set_payer_hp(player->get_health()); + } + else + { + game_screen_->set_enemy_hp(player->get_health()); + } + } + + window_->clear(sf::Color::Black); + + delete_game_objects(); + + connection_state_manager_->update(delta_time); + + physics_world_.Step(delta_time, 8, 3); + game_world_->update(delta_time); + render_scene_->draw_scene(); + physics_world_.DebugDraw(); + + game_screen_->draw_scene(); + window_->display(); + } + + void handle_event(const sf::Event& event) const + { + connection_state_manager_->handle_event(event); + } + + private: + void delete_game_objects() + { + for (const auto& game_object_id : objects_to_delete_) + { + auto game_object = game_world_->get_game_object(game_object_id); + render_scene_->remove_child(game_object->get_render_object()); + game_world_->remove_game_object(game_object); + } + objects_to_delete_.clear(); + } + + private: + float pixels_per_meters_{}; + b2World physics_world_{ b2Vec2{ 0.0f, 0.0f } }; + std::unordered_set objects_to_delete_{}; + SFMLDebugDraw debug_draw_; + + //TODO:: add more screens and screen management + std::unique_ptr game_screen_; + + std::shared_ptr window_{ nullptr }; + std::unique_ptr game_world_{ nullptr }; + std::unique_ptr render_scene_{ nullptr }; + std::shared_ptr physics_body_factory_{ nullptr }; + std::unique_ptr contact_listener_{ nullptr }; + + std::shared_ptr texture_warehouse_{ nullptr }; + std::shared_ptr game_session_{ nullptr }; + std::unordered_map> players_{}; + std::unique_ptr connection_state_manager_{ nullptr }; + }; + +} diff --git a/battle_tanks/client/headers/network/client_connection.hpp b/battle_tanks/client/headers/network/client_connection.hpp new file mode 100644 index 0000000..87d517e --- /dev/null +++ b/battle_tanks/client/headers/network/client_connection.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "network/commands.hpp" +#include "network/connection_base.hpp" + + +namespace bt +{ + class client_connection : public connection_base + { + public: + client_connection(const std::shared_ptr>& commands_in) + : connection_base{ std::make_unique(), commands_in } + { + } + + virtual ~client_connection() override = default; + + public: + void connect(const sf::IpAddress& address, const unsigned short port) const + { + socket_->connect(address, port); + } + }; +} diff --git a/battle_tanks/client/headers/network/connection_service.hpp b/battle_tanks/client/headers/network/connection_service.hpp new file mode 100644 index 0000000..0c43526 --- /dev/null +++ b/battle_tanks/client/headers/network/connection_service.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "network/client_connection.hpp" + + +namespace bt +{ + class connection_service + { + public: + explicit connection_service(const std::shared_ptr>& commands_in) + : connection_(commands_in) + { + + } + + ~connection_service() = default; + + public: + //TODO:: encapsulate it and add events/callback for incoming commands (commands_in) + bt::client_connection& get_connection() + { + return connection_; + } + + void send(const client_command& command) + { + sf::Packet packet; + command.write_to_packet(packet); + + connection_.send(packet); + } + + private: + bt::client_connection connection_; + }; +} diff --git a/battle_tanks/client/headers/renderer/container.hpp b/battle_tanks/client/headers/renderer/container.hpp new file mode 100644 index 0000000..61c5164 --- /dev/null +++ b/battle_tanks/client/headers/renderer/container.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include "SFML/Graphics/Transform.hpp" + +#include "render_object.hpp" + +namespace bt +{ + class container : public render_object + { + public: + virtual ~container() override = default; + + public: + void add_child(const std::shared_ptr& child); + + void remove_child(const std::shared_ptr& child); + void remove_child(const render_object* const child); + + protected: + void draw(const std::shared_ptr& render_target, const sf::Transform& parent_transform) const override; + + protected: + std::vector> children_{}; + }; +} diff --git a/battle_tanks/client/headers/renderer/render_object.hpp b/battle_tanks/client/headers/renderer/render_object.hpp new file mode 100644 index 0000000..190a1bf --- /dev/null +++ b/battle_tanks/client/headers/renderer/render_object.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace bt +{ + class container; + + class render_object : public sf::Transformable + { + friend bt::container; + public: + virtual ~render_object() override = default; + + public: + virtual void free(); + + //void set_parent(const std::shared_ptr& parent) + //{ + // parent_ = parent; + //} + + //std::shared_ptr get_parent() const + //{ + // return parent_; + //} + + protected: + virtual void draw(const std::shared_ptr& render_target, const sf::Transform& parent_transform) const = 0; + + private: + bt::container* parent_{ nullptr }; + }; +} diff --git a/battle_tanks/client/headers/renderer/render_scene.hpp b/battle_tanks/client/headers/renderer/render_scene.hpp new file mode 100644 index 0000000..bf18405 --- /dev/null +++ b/battle_tanks/client/headers/renderer/render_scene.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +#include "renderer/container.hpp" + +namespace bt +{ + class render_scene : public bt::container + { + public: + explicit render_scene(const std::shared_ptr& render_target) : render_target_{ render_target } + { + } + + virtual ~render_scene() override = default; + + public: + void draw_scene() const + { + bt::container::draw(render_target_, getTransform()); + } + + private: + std::shared_ptr render_target_{ nullptr }; + }; +} \ No newline at end of file diff --git a/battle_tanks/client/headers/renderer/renderer.hpp b/battle_tanks/client/headers/renderer/renderer.hpp new file mode 100644 index 0000000..ee3e489 --- /dev/null +++ b/battle_tanks/client/headers/renderer/renderer.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "renderer/textures.hpp" + +#include "renderer/render_object.hpp" +#include "renderer/container.hpp" +#include "renderer/sprite.hpp" +#include "renderer/render_scene.hpp" diff --git a/battle_tanks/client/headers/renderer/sprite.hpp b/battle_tanks/client/headers/renderer/sprite.hpp new file mode 100644 index 0000000..3ec8b95 --- /dev/null +++ b/battle_tanks/client/headers/renderer/sprite.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "renderer/textures.hpp" +#include "renderer/render_object.hpp" + +namespace bt +{ + class sprite : public bt::render_object + { + public: + explicit sprite(const std::shared_ptr& texture_render_data) + { + texture_render_data_ = texture_render_data; + sprite_.setTexture(*texture_render_data_->get_texture()); + sprite_.setTextureRect(texture_render_data_->get_texture_rect()); + } + + virtual ~sprite() override = default; + + + protected: + void draw(const std::shared_ptr& render_target, const sf::Transform& parent_transform) const override + { + if (texture_render_data_ == nullptr) + { + return; + } + render_target->draw(sprite_, parent_transform * getTransform()); + } + + private: + sf::Sprite sprite_{}; + std::shared_ptr texture_render_data_{ nullptr }; + }; +} diff --git a/battle_tanks/client/headers/renderer/textures.hpp b/battle_tanks/client/headers/renderer/textures.hpp new file mode 100644 index 0000000..74551c4 --- /dev/null +++ b/battle_tanks/client/headers/renderer/textures.hpp @@ -0,0 +1,363 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + + +namespace bt +{ + enum class texture_id + { + unknown = 0, + tank_base = 1, + tank_turret = 2, + bullet = 3, + + //atlas + grass_light_tl, + grass_light_tm, + grass_light_tr, + + grass_light_lm, + grass_light_m, + grass_light_rm, + + grass_light_bl, + grass_light_bm, + grass_light_br, + + grass_light_dec_1, + grass_light_dec_2, + grass_light_dec_3, + + grass_light_dec_4, + grass_light_dec_5, + grass_light_dec_6, + + grass_light_dec_7, + grass_light_dec_8, + grass_light_dec_9, + + grass_dark_tl, + grass_dark_tm, + grass_dark_tr, + + grass_dark_lm, + grass_dark_m, + grass_dark_rm, + + grass_dark_bl, + grass_dark_bm, + grass_dark_br, + }; + + enum class textures_pack_id : unsigned + { + unknown = 0, + tank, + map_forest, + }; + + enum class atlas_id : unsigned + { + unknown = 0, + map_forest_tile_set, + }; + + class texture_holder + { + public: + explicit texture_holder(const std::shared_ptr& atlas_texture, const sf::IntRect& texture_rect) + { + texture_ = atlas_texture; + texture_size_ = texture_->getSize(); + texture_rect_ = texture_rect; + } + + explicit texture_holder(const std::string& path) + { + texture_ = std::make_shared(); + texture_->loadFromFile(path); + texture_->setSmooth(true); + texture_size_ = texture_->getSize(); + texture_rect_ = { 0,0, + static_cast(texture_size_.x), + static_cast(texture_size_.y) + }; + } + + public: + std::shared_ptr get_texture() const + { + return texture_; + } + + const sf::Vector2u& get_size() const + { + return texture_size_; + } + + sf::IntRect get_texture_rect() const + { + return texture_rect_; + } + + private: + std::shared_ptr texture_{ nullptr }; + sf::Vector2u texture_size_{}; + sf::IntRect texture_rect_; + }; + + class texture_loader + { + public: + virtual ~texture_loader() = default; + public: + virtual std::shared_ptr load_texture(texture_id id) = 0; + }; + + class textures_pack + { + public: + textures_pack(const bt::textures_pack_id id, const std::vector& textures_ids, texture_loader* loader) + : id_{ id }, textures_ids_{ textures_ids }, loader_{ loader } + { + } + + void load_pack() + { + if (loader_ == nullptr) + { + return; + } + for (const auto& texture_id : textures_ids_) + { + loaded_textures_.emplace(texture_id, loader_->load_texture(texture_id)); + } + } + + std::shared_ptr get_texture(const texture_id id) + { + if (!loaded_textures_.contains(id)) + { + return nullptr; + } + return loaded_textures_.at(id); + } + + bool has_texture(const texture_id id) const + { + return loaded_textures_.contains(id); + } + + public: + textures_pack_id get_id() const + { + return id_; + } + + private: + bt::textures_pack_id id_{ bt::textures_pack_id::unknown }; + std::vector textures_ids_{}; + texture_loader* loader_{ nullptr }; + std::unordered_map> loaded_textures_{}; + }; + + class atlas_data + { + public: + //TODO:: move atlas data to config file and create atlas_data from it + atlas_data( + const atlas_id id, + std::string&& atlas_path, + const std::unordered_map& textures_rect) + : id_(id), textures_rect_(textures_rect), atlas_path_(std::move(atlas_path)) + { + } + + explicit atlas_data(std::string&& atlas_config_path) + { + nlohmann::json atlas_config_json; + std::ifstream atlas_config_file(atlas_config_path); + atlas_config_file >> atlas_config_json; + + id_ = atlas_config_json["id"].get(); + atlas_path_ = atlas_config_json["atlas_path"]; + textures_rect_ = {}; + for (const auto& texture_data : atlas_config_json["textures"]) + { + textures_rect_.emplace( + texture_data["id"].get(), + sf::IntRect{ + texture_data["x"].get(), + texture_data["y"].get(), + texture_data["w"].get(), + texture_data["h"].get() } + ); + } + } + + ~atlas_data() = default; + + public: + const std::string& get_atlas_path() const + { + return atlas_path_; + } + + const std::unordered_map& get_textures_rect() const + { + return textures_rect_; + } + + atlas_id get_id() const + { + return id_; + } + + private: + atlas_id id_; + std::unordered_map textures_rect_; + std::string atlas_path_; + }; + + class texture_warehouse : public texture_loader + { + public: + texture_warehouse() = default; + virtual ~texture_warehouse() override = default; + + public: + virtual std::shared_ptr load_texture(texture_id id) override + { + if (!loaded_textures_.contains(id)) + { + loaded_textures_.emplace(id, std::make_shared(get_texture_path(id))); + } + return loaded_textures_.at(id); + } + + public: + void pre_load_atlas(const atlas_data& atlas_data) + { + const auto& textures_rect = atlas_data.get_textures_rect(); + const auto atlas_texture = std::make_shared(); + atlas_texture->loadFromFile(atlas_data.get_atlas_path()); + + for (const auto& [texture_id, texture_rect] : textures_rect) + { + if (!loaded_textures_.contains(texture_id)) + { + loaded_textures_.emplace(texture_id, std::make_shared(atlas_texture, texture_rect)); + } + } + } + + std::shared_ptr load_pack(const textures_pack_id id) + { + if (!textures_packs_.contains(id)) + { + const std::vector& textures_ids = get_textures_ids(id); + textures_packs_.emplace(id, std::make_shared(id, textures_ids, this)); + textures_packs_.at(id)->load_pack(); + } + + return textures_packs_.at(id); + } + + std::shared_ptr get_texture(const textures_pack_id pack_id, const texture_id texture_id) const + { + if (!textures_packs_.contains(pack_id)) + { + return nullptr; + } + return textures_packs_.at(pack_id)->get_texture(texture_id); + } + + bool has_texture(const textures_pack_id pack_id, const texture_id texture_id) const + { + if (!textures_packs_.contains(pack_id)) + { + return false; + } + return textures_packs_.at(pack_id)->has_texture(texture_id); + } + + private: + //TODO:: move to config file + static std::vector& get_textures_ids(const textures_pack_id id) + { + static std::vector tank_textures_ids{ texture_id::tank_base, texture_id::tank_turret }; + static std::vector bullet_textures_ids{ texture_id::bullet }; + static std::vector map_forest_texture_ids{ + texture_id::grass_light_m, + texture_id::grass_light_tl, + texture_id::grass_light_tm, + texture_id::grass_light_tr, + texture_id::grass_light_bl, + texture_id::grass_light_bm, + texture_id::grass_light_br, + texture_id::grass_light_lm, + texture_id::grass_light_rm, + + texture_id::grass_light_dec_1, + texture_id::grass_light_dec_2, + texture_id::grass_light_dec_3, + texture_id::grass_light_dec_4, + texture_id::grass_light_dec_5, + texture_id::grass_light_dec_6, + texture_id::grass_light_dec_7, + texture_id::grass_light_dec_8, + texture_id::grass_light_dec_9, + + texture_id::grass_dark_m, + texture_id::grass_dark_tl, + texture_id::grass_dark_tm, + texture_id::grass_dark_tr, + texture_id::grass_dark_bl, + texture_id::grass_dark_bm, + texture_id::grass_dark_br, + texture_id::grass_dark_lm, + texture_id::grass_dark_rm, + }; + static std::vector unknown_textures_ids{ texture_id::unknown }; + switch (id) + { + case textures_pack_id::tank: + return tank_textures_ids; + case textures_pack_id::map_forest: + return map_forest_texture_ids; + case textures_pack_id::unknown: + return unknown_textures_ids; + default: + return unknown_textures_ids; + } + } + + static std::string get_texture_path(const texture_id id) + { + switch (id) + { + case texture_id::tank_base: + return "game_data/atlases/tank.png"; + case texture_id::tank_turret: + return "game_data/atlases/tower.png"; + case texture_id::bullet: + return "game_data/atlases/bullet_1.png"; + case texture_id::unknown: + default: + return ""; + } + } + + private: + std::unordered_map> textures_packs_{}; + std::unordered_map> loaded_textures_{}; + std::weak_ptr loader_; + }; +} diff --git a/battle_tanks/client/headers/ui/game_screen.hpp b/battle_tanks/client/headers/ui/game_screen.hpp new file mode 100644 index 0000000..2e478b6 --- /dev/null +++ b/battle_tanks/client/headers/ui/game_screen.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include + +#include "renderer/container.hpp" + + +namespace bt +{ + class game_screen_config final + { + public: + game_screen_config() + { + nlohmann::json config_json; + std::ifstream config_file("game_data/screens/game_screen_config.json"); + config_file >> config_json; + + wounded_threshold_ = config_json["hp_text_color_thresholds"]["wounded"].get(); + dying_threshold_ = config_json["hp_text_color_thresholds"]["dying"].get(); + } + + ~game_screen_config() = default; + + public: + unsigned get_wounded_threshold() const + { + return wounded_threshold_; + } + + unsigned get_dying_threshold() const + { + return dying_threshold_; + } + + private: + unsigned wounded_threshold_{}; + unsigned dying_threshold_{}; + }; + + class player_hp_text final : public bt::render_object + { + public: + explicit player_hp_text(const sf::Font& font, const unsigned wounded_threshold, const unsigned dying_threshold) + : wounded_threshold_(wounded_threshold), dying_threshold_(dying_threshold) + { + hp_text_.setFont(font); + hp_text_.setCharacterSize(24); + hp_text_.setFillColor(sf::Color::Green); + hp_text_.setStyle(sf::Text::Bold); + } + + virtual ~player_hp_text() override = default; + + public: + void set_hit_point(const sf::Int32 hp) + { + hp_text_.setString(std::to_string(hp)); + set_text_color(hp_text_, hp); + } + + sf::FloatRect get_local_bounds() const + { + return hp_text_.getLocalBounds(); + } + + protected: + void draw(const std::shared_ptr& render_target, const sf::Transform& parent_transform) const override + { + render_target->draw(hp_text_, parent_transform * getTransform()); + } + + private: + void set_text_color(sf::Text& text, const sf::Int32 current_hp) + { + if (current_hp > 50) + { + text.setFillColor(sf::Color::Green); + } + else if (current_hp > 25) + { + text.setFillColor(sf::Color::Yellow); + } + else + { + text.setFillColor(sf::Color::Red); + } + } + + private: + sf::Text hp_text_{}; + unsigned wounded_threshold_; + unsigned dying_threshold_; + }; + + class game_screen final : public bt::render_scene + { + public: + explicit game_screen(const std::shared_ptr& render_target) + : render_scene(render_target) + { + font_.loadFromFile("game_data/fonts/wheaton capitals.otf"); + config_ = std::make_unique(); + + player_hp_text_ = std::make_shared(font_, config_->get_wounded_threshold(), config_->get_dying_threshold()); + enemy_hp_text_ = std::make_shared(font_, config_->get_wounded_threshold(), config_->get_dying_threshold()); + + set_payer_hp(100); + set_enemy_hp(100); + + player_hp_text_->setPosition(10, 10); + enemy_hp_text_->setPosition( + static_cast(bt::game_entity_consts::game_field_size.x) - enemy_hp_text_->get_local_bounds().width - 15, + 10 + ); + + add_child(player_hp_text_); + add_child(enemy_hp_text_); + } + + virtual ~game_screen() override = default; + + public: + void set_payer_hp(const sf::Int32 hp) const + { + player_hp_text_->set_hit_point(hp); + } + + void set_enemy_hp(const sf::Int32 hp) const + { + enemy_hp_text_->set_hit_point(hp); + } + + private: + std::shared_ptr player_hp_text_{ nullptr }; + std::shared_ptr enemy_hp_text_{ nullptr }; + std::unique_ptr config_{ nullptr }; + + sf::Font font_{}; + }; +} diff --git a/battle_tanks/client/sources/dev/debug_draw.cpp b/battle_tanks/client/sources/dev/debug_draw.cpp new file mode 100644 index 0000000..1209ad6 --- /dev/null +++ b/battle_tanks/client/sources/dev/debug_draw.cpp @@ -0,0 +1,111 @@ +#include "SFML/Graphics.hpp" + +#include "dev/debug_draw.hpp" + +float SFMLDebugDraw::pixels_per_meter = 30.f; + +SFMLDebugDraw::SFMLDebugDraw(const std::shared_ptr& window, float pixels_per_meters) : m_window(window) +{ + pixels_per_meter = pixels_per_meters; +} + +void SFMLDebugDraw::DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) +{ + sf::ConvexShape polygon(vertexCount); + sf::Vector2f center; + for (int i = 0; i < vertexCount; i++) + { + //polygon.setPoint(i, SFMLDraw::B2VecToSFVec(vertices[i])); + sf::Vector2f transformedVec = SFMLDebugDraw::B2VecToSFVec(vertices[i]); + polygon.setPoint(i, sf::Vector2f(std::floor(transformedVec.x), std::floor(transformedVec.y))); // flooring the coords to fix distorted lines on flat surfaces + } // they still show up though.. but less frequently + polygon.setOutlineThickness(-1.f); + polygon.setFillColor(sf::Color::Transparent); + polygon.setOutlineColor(SFMLDebugDraw::GLColorToSFML(color)); + + m_window->draw(polygon); +} +void SFMLDebugDraw::DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) +{ + sf::ConvexShape polygon(vertexCount); + for (int i = 0; i < vertexCount; i++) + { + //polygon.setPoint(i, SFMLDraw::B2VecToSFVec(vertices[i])); + sf::Vector2f transformedVec = SFMLDebugDraw::B2VecToSFVec(vertices[i]); + polygon.setPoint(i, sf::Vector2f(std::floor(transformedVec.x), std::floor(transformedVec.y))); // flooring the coords to fix distorted lines on flat surfaces + } // they still show up though.. but less frequently + polygon.setOutlineThickness(-1.f); + polygon.setFillColor(SFMLDebugDraw::GLColorToSFML(color, 60)); + polygon.setOutlineColor(SFMLDebugDraw::GLColorToSFML(color)); + + m_window->draw(polygon); +} +void SFMLDebugDraw::DrawCircle(const b2Vec2& center, float radius, const b2Color& color) +{ + sf::CircleShape circle(radius * pixels_per_meter); + circle.setOrigin(radius * pixels_per_meter, radius * pixels_per_meter); + circle.setPosition(SFMLDebugDraw::B2VecToSFVec(center)); + circle.setFillColor(sf::Color::Transparent); + circle.setOutlineThickness(-1.f); + circle.setOutlineColor(SFMLDebugDraw::GLColorToSFML(color)); + + m_window->draw(circle); +} +void SFMLDebugDraw::DrawSolidCircle(const b2Vec2& center, float radius, const b2Vec2& axis, const b2Color& color) +{ + sf::CircleShape circle(radius * pixels_per_meter); + circle.setOrigin(radius * pixels_per_meter, radius * pixels_per_meter); + circle.setPosition(SFMLDebugDraw::B2VecToSFVec(center)); + circle.setFillColor(SFMLDebugDraw::GLColorToSFML(color, 60)); + circle.setOutlineThickness(1.f); + circle.setOutlineColor(SFMLDebugDraw::GLColorToSFML(color)); + + b2Vec2 endPoint = center + radius * axis; + sf::Vertex line[2] = + { + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(center), SFMLDebugDraw::GLColorToSFML(color)), + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(endPoint), SFMLDebugDraw::GLColorToSFML(color)), + }; + + m_window->draw(circle); + m_window->draw(line, 2, sf::Lines); +} +void SFMLDebugDraw::DrawSegment(const b2Vec2& p1, const b2Vec2& p2, const b2Color& color) +{ + sf::Vertex line[] = + { + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(p1), SFMLDebugDraw::GLColorToSFML(color)), + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(p2), SFMLDebugDraw::GLColorToSFML(color)) + }; + + m_window->draw(line, 2, sf::Lines); +} +void SFMLDebugDraw::DrawTransform(const b2Transform& xf) +{ + float lineLength = 0.4; + + /*b2Vec2 xAxis(b2Vec2(xf.p.x + (lineLength * xf.q.c), xf.p.y + (lineLength * xf.q.s)));*/ + b2Vec2 xAxis = xf.p + lineLength * xf.q.GetXAxis(); + sf::Vertex redLine[] = + { + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(xf.p), sf::Color::Red), + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(xAxis), sf::Color::Red) + }; + + // You might notice that the ordinate(Y axis) points downward unlike the one in Box2D testbed + // That's because the ordinate in SFML coordinate system points downward while the OpenGL(testbed) points upward + /*b2Vec2 yAxis(b2Vec2(xf.p.x + (lineLength * -xf.q.s), xf.p.y + (lineLength * xf.q.c)));*/ + b2Vec2 yAxis = xf.p + lineLength * xf.q.GetYAxis(); + sf::Vertex greenLine[] = + { + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(xf.p), sf::Color::Green), + sf::Vertex(SFMLDebugDraw::B2VecToSFVec(yAxis), sf::Color::Green) + }; + + m_window->draw(redLine, 2, sf::Lines); + m_window->draw(greenLine, 2, sf::Lines); +} + +void SFMLDebugDraw::DrawPoint(const b2Vec2& p, float size, const b2Color& color) +{ +} diff --git a/battle_tanks/client/sources/game/game_objects/bullet_game_object.cpp b/battle_tanks/client/sources/game/game_objects/bullet_game_object.cpp new file mode 100644 index 0000000..efbd11d --- /dev/null +++ b/battle_tanks/client/sources/game/game_objects/bullet_game_object.cpp @@ -0,0 +1,48 @@ +#include "game/game_objects//bullet_game_object.hpp" + +#include "game/entity/bullet_game_object_entity.hpp" +#include "game/entity/game_entity_constants.hpp" +#include "renderer/sprite.hpp" + +#include "utils/math.hpp" + + +bt::bullet_game_object::bullet_game_object( + const bt::uuid id, + const std::weak_ptr& ph_body_factory, + const std::shared_ptr& render_data) + : bt::game_object(id), render_data_{ render_data }, ph_body_factory_(ph_body_factory) +{ +} + +void bt::bullet_game_object::restore_from_frame(const bt::game_object_frame_restorer& restorer) +{ + game_object_entity_->restore_frame(restorer); +} + +void bt::bullet_game_object::create_render_object() +{ + render_object_ = std::make_shared(render_data_); + render_object_->setOrigin( + game_entity_consts::bullet_size.x / 2.0f, + game_entity_consts::bullet_size.y / 2.0f + ); +} + +void bt::bullet_game_object::create_game_object_entity() +{ + auto bullet_entity = std::make_unique(id_, ph_body_factory_/*, velocity_, rotation_rad_, tank_position_*/); + bullet_entity->create_phy_body(); + game_object_entity_ = std::move(bullet_entity); +} + +void bt::bullet_game_object::update(float delta_time) +{ + const auto frame = std::make_shared(); + game_object_entity_->write_to_frame(frame); + const auto& ph_body_pos = frame->position; + const float ph_body_rot = frame->rotation; + + render_object_->setPosition({ ph_body_pos.x * physics_consts::pixels_per_meters, ph_body_pos.y * physics_consts::pixels_per_meters }); + render_object_->setRotation(std::fmod(bt::rad_to_deg(ph_body_rot), 360.0f)); +} diff --git a/battle_tanks/client/sources/renderer/container.cpp b/battle_tanks/client/sources/renderer/container.cpp new file mode 100644 index 0000000..4494bb2 --- /dev/null +++ b/battle_tanks/client/sources/renderer/container.cpp @@ -0,0 +1,46 @@ +#include "renderer/container.hpp" +#include "renderer/render_object.hpp" + +void bt::container::add_child(const std::shared_ptr& child) +{ + if (child->parent_ != nullptr) + { + child->parent_->remove_child(child); + } + + children_.push_back(child); + child->parent_ = this; +} + +void bt::container::remove_child(const std::shared_ptr& child) +{ + for (auto it = children_.begin(); it != children_.end(); ++it) + { + if (*it == child) + { + children_.erase(it); + break; + } + } +} + +void bt::container::remove_child(const render_object* const child) +{ + for (auto it = children_.begin(); it != children_.end(); ++it) + { + if (it->get() == child) + { + children_.erase(it); + break; + } + } +} + +void bt::container::draw(const std::shared_ptr& render_target, const sf::Transform& parent_transform) const +{ + const sf::Transform transform = parent_transform * getTransform(); + for (const auto& ch : children_) + { + ch->draw(render_target, transform); + } +} diff --git a/battle_tanks/client/sources/renderer/render_object.cpp b/battle_tanks/client/sources/renderer/render_object.cpp new file mode 100644 index 0000000..fab3457 --- /dev/null +++ b/battle_tanks/client/sources/renderer/render_object.cpp @@ -0,0 +1,12 @@ +#include "renderer/render_object.hpp" +#include "renderer/container.hpp" + + +void bt::render_object::free() +{ + if (parent_ != nullptr) + { + parent_->remove_child(this); + parent_ = nullptr; + } +} diff --git a/battle_tanks/common_logic/CMakeLists.txt b/battle_tanks/common_logic/CMakeLists.txt new file mode 100644 index 0000000..c82c4d4 --- /dev/null +++ b/battle_tanks/common_logic/CMakeLists.txt @@ -0,0 +1,29 @@ +project(common-logic) +cmake_minimum_required (VERSION 3.20) + +set(LIB_NAME bt-common) + +include_directories(${PROJECT_SOURCE_DIR}/headers) +file(GLOB_RECURSE SOURCES_CPP "./*.cpp") + +find_package(SFML COMPONENTS system CONFIG REQUIRED) +find_package(box2d CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) + +add_library(${LIB_NAME} STATIC ${SOURCES_CPP}) + +if (CMAKE_VERSION VERSION_GREATER 3.12) + set_property(TARGET ${LIB_NAME} PROPERTY CXX_STANDARD 20) +endif() + +set_target_properties(${LIB_NAME} PROPERTIES LINKER_LANGUAGE CXX) + +target_include_directories(${LIB_NAME} PUBLIC include) + +target_link_libraries(${LIB_NAME} + PUBLIC + sfml-system + sfml-network + box2d::box2d + nlohmann_json::nlohmann_json +) \ No newline at end of file diff --git a/battle_tanks/common_logic/headers/data_structures/ts_deque.hpp b/battle_tanks/common_logic/headers/data_structures/ts_deque.hpp new file mode 100644 index 0000000..bc344ef --- /dev/null +++ b/battle_tanks/common_logic/headers/data_structures/ts_deque.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + + +template +class ts_deque final +{ +public: + ts_deque() = default; + ts_deque(const ts_deque&) = delete; + ~ts_deque() = default; + +public: + const T& front() + { + std::scoped_lock lock(mux_queue_); + return deq_queue_.front(); + } + + const T& back() + { + std::scoped_lock lock(mux_queue_); + return deq_queue_.back(); + } + + T pop_front() + { + std::scoped_lock lock(mux_queue_); + auto t = std::move(deq_queue_.front()); + deq_queue_.pop_front(); + return t; + } + + // Removes and returns item from back of Queue + T pop_back() + { + std::scoped_lock lock(mux_queue_); + auto t = std::move(deq_queue_.back()); + deq_queue_.pop_back(); + return t; + } + + void push_back(const T& item) + { + std::scoped_lock lock(mux_queue_); + deq_queue_.emplace_back(std::move(item)); + + std::unique_lock ul(mux_blocking_); + cv_blocking_.notify_one(); + } + + void push_back(T&& item) + { + std::scoped_lock lock(mux_queue_); + deq_queue_.emplace_back(std::move(item)); + + std::unique_lock ul(mux_blocking_); + cv_blocking_.notify_one(); + } + + void push_front(const T& item) + { + std::scoped_lock lock(mux_queue_); + deq_queue_.emplace_front(std::move(item)); + + std::unique_lock ul(mux_blocking_); + cv_blocking_.notify_one(); + } + + bool empty() + { + std::scoped_lock lock(mux_queue_); + return deq_queue_.empty(); + } + + size_t count() + { + std::scoped_lock lock(mux_queue_); + return deq_queue_.size(); + } + + void clear() + { + std::scoped_lock lock(mux_queue_); + deq_queue_.clear(); + } + + void wait() + { + while (empty()) + { + std::unique_lock ul(mux_blocking_); + cv_blocking_.wait(ul); + } + } + +private: + std::mutex mux_queue_; + std::deque deq_queue_; + std::condition_variable cv_blocking_; + std::mutex mux_blocking_; +}; diff --git a/battle_tanks/common_logic/headers/game/entity/bullet_game_object_entity.hpp b/battle_tanks/common_logic/headers/game/entity/bullet_game_object_entity.hpp new file mode 100644 index 0000000..89a6765 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/bullet_game_object_entity.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include "box2d/b2_fixture.h" +#include "box2d/b2_polygon_shape.h" +#include "SFML/Config.hpp" + +#include "game/entity/phy_game_object_entity.hpp" +#include "game/entity/game_entity_constants.hpp" +#include "physics/physics_body_factory.hpp" +#include "game/player_action_controller.hpp" +#include "game/game_objects/game_object_frame_restorer.hpp" +#include "physics/game_object_b2d_link.hpp" + +namespace bt +{ + class bullet_game_object_entity : public bt::phy_game_object_entity + { + public: + bullet_game_object_entity(const sf::Uint32 id, const std::weak_ptr& ph_body_factory) + : bt::phy_game_object_entity(id, game_object_type::bullet, ph_body_factory) + { + } + + virtual ~bullet_game_object_entity() override = default; + + public: + virtual void create_phy_body() override + { + if (ph_body_factory_.expired()) + { + return; + } + b2BodyDef bullet_body_def; + bullet_body_def.type = b2_dynamicBody; + phy_body_ = ph_body_factory_.lock()->create_body(bullet_body_def); + b2PolygonShape bullet_shape; + bullet_shape.SetAsBox( + game_entity_consts::bullet_size.x / physics_consts::pixels_per_meters / 2, + game_entity_consts::bullet_size.y / physics_consts::pixels_per_meters / 2 + ); + auto* fixture = phy_body_->CreateFixture(&bullet_shape, 1.0f); + + auto* bullet_link = new bt::game_object_b2d_link{ bt::game_object_type::bullet, id_ }; + phy_body_->GetUserData().pointer = reinterpret_cast(bullet_link); + + b2Filter filter; + filter.categoryBits = 0x0001; + filter.maskBits = 0x8005;//4-player, 1-bullet + fixture->SetFilterData(filter); + } + + virtual void update(const float dt) override + { + } + + virtual void restore_frame(const bt::game_object_frame_restorer& restorer) override + { + game_object_frame frame{}; + restorer.restore_frame(frame); + + phy_body_->SetTransform( + b2Vec2( + frame.position.x, + frame.position.y + ), + frame.rotation + ); + phy_body_->SetLinearVelocity( + b2Vec2( + frame.velocity.x, + frame.velocity.y + ) + ); + //phy_body_->SetAngularVelocity(frame.velocity_angle); + } + public: + void create_physics_body(const b2Vec2& player_position, const float rotation_rad, const float velocity) + { + create_phy_body(); + const b2Vec2 bullet_pos{ + (player_position.x + game_entity_consts::turret_length * std::sin(rotation_rad)), + (player_position.y - game_entity_consts::turret_length * std::cos(rotation_rad)) + }; + + const auto force = b2Vec2{ + std::sin(rotation_rad) * velocity, + -std::cos(rotation_rad) * velocity + }; + phy_body_->SetTransform(bullet_pos, rotation_rad); + phy_body_->SetLinearVelocity(force); + } + + protected: + [[nodiscard]] + virtual std::shared_ptr create_frame() const override + { + auto go_frame = std::make_unique(); + return go_frame; + } + + }; +} diff --git a/battle_tanks/common_logic/headers/game/entity/game_entity_constants.hpp b/battle_tanks/common_logic/headers/game/entity/game_entity_constants.hpp new file mode 100644 index 0000000..fdb28e5 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/game_entity_constants.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "SFML/System/Vector2.hpp" + + +//TOD::move to config, configuration for tanks types and weapons types/damage +namespace bt +{ + namespace game_entity_consts + { + static sf::Vector2f tank_size{33.0f, 54.0f}; + static sf::Vector2f bullet_size{ 6.0f, 24.0f }; + static float turret_length = 8.0f;//meters + static sf::Vector2u game_field_size = sf::Vector2u{ 1024, 768 }; + + static sf::Int32 tank_hp = 100; + static sf::Int32 bullet_damage = 15; + } + + namespace physics_consts + { + static float pixels_per_meters = 10.0f; // 1 meter = 10 pixels + static b2Vec2 game_field_size = b2Vec2{ 102.4f, 76.8f }; + + static float tank_speed = 50.0f; //meters per second + static float bullet_speed = 500.0f; //meters per second + } +} diff --git a/battle_tanks/common_logic/headers/game/entity/game_map_entity.hpp b/battle_tanks/common_logic/headers/game/entity/game_map_entity.hpp new file mode 100644 index 0000000..2fcf7e9 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/game_map_entity.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include + +#include "game_object_type.hpp" + + +class game_map_entity : public bt::game_object_entity +{ +public: + explicit game_map_entity(const sf::Uint32 id, const b2Vec2& game_field_size, const std::weak_ptr& ph_body_factory) + : game_object_entity(id, bt::game_object_type::map_forest), game_field_size_{game_field_size}, ph_body_factory_{ph_body_factory} + { + } + + virtual ~game_map_entity() override + { + if (!ph_body_factory_.expired()) + { + const auto factory = ph_body_factory_.lock(); + for (auto* phy_body : phy_bodies_) + { + factory->destroy_body(phy_body); + } + + phy_bodies_.clear(); + } + } + +public: + void build_map() + { + if (ph_body_factory_.expired()) + { + return; + } + + const auto phy_body_factory = ph_body_factory_.lock(); + build_edges(phy_body_factory); + } + + void create_rock(const b2Vec2 position, const b2Vec2 size) + { + if (ph_body_factory_.expired()) + { + return; + } + const auto phy_body_factory = ph_body_factory_.lock(); + + b2BodyDef rock_body_def; + rock_body_def.type = b2_staticBody; + rock_body_def.position.Set(position.x, position.y); + const auto rock_body = phy_body_factory->create_body(rock_body_def); + phy_bodies_.push_back(rock_body); + + b2PolygonShape rock_shape; + rock_shape.SetAsBox(size.x /2.0f, size.y / 2.0f); + auto* fixture = rock_body->CreateFixture(&rock_shape, 1.0f); + b2Filter filter; + filter.categoryBits = 0x0002; + filter.maskBits = 0x8006; + + fixture->SetFilterData(filter); + } + +public: + void restore_frame(const bt::game_object_frame_restorer& restorer) override + { + } + + void update(const float delta_time) override + { + } + + virtual bool is_out_of_edges(const b2Vec2& size) const override + { + return false; + } + +protected: + + //TODO:: create map from server configuration + [[nodiscard]] std::shared_ptr create_frame() const override + { + return nullptr; + } + void fill_frame_data(const std::shared_ptr& object_frame) const override + {} + +private: + void build_edges(const std::shared_ptr& factory) + { + b2BodyDef edge_body_def; + edge_body_def.type = b2_staticBody; + b2Body* edges_body = factory->create_body(edge_body_def); + phy_bodies_.push_back(edges_body); + + b2Filter edge_filter; + edge_filter.categoryBits = 0x8000; + edge_filter.maskBits = 0x0007; + + b2EdgeShape top_edge_shape; + top_edge_shape.SetTwoSided(b2Vec2(0.0f, 0.0f), b2Vec2(game_field_size_.x, 0.0f)); + auto* edge_fixture = edges_body->CreateFixture(&top_edge_shape, 1.0f); + edge_fixture->SetFilterData(edge_filter); + + b2EdgeShape bottom_edge_shape; + bottom_edge_shape.SetTwoSided(b2Vec2(0.0f, game_field_size_.y), game_field_size_); + edge_fixture = edges_body->CreateFixture(&bottom_edge_shape, 1.0f); + edge_fixture->SetFilterData(edge_filter); + + b2EdgeShape left_edge_shape; + left_edge_shape.SetTwoSided(b2Vec2(0.0f, 0.0f), b2Vec2(0.0f, game_field_size_.y)); + edge_fixture = edges_body->CreateFixture(&left_edge_shape, 1.0f); + edge_fixture->SetFilterData(edge_filter); + + b2EdgeShape right_edge_shape; + right_edge_shape.SetTwoSided(b2Vec2(game_field_size_.x, 0.0f), game_field_size_); + edge_fixture = edges_body->CreateFixture(&right_edge_shape, 1.0f); + edge_fixture->SetFilterData(edge_filter); + } + +private: + b2Vec2 game_field_size_; + std::weak_ptr ph_body_factory_; + std::vector phy_bodies_{}; +}; diff --git a/battle_tanks/common_logic/headers/game/entity/game_object_entity.hpp b/battle_tanks/common_logic/headers/game/entity/game_object_entity.hpp new file mode 100644 index 0000000..21ed278 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/game_object_entity.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "box2d/b2_math.h" +#include "game/entity/game_object_type.hpp" +#include "game/game_objects/game_object_frame_restorer.hpp" +#include "network/commands.hpp" + + +namespace bt +{ + class game_object_entity + { + public: + game_object_entity(const sf::Uint32 id, const bt::game_object_type type) : id_{ id }, type_ { type } + { + } + + virtual ~game_object_entity() = default; + + public: + virtual void restore_frame(const bt::game_object_frame_restorer& restorer) = 0; + virtual void update(const float delta_time) = 0; + virtual bool is_out_of_edges(const b2Vec2& size) const = 0; + + public: + sf::Uint32 get_id() const + { + return id_; + } + + bt::game_object_type get_type() const + { + return type_; + } + + [[nodiscard]] + std::shared_ptr get_frame() const + { + auto object_frame = create_frame(); + fill_frame_data(object_frame); + return object_frame; + } + + void write_to_frame(const std::shared_ptr& object_frame) const + { + fill_frame_data(object_frame); + } + + protected: + [[nodiscard]] + virtual std::shared_ptr create_frame() const = 0; + + virtual void fill_frame_data(const std::shared_ptr& object_frame) const + { + object_frame->game_object_id = id_; + object_frame->game_object_type = type_; + } + + protected: + sf::Uint32 id_; + bt::game_object_type type_; + + }; +} diff --git a/battle_tanks/common_logic/headers/game/entity/game_object_type.hpp b/battle_tanks/common_logic/headers/game/entity/game_object_type.hpp new file mode 100644 index 0000000..bd45cda --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/game_object_type.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace bt +{ + enum class game_object_type : unsigned + { + unknown = 0, + tank, + bullet, + wall, + map_forest, + }; + +} \ No newline at end of file diff --git a/battle_tanks/common_logic/headers/game/entity/phy_game_object_entity.hpp b/battle_tanks/common_logic/headers/game/entity/phy_game_object_entity.hpp new file mode 100644 index 0000000..8e1d861 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/phy_game_object_entity.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "SFML/Config.hpp" + +#include "physics/physics_body_factory.hpp" +#include "game/entity/game_object_entity.hpp" + + +namespace bt +{ + class phy_game_object_entity : public bt::game_object_entity + { + public: + phy_game_object_entity(const sf::Uint32 id, const bt::game_object_type type, const std::weak_ptr& ph_body_factory) + : bt::game_object_entity(id, type), ph_body_factory_{ ph_body_factory } + { + } + + virtual ~phy_game_object_entity() override + { + if (phy_body_ != nullptr && !ph_body_factory_.expired()) + { + ph_body_factory_.lock()->destroy_body(phy_body_); + phy_body_ = nullptr; + } + } + + public: + virtual void create_phy_body() = 0; + + virtual bool is_out_of_edges(const b2Vec2& size) const override + { + if (phy_body_ == nullptr) + { + return false; + } + const auto pos = phy_body_->GetPosition(); + return pos.x < 0.0f || pos.x > size.x || pos.y < 0.0f || pos.y > size.y; + } + + protected: + virtual void fill_frame_data(const std::shared_ptr& object_frame) const override + { + bt::game_object_entity::fill_frame_data(object_frame); + object_frame->position = { phy_body_->GetPosition().x, phy_body_->GetPosition().y }; + object_frame->rotation = phy_body_->GetAngle(); + object_frame->velocity = { phy_body_->GetLinearVelocity().x, phy_body_->GetLinearVelocity().y }; + object_frame->velocity_angle = phy_body_->GetAngularVelocity(); + } + + protected: + b2Body* phy_body_{ nullptr }; + std::weak_ptr ph_body_factory_; + }; +} diff --git a/battle_tanks/common_logic/headers/game/entity/player_game_object_entity.hpp b/battle_tanks/common_logic/headers/game/entity/player_game_object_entity.hpp new file mode 100644 index 0000000..60f20b1 --- /dev/null +++ b/battle_tanks/common_logic/headers/game/entity/player_game_object_entity.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include "box2d/b2_fixture.h" +#include "box2d/b2_polygon_shape.h" +#include "SFML/Config.hpp" + +#include "game/entity/phy_game_object_entity.hpp" +#include "game/entity/game_entity_constants.hpp" +#include "physics/physics_body_factory.hpp" +#include "game/player_action_controller.hpp" +#include "game/game_objects/game_object_frame_restorer.hpp" +#include "physics/game_object_b2d_link.hpp" + + +namespace bt +{ + class player_game_object_entity : public bt::phy_game_object_entity + { + public: + player_game_object_entity(const sf::Uint32 id, const std::weak_ptr& ph_body_factory) + : bt::phy_game_object_entity(id, game_object_type::tank, ph_body_factory) + { + } + + virtual ~player_game_object_entity() override = default; + + public: + virtual void create_phy_body() override + { + if (ph_body_factory_.expired()) + { + return; + } + + b2BodyDef tank_body_def; + tank_body_def.type = b2_dynamicBody; + phy_body_ = ph_body_factory_.lock()->create_body(tank_body_def); + b2PolygonShape tank_shape; + tank_shape.SetAsBox( + bt::game_entity_consts::tank_size.x / bt::physics_consts::pixels_per_meters / 2, + bt::game_entity_consts::tank_size.y / physics_consts::pixels_per_meters / 2 + ); + auto* tank_fixture = phy_body_->CreateFixture(&tank_shape, 1.0f); + + auto* bullet_link = new bt::game_object_b2d_link{ bt::game_object_type::tank, id_ }; + phy_body_->GetUserData().pointer = reinterpret_cast(bullet_link); + + b2Filter tank_filter; + tank_filter.categoryBits = 0x0004; + tank_filter.maskBits = 0x8007;//4-player, 2-rock, 1-bullet + tank_fixture->SetFilterData(tank_filter); + + action_controller_ = std::make_shared( + phy_body_ + //move_speed_, + //rotate_speed_ + ); + } + + virtual void update(const float dt) override + { + action_controller_->update(dt); + } + + virtual void restore_frame(const bt::game_object_frame_restorer& restorer) override + { + player_game_object_frame frame{}; + restorer.restore_frame(frame); + health_ = frame.health; + + phy_body_->SetTransform( + b2Vec2( + frame.position.x, + frame.position.y + ), + frame.rotation + ); + + if (frame.velocity.x == 0.0f && frame.velocity.y == 0.0f) + { + phy_body_->SetLinearVelocity( + b2Vec2( + frame.velocity.x, + frame.velocity.y + ) + ); + } + + if (frame.velocity_angle == 0.0f) + { + phy_body_->SetAngularVelocity(frame.velocity_angle); + } + + action_controller_->set_turret_rotation(frame.turret_rotation); + } + + public: + sf::Int32 get_health() const + { + return health_; + } + + void take_damage(const sf::Int32 damage) + { + health_ -= damage; + if (health_ < 0) + { + health_ = 0; + } + } + + [[nodiscard]] + std::weak_ptr get_action_controller() const + { + return action_controller_; + } + + void create_physics_body(const b2Vec2 position, const float rotation = 0.0f) + { + create_phy_body(); + phy_body_->SetTransform(position, rotation); + } + + protected: + [[nodiscard]] + virtual std::shared_ptr create_frame() const override + { + auto go_frame = std::make_unique(); + + go_frame->health = health_; + go_frame->player_score = 0; + go_frame->turret_rotation = action_controller_->get_turret_rotation(); + + return go_frame; + } + + virtual void fill_frame_data(const std::shared_ptr& object_frame) const override + { + bt::phy_game_object_entity::fill_frame_data(object_frame); + auto* go_frame = dynamic_cast(object_frame.get()); + go_frame->turret_rotation = action_controller_->get_turret_rotation(); + } + + private: + sf::Int32 health_{ game_entity_consts::tank_hp }; + + std::shared_ptr action_controller_{ nullptr }; + + }; +} + diff --git a/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer.hpp b/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer.hpp new file mode 100644 index 0000000..7f2c65f --- /dev/null +++ b/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "network/commands.hpp" + +namespace bt +{ + class game_object_frame_restorer + { + public: + virtual ~game_object_frame_restorer() = default; + public: + virtual void restore_frame(game_object_frame& frame) const = 0; + }; +} diff --git a/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer_packet.hpp b/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer_packet.hpp new file mode 100644 index 0000000..26cf22e --- /dev/null +++ b/battle_tanks/common_logic/headers/game/game_objects/game_object_frame_restorer_packet.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "network/commands.hpp" +#include "game/game_objects/game_object_frame_restorer.hpp" + +namespace bt +{ + class game_object_frame_restorer_packet : public game_object_frame_restorer + { + public: + explicit game_object_frame_restorer_packet(const std::weak_ptr& packet) + : packet_{ packet } + { + } + + ~game_object_frame_restorer_packet() override = default; + + public: + virtual void restore_frame(game_object_frame& frame) const override + { + if (packet_.expired()) + { + return; + } + const auto packet = packet_.lock(); + frame.read_from_packet(*packet); + } + + private: + std::weak_ptr packet_; + }; +} diff --git a/battle_tanks/common_logic/headers/game/player_action_controller.hpp b/battle_tanks/common_logic/headers/game/player_action_controller.hpp new file mode 100644 index 0000000..f74744b --- /dev/null +++ b/battle_tanks/common_logic/headers/game/player_action_controller.hpp @@ -0,0 +1,153 @@ +#pragma once + +#include +#include + +#include "network/player_actions.hpp" +#include "utils/math.hpp" + + +namespace bt +{ + class player_action_controller + { + public: + explicit player_action_controller(b2Body* physics_body) : phy_body_(physics_body) + { + } + + ~player_action_controller() = default; + + public: + void set_move_action(const player_action action) + { + move_action_ = action; + + const auto rad_rotation = phy_body_->GetAngle(); + constexpr float move_speed = 100.0; + if (move_action_ == player_action::move_forward) + { + const auto force = b2Vec2{ + std::sin(rad_rotation) * move_speed / physics_consts::pixels_per_meters, + -std::cos(rad_rotation) * move_speed / physics_consts::pixels_per_meters + }; + + phy_body_->SetLinearVelocity(force); + } + else if (move_action_ == player_action::move_backward) + { + const auto force = b2Vec2{ + -std::sin(rad_rotation) * move_speed / physics_consts::pixels_per_meters, + std::cos(rad_rotation) * move_speed / physics_consts::pixels_per_meters + }; + + phy_body_->SetLinearVelocity(force); + } + else + { + phy_body_->SetLinearVelocity({ 0.0f, 0.0f }); + } + } + + void set_rotate_action(const player_action action) + { + rotate_action_ = action; + + constexpr float rotate_speed_rad = static_cast(std::numbers::pi) / 2.0f; + if (rotate_action_ == player_action::turn_left) + { + phy_body_->SetAngularVelocity(-rotate_speed_rad); + } + else if (rotate_action_ == player_action::turn_right) + { + phy_body_->SetAngularVelocity(rotate_speed_rad); + } + else + { + phy_body_->SetAngularVelocity(0.0f); + } + } + + void set_turret_rotate_action(const player_action action) + { + //TODO:: add independent from game session loop delta_time + turret_rotate_action_ = action; + } + + void set_turret_fire_action(const player_action action) + { + turret_fire_ = action; + } + + float get_turret_rotation() const + { + return turret_rotation_; + } + + void set_turret_rotation(const float rotation) + { + turret_rotation_ = rotation; + } + + bool set_fire_action(const player_action action) + { + if (action == player_action::turret_stop_fire) + { + return false; + } + + const auto current_time = std::chrono::high_resolution_clock::now(); + if (std::chrono::duration_cast(current_time - turret_last_fire_time_).count() < 1000) + { + return false; + } + + turret_last_fire_time_ = current_time; + return true; + } + + void update(const float delta_time) + { + constexpr float rotate_speed = 90.0f; + if (turret_rotate_action_ == player_action::turn_turret_left) + { + turret_rotation_ -= rotate_speed * delta_time; + } + else if (turret_rotate_action_ == player_action::turn_turret_right) + { + turret_rotation_ += rotate_speed * delta_time; + } + + if (move_action_ == player_action::stop_move) + { + phy_body_->SetLinearVelocity({ 0.0f, 0.0f }); + } + + if (rotate_action_ == player_action::stop_turn) + { + phy_body_->SetAngularVelocity(0.0f); + } + } + + b2Vec2 get_position() const + { + return phy_body_->GetPosition(); + } + + float get_absolute_turret_rotation() const + { + return phy_body_->GetAngle() + bt::deg_to_rad(turret_rotation_); + } + + private: + player_action move_action_{player_action::stop_move}; + player_action rotate_action_{player_action::stop_turn}; + player_action turret_rotate_action_{player_action::stop_turn_turret}; + player_action turret_fire_{player_action::turret_stop_fire}; + + float turret_rotation_{0.0f}; + std::chrono::time_point turret_last_fire_time_{}; + + b2Body* phy_body_{ nullptr }; + }; +} diff --git a/battle_tanks/common_logic/headers/network/commands.hpp b/battle_tanks/common_logic/headers/network/commands.hpp new file mode 100644 index 0000000..fc494ca --- /dev/null +++ b/battle_tanks/common_logic/headers/network/commands.hpp @@ -0,0 +1,314 @@ +#pragma once + +#include +#include +#include "SFML/System/Vector2.hpp" + +#include "network/player_actions.hpp" +#include "game/entity/game_object_type.hpp" + + +enum class command_id_client : sf::Uint32 +{ + unknown = 0, + + create_session = 90001, + join_session = 90002, + quit_session = 90003, + + player_action = 90004, + + lost_connection = 90005, +}; + +enum class command_id_server : sf::Uint32 +{ + unknown = 0, + + connection_established = 10001, + + session_created = 10002, + player_joined_to_session = 10003, + player_quit_from_session = 10004, + session_started = 10006, + + update_game_frame = 10007, + + delete_game_object = 10008, + + player_shoot = 10009, +}; + +template +struct simple_command +{ + explicit simple_command(const T id) + { + command_id = id; + } + virtual ~simple_command() = default; + + T command_id; + + virtual void write_to_packet(sf::Packet& packet) const + { + packet << static_cast(command_id); + } + + virtual void read_from_packet(sf::Packet& packet) + { + packet >> reinterpret_cast(command_id); + } +}; + +////// TODO:: move from common to CLIENT +struct client_command : simple_command +{ + explicit client_command(const command_id_client id) : simple_command{ id } + {} + + virtual ~client_command() override = default; +}; + +struct join_session_command final : client_command +{ + explicit join_session_command(const sf::Uint32 session_id) + : client_command(command_id_client::join_session), session_id{ session_id } + { + } + virtual ~join_session_command() override = default; + + sf::Uint32 session_id; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << session_id; + } +}; + +struct player_action_command : client_command +{ + explicit player_action_command(const player_action action_type) + : client_command(command_id_client::player_action) + { + action = action_type; + } + virtual ~player_action_command() override = default; + + player_action action; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << static_cast(action); + } +}; + + +////// TODO:: move to server_commands +struct server_command : simple_command +{ + explicit server_command(const command_id_server id) : simple_command{ id } + { + } + virtual ~server_command() override = default; +}; + +struct game_object_frame +{ + game_object_frame() = default; + virtual ~game_object_frame() = default; + + sf::Uint32 game_object_id; + bt::game_object_type game_object_type; + sf::Vector2f position; + float rotation; + + sf::Vector2f velocity; + float velocity_angle; + + virtual void write_to_packet(sf::Packet& packet) const + { + packet << game_object_id; + packet << static_cast(game_object_type); + packet << position.x; + packet << position.y; + packet << rotation; + packet << velocity.x; + packet << velocity.y; + packet << velocity_angle; + } + + virtual void read_from_packet(sf::Packet& packet) + { + //packet >> game_object_id; + packet >> reinterpret_cast(game_object_type); + packet >> position.x; + packet >> position.y; + packet >> rotation; + packet >> velocity.x; + packet >> velocity.y; + packet >> velocity_angle; + } + +}; + +struct player_game_object_frame : game_object_frame +{ + virtual ~player_game_object_frame() override = default; + + float turret_rotation; + sf::Int32 health; + sf::Uint32 player_score; + + virtual void write_to_packet(sf::Packet& packet) const override + { + game_object_frame::write_to_packet(packet); + packet << turret_rotation; + packet << health; + packet << player_score; + } + + virtual void read_from_packet(sf::Packet& packet) override + { + game_object_frame::read_from_packet(packet); + packet >> turret_rotation; + packet >> health; + packet >> player_score; + } +}; + +struct game_frame_command final : server_command +{ + game_frame_command() : server_command(command_id_server::update_game_frame) + { + } + virtual ~game_frame_command() override = default; + + std::vector> game_objects; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << static_cast(game_objects.size()); + for (const auto& game_object : game_objects) + { + game_object->write_to_packet(packet); + } + } +}; + +struct deleted_game_object_command final : server_command +{ + explicit deleted_game_object_command(const sf::Uint32 game_object_id) + : server_command(command_id_server::delete_game_object), game_object_id{ game_object_id } + { + } + virtual ~deleted_game_object_command() override = default; + + sf::Uint32 game_object_id; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << game_object_id; + } +}; + +struct session_created_command : server_command +{ + session_created_command() : server_command(command_id_server::session_created) + { + } + virtual ~session_created_command() override = default; + + sf::Uint32 session_id; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << session_id; + } +}; + +struct player_joined_to_session_command : server_command +{ + player_joined_to_session_command() : server_command(command_id_server::player_joined_to_session) + { + } + virtual ~player_joined_to_session_command() override = default; + + sf::Uint32 session_id{}; + sf::Uint32 player_id{}; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << session_id; + packet << player_id; + } +}; + +struct session_started_command : server_command +{ + session_started_command() : server_command(command_id_server::session_started) + { + } + virtual ~session_started_command() override = default; + + sf::Uint32 session_id{}; + std::vector> game_objects; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << session_id; + packet << static_cast(game_objects.size()); + for (const auto& game_object : game_objects) + { + game_object->write_to_packet(packet); + } + } +}; + +struct player_shoot final : server_command +{ + player_shoot() : server_command(command_id_server::player_shoot) + { + } + virtual ~player_shoot() override = default; + + sf::Uint32 player_id{}; + game_object_frame game_object{}; + + void write_to_packet(sf::Packet& packet) const override + { + simple_command::write_to_packet(packet); + packet << player_id; + game_object.write_to_packet(packet); + } + + virtual void read_from_packet(sf::Packet& packet) override + { + //simple_command::read_from_packet(packet); + packet >> player_id; + game_object.read_from_packet(packet); + } +}; + +/////////////////////////////////// +struct connection_command_in +{ + sf::Uint32 connection_id; + //TODO:: remove + sf::Uint32 session_id; + std::shared_ptr packet; +}; + +struct connection_command_out +{ + std::vector connection_id; + std::unique_ptr command; +}; + diff --git a/battle_tanks/common_logic/headers/network/connection_base.hpp b/battle_tanks/common_logic/headers/network/connection_base.hpp new file mode 100644 index 0000000..aaed8eb --- /dev/null +++ b/battle_tanks/common_logic/headers/network/connection_base.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include + +#include "network/commands.hpp" +#include "data_structures/ts_deque.hpp" + + +class connection_base +{ +public: + connection_base(std::unique_ptr&& socket, const std::shared_ptr>& commands_in) + : socket_{ std::move(socket) }, commands_in_{ commands_in } + { + socket_->setBlocking(false); + } + + virtual ~connection_base() = default; + +public: + void set_session_id(const sf::Uint32 id); + sf::Uint32 get_session_id(); + sf::Uint32 get_connection_id() const; + + //void connect(const sf::Uint32 id); + void send(const sf::Packet& packet); + sf::Socket::Status update(); + +protected: + std::unique_ptr socket_; + ts_deque> packages_out_; + std::shared_ptr> commands_in_{ nullptr }; + sf::Uint32 connection_id_{}; + sf::Uint32 session_id_{}; +}; + +void connection_base::set_session_id(const sf::Uint32 id) +{ + session_id_ = id; +} + +sf::Uint32 connection_base::get_session_id() +{ + return session_id_; +} + +sf::Uint32 connection_base::get_connection_id() const +{ + return connection_id_; +} + +//void connection::connect(const sf::Uint32 id) +//{ +// connection_id_ = id; +// sf::Packet packet{}; +// constexpr auto command_id = static_cast(command_id_server::connection_established); +// packet << command_id << connection_id_; +// send(packet); +//} + +void connection_base::send(const sf::Packet& packet) +{ + packages_out_.push_back(std::make_unique(packet)); +} + +sf::Socket::Status connection_base::update() +{ + //receive packets + auto packet = sf::Packet{}; + auto status = socket_->receive(packet); + if (status == sf::Socket::Status::Done) + { + const connection_command_in command{ connection_id_, session_id_, std::make_shared(packet) }; + commands_in_->push_back(command); + } + + if (status == sf::Socket::Status::Disconnected) + { + return status; + } + + + //send packets + if (packages_out_.empty()) + { + return status; + } + + do + { + const auto& packet_out = packages_out_.front(); + + status = socket_->send(*packet_out); + if (status == sf::Socket::Status::Done) + { + packages_out_.pop_front(); + } + + if (status == sf::Socket::Status::Disconnected || status == sf::Socket::NotReady) + { + break; + } + } + while (status == sf::Socket::Status::Done && !packages_out_.empty()); + + return status; +} diff --git a/battle_tanks/common_logic/headers/network/player_actions.hpp b/battle_tanks/common_logic/headers/network/player_actions.hpp new file mode 100644 index 0000000..7798c2f --- /dev/null +++ b/battle_tanks/common_logic/headers/network/player_actions.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + + +enum class player_action : sf::Uint32 +{ + unknown = 0, + move_forward, + move_backward, + stop_move, + + turn_left, + turn_right, + stop_turn, + + turn_turret_left, + turn_turret_right, + stop_turn_turret, + + turret_fire, + turret_stop_fire, +}; diff --git a/battle_tanks/common_logic/headers/physics/collision_listener.hpp b/battle_tanks/common_logic/headers/physics/collision_listener.hpp new file mode 100644 index 0000000..0e00570 --- /dev/null +++ b/battle_tanks/common_logic/headers/physics/collision_listener.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include + +#include "physics/game_object_b2d_link.hpp" + +namespace bt +{ + class collision_listener final : public b2ContactListener + { + public: + collision_listener() = default; + + virtual ~collision_listener() override + { + collision_handler_ = nullptr; + } + + public: + + void add_collision_handler(const std::function& collision_handler) + { + collision_handler_ = collision_handler; + } + + void BeginContact(b2Contact* contact) override + { + b2Fixture* fixture_a = contact->GetFixtureA(); + b2Fixture* fixture_b = contact->GetFixtureB(); + + b2Body* body_a = fixture_a->GetBody(); + b2Body* body_b = fixture_b->GetBody(); + const b2BodyUserData user_data_a = body_a->GetUserData(); + const b2BodyUserData user_data_b = body_b->GetUserData(); + + const game_object_b2d_link* link_a{ nullptr }; + const game_object_b2d_link* link_b{ nullptr }; + + if (user_data_a.pointer != 0) + { + link_a = reinterpret_cast(user_data_a.pointer); + } + + if (user_data_b.pointer != 0) + { + link_b = reinterpret_cast(user_data_b.pointer); + } + + if (collision_handler_) + { + collision_handler_( + link_a != nullptr ? *link_a : game_object_b2d_link{ game_object_type::unknown, 0 }, + link_b != nullptr ? *link_b : game_object_b2d_link{ game_object_type::unknown, 0 } + ); + } + } + + private: + std::function collision_handler_{}; + }; +} \ No newline at end of file diff --git a/battle_tanks/common_logic/headers/physics/game_object_b2d_link.hpp b/battle_tanks/common_logic/headers/physics/game_object_b2d_link.hpp new file mode 100644 index 0000000..7a81511 --- /dev/null +++ b/battle_tanks/common_logic/headers/physics/game_object_b2d_link.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "game/entity/game_object_type.hpp" +#include + +namespace bt +{ + struct game_object_b2d_link + { + game_object_type type{ game_object_type::unknown }; + bt::uuid id{ 0 }; + }; +} diff --git a/battle_tanks/common_logic/headers/physics/physics_body_factory.hpp b/battle_tanks/common_logic/headers/physics/physics_body_factory.hpp new file mode 100644 index 0000000..c5c92b3 --- /dev/null +++ b/battle_tanks/common_logic/headers/physics/physics_body_factory.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + + +namespace bt +{ + class physics_body_factory + { + public: + physics_body_factory(b2World* world) : world_{ world } + { + } + + ~physics_body_factory() = default; + + public: + + b2Body* create_body(const b2BodyDef& body_def) const + { + return world_->CreateBody(&body_def); + } + + void destroy_body(b2Body* body) const + { + if (body == nullptr) + { + return; + } + world_->DestroyBody(body); + } + + private: + b2World* world_; + }; +} \ No newline at end of file diff --git a/battle_tanks/common_logic/headers/utils/math.hpp b/battle_tanks/common_logic/headers/utils/math.hpp new file mode 100644 index 0000000..c4a799f --- /dev/null +++ b/battle_tanks/common_logic/headers/utils/math.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace bt +{ + constexpr float deg_to_rad_multiplier = static_cast(std::numbers::pi / 180.0); + constexpr float rad_to_deg_multiplier = static_cast(180.0 / std::numbers::pi); + + inline float deg_to_rad(const float rotation) + { + return deg_to_rad_multiplier * rotation; + } + + inline float rad_to_deg(const float rotation) + { + return rad_to_deg_multiplier * rotation; + } + + inline float rad_to_deg_360(const float rotation) + { + return std::fmod(rad_to_deg(rotation), 360); + } +} \ No newline at end of file diff --git a/battle_tanks/common_logic/headers/utils/uuid.hpp b/battle_tanks/common_logic/headers/utils/uuid.hpp new file mode 100644 index 0000000..c6b0764 --- /dev/null +++ b/battle_tanks/common_logic/headers/utils/uuid.hpp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace bt +{ + using uuid = std::uint32_t; + + uuid generate_uuid(); + +} diff --git a/battle_tanks/common_logic/sources/util/uuid.cpp b/battle_tanks/common_logic/sources/util/uuid.cpp new file mode 100644 index 0000000..6613624 --- /dev/null +++ b/battle_tanks/common_logic/sources/util/uuid.cpp @@ -0,0 +1,12 @@ +#include "utils/uuid.hpp" + +namespace +{ + bt::uuid current_uuid{ 1 }; +} + + +bt::uuid bt::generate_uuid() +{ + return current_uuid++; +} diff --git a/battle_tanks/server/CMakeLists.txt b/battle_tanks/server/CMakeLists.txt new file mode 100644 index 0000000..352ea18 --- /dev/null +++ b/battle_tanks/server/CMakeLists.txt @@ -0,0 +1,22 @@ +project(battle_tanks_server) + +file(GLOB_RECURSE SOURCES "./*.cpp") + +include_directories(${PROJECT_SOURCE_DIR}/headers) + +add_executable (${PROJECT_NAME} ${SOURCES}) + +if (CMAKE_VERSION VERSION_GREATER 3.12) + set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 20) +endif() + +target_include_directories(${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../external/headers + ${CMAKE_CURRENT_SOURCE_DIR}/../common_logic/headers +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + bt-common +) diff --git a/battle_tanks/server/battle_tanks_server.cpp b/battle_tanks/server/battle_tanks_server.cpp new file mode 100644 index 0000000..96ddca7 --- /dev/null +++ b/battle_tanks/server/battle_tanks_server.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "game_server.hpp" + +int main(int argc, char* argv[]) +{ + unsigned int port = 52000; + + if (argc > 1) { + if (strcmp(argv[1], "-port") == 0 && argc > 2) { + port = atoi(argv[2]); + } + } + + game_server server{}; + + if (server.start(port)) + { + std::cout << "Server started on port " << port << std::endl; + server.run_infinity_loop(100, 100, true); + } + + return 0; +} diff --git a/battle_tanks/server/headers/game_server.hpp b/battle_tanks/server/headers/game_server.hpp new file mode 100644 index 0000000..255946d --- /dev/null +++ b/battle_tanks/server/headers/game_server.hpp @@ -0,0 +1,209 @@ +#pragma once + +#include +#include +#include + +#include "SFML/Config.hpp" +#include "SFML/Network/Packet.hpp" + +#include "network/network_server.hpp" +#include "game_session.hpp" + + +class game_server +{ +public: + game_server() + { + commands_out_ = std::make_shared>(); + } + + ~game_server() + { + is_running_ = false; + if (thread_commands_send_.joinable()) + { + thread_commands_send_.join(); + } + } + +public: + bool start(const unsigned int port); + [[noreturn]] void run_infinity_loop(const size_t max_messages_in, const size_t max_messages_out, const bool wait_in); + +private: + void send_commands(); + + void create_game_session(const sf::Uint32 connection_id); + void join_game_session(const sf::Uint32 connection_id, const sf::Uint32 connection_session_id, const sf::Uint32 join_session_id); + +private: + bool is_running_{ false }; + size_t max_messages_out_{}; + std::unordered_map> sessions_{}; + std::unique_ptr network_server_{}; + std::shared_ptr> commands_out_{ nullptr }; + std::thread thread_commands_send_; + + std::unordered_map connection_to_session_link_{}; +}; + +bool game_server::start(const unsigned int port) +{ + network_server_ = std::make_unique(port); + + network_server_->register_client_command_handler(command_id_client::create_session, + [this](const sf::Uint32 connection_id, sf::Packet& packet) + { + create_game_session(connection_id); + }); + + network_server_->register_client_command_handler(command_id_client::join_session, + [this](const sf::Uint32 connection_id, sf::Packet& packet) + { + sf::Uint32 join_session_id; + packet >> join_session_id; + + sf::Uint32 current_session_id{}; + if (connection_to_session_link_.contains(connection_id)) + { + current_session_id = connection_to_session_link_.at(connection_id); + } + join_game_session(connection_id, current_session_id, join_session_id); + }); + + network_server_->register_client_command_handler(command_id_client::player_action, + [this](const sf::Uint32 connection_id, sf::Packet& packet) + { + sf::Uint32 action_rough; + packet >> action_rough; + + const auto action = static_cast(action_rough); + + sf::Uint32 session_id{}; + if (connection_to_session_link_.contains(connection_id)) + { + session_id = connection_to_session_link_.at(connection_id); + } + //TODO:: else send error to client + + if (const auto session = sessions_.find(session_id); session != sessions_.end()) + { + switch (action) + { + case player_action::move_forward: + case player_action::move_backward: + case player_action::stop_move: + session->second->move_player(connection_id, action); + break; + case player_action::turn_left: + case player_action::turn_right: + case player_action::stop_turn: + session->second->rotate_player(connection_id, action); + break; + case player_action::turn_turret_left: + case player_action::turn_turret_right: + case player_action::stop_turn_turret: + session->second->rotate_player_turret(connection_id, action); + break; + case player_action::turret_fire: + case player_action::turret_stop_fire: + session->second->player_turret_fire(connection_id, action); + break; + case player_action::unknown: + default: + //TODO:: send error to client + ; + } + } + + }); + + network_server_->register_client_command_handler(command_id_client::lost_connection, + [this](const sf::Uint32 connection_id, sf::Packet& packet) + { + if (connection_to_session_link_.contains(connection_id)) + { + const auto session_id = connection_to_session_link_.at(connection_id); + if (sessions_.contains(session_id)) + { + const auto& session = sessions_.at(session_id); + session->player_lost_connection(connection_id); + if (session->get_state() == game_session_state::finished) + { + sessions_.erase(session_id); + } + } + connection_to_session_link_.erase(connection_id); + } + }); + + return network_server_->start(); +} + +[[noreturn]] void game_server::run_infinity_loop(const size_t max_messages_in, const size_t max_messages_out, const bool wait_in) +{ + is_running_ = true; + max_messages_out_ = max_messages_out; + thread_commands_send_ = std::thread(&game_server::send_commands, this); + while (true) + { + network_server_->update(max_messages_in, wait_in); + } +} + +void game_server::send_commands() +{ + //TODO::check sessions state and delete finished or broken + while (is_running_) + { + commands_out_->wait(); + + size_t message_count = 0; + while (message_count < max_messages_out_ && !commands_out_->empty()) + { + auto [connection_ids, command] = commands_out_->pop_front(); + network_server_->send_command(connection_ids, command); + message_count++; + } + } +} + +void game_server::create_game_session(const sf::Uint32 connection_id) +{ + if (const auto session_pair = sessions_.find(connection_id); session_pair == sessions_.end()) + { + const auto session_id = bt::generate_uuid(); + sessions_[session_id] = std::make_unique(session_id, commands_out_); + + auto command = std::make_unique(); + command->session_id = session_id; + + network_server_->send_command(connection_id, std::move(command)); + + //sessions_[session_id]->join_player(connection_id); + } +} + +void game_server::join_game_session(const sf::Uint32 connection_id, const sf::Uint32 connection_session_id, const sf::Uint32 join_session_id) +{ + //TODO:: player has no active sessions + if (connection_session_id == 0) + { + if (const auto session_pair = sessions_.find(join_session_id); session_pair != sessions_.end()) + { + const auto& session = session_pair->second; + + if (session->get_state() == game_session_state::waiting_for_players) + { + if (session->join_player(connection_id)) + { + connection_to_session_link_[connection_id] = join_session_id; + } + // TODO:: else{} send error + } + } + + } +} diff --git a/battle_tanks/server/headers/game_session.hpp b/battle_tanks/server/headers/game_session.hpp new file mode 100644 index 0000000..b12d783 --- /dev/null +++ b/battle_tanks/server/headers/game_session.hpp @@ -0,0 +1,345 @@ +#pragma once + +#include + +#include "SFML/System/Clock.hpp" +#include "box2d/b2_world.h" + +#include "game/entity/game_object_entity.hpp" +#include "game/entity/bullet_game_object_entity.hpp" +#include "game/entity/player_game_object_entity.hpp" +#include "game/entity/game_map_entity.hpp" + +#include "network/commands.hpp" +#include "physics/collision_listener.hpp" + + +enum class game_session_type : sf::Uint32 +{ + unknown = 0, + pvp_1x1, + team_2x2, + death_match_4, +}; + +enum class game_session_state : unsigned +{ + unknown = 0, + waiting_for_players, + starting, + game_play_progress, + finished +}; + +class game_session +{ +public: + explicit game_session(const sf::Uint32 session_id, const std::shared_ptr>& commands_out) + : commands_out_{ commands_out }, session_id_{ session_id } + { + game_objects_ = std::unordered_map>{}; + physics_world_.SetAllowSleeping(true); + //physics_world.SetContinuousPhysics(true); + physics_world_.SetContactListener(&contact_listener_); + + contact_listener_.add_collision_handler( + [this](const bt::game_object_b2d_link game_object_a, const bt::game_object_b2d_link game_object_b) + { + if (game_object_a.type == bt::game_object_type::bullet) + { + objects_to_delete_.insert(game_object_a.id); + } + else if (game_object_b.type == bt::game_object_type::bullet) + { + objects_to_delete_.insert(game_object_b.id); + } + + //TODO:: check if it is bullet and read damage from bullet, depending on weapon type when shot was made + if (game_object_a.type == bt::game_object_type::tank) + { + players_took_damage_.emplace(game_object_a.id, bt::game_entity_consts::bullet_damage); + } + if (game_object_b.type == bt::game_object_type::tank) + { + players_took_damage_.emplace(game_object_b.id, bt::game_entity_consts::bullet_damage); + } + + }); + + state_ = game_session_state::waiting_for_players; + + physics_body_factory_ = std::make_shared(&physics_world_); + } + + ~game_session() + { + state_ = game_session_state::finished; + if (thread_game_loop_.joinable()) + { + thread_game_loop_.join(); + } + } + + sf::Uint32 get_session_id() const + { + return session_id_; + } + + game_session_state get_state() const + { + return state_; + } + + const std::vector& get_players_ids() const + { + return players_ids_; + } + + bool join_player(sf::Uint32 player_id); + + void move_player(const sf::Uint32 player_id, const player_action action) + { + std::lock_guard lock{ mutex_game_loop_ }; + if (const auto controller = get_player_action_controller(player_id)) + { + controller->set_move_action(action); + } + } + + void rotate_player(const sf::Uint32 player_id, const player_action action) + { + std::lock_guard lock{ mutex_game_loop_ }; + if (const auto controller = get_player_action_controller(player_id)) + { + controller->set_rotate_action(action); + } + } + + void rotate_player_turret(const sf::Uint32 player_id, const player_action action) + { + std::lock_guard lock{ mutex_game_loop_ }; + if (const auto controller = get_player_action_controller(player_id)) + { + controller->set_turret_rotate_action(action); + } + } + + void player_turret_fire(const sf::Uint32 player_id, const player_action action) + { + std::lock_guard lock{ mutex_game_loop_ }; + if (const auto controller = get_player_action_controller(player_id)) + { + if (controller->set_fire_action(action)) + { + const auto& player_controller = players_action_controllers_.at(player_id); + + if (player_controller.expired()) + { + return; + } + const auto controller_ptr = player_controller.lock(); + + auto bullet_id = bt::generate_uuid(); + auto bullet = std::make_unique(bullet_id, physics_body_factory_); + + bullet->create_physics_body( + controller_ptr->get_position(), + controller_ptr->get_absolute_turret_rotation(), + 50.0f // m/s + ); + + auto shot_command = std::make_unique(); + shot_command->player_id = player_id; + shot_command->game_object = *bullet->get_frame(); + + game_objects_.emplace(bullet_id, std::move(bullet)); + commands_out_->push_back({ players_ids_,std::move(shot_command) }); + } + } + } + + void player_lost_connection(const bt::uuid player_id) + { + std::lock_guard lock{ mutex_game_loop_ }; + if (players_action_controllers_.contains(player_id)) + { + players_action_controllers_.erase(player_id); + game_objects_.erase(player_id); + std::erase(players_ids_, player_id); + } + if (players_ids_.empty()) + { + state_ = game_session_state::finished; + } + } + +private: + void update(); + + void delete_game_objects(); + void send_game_session_frame(const float delta_time); + + + std::shared_ptr get_player_action_controller(const sf::Uint32 player_id) + { + //std::lock_guard lock{ mutex_game_loop_ }; + if (const auto action_controller = players_action_controllers_.find(player_id); action_controller != players_action_controllers_.end()) + { + if (action_controller->second.expired()) + { + players_action_controllers_.erase(player_id); + } + else + { + return action_controller->second.lock(); + } + } + return nullptr; + } + +private: + std::shared_ptr> commands_out_{ nullptr }; + + std::thread thread_game_loop_; + std::mutex mutex_game_loop_; + + sf::Uint32 session_id_{}; + game_session_state state_{}; + + b2World physics_world_ = b2World{ b2Vec2{ 0.0f, 0.0f } }; + bt::collision_listener contact_listener_{}; + std::shared_ptr physics_body_factory_{ nullptr }; + + std::unique_ptr game_map_{ nullptr }; + + std::unordered_set objects_to_delete_{}; + std::unordered_map> game_objects_{}; + std::unordered_map> players_action_controllers_{}; + + std::vector players_ids_{}; + std::unordered_map players_took_damage_{}; +}; + +void game_session::update() +{ + sf::Clock clock; + constexpr int count_down_to_start_milliseconds{ 3000 }; + int current_count_down_time{ count_down_to_start_milliseconds }; + while (state_ != game_session_state::finished) + { + if (state_ == game_session_state::starting) + { + current_count_down_time -= clock.restart().asMilliseconds(); + if (current_count_down_time <= 0) + { + game_map_ = std::make_unique(0, bt::physics_consts::game_field_size, physics_body_factory_); + game_map_->build_map(); + game_map_->create_rock({ 20.0f, 20.0f }, { 5.7f, 4.2f }); + + state_ = game_session_state::game_play_progress; + + auto command = std::make_unique(); + command->session_id = session_id_; + + for (auto player_id : players_ids_) + { + command->game_objects.push_back(game_objects_.at(player_id)->get_frame()); + } + + commands_out_->push_back({ players_ids_, std::move(command) }); + continue; + } + } + + if (state_ == game_session_state::game_play_progress) + { + std::lock_guard lock{ mutex_game_loop_ }; + delete_game_objects(); + const float delta_time = clock.restart().asSeconds(); + physics_world_.Step(delta_time, 2, 1); + send_game_session_frame(delta_time); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 60)); + } +} + +void game_session::delete_game_objects() +{ + for (const auto& game_object_id : objects_to_delete_) + { + game_objects_.erase(game_object_id); + commands_out_->push_back({ + players_ids_, + std::make_unique(game_object_id) + }); + } + objects_to_delete_.clear(); +} + +void game_session::send_game_session_frame(const float delta_time) +{ + auto command = std::make_unique(); + for (const auto& game_object : game_objects_ | std::views::values) + { + auto game_object_id = game_object->get_id(); + if (players_took_damage_.contains(game_object_id)) + { + auto* player_ptr = dynamic_cast(game_objects_.at(game_object_id).get()); + player_ptr->take_damage(players_took_damage_.at(game_object_id)); + if (player_ptr->get_health() <= 0) + { + objects_to_delete_.insert(game_object_id); + } + } + + game_object->update(delta_time); + + command->game_objects.push_back(game_object->get_frame()); + if (game_object->is_out_of_edges(bt::physics_consts::game_field_size)) + { + objects_to_delete_.insert(game_object->get_id()); + } + } + commands_out_->push_back({ players_ids_, std::move(command) }); + players_took_damage_.clear(); +} + +bool game_session::join_player(sf::Uint32 player_id) +{ + if (state_ != game_session_state::waiting_for_players) + { + return false; + } + + std::lock_guard lock{ mutex_game_loop_ }; + + auto player_entity = std::make_unique(player_id, physics_body_factory_); + + //TODO:: new player state configuration + if (players_ids_.empty()) + { + player_entity->create_physics_body({ 10.0f, 10.0f }); + } + else + { + player_entity->create_physics_body({ 40.0f, 40.0f }); + } + + players_action_controllers_.emplace(player_id, player_entity->get_action_controller()); + game_objects_.emplace(player_id, std::move(player_entity)); + players_ids_.push_back(player_id); + + auto joined_command = std::make_unique(); + joined_command->session_id = session_id_; + joined_command->player_id = player_id; + commands_out_->push_back({ players_ids_, std::move(joined_command) }); + + if (players_ids_.size() == 2) + { + state_ = game_session_state::starting; + thread_game_loop_ = std::thread{ &game_session::update, this }; + } + + return true; +} diff --git a/battle_tanks/server/headers/network/network_server.hpp b/battle_tanks/server/headers/network/network_server.hpp new file mode 100644 index 0000000..8bd08d2 --- /dev/null +++ b/battle_tanks/server/headers/network/network_server.hpp @@ -0,0 +1,219 @@ +#pragma once + +#include +#include +#include + +#include + +#include "network/server_connection.hpp" +#include "network/commands.hpp" +#include "utils/uuid.hpp" + +using client_command_handler = std::function; +//template +//using client_command_handler = std::function; + +class network_server +{ +public: + network_server(const unsigned short port) : port_{ port } + {} + + ~network_server() + { + stop(); + } + + bool start(); + void stop(); + void update(size_t max_messages = -1, bool wait = false); + + void send_command(const sf::Uint32 connection_id, const std::unique_ptr& command); + void send_command(const std::vector& connection_id, const std::unique_ptr& command); + + void register_client_command_handler(const command_id_client command_id, const client_command_handler& handler); + +private: + void wait_for_client_connection(); + sf::Uint32 generate_client_id(); + void on_client_message(const sf::Uint32 connection_id, const std::shared_ptr& packet); + void update_connection(); + +private: + unsigned short port_{}; + bool is_running_{ false }; + + std::thread thread_accept_awaiter_; + std::thread thread_connections_worker_; + std::mutex mutex_connections_; + + std::unique_ptr listener_{ nullptr }; + + std::unordered_map> connections_{}; + + std::shared_ptr> commands_in_{ nullptr }; + + std::unordered_map client_command_handlers_; + +}; + +bool network_server::start() +{ + commands_in_ = std::make_shared>(); + listener_ = std::make_unique(); + + if (listener_->listen(port_) != sf::Socket::Done) + { + return false; + } + + is_running_ = true; + thread_accept_awaiter_ = std::thread{ &network_server::wait_for_client_connection, this }; + thread_connections_worker_ = std::thread{ &network_server::update_connection, this }; + + + return true; +} + +void network_server::stop() +{ + is_running_ = false; + if (thread_accept_awaiter_.joinable()) + { + thread_accept_awaiter_.join(); + } + if (thread_connections_worker_.joinable()) + { + thread_connections_worker_.join(); + } +} + +void network_server::update(const size_t max_messages, const bool wait) +{ + if (wait) + { + commands_in_->wait(); + } + + size_t message_count = 0; + while (message_count < max_messages && !commands_in_->empty()) + { + auto [client_id, session_id, packet] = commands_in_->pop_front(); + + on_client_message(client_id, packet); + + message_count++; + } +} + +void network_server::send_command(const sf::Uint32 connection_id, const std::unique_ptr& command) +{ + auto packet = sf::Packet{}; + command->write_to_packet(packet); + + std::lock_guard lock(mutex_connections_); + if (connections_.contains(connection_id)) + { + connections_[connection_id]->send(packet); + } +} + +void network_server::send_command(const std::vector& connection_id, const std::unique_ptr& command) +{ + auto packet = sf::Packet{}; + command->write_to_packet(packet); + + std::lock_guard lock(mutex_connections_); + for (const auto& id : connection_id) + { + if (connections_.contains(id)) + { + connections_[id]->send(packet); + } + } +} + +void network_server::register_client_command_handler(const command_id_client command_id, const client_command_handler& handler) +{ + client_command_handlers_[command_id] = handler; +} + +void network_server::on_client_message(const sf::Uint32 connection_id, const std::shared_ptr& packet) +{ + sf::Uint32 rough_cmd_id; + *packet >> rough_cmd_id; + + const auto command_id = static_cast<::command_id_client>(rough_cmd_id); + + if (client_command_handlers_.contains(command_id)) + { + client_command_handlers_[command_id](connection_id, *packet); + } + else + { + std::cout << "Unregistered command handler for command id: " << rough_cmd_id + << "; connection id: " << connection_id << std::endl; + } +} + +void network_server::update_connection() +{ + while (is_running_) + { + //TODO:: check connection without valuable packaged during some timeout and kill + std::lock_guard lock(mutex_connections_); + std::vector connections_to_remove; + for (const auto& connection : connections_ | std::views::values) + { + if (connection->update() == sf::Socket::Disconnected) + { + auto packet = sf::Packet{}; + const auto connection_id = connection->get_connection_id(); + const auto session_id = connection->get_session_id(); + + packet << static_cast(command_id_client::lost_connection); + packet << connection_id; + packet << session_id; + connection_command_in command + { + connection_id, + session_id, + std::make_shared(packet) + }; + + commands_in_->push_back(command); + connections_to_remove.push_back(connection_id); + } + } + + for (const auto connection_id : connections_to_remove) + { + connections_.erase(connection_id); + } + } +} + +void network_server::wait_for_client_connection() +{ + while (is_running_) + { + auto client_socket = std::make_unique(); + listener_->accept(*client_socket); + + std::cout << "Client connected: " << client_socket->getRemoteAddress().toString() << std::endl; + + const auto client_connection = std::make_shared(std::move(client_socket), commands_in_); + const sf::Uint32 connection_id = generate_client_id(); + + client_connection->connect(connection_id); + + std::lock_guard lock(mutex_connections_); + connections_.emplace(connection_id, client_connection); + } +} + +sf::Uint32 network_server::generate_client_id() +{ + return bt::generate_uuid(); +} diff --git a/battle_tanks/server/headers/network/server_connection.hpp b/battle_tanks/server/headers/network/server_connection.hpp new file mode 100644 index 0000000..df6c7c2 --- /dev/null +++ b/battle_tanks/server/headers/network/server_connection.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "network/commands.hpp" +#include "network/connection_base.hpp" + + +class server_connection : public connection_base +{ +public: + server_connection(std::unique_ptr&& socket, const std::shared_ptr>& commands_in) + : connection_base{ std::move(socket) , commands_in } + { + } + + virtual ~server_connection() override = default; + +public: + void connect(const sf::Uint32 id); +}; + +void server_connection::connect(const sf::Uint32 id) +{ + connection_id_ = id; + sf::Packet packet{}; + constexpr auto command_id = static_cast(command_id_server::connection_established); + packet << command_id << connection_id_; + send(packet); +} diff --git a/gitattributes b/gitattributes index 1c42e45..ba50d55 100644 --- a/gitattributes +++ b/gitattributes @@ -3,3 +3,5 @@ *.[pP][dD][bB] filter=lfs diff=lfs merge=lfs -text *.[d][lL][lL] filter=lfs diff=lfs merge=lfs -text *.[lL][iI][bB] filter=lfs diff=lfs merge=lfs -text + +*.[pP][nN][gG] filter=lfs diff=lfs merge=lfs -text diff --git a/vcpkg.json b/vcpkg.json index b246429..63814f3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "cpp-gamedev-manifest", "version-string": "0.1.0", "dependencies": [ @@ -8,6 +8,9 @@ }, { "name": "sfml" + }, + { + "name": "box2d" } ] } diff --git a/vcpkg_r_d_cpp_gamedev b/vcpkg_r_d_cpp_gamedev index 26588c0..486a464 160000 --- a/vcpkg_r_d_cpp_gamedev +++ b/vcpkg_r_d_cpp_gamedev @@ -1 +1 @@ -Subproject commit 26588c07d59311e9c7959e157faacc00e0c5fb8f +Subproject commit 486a4640db740f5994e492eb60748111dfc48de7