feat(gameplay): add entity managers and refactor to ECS components

This commit is contained in:
2026-07-29 18:06:04 +08:00
parent d15037e7a3
commit 3a9ab6a14a
20 changed files with 294 additions and 71 deletions

View File

@@ -0,0 +1,54 @@
#pragma once
#include "Cubed/gameplay/ecs/entity.hpp"
#include "glm/ext/vector_float3.hpp"
#include "world/entity.pb.h"
#include <entt/entt.hpp>
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
namespace Cubed {
class ClientWorld;
class ClientEntityManager {
public:
enum class Command { CREATE };
ClientEntityManager(ClientWorld& world);
void update();
void init();
// not thread safe
void add_entity(EntityID id, std::string_view name, const glm::vec3& pos);
void receive_entity_create(S2CEntityCreate& s2c);
private:
struct EntityCreateElement {
EntityID id;
std::string name;
glm::vec3 pos;
};
using EntityMap = tbb::concurrent_hash_map<EntityID, entt::entity>;
using acc = EntityMap::accessor;
using cacc = EntityMap::const_accessor;
using CreateFunc = std::function<void(EntityID id)>;
using TaskElement = std::variant<EntityCreateElement>;
using TaskPair = std::pair<Command, TaskElement>;
ClientWorld& m_world;
entt::registry m_registry;
EntityMap m_entities;
std::unordered_map<std::string_view, CreateFunc> m_factories;
tbb::concurrent_queue<TaskPair> m_tasks;
void handle_task();
template <typename... Args> void add_entity(EntityID id, Args&&... args) {
auto entity = m_registry.create();
((m_registry.emplace<std::remove_cvref_t<Args>>(
entity, std::forward<Args>(args))),
...);
m_entities.emplace(id, entity);
return;
}
};
} // namespace Cubed

View File

@@ -1,15 +1,9 @@
#pragma once
#include "Cubed/gameplay/ecs/animation.hpp"
#include "Cubed/gameplay/ecs/entity.hpp"
#include "Cubed/gameplay/ecs/identity.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
#include "Cubed/gameplay/model.hpp"
namespace Cubed {
struct ClientEntity {
Entity entity;
EntityInfo info;
struct BaseClientCreature {
Transform transform;

View File

@@ -1,32 +1,27 @@
#pragma once
#include "Cubed/gameplay/ecs/entity.hpp"
#include "Cubed/gameplay/ecs/health.hpp"
#include "Cubed/gameplay/ecs/identity.hpp"
#include "Cubed/gameplay/ecs/movement.hpp"
#include "Cubed/gameplay/ecs/state.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
#include "Cubed/gameplay/hitbox.hpp"
namespace Cubed {
struct ServerEntity {
Entity entity;
struct BaseServerCreature {
Transform transform{};
EntityInfo info;
Velocity velocity{};
Transform transform;
Movement movement{};
Velocity velocity;
MoveState move_state{};
Movement movement;
Direction direction{};
MoveState move_state;
Gravity gravity{};
Direction direction;
Health health{};
Gravity gravity;
Health health;
HitboxID hitbox;
HitboxID hitbox{};
};
} // namespace Cubed

View File

@@ -16,9 +16,9 @@ struct Direction {
};
struct Transform {
Position position;
Orientation orientation;
Direction direction;
Position position{};
Orientation orientation{};
Direction direction{};
};
} // namespace Cubed

View File

@@ -60,6 +60,7 @@ enum class PacketEnum : uint16_t {
BLOCK_CHANGE_REQ = 3003,
BLOCK_CHANGE_RSP = 3004,
S2C_CLEAR_ALL_CHUNKS = 3005,
S2C_ENTITY_CREATE = 3006,
UPDATE_TIME = 3006,
CHAT_MSG = 4001,
VOICE_MSG = 4002,
@@ -111,6 +112,9 @@ template <> constexpr uint16_t get_packet_id<BlockChangeRsp>() {
template <> constexpr uint16_t get_packet_id<S2C_ClearAllChunks>() {
return std::to_underlying(PacketEnum::S2C_CLEAR_ALL_CHUNKS);
}
template <> constexpr uint16_t get_packet_id<S2CEntityCreate>() {
return std::to_underlying(PacketEnum::S2C_ENTITY_CREATE);
}
template <> constexpr uint16_t get_packet_id<UpdateTime>() {
return std::to_underlying(PacketEnum::UPDATE_TIME);
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "Cubed/gameplay/ecs/entity.hpp"
#include "glm/ext/vector_float3.hpp"
#include <entt/entt.hpp>
#include <tbb/concurrent_hash_map.h>
namespace Cubed {
class ServerWorld;
class ServerEntityManager {
public:
ServerEntityManager(ServerWorld& world);
void init();
// not thread safe
void add_entity(std::string_view name, const glm::vec3& pos);
private:
using EntityMap = tbb::concurrent_hash_map<EntityID, entt::entity>;
using acc = EntityMap::accessor;
using cacc = EntityMap::const_accessor;
using CreateFunc = std::function<EntityID()>;
ServerWorld& m_world;
entt::registry m_registry;
EntityID m_next = 0;
EntityMap m_entities;
std::unordered_map<std::string_view, CreateFunc> m_factories;
void send_entity_create(EntityID id, std::string_view name,
const glm::vec3& pos);
template <typename... Args> EntityID add_entity(Args&&... args) {
auto entity = m_registry.create();
((m_registry.emplace<std::remove_cvref_t<Args>>(
entity, std::forward<Args>(args))),
...);
auto id = m_next++;
m_entities.emplace(id, entity);
return id;
}
};
} // namespace Cubed

View File

@@ -7,6 +7,7 @@
#include "Cubed/gameplay/packet.hpp" // IWYU pragma: keep
#include "Cubed/gameplay/river_worm.hpp"
#include "Cubed/gameplay/server_chunk.hpp"
#include "Cubed/gameplay/server_entity_manager.hpp"
#include "Cubed/gameplay/server_player.hpp"
#include "Cubed/gameplay/world.hpp"
#include "Cubed/tools/priority_thread_pool.hpp"
@@ -87,6 +88,8 @@ public:
void handle_voice_message(VoiceMsg& msg);
int chunk_size() const;
std::vector<std::shared_ptr<Session>> get_all_session() const;
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;
@@ -132,7 +135,7 @@ private:
using uuid_cacc = PlayerUUIDMap::const_accessor;
Config& m_config;
ServerEntityManager m_entity_manager;
// key = uuid
PlayerHashMap m_players;
ChunkHashMap m_chunks;
@@ -162,7 +165,7 @@ private:
std::atomic<bool> m_tick_running{true};
std::atomic<int> m_per_tick_time = DEFAULT_PER_TICK_TIME; // ms
mutable std::shared_mutex m_player_mutex;
mutable std::shared_mutex m_players_mutex;
std::mutex m_need_gen_queue_mutex;
std::condition_variable_any m_gen_cv;

View File

@@ -1,5 +1,7 @@
#pragma once
#include "Cubed//gameplay/ecs/server_entity.hpp"
#include "Cubed/gameplay/ecs/movement.hpp"
#include "Cubed/gameplay/ecs/state.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
#include "Cubed/gameplay/world.hpp"
namespace Cubed {
@@ -7,10 +9,6 @@ class PhysicalSystem {
public:
static glm::vec3 get_move_distance(float dt, const Direction& d,
const Velocity& v);
static std::tuple<bool, bool, bool> update(float dt, ServerEntity& e,
World& world);
static std::tuple<bool, bool, bool>
update(float dt, ServerEntity& e, World& world, glm::vec3& moved_pos);
static std::tuple<bool, bool, bool>
update(float dt, World& world, glm::vec3& moved_pos, Velocity& v,

View File

@@ -1,12 +1,12 @@
#pragma once
#include "Cubed/gameplay/ecs/server_entity.hpp"
#include "Cubed/gameplay/ecs/movement.hpp"
#include "Cubed/gameplay/ecs/state.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
namespace Cubed {
class SpeedSystem {
public:
static void update(float dt, ServerEntity& e);
static void update(float dt, Velocity& v, MoveState& move_state,
Movement& movement, Direction& direction,
const Gravity& g);

View File

@@ -0,0 +1,7 @@
#pragma once
#include <type_traits>
namespace Cubed {
template <typename T>
concept Ptr = std::is_pointer_v<T>;
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include "Cubed/tools/cubed_concepts.hpp"
#include "common/vector3.pb.h"
#include "glm/ext/vector_float3.hpp"
namespace Cubed {
namespace Tools {
template <Ptr T> void set_net_pos(T ptr, const glm::vec3& pos) {
Vec3* p = ptr->mutable_pos();
p->set_x(pos.x);
p->set_y(pos.y);
p->set_z(pos.z);
}
} // namespace Tools
} // namespace Cubed

View File

@@ -100,4 +100,6 @@ target_sources(${PROJECT_NAME}
gameplay/systems/physical_system.cpp
gameplay/systems/speed_system.cpp
gameplay/client_player_manager.cpp
gameplay/server_entity_manager.cpp
gameplay/client_entity_manager.cpp
)

View File

@@ -0,0 +1,53 @@
#include "Cubed/gameplay/client_entity_manager.hpp"
#include "Cubed/gameplay/creatures/pig.hpp"
#include "Cubed/gameplay/ecs/client_entity.hpp"
#include "Cubed/gameplay/ecs/identity.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
#include "Cubed/render/model_manager.hpp"
#include "Cubed/tools/cubed_assert.hpp"
namespace Cubed {
ClientEntityManager::ClientEntityManager(ClientWorld& world) : m_world(world) {}
void ClientEntityManager::update() { handle_task(); }
void ClientEntityManager::init() {
m_factories.emplace("cubed:pig", [this](EntityID id) {
BaseClientCreature c;
c.model = ModelManager::instance().get_model_id("cubed:pig");
add_entity(id, Entity{id}, EntityInfo{"cubed:pig", ""}, std::move(c),
PigTag{});
});
}
// not thread safe
void ClientEntityManager::add_entity(EntityID id, std::string_view name,
const glm::vec3& pos) {
m_factories[name](id);
acc a;
ASSERT(m_entities.find(a, id));
auto* c = m_registry.try_get<Transform>(a->second);
ASSERT(c);
c->position.value = pos;
}
void ClientEntityManager::receive_entity_create(S2CEntityCreate& s2c) {
EntityCreateElement c;
c.id = s2c.id();
c.name = s2c.name();
c.pos = {s2c.pos().x(), s2c.pos().y(), s2c.pos().z()};
m_tasks.emplace(Command::CREATE, std::move(c));
}
void ClientEntityManager::handle_task() {
TaskPair pair;
while (m_tasks.try_pop(pair)) {
switch (pair.first) {
case Command::CREATE:
auto* p = std::get_if<EntityCreateElement>(&pair.second);
ASSERT(p);
add_entity(p->id, p->name, p->pos);
}
}
}
} // namespace Cubed

View File

@@ -1,4 +1,3 @@
#pragma once
#include "Cubed/gameplay/client_player_manager.hpp"
#include "Cubed/gameplay/client_world.hpp"

View File

@@ -0,0 +1,53 @@
#include "Cubed/gameplay/server_entity_manager.hpp"
#include "Cubed/gameplay/creatures/pig.hpp"
#include "Cubed/gameplay/ecs/identity.hpp"
#include "Cubed/gameplay/ecs/server_entity.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/gameplay/server_world.hpp"
#include "Cubed/gameplay/session.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/net_utils.hpp"
using namespace google::protobuf;
namespace Cubed {
ServerEntityManager::ServerEntityManager(ServerWorld& world) : m_world(world) {}
void ServerEntityManager::init() {
m_factories.try_emplace("cubed:pig", [this]() {
BaseServerCreature c;
c.hitbox = HitboxManager::instance().get_hitbox_id("cubed:pig");
return add_entity(Entity{m_next}, EntityInfo{"cubed:pig", ""},
std::move(c), PigTag{});
});
}
void ServerEntityManager::add_entity(std::string_view name,
const glm::vec3& pos) {
ASSERT(m_factories.contains(name));
auto e = m_factories[name]();
acc c;
if (m_entities.find(c, e)) {
auto t = m_registry.try_get<BaseServerCreature>(c->second);
ASSERT(t);
t->transform.position.value = pos;
}
send_entity_create(e, name, pos);
}
void ServerEntityManager::send_entity_create(EntityID id, std::string_view name,
const glm::vec3& pos) {
auto sessions = m_world.get_all_session();
Arena arena;
auto* s2c = Arena::Create<S2CEntityCreate>(&arena);
s2c->set_id(id);
s2c->set_name(name);
Tools::set_net_pos(s2c, pos);
for (auto& s : sessions) {
s->send(make_packet(*s2c));
}
}
} // namespace Cubed

View File

@@ -16,7 +16,8 @@ using namespace google::protobuf;
namespace fs = std::filesystem;
using nlohmann::json;
namespace Cubed {
ServerWorld::ServerWorld(Config& config) : m_config(config) {}
ServerWorld::ServerWorld(Config& config)
: m_config(config), m_entity_manager(*this) {}
ServerWorld::~ServerWorld() { stop(); }
@@ -85,7 +86,7 @@ void ServerWorld::send_time() {
rsp->set_day_tick(m_day_tick);
rsp->set_game_tick(m_game_ticks);
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [uuid, player] : m_players) {
player.get_session()->send(make_packet(*rsp), 3);
}
@@ -96,7 +97,7 @@ void ServerWorld::send_chunk(int task_id, const std::string& uuid,
ChunkPos pos) {
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it == m_players.end()) {
return;
@@ -159,7 +160,7 @@ void ServerWorld::send_chunk(int task_id, const std::string& uuid,
}
std::shared_ptr<Session> s;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it != m_players.end()) {
s = it->second.get_session();
@@ -176,10 +177,12 @@ void ServerWorld::send_chunk(int task_id, const std::string& uuid,
void ServerWorld::init_world() {
m_entity_manager.init();
register_timer("player disconnect", 5, [this]() {
std::vector<std::string> disconnect;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [uuid, player] : m_players) {
if (player.is_disconnect(m_game_ticks)) {
disconnect.emplace_back(uuid);
@@ -243,7 +246,7 @@ void ServerWorld::gen_chunks_internal(const std::string& uuid) {
ChunkPosSet old_set;
sync_and_collect_missing_chunks(need_gen_chunks_pos, required_chunks_set);
{
std::lock_guard lock(m_player_mutex);
std::lock_guard lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it == m_players.end()) {
return;
@@ -564,7 +567,7 @@ void ServerWorld::sync_player_pos(const C2S_PlayerInfo& prsp) {
auto yaw = prsp.yaw();
auto pitch = prsp.pitch();
{
std::lock_guard lock(m_player_mutex);
std::lock_guard lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it == m_players.end()) {
Logger::warn("Player {} is not in this Server", uuid);
@@ -583,7 +586,7 @@ void ServerWorld::sync_player_pos(const C2S_PlayerInfo& prsp) {
// update other player pos;
std::vector<std::shared_ptr<Session>> other;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [o_uuid, player] : m_players) {
if (o_uuid == uuid) {
continue;
@@ -624,7 +627,7 @@ void ServerWorld::sync_player_water_sound(const PlayerWaterSound& rsp) {
std::vector<std::shared_ptr<Session>> other;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [o_uuid, player] : m_players) {
if (o_uuid == uuid) {
continue;
@@ -660,7 +663,7 @@ void ServerWorld::handle_player_login(const std::string& name,
Logger::info("Player {} (uuid {}) join the world", name, uuid);
bool sucess = true;
{
std::lock_guard lock(m_player_mutex);
std::lock_guard lock(m_players_mutex);
auto [_, inserted] = m_players.emplace(
std::piecewise_construct, std::forward_as_tuple(std::string(uuid)),
std::forward_as_tuple(name, uuid, *this, session, m_game_ticks));
@@ -710,7 +713,7 @@ void ServerWorld::handle_player_exit(const std::string& uuid) {
ChunkPosSet old_set;
std::string name;
{
std::lock_guard lock(m_player_mutex);
std::lock_guard lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it != m_players.end()) {
name = it->second.get_name();
@@ -737,7 +740,7 @@ void ServerWorld::handle_player_exit(const std::string& uuid) {
std::vector<std::shared_ptr<Session>> sessions;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [uuid, player] : m_players) {
sessions.emplace_back(player.get_session());
}
@@ -754,7 +757,7 @@ void ServerWorld::handle_player_exit(const std::string& uuid) {
}
glm::vec3 ServerWorld::get_player_pos(const std::string& uuid) const {
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it == m_players.end()) {
Logger::error("Can't find player uuid {}", uuid);
@@ -766,7 +769,7 @@ glm::vec3 ServerWorld::get_player_pos(const std::string& uuid) const {
void ServerWorld::handle_chunk_req(int task_id, const std::string& uuid,
ChunkPos pos) {
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
auto it = m_players.find(uuid);
if (it == m_players.end()) {
return;
@@ -804,7 +807,7 @@ void ServerWorld::handle_voice_message(VoiceMsg& msg) {
std::vector<std::shared_ptr<Session>> session;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [key, player] : m_players) {
if (key == uuid) {
continue;
@@ -851,7 +854,7 @@ void ServerWorld::handle_block_change(const BlockChangeReq& req) {
std::vector<std::shared_ptr<Session>> sessions;
auto chunk_pos = get_chunk_pos(x, z);
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [uuid, player] : m_players) {
if (player.has_player(chunk_pos)) {
auto session = player.get_session();
@@ -934,7 +937,7 @@ void ServerWorld::send_server_stop() {
Arena arena;
auto* rsp = Arena::Create<LogoutRsp>(&arena);
rsp->set_server_stop(true);
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [uuid, player] : m_players) {
player.get_session()->send(make_packet(*rsp), 0);
}
@@ -947,7 +950,7 @@ void ServerWorld::boardcast_message(const std::string& name,
std::vector<std::shared_ptr<Session>> m_session;
{
std::shared_lock lock(m_player_mutex);
std::shared_lock lock(m_players_mutex);
for (auto& [_, p] : m_players) {
m_session.emplace_back(p.get_session());
}
@@ -990,6 +993,15 @@ 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 {
std::shared_lock lock(m_players_mutex);
std::vector<std::shared_ptr<Session>> sessions;
for (const auto& [_, player] : m_players) {
sessions.emplace_back(player.get_session());
}
return sessions;
}
int ServerWorld::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;

View File

@@ -99,21 +99,6 @@ bool update_z(const glm::vec3& pos, const glm::vec3& distance, World& world,
}
} // namespace
std::tuple<bool, bool, bool> PhysicalSystem::update(float dt, ServerEntity& e,
World& world) {
glm::vec3 pos = e.transform.position.value;
auto ans = update(dt, e, world, pos);
e.transform.position.value = pos;
return ans;
}
std::tuple<bool, bool, bool> PhysicalSystem::update(float dt, ServerEntity& e,
World& world,
glm::vec3& moved_pos) {
return update(dt, world, moved_pos, e.velocity, e.direction, e.move_state,
e.hitbox);
}
std::tuple<bool, bool, bool>
PhysicalSystem::update(float dt, World& world, glm::vec3& moved_pos,

View File

@@ -1,9 +1,6 @@
#include "Cubed/gameplay/systems/speed_system.hpp"
namespace Cubed {
void SpeedSystem::update(float dt, ServerEntity& e) {
update(dt, e.velocity, e.move_state, e.movement, e.direction, e.gravity);
}
void SpeedSystem::update(float dt, Velocity& v, MoveState& move_state,
Movement& movement, Direction& direction,

View File

@@ -10,4 +10,5 @@ import "system/pong.proto";
import "world/chunk_data.proto";
import "world/block_change.proto";
import "world/time.proto";
import "world/entity.proto";
import "chat/chat.proto";

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "common/vector3.proto";
message S2CEntityCreate {
uint64 id = 1;
string name = 2;
Vec3 pos = 3;
}