From 930226cc4722ba4d8106aa17cb0d2bd2b6085559 Mon Sep 17 00:00:00 2001 From: zhenyan121 <3367366583@qq.com> Date: Sat, 25 Jul 2026 17:47:13 +0800 Subject: [PATCH] feat(gameplay): implement entity movement system with collision detection - Add Velocity and HitBoxes components to entity - Introduce HitboxManager for loading per-entity collision AABBs from JSON - Create MoveSystem with per-axis collision handling - Move get_block_aabb to base World class and add virtual get_per_tick_time - Remove static get_block_aabb from ClientWorld; use member m_per_tick_time for tick duration --- include/Cubed/gameplay/client_world.hpp | 4 +- include/Cubed/gameplay/entity.hpp | 11 ++ include/Cubed/gameplay/hitbox_manager.hpp | 24 +++++ include/Cubed/gameplay/move_system.hpp | 20 ++++ include/Cubed/gameplay/server_world.hpp | 2 +- include/Cubed/gameplay/world.hpp | 9 ++ src/CMakeLists.txt | 2 + src/gameplay/client_world.cpp | 14 +-- src/gameplay/hitbox_manager.cpp | 63 +++++++++++ src/gameplay/move_system.cpp | 123 ++++++++++++++++++++++ src/gameplay/server_world.cpp | 4 +- 11 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 include/Cubed/gameplay/hitbox_manager.hpp create mode 100644 include/Cubed/gameplay/move_system.hpp create mode 100644 src/gameplay/hitbox_manager.cpp create mode 100644 src/gameplay/move_system.cpp diff --git a/include/Cubed/gameplay/client_world.hpp b/include/Cubed/gameplay/client_world.hpp index 525d171..7af32c1 100644 --- a/include/Cubed/gameplay/client_world.hpp +++ b/include/Cubed/gameplay/client_world.hpp @@ -91,7 +91,7 @@ public: void request_exit(); bool is_receive_exit(); int chunk_size() const; - static AABB get_block_aabb(const glm::ivec3& pos); + AudioEngine& get_audio(); const AudioEngine& get_audio() const; Config& get_config(); @@ -103,6 +103,7 @@ public: void receive_voice_message(VoiceMsg& msg); bool enable_voice_chat() const; const entt::registry& get_registry(); + int get_per_tick_time() const override; template void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) { m_ticktimers.emplace( @@ -181,6 +182,7 @@ private: std::atomic m_rendering_distance{24}; std::atomic m_game_ticks{0}; std::atomic m_day_tick{6000}; + std::atomic m_per_tick_time = DEFAULT_PER_TICK_TIME; std::atomic m_requesting_chunk{false}; std::atomic m_is_rebuilding{false}; std::atomic m_chunk_task_id{0}; diff --git a/include/Cubed/gameplay/entity.hpp b/include/Cubed/gameplay/entity.hpp index fd27395..d6a4a5f 100644 --- a/include/Cubed/gameplay/entity.hpp +++ b/include/Cubed/gameplay/entity.hpp @@ -1,4 +1,5 @@ #pragma once +#include "Cubed/AABB.hpp" #include "Cubed/gameplay/player.hpp" #include @@ -34,4 +35,14 @@ struct ViewAngles { float angle = 0.0f; }; +struct Velocity { + float dx = 0.0f; + float dy = 0.0f; + float dz = 0.0f; +}; + +struct HitBoxes { + std::vector boxex; +}; + } // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/gameplay/hitbox_manager.hpp b/include/Cubed/gameplay/hitbox_manager.hpp new file mode 100644 index 0000000..d62f0e5 --- /dev/null +++ b/include/Cubed/gameplay/hitbox_manager.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "Cubed/AABB.hpp" + +#include +namespace Cubed { +class HitboxManager { +public: + HitboxManager(); + ~HitboxManager(); + static HitboxManager& instance(); + + AABB get_aabb(const std::string& key); + + static AABB aabb(const std::string& key); + +private: + using HitBoxMap = tbb::concurrent_hash_map; + using cacc = HitBoxMap::const_accessor; + using acc = HitBoxMap::accessor; + HitBoxMap m_hitboxes; + + AABB load(const std::string& path); +}; +} // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/gameplay/move_system.hpp b/include/Cubed/gameplay/move_system.hpp new file mode 100644 index 0000000..a5fee1c --- /dev/null +++ b/include/Cubed/gameplay/move_system.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "Cubed/gameplay/entity.hpp" +#include "Cubed/gameplay/world.hpp" + +#include +namespace Cubed { + +class MoveSystem { +public: + static void update(World& world, entt::registry& registry); + +private: + static void move_x(World& world, Transform& transform, Velocity& v, + const EntityInfo& info); + static void move_y(World& world, Transform& transform, Velocity& v, + const EntityInfo& info); + static void move_z(World& world, Transform& transform, Velocity& v, + const EntityInfo& info); +}; +} // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/gameplay/server_world.hpp b/include/Cubed/gameplay/server_world.hpp index bb1ac41..da6ddb0 100644 --- a/include/Cubed/gameplay/server_world.hpp +++ b/include/Cubed/gameplay/server_world.hpp @@ -91,7 +91,7 @@ public: bool is_solid(const glm::ivec3& block_pos) const override; bool can_pass_block(const glm::ivec3& block_pos) const override; BlockType get_block_tpye(const glm::ivec3& block_pos) const override; - + int get_per_tick_time() const override; template void register_timer(std::string_view id, TickType threshold, Fn&& f) { m_timers.emplace(std::piecewise_construct, diff --git a/include/Cubed/gameplay/world.hpp b/include/Cubed/gameplay/world.hpp index 84dc4df..038cee1 100644 --- a/include/Cubed/gameplay/world.hpp +++ b/include/Cubed/gameplay/world.hpp @@ -1,4 +1,5 @@ #pragma once +#include "Cubed/AABB.hpp" #include "Cubed/gameplay/block.hpp" #include @@ -16,5 +17,13 @@ public: virtual bool is_solid(const glm::ivec3& block_pos) const = 0; virtual bool can_pass_block(const glm::ivec3& block_pos) const = 0; virtual BlockType get_block_tpye(const glm::ivec3& block_pos) const = 0; + virtual int get_per_tick_time() const = 0; + + static AABB get_block_aabb(const glm::ivec3& pos) { + return {glm::vec3{static_cast(pos.x) + 0.5f, + static_cast(pos.y) + 0.5f, + static_cast(pos.z) + 0.5f}, + glm::vec3{0.5f, 0.5f, 0.5f}}; + } }; } // namespace Cubed \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6ea590..22c4686 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -96,4 +96,6 @@ target_sources(${PROJECT_NAME} render/model_manager.cpp render/model_renderer.cpp gameplay/chunk.cpp + gameplay/move_system.cpp + gameplay/hitbox_manager.cpp ) \ No newline at end of file diff --git a/src/gameplay/client_world.cpp b/src/gameplay/client_world.cpp index 4ab4078..ee4702e 100644 --- a/src/gameplay/client_world.cpp +++ b/src/gameplay/client_world.cpp @@ -505,7 +505,7 @@ void ClientWorld::start_client_thread(std::string_view uuid) { // Wait for 20 ticks, after the server's central chunk is generated, then // request chunks - std::this_thread::sleep_for(milliseconds(20 * DEFAULT_PER_TICK_TIME)); + std::this_thread::sleep_for(milliseconds(20 * m_per_tick_time)); request_chunk(); } @@ -557,7 +557,7 @@ void ClientWorld::client_run(std::stop_token stoken) { Logger::info("Client Thread Started"); using Clock = std::chrono::steady_clock; - constexpr auto TICK = std::chrono::milliseconds(DEFAULT_PER_TICK_TIME); + const auto TICK = std::chrono::milliseconds(m_per_tick_time); auto next = Clock::now(); while (!stoken.stop_requested()) { @@ -730,13 +730,6 @@ bool ClientWorld::is_receive_exit() { return m_receive_exit; } int ClientWorld::chunk_size() const { return m_chunks.size(); } -AABB ClientWorld::get_block_aabb(const glm::ivec3& pos) { - return {glm::vec3{static_cast(pos.x) + 0.5f, - static_cast(pos.y) + 0.5f, - static_cast(pos.z) + 0.5f}, - glm::vec3{0.5f, 0.5f, 0.5f}}; -} - AudioEngine& ClientWorld::get_audio() { return m_audio; } const AudioEngine& ClientWorld::get_audio() const { return m_audio; } Config& ClientWorld::get_config() { return m_config; } @@ -755,7 +748,7 @@ void ClientWorld::request_exit() { if (m_client->is_connect_error() || m_exit_direct) { break; } - std::this_thread::sleep_for(milliseconds(DEFAULT_PER_TICK_TIME)); + std::this_thread::sleep_for(milliseconds(m_per_tick_time)); ++cnt; if (cnt >= WORLD_EXIT_TIMEOUT) { Logger::warn("Can't Receive Server Exit Sign"); @@ -780,6 +773,7 @@ void ClientWorld::receive_voice_message(VoiceMsg& msg) { } bool ClientWorld::enable_voice_chat() const { return m_voice_chat.load(); } const entt::registry& ClientWorld::get_registry() { return m_registry; } +int ClientWorld::get_per_tick_time() const { return m_per_tick_time; } void ClientWorld::send_chat_message(ChatMessage& message) { Arena arena; auto msg = Arena::Create(&arena); diff --git a/src/gameplay/hitbox_manager.cpp b/src/gameplay/hitbox_manager.cpp new file mode 100644 index 0000000..02e1ed3 --- /dev/null +++ b/src/gameplay/hitbox_manager.cpp @@ -0,0 +1,63 @@ +#include "Cubed/gameplay/hitbox_manager.hpp" + +#include "Cubed/tools/log.hpp" + +#include +namespace fs = std::filesystem; +using nlohmann::json; +namespace Cubed { +HitboxManager::HitboxManager() {} +HitboxManager::~HitboxManager() {} + +HitboxManager& HitboxManager::instance() { + static HitboxManager inst; + return inst; +} + +AABB HitboxManager::aabb(const std::string& key) { + return instance().get_aabb(key); +} + +AABB HitboxManager::get_aabb(const std::string& key) { + { + cacc c; + if (m_hitboxes.find(c, key)) { + return c->second; + } + } + + return load(key); +} + +AABB HitboxManager::load(const std::string& path) { + fs::path p = ASSETS_PATH + path; + try { + glm::vec3 center; + glm::vec3 half; + std::ifstream s{p}; + json j = json::parse(s); + if (j.contains("boxes")) { + if (j["boxes"].contains("center")) { + center.x = j["boxes"]["center"].at(0).get(); + center.y = j["boxes"]["center"].at(1).get(); + center.z = j["boxes"]["center"].at(2).get(); + } + if (j["boxes"].contains("half")) { + half.x = j["boxes"]["half"].at(0).get(); + half.y = j["boxes"]["half"].at(1).get(); + half.z = j["boxes"]["half"].at(2).get(); + } + } + acc a; + if (m_hitboxes.insert(a, path)) { + a->second = AABB{center, half}; + return a->second; + } + } catch (const std::exception& e) { + Logger::error("Load Hitbox error {}", e.what()); + } + Logger::error("Load hitbox {} Failed", path); + return AABB{glm::vec3(0.0f), glm::vec3(0.0f)}; +} + +} // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/move_system.cpp b/src/gameplay/move_system.cpp new file mode 100644 index 0000000..2cac70f --- /dev/null +++ b/src/gameplay/move_system.cpp @@ -0,0 +1,123 @@ +#include "Cubed/gameplay/move_system.hpp" + +#include "Cubed/gameplay/entity.hpp" +#include "Cubed/gameplay/hitbox_manager.hpp" +namespace Cubed { +void MoveSystem::update(World& world, entt::registry& registry) { + auto view = registry.view(); + for (auto entity : view) { + auto [transform, v, info] = + view.get(entity); + move_x(world, transform, v, info); + move_y(world, transform, v, info); + move_z(world, transform, v, info); + } +} + +void MoveSystem::move_x(World& world, Transform& transform, Velocity& v, + const EntityInfo& info) { + + auto& pos = transform.pos; + float distance = v.dx * world.get_per_tick_time() / 1000.0f; + pos.x += distance; + + AABB box = HitboxManager::aabb( + std::format("model/creature/{}/collision.json", info.name)); + glm::vec3 min = box.min(); + glm::vec3 max = box.max(); + + int minx = std::floor(min.x); + int maxx = std::floor(max.x); + int miny = std::floor(min.y); + int maxy = std::floor(max.y); + int minz = std::floor(min.z); + int maxz = std::floor(max.z); + + for (int x = minx; x <= maxx; ++x) { + for (int y = miny; y <= maxy; ++y) { + for (int z = minz; z <= maxz; ++z) { + glm::ivec3 block_pos{x, y, z}; + if (!world.can_pass_block(block_pos)) { + AABB block_box = World::get_block_aabb(block_pos); + if (box.intersects(block_box)) { + pos.x -= distance; + v.dx = 0.0f; + return; + } + } + } + } + } +} + +void MoveSystem::move_y(World& world, Transform& transform, Velocity& v, + const EntityInfo& info) { + auto& pos = transform.pos; + float distance = v.dy * world.get_per_tick_time() / 1000.0f; + pos.y += distance; + + AABB box = HitboxManager::aabb( + std::format("model/creature/{}/collision.json", info.name)); + glm::vec3 min = box.min(); + glm::vec3 max = box.max(); + + int minx = std::floor(min.x); + int maxx = std::floor(max.x); + int miny = std::floor(min.y); + int maxy = std::floor(max.y); + int minz = std::floor(min.z); + int maxz = std::floor(max.z); + + for (int x = minx; x <= maxx; ++x) { + for (int y = miny; y <= maxy; ++y) { + for (int z = minz; z <= maxz; ++z) { + glm::ivec3 block_pos{x, y, z}; + if (!world.can_pass_block(block_pos)) { + AABB block_box = World::get_block_aabb(block_pos); + if (box.intersects(block_box)) { + pos.y -= distance; + v.dy = 0.0f; + return; + } + } + } + } + } +} + +void MoveSystem::move_z(World& world, Transform& transform, Velocity& v, + const EntityInfo& info) { + auto& pos = transform.pos; + float distance = v.dz * world.get_per_tick_time() / 1000.0f; + pos.z += distance; + + AABB box = HitboxManager::aabb( + std::format("model/creature/{}/collision.json", info.name)); + glm::vec3 min = box.min(); + glm::vec3 max = box.max(); + + int minx = std::floor(min.x); + int maxx = std::floor(max.x); + int miny = std::floor(min.y); + int maxy = std::floor(max.y); + int minz = std::floor(min.z); + int maxz = std::floor(max.z); + + for (int x = minx; x <= maxx; ++x) { + for (int y = miny; y <= maxy; ++y) { + for (int z = minz; z <= maxz; ++z) { + glm::ivec3 block_pos{x, y, z}; + if (!world.can_pass_block(block_pos)) { + AABB block_box = World::get_block_aabb(block_pos); + if (box.intersects(block_box)) { + pos.z -= distance; + v.dz = 0.0f; + return; + } + } + } + } + } +} + +} // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/server_world.cpp b/src/gameplay/server_world.cpp index ff9f55c..bfb66ac 100644 --- a/src/gameplay/server_world.cpp +++ b/src/gameplay/server_world.cpp @@ -454,7 +454,7 @@ void ServerWorld::serever_run(std::stop_token stoken) { Logger::info("Server Thread Started!"); using Clock = std::chrono::steady_clock; - constexpr auto TICK = std::chrono::milliseconds(DEFAULT_PER_TICK_TIME); + const auto TICK = std::chrono::milliseconds(m_per_tick_time); auto next = Clock::now(); while (!stoken.stop_requested()) { @@ -1074,4 +1074,6 @@ BlockType ServerWorld::get_block_tpye(const glm::ivec3& block_pos) const { return chunk_blocks[Chunk::index(x, y, z)]; } +int ServerWorld::get_per_tick_time() const { return m_per_tick_time; } + } // namespace Cubed \ No newline at end of file