From 4a1ef542581f8a336dc27403fd00e750696ae680 Mon Sep 17 00:00:00 2001 From: zhenyan121 <3367366583@qq.com> Date: Thu, 25 Jun 2026 16:49:45 +0800 Subject: [PATCH] feat(gameplay): integrate network client and thread-safe player pos --- include/Cubed/gameplay/client_player.hpp | 16 +- include/Cubed/gameplay/client_world.hpp | 29 ++- include/Cubed/gameplay/game_time.hpp | 32 ++- include/Cubed/gameplay/network_client.hpp | 5 +- src/gameplay/client_chunk.cpp | 1 + src/gameplay/client_player.cpp | 44 +++- src/gameplay/client_world.cpp | 243 ++++++++++++++++++++++ src/gameplay/network_client.cpp | 19 +- src/gameplay/server_world.cpp | 6 +- src/proto/common/chunk_pos.proto | 2 +- src/proto/packet.proto | 1 - src/proto/world/chunk_data.proto | 4 +- 12 files changed, 372 insertions(+), 30 deletions(-) diff --git a/include/Cubed/gameplay/client_player.hpp b/include/Cubed/gameplay/client_player.hpp index e53661a..6fae38a 100644 --- a/include/Cubed/gameplay/client_player.hpp +++ b/include/Cubed/gameplay/client_player.hpp @@ -8,18 +8,20 @@ #include #include +#include namespace Cubed { enum class Gait { WALK = 0, RUN }; class ClientWorld; class ClientPlayer { public: - ClientPlayer(ClientWorld& world, const std::string& name); + ClientPlayer(ClientWorld& world, std::string_view name); ~ClientPlayer(); AABB get_aabb() const; const glm::vec3& get_front() const; const Gait& get_gait() const; const std::optional& get_look_block_pos() const; - const glm::vec3& get_player_pos() const; + // thread safe + glm::vec3 get_player_pos() const; const MoveState& get_move_state() const; void change_mode(GameMode mode); @@ -48,6 +50,7 @@ public: void set_uuid(std::string_view uuid); const std::string& get_uuid() const; + const std::string& get_name() const; private: using enum GameMode; @@ -94,6 +97,9 @@ private: std::string m_name{}; std::string m_uuid; ClientWorld& m_world; + + std::shared_mutex m_player_pos_mutex; + bool ray_cast(const glm::vec3& start, const glm::vec3& dir, glm::ivec3& block_pos, glm::vec3& normal, float distance = 4.0f); @@ -101,8 +107,8 @@ private: void update_direction(); void update_lookup_block(); void update_move(float delta_time); - void update_x_move(); - void update_y_move(); - void update_z_move(); + void update_x_move(glm::vec3& player_pos); + void update_y_move(glm::vec3& player_pos); + void update_z_move(glm::vec3& player_pos); }; } // namespace Cubed diff --git a/include/Cubed/gameplay/client_world.hpp b/include/Cubed/gameplay/client_world.hpp index 908f176..addb1dc 100644 --- a/include/Cubed/gameplay/client_world.hpp +++ b/include/Cubed/gameplay/client_world.hpp @@ -4,6 +4,8 @@ #include "Cubed/gameplay/chunk_pos.hpp" #include "Cubed/gameplay/client_chunk.hpp" #include "Cubed/gameplay/client_player.hpp" +#include "Cubed/gameplay/game_time.hpp" +#include "Cubed/gameplay/network_client.hpp" #include #include @@ -11,7 +13,8 @@ namespace Cubed { class ClientWorld { public: - ClientWorld(); + ClientWorld(std::string_view player_name, + std::shared_ptr client); ~ClientWorld(); void init(); void update(float delta_time); @@ -32,16 +35,23 @@ public: int rendering_distance() const; void rendering_distance(int rendering_distance); - void start_client_thread(); + void start_client_thread(std::string_view uuid); void stop_client_thread(); - std::vector& planes(); std::vector& render_snapshots(); glm::vec3 sunlight_dir() const; + void receive_chunk(const ChunkDataRsp& data); + template + void register_timer(std::string_view id, TickType threshold, Fn&& f) { + m_timers.emplace(std::piecewise_construct, + std::forward_as_tuple(std::string(id)), + std::forward_as_tuple(threshold, std::forward(f))); + } private: using ChunkHashMap = tbb::concurrent_unordered_map; + using ChunkPosSet = std::unordered_set; ClientPlayer m_player; ChunkHashMap m_chunks; std::vector m_planes; @@ -50,10 +60,23 @@ private: mutable std::shared_mutex m_chunks_mutex; std::mutex m_delete_vbo_mutex; std::mutex m_delete_vao_mutex; + std::mutex m_pending_queue_mutex; + std::deque m_pending_queue; std::vector m_pending_delete_vbo; std::vector m_pending_delete_vao; std::deque m_dirty_queue; std::vector m_render_snapshots; + tbb::concurrent_unordered_map m_timers; + std::atomic m_game_running{false}; + std::atomic m_rendering_distance{24}; + std::shared_ptr m_client; + void client_run(std::stop_token token); + + void set_player_pos(); + + void report_player_pos(); + + void request_chunk(); }; } // namespace Cubed diff --git a/include/Cubed/gameplay/game_time.hpp b/include/Cubed/gameplay/game_time.hpp index 5cb08b7..64aab77 100644 --- a/include/Cubed/gameplay/game_time.hpp +++ b/include/Cubed/gameplay/game_time.hpp @@ -1,10 +1,40 @@ #pragma once // Prevent unsigned underflow issues in subtraction +#include "Cubed/tools/cubed_assert.hpp" + +#include +#include +namespace Cubed { using TickType = long long; constexpr int DEFAULT_PER_TICK_TIME = 50; constexpr TickType DAY_TIME = 24000; -constexpr TickType PER_HOUR = 1000; \ No newline at end of file +constexpr TickType PER_HOUR = 1000; + +class Timer { +public: + template + Timer(TickType threshold, Fn&& f) + : m_fn(std::forward(f)), m_threshold(threshold) { + ASSERT_MSG(threshold > 0, "Threshold Must Rreater Than 0"); + } + bool update() { + if (++m_current >= m_threshold) { + m_current = 0; + m_fn(); + return true; + } + return false; + } + void reset() { m_current = 0; } + +private: + std::function m_fn; + TickType m_threshold; + TickType m_current = 0; +}; + +} // namespace Cubed diff --git a/include/Cubed/gameplay/network_client.hpp b/include/Cubed/gameplay/network_client.hpp index 52713b8..de9abe8 100644 --- a/include/Cubed/gameplay/network_client.hpp +++ b/include/Cubed/gameplay/network_client.hpp @@ -7,9 +7,10 @@ #include namespace Cubed { using asio::ip::tcp; +class ClientWorld; class NetworkClient : std::enable_shared_from_this { public: - NetworkClient(); + NetworkClient(ClientWorld& world); ~NetworkClient(); void close(); asio::awaitable connect(std::string ip, int port); @@ -29,7 +30,7 @@ private: std::atomic m_closed{false}; // ClientWorld is managed by App - // ClientWorld& m_world; + ClientWorld& m_world; void do_write(); }; diff --git a/src/gameplay/client_chunk.cpp b/src/gameplay/client_chunk.cpp index 2657cea..050d37b 100644 --- a/src/gameplay/client_chunk.cpp +++ b/src/gameplay/client_chunk.cpp @@ -516,6 +516,7 @@ void ClientChunk::receive_chunk(const ChunkDataRsp& data) { } gen_vertex_data(neighbor); + mark_dirty(); } } // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/client_player.cpp b/src/gameplay/client_player.cpp index 1d92ebe..b3dcd1c 100644 --- a/src/gameplay/client_player.cpp +++ b/src/gameplay/client_player.cpp @@ -5,8 +5,8 @@ #include "Cubed/gameplay/client_world.hpp" namespace Cubed { -ClientPlayer::ClientPlayer(ClientWorld& world, const std::string& name) - : m_world(world) {} +ClientPlayer::ClientPlayer(ClientWorld& world, std::string_view name) + : m_name(name), m_world(world) {} ClientPlayer::~ClientPlayer() {} AABB ClientPlayer::get_aabb() const { @@ -28,7 +28,11 @@ const Gait& ClientPlayer::get_gait() const { return m_gait; } const std::optional& ClientPlayer::get_look_block_pos() const { return m_look_block; } -const glm::vec3& ClientPlayer::get_player_pos() const { return m_player_pos; } +glm::vec3 ClientPlayer::get_player_pos() const { + + std::shared_lock lock(m_player_pos_mutex); + return m_player_pos; +} const MoveState& ClientPlayer::get_move_state() const { return m_move_state; } @@ -305,6 +309,14 @@ void ClientPlayer::update_move(float delta_time) { if (delta_time > 1.0f) { return; } + // ensure the thread safe + glm::vec3 player_pos; + + { + std::shared_lock lock(m_player_pos_mutex); + player_pos = m_player_pos; + } + if (m_game_mode != SPECTATOR) { if (m_gait == Gait::RUN) { m_max_speed = m_max_run_speed; @@ -366,19 +378,24 @@ void ClientPlayer::update_move(float delta_time) { move_distance.y = m_y_speed * delta_time; // y - update_y_move(); + update_y_move(player_pos); // x - update_x_move(); + update_x_move(player_pos); - update_z_move(); + update_z_move(player_pos); - if (m_player_pos.y < -15.0f) { + if (player_pos.y < -15.0f) { Logger::warn("y is tow low"); - m_player_pos += glm::vec3(1.0f, 100.0f, 1.0f); + player_pos += glm::vec3(1.0f, 100.0f, 1.0f); + } + + { + std::lock_guard lock(m_player_pos_mutex); + m_player_pos = player_pos; } } -void ClientPlayer::update_x_move() { +void ClientPlayer::update_x_move(glm::vec3& m_player_pos) { m_player_pos.x += move_distance.x; if (m_game_mode == SPECTATOR) { return; @@ -412,7 +429,7 @@ void ClientPlayer::update_x_move() { } } -void ClientPlayer::update_y_move() { +void ClientPlayer::update_y_move(glm::vec3& m_player_pos) { m_player_pos.y += move_distance.y; if (m_game_mode == SPECTATOR) { return; @@ -450,7 +467,7 @@ void ClientPlayer::update_y_move() { } } -void ClientPlayer::update_z_move() { +void ClientPlayer::update_z_move(glm::vec3& m_player_pos) { m_player_pos.z += move_distance.z; if (m_game_mode == SPECTATOR) { return; @@ -522,4 +539,9 @@ unsigned ClientPlayer::place_block() const { return m_place_block; }; Gait& ClientPlayer::gait() { return m_gait; } GameMode& ClientPlayer::game_mode() { return m_game_mode; } const ClientWorld& ClientPlayer::get_world() const { return m_world; } + +void ClientPlayer::set_uuid(std::string_view uuid) { m_uuid = uuid; } +const std::string& ClientPlayer::get_uuid() const { return m_uuid; } +const std::string& ClientPlayer::get_name() const { return m_name; } + } // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/client_world.cpp b/src/gameplay/client_world.cpp index e69de29..014f0c0 100644 --- a/src/gameplay/client_world.cpp +++ b/src/gameplay/client_world.cpp @@ -0,0 +1,243 @@ +#include "Cubed/gameplay/client_world.hpp" + +#include "Cubed/gameplay/game_time.hpp" +#include "Cubed/gameplay/packet.hpp" +using namespace std::chrono; +using namespace std::chrono_literals; +namespace Cubed { + +namespace { +struct ChunkRenderData { + std::array*, 4> neighbor_block; + ClientChunk* chunk; +}; +} // namespace + +ClientWorld::ClientWorld(std::string_view player_name, + std::shared_ptr client) + : m_player(*this, player_name), m_client(client) {} + +ClientWorld::~ClientWorld() { + stop_client_thread(); + + { + std::lock_guard lock(m_chunks_mutex); + m_chunks.clear(); + } + { + std::lock_guard lk(m_delete_vbo_mutex); + for (auto x : m_pending_delete_vbo) { + glDeleteBuffers(1, &x); + } + m_pending_delete_vbo.clear(); + } + { + std::lock_guard lk(m_delete_vao_mutex); + for (auto x : m_pending_delete_vao) { + glDeleteVertexArrays(1, &x); + } + m_pending_delete_vao.clear(); + } + m_timers.clear(); +} + +const std::optional& +ClientWorld::get_look_block_pos(const std::string& name) const { + + return m_player.get_look_block_pos(); +} + +ClientPlayer& ClientWorld::get_player() { return m_player; } + +void ClientWorld::init() { + m_chunks.reserve(MAX_DISTANCE * MAX_DISTANCE * 4); + + // timer + register_timer("player_pos", 2, [this]() { report_player_pos(); }); + + LoginReq req; + req.set_name(m_player.get_name()); + // request login + m_client->send(make_packet(req)); +} + +void ClientWorld::start_client_thread(std::string_view uuid) { + if (m_game_running) { + Logger::error("Game Already Running"); + return; + } + // response + m_player.set_uuid(uuid); + m_client_thread = std::jthread([this](std::stop_token token) { + m_game_running = true; + client_run(token); + }); +} + +void ClientWorld::stop_client_thread() { + m_client_thread.request_stop(); + if (m_client_thread.joinable()) { + m_client_thread.join(); + } + m_game_running = false; +} + +void ClientWorld::client_run(std::stop_token stoken) { + Logger::info("Client Thread Started"); + while (!stoken.stop_requested()) { + for (auto& x : m_timers) { + x.second.update(); + } + std::this_thread::sleep_for(milliseconds(DEFAULT_PER_TICK_TIME)); + } +} + +void ClientWorld::report_player_pos() { + if (!m_client) { + return; + } + PlayerPos pos; + pos.set_uuid(m_player.get_uuid()); + glm::vec3 player_pos = m_player.get_player_pos(); + auto* v3 = pos.mutable_pos(); + v3->set_x(player_pos.x); + v3->set_y(player_pos.y); + v3->set_z(player_pos.z); + m_client->send(make_packet(pos)); +} + +void ClientWorld::request_chunk() { + ChunkPosSet required_chunks; + + glm::vec3 player_pos = m_player.get_player_pos(); + + int x = std::floor(player_pos.x); + int z = std::floor(player_pos.z); + auto [chunk_x, chunk_z] = get_chunk_pos(x, z); + int radius = m_rendering_distance; + int r2 = radius * radius; + required_chunks.reserve(radius * radius); + + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (dx * dx + dz * dz <= r2) { + required_chunks.emplace(chunk_x + dx, chunk_z + dz); + } + } + } + + ChunkPosSet need_send_pos; + { + std::lock_guard lk(m_chunks_mutex); + for (auto it = m_chunks.begin(); it != m_chunks.end();) { + if (required_chunks.find(it->first) == required_chunks.end()) { + it = m_chunks.unsafe_erase(it); + } else { + ++it; + } + } + + for (auto pos : required_chunks) { + auto it = m_chunks.find(pos); + if (it == m_chunks.end()) { + need_send_pos.emplace(pos); + } + } + } + if (need_send_pos.empty()) { + return; + } + auto uuid = m_player.get_uuid(); + ChunkDataReq req; + for (const auto& pos : need_send_pos) { + req.set_uuid(uuid); + auto* p = req.mutable_pos(); + p->set_x(pos.x); + p->set_z(pos.z); + m_client->send(make_packet(req)); + } +} + +void ClientWorld::receive_chunk(const ChunkDataRsp& data) { + ClientChunk chunk{*this}; + chunk.receive_chunk(data); + { + std::lock_guard lock(m_pending_queue_mutex); + m_pending_queue.emplace_back(std::move(chunk)); + } +} + +void ClientWorld::update(float delta_time) { + m_player.update(delta_time); + { + std::lock_guard lk(m_delete_vbo_mutex); + for (auto x : m_pending_delete_vbo) { + glDeleteBuffers(1, &x); + } + m_pending_delete_vbo.clear(); + } + + { + std::lock_guard lk(m_delete_vao_mutex); + for (auto x : m_pending_delete_vao) { + glDeleteVertexArrays(1, &x); + } + m_pending_delete_vao.clear(); + } + std::vector new_chunks; + { + std::lock_guard lock(m_pending_queue_mutex); + for (auto& c : m_pending_queue) { + new_chunks.emplace_back(std::move(c)); + } + } + for (auto& c : new_chunks) { + c.upload_to_gpu(); + } + { + std::lock_guard lock(m_chunks_mutex); + for (auto& c : new_chunks) { + m_chunks.emplace(c.get_chunk_pos(), std::move(c)); + } + m_render_snapshots.clear(); + for (auto& [pos, chunk] : m_chunks) { + if (chunk.is_dirty()) { + // the curial fator influence + OptionalBlockVectorArray neighbor_block; + for (int i = 0; i < 4; i++) { + auto it = m_chunks.find(pos + CHUNK_DIR[i]); + if (it != m_chunks.end()) { + neighbor_block[i] = (it->second.get_chunk_blocks()); + } else { + neighbor_block[i] = std::nullopt; + } + } + chunk.gen_vertex_data(neighbor_block); + chunk.upload_to_gpu(); + } + if (!chunk.is_dirty()) { + if (chunk.is_need_upload()) { + chunk.upload_to_gpu(); + } + m_render_snapshots.push_back( + {chunk.get_normal_vao(), chunk.get_normal_vertices_sum(), + chunk.get_cross_vao(), chunk.get_cross_vertices_sum(), + chunk.get_normal_discard_vao(), + chunk.get_normal_discard_vertices_sum(), + chunk.get_normal_blend_vao(), + chunk.get_normal_blend_vertices_sum(), + chunk.get_water_vao(), chunk.get_water_vertices_sum(), + glm::vec3(static_cast(pos.x * CHUNK_SIZE) + + static_cast(CHUNK_SIZE / 2), + static_cast(WORLD_SIZE_Y / 2), + static_cast(pos.z * CHUNK_SIZE) + + static_cast(CHUNK_SIZE / 2)), + glm::vec3(static_cast(CHUNK_SIZE / 2), + static_cast(WORLD_SIZE_Y / 2), + static_cast(CHUNK_SIZE / 2))}); + } + } + } +} + +} // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/network_client.cpp b/src/gameplay/network_client.cpp index 234ecb1..7e18474 100644 --- a/src/gameplay/network_client.cpp +++ b/src/gameplay/network_client.cpp @@ -1,9 +1,10 @@ #include "Cubed/gameplay/network_client.hpp" +#include "Cubed/gameplay/client_world.hpp" #include "Cubed/tools/log.hpp" - namespace Cubed { -NetworkClient::NetworkClient() : m_socket(m_io) {} +NetworkClient::NetworkClient(ClientWorld& world) + : m_socket(m_io), m_world(world) {} NetworkClient::~NetworkClient() { close(); } @@ -58,6 +59,20 @@ asio::awaitable NetworkClient::read_loop() { constexpr auto& to_num = std::to_underlying; switch (cmd_id) { case to_num(PacketEnum::LOGIN_RSP): { + LoginRsp rsp; + if (rsp.ParseFromArray(body_data.data(), body_data.size())) { + if (rsp.success()) { + m_world.start_client_thread(rsp.uuid()); + } else { + Logger::error("Connected Server Fail"); + } + } + } + case to_num(PacketEnum::CHUNK_DATA_RSP): { + ChunkDataRsp rsp; + if (rsp.ParseFromArray(body_data.data(), body_data.size())) { + m_world.receive_chunk(rsp); + } } } } diff --git a/src/gameplay/server_world.cpp b/src/gameplay/server_world.cpp index 5e613a9..9272aa2 100644 --- a/src/gameplay/server_world.cpp +++ b/src/gameplay/server_world.cpp @@ -19,8 +19,10 @@ ServerWorld::~ServerWorld() { stop_server_thread(); wait_all_chunk_tasks(); stop_thread_pool(); - - m_chunks.clear(); + { + std::lock_guard lock(m_chunks_mutex); + m_chunks.clear(); + } } void ServerWorld::wait_all_chunk_tasks() { diff --git a/src/proto/common/chunk_pos.proto b/src/proto/common/chunk_pos.proto index 5872d09..fc5f21a 100644 --- a/src/proto/common/chunk_pos.proto +++ b/src/proto/common/chunk_pos.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -message ChunkPos { +message ChunkPosNet { int32 x = 1; int32 z = 2; } \ No newline at end of file diff --git a/src/proto/packet.proto b/src/proto/packet.proto index 47a4579..42e1d6c 100644 --- a/src/proto/packet.proto +++ b/src/proto/packet.proto @@ -1,6 +1,5 @@ syntax = "proto3"; -import "common/chunk_pos.proto"; import "common/error.proto"; import "common/player_info.proto"; import "common/vector3.proto"; diff --git a/src/proto/world/chunk_data.proto b/src/proto/world/chunk_data.proto index a025362..4c070b2 100644 --- a/src/proto/world/chunk_data.proto +++ b/src/proto/world/chunk_data.proto @@ -4,11 +4,11 @@ import "common/chunk_pos.proto"; message ChunkDataReq { string uuid = 1; - ChunkPos pos = 2; + ChunkPosNet pos = 2; } message ChunkDataRsp { - ChunkPos pos = 1; + ChunkPosNet pos = 1; uint32 chunk_seed = 2; int32 biome_type = 3; repeated uint32 chunk_blocks = 4 [packed=true];