‹›
JoltPhysics

4. Picking up objects with Grabber

Mark the crate as grabbable and connect Carry to the player and the camera.

Steps#

  • Replace the same six events with this final snapshot. Wait for the crate to land, press Tab, aim the centre crosshair at the crate, then click the left mouse button.
  • Left click toggles grab/release. Hold right mouse to rotate the held item; the camera does not consume that mouse delta. Release right mouse before pressing R to realign; F throws, and the wheel changes holding distance. Tab also releases the held item through update_controls.
  • set_distances(6,2,1,3,8) means: acquire within 6 m, hold at 2 m, allow holding between 1 and 3 m, release beyond the 8 m break limit. The break limit must exceed both acquire and maximum holding distance.
  • set_camera(id) points at this controller instance. Automatic controls require mouse_locked plus camera.Position/Target/Up with uppercase X/Y/Z, and carry_mouse_input for mouse rotation. refresh_camera and controls_use_mouse implement that contract here.
  • The 2 kg crate is below max_mass=20 kg and explicitly enables grabbing. Static floor bodies cannot be carried. A failed attempt is explained by Grab reason in the GUI; aim at the crate surface within reach and ensure Tab has locked the mouse.

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

crate.set_grab_enabled(true);
carry = jolt_grabber();
if (is_undefined(carry)) {
    show_error(jolt_get_error_message(), true);
    exit;
}
carry.set_holder(player);
carry.set_camera(id);
carry.set_distances(6, 2, 1, 3, 8);
carry.set_max_mass(20);
carry.set_grab_button(mb_left);
carry.set_rotate_button(mb_right);
carry.set_realign_key(ord("R"));
carry.set_throw_key(ord("F"));
carry.set_throw_impulse(10);
carry.set_throw_max_speed(6);
carry.set_debug_labels(false);
jolt_debug_set(JOLT_DEBUG_GRABBER, 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
After the crate lands, click inside the game window, press Tab, aim the crosshair at the crate and click LMB once.Holding becomes 1. You do not need to hold LMB; a second click releases the crate.
Grab again. Hold RMB while slowly moving the mouse.The held crate rotates and the camera stops consuming mouse movement during RMB. Holding RMB without moving the mouse does not rotate anything.
Release RMB before pressing R.Realignment starts. Current controls ignore R while RMB is held.
Use the wheel while holding, then press F.The wheel changes distance within 1–3 m. F throws the crate and Holding returns to 0.
Grab again and press Tab or switch to another window.The crate is released and the cursor unlocked. Press Tab again after returning.
Try to grab the floor.The static floor cannot be carried. Failure here is expected.

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

The crate is a symmetric untextured cube. A rotation of exactly 90° can look identical to its original orientation; use small continuous motions and watch its edges when checking RMB. Grab reason reports the last operation result, not a live description of the object under the crosshair. Use Holding to check current attachment.

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. Holding becomes 1 on acquisition and 0 on release, throw or mouse unlock. 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.