summary refs log tree commit diff
path: root/src/Entities/Player.cpp
blob: f87f2f9568365fe511b4b86fa45599ba5b9a1eab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include "Player.hpp"

#include <unordered_set>

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<Response> 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<AABB> 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<AABB> 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<World::ChunkIndex> 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()};
}

}