Bodies

Bodies Overview

JoltBody is a safe high-level GML representation of one rigid body in the default Jolt world.

This page covers the lifecycle, motion types, options, properties, collision features, raycasts, and verified implementation limits shared by the Bodies section.

JoltBody#

JoltBody stores a generational native handle, world handle, shape and motion types, dimension metadata, GML user_data, an optional owning GameMaker instance, and collision events from the last updated frame. It is not a C++ pointer. The handle encodes a slot and generation; destroying an object advances the generation so a stale handle is rejected. Zero is invalid. Normal code should keep the JoltBody instead of the number returned by body.get_native_handle().

Motion types#

Motion typeConstantBehaviour
DynamicJOLT_MOTION_DYNAMICA simulated body affected by gravity, contacts, forces, impulses, and constraints. Creators ending in _create_dynamic create Dynamic bodies.
StaticJOLT_MOTION_STATICImmovable collision geometry for floors, walls, level meshes, and terrain. It does not integrate velocity and is not intended for forces.
KinematicJOLT_MOTION_KINEMATICMoved by the application through move_kinematic or move_kinematic_euler. It can physically affect Dynamic bodies.

Read the type with body.get_motion_type() and change it with body.set_motion_type(type). Mesh, HeightField, and Plane high-level creators and resources support Static only.

Coordinates and units#

  • The public API is right-handed and Z-up: X left/right, Y forward/back, and Z up/down.
  • Native rigid-body coordinate conversion is the identity.
  • The recommended convention is 1 unit = 1 metre; the wrapper does not enforce a physical unit.
  • High-level Euler angles use degrees and quaternions use {x, y, z, w}.
  • High-level primitive creators accept full dimensions; low-level primitive functions accept the explicitly named half extents or half height.

Lifecycle#

GML
jolt_start();
var body = jolt_box_create_dynamic(0, 0, 5, 1, 1, 1);

// Step event
jolt_update();

// Clean Up event
body.destroy();
jolt_stop();

jolt_start(options) creates the default world and is safe to call repeatedly. jolt_update(frame_seconds) uses a fixed timestep, 1/60 s by default, simulates, and then exposes contacts. jolt_stop() is idempotent and destroys managed bodies, constraints, shape resources, compound builders, and the world.

body.destroy() returns true even when repeated. Afterwards body.is_valid() is false, the GML handle is zero, stale native handles are rejected, and related high-level constraints are invalidated.

Shared options struct#

FieldDefaultDescription
rotation{0,0,0,1}Finite non-zero quaternion {x,y,z,w}; Native normalizes it.
rotation_euler{0,0,0}Euler degrees. When both rotations are supplied, rotation_euler is applied last; supply only one.
velocity{0,0,0}Initial world-space linear velocity; unavailable for Static.
angular_velocity{0,0,0}Initial angular velocity in radians per second; unavailable for Static.
friction0.2Finite value >= 0; this default is from Jolt 5.6.0.
restitution0.0Finite value in 0..1.
gravity_factor1.0Finite world-gravity multiplier; requires motion properties.
linear_damping0.05Finite value >= 0; requires motion properties.
angular_damping0.05Finite value >= 0; requires motion properties.
massJolt mass propertiesFinite value > 0; Dynamic only.
allow_sleepingtrueBoolean; requires motion properties.
layerjolt_default_layer()A valid JoltLayer from the same world.
sensorfalseBoolean trigger/sensor state.
impact_sensorfalseBoolean or {min_value:real >= 0}; stores the last relevant impact, with min_value measured in N·s.
ccdfalseBoolean; true enables Jolt LinearCast CCD for a Dynamic rigid body.
motion_qualityDiscreteAdvanced Discrete or LinearCast constant; takes precedence over ccd.
center_of_mass_offset{0,0,0}Finite local {x,y,z} offset between the body origin and its centre of mass.

Errors and return values#

GML
if (is_undefined(body)) {
    var error = jolt_get_error();
    show_debug_message(error.context + ": " + error.message);
}

jolt_get_error() returns {code, message, context}. Also available: jolt_has_error(), jolt_get_error_message(), jolt_clear_error(), plus jolt_get_last_error_code() and jolt_get_last_error_message() for low-level/native state.

JoltBody methods#

Lifecycle and identity#

GML
body.is_valid()
body.get_native_handle()
body.get_world_handle()
body.destroy()

Transform and motion#

GML
body.get_position()
body.set_position(x, y, z)
body.get_rotation()
body.get_rotation_euler()
body.set_rotation(x, y, z)
body.set_rotation(x, y, z, w)
body.set_rotation_euler(x, y, z)
body.get_transform()
body.get_velocity() / body.set_velocity(x, y, z)
body.get_angular_velocity() / body.set_angular_velocity(x, y, z)
body.move_kinematic(...)
body.move_kinematic_euler(...)

Forces and impulses — Dynamic only#

GML
body.add_force(x, y, z)
body.add_force_at_position(fx, fy, fz, px, py, pz)
body.add_force_local(fx, fy, fz, local_x, local_y, local_z)
body.add_impulse(x, y, z)
body.add_impulse_at_position(ix, iy, iz, px, py, pz)
body.add_impulse_local(ix, iy, iz, local_x, local_y, local_z)
body.add_torque(x, y, z)
body.add_angular_impulse(x, y, z)

The at_position variants use a world-space point.

Properties and activation#

GML
body.set_friction(v) / body.get_friction()
body.set_restitution(v) / body.get_restitution()
body.set_gravity_factor(v) / body.get_gravity_factor()
body.set_linear_damping(v) / body.get_linear_damping()
body.set_angular_damping(v) / body.get_angular_damping()
body.set_mass(v) / body.get_mass()
body.activate() / body.deactivate() / body.is_active()
body.set_allow_sleeping(v) / body.get_allow_sleeping()
body.set_ccd(enabled) / body.get_ccd()
body.set_motion_quality(quality) / body.get_motion_quality()

Impact Sensor#

GML
var box = jolt_box_create_dynamic(0, 0, 5, 1, 1, 1, {
    impact_sensor: {min_value: 10.0}
});
var impact = box.get_impact_sensor();

box.set_impact_sensor(true, 10.0);

Impact Sensor is a per-body opt-in persistent snapshot of the latest relevant OnContactAdded collision. value and impulse are the summed estimated normal contact impulses from Jolt EstimateCollisionResponse, in N·s. The persistent sequence lets gameplay consume each new impact once. Resting and sliding OnContactPersisted contacts do not overwrite it.

The result contains hit, value, impulse, normal_speed, position, normal, other_handle, live other_body/other_user_data or undefined, and sequence. Before the first impact, hit is false, sequence is zero, and numeric values are zero. When several impacts occur in one native physics step, the strongest one is stored. One jolt_update() may contain several steps. See Collision System → Impact Sensor for the complete contract and one-shot reaction example.

Continuous Collision Detection (CCD)#

The default Discrete mode checks contacts at individual physics steps, so a small fast body can jump over a thin obstacle. LinearCast uses Jolt's native shape cast along linear movement automatically during jolt_update().

GML
var bullet = jolt_sphere_create_dynamic(0, 0, 1, 0.05, {
    mass: 0.02,
    ccd: true
});
bullet.set_velocity(0, 100, 0);

bullet.set_ccd(false); // Discrete
bullet.set_ccd(true);  // LinearCast

The constants are JOLT_MOTION_QUALITY_DISCRETE and JOLT_MOTION_QUALITY_LINEAR_CAST. Runtime changes preserve activation, sleeping permission, and both velocities. The wrapper accepts CCD only on Dynamic non-sensor rigid bodies with a positive inner radius; current Box, Sphere, Capsule, Cylinder, Tapered Capsule, Tapered Cylinder, Convex, and Compound shapes qualify.

CCD is not jolt_raycast(), jolt_raycast_all(), or a Shape Query. No manual cast is required; it is a JoltBody solver property.

Centre of mass#

GML
body.get_center_of_mass_position()
body.get_center_of_mass_offset()

center_of_mass_offset is selected at creation time and cannot be changed afterwards. The first method returns the current world-space centre of mass; the second returns the local offset stored by the high-level wrapper. This is useful when a body's visual pivot and balance point should differ.

Collision layers#

jolt_default_layer() returns the default layer. jolt_layer_create(name) creates another JoltLayer; a world supports at most 32 user layers including Default. jolt_layer_set_collision(layer_a, layer_b, enabled) symmetrically configures a pair, and new layers collide by default.

GML
var trigger_layer = jolt_layer_create("Triggers");
jolt_layer_set_collision(trigger_layer, jolt_default_layer(), true);
var body = jolt_box_create_dynamic(0, 0, 3, 1, 1, 1, {layer: trigger_layer});

Supply a layer through options or body.set_layer(). body.get_layer() returns a JoltLayer, or undefined for an error or unknown handle.

Sensors#

Every rigid shape can use {sensor:true} or body.set_sensor(true); body.is_sensor() reads the state. A sensor produces contact callbacks without physical response, acts as a trigger volume, and can be hit by a raycast.

user_data and GameMaker instances#

GML
body.set_user_data({kind:"Door", id:17});
var data = body.get_user_data();

user_data exists only in high-level GML. body.bind_instance(instance, use_as_user_data=true) binds an existing instance and normally stores it as user_data. body.sync() explicitly writes x, y, z and jolt_rotation_x/y/z/w; synchronization is not automatic. body.unbind_instance() removes the binding. jolt_box_create_dynamic_for_instance(instance, width, depth, height, options) creates and binds a Dynamic Box.

Contacts#

After jolt_update(), jolt_get_collisions() returns world events with type, body_a/body_b, handles and validity, position, normal, penetration_depth, sensor flags, and user_data_a/user_data_b. Type is JOLT_COLLISION_ENTER, STAY, or EXIT.

body.get_collisions() returns per-body records with type, body, other_body, entity, other_entity, other_handle, other_valid, position, normal, penetration_depth, and sensor. jolt_get_dropped_collision_count() reports events discarded when the native queue overflowed.

Raycasts#

GML
var hit = jolt_raycast(x1, y1, z1, x2, y2, z2, filter);
var hits = jolt_raycast_all(x1, y1, z1, x2, y2, z2, filter);

filter accepts a JoltLayer, an array of layers, a numeric mask, {layers:[...], ignore:body}, or {mask:value, ignore:body}. The default hits all layers. A hit contains hit, body, body_handle, entity, position, normal, fraction, distance, error_code, and error_message. jolt_raycast_all() returns sorted hits. Mesh triangles are one-sided and default raycasts ignore back faces.

Debug Draw#

GML
jolt_debug_enable(true);
jolt_debug_set(JOLT_DEBUG_SHAPES, true);
jolt_debug_set(JOLT_DEBUG_SENSORS, true);
jolt_debug_draw();

JOLT_DEBUG_SHAPES draws non-sensors; sensors require JOLT_DEBUG_SENSORS. Motion and sleep states have different colours. This is a high-level visualization from metadata, not exported internal Jolt geometry. Draw it in the final 3D pass, such as Draw End.

Verified implementation limits#

  • Native body handles are 32-bit generational values, not pointers.
  • Convex accepts up to 4096 points and produces at most 256 hull vertices.
  • Mesh: 65536 vertices and 131072 triangles.
  • HeightField: square 4..512 sample grid.
  • Compound: at most 64 children.
  • GML .jcol buffer: at most 8 MiB.
  • Shape cache key: explicit ASCII up to 255 characters; large arrays are not automatically hashed.

Native smoke tests cover Dynamic Convex, Static Mesh, Static HeightField, raycasts, Z-up normals, contacts, layers, sensors, invalid coplanar Convex data, Dynamic Mesh rejection, mesh sanitation, reusable shapes, and stale handles.

Verified operation contracts#

These contracts distinguish units, return values, errors and lifetime according to the current implementation. Check results: false, 0 and undefined can mean different things for different methods.

Symbol / ownerSignatureOperation contract
JoltBody.get_closest_pointJoltBody.get_closest_point(x, y, z)World XYZ m, queries this body only; same hit/miss snapshot as jolt_closest_point. Failure is hit:false with error_code, not undefined.
JoltBody.get_transformJoltBody.get_transform()World {x, y, z} m / XYZW quaternion / Euler XYZ° / {position, rotation}, according to method. undefined on error. Position is body origin, not COM.
JoltBody.get_world_handleJoltBody.get_world_handle()Stored body/world handle when valid, otherwise 0. Do not retain across destroy or world restart.
JoltBody.set_bottom_radiusJoltBody.set_bottom_radius(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.
JoltBody.set_depthJoltBody.set_depth(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.
JoltBody.set_half_extentJoltBody.set_half_extent(value)Boolean; plane only, nonzero finite normal / positive half extent in m; preserves the other parameter.
JoltBody.set_heightJoltBody.set_height(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.
JoltBody.set_heightfieldJoltBody.set_heightfield(heights,width,height,cell_size,cell_size_y=undefined)Boolean; replaces geometry only for convex / mesh / heightfield according to name, using the corresponding jolt_shape_* factory validation. Body identity remains.
JoltBody.set_meshJoltBody.set_mesh(vertices,indices)Boolean; replaces geometry only for convex / mesh / heightfield according to name, using the corresponding jolt_shape_* factory validation. Body identity remains.
JoltBody.set_normalJoltBody.set_normal(x,y,z)Boolean; plane only, nonzero finite normal / positive half extent in m; preserves the other parameter.
JoltBody.set_pointsJoltBody.set_points(points)Boolean; replaces geometry only for convex / mesh / heightfield according to name, using the corresponding jolt_shape_* factory validation. Body identity remains.
JoltBody.set_radiusJoltBody.set_radius(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.
JoltBody.set_shapeJoltBody.set_shape(resource)Boolean; live same-world JoltShape/JoltCompound, preserves body identity/gameplay settings and COM offset. Static-only geometry cannot form Dynamic/Kinematic bodies. Updates metadata and debug cache.
JoltBody.set_sizeJoltBody.set_size(a,b,c=undefined)Boolean; box full width/depth/height, capsule/cylinder height/radius, tapered height/top/bottom radius, all m. Other shapes require their specific setter.
JoltBody.set_top_radiusJoltBody.set_top_radius(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.
JoltBody.set_widthJoltBody.set_width(value)Boolean; positive finite dimension in m for the corresponding shape type. Incompatible types fail; set_radius sets both tapered radii. Other dimensions are preserved.