feat(client): add client-side chunk, player, world and network classes

This commit is contained in:
2026-06-24 21:49:30 +08:00
parent 4e6f29bedd
commit c92e2249a4
5 changed files with 444 additions and 0 deletions

View File

@@ -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 <atomic>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <mutex>
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<BlockType>& 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<BlockType>& 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<bool> m_dirty{false};
std::atomic<bool> m_need_upload{true};
std::atomic<bool> m_is_on_gen_vertex_data{false};
std::atomic<BiomeType> 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<BlockType> m_blocks;
/*
0 - normal
1 - cross_plane
2 - normal_discard
3 - transparent and blend
4 - water
*/
std::vector<VertexData> 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

View File

@@ -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 <glm/glm.hpp>
#include <optional>
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<LookBlock>& 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<LookBlock> 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

View File

@@ -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 <deque>
#include <tbb/concurrent_unordered_map.h>
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<LookBlock>&
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<glm::vec4>& planes();
std::vector<ChunkRenderSnapshot>& render_snapshots();
glm::vec3 sunlight_dir() const;
private:
using ChunkHashMap =
tbb::concurrent_unordered_map<ChunkPos, ClientChunk, ChunkPos::Hash>;
ClientPlayer m_player;
ChunkHashMap m_chunks;
std::vector<glm::vec4> 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<GLuint> m_pending_delete_vbo;
std::vector<GLuint> m_pending_delete_vao;
std::deque<ChunkPos> m_dirty_queue;
std::vector<ChunkRenderSnapshot> m_render_snapshots;
};
} // namespace Cubed

View File

@@ -0,0 +1,36 @@
#pragma once
#include "Cubed/gameplay/packet.hpp"
#include <asio.hpp>
#include <string>
#include <thread>
namespace Cubed {
using asio::ip::tcp;
class NetworkClient : std::enable_shared_from_this<NetworkClient> {
public:
NetworkClient();
~NetworkClient();
void close();
asio::awaitable<void> 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<char> m_read_buffer;
std::deque<Packet> m_write_queue;
asio::strand<asio::io_context::executor_type> m_strand;
asio::awaitable<void> read_loop();
std::atomic<bool> m_closed{false};
// ClientWorld is managed by App
// ClientWorld& m_world;
void do_write();
};
} // namespace Cubed

View File

@@ -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<void> 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<void> NetworkClient::read_loop() {
try {
while (true) {
std::array<char, HEADER_LEN> 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<uint8_t> 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<PacketEnum>;
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<std::vector<uint8_t>> 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