Files
Towards/modules/server/src/ZoneManager.cpp
T
2026-08-03 23:30:18 +02:00

179 lines
6.5 KiB
C++

#include "ZoneManager.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/Transform.hpp"
#include "world/WorldEntity.hpp"
#include <spdlog/spdlog.h>
#include <tracy/Tracy.hpp>
namespace tw::net {
ZoneManager::ZoneManager(im::AreaBounds zone_bounds)
: m_world(std::make_unique<World>()),
m_physics_world(m_world.get()),
m_spatial_backend(std::make_unique<SpatialBackendType>(zone_bounds.min.x, zone_bounds.max.x, zone_bounds.min.y, zone_bounds.max.y)),
m_interest_system(std::make_unique<im::InterestSystem<SpatialBackendType>>(
m_world.get(), m_spatial_backend.get()
))
{}
im::AreaBounds ZoneManager::area() const {
return {
{ (float)m_spatial_backend->world_min_x(), (float)m_spatial_backend->world_min_z() },
{ (float)m_spatial_backend->world_max_x(), (float)m_spatial_backend->world_max_z() }
};
}
void ZoneManager::transfer_entity(EntityInfo&& info) {
spdlog::info("Transfered entity to zone ({} {}) ({} {})", area().min.x, area().min.y, area().max.x, area().max.y);
spawn_entity(std::move(info));
}
entt::entity ZoneManager::spawn_entity(EntityInfo&& info) {
entt::entity entity = m_world->registry().create();
m_world->registry().emplace<WorldEntity>(entity, info.name, (uint32_t)entity);
m_world->registry().emplace<Transform>(entity, Transform(info.position));
m_world->registry().emplace<CharacterController>(entity, 20.0f);
m_world->registry().emplace<CharacterBody>(
entity,
m_physics_world.create_character(
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
info.position
)
);
return entity;
}
void ZoneManager::add_client(im::InterestId interest_id, entt::entity entity) {
m_client_entities[interest_id] = entity;
m_entity_sessions[entity] = interest_id;
m_interest_system->set_entity_interest(interest_id, entity);
}
entt::entity ZoneManager::client_entity(im::InterestId interest_id) const {
auto it = m_client_entities.find(interest_id);
return it != m_client_entities.end() ? it->second : entt::null;
}
void ZoneManager::on_player_move(SessionId session_id, mmo::PlayerMoveMessage&& message) {
ZoneScoped;
entt::entity entity = client_entity(session_id);
if (entity == entt::null) return;
m_session_input_frame[session_id] = message.frame_idx();
auto* controller = m_world->registry().try_get<CharacterController>(entity);
if (controller) {
controller->set_input(
message.frame_idx(),
glm::vec3(message.input().x(), message.input().y(), message.input().z())
);
} else {
spdlog::error("Player {} has no character controller", (uint32_t)entity);
}
}
void ZoneManager::register_neighbor_zone(ZoneProxy* neighbor) {
im::InterestId id = m_next_neighbor_id++;
m_neighbors.push_back({ neighbor, id });
// Expand bounds so entities approaching the border appear in the interest
// system before they cross. Transfer still uses the true (unexpanded) bounds.
auto b = neighbor->area();
im::AreaBounds expanded {
{ b.min.x - kNeighborBorderOverlap, b.min.y - kNeighborBorderOverlap },
{ b.max.x + kNeighborBorderOverlap, b.max.y + kNeighborBorderOverlap }
};
m_interest_system->set_area_interest(id, expanded);
}
const im::Interest* ZoneManager::get_interest(im::InterestId id) const {
return m_interest_system->get_interest(id);
}
uint32_t ZoneManager::acked_input_frame(im::InterestId interest_id) const {
auto it = m_session_acked_frame.find(interest_id);
return it != m_session_acked_frame.end() ? it->second : 0;
}
void ZoneManager::check_neighbor_transfers() {
ZoneScopedN("ZoneManager::check_neighbor_transfers");
for (const auto& [proxy, interest_id] : m_neighbors) {
const im::Interest* interest = m_interest_system->get_interest(interest_id);
if (!interest || interest->interest().empty()) continue;
// Only transfer entities that have actually crossed into the neighbour's
// true area (not just the expanded interest margin).
auto true_bounds = proxy->area();
std::vector<entt::entity> to_transfer;
for (entt::entity entity : interest->interest()) {
const Transform* transform = m_world->registry().try_get<Transform>(entity);
if (!transform) continue;
glm::vec3 pos = transform->position();
if (pos.x >= true_bounds.min.x && pos.x <= true_bounds.max.x &&
pos.z >= true_bounds.min.y && pos.z <= true_bounds.max.y) {
to_transfer.push_back(entity);
}
}
for (entt::entity entity : to_transfer) {
auto* world_entity = m_world->registry().try_get<WorldEntity>(entity);
auto* transform = m_world->registry().try_get<Transform>(entity);
if (!world_entity || !transform) continue;
EntityInfo info {
.name = world_entity->name,
.position = transform->position()
};
// Remove client tracking on the sending side.
// The owner (e.g. ZoneServer) is responsible for calling add_client on
// the receiving zone with the correct session mapping.
auto session_it = m_entity_sessions.find(entity);
if (session_it != m_entity_sessions.end()) {
spdlog::info(
"Transferring interest {} entity {} to neighbour zone",
session_it->second, (uint32_t)entity
);
m_interest_system->remove_interest(session_it->second);
m_client_entities.erase(session_it->second);
m_entity_sessions.erase(session_it);
}
proxy->transfer_entity(std::move(info));
// TODO: notify physics world to release the Jolt CharacterVirtual.
m_world->registry().destroy(entity);
}
}
}
void ZoneManager::update(uint32_t frame_idx, float delta_time) {
ZoneScopedN("ZoneManager::tick");
FrameMarkStart("Interest System");
m_interest_system->update();
FrameMarkEnd("Interest System");
check_neighbor_transfers();
FrameMarkStart("World step");
m_world->step(delta_time);
FrameMarkEnd("World step");
FrameMarkStart("Physics step");
m_physics_world.step(frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME);
FrameMarkEnd("Physics step");
m_session_acked_frame = m_session_input_frame;
}
} // namespace tw::net