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.
This commit is contained in:
2026-08-06 20:13:55 +08:00
parent 33cfea1f36
commit 94d92f2159
10 changed files with 90 additions and 38 deletions

View File

@@ -3,9 +3,12 @@
namespace Cubed { namespace Cubed {
using EntityID = uint64_t; using EntityID = uint64_t;
enum class EntityType { CREATURE, ITEM };
struct Entity { struct Entity {
EntityID id; EntityID id;
explicit Entity(EntityID id) : id(id) {} EntityType type;
Entity(EntityID id, EntityType type) : id(id), type(type) {}
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -12,15 +12,20 @@ class ServerWorld;
class Session; class Session;
class ServerEntityManager { class ServerEntityManager {
public: public:
static constexpr size_t PER_CREATURE_LIMITS = 100;
ServerEntityManager(ServerWorld& world); ServerEntityManager(ServerWorld& world);
void init(); void init();
void update(); void update();
// not thread safe void add_creature(std::string_view name, const glm::vec3& world_pos);
void add_entity(std::string_view name, const glm::vec3& world_pos);
void destory(EntityID id); void destory(EntityID id);
void handle_player_login(std::shared_ptr<Session> session); void handle_player_login(std::shared_ptr<Session> session);
size_t max_creature_sum() const;
size_t creature_sum() const;
size_t entity_sum() const;
private: private:
enum class Command { CREATE, SEND_ALL_ENTITIES, DESTORY }; enum class Command { CREATE, SEND_ALL_ENTITIES, DESTORY };
struct EntityCreateElement { struct EntityCreateElement {
@@ -43,6 +48,8 @@ private:
std::variant<std::shared_ptr<Session>, EntityCreateElement, EntityID>; std::variant<std::shared_ptr<Session>, EntityCreateElement, EntityID>;
using TaskPair = std::pair<Command, TaskElement>; using TaskPair = std::pair<Command, TaskElement>;
ServerWorld& m_world; ServerWorld& m_world;
std::atomic<size_t> m_creature_sum{0};
std::atomic<size_t> m_entity_sum{0};
tbb::concurrent_queue<TaskPair> m_tasks; tbb::concurrent_queue<TaskPair> m_tasks;
entt::registry m_registry; entt::registry m_registry;
EntityID m_next = 0; EntityID m_next = 0;

View File

@@ -98,6 +98,8 @@ public:
uint32_t get_chunk_ref_count(const glm::vec3& pos) const; uint32_t get_chunk_ref_count(const glm::vec3& pos) const;
ServerEntityManager& entity_manager(); ServerEntityManager& entity_manager();
std::shared_ptr<ThreadPool> get_compute_pool(); std::shared_ptr<ThreadPool> get_compute_pool();
size_t player_sum() const;
int get_block(const glm::ivec3& block_pos) const override; int get_block(const glm::ivec3& block_pos) const override;
bool is_solid(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; bool can_pass_block(const glm::ivec3& block_pos) const override;

View File

@@ -42,6 +42,8 @@ public:
bool is_recording() const; bool is_recording() const;
RunMode runmode() const;
private: private:
enum class PauseUI { PAUSE_MENU, INVENTORY }; enum class PauseUI { PAUSE_MENU, INVENTORY };
SceneManager& m_scene_manager; SceneManager& m_scene_manager;
@@ -61,6 +63,7 @@ private:
ErrorUI m_error_ui; ErrorUI m_error_ui;
const Argument& m_argument; const Argument& m_argument;
VoiceInputType m_input_type; VoiceInputType m_input_type;
RunMode m_runmode = RunMode::HYBRID;
bool handle_mouse_move_event(const MouseMoveEvent& e) override; bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override; bool handle_mouse_button_event(const MouseButtonEvent& e) override;
bool handle_window_resize_event(const WindowResizeEvent& e) override; bool handle_window_resize_event(const WindowResizeEvent& e) override;

View File

@@ -343,17 +343,20 @@ void DevPanel::show_world_tab_item() {
if (ImGui::BeginTabItem("world")) { if (ImGui::BeginTabItem("world")) {
if (ImGui::BeginTabBar("World Kind")) { if (ImGui::BeginTabBar("World Kind")) {
auto& param = m_world_scene.scene_manager().world_scene_param(); auto mode = m_world_scene.runmode();
if (param.host_game) { if (mode == RunMode::HYBRID || mode == RunMode::SERVER_ONLY) {
if (ImGui::BeginTabItem("ServerWorld")) { if (ImGui::BeginTabItem("ServerWorld")) {
show_server_world_table_bar(); show_server_world_table_bar();
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
} }
if (ImGui::BeginTabItem("Client World")) { if (mode == RunMode::HYBRID || mode == RunMode::CLIENT_ONLY) {
show_client_world_table_bar(); if (ImGui::BeginTabItem("Client World")) {
ImGui::EndTabItem(); show_client_world_table_bar();
ImGui::EndTabItem();
}
} }
ImGui::EndTabBar(); ImGui::EndTabBar();
} }
@@ -366,47 +369,49 @@ void DevPanel::show_world_tab_item() {
} }
void DevPanel::show_server_world_table_bar() { void DevPanel::show_server_world_table_bar() {
auto& world = m_world_scene.server_world();
ImGui::Text("ChunkGenerator Seed: %u", ChunkGenerator::seed()); ImGui::Text("ChunkGenerator Seed: %u", ChunkGenerator::seed());
ImGui::Text("Pool Threads %d Max Support Threads %d Reserved Threads %d", ImGui::Text("Pool Threads %d Max Support Threads %d Reserved Threads %d",
m_world_scene.server_world().gen_pool_threads(), world.gen_pool_threads(), world.max_threads(),
m_world_scene.server_world().max_threads(), RESERVED_THREADS); RESERVED_THREADS);
ImGui::SliderInt("Set Pool Threads", &m_threads, 1, ImGui::SliderInt("Set Pool Threads", &m_threads, 1, world.max_threads());
m_world_scene.server_world().max_threads());
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Set")) { if (ImGui::Button("Set")) {
m_world_scene.server_world().change_pool_threads( world.change_pool_threads(ServerWorld::ThreadPoolKind::GEN, m_threads);
ServerWorld::ThreadPoolKind::GEN, m_threads);
} }
if (m_threads > if (m_threads > world.max_threads() - RESERVED_THREADS) {
m_world_scene.server_world().max_threads() - RESERVED_THREADS) {
ImGui::TextColored( ImGui::TextColored(
ImVec4(1.0f, 1.0f, 0.0f, 1.0f), ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Waring: When the threads in the thread pool exceed \n(maximum " "Waring: When the threads in the thread pool exceed \n(maximum "
"threads minus reserved threads), \nit may cause stuttering."); "threads minus reserved threads), \nit may cause stuttering.");
} }
m_chunk_style = m_world_scene.server_world().chunk_load_style(); m_chunk_style = world.chunk_load_style();
if (ImGui::Combo("ChunkLoadStyle", &m_chunk_style, CHUNK_LOAD_STYLE, if (ImGui::Combo("ChunkLoadStyle", &m_chunk_style, CHUNK_LOAD_STYLE,
IM_ARRAYSIZE(CHUNK_LOAD_STYLE))) { IM_ARRAYSIZE(CHUNK_LOAD_STYLE))) {
m_world_scene.server_world().set_chunk_load_style(m_chunk_style); world.set_chunk_load_style(m_chunk_style);
} }
if (ImGui::Button("Request Chunk Build")) { if (ImGui::Button("Request Chunk Build")) {
m_world_scene.server_world().need_gen(m_player->get_uuid()); world.need_gen(m_player->get_uuid());
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Checkbox("Gen Thread", &m_gen_thread_running)) { if (ImGui::Checkbox("Gen Thread", &m_gen_thread_running)) {
if (m_gen_thread_running) { if (m_gen_thread_running) {
m_world_scene.server_world().start_gen_thread(); world.start_gen_thread();
} else { } else {
m_world_scene.server_world().stop_gen_thread(); world.stop_gen_thread();
} }
} }
ImGui::Text("Server Chunk Size %d", ImGui::Text("Server Chunk Size %d", world.chunk_size());
m_world_scene.server_world().chunk_size()); ImGui::SameLine();
ImGui::Text("Server Player Sum %zu", world.player_sum());
ImGui::Text("Server Entity Sum %zu", world.entity_manager().entity_sum());
ImGui::SameLine();
ImGui::Text("Server Creature Sum %zu",
world.entity_manager().creature_sum());
if (ImGui::BeginTabBar("World Settings")) { if (ImGui::BeginTabBar("World Settings")) {
if (ImGui::BeginTabItem("Time")) { if (ImGui::BeginTabItem("Time")) {
show_time_table_bar(); show_time_table_bar();

View File

@@ -829,7 +829,7 @@ void ChunkGenerator::spawn_creature() {
} }
auto [world_x, world_y, world_z] = Chunk::block_to_world( auto [world_x, world_y, world_z] = Chunk::block_to_world(
x, y + 1, z, chunk_pos.x, chunk_pos.z); x, y + 1, z, chunk_pos.x, chunk_pos.z);
m_chunk.world().entity_manager().add_entity( m_chunk.world().entity_manager().add_creature(
SpawnDefaults::PIG.name, SpawnDefaults::PIG.name,
glm::vec3{world_x, world_y, world_z}); glm::vec3{world_x, world_y, world_z});
} }

View File

@@ -31,8 +31,9 @@ void ClientEntityManager::init() {
m_factories.emplace("cubed:pig", [this](EntityID id) { m_factories.emplace("cubed:pig", [this](EntityID id) {
BaseClientCreature c; BaseClientCreature c;
c.model = ModelManager::instance().get_model_id("cubed:pig"); c.model = ModelManager::instance().get_model_id("cubed:pig");
create_entity_in_registry(id, Entity{id}, EntityInfo{"cubed:pig", ""}, create_entity_in_registry(id, Entity{id, EntityType::CREATURE},
std::move(c), PigTag{}, RenderTransform{}); EntityInfo{"cubed:pig", ""}, std::move(c),
PigTag{}, RenderTransform{});
}); });
} }
// not thread safe // not thread safe

View File

@@ -27,8 +27,8 @@ void ServerEntityManager::init() {
c.movement.deceleration = PigDefaults::DECELERATION; c.movement.deceleration = PigDefaults::DECELERATION;
c.velocity.max.x = c.velocity.max.z = PigDefaults::MAX_SPEED; c.velocity.max.x = c.velocity.max.z = PigDefaults::MAX_SPEED;
return create_entity_in_factory( return create_entity_in_factory(
Entity{m_next}, EntityInfo{"cubed:pig", ""}, std::move(c), PigTag{}, Entity{m_next, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""},
AIBase{}, WanderAITag{}, MoveBoost{}); std::move(c), PigTag{}, AIBase{}, WanderAITag{}, MoveBoost{});
}); });
} }
void ServerEntityManager::update() { void ServerEntityManager::update() {
@@ -134,8 +134,13 @@ void ServerEntityManager::handle_task() {
} }
} }
void ServerEntityManager::add_entity(std::string_view name, void ServerEntityManager::add_creature(std::string_view name,
const glm::vec3& world_pos) { const glm::vec3& world_pos) {
if (m_creature_sum.fetch_add(1) >= max_creature_sum()) {
m_creature_sum.fetch_sub(1);
return;
}
++m_entity_sum;
m_tasks.emplace(Command::CREATE, m_tasks.emplace(Command::CREATE,
EntityCreateElement{std::string(name), world_pos}); EntityCreateElement{std::string(name), world_pos});
} }
@@ -149,6 +154,15 @@ void ServerEntityManager::handle_player_login(
m_tasks.emplace(Command::SEND_ALL_ENTITIES, std::move(session)); m_tasks.emplace(Command::SEND_ALL_ENTITIES, std::move(session));
} }
size_t ServerEntityManager::max_creature_sum() const {
return PER_CREATURE_LIMITS * m_world.player_sum();
}
size_t ServerEntityManager::creature_sum() const {
return m_creature_sum.load();
}
size_t ServerEntityManager::entity_sum() const { return m_entity_sum.load(); }
void ServerEntityManager::create_entity(std::string_view name, void ServerEntityManager::create_entity(std::string_view name,
const glm::vec3& pos) { const glm::vec3& pos) {
ASSERT(m_factories.contains(name)); ASSERT(m_factories.contains(name));
@@ -159,6 +173,7 @@ void ServerEntityManager::create_entity(std::string_view name,
ASSERT(t); ASSERT(t);
t->transform.position.value = pos; t->transform.position.value = pos;
} }
handle_entity_create(e, name, pos); handle_entity_create(e, name, pos);
} }
@@ -193,12 +208,23 @@ void ServerEntityManager::handle_entity_create(EntityID id,
} }
void ServerEntityManager::handle_entity_destory(EntityID id) { void ServerEntityManager::handle_entity_destory(EntityID id) {
if (m_entity_sum == 0) {
Logger::error("entity sum is 0!");
return;
}
acc a; acc a;
if (!m_entities.find(a, id)) { if (!m_entities.find(a, id)) {
return; return;
} }
auto e = m_registry.try_get<Entity>(a->second);
ASSERT(e);
if (e->type == EntityType::CREATURE) {
--m_creature_sum;
}
m_registry.destroy(a->second); m_registry.destroy(a->second);
m_entities.erase(a); m_entities.erase(a);
--m_entity_sum;
auto sessions = m_world.get_all_session(); auto sessions = m_world.get_all_session();
Arena arena; Arena arena;
auto* s2c = Arena::Create<S2CEntityDestory>(&arena); auto* s2c = Arena::Create<S2CEntityDestory>(&arena);

View File

@@ -896,7 +896,7 @@ void ServerWorld::handle_block_change(const BlockChangeReq& req) {
void ServerWorld::handle_entity_create(C2SEntityCreateRequest& req) { void ServerWorld::handle_entity_create(C2SEntityCreateRequest& req) {
m_entity_manager.add_entity(req.name(), Tools::get_net_vec3(req.pos())); m_entity_manager.add_creature(req.name(), Tools::get_net_vec3(req.pos()));
} }
void ServerWorld::handle_entity_destory(C2SEntityDestoryRequest& req) { void ServerWorld::handle_entity_destory(C2SEntityDestoryRequest& req) {
m_entity_manager.destory(req.id()); m_entity_manager.destory(req.id());
@@ -1133,4 +1133,7 @@ ServerEntityManager& ServerWorld::entity_manager() { return m_entity_manager; }
std::shared_ptr<ThreadPool> ServerWorld::get_compute_pool() { std::shared_ptr<ThreadPool> ServerWorld::get_compute_pool() {
return m_compute_thread_pool.load(); return m_compute_thread_pool.load();
} }
size_t ServerWorld::player_sum() const { return m_player_sum.load(); }
} // namespace Cubed } // namespace Cubed

View File

@@ -129,21 +129,20 @@ void WorldScene::on_enter() {
load_config(); load_config();
m_error_ui.init(); m_error_ui.init();
m_client = std::make_shared<NetworkClient>(m_client_world); m_client = std::make_shared<NetworkClient>(m_client_world);
RunMode mode = RunMode::HYBRID;
if (m_argument.direct_enter) { if (m_argument.direct_enter) {
if (m_argument.ip) { if (m_argument.ip) {
mode = RunMode::CLIENT_ONLY; m_runmode = RunMode::CLIENT_ONLY;
} }
} else { } else {
if (!m_scene_manager.world_scene_param().host_game) { if (!m_scene_manager.world_scene_param().host_game) {
mode = RunMode::CLIENT_ONLY; m_runmode = RunMode::CLIENT_ONLY;
} }
} }
if (m_argument.direct_enter) { if (m_argument.direct_enter) {
if (!m_argument.ip) { if (!m_argument.ip) {
ChunkGenerator::init(); ChunkGenerator::init();
m_server.start_server(*m_argument.port, mode); m_server.start_server(*m_argument.port, m_runmode);
m_client->start("127.0.0.1", *m_argument.port); m_client->start("127.0.0.1", *m_argument.port);
} else { } else {
m_client->start(*m_argument.ip, *m_argument.port); m_client->start(*m_argument.ip, *m_argument.port);
@@ -159,7 +158,7 @@ void WorldScene::on_enter() {
} else { } else {
ChunkGenerator::init(); ChunkGenerator::init();
} }
m_server.start_server(param.port, mode); m_server.start_server(param.port, m_runmode);
} }
m_client->start(param.ip, param.port); m_client->start(param.ip, param.port);
@@ -169,7 +168,7 @@ void WorldScene::on_enter() {
try { 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); m_runmode);
Logger::info("World Init Success"); Logger::info("World Init Success");
m_camera.camera_init(&m_client_world.get_player()); m_camera.camera_init(&m_client_world.get_player());
@@ -461,6 +460,9 @@ void WorldScene::handle_chat_message(ChatMessage& message) {
bool WorldScene::is_recording() const { bool WorldScene::is_recording() const {
return m_client_world.get_audio().audio_recording().is_recording(); return m_client_world.get_audio().audio_recording().is_recording();
} }
RunMode WorldScene::runmode() const { return m_runmode; }
void WorldScene::set_error(std::string_view error) { void WorldScene::set_error(std::string_view error) {
Logger::error("WorldScene Error Set {}", error); Logger::error("WorldScene Error Set {}", error);
m_error_ui.set_error(error); m_error_ui.set_error(error);