mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-09 02:07:04 +08:00
* build(deps): add assimp library as dependency * feat(render): add model loading and rendering pipeline * refactor(render): rename depth player shaders to depth model and clean up * build: add EnTT library * feat(entity): add ECS-based entity rendering * feat(render): add shadow pass for entity models * refactor(render): unify model and player rendering with single shader pipeline * refactor(collision): convert AABB to center-half representation and migrate player data to ECS * refactor(gameplay): add base classes Chunk and World for shared logic * 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 * fix(gameplay): correct hitbox insertion in HitboxManager::load * build: remove entt library * refactor(gameplay): replace entt components with SparseVector for player data Refactor client world to use a custom SparseVector for player data instead of entt registry. Consolidate Transform, ViewAngles, and related structs into Position, Orientation, WalkPose. Introduce PlayerData and PlayerRenderData. Remove the unused move_system.cpp. Unify player render and shadow render into a single function. * feat(render): add model ID system with concurrent lookup and namespace-based loading - Extract `ModelID` and `Model` struct to new `model.hpp` - Replace `std::unordered_map` with `tbb::concurrent_hash_map` for thread safety - Add `get_model(ModelID)`, `get_model_id`, `get_model_name` methods - Parse model names in `namespace:name` format to construct asset paths - Update `load_model` to accept `string_view` and use ID-based management * refactor(gameplay): extract movement, gravity, orientation, and walk pose into structs * refactor(gameplay): extract Entity base class from ClientPlayer Move position, walk pose, velocity, orientation, movement, and gravity fields and their accessors into a new Entity base class. ClientPlayer now inherits from Entity, removing duplicated members. Also update velocity handling to use 3D vector per axis and adjust related logic. * feat(player): split max speed into horizontal/vertical, set spawn pos * refactor: move movement logic into SpeedSystem and Entity components * refactor(gameplay): extract physics and collision from ClientPlayer into PhysicalSystem Move per-axis collision detection and move distance calculation to new PhysicalSystem. Move SpeedSystem implementation from inline header to separate .cpp file. Add const accessors to Entity. Replace inline AABB helper with HitboxManager registration of player hitbox using new PLAYER_SIZE constant. Remove obsolete members and functions from ClientPlayer. * refactor(gameplay): integrate model and hitbox ID system into Entity - Introduce HitboxID, EntityID, and ModelID types for safer ID-based lookups - Replace AABB struct with Hitbox (includes HitboxID) - Convert ModelManager and HitboxManager to singletons with Handle structs - Update Entity to store IDs for model and hitbox, removing direct references - Reorganize creature model assets into subdirectories per entity type - Add player model (player.glb) and collision data for pig - Remove ModelManager dependency from App and Renderer - Add namespace parsing utility for asset paths - Mark sparse_vector::insert() with [[nodiscard]] * build: add entt library * refactor(gameplay): rename ClientPlayer to LocalPlayer * refactor(gameplay): separate player logic into manager and ECS components * feat(gameplay): add entity managers and refactor to ECS components * refactor(gameplay): replace client thread with timer-based system and add entity manager * feat(gameplay): implement entity system with concurrent task handling and fix model loading * feat(gameplay): add entity destruction support * feat(entity): add client-to-server entity creation and destruction requests Extend the network protocol with `C2SEntityCreateRequest` and `C2SEntityDestoryRequest` packets. Refactor entity managers to split public client-facing create/destroy methods that send requests over the network, from internal handlers that process received packets. Add utility functions for converting protobuf Vec3 to glm::vec3. * feat(gameplay): implement entity update packets Add S2CEntityUpdate to sync entity positions from server to client, including update handling in the client entity manager and server-side AI updates. * feat(ai): add wander AI system with move boost Add AIBase and WanderAITag components, a WanderAISystem, and a MoveBoost component to control wandering behavior. Replace the old MoveState with MoveBoost and include a horizontal random direction helper. * refactor(ecs): move entity physics to tick-based systems Update PhysicalSystem and SpeedSystem to operate on entt::registry instead of individual components. Add TickVelocity for server creatures so movement is calculated per tick without frame delta time. LocalPlayer now implements its own client-side physics with collision detection. * feat(gameplay): add server-side entity movement and AI Refactor LocalPlayer::update_physical to operate on a passed position, and run speed, physical, and wander AI systems in ServerEntityManager. * feat(gameplay): add pig wander AI and refine speed physics * feat(server): send entity position updates to all sessions * fix: correct velocity clamping and hitbox loading Preserve sign when clamping velocity; negative velocities now clamp to zero instead of flipping direction. Support loading hitbox definitions from JSON arrays and objects. Tune pig entity movement, spawn height, and wander probabilities. * feat(creatures): refine pig movement physics Add movement constants for pigs and switch deceleration to per-axis friction so creatures stop naturally. Extend wander boost duration for smoother behavior. * feat(physics): implement step-up for horizontal collisions * feat: sync entity direction and rotate models accordingly Move direction into the transform component and include it in server-to-client entity updates. Client now sets transform direction from the network message and uses it to compute yaw for model rotation, so entities visually face their movement direction. Refactor net_utils to support arbitrary Vec3 fields. * feat(render): interpolate entity transforms for rendering Add RenderTransform component to smooth position/direction updates and use it in entity and shadow passes. * fix(server_world): send time before entity updates * build: replace nlohmann json with rapidjson * refactor(json): migrate from nlohmann to rapidjson Replace nlohmann::json with rapidjson across localization, hitbox manager, server world, and sensitive filter. Add json_utils helper for converting rapidjson documents to maps, and improve parse error handling. * feat(item): add item manager with JSON asset loading Implement ItemManager to load and query item definitions from assets/item JSON files. Add ItemData struct, item asset metadata, CMake source registration, and initialize the manager during app startup. Also add AGENTS.md repository guidelines. * refactor(item): decouple items from block types The item system now supports an ItemKind and property, allowing item JSON to declare a type instead of assuming every item is a block. BlockManager was moved to its own header, redesigned around concurrent hash maps, and exposes id_from_name(). Texture and UI code now key items by ItemID, while block placement resolves the block type from the item registry. * fix(gameplay): use inline const for static EMPTY members Use inline const for static EMPTY members to avoid ODR and linker issues. Add missing block_manager.hpp includes in gameplay sources. * fix: stabilize block item registration and display Store item names as owned strings in ItemManager, set block type property for block items, validate item kind before placement, and handle items without textures gracefully in inventory UI. * feat: add pig spawn egg item Add pig spawn egg asset and texture, parse spawn egg item kind and creature property, and spawn the configured entity when used on an empty block. Also fix item texture loading to use actual image dimensions and update selected item UI sizing. * refactor(gameplay): update systems per entity with chunk check Refactor server entity update flow to process entities individually, skipping those whose chunk is not loaded. System update methods now accept a single entity instead of iterating the full registry view, and `ServerWorld::get_chunk_ref_count` is added to determine if an entity's chunk is active. * perf(render): skip entities outside loaded chunks * feat: add --direct-enter option to skip to world scene Support a new CLI flag that launches the app directly into the world scene. If no IP is provided, it starts a local server on the specified port and connects to 127.0.0.1; otherwise it connects to the given IP. With this flag enabled, a port must be specified via -p. Also expose SceneManager::push as public so scenes can be pushed immediately when bypassing the normal menu flow. * refactor(localization): switch block translations to item naming Remove the `name_key` field from block definitions and use item-based localization keys (`item.*.name`) for inventory display. Item data now stores the localized name at load time. * feat(gameplay): spawn creatures during chunk generation Add creature spawning to chunk generation with a configurable SpawnConfig, including a default pig spawn. Spawning occurs in the final generation phase and registers entities through the server world's entity manager. Also destroy entities when their chunk is unloaded to prevent orphaned entities and expose the entity manager from ServerWorld. * feat(gameplay): add run mode based thread pool config Introduce RunMode enum and thread pool sizing helpers for client, server, and hybrid modes. Add compute pool to server world and pass mode through server/client initialization. * perf(server): parallelize entity update loop Use parallel_do to process entities concurrently via the compute pool. Switch get_all_session to a thread-safe tbb::concurrent_vector and make ChunkEntity's ref_count atomic to avoid data races. Also remove the debug pig spawn from world init. * feat(creatures): animate creature walk cycles Add a Gait component synchronized over the network and procedural animation for model nodes. Load animation parameters from assets/model/creature/pig/animation.json and apply leg swing, body bob, and head motion based on gait. Refactor Gait into its own header for reuse. * fix(gameplay): make entity interpolation frame-rate independent * perf(render): batch model rendering with instancing Replaces per-entity draw calls with instanced rendering for both main and shadow passes. Each model's node hierarchy is flattened once and instance matrices are updated per frame, reducing draw calls and CPU overhead. * refactor(render): build entity instance buffers once per frame Move instance matrix upload into build_vertices and use a precomputed instance data map for both shadow and color passes, avoiding duplicate GPU buffer updates. * perf(gameplay): batch entity updates into single packet Aggregate per-entity update messages into S2CEntityUpdateBatch and broadcast once per tick. Reuse serialized packets in several broadcast loops to avoid repeated make_packet calls. * fix: correct thread count and shadow projection uniform - Add reserved client thread count to client threads in server generation calculation. - Set projection matrix uniform only for non-shadow rendering. * feat(ecs): add entity type separation and creature limits Replace generic entity creation with typed creatures and items. Rename `add_entity` to `add_creature`, enforce per-player creature cap, and track entity/creature totals for metrics. Expose run mode to dev panel and show new counts. * perf(render): frustum cull entities before instance building Add frustum culling to entity instance data collection using AABB vs camera frustum planes. Pass renderer into get_instances_data_map to compute MVP and extract planes. Also add "Rendered Entities" debug counter for visibility. * feat(audio): add ambient pig sounds Add pig call audio with randomized timing, triggered by proximity to the player as 3D positional sound. * feat(client-entity): use snapshot history for entity interpolation Replace exponential smoothing with a buffered snapshot system. Each entity keeps a deque of position/direction snapshots with timestamps; the render transform is interpolated at a fixed 100 ms render delay to hide network jitter. Snapshots are capped to 16 entries. * refactor(hitbox): move player hitbox definition to JSON asset * feat(client_player): use snapshot interpolation for remote players Replace exponential position smoothing with snapshot interpolation. Remote player transforms are rendered from a delayed snapshot history, using linear interpolation for position and shortest-path interpolation for yaw/pitch. A 100 ms render delay compensates for network tick rate.
928 lines
28 KiB
C++
928 lines
28 KiB
C++
#include "Cubed/gameplay/client_world.hpp"
|
|
|
|
#include "Cubed/config.hpp"
|
|
#include "Cubed/gameplay/block_manager.hpp"
|
|
#include "Cubed/gameplay/chunk_generator.hpp"
|
|
#include "Cubed/gameplay/game_time.hpp"
|
|
#include "Cubed/gameplay/packet.hpp"
|
|
#include "Cubed/scene/world_scene.hpp"
|
|
#include "Cubed/tools/threas_utils.hpp"
|
|
#include "Cubed/tools/time_tools.hpp"
|
|
|
|
#include <absl/container/inlined_vector.h>
|
|
#include <numbers>
|
|
|
|
using namespace std::chrono;
|
|
using namespace std::chrono_literals;
|
|
using namespace google::protobuf;
|
|
namespace Cubed {
|
|
|
|
namespace {
|
|
struct ChunkRenderData {
|
|
std::array<const std::vector<BlockType>*, 4> neighbor_block;
|
|
ClientChunk* chunk;
|
|
};
|
|
} // namespace
|
|
|
|
ClientWorld::ClientWorld(AudioEngine& auido, Config& config, WorldScene& scene)
|
|
: m_entity_manager(*this), m_player_manager(*this), m_audio(auido),
|
|
m_config(config), m_world_scene(scene) {}
|
|
|
|
ClientWorld::~ClientWorld() {
|
|
m_client->close();
|
|
|
|
stop_client_thread();
|
|
stop_thread_pool();
|
|
// Must first clean up and push the generated chunk data into
|
|
// m_pending_delete_vbo and m_pending_delete_vao; cannot delete them in the
|
|
// destructor, otherwise it will cause leaks and use-after-free.
|
|
m_dirty_chunk_queue.clear();
|
|
m_pending_upload_queue.clear();
|
|
|
|
m_chunks.clear();
|
|
|
|
if (m_is_pending_delete_queue_free.exchange(true)) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard lk(m_delete_vbo_mutex);
|
|
m_pending_delete_vbo.clear();
|
|
}
|
|
{
|
|
std::lock_guard lk(m_delete_vao_mutex);
|
|
m_pending_delete_vao.clear();
|
|
}
|
|
m_timers.clear();
|
|
}
|
|
|
|
const std::optional<LookBlock>& ClientWorld::get_look_block_pos() const {
|
|
|
|
return m_player_manager.get_local().get_look_block_pos();
|
|
}
|
|
|
|
LocalPlayer& ClientWorld::get_player() { return m_player_manager.get_local(); }
|
|
const LocalPlayer& ClientWorld::get_player() const {
|
|
return m_player_manager.get_local();
|
|
}
|
|
int ClientWorld::get_block(const glm::ivec3& block_pos) const {
|
|
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
|
|
chunk_cacc cacc;
|
|
|
|
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
|
|
return 0;
|
|
}
|
|
|
|
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
|
|
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
|
|
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
|
|
z >= CHUNK_SIZE) {
|
|
return 0;
|
|
}
|
|
return chunk_blocks[ClientChunk::index(x, y, z)];
|
|
}
|
|
bool ClientWorld::is_solid(const glm::ivec3& block_pos) const {
|
|
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
|
|
chunk_cacc cacc;
|
|
|
|
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
|
|
return false;
|
|
}
|
|
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
|
|
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
|
|
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
|
|
z >= CHUNK_SIZE) {
|
|
return false;
|
|
}
|
|
auto id = chunk_blocks[ClientChunk::index(x, y, z)];
|
|
if (BlockManager::is_gas(id) || BlockManager::is_liquid(id)) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
bool ClientWorld::can_pass_block(const glm::ivec3& block_pos) const {
|
|
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
|
|
chunk_cacc cacc;
|
|
|
|
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
|
|
return true;
|
|
}
|
|
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
|
|
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
|
|
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
|
|
z >= CHUNK_SIZE) {
|
|
return true;
|
|
}
|
|
auto id = chunk_blocks[ClientChunk::index(x, y, z)];
|
|
return BlockManager::is_passable(id);
|
|
}
|
|
|
|
void ClientWorld::rebuild_world() {
|
|
if (m_is_rebuilding.exchange(true)) {
|
|
return;
|
|
}
|
|
|
|
stop_client_thread();
|
|
stop_thread_pool();
|
|
|
|
m_chunks.clear();
|
|
|
|
m_pending_upload_queue.clear();
|
|
|
|
start_thread_pool();
|
|
start_client_thread(m_player_manager.get_local().get_uuid());
|
|
request_chunk();
|
|
m_is_rebuilding = false;
|
|
}
|
|
|
|
BlockType ClientWorld::get_block_tpye(const glm::ivec3& block_pos) const {
|
|
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
|
|
chunk_cacc cacc;
|
|
|
|
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
|
|
// Logger::error("Can't Find Block {} {} {}", block_pos.x, block_pos.y,
|
|
// block_pos.z);
|
|
return 0;
|
|
}
|
|
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
|
|
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
|
|
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
|
|
z >= CHUNK_SIZE) {
|
|
// Logger::error("Can't Find Block {} {} {}", block_pos.x, block_pos.y,
|
|
// block_pos.z);
|
|
return 0;
|
|
}
|
|
return chunk_blocks[ClientChunk::index(x, y, z)];
|
|
}
|
|
void ClientWorld::set_block(const glm::ivec3& block_pos, unsigned id) {
|
|
int world_x, world_y, world_z;
|
|
world_x = block_pos.x;
|
|
world_y = block_pos.y;
|
|
world_z = block_pos.z;
|
|
|
|
auto [chunk_x, chunk_z] = get_chunk_pos(world_x, world_z);
|
|
ChunkPos pos{chunk_x, chunk_z};
|
|
|
|
BlockType origin_id = 0;
|
|
{
|
|
chunk_acc acc;
|
|
|
|
if (!m_chunks.find(acc, pos)) {
|
|
return;
|
|
}
|
|
auto [x, y, z] = ClientChunk::world_to_block(world_x, world_y, world_z,
|
|
chunk_x, chunk_z);
|
|
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
|
|
z >= CHUNK_SIZE) {
|
|
return;
|
|
}
|
|
int idx = ClientChunk::index(x, y, z);
|
|
origin_id = acc->second->get_chunk_block(idx);
|
|
acc->second->set_chunk_block(idx, id);
|
|
acc->second->mark_dirty();
|
|
}
|
|
|
|
glm::vec3 sound_pos{world_x + 0.5f, world_y + 0.5f, world_z + 0.5f};
|
|
|
|
if (id == 0) {
|
|
std::string name = BlockManager::name_form_id(origin_id);
|
|
std::string sound = "block/" + name + "/break.ogg";
|
|
m_pending_sound.emplace(sound, sound_pos);
|
|
} else {
|
|
std::string name = BlockManager::name_form_id(id);
|
|
std::string sound = "block/" + name + "/place.ogg";
|
|
m_pending_sound.emplace(sound, sound_pos);
|
|
}
|
|
|
|
auto pool = m_thread_pool.load();
|
|
|
|
pool->enqueue(0, [this, pos]() {
|
|
std::shared_ptr<ClientChunk> chunk;
|
|
|
|
{
|
|
chunk_acc acc;
|
|
if (m_chunks.find(acc, pos)) {
|
|
chunk = acc->second;
|
|
}
|
|
}
|
|
|
|
if (!chunk) {
|
|
return;
|
|
}
|
|
|
|
OptionalBlockVectorArray neighbor_block;
|
|
for (int i = 0; i < 4; i++) {
|
|
chunk_cacc cacc;
|
|
if (m_chunks.find(cacc, pos + CHUNK_DIR[i])) {
|
|
neighbor_block[i] = (cacc->second->get_chunk_blocks());
|
|
} else {
|
|
neighbor_block[i] = std::nullopt;
|
|
}
|
|
}
|
|
|
|
chunk->gen_vertex_data(neighbor_block);
|
|
m_dirty_chunk_queue.emplace(pos);
|
|
});
|
|
|
|
static const glm::ivec3 NEIGHBOR_DIRS[] = {
|
|
{1, 0, 0}, {-1, 0, 0}, {0, 0, -1}, {0, 0, 1}};
|
|
static constexpr int NPOS_SUM = sizeof(NEIGHBOR_DIRS);
|
|
|
|
absl::InlinedVector<ChunkPos, NPOS_SUM> nposes;
|
|
|
|
for (const auto& dir : NEIGHBOR_DIRS) {
|
|
glm::ivec3 neighbor = block_pos + dir;
|
|
|
|
auto [cx, cz] = get_chunk_pos(neighbor.x, neighbor.z);
|
|
{
|
|
chunk_acc acc;
|
|
if (m_chunks.find(acc, {cx, cz})) {
|
|
if (acc->second->is_dirty()) {
|
|
continue;
|
|
}
|
|
nposes.emplace_back(acc->first);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& npos : nposes) {
|
|
pool->enqueue(0, [this, npos]() {
|
|
std::shared_ptr<ClientChunk> chunk;
|
|
|
|
{
|
|
chunk_acc acc;
|
|
if (m_chunks.find(acc, npos)) {
|
|
chunk = acc->second;
|
|
}
|
|
}
|
|
|
|
if (!chunk) {
|
|
return;
|
|
}
|
|
|
|
OptionalBlockVectorArray neighbor_block;
|
|
for (int i = 0; i < 4; i++) {
|
|
chunk_cacc cacc;
|
|
if (m_chunks.find(cacc, npos + CHUNK_DIR[i])) {
|
|
neighbor_block[i] = (cacc->second->get_chunk_blocks());
|
|
} else {
|
|
neighbor_block[i] = std::nullopt;
|
|
}
|
|
}
|
|
|
|
chunk->gen_vertex_data(neighbor_block);
|
|
|
|
m_dirty_chunk_queue.emplace(npos);
|
|
});
|
|
}
|
|
}
|
|
void ClientWorld::push_delete_vbo(std::unique_ptr<VertexBuffer>& vbo) {
|
|
if (m_is_pending_delete_queue_free) {
|
|
Logger::error("Push delete vbo Use After Free");
|
|
return;
|
|
}
|
|
std::lock_guard lk(m_delete_vbo_mutex);
|
|
m_pending_delete_vbo.push_back(std::move(vbo));
|
|
}
|
|
void ClientWorld::push_delete_vao(std::unique_ptr<VertexArray>& vao) {
|
|
if (m_is_pending_delete_queue_free) {
|
|
Logger::error("Push delete vao Use After Free");
|
|
return;
|
|
}
|
|
std::lock_guard lk(m_delete_vao_mutex);
|
|
m_pending_delete_vao.push_back(std::move(vao));
|
|
}
|
|
|
|
void ClientWorld::report_block_change(const glm::ivec3& pos,
|
|
unsigned id) const {
|
|
if (id != 0) {
|
|
Hitbox block_box = get_block_aabb(pos);
|
|
if (m_player_manager.has_player(block_box)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Arena arena;
|
|
auto* req = Arena::Create<BlockChangeReq>(&arena);
|
|
req->set_uuid(m_player_manager.get_local().get_uuid());
|
|
req->set_block(id);
|
|
auto* p = req->mutable_pos();
|
|
p->set_x(pos.x);
|
|
p->set_y(pos.y);
|
|
p->set_z(pos.z);
|
|
m_client->send(make_packet(*req), 0);
|
|
}
|
|
|
|
void ClientWorld::receive_block_change(const BlockChangeRsp& rsp) {
|
|
glm::vec3 pos{rsp.pos().x(), rsp.pos().y(), rsp.pos().z()};
|
|
set_block(pos, rsp.block());
|
|
}
|
|
|
|
void ClientWorld::receive_time(const UpdateTime& rsp) {
|
|
m_game_ticks = rsp.game_tick();
|
|
m_day_tick = rsp.day_tick();
|
|
}
|
|
|
|
void ClientWorld::receive_player_logout(const LogoutRsp& rsp) {
|
|
if (rsp.server_stop()) {
|
|
m_receive_exit = true;
|
|
return;
|
|
}
|
|
if (rsp.uuid() == m_player_manager.get_local().get_uuid()) {
|
|
m_receive_exit = true;
|
|
return;
|
|
}
|
|
m_player_manager.receive_player_logout(rsp);
|
|
}
|
|
|
|
void ClientWorld::receive_player_water_sound(const PlayerWaterSound& rsp) {
|
|
if (rsp.uuid() == m_player_manager.get_local().get_uuid()) {
|
|
return;
|
|
}
|
|
glm::vec3 pos = {rsp.pos().x(), rsp.pos().y(), rsp.pos().z()};
|
|
|
|
Logger::info("Client: Receive Player Water Sound");
|
|
|
|
m_pending_sound.emplace("ambient/water/in_and_out_of_water.flac", pos);
|
|
}
|
|
|
|
void ClientWorld::send_player_water_sound(bool underwater,
|
|
const glm::vec3& pos) {
|
|
Arena arena;
|
|
auto* r = Arena::Create<PlayerWaterSound>(&arena);
|
|
|
|
r->set_underwater(underwater);
|
|
auto* p = r->mutable_pos();
|
|
p->set_x(pos.x);
|
|
p->set_y(pos.y);
|
|
p->set_z(pos.z);
|
|
r->set_uuid(m_player_manager.get_local().get_uuid());
|
|
|
|
m_client->send(make_packet(*r));
|
|
Logger::info("Client: Send Player Water Sound");
|
|
}
|
|
|
|
void ClientWorld::init(std::string_view player_name,
|
|
std::shared_ptr<NetworkClient> client, RunMode mode) {
|
|
m_runmode = mode;
|
|
m_entity_manager.init();
|
|
m_player_manager.init(player_name);
|
|
m_client = client;
|
|
|
|
reload_config(false);
|
|
|
|
m_random.init(ChunkGenerator::seed());
|
|
|
|
// timer
|
|
register_timer("player_pos", 0.05f, [this]() {
|
|
m_player_manager.report_player_info(m_client.get());
|
|
});
|
|
register_timer("Birds Sound", 60.0f, [this]() {
|
|
auto player_pos = m_player_manager.get_local().get_player_pos();
|
|
if (player_pos.y < SEA_LEVEL) {
|
|
return;
|
|
}
|
|
ChunkPos pos = get_chunk_pos(player_pos.x, player_pos.z);
|
|
{
|
|
chunk_cacc cacc;
|
|
if (m_chunks.find(cacc, pos)) {
|
|
if (cacc->second->get_biome() == BiomeType::FOREST) {
|
|
m_audio.play_2d("ambient/birds.ogg", true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
register_timer("Ocean Wave", 3.0f, [this]() {
|
|
auto player_pos = m_player_manager.get_local().get_player_pos();
|
|
if (player_pos.y < SEA_LEVEL - 10 || player_pos.y > SEA_LEVEL + 10) {
|
|
return;
|
|
}
|
|
|
|
auto ans = m_random.random_int(1, 4);
|
|
std::string sound =
|
|
"ambient/ocean/wave00" + std::to_string(ans) + ".flac";
|
|
ChunkPos pos = get_chunk_pos(player_pos.x, player_pos.z);
|
|
{
|
|
chunk_cacc cacc;
|
|
if (m_chunks.find(cacc, pos)) {
|
|
if (cacc->second->get_biome() == BiomeType::OCEAN) {
|
|
m_audio.play_2d(sound, true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
register_timer("under water bubble", 1.5f, [this]() {
|
|
if (m_player_manager.get_local().is_underwater()) {
|
|
auto ans = m_random.random_int(1, 2);
|
|
std::string sound =
|
|
"ambient/water/bubble00" + std::to_string(ans) + ".ogg";
|
|
m_audio.play_3d(
|
|
sound, m_player_manager.get_local().get_player_pos(), true);
|
|
}
|
|
});
|
|
|
|
register_timer("bgm change", 350.0f, [this]() {
|
|
if (m_day_tick >= 17000 || m_day_tick < 5000) {
|
|
m_audio.change_bgm("bgm/bgm002.ogg");
|
|
} else {
|
|
m_audio.change_bgm("bgm/bgm001.mp3");
|
|
}
|
|
});
|
|
|
|
LoginReq req;
|
|
req.set_name(m_player_manager.get_local().get_name());
|
|
while (!client->is_connected()) {
|
|
if (client->is_connect_error()) {
|
|
throw std::runtime_error(client->get_error_string());
|
|
}
|
|
std::this_thread::sleep_for(milliseconds(200));
|
|
}
|
|
start_thread_pool();
|
|
// request login
|
|
Logger::info("Send Login Request");
|
|
m_client->send(make_packet(req), 0);
|
|
m_audio.play_bgm();
|
|
}
|
|
|
|
void ClientWorld::receive_login_rsp(LoginRsp& rsp) {
|
|
m_voice_chat = rsp.voice_chat();
|
|
start_client_thread(rsp.uuid());
|
|
}
|
|
|
|
void ClientWorld::start_client_thread(std::string_view uuid) {
|
|
if (m_game_running) {
|
|
Logger::error("Game Already Running");
|
|
return;
|
|
}
|
|
// response
|
|
m_player_manager.get_local().set_uuid(uuid);
|
|
/*
|
|
m_client_thread = std::jthread([this](std::stop_token token) {
|
|
m_game_running = true;
|
|
client_run(token);
|
|
});*/
|
|
|
|
// Wait for 20 ticks, after the server's central chunk is generated, then
|
|
// request chunks
|
|
|
|
std::this_thread::sleep_for(milliseconds(20 * m_per_tick_time));
|
|
|
|
request_chunk();
|
|
}
|
|
|
|
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::start_thread_pool() {
|
|
auto threads = Tools::get_client_threads(m_runmode);
|
|
Logger::info("Client pool threads {}", threads);
|
|
change_pool_threads(threads);
|
|
}
|
|
void ClientWorld::stop_thread_pool() {
|
|
auto pool_ptr = m_thread_pool.load();
|
|
if (pool_ptr) {
|
|
pool_ptr->stop();
|
|
}
|
|
m_thread_pool.store(nullptr);
|
|
Logger::info("Thread Pool Stopped");
|
|
}
|
|
|
|
void ClientWorld::change_pool_threads(int threads) {
|
|
int m_max_threads = std::thread::hardware_concurrency();
|
|
if (m_max_threads < 1) {
|
|
Logger::warn("Can't Get Max Support Threads, Set Max Threads to 4");
|
|
m_max_threads = 1;
|
|
}
|
|
int used_thread = std::clamp(threads, 1, m_max_threads);
|
|
m_thread_pool.store(std::make_shared<PriorityThreadPool>(used_thread));
|
|
}
|
|
|
|
void ClientWorld::reload_config(bool chunk_build) {
|
|
int dist = m_config.get<int>("world.rendering_distance", PRE_LOAD_DISTANCE);
|
|
Logger::info("Get Config Randering dist {}", dist);
|
|
m_rendering_distance = dist <= MAX_DISTANCE ? dist : MAX_DISTANCE;
|
|
if (chunk_build) {
|
|
request_chunk();
|
|
}
|
|
|
|
m_player_manager.reload_config();
|
|
}
|
|
|
|
/*
|
|
void ClientWorld::client_run(std::stop_token stoken) {
|
|
Logger::info("Client Thread Started");
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
const auto TICK = std::chrono::milliseconds(m_per_tick_time);
|
|
|
|
auto next = Clock::now();
|
|
while (!stoken.stop_requested()) {
|
|
next += TICK;
|
|
for (auto& x : m_ticktimers) {
|
|
x.second.update();
|
|
}
|
|
std::this_thread::sleep_until(next);
|
|
}
|
|
}
|
|
*/
|
|
|
|
void ClientWorld::update_chunk(const ChunkPosSet& old, const ChunkPosSet& now) {
|
|
|
|
// Elements in the old set that are not contained in now are not needed by
|
|
// the current player.
|
|
|
|
for (auto& pos : old) {
|
|
if (!now.contains(pos)) {
|
|
|
|
chunk_acc acc;
|
|
if (!m_chunks.find(acc, pos)) {
|
|
Logger::warn("Update Ref Count Error, can't Find old pos "
|
|
"in m_chunks");
|
|
continue;
|
|
}
|
|
|
|
m_chunks.erase(acc);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClientWorld::request_chunk() {
|
|
if (m_requesting_chunk.exchange(true)) {
|
|
Logger::warn("It is requesting new chunk!");
|
|
return;
|
|
}
|
|
ChunkPosSet required_chunks;
|
|
|
|
glm::vec3 player_pos = m_player_manager.get_local().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;
|
|
Logger::info("Client Chunk Radius {}", radius);
|
|
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 old = m_player_manager.get_local().get_chunk_pos_set();
|
|
m_player_manager.get_local().update_chunk_set(required_chunks);
|
|
|
|
ChunkPosVector need_send_pos;
|
|
|
|
for (auto pos : required_chunks) {
|
|
chunk_cacc cacc;
|
|
if (!m_chunks.find(cacc, pos)) {
|
|
need_send_pos.emplace_back(pos);
|
|
}
|
|
}
|
|
|
|
update_chunk(old, required_chunks);
|
|
|
|
if (need_send_pos.empty()) {
|
|
m_requesting_chunk = false;
|
|
return;
|
|
}
|
|
using enum ChunkLoadStyle;
|
|
switch (m_chunk_load_style) {
|
|
case RANDOM:
|
|
|
|
break;
|
|
case CENTER: {
|
|
|
|
glm::vec3 player_pos = m_player_manager.get_local().get_player_pos();
|
|
ChunkPos player_chunk_pos = get_chunk_pos(player_pos.x, player_pos.z);
|
|
auto dist2 = [player_chunk_pos](ChunkPos chunk_pos) {
|
|
float dx = player_chunk_pos.x - chunk_pos.x;
|
|
float dz = player_chunk_pos.z - chunk_pos.z;
|
|
return dx * dx + dz * dz;
|
|
};
|
|
|
|
std::sort(need_send_pos.begin(), need_send_pos.end(),
|
|
[&dist2](const auto& a, const auto& b) {
|
|
return dist2(a) < dist2(b);
|
|
});
|
|
}
|
|
}
|
|
auto uuid = m_player_manager.get_local().get_uuid();
|
|
Arena arena;
|
|
++m_chunk_task_id;
|
|
auto* req = Arena::Create<ChunkDataReq>(&arena);
|
|
for (const auto& pos : need_send_pos) {
|
|
req->set_task_id(m_chunk_task_id.load());
|
|
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));
|
|
}
|
|
Logger::info("Send Chunk Request Success");
|
|
m_requesting_chunk = false;
|
|
}
|
|
void ClientWorld::reset_key_status() {
|
|
m_player_manager.get_local().reset_input_status();
|
|
}
|
|
void ClientWorld::receive_chunk(std::vector<uint8_t> raw_data,
|
|
PacketHeader header) {
|
|
|
|
// vertex data will genrator in client thread pool instead of net thread;
|
|
auto pool = m_thread_pool.load();
|
|
if (!pool) {
|
|
Logger::error("Client Thread Pool is nullptr");
|
|
return;
|
|
}
|
|
pool->enqueue(
|
|
[this, raw_data = std::move(raw_data), header = std::move(header)]() {
|
|
Arena arena;
|
|
auto* data = Arena::Create<ChunkDataRsp>(&arena);
|
|
if (!decode_packet(*data, raw_data, header)) {
|
|
return;
|
|
}
|
|
|
|
if (data->task_id() < m_chunk_task_id) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
chunk_cacc cacc;
|
|
ChunkPos pos{data->pos().x(), data->pos().z()};
|
|
if (m_chunks.find(cacc, pos)) {
|
|
Logger::warn("Chunk {} {} has already in client world",
|
|
pos.x, pos.z);
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<ClientChunk> chunk =
|
|
std::make_unique<ClientChunk>(*this);
|
|
chunk->receive_chunk(*data);
|
|
|
|
m_pending_upload_queue.emplace(std::move(chunk));
|
|
});
|
|
}
|
|
bool ClientWorld::is_receive_exit() { return m_receive_exit; }
|
|
|
|
int ClientWorld::chunk_size() const { return m_chunks.size(); }
|
|
|
|
AudioEngine& ClientWorld::get_audio() { return m_audio; }
|
|
const AudioEngine& ClientWorld::get_audio() const { return m_audio; }
|
|
Config& ClientWorld::get_config() { return m_config; }
|
|
WorldScene& ClientWorld::world_scene() { return m_world_scene; }
|
|
ClientPlayerManager& ClientWorld::player_manager() { return m_player_manager; }
|
|
ClientEntityManager& ClientWorld::entity_manager() { return m_entity_manager; }
|
|
std::shared_ptr<NetworkClient> ClientWorld::get_client() const {
|
|
return m_client;
|
|
}
|
|
void ClientWorld::set_direct_exit() { m_exit_direct = true; }
|
|
void ClientWorld::request_exit() {
|
|
if (m_receive_exit) {
|
|
return;
|
|
}
|
|
Arena arena;
|
|
auto* req = Arena::Create<LogoutReq>(&arena);
|
|
req->set_uuid(m_player_manager.get_local().get_uuid());
|
|
m_client->send(make_packet(*req));
|
|
int cnt = 0;
|
|
while (!m_receive_exit) {
|
|
if (m_client->is_connect_error() || m_exit_direct) {
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(milliseconds(m_per_tick_time));
|
|
++cnt;
|
|
if (cnt >= WORLD_EXIT_TIMEOUT) {
|
|
Logger::warn("Can't Receive Server Exit Sign");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClientWorld::receive_chat_message(ChatMsg& msg) {
|
|
Color color = color_from_int(msg.color());
|
|
|
|
m_message_queue.emplace(msg.name(), msg.msg(), color, msg.system_msg(),
|
|
Tools::get_time_ticks());
|
|
}
|
|
|
|
void ClientWorld::receive_voice_message(VoiceMsg& msg) {
|
|
if (!m_voice_chat) {
|
|
return;
|
|
}
|
|
glm::vec3 pos{msg.pos().x(), msg.pos().y(), msg.pos().z()};
|
|
m_voice_queue.emplace(msg.opus_data(), pos);
|
|
}
|
|
bool ClientWorld::enable_voice_chat() const { return m_voice_chat.load(); }
|
|
int ClientWorld::get_per_tick_time() const { return m_per_tick_time; }
|
|
|
|
bool ClientWorld::is_render(const glm::vec3& pos) const {
|
|
ChunkPos p = get_chunk_pos(pos.x, pos.z);
|
|
chunk_cacc cacc;
|
|
return m_chunks.find(cacc, p);
|
|
}
|
|
|
|
void ClientWorld::send_chat_message(ChatMessage& message) {
|
|
Arena arena;
|
|
auto msg = Arena::Create<ChatMsg>(&arena);
|
|
msg->set_name(message.player);
|
|
msg->set_msg(message.text);
|
|
m_client->send(make_packet(*msg));
|
|
}
|
|
|
|
void ClientWorld::update(float delta_time) {
|
|
m_player_manager.update(delta_time);
|
|
m_entity_manager.update(delta_time);
|
|
{
|
|
std::lock_guard lk(m_delete_vbo_mutex);
|
|
m_pending_delete_vbo.clear();
|
|
}
|
|
|
|
{
|
|
std::lock_guard lk(m_delete_vao_mutex);
|
|
m_pending_delete_vao.clear();
|
|
}
|
|
|
|
std::vector<std::unique_ptr<ClientChunk>> new_chunks;
|
|
{
|
|
std::unique_ptr<ClientChunk> chunk;
|
|
int sum = 0;
|
|
while (m_pending_upload_queue.try_pop(chunk)) {
|
|
new_chunks.emplace_back(std::move(chunk));
|
|
++sum;
|
|
if (sum >= MAX_UPLOAD_CHUNK_SUM) {
|
|
break; // Limit the maximum number of uploads per frame to
|
|
// improve frame rate performance
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& c : new_chunks) {
|
|
c->upload_to_gpu();
|
|
}
|
|
|
|
for (auto& c : new_chunks) {
|
|
m_chunks.emplace(c->get_chunk_pos(), std::move(c));
|
|
}
|
|
m_render_snapshots.clear();
|
|
|
|
ChunkPos pos;
|
|
|
|
while (m_dirty_chunk_queue.try_pop(pos)) {
|
|
std::shared_ptr<ClientChunk> chunk;
|
|
{
|
|
chunk_acc acc;
|
|
if (m_chunks.find(acc, pos)) {
|
|
chunk = acc->second;
|
|
}
|
|
}
|
|
if (!chunk) {
|
|
continue;
|
|
}
|
|
|
|
chunk->upload_to_gpu();
|
|
}
|
|
|
|
auto chunk_pos_set = m_player_manager.get_local().get_chunk_pos_set();
|
|
|
|
for (auto& pos : chunk_pos_set) {
|
|
std::shared_ptr<ClientChunk> chunk;
|
|
{
|
|
chunk_acc acc;
|
|
if (m_chunks.find(acc, pos)) {
|
|
chunk = acc->second;
|
|
}
|
|
}
|
|
if (!chunk) {
|
|
continue;
|
|
}
|
|
|
|
m_render_snapshots.push_back(chunk->get_render_snapshot());
|
|
}
|
|
|
|
// sound
|
|
PendingSound pending_sound;
|
|
while (m_pending_sound.try_pop(pending_sound)) {
|
|
m_audio.play_3d(pending_sound.sound, pending_sound.sound_pos);
|
|
}
|
|
|
|
for (auto& [pos, timer] : m_timers) {
|
|
timer.update(delta_time);
|
|
}
|
|
ChatMessage message;
|
|
while (m_message_queue.try_pop(message)) {
|
|
m_world_scene.handle_chat_message(message);
|
|
}
|
|
|
|
VoiceMessage vm;
|
|
while (m_voice_queue.try_pop(vm)) {
|
|
m_audio.receive_voice(vm.data, vm.pos);
|
|
}
|
|
}
|
|
|
|
bool ClientWorld::handle_event(const Event& e) {
|
|
return std::visit(
|
|
Overloaded{
|
|
[this](const MouseButtonEvent& e) {
|
|
if (m_player_manager.get_local().handle_mouse_button_event(e)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
[](const MouseMoveEvent&) { return false; },
|
|
[this](const MouseWheelEvent& e) {
|
|
if (m_player_manager.get_local().handle_mouse_wheel_event(e)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
[this](const KeyEvent& e) {
|
|
if (m_player_manager.get_local().handle_key_event(e)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
[](const TextInputEvent&) { return false; },
|
|
[](const WindowResizeEvent&) { return false; },
|
|
[](const FrameBufferResizeEvent&) { return false; }
|
|
|
|
},
|
|
e);
|
|
}
|
|
|
|
glm::vec3 ClientWorld::sunlight_dir() const {
|
|
float altitude = sin((m_day_tick - 6 * PER_HOUR) /
|
|
static_cast<float>(DAY_TIME / 2) * std::numbers::pi) *
|
|
90.0f;
|
|
|
|
float t = static_cast<float>(m_day_tick) / DAY_TIME;
|
|
float azimuth = 90.0f - 360.0f * (t - 0.25f);
|
|
|
|
float alt = glm::radians(altitude);
|
|
float az = glm::radians(azimuth);
|
|
glm::vec3 dir;
|
|
dir.x = cos(alt) * sin(az);
|
|
dir.y = sin(alt);
|
|
dir.z = cos(alt) * cos(az);
|
|
|
|
return glm::normalize(-dir);
|
|
}
|
|
|
|
bool ClientWorld::sphere_collide_world(glm::vec3 center, float radius) const {
|
|
glm::ivec3 min = glm::floor(center - glm::vec3(radius));
|
|
glm::ivec3 max = glm::floor(center + glm::vec3(radius));
|
|
|
|
for (int x = min.x; x <= max.x; ++x) {
|
|
for (int y = min.y; y <= max.y; ++y) {
|
|
for (int z = min.z; z <= max.z; ++z) {
|
|
if (!is_solid({x, y, z}))
|
|
continue;
|
|
|
|
glm::vec3 closest;
|
|
closest.x = glm::clamp(center.x, float(x), float(x + 1));
|
|
closest.y = glm::clamp(center.y, float(y), float(y + 1));
|
|
closest.z = glm::clamp(center.z, float(z), float(z + 1));
|
|
|
|
glm::vec3 d = center - closest;
|
|
|
|
if (glm::dot(d, d) < radius * radius)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int ClientWorld::rendering_distance() const {
|
|
return m_rendering_distance.load();
|
|
}
|
|
|
|
void ClientWorld::rendering_distance(int rendering_distance) {
|
|
m_rendering_distance = rendering_distance;
|
|
Logger::info("Set Rendering dist {} , the value is {}", rendering_distance,
|
|
m_rendering_distance.load());
|
|
request_chunk();
|
|
}
|
|
|
|
int ClientWorld::get_chunk_task_id() const { return m_chunk_task_id.load(); }
|
|
|
|
const std::vector<const ChunkRenderSnapshot*>&
|
|
ClientWorld::render_snapshots() const {
|
|
return m_render_snapshots;
|
|
};
|
|
|
|
std::vector<glm::vec4>& ClientWorld::planes() { return m_planes; }
|
|
} // namespace Cubed
|