From c92e2249a4549ce5d21dc4adeea9ebd869566ea7 Mon Sep 17 00:00:00 2001 From: zhenyan121 <3367366583@qq.com> Date: Wed, 24 Jun 2026 21:49:30 +0800 Subject: [PATCH] feat(client): add client-side chunk, player, world and network classes --- include/Cubed/gameplay/client_chunk.hpp | 114 ++++++++++++++++++++ include/Cubed/gameplay/client_player.hpp | 109 +++++++++++++++++++ include/Cubed/gameplay/client_world.hpp | 59 ++++++++++ include/Cubed/gameplay/network_client.hpp | 36 +++++++ src/gameplay/network_client.cpp | 126 ++++++++++++++++++++++ 5 files changed, 444 insertions(+) diff --git a/include/Cubed/gameplay/client_chunk.hpp b/include/Cubed/gameplay/client_chunk.hpp index e69de29..c69384b 100644 --- a/include/Cubed/gameplay/client_chunk.hpp +++ b/include/Cubed/gameplay/client_chunk.hpp @@ -0,0 +1,114 @@ +#pragma once +#include "Cubed/constants.hpp" +#include "Cubed/gameplay/biome.hpp" +#include "Cubed/gameplay/block.hpp" +#include "Cubed/gameplay/chunk_pos.hpp" +#include "Cubed/gameplay/vertex_data.hpp" + +#include +#include +#include +#include +namespace Cubed { +class ClientWorld; +struct ChunkRenderSnapshot { + GLuint normal_vao; + size_t normal_vertices_count; + GLuint cross_vao; + size_t cross_vertices_count; + GLuint normal_discard_vao; + size_t normal_discard_vertices_count; + GLuint normal_blend_vao; + size_t normal_blend_vertices_count; + GLuint water_vao; + size_t water_vertices_count; + glm::vec3 center; + glm::vec3 half_extents; +}; +class ClientChunk { +public: + ClientChunk(ClientWorld& world); + ~ClientChunk(); + ClientChunk(const ClientChunk&) = delete; + ClientChunk& operator=(const ClientChunk&) = delete; + ClientChunk(ClientChunk&&) noexcept; + ClientChunk& operator=(ClientChunk&&) noexcept; + + BiomeType get_biome() const; + ChunkPos get_chunk_pos() const; + const std::vector& get_chunk_blocks() const; + void gen_vertex_data(const OptionalBlockVectorArray& neighbor_block); + void upload_to_gpu(); + + GLuint get_normal_vao() const; + size_t get_normal_vertices_sum() const; + + GLuint get_cross_vao() const; + size_t get_cross_vertices_sum() const; + + GLuint get_normal_discard_vao() const; + size_t get_normal_discard_vertices_sum() const; + + GLuint get_normal_blend_vao() const; + size_t get_normal_blend_vertices_sum() const; + + GLuint get_water_vao() const; + size_t get_water_vertices_sum() const; + + bool is_dirty() const; + void mark_dirty(); + + bool is_need_upload() const; + void need_upload(); + + void set_chunk_block(int index, unsigned id); + bool is_temp_chunk() const; + ChunkPos chunk_pos() const; + BiomeType biome() const; + void biome(BiomeType b); + std::vector& blocks(); + ClientWorld& world(); + unsigned seed() const; + +private: + struct FaceKey { + BlockType block_id = 0; + int face = -1; // 0-5, used to index NORMALS/TANGENTS/TEX_COORDS + + bool valid() const { return block_id != 0; } + bool operator==(const FaceKey& o) const { + return block_id == o.block_id && face == o.face; + } + bool operator!=(const FaceKey& o) const { return !(*this == o); } + }; + + static constexpr int SIZE_X = CHUNK_SIZE; + static constexpr int SIZE_Y = WORLD_SIZE_Y; + static constexpr int SIZE_Z = CHUNK_SIZE; + static constexpr int VERTEX_DATA_SUM = 5; + std::atomic m_dirty{false}; + std::atomic m_need_upload{true}; + std::atomic m_is_on_gen_vertex_data{false}; + std::atomic m_biome = BiomeType::PLAIN; + std::mutex m_vertexs_data_mutex; + ChunkPos m_chunk_pos; + ClientWorld& m_world; + // the index is a array of block id + std::vector m_blocks; + /* + 0 - normal + 1 - cross_plane + 2 - normal_discard + 3 - transparent and blend + 4 - water + */ + std::vector m_vertex_data; + unsigned m_seed = 0; + void clear_dirty(); + void gen_vertices(const OptionalBlockVectorArray& neighbor_block); + void gen_cross_plane_vertices(int world_x, int world_y, int world_z, + BlockType id); + void emit_quad(int axis, int face_dir, int layer, int i, int j, int w, + int h, int u_axis, int v_axis, FaceKey key); +}; +} // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/gameplay/client_player.hpp b/include/Cubed/gameplay/client_player.hpp index e69de29..e6dc38b 100644 --- a/include/Cubed/gameplay/client_player.hpp +++ b/include/Cubed/gameplay/client_player.hpp @@ -0,0 +1,109 @@ +#pragma once +#include "Cubed/AABB.hpp" +#include "Cubed/constants.hpp" +#include "Cubed/gameplay/block.hpp" +#include "Cubed/gameplay/chunk_pos.hpp" +#include "Cubed/gameplay/game_mode.hpp" +#include "Cubed/input.hpp" + +#include +#include +namespace Cubed { +enum class Gait { WALK = 0, RUN }; +class ClientWorld; +class ClientPlayer { +public: + ClientPlayer(ClientWorld& world); + ~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; + const MoveState& get_move_state() const; + + void change_mode(GameMode mode); + void hot_reload(); + void set_player_pos(const glm::vec3& pos); + void set_place_block(unsigned id); + void update(float delta_time); + void update_front_vec(float offset_x, float offset_y); + void update_player_move_state(int key, int action); + void update_scroll(double yoffset); + + float& max_walk_speed(); + float& max_run_speed(); + float& max_speed(); + float& acceleration(); + float& deceleration(); + float& g(); + float& fly_y_speed(); + + unsigned place_block() const; + + Gait& gait(); + GameMode& game_mode(); + + const ClientWorld& get_world() const; + + void set_uuid(std::string_view uuid); + const std::string& get_uuid() const; + +private: + using enum GameMode; + float m_max_walk_speed = DEFAULT_MAX_WALK_SPEED; + float m_max_run_speed = DEFAULT_MAX_RUN_SPEED; + float m_acceleration = DEFAULT_ACCELERATION; + float m_deceleration = DEFAULT_DECELERATION; + float m_g = DEFAULT_G; + constexpr static float MAX_SPACE_ON_TIME = 0.3f; + + float m_yaw = 0.0f; + float m_pitch = 0.0f; + + float m_sensitivity = 0.15f; + + float m_max_speed = m_max_walk_speed; + float m_y_speed = 0.0f; + float m_fly_y_speed = 7.5f; + bool can_up = true; + + float space_on_time = 0.0f; + bool space_on = false; + bool is_fly = false; + + float m_xz_speed = 0.0f; + + unsigned m_place_block = 1; + + glm::vec3 direction = glm::vec3(0.0f, 0.0f, 0.0f); + glm::vec3 move_distance{0.0f, 0.0f, 0.0f}; + // player is tow block tall, the pos is the lower pos + + glm::vec3 m_player_pos{0.0f, 255.0f, 0.0f}; + ChunkPos m_player_chunk_pos{0, 0}; + + glm::vec3 m_front{0, 0, -1}; + glm::vec3 m_right{0, 0, 0}; + glm::vec3 m_size{0.6f, 1.8f, 0.6f}; + + Gait m_gait = Gait::WALK; + MoveState m_move_state{}; + GameMode m_game_mode = CREATIVE; + std::optional m_look_block = std::nullopt; + std::string m_name{}; + std::string m_uuid; + ClientWorld& m_world; + bool ray_cast(const glm::vec3& start, const glm::vec3& dir, + glm::ivec3& block_pos, glm::vec3& normal, + float distance = 4.0f); + + void check_player_chunk_transition(); + 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(); +}; +} // namespace Cubed diff --git a/include/Cubed/gameplay/client_world.hpp b/include/Cubed/gameplay/client_world.hpp index e69de29..908f176 100644 --- a/include/Cubed/gameplay/client_world.hpp +++ b/include/Cubed/gameplay/client_world.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "Cubed/AABB.hpp" +#include "Cubed/gameplay/block.hpp" +#include "Cubed/gameplay/chunk_pos.hpp" +#include "Cubed/gameplay/client_chunk.hpp" +#include "Cubed/gameplay/client_player.hpp" + +#include +#include +namespace Cubed { + +class ClientWorld { +public: + ClientWorld(); + ~ClientWorld(); + void init(); + void update(float delta_time); + bool can_move(const AABB& player_box) const; + const std::optional& + get_look_block_pos(const std::string& name) const; + ClientPlayer& get_player(); + int get_block(const glm::ivec3& block_pos) const; + bool is_solid(const glm::ivec3& block_pos) const; + bool can_pass_block(const glm::ivec3& block_pos) const; + BlockType get_block_tpye(const glm::ivec3& block_pos) const; + void set_block(const glm::ivec3& pos, unsigned id); + void push_delete_vbo(GLuint vbo); + void push_delete_vao(GLuint vao); + void hot_reload(); + + void rebuild_world(); + + int rendering_distance() const; + void rendering_distance(int rendering_distance); + void start_client_thread(); + void stop_client_thread(); + + std::vector& planes(); + std::vector& render_snapshots(); + glm::vec3 sunlight_dir() const; + +private: + using ChunkHashMap = + tbb::concurrent_unordered_map; + ClientPlayer m_player; + ChunkHashMap m_chunks; + std::vector m_planes; + std::jthread m_client_thread; + + mutable std::shared_mutex m_chunks_mutex; + std::mutex m_delete_vbo_mutex; + std::mutex m_delete_vao_mutex; + + std::vector m_pending_delete_vbo; + std::vector m_pending_delete_vao; + std::deque m_dirty_queue; + std::vector m_render_snapshots; +}; +} // namespace Cubed diff --git a/include/Cubed/gameplay/network_client.hpp b/include/Cubed/gameplay/network_client.hpp index e69de29..52713b8 100644 --- a/include/Cubed/gameplay/network_client.hpp +++ b/include/Cubed/gameplay/network_client.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "Cubed/gameplay/packet.hpp" + +#include +#include +#include +namespace Cubed { +using asio::ip::tcp; +class NetworkClient : std::enable_shared_from_this { +public: + NetworkClient(); + ~NetworkClient(); + void close(); + asio::awaitable connect(std::string ip, int port); + void send(Packet packet); + void start(std::string ip, int port = 25530); + +private: + asio::io_context m_io; + + std::thread m_net_thread; + static constexpr uint32_t MAX_PACKET_SIZE = 4 * 1024 * 1024; + tcp::socket m_socket; + std::vector m_read_buffer; + std::deque m_write_queue; + asio::strand m_strand; + asio::awaitable read_loop(); + std::atomic m_closed{false}; + + // ClientWorld is managed by App + // ClientWorld& m_world; + + void do_write(); +}; +} // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/network_client.cpp b/src/gameplay/network_client.cpp index e69de29..234ecb1 100644 --- a/src/gameplay/network_client.cpp +++ b/src/gameplay/network_client.cpp @@ -0,0 +1,126 @@ +#include "Cubed/gameplay/network_client.hpp" + +#include "Cubed/tools/log.hpp" + +namespace Cubed { +NetworkClient::NetworkClient() : m_socket(m_io) {} + +NetworkClient::~NetworkClient() { close(); } + +void NetworkClient::start(std::string ip, int port) { + if (m_net_thread.joinable()) { + return; + } + m_net_thread = std::thread([self = shared_from_this(), ip, port]() { + asio::co_spawn(self->m_strand, self->connect(ip, port), asio::detached); + self->m_io.run(); + }); +} + +asio::awaitable NetworkClient::connect(std::string ip, int port) { + try { + auto ex = co_await asio::this_coro::executor; + tcp::resolver resolver(ex); + auto eps = co_await resolver.async_resolve(ip, std::to_string(port), + asio::use_awaitable); + co_await async_connect(m_socket, eps, asio::use_awaitable); + asio::co_spawn(m_strand, read_loop(), asio::detached); + co_return; + + } catch (const std::exception& e) { + Logger::error("Client Error {}", e.what()); + } +} + +asio::awaitable NetworkClient::read_loop() { + try { + while (true) { + std::array header; + co_await asio::async_read(m_socket, asio::buffer(header), + asio::use_awaitable); + uint32_t total_len_net; + std::memcpy(&total_len_net, header.data(), sizeof(total_len_net)); + uint32_t total_len = ntohl(total_len_net); + + uint16_t cmd_id_net; + std::memcpy(&cmd_id_net, header.data() + 4, sizeof(cmd_id_net)); + uint16_t cmd_id = ntohs(cmd_id_net); + if (total_len < HEADER_LEN || total_len > MAX_PACKET_SIZE) { + + throw std::runtime_error("invalid packet length"); + } + uint32_t body_len = total_len - HEADER_LEN; + std::vector body_data(body_len); + if (body_len > 0) { + co_await asio::async_read(m_socket, asio::buffer(body_data), + asio::use_awaitable); + } + constexpr auto& to_num = std::to_underlying; + switch (cmd_id) { + case to_num(PacketEnum::LOGIN_RSP): { + } + } + } + } catch (const asio::system_error& e) { + auto ec = e.code(); + + if (ec == asio::error::eof || ec == asio::error::operation_aborted) { + + Logger::info("Client disconnected"); + } else { + Logger::warn("Asio Error {}", e.what()); + } + + close(); + } catch (const std::exception& e) { + Logger::error("Session Error {}", e.what()); + close(); + } catch (...) { + Logger::error("Unknow Error"); + close(); + } + co_return; +} + +void NetworkClient::send(std::shared_ptr> packet) { + asio::post(m_strand, [self = shared_from_this(), + packet = std::move(packet)]() mutable { + bool idle = self->m_write_queue.empty(); + self->m_write_queue.emplace_back(std::move(packet)); + if (idle) { + self->do_write(); + } + }); +} + +void NetworkClient::do_write() { + + auto self = shared_from_this(); + asio::async_write( + m_socket, asio::buffer(*(m_write_queue.front())), + asio::bind_executor(m_strand, [self](std::error_code ec, size_t) { + if (ec) { + Logger::warn("Write Ec {}", ec.message()); + self->close(); + return; + } + self->m_write_queue.pop_front(); + if (!self->m_write_queue.empty()) { + self->do_write(); + } + })); +} + +void NetworkClient::close() { + if (m_closed.exchange(true)) { + return; + } + + std::error_code ec; + + m_socket.shutdown(tcp::socket::shutdown_both, ec); + + m_socket.close(ec); +} + +} // namespace Cubed \ No newline at end of file