#include "Player.hpp" #include "../Common/Casts.hpp" #include "../Math/MVP.hpp" #include namespace MC::Entities { Player::Player(Position::World position, Real ascept, Real fov, Real near, Real far) : m_transform(position), m_outline_program( {GFX::Shading::Shader::Type::Vertex, outline_vertex}, {GFX::Shading::Shader::Type::Fragment, outline_fragment} ), m_outline_mesh(create_outline_cube_mesh()), m_outline_model_uniform(), m_outline_view_uniform(), m_outline_projection_uniform() { m_outline_program.bind(); m_outline_model_uniform = m_outline_program.uniform("model_matrix"); m_outline_view_uniform = m_outline_program.uniform("view_matrix"); m_outline_projection_uniform = m_outline_program.uniform("projection_matrix"); m_outline_model_uniform.set(Math::MVP::model({}, {}, {})); m_outline_view_uniform.set(Math::MVP::view({}, {})); m_outline_projection_uniform.set(Math::MVP::perspective_projection(ascept, fov, near, far)); m_outline_program.unbind(); } void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world) { auto const input_direction = directional_input(window); auto destination = movement(window, time, input_direction); if (can_collide()) { auto const [position, blocked_axes] = process_collisions(world, m_transform.position(), destination); destination = position; m_on_ground = blocked_axes.y().negative; for (UInt axis = 0; axis < 3; axis++) { if (blocked_axes[axis].positive) m_velocity[axis] = std::min(m_velocity[axis], 0.0); if (blocked_axes[axis].negative) m_velocity[axis] = std::max(m_velocity[axis], 0.0); } } move_to(destination); rotate(rotational_input(window)); update_camera_position(camera); update_targeted_block(world); actions(window, world); } // TODO: Proof-of-concept, will all be moved to the rendering system. void Player::render(const GFX::Camera& camera) { // Render currently targeted block outline. if (m_targeted_block.has_value()) { auto targeted_block = m_targeted_block.value(); m_outline_program.bind(); m_outline_view_uniform.set(Math::MVP::view(camera.position(), camera.angles())); m_outline_model_uniform.set(Math::MVP::model(Vec3(targeted_block.position), Vec3::one(), {})); m_outline_mesh.bind(); // TODO: These are very thin lines, do we have any better easy options? glDrawElements(GL_LINES, m_outline_mesh.size(), GL_UNSIGNED_INT, nullptr); m_outline_mesh.unbind(); m_outline_program.unbind(); } } void Player::move(Position::WorldOffset by) { m_transform.position() += by; } void Player::rotate(Rotation by) { m_transform.rotation() += by; m_transform.rotation().pitch() = std::clamp(m_transform.rotation().pitch(), -89.0, 89.0); } void Player::move_to(Position::World to) { m_transform.position() = to; } void Player::rotate_to(Rotation to) { m_transform.rotation() = to; } AABB Player::bounds() const { return bounding_box_for_position(m_transform.position()); } Bool Player::can_collide() const { // Collisions are always on, except when in noclip mode. return m_movement != MovementMode::NoClip; } Position::World Player::movement(GFX::Window& window, const Time& time, Vec3 input_direction) { switch (m_movement) { case MovementMode::Walking: m_velocity = walking_velocity(window, time, input_direction); break; case MovementMode::Flying: m_velocity = flying_velocity(window, time, input_direction); break; case MovementMode::NoClip: m_velocity = noclip_velocity(window, time, input_direction); break; } return m_transform.position() + m_velocity; } Vec3 Player::walking_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) { constexpr auto base_move_speed = 8.0; constexpr auto initial_jump_velocity = 0.16; constexpr auto gravity = 0.6; auto walking_direction = m_transform.right() * input_direction.x() + m_transform.forward() * input_direction.z(); walking_direction.y() = 0; if (!walking_direction.is_zero()) walking_direction = walking_direction.normalize(); auto const walking_velocity = walking_direction * base_move_speed * time.delta(); auto vertical_velocity = 0.0; if (m_on_ground && input_direction.y() > 0.5) { vertical_velocity = initial_jump_velocity; m_on_ground = false; } else { // TODO: This integration depends on frame delta. vertical_velocity = m_velocity.y() - gravity * time.delta(); } return { walking_velocity.x(), vertical_velocity, walking_velocity.z(), }; } Vec3 Player::flying_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) { constexpr auto base_move_speed = 10.0; auto flying_direction = m_transform.right() * input_direction.x() + m_transform.forward() * input_direction.z(); flying_direction.y() = 0; if (!flying_direction.is_zero()) flying_direction = flying_direction.normalize(); auto const flying_velocity = flying_direction * base_move_speed * time.delta(); auto vertical_velocity = input_direction.y() * base_move_speed * time.delta(); return { flying_velocity.x(), vertical_velocity, flying_velocity.z(), }; } Vec3 Player::noclip_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) { constexpr auto base_move_speed = 10.0; Real const boost = TO(Real, window.key(GLFW_KEY_LEFT_CONTROL, GLFW_PRESS)) * 75.0; auto const [x, y, z] = input_direction.elements; auto const direction = m_transform.right() * x + Vec3(0, y, 0) + m_transform.forward() * z; return direction * (base_move_speed + boost) * time.delta(); } void Player::update_targeted_block(World::World& world) { auto constexpr max_block_reach = 4.0; auto const look_transform = camera_transform(); Ray const ray{look_transform.position(), -look_transform.forward()}; auto const traversal = world.traverse( ray, [](auto b) { return b.type.is_solid(); }, max_block_reach ); if (traversal.hit) m_targeted_block = {traversal.block, traversal.normal}; else m_targeted_block = {}; } void Player::actions(GFX::Window& window, World::World& world) { // Breaking and placing blocks. auto left_click = window.mouse(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS); auto right_click = window.mouse(GLFW_MOUSE_BUTTON_RIGHT, GLFW_PRESS); if (left_click || right_click) { if (m_targeted_block.has_value()) { auto targeted_block = m_targeted_block.value(); if (left_click) { world.break_block(targeted_block.position); } else { auto const new_block = Position::BlockWorld(targeted_block.position + targeted_block.normal); auto const current_box = bounding_box_for_position(m_transform.position()); auto const block_box = World::Chunk::block_bounds(new_block); if (!current_box.collides(block_box)) // Don't place blocks inside the player. world.set_block(new_block, World::BlockType::Stone); } } } // Toggle movement modes. // TODO: Need an input system for this, can't get an event only at // the moment the key is pressed, so can't toggle between modes on single press. if (window.key(GLFW_KEY_1, GLFW_PRESS)) m_movement = MovementMode::Walking; else if (window.key(GLFW_KEY_2, GLFW_PRESS)) m_movement = MovementMode::Flying; else if (window.key(GLFW_KEY_3, GLFW_PRESS)) m_movement = MovementMode::NoClip; } #define STUCK_THRESHOLD 100 Player::ProcessCollisionsResult Player::process_collisions(World::World& world, Position::World from, Position::World to) { if (from.mostly_equal(to)) return {to, {}}; auto const current_box = bounding_box_for_position(from); // All the blocks we could theoretically collide with. // NOTE: It isn't updated as new responses are applied, // as that would be (currently) too expensive. // At very high speeds, this may lead to phasing through blocks. auto const collision_domain = terrain_collision_domain(from, to, world); // Sort the responses first by the magnitude of the velocity until the collision, // and then by the distance from the entity, so that we slide along the closest block. // If we apply a response, we need to check again for collisions, // since we might have slid into another block. (up to STUCK_THRESHOLD times) // If there are no more responses, we have no collisions, // and the player can move freely to the proposed position. Vector<3, BlockedAxis> blocked_axes; struct Response { AABB::CollisionResponse response; Real distance_from_entity_squared; Real magnitude_squared; }; std::vector responses; for (UInt stuck = 0; stuck < STUCK_THRESHOLD; stuck++) { auto const v = to - from; for (auto possible_collision : collision_domain) { auto const response = current_box.collision_response(v, possible_collision); auto const total_velocity = response.v_to_collision + response.v_slide; if (!total_velocity.mostly_equal(v) && !total_velocity.is_nan()) { responses.push_back({ response, possible_collision.center().distance_squared(from), response.v_to_collision.magnitude_squared(), }); } } if (responses.empty()) return {to, blocked_axes}; std::sort(responses.begin(), responses.end(), [=](const Response& a, const Response& b) -> bool { return std::tie(a.magnitude_squared, a.distance_from_entity_squared) < std::tie(b.magnitude_squared, b.distance_from_entity_squared); }); // TODO: This applies the entire response, even though ideally we'd apply it in two parts, // since technically the total velocity is a diagonal of the two components. // This should only be a marginal issue, though. auto const response = responses[0].response; to = from + response.v_to_collision + response.v_slide; auto check_axis = [&](Real axis_normal, BlockedAxis& axis) -> Bool { if (axis_normal < -0.5) { axis.negative = true; return true; } if (axis_normal > 0.5) { axis.positive = true; return true; } return false; }; check_axis(response.normal.x(), blocked_axes.x()) || check_axis(response.normal.y(), blocked_axes.y()) || check_axis(response.normal.z(), blocked_axes.z()); responses.clear(); } // We got stuck, don't move. // Also, if we're stuck, we're also presumably touching the ground on any axis. return {from, {BlockedAxis{true, true}, BlockedAxis{true, true}, BlockedAxis{true, true}}}; } std::vector Player::terrain_collision_domain( Position::World from, Position::World to, World::World& world ) { // Make the box a bit bigger so we don't clip through blocks. auto domain_box = bounding_box_for_position(from) .unite(bounding_box_for_position(to)) .sum(AABB{{1, 1, 1}}); std::vector colliding_blocks; // TODO: Unbind from chunks and loop through all the // blocks individually inside domain. // This will be more efficient and actually accurate, // since right now if you're fast enough you can clip // through whole chunks. std::unordered_set chunks_to_check; for (auto corner : domain_box.corners()) { chunks_to_check.insert(World::ChunkIndex::from_position(corner)); } for (auto chunk_index : chunks_to_check) { auto chunk = world.chunks().get(chunk_index); if (!chunk.chunk.has_value()) continue; auto chunk_position = chunk.chunk.value().position(); chunk.chunk->for_each([&](auto p, auto b) { if (!b.type.is_solid()) return; auto block_bounds = World::Chunk::block_bounds(p).offset(chunk_position); if (domain_box.collides(block_bounds)) colliding_blocks.push_back(block_bounds); }); } return colliding_blocks; } Transform Player::camera_transform() const { auto position = m_transform.position(); position.y() += 1.5; return { position, m_transform.rotation(), m_transform.scale(), }; } void Player::update_camera_position(GFX::Camera& camera) const { auto cam_transform = camera_transform(); camera.set_position(cam_transform.position()); camera.set_angles(cam_transform.rotation()); } Vec3 Player::directional_input(GFX::Window& window) { auto key = [&](Int k) -> Real { return window.key(k, GLFW_PRESS); }; Real x = key(GLFW_KEY_D) - key(GLFW_KEY_A); Real y = key(GLFW_KEY_SPACE) - key(GLFW_KEY_LEFT_SHIFT); Real z = key(GLFW_KEY_S) - key(GLFW_KEY_W); return {x, y, z}; } Rotation Player::rotational_input(GFX::Window& window) { constexpr auto base_rotation_speed = 0.1f; auto r = window.mouse_delta(); return {r.y() * base_rotation_speed, r.x() * base_rotation_speed, 0.0f}; } AABB Player::bounding_box_for_position(Position::World position) { Vec3 box_start = { position.x() - s_bounds.max.x() / 2, position.y(), position.z() - s_bounds.max.z() / 2, }; return s_bounds.offset(box_start); } Position::World Player::position_for_bounding_box(AABB box) { auto center = box.center(); return {center.x(), box.min.y(), center.z()}; } GFX::Mesh Player::create_outline_cube_mesh() { GFX::Util::MeshBuilder> builder{}; auto slightly_bigger_block_bounds = AABB{{1, 1, 1}}.sum(AABB{{0.01, 0.01, 0.01}}); builder.primitive(GFX::Util::Primitives::line_box(slightly_bigger_block_bounds)); return builder.mesh(); } const Char* Player::outline_vertex = R"v( #version 330 core uniform mat4 model_matrix; uniform mat4 view_matrix; uniform mat4 projection_matrix; layout (location = 0) in vec3 position; void main() { vec4 world_position = model_matrix * vec4(position, 1.0); vec4 view_position = view_matrix * world_position; vec4 clip_position = projection_matrix * view_position; gl_Position = clip_position; } )v"; const Char* Player::outline_fragment = R"f( #version 330 core out vec4 color; void main() { color = vec4(0.0, 0.0, 0.0, 1.0); } )f"; }