3 Commits

Author SHA1 Message Date
b2187bf0ad 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.
2026-08-05 20:30:01 +08:00
2aa9d07e43 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.
2026-08-05 19:49:06 +08:00
8ff789c576 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.
2026-08-05 16:56:02 +08:00
15 changed files with 319 additions and 59 deletions

View File

@@ -50,6 +50,8 @@ public:
void generate_cave();
void generate_river();
void spawn_creature();
private:
static inline std::atomic<bool> is_init{false};
static inline unsigned m_generator_seed{0};

View File

@@ -32,7 +32,7 @@ public:
ClientWorld(AudioEngine& auido, Config& config, WorldScene& scene);
~ClientWorld();
void init(std::string_view player_name,
std::shared_ptr<NetworkClient> client);
std::shared_ptr<NetworkClient> client, RunMode mode);
void update(float dt);
bool handle_event(const Event& e);
const std::optional<LookBlock>& get_look_block_pos() const;
@@ -132,6 +132,7 @@ private:
static constexpr int WORLD_EXIT_TIMEOUT = 200;
static constexpr int MAX_UPLOAD_CHUNK_SUM = 16;
std::atomic<RunMode> m_runmode = RunMode::HYBRID;
ClientEntityManager m_entity_manager;
ClientPlayerManager m_player_manager;
ChunkHashMap m_chunks;

View File

@@ -0,0 +1,20 @@
#pragma once
#include "Cubed/gameplay/biome.hpp"
#include <array>
#include <span>
#include <string_view>
namespace Cubed {
struct SpawnConfig {
std::string_view name; // factory key, e.g. "cubed:pig"
std::span<const BiomeType> biomes; // allowed biomes
float probability = 0.0f; // per chunk spawn probability
unsigned max_spawn_count = 0; // per chunk_max_spawn_sum
};
namespace SpawnDefaults {
constexpr std::array<BiomeType, 2> PIG_BIOMES{BiomeType::PLAIN,
BiomeType::FOREST};
constexpr SpawnConfig PIG{"cubed:pig", PIG_BIOMES, 0.02f, 3};
} // namespace SpawnDefaults
} // namespace Cubed

View File

@@ -14,8 +14,7 @@ public:
void stop();
// Run in another thread after initialization is complete
void start_server(int port);
void start_server();
void start_server(int port, RunMode mode);
int port() const;
ServerWorld& server_world();

View File

@@ -5,6 +5,7 @@
#include <entt/entt.hpp>
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
#include <tbb/concurrent_vector.h>
namespace Cubed {
class ServerWorld;
class Session;
@@ -15,7 +16,7 @@ public:
void init();
void update();
// not thread safe
void add_entity(std::string_view name, const glm::vec3& pos);
void add_entity(std::string_view name, const glm::vec3& world_pos);
void destory(EntityID id);
void handle_player_login(std::shared_ptr<Session> session);
@@ -46,8 +47,9 @@ private:
void send_all_entities(std::shared_ptr<Session>& session);
void update_ai(entt::entity e);
void update_move(entt::entity e);
void update_send(entt::entity e,
std::span<std::shared_ptr<Session>> sessions);
void
update_send(entt::entity e,
tbb::concurrent_vector<std::shared_ptr<Session>>& sessions);
template <typename... Args>
EntityID create_entity_in_factory(Args&&... args) {
auto entity = m_registry.create();

View File

@@ -22,6 +22,7 @@
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
#include <tbb/concurrent_unordered_map.h>
#include <tbb/concurrent_vector.h>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -34,7 +35,7 @@ public:
~ServerWorld();
void stop();
void handle_player_exit(const std::string& uuid);
void init_world();
void init_world(RunMode mode);
void need_gen(std::string uuid);
void update();
void hot_reload();
@@ -92,10 +93,11 @@ public:
int chunk_size() const;
std::vector<std::shared_ptr<Session>> get_all_session() const;
tbb::concurrent_vector<std::shared_ptr<Session>> get_all_session() const;
uint32_t get_chunk_ref_count(const glm::vec3& pos) const;
ServerEntityManager& entity_manager();
std::shared_ptr<ThreadPool> get_compute_pool();
int get_block(const glm::ivec3& block_pos) const override;
bool is_solid(const glm::ivec3& block_pos) const override;
bool can_pass_block(const glm::ivec3& block_pos) const override;
@@ -113,7 +115,27 @@ private:
struct ChunkEntity {
ChunkState state;
std::shared_ptr<ServerChunk> chunk;
uint32_t ref_count = 0;
std::atomic<uint32_t> ref_count = 0;
ChunkEntity() = default;
ChunkEntity(ChunkState s, std::shared_ptr<ServerChunk> c = {})
: state(s), chunk(std::move(c)) {}
ChunkEntity& operator=(ChunkEntity&& o) noexcept {
if (this == &o) {
return *this;
}
state = std::exchange(o.state, ServerWorld::ChunkState::NONE);
chunk = std::move(o.chunk);
ref_count = o.ref_count.exchange(0);
return *this;
}
ChunkEntity(ChunkEntity&& o) noexcept
: state(std::exchange(o.state, ServerWorld::ChunkState::NONE)),
chunk(std::move(o.chunk)), ref_count(o.ref_count.exchange(0)) {}
ChunkEntity(const ChunkEntity&) = delete;
ChunkEntity& operator=(const ChunkEntity&) = delete;
};
enum class ChunkLoadStyle { RANDOM, CENTER };
@@ -141,6 +163,7 @@ private:
using uuid_cacc = PlayerUUIDMap::const_accessor;
Config& m_config;
std::atomic<RunMode> m_runmode{RunMode::HYBRID};
ServerEntityManager m_entity_manager;
// key = uuid
PlayerHashMap m_players;
@@ -162,8 +185,9 @@ private:
std::atomic<bool> m_init{false};
std::atomic<bool> m_stopped{false};
std::atomic<int> m_rendering_distance{24};
std::atomic<int> m_gen_pool_threads{0};
std::atomic<int> m_net_pool_threads{0};
std::atomic<int> m_gen_threads{0};
std::atomic<int> m_net_threads{0};
std::atomic<int> m_compute_threads{0};
std::atomic<int> m_max_threads{1};
std::atomic<size_t> m_player_sum{0};
std::atomic<TickType> m_game_ticks{0};
@@ -179,6 +203,7 @@ private:
std::atomic<std::shared_ptr<PriorityThreadPool>> m_gen_thread_pool;
std::atomic<std::shared_ptr<ThreadPool>> m_net_thread_pool;
std::atomic<std::shared_ptr<ThreadPool>> m_compute_thread_pool;
std::atomic<ChunkLoadStyle> m_chunk_load_style{ChunkLoadStyle::CENTER};

View File

@@ -26,4 +26,7 @@ public:
glm::vec3{0.5f, 0.5f, 0.5f}};
}
};
enum class RunMode { CLIENT_ONLY, SERVER_ONLY, HYBRID };
} // namespace Cubed

View File

@@ -0,0 +1,130 @@
#pragma once
#include "Cubed/gameplay/world.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include <algorithm>
#include <thread>
namespace Cubed {
namespace Tools {
constexpr size_t SERVER_RESERVED_THREADS = 3; // tick + netio + gen scheduler
constexpr size_t CLIENT_RESERVED_THREADS =
3; // main/render + netio + system reserved
constexpr size_t safe_sub(size_t a, size_t b) { return a > b ? a - b : 0; }
inline size_t get_hardware_threads() {
auto hc = std::thread::hardware_concurrency();
return hc == 0 ? 4 : static_cast<size_t>(hc);
}
inline size_t get_server_available_threads() {
return std::max<size_t>(
1, safe_sub(get_hardware_threads(), SERVER_RESERVED_THREADS));
}
inline size_t get_client_available_threads() {
return std::max<size_t>(
1, safe_sub(get_hardware_threads(), CLIENT_RESERVED_THREADS));
}
inline size_t get_client_threads(RunMode mode) {
switch (mode) {
case RunMode::SERVER_ONLY:
ASSERT_MSG(false, "Server Only don't need client pool");
return 1;
case RunMode::CLIENT_ONLY: {
auto available = get_client_available_threads();
return std::clamp<size_t>(available, 1, 16);
}
case RunMode::HYBRID: {
auto available = get_client_available_threads();
return std::clamp<size_t>(available / 2, 1, 4);
}
}
return 1;
}
inline size_t get_server_net_pool_threads(RunMode mode) {
switch (mode) {
case RunMode::SERVER_ONLY: {
auto available = get_server_available_threads();
return std::clamp<size_t>(available / 4, 1,
std::min<size_t>(4, available));
}
case RunMode::CLIENT_ONLY:
ASSERT_MSG(false, "Client Only don't need net pool");
return 1;
case RunMode::HYBRID: {
auto available = get_server_available_threads();
return std::clamp<size_t>(available / 8, 1,
std::min<size_t>(4, available));
}
}
return 1;
}
inline size_t get_server_compute_treads(RunMode mode) {
switch (mode) {
case RunMode::HYBRID:
case RunMode::SERVER_ONLY: {
auto available = get_server_available_threads();
return std::clamp<size_t>(available / 4, 1,
std::min<size_t>(4, available));
}
case RunMode::CLIENT_ONLY:
ASSERT_MSG(false, "Client Only don't need update pool");
return 1;
}
return 1;
}
inline size_t get_server_gen_threads(RunMode mode) {
switch (mode) {
case RunMode::SERVER_ONLY: {
auto available = get_server_available_threads();
auto net_pool = get_server_net_pool_threads(mode);
auto update_pool = get_server_compute_treads(mode);
size_t remain = available;
remain -= std::min(remain, net_pool);
remain -= std::min(remain, update_pool);
return std::max<size_t>(1, remain);
}
case RunMode::CLIENT_ONLY:
ASSERT_MSG(false, "Client Only don't need gen pool");
return 1;
case RunMode::HYBRID: {
auto available = get_server_available_threads();
auto net_pool = get_server_net_pool_threads(mode);
auto update_pool = get_server_compute_treads(mode);
auto client_pool = get_client_threads(mode);
size_t remain = available;
remain -= std::min(remain, net_pool);
remain -= std::min(remain, update_pool);
remain -= std::min(remain, client_pool);
return std::max<size_t>(1, remain);
}
}
return 1;
}
} // namespace Tools
} // namespace Cubed

View File

@@ -9,6 +9,7 @@
#include "Cubed/gameplay/builders/river_builder.hpp"
#include "Cubed/gameplay/builders/snowy_plain_builder.hpp"
#include "Cubed/gameplay/cave_path.hpp"
#include "Cubed/gameplay/creatures/spawn.hpp"
#include "Cubed/gameplay/river.path.hpp"
#include "Cubed/gameplay/server_chunk.hpp"
#include "Cubed/gameplay/server_world.hpp"
@@ -17,6 +18,8 @@
#include "Cubed/tools/cubed_hash.hpp"
#include "Cubed/tools/math_tools.hpp"
#include "Cubed/tools/perlin_noise.hpp"
#include <algorithm>
namespace Cubed {
namespace {
@@ -805,6 +808,35 @@ void ChunkGenerator::generate_river() {
}
}
void ChunkGenerator::spawn_creature() {
auto biome = m_chunk.biome();
const auto& blocks = m_chunk.blocks();
const auto& heightmap = m_chunk.heightmap();
const auto& chunk_pos = m_chunk.chunk_pos();
if (std::ranges::contains(SpawnDefaults::PIG.biomes, biome)) {
if (m_random.random_bool(SpawnDefaults::PIG.probability)) {
int want =
m_random.random_int(0, SpawnDefaults::PIG.max_spawn_count);
for (int i = 0; i < want; ++i) {
int x = m_random.random_int(0, CHUNK_SIZE - 1);
int z = m_random.random_int(0, CHUNK_SIZE - 1);
int y = static_cast<int>(heightmap[x][z]);
glm::vec3 pos(x, y + 1, z);
auto type = blocks[Chunk::index(pos)];
if (type != 0) {
continue;
}
auto [world_x, world_y, world_z] = Chunk::block_to_world(
x, y + 1, z, chunk_pos.x, chunk_pos.z);
m_chunk.world().entity_manager().add_entity(
SpawnDefaults::PIG.name,
glm::vec3{world_x, world_y, world_z});
}
}
}
}
ServerChunk& ChunkGenerator::chunk() { return m_chunk; }
Random& ChunkGenerator::random() { return m_random; }

View File

@@ -6,6 +6,7 @@
#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>
@@ -363,7 +364,8 @@ void ClientWorld::send_player_water_sound(bool underwater,
}
void ClientWorld::init(std::string_view player_name,
std::shared_ptr<NetworkClient> client) {
std::shared_ptr<NetworkClient> client, RunMode mode) {
m_runmode = mode;
m_entity_manager.init();
m_player_manager.init(player_name);
m_client = client;
@@ -480,8 +482,8 @@ void ClientWorld::stop_client_thread() {
m_game_running = false;
}
void ClientWorld::start_thread_pool() {
int max_threads = std::thread::hardware_concurrency();
int threads = std::min<size_t>(max_threads, 4);
auto threads = Tools::get_client_threads(m_runmode);
Logger::info("Client pool threads {}", threads);
change_pool_threads(threads);
}
void ClientWorld::stop_thread_pool() {
@@ -500,7 +502,6 @@ void ClientWorld::change_pool_threads(int threads) {
m_max_threads = 1;
}
int used_thread = std::clamp(threads, 1, m_max_threads);
Logger::info("Create New Thread Pool Use {} Threads", used_thread);
m_thread_pool.store(std::make_shared<PriorityThreadPool>(used_thread));
}

View File

@@ -92,14 +92,10 @@ void NetworkServer::net_run() {
Logger::info("Server Started!");
}
void NetworkServer::start_server(int port) {
void NetworkServer::start_server(int port, RunMode mode) {
m_port = port;
m_config.set("port", m_port);
start_server();
}
void NetworkServer::start_server() {
m_world.init_world();
m_world.init_world(mode);
net_run();
m_started = true;
}

View File

@@ -141,6 +141,7 @@ void ServerChunk::gen_phase_five() {
m_generator->generate_cave();
m_generator->generate_vegetation();
m_generator->spawn_creature();
m_generator = nullptr;
}

View File

@@ -33,16 +33,31 @@ void ServerEntityManager::update() {
handle_task();
auto view = m_registry.view<BaseServerCreature>();
auto pool = m_world.get_compute_pool();
if (!pool) {
return;
}
auto sessions = m_world.get_all_session();
std::vector<entt::entity> entities;
for (auto e : view) {
const auto& c = view.get<BaseServerCreature>(e);
entities.push_back(e);
}
// parallel block touches disjoint entities only;
// structural registry changes stay on the server thread via m_tasks.
parallel_do(
*pool, entities.begin(), entities.end(), pool->thread_sum(),
[this, &sessions](entt::entity e) {
const auto& c = m_registry.get<BaseServerCreature>(e);
if (!m_world.get_chunk_ref_count(c.transform.position.value)) {
continue;
const auto& entity = m_registry.get<Entity>(e);
destory(entity.id);
return;
}
update_ai(e);
update_move(e);
update_send(e, sessions);
}
});
}
void ServerEntityManager::update_ai(entt::entity e) {
@@ -57,7 +72,8 @@ void ServerEntityManager::update_move(entt::entity e) {
}
void ServerEntityManager::update_send(
entt::entity e, std::span<std::shared_ptr<Session>> sessions) {
entt::entity e,
tbb::concurrent_vector<std::shared_ptr<Session>>& sessions) {
if (!m_registry.all_of<Entity, BaseServerCreature>(e)) {
return;
@@ -102,9 +118,9 @@ void ServerEntityManager::handle_task() {
}
void ServerEntityManager::add_entity(std::string_view name,
const glm::vec3& pos) {
const glm::vec3& world_pos) {
m_tasks.emplace(Command::CREATE,
EntityCreateElement{std::string(name), pos});
EntityCreateElement{std::string(name), world_pos});
}
void ServerEntityManager::destory(EntityID id) {

View File

@@ -7,6 +7,7 @@
#include "Cubed/tools/log.hpp"
#include "Cubed/tools/math_tools.hpp"
#include "Cubed/tools/net_utils.hpp"
#include "Cubed/tools/threas_utils.hpp"
#include "Cubed/tools/uuid.hpp"
#include <ranges>
@@ -178,10 +179,9 @@ void ServerWorld::send_chunk(int task_id, const std::string& uuid,
s->send(make_packet(*rsp));
}
void ServerWorld::init_world() {
void ServerWorld::init_world(RunMode mode) {
m_runmode = mode;
m_entity_manager.init();
m_entity_manager.add_entity("cubed:pig", {0, 225, 0});
register_timer("player disconnect", 5, [this]() {
std::vector<std::string> disconnect;
{
@@ -318,7 +318,7 @@ void ServerWorld::sync_and_collect_missing_chunks(
chunk_acc acc;
if (m_chunks.insert(acc, pos)) {
need_gen_chunks_pos.push_back(pos);
acc->second = ChunkEntity{ChunkState::GENERATING, nullptr, 0};
acc->second = ChunkEntity{ChunkState::GENERATING};
}
}
}
@@ -361,7 +361,7 @@ void ServerWorld::submit_new_chunks(const std::string& uuid,
return dist2(a.first) < dist2(b.first);
});
const int CHUNKS_PER_PRIORITY = m_gen_pool_threads;
const int CHUNKS_PER_PRIORITY = m_gen_threads;
for (size_t i = 0; i < tasks.size(); ++i) {
int priority = 10 + static_cast<int>(i / CHUNKS_PER_PRIORITY);
@@ -411,20 +411,30 @@ void ServerWorld::start_server_thread() {
}
void ServerWorld::start_thread_pool() {
int max_thread = std::thread::hardware_concurrency();
if (m_gen_pool_threads == 0) {
m_gen_pool_threads = change_pool_threads(m_gen_thread_pool,
max_thread - RESERVED_THREADS);
if (m_gen_threads == 0) {
auto gen_threads = Tools::get_server_gen_threads(m_runmode);
Logger::info("Server Gen pool threads {}", gen_threads);
m_gen_threads = change_pool_threads(m_gen_thread_pool, gen_threads);
} else {
m_gen_pool_threads =
change_pool_threads(m_gen_thread_pool, m_gen_pool_threads);
m_gen_threads = change_pool_threads(m_gen_thread_pool, m_gen_threads);
}
if (m_net_pool_threads == 0) {
m_net_pool_threads = change_pool_threads(m_net_thread_pool, 4);
if (m_net_threads == 0) {
auto net_threads = Tools::get_server_net_pool_threads(m_runmode);
Logger::info("Server Net pool threads {}", net_threads);
m_net_threads = change_pool_threads(m_net_thread_pool, net_threads);
} else {
m_net_pool_threads =
change_pool_threads(m_net_thread_pool, m_net_pool_threads);
m_net_threads = change_pool_threads(m_net_thread_pool, m_net_threads);
}
if (m_compute_threads == 0) {
auto compute_threads = Tools::get_server_compute_treads(m_runmode);
Logger::info("Server compute pool threads {}", compute_threads);
m_compute_threads =
change_pool_threads(m_compute_thread_pool, compute_threads);
} else {
m_compute_threads =
change_pool_threads(m_compute_thread_pool, m_compute_threads);
}
}
@@ -459,6 +469,13 @@ void ServerWorld::stop_thread_pool() {
}
m_net_thread_pool.store(nullptr);
Logger::info("Net Thread Pool Stopped");
auto c = m_compute_thread_pool.load();
if (c) {
c->stop();
}
m_compute_thread_pool.store(nullptr);
Logger::info("Compute Thread Pool Stopped");
}
void ServerWorld::serever_run(std::stop_token stoken) {
@@ -910,16 +927,16 @@ void ServerWorld::per_tick_time(int ms) { m_per_tick_time = ms; }
bool ServerWorld::is_tick_running() const { return m_tick_running.load(); }
void ServerWorld::tick_running(bool run) { m_tick_running = run; }
int ServerWorld::gen_pool_threads() const { return m_gen_pool_threads.load(); }
int ServerWorld::gen_pool_threads() const { return m_gen_threads.load(); }
int ServerWorld::max_threads() const { return m_max_threads.load(); }
void ServerWorld::change_pool_threads(ThreadPoolKind kind, int threads) {
switch (kind) {
case ThreadPoolKind::NET:
m_net_pool_threads = change_pool_threads(m_net_thread_pool, threads);
m_net_threads = change_pool_threads(m_net_thread_pool, threads);
break;
case ThreadPoolKind::GEN:
m_gen_pool_threads = change_pool_threads(m_gen_thread_pool, threads);
m_gen_threads = change_pool_threads(m_gen_thread_pool, threads);
break;
}
}
@@ -946,7 +963,6 @@ int ServerWorld::change_pool_threads(
m_max_threads = 1;
}
int used_thread = std::clamp(threads, 1, m_max_threads.load());
Logger::info("Create New Thread Pool Use {} Threads", used_thread);
thread_pool.store(std::make_shared<PriorityThreadPool>(used_thread));
return used_thread;
}
@@ -1011,9 +1027,10 @@ void ServerWorld::set_chunk_load_style(int id) {
int ServerWorld::chunk_size() const { return m_chunks.size(); }
std::vector<std::shared_ptr<Session>> ServerWorld::get_all_session() const {
tbb::concurrent_vector<std::shared_ptr<Session>>
ServerWorld::get_all_session() const {
std::shared_lock lock(m_players_mutex);
std::vector<std::shared_ptr<Session>> sessions;
tbb::concurrent_vector<std::shared_ptr<Session>> sessions;
for (const auto& [_, player] : m_players) {
sessions.emplace_back(player.get_session());
}
@@ -1114,5 +1131,8 @@ BlockType ServerWorld::get_block_tpye(const glm::ivec3& block_pos) const {
}
int ServerWorld::get_per_tick_time() const { return m_per_tick_time; }
ServerEntityManager& ServerWorld::entity_manager() { return m_entity_manager; }
std::shared_ptr<ThreadPool> ServerWorld::get_compute_pool() {
return m_compute_thread_pool.load();
}
} // namespace Cubed

View File

@@ -129,10 +129,21 @@ void WorldScene::on_enter() {
load_config();
m_error_ui.init();
m_client = std::make_shared<NetworkClient>(m_client_world);
RunMode mode = RunMode::HYBRID;
if (m_argument.direct_enter) {
if (m_argument.ip) {
mode = RunMode::CLIENT_ONLY;
}
} else {
if (!m_scene_manager.world_scene_param().host_game) {
mode = RunMode::CLIENT_ONLY;
}
}
if (m_argument.direct_enter) {
if (!m_argument.ip) {
ChunkGenerator::init();
m_server.start_server(*m_argument.port);
m_server.start_server(*m_argument.port, mode);
m_client->start("127.0.0.1", *m_argument.port);
} else {
m_client->start(*m_argument.ip, *m_argument.port);
@@ -148,7 +159,7 @@ void WorldScene::on_enter() {
} else {
ChunkGenerator::init();
}
m_server.start_server(param.port);
m_server.start_server(param.port, mode);
}
m_client->start(param.ip, param.port);
@@ -157,7 +168,8 @@ void WorldScene::on_enter() {
// init will send packet
try {
m_client_world.init(m_argument.player.value_or("Unknown"), m_client);
m_client_world.init(m_argument.player.value_or("Unknown"), m_client,
mode);
Logger::info("World Init Success");
m_camera.camera_init(&m_client_world.get_player());