JoltPhysics

3. A controllable player

Add CharacterVirtual, a first-person view, WASD movement and jumping.

Steps#

  • Replace the same six event bodies. Keep one controller and one physics update; the room still has no GameMaker built-in physics.
  • Press Tab to capture the mouse, then use WASD and Space. Tab releases the cursor; losing window focus also unlocks it and stops movement input. The example deliberately starts unlocked.
  • Character position is at the feet. Its total height is 1.8 m and radius 0.4 m; eye height is a simple 1.6 m offset for this standing-only lesson. This is not a crouch/attachment camera.
  • The camera is a small data struct, not a GameMaker camera handle. Draw installs and restores its view/projection matrices. Do not assign that struct to view_camera or camera_apply.
  • Try walking and jumping, then releasing all keys. The character stands on the floor and returns after a jump. The floor has no boundary walls: walking off its edge intentionally allows falling.

Event order#

Create allocates resources. Step reads input and prepares the camera. End Step updates Grabber, calls the single jolt_update and refreshes the camera after simulation. Draw renders 3D debug; Draw GUI renders text. Clean Up releases helpers, Character, bodies and finally the world.

o_tutorial — Create#

GML
game_set_speed(60, gamespeed_fps);
floor_body = undefined;
crate = undefined;
player = undefined;
carry = undefined;
frame_seconds = 1 / 60;
mouse_locked = false;
carry_mouse_input = false;
yaw = 90;
pitch = -17;
sensitivity = 0.12;

if (!jolt_start()) {
    show_error(jolt_get_error_message(), true);
    exit;
}
jolt_debug_enable(true);
jolt_debug_set(0);
jolt_debug_set(JOLT_DEBUG_SHAPES, true);

floor_body = jolt_box_create_static(0, 0, -0.25, 12, 12, 0.5);
crate = jolt_box_create_dynamic(0, 2, 3, 1, 1, 1);
if (is_undefined(floor_body) || is_undefined(crate)) {
    show_error(jolt_get_error_message(), true);
    exit;
}
crate.set_mass(2);
crate.set_friction(0.6);
crate.set_restitution(0.1);

player = jolt_character();
if (is_undefined(player)) {
    show_error(jolt_get_error_message(), true);
    exit;
}
player.set_radius(0.4);
player.set_height(1.8);
player.set_position(0, -2, 0.1);
player.set_speed(4);
player.set_jump_speed(5);
jolt_debug_set(JOLT_DEBUG_CHARACTERS, true);

camera = {
    Position: {X:8, Y:-10, Z:7},
    Target: {X:0, Y:0, Z:1},
    Up: {X:0, Y:0, Z:1}
};
refresh_camera = function() {
    if (is_undefined(player)) return;
    var _feet = player.get_position();
    camera.Position = {X:_feet.x, Y:_feet.y, Z:_feet.z + 1.6};
    camera.Target = {
        X:camera.Position.X + dcos(yaw) * dcos(pitch),
        Y:camera.Position.Y + dsin(yaw) * dcos(pitch),
        Z:camera.Position.Z + dsin(pitch)
    };
};
refresh_camera();

o_tutorial — Step#

GML
frame_seconds = clamp(delta_time / 1000000, 0, 0.05);

if (keyboard_check_pressed(vk_tab)) {
    mouse_locked = !mouse_locked;
    window_mouse_set_locked(mouse_locked);
}
if (!window_has_focus()) {
    mouse_locked = false;
    window_mouse_set_locked(false);
}
window_set_cursor(mouse_locked ? cr_none : cr_default);
carry_mouse_input = !is_undefined(carry) && carry.controls_use_mouse(id);
if (mouse_locked && !carry_mouse_input) {
    yaw += window_mouse_get_delta_x() * sensitivity;
    pitch = clamp(pitch - window_mouse_get_delta_y() * sensitivity, -85, 85);
}

var _forward = 0;
var _side = 0;
if (mouse_locked) {
    _forward = keyboard_check(ord("W")) - keyboard_check(ord("S"));
    _side = keyboard_check(ord("D")) - keyboard_check(ord("A"));
    if (keyboard_check_pressed(vk_space)) player.jump();
}
var _mx = _forward * dcos(yaw) + _side * dcos(yaw - 90);
var _my = _forward * dsin(yaw) + _side * dsin(yaw - 90);
var _length = point_distance(0, 0, _mx, _my);
if (_length > 1) { _mx /= _length; _my /= _length; }
player.set_move(_mx, _my);
refresh_camera();

o_tutorial — Step → End Step#

GML
if (!is_undefined(carry)) carry.update_controls();
if (jolt_update(frame_seconds) < 0) {
    show_error(jolt_get_error_message(), true);
    exit;
}
refresh_camera();

o_tutorial — Draw#

GML
var _world = matrix_get(matrix_world);
var _view = matrix_get(matrix_view);
var _projection = matrix_get(matrix_projection);
var _p = camera.Position;
var _t = camera.Target;
matrix_set(matrix_world, matrix_build_identity());
matrix_set(matrix_view, matrix_build_lookat(
    _p.X, _p.Y, _p.Z, _t.X, _t.Y, _t.Z, 0, 0, 1));
matrix_set(matrix_projection, matrix_build_projection_perspective_fov(
    70, room_width / room_height, 0.05, 100));
jolt_debug_draw();
matrix_set(matrix_world, _world);
matrix_set(matrix_view, _view);
matrix_set(matrix_projection, _projection);

o_tutorial — Draw → Draw GUI#

GML
draw_set_colour(c_white);
draw_text(16, 16, "World: " + string(jolt_is_started()));
if (!is_undefined(crate)) {
    draw_text(16, 40, "Crate Z: " + string_format(crate.get_position().z, 1, 2));
}
if (!is_undefined(player)) {
    draw_text(16, 64, "Tab: mouse | WASD: move | Space: jump");
    var _cx = display_get_gui_width() * 0.5;
    var _cy = display_get_gui_height() * 0.5;
    draw_line(_cx - 5, _cy, _cx + 5, _cy);
    draw_line(_cx, _cy - 5, _cx, _cy + 5);
}
if (!is_undefined(carry)) {
    draw_text(16, 88, "LMB: grab/release | RMB: rotate | R: realign | F: throw");
    draw_text(16, 112, "Holding: " + string(carry.is_holding()));
    draw_text(16, 136, "Grab reason: " + carry.get_last_reason());
}

o_tutorial — Clean Up#

GML
window_mouse_set_locked(false);
window_set_cursor(cr_default);
if (is_struct(carry)) carry.destroy();
if (is_struct(player)) player.destroy();
if (is_struct(crate)) crate.destroy();
if (is_struct(floor_body)) floor_body.destroy();
jolt_stop();

Try it step by step#

Action in the game windowExpected result
Click inside the game window, then press Tab once.The cursor disappears and mouse look becomes active. Each new run starts with the mouse released.
Move the mouse; hold W and release it. Try A, S and D.The view changes, W moves forward relative to the camera and A/D move sideways. Releasing movement keys stops the player.
Briefly press Space while standing on the floor.The player jumps and lands. Pressing again while airborne is not a normal grounded jump test.
Press Tab, then Tab again. Switch to another window and return.Tab releases/captures the cursor. Losing focus disables controls; enable them again with Tab on return.

This is a reader checklist, not a claim that every manual step has been successfully tested. See Example Verification for the actual verification scope.

Check the result#

GUI must show World: 1. The crate lands and its centre stays near Z=0.5. The player walks and lands after jumping. Exiting must not produce an invalid-handle error.

Troubleshooting#

  • Unknown jolt_* function or BBMOD type: an imported resource is missing; this is not another object setting.
  • World: 0 or a DLL error: check the Windows x64 target and matching DLL using Installation.
  • World: 1 but nothing visible: check the Draw event, both debug shaders, object visibility and disabled viewport cameras. Physics bodies do not create visual game models.
  • Movement too fast: do not run a second jolt_update controller; convert delta_time from microseconds to seconds.
  • Error when leaving the room: release Grabber before Character and the world last. Do not use a persistent controller here.

Example files#

Verification scope#

The isolated test project uses these exact files. Compilation and runtime check results are listed on Practical Example Verification. Automated tests do not replace manual assessment of mouse feel and scene appearance.