aboutsummaryrefslogtreecommitdiff
path: root/src/shared/movement
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/movement')
-rw-r--r--src/shared/movement/movement.cc580
-rw-r--r--src/shared/movement/movement.hh75
-rw-r--r--src/shared/movement/struct.cc1
-rw-r--r--src/shared/movement/struct.hh20
4 files changed, 676 insertions, 0 deletions
diff --git a/src/shared/movement/movement.cc b/src/shared/movement/movement.cc
new file mode 100644
index 0000000..53d7a23
--- /dev/null
+++ b/src/shared/movement/movement.cc
@@ -0,0 +1,580 @@
+#include "shared/movement/movement.hh"
+
+namespace shared {
+namespace movement {
+
+constexpr float MAX_SPEED_XZ = 5.0f; // max speed before sprinting
+constexpr float MAX_SPEED_XZ_LIQUID = 2.8f; // max water speed before sprinting
+constexpr float MAX_SPEED_XZ_FLYING = 11.0f; // max water speed before sprinting
+constexpr float MAX_SPEED_Y = 80.0f;
+constexpr float MAX_SPEED_Y_LIQUID = 3.0f;
+constexpr float MAX_SPEED_Y_FLYING = 8.0f;
+constexpr float MOVE_ACCEL = 75.0f; // base acceleration in m*s^-2
+constexpr float AIR_MULT = 0.125f; // multiplier to acceleration if in air
+constexpr float SPRINT_MULT = 1.20f; // multiplier to acceleration if sprinting
+constexpr float SWIM_ACCEL = 50.0f; // y-axis accel when swimming in m*s^-2
+constexpr float JUMP_ACCEL = 7.85f; // y-axis accel when jumping in m*s^-2
+constexpr float FLY_ACCEL = 75.0f; // y-axis accel when swimming in m*s^-2
+
+constexpr float GRAVITY = 28.0f;
+constexpr float DRAG = 0.1f;
+constexpr float FRICTION = 15.0f;
+constexpr float FLY_DRAG = FRICTION;
+constexpr float VISCOSITY = 25.0f;
+
+glm::vec3 make_relative(const shared::math::coords& base_chunk_pos,
+ const glm::vec3& other_local_pos,
+ const shared::math::coords& other_chunk_pos) noexcept {
+ const auto diff_x = static_cast<std::int64_t>(other_chunk_pos.x) -
+ static_cast<std::int64_t>(base_chunk_pos.x);
+ const auto diff_z = static_cast<std::int64_t>(other_chunk_pos.z) -
+ static_cast<std::int64_t>(base_chunk_pos.z);
+ return {
+ other_local_pos.x + static_cast<float>(world::chunk::WIDTH * diff_x),
+ other_local_pos.y,
+ other_local_pos.z + static_cast<float>(world::chunk::WIDTH * diff_z)};
+}
+
+void normalise_position(glm::vec3& local_pos,
+ shared::math::coords& chunk_pos) noexcept {
+ const bool g_x = (local_pos.x >= world::chunk::WIDTH);
+ const bool l_x = (local_pos.x < 0.0f);
+ const bool g_z = (local_pos.z >= world::chunk::WIDTH);
+ const bool l_z = (local_pos.z < 0.0f);
+
+ chunk_pos.x += g_x - l_x;
+ local_pos.x += shared::world::chunk::WIDTH * (l_x - g_x);
+ chunk_pos.z += g_z - l_z;
+ local_pos.z += shared::world::chunk::WIDTH * (l_z - g_z);
+}
+
+glm::ivec2 get_move_xy(const std::uint32_t& tickrate,
+ const shared::moveable& moveable) noexcept {
+ const float max_x = std::max(
+ MAX_SPEED_XZ, std::max(MAX_SPEED_XZ_LIQUID, MAX_SPEED_XZ_FLYING));
+ const float max_y =
+ std::max(MAX_SPEED_Y, std::max(MAX_SPEED_Y_LIQUID, MAX_SPEED_Y_FLYING));
+ const float mult = 1.0f / static_cast<float>(tickrate);
+ const auto& aabb = moveable.get_aabb();
+ return {max_x * mult + aabb.max.x + 1.0f, max_y * mult + aabb.max.y + 1.0f};
+}
+
+static float get_block_friction(const enum world::block::type block) noexcept {
+ if (world::block::is_liquid(block)) {
+ return VISCOSITY;
+ }
+ if (world::block::is_collidable(block)) {
+ return FRICTION;
+ }
+ return DRAG;
+}
+
+bool intersect_aabbs(const aabb& a, const aabb& b) noexcept {
+ if (a.max.x < b.min.x || a.min.x > b.max.x) {
+ return false;
+ }
+ if (a.max.y < b.min.y || a.min.y > b.max.y) {
+ return false;
+ }
+ if (a.max.z < b.min.z || a.min.z > b.max.z) {
+ return false;
+ }
+ return true;
+}
+
+static glm::vec3 plane_to_normal(const int plane) noexcept {
+ glm::vec3 normal{0.0f, 0.0f, 0.0f};
+ normal[std::abs(plane) - 1] = std::signbit(plane) ? -1.0f : 1.0f;
+ return normal;
+}
+
+std::optional<ray_aabb_ret> intersect_ray_aabb(const line& line,
+ const aabb& aabb) noexcept {
+ float tmin = -std::numeric_limits<float>::max();
+ float tmax = std::numeric_limits<float>::max();
+
+ int p = 0;
+ for (int i = 0; i < 3; ++i) {
+ if (std::abs(line.dir[i]) < EPSILON) {
+
+ // Ray is parallel to slab, no hit if origin not within slab.
+ if (line.origin[i] < aabb.min[i] || line.origin[i] > aabb.max[i]) {
+ return std::nullopt;
+ }
+
+ } else {
+ // Intersection t value of ray with near and far plane of slab.
+ const float ood = 1.0f / line.dir[i];
+ float t1 = (aabb.min[i] - line.origin[i]) * ood;
+ float t2 = (aabb.max[i] - line.origin[i]) * ood;
+
+ if (t1 > t2) {
+ std::swap(t1, t2);
+ }
+ auto old = tmin;
+ tmin = std::max(tmin, t1);
+ if (tmin != old) {
+ p = (i + 1);
+ }
+ tmax = std::min(tmax, t2);
+ if (tmin > tmax) {
+ return std::nullopt;
+ }
+ }
+ }
+
+ if (tmin <= 0.0f) {
+ return std::nullopt;
+ }
+
+ return ray_aabb_ret{.position = line.origin + line.dir * tmin,
+ .time = tmin,
+ .normal = plane_to_normal(p)};
+}
+
+std::optional<moving_aabb_ret>
+intersect_moving_aabbs(const moving_aabb& a, const moving_aabb& b) noexcept {
+
+ if (intersect_aabbs(a.aabb, b.aabb)) {
+ return std::nullopt;
+ }
+
+ const glm::vec3 velocity = b.velocity - a.velocity;
+ float tfirst = 0.0f;
+ float tlast = 10.0f;
+ int p = 0;
+
+ for (int i = 0; i < 3; ++i) {
+ if (velocity[i] < 0.0f) {
+ if (b.aabb.max[i] < a.aabb.min[i]) {
+ return std::nullopt;
+ }
+ if (a.aabb.max[i] < b.aabb.min[i]) {
+ const auto old = tfirst;
+ tfirst = std::max((a.aabb.max[i] - b.aabb.min[i]) / velocity[i],
+ tfirst);
+ if (tfirst != old) {
+ p = (i + 1);
+ }
+ }
+ if (b.aabb.max[i] > a.aabb.min[i]) {
+ tlast = std::min((a.aabb.min[i] - b.aabb.max[i]) / velocity[i],
+ tlast);
+ }
+ } else if (velocity[i] > 0.0f) {
+ if (b.aabb.min[i] > a.aabb.max[i]) {
+ return std::nullopt;
+ }
+ if (b.aabb.max[i] < a.aabb.min[i]) {
+ const auto old = tfirst;
+ tfirst = std::max((a.aabb.min[i] - b.aabb.max[i]) / velocity[i],
+ tfirst);
+ if (tfirst != old) {
+ p = -(i + 1);
+ }
+ }
+ if (a.aabb.max[i] > b.aabb.min[i]) {
+ tlast = std::min((a.aabb.max[i] - b.aabb.min[i]) / velocity[i],
+ tlast);
+ }
+ } else {
+ if (b.aabb.max[i] < a.aabb.min[i] ||
+ b.aabb.min[i] > a.aabb.max[i]) {
+ return std::nullopt;
+ }
+ }
+ if (tfirst > tlast) {
+ return std::nullopt;
+ }
+ }
+
+ return moving_aabb_ret{.time = tfirst, .normal = plane_to_normal(p)};
+}
+
+// Gets the closest collision of an entity to a block.
+struct collide_ret {
+ moving_aabb_ret collision;
+ shared::world::block block;
+};
+static std::optional<collide_ret> collide(const aabb& aabb,
+ const blocks& blocks,
+ const glm::vec3& move) noexcept {
+
+ const moving_aabb moving_aabb{.aabb = aabb, .velocity = move};
+
+ // TODO: It's possible we collide at the same time, but on a different block
+ // If this occurs, we will phase through it as we ignore the other
+ // collision. So fix it by doing some maths.
+ std::optional<collide_ret> collision;
+ for (const auto& block_aabb : blocks) {
+ if (!shared::world::block::is_collidable(block_aabb.block)) {
+ continue;
+ }
+
+ const struct moving_aabb block_moving_aabb {
+ .aabb = block_aabb.aabb, .velocity = { 0.0f, 0.0f, 0.0f }
+ };
+ const auto intersect =
+ intersect_moving_aabbs(moving_aabb, block_moving_aabb);
+
+ if (!intersect.has_value() || intersect->time >= 1.0f) {
+ continue;
+ }
+
+ // Update collision if it doesn't exist or if this one is closer.
+ if (!collision.has_value() ||
+ intersect->time < collision->collision.time) {
+
+ collision =
+ collide_ret{.collision = *intersect, .block = block_aabb.block};
+ }
+ }
+
+ return collision;
+}
+
+// Returns the closest ground object if such an object exists.
+static std::optional<shared::world::block>
+maybe_get_ground(const blocks& blocks, const aabb& aabb,
+ bool (*filter)(const enum shared::world::block::type) =
+ world::block::is_tangible) noexcept {
+ const moving_aabb moving_aabb{.aabb = aabb,
+ .velocity = {0.0f, -1.0f, 0.0f}};
+
+ // blockinfo, cur max distance
+ std::optional<std::pair<block, float>> ground;
+ for (const auto& block : blocks) {
+
+ if (!filter(block.block)) {
+ continue;
+ }
+
+ const struct moving_aabb block_moving_aabb {
+ .aabb = block.aabb, .velocity = { 0.0f, 0.0f, 0.0f }
+ };
+
+ if (const auto intersect =
+ intersect_moving_aabbs(moving_aabb, block_moving_aabb);
+ intersect.has_value()) {
+
+ if (intersect->time > EPSILON2) {
+ continue;
+ }
+
+ const float distance =
+ glm::distance(aabb.min, block.aabb.min); // cool hack
+
+ if (!ground.has_value()) {
+ ground.emplace(std::make_pair(block, distance));
+ continue;
+ }
+
+ const auto& [cur_max_block, cur_max_distance] = *ground;
+ if (distance >= cur_max_distance) {
+ continue;
+ }
+
+ ground.emplace(std::make_pair(block, distance));
+ }
+ }
+ if (ground.has_value()) {
+ return ground->first.block;
+ }
+ return std::nullopt;
+}
+
+static bool is_intersecting(
+ const blocks& blocks, const aabb& aabb,
+ bool (*filter)(const enum shared::world::block::type)) noexcept {
+ for (const auto& block_aabb : blocks) {
+ if (!filter(block_aabb.block)) {
+ continue;
+ }
+ if (!intersect_aabbs(block_aabb.aabb, aabb)) {
+ continue;
+ }
+ return true;
+ }
+ return false;
+}
+
+struct vectors {
+ glm::vec3 up;
+ glm::vec3 front;
+ glm::vec3 right;
+};
+
+static glm::vec3 get_accel(const blocks& blocks, const aabb& aabb,
+ const vectors& vectors,
+ const entity::index_t& commands) noexcept {
+ glm::vec3 acceleration{};
+
+ const auto add_input = [&](const auto& mask, const glm::vec3& dir) {
+ if (commands & mask) {
+ acceleration += dir;
+ }
+ };
+ add_input(animate::mask::forward, vectors.front);
+ add_input(animate::mask::left, -vectors.right);
+ add_input(animate::mask::backward, -vectors.front);
+ add_input(animate::mask::right, vectors.right);
+
+ acceleration.y = 0.0f; // so we don't move faster when facing up/down
+ if (acceleration != glm::vec3{}) {
+ acceleration = glm::normalize(acceleration);
+ }
+ acceleration *= MOVE_ACCEL; // 7.25 blocks/sec^2 by default
+ if (commands & animate::mask::sprint) {
+ acceleration *= SPRINT_MULT;
+ }
+
+ const bool in_water =
+ is_intersecting(blocks, aabb, world::block::is_liquid);
+
+ if ((commands & animate::mask::jump) && in_water) {
+ acceleration += SWIM_ACCEL * vectors.up;
+ }
+
+ // flying up/down
+ if (commands & animate::mask::flying) {
+ if (commands & animate::mask::jump) {
+ acceleration += FLY_ACCEL * vectors.up;
+ }
+ if (commands & animate::mask::crouch) {
+ acceleration -= FLY_ACCEL * vectors.up;
+ }
+ }
+
+ const bool on_ground =
+ maybe_get_ground(blocks, aabb, world::block::is_collidable).has_value();
+ // Move slower in the air, also our hitbox must be outside non-tangible.
+ if (!on_ground && !in_water && !(commands & animate::mask::flying)) {
+ acceleration *= AIR_MULT;
+ }
+
+ // gravity - only applied if there isn't a collidable block beneath us.
+ // or if we're not flying
+ if (!on_ground && !(commands & animate::mask::flying)) {
+ acceleration -= GRAVITY * vectors.up;
+ }
+
+ return acceleration;
+}
+
+static float get_decel_factor(const blocks& blocks, const aabb& aabb,
+ const entity::index_t& commands) noexcept {
+
+ const float drag = [&]() -> float { // max drag of all intersecting blocks
+ float max = 0.0f;
+ for (const auto& block : blocks) {
+ if (!intersect_aabbs(aabb, block.aabb)) {
+ continue;
+ }
+ max = std::max(max, get_block_friction(block.block));
+ }
+
+ if (commands & animate::flying) {
+ return std::max(max, FLY_DRAG);
+ }
+
+ return max;
+ }();
+
+ const float friction = [&]() -> float {
+ const auto ground =
+ maybe_get_ground(blocks, aabb, world::block::is_collidable);
+ return ground.has_value() ? get_block_friction(*ground) : 0.0f;
+ }();
+
+ // Our deceleration is simply the max of what we're in and what we're on
+ return std::max(drag, friction);
+}
+
+static void decelerate(glm::vec3& velocity, const blocks& blocks,
+ const aabb& aabb, const entity::index_t& commands,
+ const float max_time) noexcept {
+ const float decel =
+ get_decel_factor(blocks, aabb, commands) * max_time * 2.0f;
+
+ if (const float xy_speed = glm::length(glm::vec2{velocity.x, velocity.z});
+ xy_speed > 0.0f) {
+
+ const float new_speed = std::max(0.0f, xy_speed - decel);
+ velocity.x *= new_speed / xy_speed;
+ velocity.z *= new_speed / xy_speed;
+ }
+
+ // we decelerate on y, but only if we're flying
+ if (const float y_speed = std::abs(velocity.y);
+ y_speed > 0.0f && (commands & shared::animate::flying)) {
+
+ velocity.y *= std::max(0.0f, y_speed - decel) / y_speed;
+ }
+}
+
+static void clamp_move_xy(float& x, float& z, const entity::index_t& commands,
+ const bool in_liquid,
+ const float mult = 1.0f) noexcept {
+
+ const float max_speed_xy = [&]() {
+ const float base = [&]() { // lol
+ if (commands & animate::mask::flying) {
+ return MAX_SPEED_XZ_FLYING;
+ }
+ if (in_liquid) {
+ return MAX_SPEED_XZ_LIQUID;
+ }
+ return MAX_SPEED_XZ;
+ }();
+ if (commands & animate::mask::sprint) {
+ return base * SPRINT_MULT;
+ }
+ return base;
+ }() * mult;
+
+ if (const float speed_xz = std::hypot(x, z); speed_xz > max_speed_xy) {
+ const float ratio = max_speed_xy / speed_xz;
+ x *= ratio;
+ z *= ratio;
+ }
+}
+
+static void clamp_move_y(float& y, const entity::index_t& commands,
+ const bool in_liquid,
+ const float mult = 1.0f) noexcept {
+ const float max_speed_y = [&]() {
+ if (commands & animate::mask::flying) {
+ return MAX_SPEED_Y_FLYING;
+ }
+ return in_liquid ? MAX_SPEED_Y_LIQUID : MAX_SPEED_Y;
+ }() * mult;
+
+ if (const float speed_y = std::abs(y); speed_y > max_speed_y) {
+ const float ratio = max_speed_y / speed_y;
+ y *= ratio;
+ }
+}
+
+static void clamp_move(glm::vec3& move, const entity::index_t& commands,
+ const bool in_liquid, const float mult = 1.0f) noexcept {
+
+ clamp_move_xy(move.x, move.z, commands, in_liquid, mult);
+ clamp_move_y(move.y, commands, in_liquid, mult);
+}
+
+static void handle_jumps(glm::vec3& velocity, const blocks& blocks,
+ const aabb& aabb, const entity::index_t& commands,
+ const bool in_liquid) noexcept {
+ if (!(commands & animate::mask::jump)) {
+ return;
+ }
+ if (!(velocity.y >= 0 && velocity.y <= EPSILON)) {
+ return;
+ }
+ if (in_liquid) {
+ return;
+ }
+ if (!maybe_get_ground(blocks, aabb, world::block::is_collidable)) {
+ return;
+ }
+ // TODO: the jump will only occur in the next movement tick
+ // fix this by modifying both our velocity and our move vector
+ velocity.y += JUMP_ACCEL;
+}
+
+static void handle_collisions(glm::vec3& velocity, glm::vec3& local_pos,
+ glm::vec3 move, const blocks& blocks, aabb aabb,
+ const entity::index_t& commands,
+ const float max_time) noexcept {
+ constexpr int MAX_COLLISIONS = 100;
+ for (int collisions = 0; collisions < MAX_COLLISIONS; ++collisions) {
+
+ {
+ const bool in_liquid =
+ is_intersecting(blocks, aabb, world::block::is_liquid);
+
+ handle_jumps(velocity, blocks, aabb, commands, in_liquid);
+ clamp_move(velocity, commands, in_liquid);
+ clamp_move(move, commands, in_liquid, max_time);
+ }
+
+ const auto collision = collide(aabb, blocks, move);
+
+ if (!collision.has_value()) {
+ local_pos += move;
+ return;
+ }
+
+ const glm::vec3 pos = move * collision->collision.time;
+ const glm::vec3 off = collision->collision.normal * EPSILON;
+
+ for (int i = 0; i < 3; ++i) {
+
+ float& move_axis = move[i];
+ float& vel_axis = velocity[i];
+
+ const float diff = pos[i] - off[i];
+ move_axis -= diff;
+
+ vel_axis -= collision->collision.normal[i] *
+ glm::dot(velocity, collision->collision.normal);
+ move_axis -= collision->collision.normal[i] *
+ glm::dot(move, collision->collision.normal);
+
+ vel_axis = std::abs(vel_axis) <= EPSILON ? 0.0f : vel_axis;
+ move_axis = std::abs(move_axis) <= EPSILON ? 0.0f : move_axis;
+
+ if (abs(pos[i]) <= EPSILON2) {
+ continue;
+ }
+
+ local_pos[i] += diff; // add movement to current position
+ aabb.min[i] += diff;
+ aabb.max[i] += diff;
+ }
+ }
+}
+
+static void handle_movement(shared::animate& animate, const blocks& blocks,
+ const aabb& aabb, const vectors& vectors,
+ const entity::index_t& commands,
+ const float max_time) noexcept {
+
+ auto& velocity = animate.get_mutable_velocity();
+ auto& position = animate.get_mutable_local_pos();
+
+ decelerate(velocity, blocks, aabb, commands, max_time);
+
+ const glm::vec3 accel = get_accel(blocks, aabb, vectors, commands);
+ const glm::vec3 move = animate.get_velocity() * max_time +
+ 0.5f * accel * std::pow(max_time, 2.0f);
+
+ animate.get_mutable_velocity() += accel * max_time;
+
+ handle_collisions(velocity, position, move, blocks, aabb, commands,
+ max_time);
+}
+
+shared::animate move(const shared::moveable& moveable, const blocks& blocks,
+ const std::uint32_t& tickrate) noexcept {
+
+ const movement::aabb& aabb = moveable.get_aabb();
+
+ const auto vectors = [&]() -> struct vectors {
+ const auto up = glm::vec3{0.0f, 1.0f, 0.0f};
+ const auto front = moveable.get_angles().to_dir();
+ const auto right = glm::normalize(glm::cross(front, up));
+ return {.up = up, .front = front, .right = right};
+ }();
+ const float max_time = 1.0f / static_cast<float>(tickrate);
+
+ shared::animate ret = moveable;
+ handle_movement(ret, blocks, aabb, vectors, moveable.get_commands(),
+ max_time);
+ normalise_position(ret.get_mutable_local_pos(),
+ ret.get_mutable_chunk_pos());
+ return ret;
+}
+
+} // namespace movement
+} // namespace shared
diff --git a/src/shared/movement/movement.hh b/src/shared/movement/movement.hh
new file mode 100644
index 0000000..9c6e17d
--- /dev/null
+++ b/src/shared/movement/movement.hh
@@ -0,0 +1,75 @@
+#ifndef SHARED_MOVEMENT_MOVEMENT_HH_
+#define SHARED_MOVEMENT_MOVEMENT_HH_
+
+#include "shared/entity/animate.hh"
+#include "shared/entity/moveable.hh"
+#include "shared/math/math.hh"
+#include "shared/movement/struct.hh"
+#include "shared/shared.hh"
+#include "shared/world/chunk.hh"
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+
+#define GLM_ENABLE_EXPERIMENTAL
+#include <glm/glm.hpp>
+#include <glm/gtx/norm.hpp>
+
+namespace shared {
+namespace movement {
+
+// Returns a local pos relative to the base chunk pos, which can be negative,
+// or over 16.0f.
+glm::vec3 make_relative(const shared::math::coords& base_chunk_pos,
+ const glm::vec3& other_local_pos,
+ const shared::math::coords& other_chunk_pos) noexcept;
+
+void normalise_position(glm::vec3& local_pos,
+ shared::math::coords& chunk_pos) noexcept;
+bool intersect_aabbs(const aabb& a, const aabb& b) noexcept;
+
+struct line {
+ glm::vec3 origin;
+ glm::vec3 dir;
+};
+struct ray_aabb_ret {
+ glm::vec3 position;
+ float time;
+ glm::vec3 normal;
+};
+std::optional<ray_aabb_ret> intersect_ray_aabb(const line& line,
+ const aabb& aabb) noexcept;
+struct moving_aabb {
+ struct aabb aabb;
+ glm::vec3 velocity;
+};
+struct moving_aabb_ret {
+ float time;
+ glm::vec3 normal;
+};
+std::optional<moving_aabb_ret>
+intersect_moving_aabbs(const moving_aabb& a, const moving_aabb& b) noexcept;
+
+// We need more information about the block during movement code.
+struct block {
+ shared::world::block block;
+ struct aabb aabb;
+ shared::math::coords chunk_pos;
+ glm::ivec3 pos;
+};
+using blocks = std::vector<block>;
+
+// Returns a vec2 describing how wide and tall the array of blocks should be (to
+// ensure you never fall through anything at variable tickrates).
+glm::ivec2 get_move_xy(const std::uint32_t& tickrate,
+ const moveable& moveable) noexcept;
+
+[[nodiscard]] shared::animate move(const shared::moveable& moveable,
+ const blocks& blocks,
+ const std::uint32_t& tickrate) noexcept;
+
+} // namespace movement
+} // namespace shared
+
+#endif
diff --git a/src/shared/movement/struct.cc b/src/shared/movement/struct.cc
new file mode 100644
index 0000000..dd079eb
--- /dev/null
+++ b/src/shared/movement/struct.cc
@@ -0,0 +1 @@
+#include "shared/movement/struct.hh"
diff --git a/src/shared/movement/struct.hh b/src/shared/movement/struct.hh
new file mode 100644
index 0000000..7cfcc0f
--- /dev/null
+++ b/src/shared/movement/struct.hh
@@ -0,0 +1,20 @@
+#ifndef SHARED_MOVEMENT_STRUCT_HH_
+#define SHARED_MOVEMENT_STRUCT_HH_
+
+#include "glm/glm.hpp"
+
+namespace shared {
+namespace movement {
+
+constexpr float EPSILON = 0.0001f; // counteract arithmetic errors
+constexpr float EPSILON2 = EPSILON + 2 * EPSILON * EPSILON;
+
+struct aabb {
+ glm::vec3 min;
+ glm::vec3 max;
+};
+
+} // namespace movement
+} // namespace shared
+
+#endif