Skip to content

Add an action to change the body type of 3D physics objects - #9270

Merged
4ian merged 6 commits into
masterfrom
claude/awesome-knuth-vaoj1s
Oct 20, 2026
Merged

4ian merged 6 commits into
masterfrom
claude/awesome-knuth-vaoj1s

Conversation

@4ian

@4ian 4ian commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Adds a "Body type" action to the 3D physics behavior, to switch an object between static, dynamic and kinematic during the game. The conditions to check the type ("Is static", "Is dynamic", "Is kinematic") already exist. The editor now shows the collision filtering of each type, and property groups can have sub-groups.

For instance, an enemy moved by a pathfinding behavior (like the navmesh) can be kinematic, so that the player bumps into it without pushing it, and become dynamic to be thrown by an impulse when it is shot. The 3D FPS starters with enemies (GDevelopApp/GDevelop-examples#1097, GDevelopApp/GDevelop-examples#1087) currently need a workaround for this: a dynamic body without gravity, with the collisions with static objects disabled and a much higher density while it walks, then the settings restored and the behavior deactivated and activated again to rebuild the body before the impulse. With this action, the death becomes "change the body type to Dynamic, apply the impulse".

Behavior

  • The body is created again right away, with the existing recreateBody, which keeps the linear and angular velocities. So an impulse applied by the next action acts on the new body (a kinematic body ignores impulses). A kinematic body moved by another behavior already gets that movement as its velocity at each step, so it keeps its momentum when it becomes dynamic.
  • Layers: static bodies are in the layers 1 to 4, dynamic and kinematic bodies in the layers 5 to 8 (as before, from getLayersAccordingToBodyType). Between kinematic and dynamic, nothing changes. To or from static, the object collides with the same objects with the default layers (1 and 5).
  • Mesh shapes can only be static (the editor already changes a mesh shape to a box for the other types): the action is ignored for them.
  • Multiplayer: the body type is synchronized, like the layers and the masks (bodyType in the props of the sync data; a client already recreates the body at each update, so the new type is used right away). The value is absent in data from older versions, and then ignored. It adds a short string per physics object to each sync message.

3D characters and 3D cars

An object is always in one of these two states:

  • No 3D character or 3D car behavior is activated (none on the object, or all deactivated): the body has the type of the physics behavior, and the action changes it, like for any physics object.
  • A 3D character or 3D car behavior is activated: it handles the body, and the body type is the one set in the editor. The action is ignored.

Adding, activating or deactivating these behaviors only moves an object between the two states:

  • The physics behavior keeps the list of these behaviors (registered in their onCreated, called when the object is created, before the next action runs), and the action checks whether one of them is activated. So an object with both a 3D character and a 3D car (even activated together by mistake) stays locked until both are deactivated.
  • When one of them is activated again, the body gets back its type from the editor (the type may have been changed while it was deactivated, and the car and the character rely on it for their layers).
  • A deactivated 3D car no longer takes the body back when an action changes its drive wheels (getPhysics3D now checks that the car is activated, like the character already did).

This allows, for instance, deactivating a 3D car when the player gets out of it and making it static (or kinematic, to be moved along a path), then activating it again when the player gets back in.

Editor: collision filtering by body type

The editor only showed the layers of the current type, and hid the masks of static objects: they couldn't be set for an object whose type is changed during the game. Showing all of them in a flat list was confusing too (the masks of a static object looked like they used static layers). Now "Collision filtering" has two sections, always in the same order whatever the type:

  • If static: the layers (1 to 4);
  • If dynamic or kinematic: the layers (5 to 8) and the masks, labelled "Masks (collides with)" (the layers 1 to 4 of the static objects, and 5 to 8 of the dynamic and kinematic ones).

The descriptions say that a static object has no masks: it collides with the dynamic and kinematic objects whose masks include its layers. Both layer rows edit the same layers value (layersIfDynamicOrKinematic is a property shown in the editor only, written to layers; Physics3D can't be overridden per instance). The description of the type mentions the "Body type" action (and the kinematic part is corrected: kinematic objects aren't moved by forces).

Sub-groups of properties: a property group can now contain a sub-group, written Group > Sub-group (here Collision filtering > If static). propertiesMapToSchema shows it as a section of the group, in the order of its first property (sub-groups aren't sorted by name, so "If static", with the layers property, comes before "If dynamic or kinematic"), and the compact properties panel draws its title smaller than the group title (isSubgroup in the schema). Groups without > are unchanged. The Physics3D behavior editor shows the same sections.

The masks of the 2D physics behavior are labelled "Masks (collides with)" too.

Physics3D behavior editor Compact properties panel
full compact

Tests

In Physics3DRuntimeBehavior.spec.js:

  • a kinematic body stays in the air, and falls once made dynamic;
  • the velocity of the body is kept;
  • an impulse applied right after the change moves the body;
  • with the default layers, a ground changed from kinematic to static, and a box changed from static to dynamic, still collide (the box lands on the ground);
  • the body type is synchronized (multiplayer).

In PhysicsCharacter3DRuntimeBehavior.spec.js:

  • changing the body type of a character right after it is created, and later, does nothing (it stays dynamic, lands and walks off a platform);
  • while the character is deactivated, the body type can be changed (made kinematic, it stays in the air); activated again, it's dynamic, lands and walks;
  • with the physics behavior deactivated too: the type can be changed, kept when only the physics behavior is activated again, and restored (and locked) when only the character is activated again, before the physics behavior.

In the new PhysicsCar3DRuntimeBehavior.spec.js (the car script is added to the karma files):

  • changing the body type of a car right after it is created, and later, does nothing (it drives forward);
  • while the car is deactivated, the body type can be changed; activated again, it's dynamic, lands and drives;
  • changing the drive wheels of a deactivated car doesn't take the body back (a kinematic car stays in the air);
  • an object with both a 3D car and a 3D character: locked while both are activated, and while only the character is (which walks); unlocked when both are deactivated, also while the physics behavior is deactivated; locked and restored when only the car is activated again; then the car lands and drives.

In PropertiesMapToSchema.spec.js: sub-groups are sections of their group, in the order of their first property, next to the fields of the group (both tests fail without the change).

Each part of the runtime change is caught by a test: without the restore of the type, 4 tests fail; checking that all of these behaviors are activated instead of one, the test with both behaviors fails; without the activation check of the car, the drive wheels test fails; without the synchronization of bodyType, the multiplayer test fails. All 1440 GDJS tests pass (karma, headless Chromium), and tsc and prettier pass. For the editor: flow and eslint pass, the extension sanity tests pass, and both panels were checked in Storybook (the screenshots above).

Checked in a real game: the FPS with enemies starter, changed to use the action (kinematic enemies moved by the navmesh, "Body type: Dynamic" before the impulse of the shot), exported and run by the gameplay tests of the examples repository with GDevelop 5.6.284 and the Physics3D files of this PR (Physics3D is the same in 5.6.284 and master): its 6 tests pass. A walking enemy blocks the player completely, and a shot enemy becomes dynamic, is thrown back, tumbles and lies on the ground. With the unchanged 5.6.284, the action is unknown and skipped: the enemy is not thrown, and the test fails.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj

An object can now switch between static, dynamic and kinematic during the
game. For instance, an enemy moved by a pathfinding behavior can be
kinematic (other objects can't push it), and become dynamic to be thrown by
an impulse when it is hit.

The body is created again right away, keeping its velocities, so that an
impulse applied by the next action acts on the new body. Static bodies are
in the layers 1 to 4, dynamic and kinematic bodies in the layers 5 to 8: with
the default layers (1 and 5), the object collides with the same objects
after the change. Bodies of 3D characters and 3D cars, and mesh shapes (which
can only be static), are not changed. The body type is synchronized in
multiplayer games.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

⏱️ Game engine benchmarks: ✅ no significant change on the 31 benchmarks

All the 31 benchmarks and how they are measured
Benchmark Base This version Change Range between rounds
≈ platformer objects jumping in loop(1000x) 140.1ms 132.6ms -5.4% -14.3% to +3.7%
≈ hitBoxesCollisionTest of 10 objects with 10 objects(10000x) 81.9ms 82.1ms +0.3% -3.4% to +4.1%
≈ hitBoxesCollisionTest of 30 objects with 30 objects(1112x) 24.7ms 25.2ms +1.9% -2.5% to +12.2%
≈ hitBoxesCollisionTest of 100 objects with 100 objects(100x) 8.316ms 8.296ms -0.2% -1.7% to +2.4%
≈ hitBoxesCollisionTest of 500 objects with 500 objects(4x) 1.869ms 1.888ms +1.0% -1.7% to +4.2%
≈ hitBoxesCollisionTest of 1000 objects with 1000 objects(1x) 1.071ms 1.023ms -4.6% -6.0% to +0.5%
≈ hitBoxesCollisionTest of 1 object with 1000 objects(1000x) 52.5ms 54.1ms +3.0% -0.4% to +5.1%
≈ hitBoxesCollisionTest of 10 objects with 1000 objects(100x) 10.9ms 11.1ms +1.1% -5.9% to +4.4%
≈ hitBoxesCollisionTest of 1000 objects with 1 object(1000x) 56.0ms 56.5ms +1.0% -2.5% to +3.4%
≈ hitBoxesCollisionTest of 500 objects with themselves(4x) 1.296ms 1.318ms +1.6% -3.7% to +3.1%
≈ setAngle and setLength(100000x) 11.5ms 11.5ms -0.1% -1.3% to +0.0%
≈ convertCoords(100000x) 21.2ms 21.3ms +0.1% -1.9% to +1.2%
≈ convertInverseCoords(100000x) 21.3ms 21.5ms +0.6% -1.0% to +1.8%
≈ collisionTest between two overlapping rectangles(120000x) 47.4ms 47.5ms +0.2% -2.1% to +2.0%
≈ collisionTest between two non overlapping rectangles(120000x) 11.0ms 11.1ms +0.5% -1.8% to +1.3%
≈ getAABB of a non rotated, default center(180000x) 19.1ms 19.0ms -0.3% -0.6% to +0.1%
≈ getAABB of a rotated, default center(180000x) 19.1ms 19.1ms -0.0% -0.5% to +1.0%
≈ getAABB of a non rotated, non default center(180000x) 21.2ms 20.2ms -4.8% -9.9% to +3.2%
≈ getAABB of a rotated, non default center(180000x) 60.5ms 61.4ms +1.4% -3.8% to +5.3%
≈ getAABB of a non rotated sprite, with custom hitboxes, origin and center(60000x) 19.7ms 19.6ms -0.7% -1.8% to +1.7%
≈ getAABB of a rotated sprite, with custom hitboxes, origin and center(60000x) 67.7ms 67.2ms -0.8% -1.7% to +0.6%
≈ get(400000x) 11.6ms 11.6ms 0.0% -0.2% to +0.1%
≈ Scene: Static sprites (10000) 5.219ms 5.124ms -1.8% -10.1% to +15.3%
≈ Scene: Moving and rotating sprites (5000) 6.639ms 6.365ms -4.1% -6.8% to +1.1%
≈ Scene: Collisions and separation (500 vs 500) 15.3ms 15.5ms +1.3% -7.1% to +15.9%
≈ Scene: Physics 2D (500 bodies) 3.413ms 3.360ms -1.5% -5.4% to +1.7%
≈ Scene: Physics 3D (500 bodies) 13.0ms 12.8ms -1.3% -5.8% to +1.8%
≈ Scene: Platformer characters (300) 1.040ms 1.022ms -1.7% -4.3% to +1.0%
≈ Scene: Text updates (1000) 56.8ms 55.9ms -1.6% -2.9% to +3.8%
≈ Scene: Objects creation and deletion (100 per frame) 1.938ms 2.147ms +10.8% -10.8% to +24.6%
≈ Scene: Heavy events (variables, loops, strings) 1.569ms 1.607ms +2.5% -0.2% to +10.3%

✅ The counters (draw calls, objects count and final state) of the 9 scene benchmarks run on both versions are unchanged.

Comparing 69a0516e to its base 93577554 (on a pull request, the version tested is the pull request merged into its base), run alternately 4 times on the same machine (AMD EPYC 7763 64-Core Processor, 4 cores, rendering without GPU). Times are the median duration of a batch of iterations (micro benchmarks) or of a frame (scene benchmarks). A change is reported only when larger than 5% and than the differences between the rounds of a same version, and measured by every pair of rounds.

The 3D character and car behaviors replace the body updater of the physics
behavior only when they first handle the body, at their first step. An
action changing the body type right after the object is created (like
"Create Player" then "Change the body type of Player") ran before that,
and changed the body type of the character.

They now mark the physics behavior when they are created, and the body type
of a marked behavior is never changed. The test changes the body type of a
character right after it is created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
The body type can't be changed while a 3D character or 3D car behavior of
the object is activated, and can be changed otherwise, like for any other
physics object. When one of them is activated again, the body gets back its
type from the editor.

The physics behavior keeps the list of these behaviors instead of a flag,
so an object with both (even activated together by mistake) stays locked
until both are deactivated.

A deactivated 3D car no longer takes the body back when its drive wheels
are changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
Static objects use the layers 1 to 4 and moving objects the layers 5 to 8.
The editor only showed the layers of the current type, and hid the masks of
static objects, so they couldn't be set for an object whose type is changed
during the game.

Now both are shown: the layers of the current type, then "Layers if made
static" (or "if made dynamic or kinematic"), and the masks, labelled "Masks
if made dynamic or kinematic" for a static object, with descriptions saying
when they are used. The description of the type mentions the "Body type"
action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Storybook for this branch: latest · commit cae1aee

Updated automatically after each Storybook build.

The layers and masks are shown in two sections of "Collision filtering":
"If static" (the layers 1 to 4) and "If dynamic or kinematic" (the layers
5 to 8 and the masks, which say which layers the object collides with).
Before, the rows were reordered and relabelled when the type changed, and
the masks of a static object looked like they used static layers.

Property groups can now contain a sub-group, written "Group > Sub-group":
the properties panels show it as a section of the group, with a smaller
title, in the order of its first property. The Physics3D behavior editor
shows the same sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
Like the 3D physics behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wa8uKpYuQrYqGYnqGCsquj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants