refactor(ecs): move entity physics to tick-based systems

Update PhysicalSystem and SpeedSystem to operate on entt::registry
instead of individual components. Add TickVelocity for server creatures
so movement is calculated per tick without frame delta time. LocalPlayer
now implements its own client-side physics with collision detection.
This commit is contained in:
2026-07-31 20:39:40 +08:00
parent 412f88565a
commit c5ce91e653
8 changed files with 276 additions and 121 deletions

View File

@@ -3,10 +3,17 @@
#include <glm/glm.hpp>
namespace Cubed {
struct TickVelocity {
glm::vec3 value{0.0f};
// blocks/tick!!! -1 for in
glm::vec3 max{1.0f, -1.0f, 1.0f};
};
struct Velocity {
glm::vec3 value{0.0f};
// blocks/second!!!
glm::vec3 max{4.5f, 7.5f, 7.5f};
};
@@ -23,4 +30,5 @@ struct Gravity {
float value = DEFAULT_G;
};
} // namespace Cubed

View File

@@ -8,7 +8,7 @@ namespace Cubed {
struct BaseServerCreature {
Transform transform{};
Velocity velocity{};
TickVelocity velocity{};
Movement movement{};

View File

@@ -149,12 +149,14 @@ private:
void update_direction();
void update_lookup_block();
void update_move(float delta_time);
void update_move(float dt);
void update_player_chunk();
void play_walk_sound(float dt);
Gait compute_gait() const;
void update_speed(float dt);
std::tuple<bool, bool, bool> update_physical(float dt);
glm::vec3 get_move_distance(float dt);
};
} // namespace Cubed

View File

@@ -1,18 +1,16 @@
#pragma once
#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 {
#include <entt/entt.hpp>
namespace Cubed {
class ServerWorld;
class PhysicalSystem {
public:
static glm::vec3 get_move_distance(float dt, const Direction& d,
const Velocity& v);
static glm::vec3 get_move_distance(const Direction& d,
const TickVelocity& v);
static std::tuple<bool, bool, bool>
update(float dt, World& world, glm::vec3& moved_pos, Velocity& v,
Direction& direction, MoveState& move_state, HitboxID hitbox);
static void update(ServerWorld& world, entt::registry& registry);
private:
};

View File

@@ -1,15 +1,10 @@
#pragma once
#include "Cubed/gameplay/ecs/movement.hpp"
#include "Cubed/gameplay/ecs/state.hpp"
#include "Cubed/gameplay/ecs/transform.hpp"
#include <entt/entt.hpp>
namespace Cubed {
class SpeedSystem {
public:
static void update(float dt, Velocity& v, MoveState& move_state,
Movement& movement, Direction& direction,
const Gravity& g);
static void update(float dt, entt::registry& registry);
private:
};

View File

@@ -5,12 +5,110 @@
#include "Cubed/debug_collector.hpp"
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/gameplay/systems/physical_system.hpp"
#include "Cubed/gameplay/systems/speed_system.hpp"
namespace {} // namespace
namespace Cubed {
LocalPlayer::LocalPlayer(ClientWorld& world) : m_world(world) {}
namespace {
bool update_x(const glm::vec3& pos, const glm::vec3& distance,
ClientWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.x += distance.x;
Hitbox b = box;
b.center += p;
glm::vec3 min = b.min();
glm::vec3 max = b.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)) {
Hitbox block_box = World::get_block_aabb(block_pos);
if (b.intersects(block_box)) {
return false;
}
}
}
}
}
return true;
}
bool update_y(const glm::vec3& pos, const glm::vec3& distance,
ClientWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.y += distance.y;
Hitbox b = box;
b.center += p;
glm::vec3 min = b.min();
glm::vec3 max = b.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)) {
Hitbox block_box = World::get_block_aabb(block_pos);
if (b.intersects(block_box)) {
return false;
}
}
}
}
}
return true;
}
bool update_z(const glm::vec3& pos, const glm::vec3& distance,
ClientWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.z += distance.z;
Hitbox b = box;
b.center += p;
glm::vec3 min = b.min();
glm::vec3 max = b.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)) {
Hitbox block_box = World::get_block_aabb(block_pos);
if (b.intersects(block_box)) {
return false;
}
}
}
}
}
return true;
}
} // namespace
LocalPlayer::LocalPlayer(ClientWorld& world) : m_world(world) {
m_hitbox = HitboxManager::instance().get_hitbox_id("cubed:player");
}
LocalPlayer::~LocalPlayer() {}
const glm::vec3& LocalPlayer::get_front() const { return m_front; }
@@ -333,9 +431,9 @@ LocalPlayer::get_hotbar() const {
return m_hotbar;
}
void LocalPlayer::update_move(float delta_time) {
void LocalPlayer::update_move(float dt) {
// if frame rate less than 1 frame per second, don't update
if (delta_time > 1.0f) {
if (dt > 1.0f) {
return;
}
@@ -343,7 +441,7 @@ void LocalPlayer::update_move(float delta_time) {
m_sprinting = false;
}
if (space_on) {
space_on_time += delta_time;
space_on_time += dt;
if (space_on_time >= MAX_SPACE_ON_TIME) {
space_on = false;
space_on_time = 0.0f;
@@ -360,8 +458,7 @@ void LocalPlayer::update_move(float delta_time) {
glm::vec3{m_max_run_speed, m_max_y_speed, m_max_run_speed};
}
SpeedSystem::update(delta_time, m_velocity, m_move_state, m_movement,
m_direction, m_gravity);
update_speed(dt);
update_direction();
@@ -374,12 +471,9 @@ void LocalPlayer::update_move(float delta_time) {
}
if (m_game_mode == SPECTATOR) {
player_pos += PhysicalSystem::get_move_distance(delta_time, m_direction,
m_velocity);
player_pos += get_move_distance(dt);
} else {
auto [x, y, z] =
PhysicalSystem::update(delta_time, m_world, player_pos, m_velocity,
m_direction, m_move_state, m_hitbox);
auto [x, y, z] = update_physical(dt);
if (!x || !z) {
m_sprinting = false;
}
@@ -406,7 +500,7 @@ void LocalPlayer::update_move(float delta_time) {
}
for (auto& [key, timer] : m_timers) {
timer.update(delta_time);
timer.update(dt);
}
}
@@ -593,6 +687,96 @@ void LocalPlayer::init(std::string_view name) {
}
}
void LocalPlayer::update_speed(float dt) {
// calculate speed
auto& v = m_velocity;
if (m_move_state.forward || m_move_state.back || m_move_state.left ||
m_move_state.right) {
m_direction.value = glm::vec3(0.0f, 0.0f, 0.0f);
v.value.x += m_movement.acceleration * dt;
v.value.z += m_movement.acceleration * dt;
if (v.value.x > v.max.x) {
v.value.x = v.max.x;
}
if (v.value.z > v.max.z) {
v.value.z = v.max.z;
}
} else {
v.value.x += -m_movement.deceleration * dt;
v.value.z += -m_movement.deceleration * dt;
if (v.value.z < 0.0f) {
v.value.z = 0.0f;
}
if (v.value.x < 0.0f) {
v.value.x = 0.0f;
}
if (v.value.z < 0.0f && v.value.x < 0.0f) {
m_direction.value = glm::vec3(0.0f, 0.0f, 0.0f);
}
}
if (m_move_state.is_fly) {
if (m_move_state.up) {
v.value.y = v.max.y;
}
if (m_move_state.down) {
v.value.y = -v.max.y;
}
if (!m_move_state.down && !m_move_state.up) {
v.value.y = 0.0f;
}
} else {
if (m_move_state.up && m_move_state.can_up) {
v.value.y = m_movement.jump_power;
m_move_state.can_up = false;
}
v.value.y += -m_gravity.value * dt;
}
}
std::tuple<bool, bool, bool> LocalPlayer::update_physical(float dt) {
auto distance = get_move_distance(dt);
auto box = HitboxManager::hitbox(m_hitbox);
bool x = false;
bool y = false;
bool z = false;
if (update_x(m_pos.value, distance, m_world, box.box)) {
m_pos.value.x += distance.x;
x = true;
} else {
m_velocity.value.x = 0.0f;
}
if (update_y(m_pos.value, distance, m_world, box.box)) {
m_pos.value.y += distance.y;
y = true;
} else {
m_velocity.value.y = 0.0f;
if (distance.y < 0) {
m_move_state.can_up = true;
m_move_state.is_fly = false;
}
}
if (update_z(m_pos.value, distance, m_world, box.box)) {
m_pos.value.z += distance.z;
z = true;
} else {
m_velocity.value.z = 0.0f;
}
return {x, y, z};
}
glm::vec3 LocalPlayer::get_move_distance(float dt) {
const auto& d = m_direction;
const auto& v = m_velocity;
return glm::vec3{d.value.x * v.value.x * dt, v.value.y * dt,
d.value.z * v.value.z * dt};
}
bool LocalPlayer::is_underwater() const { return m_underwater; }
void LocalPlayer::set_underwater(bool u) { m_underwater = u; }

View File

@@ -1,12 +1,14 @@
#include "Cubed/gameplay/systems/physical_system.hpp"
#include "Cubed/gameplay/ecs/server_entity.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/gameplay/server_world.hpp"
namespace Cubed {
namespace {
bool update_x(const glm::vec3& pos, const glm::vec3& distance, World& world,
const Hitbox& box) {
bool update_x(const glm::vec3& pos, const glm::vec3& distance,
ServerWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.x += distance.x;
Hitbox b = box;
@@ -36,8 +38,8 @@ bool update_x(const glm::vec3& pos, const glm::vec3& distance, World& world,
return true;
}
bool update_y(const glm::vec3& pos, const glm::vec3& distance, World& world,
const Hitbox& box) {
bool update_y(const glm::vec3& pos, const glm::vec3& distance,
ServerWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.y += distance.y;
Hitbox b = box;
@@ -67,8 +69,8 @@ bool update_y(const glm::vec3& pos, const glm::vec3& distance, World& world,
return true;
}
bool update_z(const glm::vec3& pos, const glm::vec3& distance, World& world,
const Hitbox& box) {
bool update_z(const glm::vec3& pos, const glm::vec3& distance,
ServerWorld& world, const Hitbox& box) {
glm::vec3 p = pos;
p.z += distance.z;
Hitbox b = box;
@@ -100,45 +102,41 @@ bool update_z(const glm::vec3& pos, const glm::vec3& distance, World& world,
} // namespace
std::tuple<bool, bool, bool>
PhysicalSystem::update(float dt, World& world, glm::vec3& moved_pos,
Velocity& v, Direction& direction, MoveState& move_state,
HitboxID hitbox) {
auto distance = get_move_distance(dt, direction, v);
auto box = HitboxManager::hitbox(hitbox);
bool x = false;
bool y = false;
bool z = false;
if (update_x(moved_pos, distance, world, box.box)) {
moved_pos.x += distance.x;
x = true;
void PhysicalSystem::update(ServerWorld& world, entt::registry& registry) {
auto view = registry.view<BaseServerCreature>();
for (auto e : view) {
auto& creature = view.get<BaseServerCreature>(e);
auto distance =
get_move_distance(creature.direction, creature.velocity);
auto box = HitboxManager::hitbox(creature.hitbox);
auto& pos = creature.transform.position.value;
auto& v = creature.velocity;
if (update_x(pos, distance, world, box.box)) {
pos.x += distance.x;
} else {
v.value.x = 0.0f;
}
if (update_y(moved_pos, distance, world, box.box)) {
moved_pos.y += distance.y;
y = true;
if (update_y(pos, distance, world, box.box)) {
pos.y += distance.y;
} else {
v.value.y = 0.0f;
if (distance.y < 0) {
move_state.can_up = true;
move_state.is_fly = false;
}
}
if (update_z(moved_pos, distance, world, box.box)) {
moved_pos.z += distance.z;
z = true;
if (update_z(pos, distance, world, box.box)) {
pos.z += distance.z;
} else {
v.value.z = 0.0f;
}
return {x, y, z};
}
}
glm::vec3 PhysicalSystem::get_move_distance(float dt, const Direction& d,
const Velocity& v) {
return glm::vec3{d.value.x * v.value.x * dt, v.value.y * dt,
d.value.z * v.value.z * dt};
glm::vec3 PhysicalSystem::get_move_distance(const Direction& d,
const TickVelocity& v) {
return glm::vec3{d.value.x * v.value.x, v.value.y, d.value.z * v.value.z};
}
} // namespace Cubed

View File

@@ -1,54 +1,24 @@
#include "Cubed/gameplay/systems/speed_system.hpp"
#include "Cubed/gameplay/ecs/ai_struct.hpp"
#include "Cubed/gameplay/ecs/server_entity.hpp"
namespace Cubed {
void SpeedSystem::update(float dt, Velocity& v, MoveState& move_state,
Movement& movement, Direction& direction,
const Gravity& g) {
// calculate speed
if (move_state.forward || move_state.back || move_state.left ||
move_state.right) {
direction.value = glm::vec3(0.0f, 0.0f, 0.0f);
v.value.x += movement.acceleration * dt;
v.value.z += movement.acceleration * dt;
if (v.value.x > v.max.x) {
v.value.x = v.max.x;
}
if (v.value.z > v.max.z) {
v.value.z = v.max.z;
}
void SpeedSystem::update(float dt, entt::registry& registry) {
auto view = registry.view<BaseServerCreature, MoveBoost>();
for (auto e : view) {
auto [creature, moveboost] = view.get<BaseServerCreature, MoveBoost>(e);
if (moveboost.count <= moveboost.duration) {
++moveboost.count;
creature.velocity.value +=
creature.direction.value * creature.movement.acceleration;
} else {
v.value.x += -movement.deceleration * dt;
v.value.z += -movement.deceleration * dt;
if (v.value.z < 0.0f) {
v.value.z = 0.0f;
creature.velocity.value -=
creature.direction.value * creature.movement.deceleration;
}
if (v.value.x < 0.0f) {
v.value.x = 0.0f;
}
if (v.value.z < 0.0f && v.value.x < 0.0f) {
direction.value = glm::vec3(0.0f, 0.0f, 0.0f);
}
}
if (move_state.is_fly) {
if (move_state.up) {
v.value.y = v.max.y;
}
if (move_state.down) {
v.value.y = -v.max.y;
}
if (!move_state.down && !move_state.up) {
v.value.y = 0.0f;
}
} else {
if (move_state.up && move_state.can_up) {
v.value.y = movement.jump_power;
move_state.can_up = false;
}
v.value.y += -g.value * dt;
creature.velocity.value.y = creature.gravity.value * dt;
}
}