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
This commit is contained in:
2026-07-25 17:47:13 +08:00
parent f9bf2f4b89
commit 930226cc47
11 changed files with 263 additions and 13 deletions

View File

@@ -91,7 +91,7 @@ public:
void request_exit(); void request_exit();
bool is_receive_exit(); bool is_receive_exit();
int chunk_size() const; int chunk_size() const;
static AABB get_block_aabb(const glm::ivec3& pos);
AudioEngine& get_audio(); AudioEngine& get_audio();
const AudioEngine& get_audio() const; const AudioEngine& get_audio() const;
Config& get_config(); Config& get_config();
@@ -103,6 +103,7 @@ public:
void receive_voice_message(VoiceMsg& msg); void receive_voice_message(VoiceMsg& msg);
bool enable_voice_chat() const; bool enable_voice_chat() const;
const entt::registry& get_registry(); const entt::registry& get_registry();
int get_per_tick_time() const override;
template <typename Fn> template <typename Fn>
void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) { void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) {
m_ticktimers.emplace( m_ticktimers.emplace(
@@ -181,6 +182,7 @@ private:
std::atomic<int> m_rendering_distance{24}; std::atomic<int> m_rendering_distance{24};
std::atomic<TickType> m_game_ticks{0}; std::atomic<TickType> m_game_ticks{0};
std::atomic<TickType> m_day_tick{6000}; std::atomic<TickType> m_day_tick{6000};
std::atomic<int> m_per_tick_time = DEFAULT_PER_TICK_TIME;
std::atomic<bool> m_requesting_chunk{false}; std::atomic<bool> m_requesting_chunk{false};
std::atomic<bool> m_is_rebuilding{false}; std::atomic<bool> m_is_rebuilding{false};
std::atomic<int> m_chunk_task_id{0}; std::atomic<int> m_chunk_task_id{0};

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include "Cubed/AABB.hpp"
#include "Cubed/gameplay/player.hpp" #include "Cubed/gameplay/player.hpp"
#include <glm/glm.hpp> #include <glm/glm.hpp>
@@ -34,4 +35,14 @@ struct ViewAngles {
float angle = 0.0f; float angle = 0.0f;
}; };
struct Velocity {
float dx = 0.0f;
float dy = 0.0f;
float dz = 0.0f;
};
struct HitBoxes {
std::vector<AABB> boxex;
};
} // namespace Cubed } // namespace Cubed

View File

@@ -0,0 +1,24 @@
#pragma once
#include "Cubed/AABB.hpp"
#include <tbb/concurrent_hash_map.h>
namespace Cubed {
class HitboxManager {
public:
HitboxManager();
~HitboxManager();
static HitboxManager& instance();
AABB get_aabb(const std::string& key);
static AABB aabb(const std::string& key);
private:
using HitBoxMap = tbb::concurrent_hash_map<std::string, AABB>;
using cacc = HitBoxMap::const_accessor;
using acc = HitBoxMap::accessor;
HitBoxMap m_hitboxes;
AABB load(const std::string& path);
};
} // namespace Cubed

View File

@@ -0,0 +1,20 @@
#pragma once
#include "Cubed/gameplay/entity.hpp"
#include "Cubed/gameplay/world.hpp"
#include <entt/entt.hpp>
namespace Cubed {
class MoveSystem {
public:
static void update(World& world, entt::registry& registry);
private:
static void move_x(World& world, Transform& transform, Velocity& v,
const EntityInfo& info);
static void move_y(World& world, Transform& transform, Velocity& v,
const EntityInfo& info);
static void move_z(World& world, Transform& transform, Velocity& v,
const EntityInfo& info);
};
} // namespace Cubed

View File

@@ -91,7 +91,7 @@ public:
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;
BlockType get_block_tpye(const glm::ivec3& block_pos) const override; BlockType get_block_tpye(const glm::ivec3& block_pos) const override;
int get_per_tick_time() const override;
template <typename Fn> template <typename Fn>
void register_timer(std::string_view id, TickType threshold, Fn&& f) { void register_timer(std::string_view id, TickType threshold, Fn&& f) {
m_timers.emplace(std::piecewise_construct, m_timers.emplace(std::piecewise_construct,

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include "Cubed/AABB.hpp"
#include "Cubed/gameplay/block.hpp" #include "Cubed/gameplay/block.hpp"
#include <glm/glm.hpp> #include <glm/glm.hpp>
@@ -16,5 +17,13 @@ public:
virtual bool is_solid(const glm::ivec3& block_pos) const = 0; virtual bool is_solid(const glm::ivec3& block_pos) const = 0;
virtual bool can_pass_block(const glm::ivec3& block_pos) const = 0; virtual bool can_pass_block(const glm::ivec3& block_pos) const = 0;
virtual BlockType get_block_tpye(const glm::ivec3& block_pos) const = 0; virtual BlockType get_block_tpye(const glm::ivec3& block_pos) const = 0;
virtual int get_per_tick_time() const = 0;
static AABB get_block_aabb(const glm::ivec3& pos) {
return {glm::vec3{static_cast<float>(pos.x) + 0.5f,
static_cast<float>(pos.y) + 0.5f,
static_cast<float>(pos.z) + 0.5f},
glm::vec3{0.5f, 0.5f, 0.5f}};
}
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -96,4 +96,6 @@ target_sources(${PROJECT_NAME}
render/model_manager.cpp render/model_manager.cpp
render/model_renderer.cpp render/model_renderer.cpp
gameplay/chunk.cpp gameplay/chunk.cpp
gameplay/move_system.cpp
gameplay/hitbox_manager.cpp
) )

View File

@@ -505,7 +505,7 @@ void ClientWorld::start_client_thread(std::string_view uuid) {
// Wait for 20 ticks, after the server's central chunk is generated, then // Wait for 20 ticks, after the server's central chunk is generated, then
// request chunks // request chunks
std::this_thread::sleep_for(milliseconds(20 * DEFAULT_PER_TICK_TIME)); std::this_thread::sleep_for(milliseconds(20 * m_per_tick_time));
request_chunk(); request_chunk();
} }
@@ -557,7 +557,7 @@ void ClientWorld::client_run(std::stop_token stoken) {
Logger::info("Client Thread Started"); Logger::info("Client Thread Started");
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
constexpr auto TICK = std::chrono::milliseconds(DEFAULT_PER_TICK_TIME); const auto TICK = std::chrono::milliseconds(m_per_tick_time);
auto next = Clock::now(); auto next = Clock::now();
while (!stoken.stop_requested()) { while (!stoken.stop_requested()) {
@@ -730,13 +730,6 @@ bool ClientWorld::is_receive_exit() { return m_receive_exit; }
int ClientWorld::chunk_size() const { return m_chunks.size(); } int ClientWorld::chunk_size() const { return m_chunks.size(); }
AABB ClientWorld::get_block_aabb(const glm::ivec3& pos) {
return {glm::vec3{static_cast<float>(pos.x) + 0.5f,
static_cast<float>(pos.y) + 0.5f,
static_cast<float>(pos.z) + 0.5f},
glm::vec3{0.5f, 0.5f, 0.5f}};
}
AudioEngine& ClientWorld::get_audio() { return m_audio; } AudioEngine& ClientWorld::get_audio() { return m_audio; }
const AudioEngine& ClientWorld::get_audio() const { return m_audio; } const AudioEngine& ClientWorld::get_audio() const { return m_audio; }
Config& ClientWorld::get_config() { return m_config; } Config& ClientWorld::get_config() { return m_config; }
@@ -755,7 +748,7 @@ void ClientWorld::request_exit() {
if (m_client->is_connect_error() || m_exit_direct) { if (m_client->is_connect_error() || m_exit_direct) {
break; break;
} }
std::this_thread::sleep_for(milliseconds(DEFAULT_PER_TICK_TIME)); std::this_thread::sleep_for(milliseconds(m_per_tick_time));
++cnt; ++cnt;
if (cnt >= WORLD_EXIT_TIMEOUT) { if (cnt >= WORLD_EXIT_TIMEOUT) {
Logger::warn("Can't Receive Server Exit Sign"); Logger::warn("Can't Receive Server Exit Sign");
@@ -780,6 +773,7 @@ void ClientWorld::receive_voice_message(VoiceMsg& msg) {
} }
bool ClientWorld::enable_voice_chat() const { return m_voice_chat.load(); } bool ClientWorld::enable_voice_chat() const { return m_voice_chat.load(); }
const entt::registry& ClientWorld::get_registry() { return m_registry; } const entt::registry& ClientWorld::get_registry() { return m_registry; }
int ClientWorld::get_per_tick_time() const { return m_per_tick_time; }
void ClientWorld::send_chat_message(ChatMessage& message) { void ClientWorld::send_chat_message(ChatMessage& message) {
Arena arena; Arena arena;
auto msg = Arena::Create<ChatMsg>(&arena); auto msg = Arena::Create<ChatMsg>(&arena);

View File

@@ -0,0 +1,63 @@
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/tools/log.hpp"
#include <nlohmann/json.hpp>
namespace fs = std::filesystem;
using nlohmann::json;
namespace Cubed {
HitboxManager::HitboxManager() {}
HitboxManager::~HitboxManager() {}
HitboxManager& HitboxManager::instance() {
static HitboxManager inst;
return inst;
}
AABB HitboxManager::aabb(const std::string& key) {
return instance().get_aabb(key);
}
AABB HitboxManager::get_aabb(const std::string& key) {
{
cacc c;
if (m_hitboxes.find(c, key)) {
return c->second;
}
}
return load(key);
}
AABB HitboxManager::load(const std::string& path) {
fs::path p = ASSETS_PATH + path;
try {
glm::vec3 center;
glm::vec3 half;
std::ifstream s{p};
json j = json::parse(s);
if (j.contains("boxes")) {
if (j["boxes"].contains("center")) {
center.x = j["boxes"]["center"].at(0).get<float>();
center.y = j["boxes"]["center"].at(1).get<float>();
center.z = j["boxes"]["center"].at(2).get<float>();
}
if (j["boxes"].contains("half")) {
half.x = j["boxes"]["half"].at(0).get<float>();
half.y = j["boxes"]["half"].at(1).get<float>();
half.z = j["boxes"]["half"].at(2).get<float>();
}
}
acc a;
if (m_hitboxes.insert(a, path)) {
a->second = AABB{center, half};
return a->second;
}
} catch (const std::exception& e) {
Logger::error("Load Hitbox error {}", e.what());
}
Logger::error("Load hitbox {} Failed", path);
return AABB{glm::vec3(0.0f), glm::vec3(0.0f)};
}
} // namespace Cubed

View File

@@ -0,0 +1,123 @@
#include "Cubed/gameplay/move_system.hpp"
#include "Cubed/gameplay/entity.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
namespace Cubed {
void MoveSystem::update(World& world, entt::registry& registry) {
auto view = registry.view<Transform, Velocity, EntityInfo>();
for (auto entity : view) {
auto [transform, v, info] =
view.get<Transform, Velocity, EntityInfo>(entity);
move_x(world, transform, v, info);
move_y(world, transform, v, info);
move_z(world, transform, v, info);
}
}
void MoveSystem::move_x(World& world, Transform& transform, Velocity& v,
const EntityInfo& info) {
auto& pos = transform.pos;
float distance = v.dx * world.get_per_tick_time() / 1000.0f;
pos.x += distance;
AABB box = HitboxManager::aabb(
std::format("model/creature/{}/collision.json", info.name));
glm::vec3 min = box.min();
glm::vec3 max = box.max();
int minx = std::floor(min.x);
int maxx = std::floor(max.x);
int miny = std::floor(min.y);
int maxy = std::floor(max.y);
int minz = std::floor(min.z);
int maxz = std::floor(max.z);
for (int x = minx; x <= maxx; ++x) {
for (int y = miny; y <= maxy; ++y) {
for (int z = minz; z <= maxz; ++z) {
glm::ivec3 block_pos{x, y, z};
if (!world.can_pass_block(block_pos)) {
AABB block_box = World::get_block_aabb(block_pos);
if (box.intersects(block_box)) {
pos.x -= distance;
v.dx = 0.0f;
return;
}
}
}
}
}
}
void MoveSystem::move_y(World& world, Transform& transform, Velocity& v,
const EntityInfo& info) {
auto& pos = transform.pos;
float distance = v.dy * world.get_per_tick_time() / 1000.0f;
pos.y += distance;
AABB box = HitboxManager::aabb(
std::format("model/creature/{}/collision.json", info.name));
glm::vec3 min = box.min();
glm::vec3 max = box.max();
int minx = std::floor(min.x);
int maxx = std::floor(max.x);
int miny = std::floor(min.y);
int maxy = std::floor(max.y);
int minz = std::floor(min.z);
int maxz = std::floor(max.z);
for (int x = minx; x <= maxx; ++x) {
for (int y = miny; y <= maxy; ++y) {
for (int z = minz; z <= maxz; ++z) {
glm::ivec3 block_pos{x, y, z};
if (!world.can_pass_block(block_pos)) {
AABB block_box = World::get_block_aabb(block_pos);
if (box.intersects(block_box)) {
pos.y -= distance;
v.dy = 0.0f;
return;
}
}
}
}
}
}
void MoveSystem::move_z(World& world, Transform& transform, Velocity& v,
const EntityInfo& info) {
auto& pos = transform.pos;
float distance = v.dz * world.get_per_tick_time() / 1000.0f;
pos.z += distance;
AABB box = HitboxManager::aabb(
std::format("model/creature/{}/collision.json", info.name));
glm::vec3 min = box.min();
glm::vec3 max = box.max();
int minx = std::floor(min.x);
int maxx = std::floor(max.x);
int miny = std::floor(min.y);
int maxy = std::floor(max.y);
int minz = std::floor(min.z);
int maxz = std::floor(max.z);
for (int x = minx; x <= maxx; ++x) {
for (int y = miny; y <= maxy; ++y) {
for (int z = minz; z <= maxz; ++z) {
glm::ivec3 block_pos{x, y, z};
if (!world.can_pass_block(block_pos)) {
AABB block_box = World::get_block_aabb(block_pos);
if (box.intersects(block_box)) {
pos.z -= distance;
v.dz = 0.0f;
return;
}
}
}
}
}
}
} // namespace Cubed

View File

@@ -454,7 +454,7 @@ void ServerWorld::serever_run(std::stop_token stoken) {
Logger::info("Server Thread Started!"); Logger::info("Server Thread Started!");
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
constexpr auto TICK = std::chrono::milliseconds(DEFAULT_PER_TICK_TIME); const auto TICK = std::chrono::milliseconds(m_per_tick_time);
auto next = Clock::now(); auto next = Clock::now();
while (!stoken.stop_requested()) { while (!stoken.stop_requested()) {
@@ -1074,4 +1074,6 @@ BlockType ServerWorld::get_block_tpye(const glm::ivec3& block_pos) const {
return chunk_blocks[Chunk::index(x, y, z)]; return chunk_blocks[Chunk::index(x, y, z)];
} }
int ServerWorld::get_per_tick_time() const { return m_per_tick_time; }
} // namespace Cubed } // namespace Cubed