#include "Player.hpp" #include namespace MC::Entities { void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world) { auto origin = m_transform.position(); auto r = window.mouse_delta(); 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); Real boost = key(GLFW_KEY_LEFT_CONTROL) * 75.0f; auto move_speed = (10.0f + boost) * time.delta(); auto rotation_speed = 0.1f; auto direction = m_transform.right() * x + Vec3(0, y, 0) + m_transform.forward() * z; auto destination = origin + direction * move_speed; destination = process_collisions(world, origin, destination); move_to(destination); rotate({r.y() * rotation_speed, r.x() * rotation_speed, 0.0f}); update_camera_position(camera); } 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()); } #define STUCK_THRESHOLD 100 Position::World Player::process_collisions(World::World& world, Position::World from, Position::World to) { if (from.mostly_equal(to)) return to; auto 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 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. 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 v = to - from; for (auto possible_collision : collision_domain) { auto response = current_box.collision_response(v, possible_collision); auto total_velocity = response.v_to_collision + response.v_slide; if (!total_velocity.mostly_equal(v)) { responses.push_back({ response, possible_collision.center().distance_squared(from), response.v_to_collision.magnitude_squared(), }); } } if (responses.empty()) return to; 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. to = from + responses[0].response.v_to_collision + responses[0].response.v_slide; responses.clear(); } // We got stuck, don't move. return from; } 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; } void Player::update_camera_position(GFX::Camera& camera) { auto camera_position = m_transform.position(); camera_position.y() += 1.5; camera.set_position(camera_position); camera.set_angles(m_transform.rotation()); } 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()}; } }