#include "Player.hpp" #include "../Input.hpp" #include namespace MC::Entities { void Player::update(Time const& time, Input const& input, GFX::Camera& camera, World::World& world) { auto const input_direction = directional_input(input); auto destination = movement(time, input_direction); if (can_collide()) { if (m_velocity.y() < 0.0) destination = rescue_from_void_on_new_chunk(time, world, destination); 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(input)); update_camera_position(camera); update_targeted_block(world); actions(input, world); } void Player::render(GFX::Actions& actions) { if (m_targeted_block.has_value()) { auto position = Vec3(m_targeted_block->position); actions.add({ .mesh = &m_outline_mesh, .program = GFX::Resources::Program::BlockOutline, .transform = Transform{position}, // TODO: These are very thin lines, do we have any better easy options? .draw_mode = GFX::DrawMode::Lines, }); } } 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(Time const& time, Vec3 input_direction) { switch (m_movement) { case MovementMode::Walking: m_velocity = walking_velocity(time, input_direction); break; case MovementMode::Flying: m_velocity = flying_velocity(time, input_direction); break; case MovementMode::NoClip: m_velocity = noclip_velocity(time, input_direction); break; } return m_transform.position() + m_velocity; } Vec3 Player::walking_velocity(Time const& 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(Time const& time, Vec3 input_direction) const { 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(Time const& time, Vec3 input_direction) const { constexpr auto base_move_speed = 10.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 * 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(Input const& input, World::World& world) { // Breaking and placing blocks. auto left_click = input.pressed(Mouse::Left); auto right_click = input.pressed(Mouse::Right); 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. if (input.double_pressed(Key::Space)) { if (m_movement == MovementMode::Walking) m_movement = MovementMode::Flying; else if (m_movement == MovementMode::Flying) m_movement = MovementMode::Walking; } if (input.pressed(Key::N)) { if (m_movement == MovementMode::NoClip) m_movement = MovementMode::Walking; else 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 Iteration::Continue; auto block_bounds = World::Chunk::block_bounds(p).offset(chunk_position); if (domain_box.collides(block_bounds)) colliding_blocks.push_back(block_bounds); return Iteration::Continue; }); } return colliding_blocks; } Position::World Player::rescue_from_void_on_new_chunk(Time const& time, World::World& world, Position::World position) { auto& chunk = world.chunks().get(World::ChunkIndex::from_position(position)); // If the chunk was generated at the previous tick, we // can rescue the player by moving them to the top of the chunk. if (chunk.generated() && chunk.generated_at_tick == time.tick() - 1){ // TODO: Move to first solid block. position.y() = World::Chunk::Height / 2.0; m_transform.position() = position; m_velocity.y() = 0; } return position; } 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(Input const& input) { Real x = input.held(Key::D) - input.held(Key::A); Real y = input.held(Key::Space) - input.held(Key::LeftShift); Real z = input.held(Key::S) - input.held(Key::W); return {x, y, z}; } Rotation Player::rotational_input(Input const& input) { constexpr auto base_rotation_speed = 0.1f; auto r = input.mouse_movement(); 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(); } }