summary refs log tree commit diff
path: root/src/Entities/Player.cpp
blob: a8abb1ddfadc682235ead625b4bf766063a049ce (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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#include "Player.hpp"

#include "../Common/Casts.hpp"
#include <unordered_set>

namespace MC::Entities {

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);
    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::max(m_velocity[axis], 0.0);
        if (blocked_axes[axis].negative) m_velocity[axis] = std::min(m_velocity[axis], 0.0);
    }

    move_to(destination);
    rotate(rotational_input(window));

    update_camera_position(camera);

    if (window.mouse(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS)) {
        auto look_transform = camera_transform();
        auto look_direction = -look_transform.forward(); // Why does this need to be inverted?
        auto block = world.traverse(
            Ray{look_transform.position(), look_direction},
            4.0,
            [](auto b) { return b.type.is_solid(); }
        );

        if (block != Position::BlockWorld{}) world.break_block(block);
    }
}

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());
}

Position::World Player::movement(GFX::Window& window, const Time& time, Vec3 input_direction) {
    m_velocity = m_flying
        ? flying_velocity(window, time, input_direction)
        : walking_velocity(window, time, input_direction);

    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 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) {
        if (window.key(GLFW_KEY_SPACE, GLFW_PRESS)) {
            vertical_velocity = 0.16;
            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;

    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();
}

#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 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.

    Vector<3, BlockedAxis> blocked_axes;

    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, 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 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<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;
}

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()};
}

}