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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
|
#include "Player.hpp"
#include "../Input.hpp"
#include <unordered_set>
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<Response> 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<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;
}
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<Vector<3, F32>> 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();
}
}
|