diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/all.texture_profiles b/physics/box2d_mouse_joint_example/box2d_mouse_joint/all.texture_profiles new file mode 100644 index 00000000..5b0d7762 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/all.texture_profiles @@ -0,0 +1,18 @@ +path_settings { + path: "**" + profile: "Default" +} +profiles { + name: "Default" + platforms { + os: OS_ID_GENERIC + formats { + format: TEXTURE_FORMAT_RGBA + compression_level: BEST + compression_type: COMPRESSION_TYPE_DEFAULT + } + mipmaps: false + max_texture_size: 0 + premultiply_alpha: true + } +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/SourceSansPro-Semibold.ttf b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/SourceSansPro-Semibold.ttf new file mode 100644 index 00000000..50205948 Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/SourceSansPro-Semibold.ttf differ diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/text28.font b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/text28.font new file mode 100644 index 00000000..ca069a57 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/fonts/text28.font @@ -0,0 +1,5 @@ +font: "/assets/fonts/SourceSansPro-Semibold.ttf" +material: "/builtins/fonts/font-df.material" +size: 28 +output_format: TYPE_DISTANCE_FIELD +characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_soft.png b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_soft.png new file mode 100644 index 00000000..928ca6ab Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_soft.png differ diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_tight.png b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_tight.png new file mode 100644 index 00000000..751d76e8 Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/body_tight.png differ diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/sprites.atlas b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/sprites.atlas new file mode 100644 index 00000000..e8102fdb --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/sprites.atlas @@ -0,0 +1,10 @@ +images { + image: "/assets/images/body_soft.png" +} +images { + image: "/assets/images/body_tight.png" +} +images { + image: "/assets/images/target.png" +} +extrude_borders: 2 diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/target.png b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/target.png new file mode 100644 index 00000000..242c88d1 Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/assets/images/target.png differ diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V2.appmanifest b/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V2.appmanifest new file mode 100644 index 00000000..b3abe8c8 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V2.appmanifest @@ -0,0 +1,95 @@ +platforms: + arm64-ios: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + x86_64-ios: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + armv7-android: + context: + excludeLibs: [] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + jetifier: true + arm64-android: + context: + excludeLibs: [] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + jetifier: true + arm64-osx: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + x86_64-osx: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + frameworks: [] + linkFlags: [] + x86_64-linux: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + arm64-linux: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + x86-win32: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + x86_64-win32: + context: + excludeLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + wasm-web: + context: + excludeLibs: [] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] + wasm_pthread-web: + context: + excludeLibs: [] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [] + linkFlags: [] diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V3.appmanifest b/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V3.appmanifest new file mode 100644 index 00000000..9751deca --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/box2D_V3.appmanifest @@ -0,0 +1,100 @@ +platforms: + armv7-ios: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + arm64-ios: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + frameworks: [] + linkFlags: [] + x86_64-ios: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + frameworks: [] + linkFlags: [] + armv7-android: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] + jetifier: true + arm64-android: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeJars: [] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] + jetifier: true + arm64-osx: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + frameworks: [] + linkFlags: [] + x86_64-osx: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + frameworks: [] + linkFlags: [] + x86_64-linux: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] + arm64-linux: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] + x86-win32: + context: + excludeLibs: [libphysics, libbox2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [libphysics_2d.lib, libbox2d.lib, libscript_box2d.lib, libphysics_3d.lib] + linkFlags: [] + x86_64-win32: + context: + excludeLibs: [libphysics, libbox2d_defold, script_box2d_defold] + excludeSymbols: [] + symbols: [] + libs: [libphysics_2d.lib, libbox2d.lib, libscript_box2d.lib, libphysics_3d.lib] + linkFlags: [] + wasm-web: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] + wasm_pthread-web: + context: + excludeLibs: [physics, box2d_defold, script_box2d_defold] + excludeJsLibs: [] + excludeSymbols: [] + symbols: [] + libs: [physics_2d, box2d, script_box2d, physics_3d] + linkFlags: [] diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example.md b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example.md new file mode 100644 index 00000000..ab189e67 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example.md @@ -0,0 +1,48 @@ +--- +tags: physics, box2d +title: Box2D Mouse Joint +brief: Create a mouse joint that pulls dynamic bodies toward a moving target. +author: Defold Foundation +scripts: box2d_mouse_joint_v3.script, box2d_mouse_joint_v2.script +thumbnail: thumbnail.webp +--- + +This example shows how to create and control Box2D mouse joints from Lua. A visible target point moves automatically until you move the mouse or drag a touch point. Two dynamic bodies follow the same target with different spring settings, making the softer joint stretch behind the target while the tighter joint follows more closely. + +## What You'll Learn + +- How to get Box2D bodies from collision object components with `b2d.get_body()`. +- How to create a mouse joint with `b2d.joint.create_mouse()`. +- How `frequency` (V2) or `hertz` (V3), `damping_ratio`, and `max_force` affect spring-like motion. +- How to update the joint target every frame with `b2d.joint.set_mouse_target()`. + +## Setup + +The collection contains 5 game objects: + +`controller` +: Contains both backend scripts, `box2d_mouse_joint_v2.script` and `box2d_mouse_joint_v3.script`. Each script checks the active Box2D version and only one script runs. + +`target` +: Contains the visible target sprite and a static collision object. The static body is used as the mouse-joint anchor. + +`soft_body` +: Contains the orange sprite and a dynamic collision object. This body is connected with lower spring frequency/hertz and damping, so it follows the target more softly. + +`tight_body` +: Contains the blue sprite and a dynamic collision object. This body is connected with higher spring frequency/hertz, damping, and max force, so it follows the target more tightly. + +`info` +: Contains labels for the soft body, tight body, input instruction, and active Box2D backend. + +The project uses `box2D_V2.appmanifest` by default. To compare the newer backend, switch the native extension app manifest in `game.project` to `box2D_V3.appmanifest`. + +![setup](setup.png) + +## How It Works + +The mouse joint connects the invisible static body to a dynamic body. The joint does not move the body instantly. Instead, it applies a limited spring-damper force toward the target position. This is why the body can stretch, overshoot, and settle. + +The V2 script uses `frequency` and `damping_ratio` in the joint definition. The V3 script uses `hertz` and `damping_ratio`. Both scripts then call `b2d.joint.set_mouse_target()` every frame so the target can move continuously. + +The example creates two mouse joints with different values. The orange body uses a lower frequency/hertz and lower damping, so it visibly lags behind. The blue body uses a higher frequency/hertz, more damping, and a larger max force, so it feels tighter and follows the target more directly. diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint.collection b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint.collection new file mode 100644 index 00000000..7d1437da --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint.collection @@ -0,0 +1,123 @@ +name: "box2d_mouse_joint" +instances { + id: "controller" + prototype: "/example/controller.go" +} +instances { + id: "target" + prototype: "/example/target.go" + position { + x: 360.0 + y: 420.0 + z: 0.2 + } +} +instances { + id: "soft_body" + prototype: "/example/soft_body.go" + position { + x: 270.0 + y: 310.0 + z: 0.1 + } +} +instances { + id: "tight_body" + prototype: "/example/tight_body.go" + position { + x: 450.0 + y: 310.0 + z: 0.1 + } +} +scale_along_z: 0 +embedded_instances { + id: "info" + data: "embedded_components {\n" + " id: \"label\"\n" + " type: \"label\"\n" + " data: \"size {\\n" + " x: 210.0\\n" + " y: 74.0\\n" + "}\\n" + "color {\\n" + " x: 0.99215686\\n" + " y: 0.4\\n" + " z: 0.13725491\\n" + "}\\n" + "line_break: true\\n" + "text: \\\"SOFT\\\\n" + "\\\"\\n" + " \\\"low frequency\\\\n" + "\\\"\\n" + " \\\"damping\\\"\\n" + "font: \\\"/assets/fonts/text28.font\\\"\\n" + "material: \\\"/builtins/fonts/label-df.material\\\"\\n" + "\"\n" + " position {\n" + " x: 120.0\n" + " y: 360.0\n" + " }\n" + "}\n" + "embedded_components {\n" + " id: \"label1\"\n" + " type: \"label\"\n" + " data: \"size {\\n" + " x: 210.0\\n" + " y: 74.0\\n" + "}\\n" + "color {\\n" + " x: 0.0\\n" + " y: 0.9019608\\n" + " z: 0.88235295\\n" + "}\\n" + "line_break: true\\n" + "text: \\\"TIGHT\\\\n" + "\\\"\\n" + " \\\"high frequency\\\\n" + "\\\"\\n" + " \\\"damping\\\"\\n" + "font: \\\"/assets/fonts/text28.font\\\"\\n" + "material: \\\"/builtins/fonts/label-df.material\\\"\\n" + "\"\n" + " position {\n" + " x: 600.0\n" + " y: 360.0\n" + " }\n" + "}\n" + "embedded_components {\n" + " id: \"label2\"\n" + " type: \"label\"\n" + " data: \"size {\\n" + " x: 680.0\\n" + " y: 46.0\\n" + "}\\n" + "line_break: true\\n" + "text: \\\"Move the mouse or drag touch\\\"\\n" + "font: \\\"/assets/fonts/text28.font\\\"\\n" + "material: \\\"/builtins/fonts/label-df.material\\\"\\n" + "\"\n" + " position {\n" + " x: 360.0\n" + " y: 600.0\n" + " }\n" + "}\n" + "embedded_components {\n" + " id: \"version_label\"\n" + " type: \"label\"\n" + " data: \"size {\\n" + " x: 640.0\\n" + " y: 42.0\\n" + "}\\n" + "line_break: true\\n" + "text: \\\"Box2D mouse joint\\\"\\n" + "font: \\\"/assets/fonts/text28.font\\\"\\n" + "material: \\\"/builtins/fonts/label-df.material\\\"\\n" + "\"\n" + " position {\n" + " x: 360.0\n" + " y: 650.0\n" + " }\n" + "}\n" + "" +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v2.script b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v2.script new file mode 100644 index 00000000..ec37c89b --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v2.script @@ -0,0 +1,252 @@ +local TOUCH = hash("touch") + +local START_TARGET = vmath.vector3(360, 420, 0) +local SOFT_START = vmath.vector3(270, 310, 0) +local TIGHT_START = vmath.vector3(450, 310, 0) + +local SOFT_LINE_COLOR = vmath.vector4(1.0, 0.65, 0.22, 1.0) +local TIGHT_LINE_COLOR = vmath.vector4(0.35, 0.85, 1.0, 1.0) +local TARGET_LINE_COLOR = vmath.vector4(0.45, 1.0, 0.65, 1.0) + +local function draw_line(from, to, color) + -- <1> Draw a debug/helper line through the render socket. + -- The line exists for one frame only, so it must be redrawn every update. + msg.post("@render:", "draw_line", { + start_point = from, + end_point = to, + color = color + }) +end + +local function set_target(self, position) + -- <2> Store the current mouse-joint target as a world-space point. + -- The mouse/touch pointer is not a Box2D body. It is only a target position + -- that the mouse joint tries to pull the dynamic body toward. + self.target = vmath.vector3(position.x, position.y, 0) + + -- <3> Move the visible target marker so the user can see the point followed + -- by both mouse joints. + go.set_position(self.target, self.target_url) +end + +local function reset_body(body, position) + -- <4> Set the Box2D body transform directly. + -- This is good for example setup/reset. Do not use this as normal per-frame + -- movement, because it teleports the body and can produce non-physical motion. + b2d.body.set_transform(body, position, 0) + + -- <5> Clear previous motion so the example starts from a deterministic state. + b2d.body.set_linear_velocity(body, vmath.vector3()) + b2d.body.set_angular_velocity(body, 0) + + -- <6> Wake the body so it reacts immediately after reset. + b2d.body.set_awake(body, true) +end + +local function setup_body(body) + -- <7> Disable gravity for this body. + -- This keeps the example focused on the mouse-joint spring behaviour. + b2d.body.set_gravity_scale(body, 0) + + -- <8> Prevent the body from rotating. + -- Rotation would add noise to the visual explanation, while this example is + -- about soft versus tight positional following. + b2d.body.set_fixed_rotation(body, true) + + -- <9> Add linear damping. + -- This reduces endless sliding and helps the body settle after movement. + b2d.body.set_linear_damping(body, 1.5) +end + +local function create_mouse_joints(self) + -- <10> Get native Box2D body handles from Defold collision object components. + -- `target` acts as the static/reference body for the mouse joints. + local anchor_body = b2d.get_body(msg.url(nil, "target", "collisionobject")) + self.soft_body = b2d.get_body(msg.url(nil, "soft_body", "collisionobject")) + self.tight_body = b2d.get_body(msg.url(nil, "tight_body", "collisionobject")) + + setup_body(self.soft_body) + setup_body(self.tight_body) + + reset_body(self.soft_body, SOFT_START) + reset_body(self.tight_body, TIGHT_START) + + -- <11> Create a soft V2 mouse joint. + -- In Box2D V2-style definitions, spring frequency is controlled by + -- `frequency`, not `hertz`. + self.soft_joint = b2d.joint.create_mouse(anchor_body, self.soft_body, { + -- <12> Initial world target. This value is later updated every frame. + target = self.target, + + -- <13> Maximum force the joint can apply. + -- Lower max force allows more visible lag/stretch. + max_force = 850, + + -- <14> V2 spring frequency. + -- Lower frequency makes the body follow more softly and slowly. + frequency = 1.5, + + -- <15> Damping ratio. + -- Lower damping allows more bounce/overshoot. + damping_ratio = 0.35, + + -- <16> The connected bodies should not collide with each other. + collide_connected = false, + }) + + -- <17> Create a tighter V2 mouse joint. + -- It follows the same target, but uses stronger/stiffer parameters. + self.tight_joint = b2d.joint.create_mouse(anchor_body, self.tight_body, { + target = self.target, + + -- <18> Higher max force means the body can be pulled more aggressively. + max_force = 6500, + + -- <19> Higher frequency means a stiffer/faster spring response. + frequency = 8.0, + + -- <20> Higher damping removes more oscillation. + damping_ratio = 0.9, + + collide_connected = false, + }) +end + +local function update_auto_target(self, dt) + if self.user_control then + return + end + + -- <21> Animate the target automatically before user interaction. + -- This makes the example demonstrate itself on the examples website. + self.time = self.time + dt + + set_target(self, vmath.vector3( + 360 + math.cos(self.time * 1.35) * 170, + 395 + math.sin(self.time * 1.10) * 95, + 0 +)) +end + +local function update_joints(self) +-- <22> Update both mouse joints with the current world target. +-- The joints are created once, but their target can be changed every frame. +b2d.joint.set_mouse_target(self.soft_joint, self.target) +b2d.joint.set_mouse_target(self.tight_joint, self.target) + +-- <23> Keep the bodies awake while the target moves. +-- This avoids relying on b2d.joint.wake_bodies(), which may not exist in +-- the active V2 runtime. +b2d.body.set_awake(self.soft_body, true) +b2d.body.set_awake(self.tight_body, true) +end + +local function draw_connections(self) +-- <24> Read the current simulated positions from Box2D. +-- These positions are the result of the physics step, not manually animated +-- sprite positions. +local soft_position = b2d.body.get_position(self.soft_body) +local tight_position = b2d.body.get_position(self.tight_body) + +-- <25> Draw spring-like helper lines from the target to each body. +-- The longer the line, the more that body is lagging/stretching. +draw_line(self.target, soft_position, SOFT_LINE_COLOR) +draw_line(self.target, tight_position, TIGHT_LINE_COLOR) + +-- <26> Draw a small cross at the target point. +draw_line(self.target + vmath.vector3(-18, 0, 0), self.target + vmath.vector3(18, 0, 0), TARGET_LINE_COLOR) +draw_line(self.target + vmath.vector3(0, -18, 0), self.target + vmath.vector3(0, 18, 0), TARGET_LINE_COLOR) +end + +function init(self) +-- <27> Run this script only when the active Box2D backend is V2. +-- The same collection may contain both V2 and V3 scripts, but only the +-- matching one should initialize. +self.active = b2d.get_version().major == 2 + +if not self.active then + return +end + +self.target_url = msg.url(nil, "target", nil) +self.time = 0 +self.user_control = false + +set_target(self, START_TARGET) +create_mouse_joints(self) + +-- <28> Show which backend-specific script is active. +label.set_text("/version_label#label", "Box2D V2 mouse joint") + +-- <29> Required before this script receives on_input() callbacks. +msg.post(".", "acquire_input_focus") +end + +function update(self, dt) +if not self.active then + return +end + +update_auto_target(self, dt) +update_joints(self) +draw_connections(self) +end + +function on_input(self, action_id, action) +if not self.active then + return +end + +-- <30> Mouse input usually comes with action_id == nil, while touch input +-- uses the configured "touch" binding. Both provide x/y screen coordinates. +if (action_id == TOUCH or action_id == nil) and action.x and action.y then + self.user_control = true + set_target(self, vmath.vector3(action.x, action.y, 0)) +end +end + +function final(self) +if not self.active then + return +end + +-- <31> Destroy scripted joints explicitly when the script is finalized. +b2d.joint.destroy(self.soft_joint) +b2d.joint.destroy(self.tight_joint) + +msg.post(".", "release_input_focus") +end + +--[[ +1. `@render:` is Defold's render socket. The "draw_line" message draws a temporary helper line. +2. A mouse joint follows a world-space target point. The pointer itself is not a physics body. +3. The target marker is only visual. It helps show what the bodies are trying to follow. +4. `b2d.body.set_transform()` directly changes the body's world transform. Good for setup/reset, not continuous movement. +5. Clearing linear and angular velocity removes previous momentum. +6. `b2d.body.set_awake()` wakes the body so it reacts immediately in the simulation. +7. `b2d.body.set_gravity_scale(body, 0)` disables gravity for this body only. +8. `b2d.body.set_fixed_rotation(body, true)` prevents rotation and keeps the visual demonstration clean. +9. `b2d.body.set_linear_damping()` damps velocity over time and reduces endless drift. +10. `b2d.get_body()` converts a Defold collision object URL into a Box2D body handle used by the b2d API. +11. `b2d.joint.create_mouse()` creates a mouse joint between two bodies. The second body is the one visibly pulled toward the target. +12. `target` is the initial world-space target point. +13. `max_force` caps how strongly the joint can pull. Lower values create more visible stretch. +14. `frequency` is the V2 spring frequency. Lower values feel softer; higher values feel tighter. +15. `damping_ratio` controls bounce and overshoot. Lower values oscillate more; higher values settle faster. +16. `collide_connected = false` prevents the connected bodies from colliding with each other. +17. The tight joint uses the same mouse-joint API but different parameters. +18. Higher `max_force` lets the joint correct the body's position more strongly. +19. Higher `frequency` makes the spring respond faster. +20. Higher `damping_ratio` removes more oscillation. +21. Automatic target motion keeps the example animated before the user interacts. +22. `b2d.joint.set_mouse_target()` updates the world target of an existing mouse joint. +23. Waking the bodies avoids sleeping-body cases where the target changes but the body does not visibly react immediately. +24. `b2d.body.get_position()` reads the current simulated world position of a body. +25. The helper lines visualize how far each body stretches away from the target. +26. The target cross is a visual helper, not part of the physics simulation. +27. `b2d.get_version().major` selects the backend-specific script. +28. `label.set_text()` updates the label. It uses message passing internally, so the URL must be correct. +29. `acquire_input_focus` is needed to receive `on_input()`. +30. Pointer input switches the example from automatic motion to direct user-controlled target movement. +31. `b2d.joint.destroy()` removes joints created through the scripted joint API. +]] \ No newline at end of file diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v3.script b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v3.script new file mode 100644 index 00000000..f9f28f9b --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/box2d_mouse_joint_v3.script @@ -0,0 +1,263 @@ +local TOUCH = hash("touch") + +local START_TARGET = vmath.vector3(360, 420, 0) +local SOFT_START = vmath.vector3(270, 310, 0) +local TIGHT_START = vmath.vector3(450, 310, 0) + +local SOFT_LINE_COLOR = vmath.vector4(1.0, 0.65, 0.22, 1.0) +local TIGHT_LINE_COLOR = vmath.vector4(0.35, 0.85, 1.0, 1.0) +local TARGET_LINE_COLOR = vmath.vector4(0.45, 1.0, 0.65, 1.0) + +local function draw_line(from, to, color) + -- <1> Draw a debug/helper line through the render socket. + -- These lines are not persistent scene objects. They exist for one frame, + -- so the script redraws them every update. + msg.post("@render:", "draw_line", { + start_point = from, + end_point = to, + color = color + }) +end + +local function set_target(self, position) + -- <2> Store the current mouse-joint target as a world-space position. + -- The mouse joint does not connect to a real "mouse body". It tracks this + -- world point, which we later pass to b2d.joint.set_mouse_target(). + self.target = vmath.vector3(position.x, position.y, 0) + + -- <3> Move the visible target marker so users can see what point the + -- mouse joints are trying to follow. + go.set_position(self.target, self.target_url) +end + +local function reset_body(body, position) + -- <4> Teleport the physics body to a known start position. + -- b2d.body.set_transform() directly sets the Box2D body's world transform. + -- This is useful for setup/reset, but should generally not be used every + -- frame for normal gameplay motion because it bypasses physical simulation. + b2d.body.set_transform(body, position, 0) + + -- <5> Clear leftover motion so both bodies start the example cleanly. + b2d.body.set_linear_velocity(body, vmath.vector3()) + b2d.body.set_angular_velocity(body, 0) + + -- <6> Wake the body so Box2D immediately simulates it after the reset. + -- This is safer here than relying on b2d.joint.wake_bodies(), which may not + -- be available in all beta/V2 runtime builds. + b2d.body.set_awake(body, true) +end + +local function setup_body(body) + -- <7> Disable gravity for this body. + -- A mouse joint is easiest to understand when the only visible force is the + -- spring-like pull toward the target, not gravity pulling the body downward. + b2d.body.set_gravity_scale(body, 0) + + -- <8> Prevent rotation. + -- This keeps the demo visually focused on mouse-joint translation/stretching + -- instead of the boxes spinning while they follow the target. + b2d.body.set_fixed_rotation(body, true) + + -- <9> Add linear damping. + -- This damps velocity over time and helps the bodies settle instead of + -- sliding forever after the target moves. + b2d.body.set_linear_damping(body, 1.5) +end + +local function create_mouse_joints(self) + -- <10> Get native Box2D body handles from Defold collision object components. + -- b2d.get_body() takes a collision object URL and returns the b2Body handle + -- used by the new Box2D scripting API. + local anchor_body = b2d.get_body(msg.url(nil, "target", "collisionobject")) + self.soft_body = b2d.get_body(msg.url(nil, "soft_body", "collisionobject")) + self.tight_body = b2d.get_body(msg.url(nil, "tight_body", "collisionobject")) + + setup_body(self.soft_body) + setup_body(self.tight_body) + + reset_body(self.soft_body, SOFT_START) + reset_body(self.tight_body, TIGHT_START) + + -- <11> Create the softer mouse joint. + -- A mouse joint connects two Box2D bodies, but its main job is to make body_b + -- follow a world-space target point. Here the target object acts as the + -- static reference body, and soft_body is the dynamic body being pulled. + self.soft_joint = b2d.joint.create_mouse(anchor_body, self.soft_body, { + -- <12> Initial world target. This will be updated every frame later. + target = self.target, + + -- <13> Maximum force the joint is allowed to apply. + -- Lower values make the body lag behind more because the joint cannot + -- instantly pull it to the target. + max_force = 850, + + -- <14> Spring frequency in Box2D V3 terms. + -- Lower hertz means a softer, slower spring. + hertz = 1.5, + + -- <15> Damping ratio controls how much oscillation is removed. + -- Lower damping allows more elastic movement and visible overshoot. + damping_ratio = 0.35, + + -- <16> The connected bodies should not collide with each other. + -- This is usually what you want for a joint demonstration. + collide_connected = false, + }) + + -- <17> Create the tighter mouse joint. + -- It follows the same target, but with much stronger spring settings. + self.tight_joint = b2d.joint.create_mouse(anchor_body, self.tight_body, { + target = self.target, + + -- <18> Higher max_force lets the joint pull harder. + max_force = 6500, + + -- <19> Higher hertz means the spring reacts faster and feels stiffer. + hertz = 8.0, + + -- <20> Higher damping removes oscillation, so the body follows in a + -- tighter, more controlled way. + damping_ratio = 0.9, + + collide_connected = false, + }) +end + +local function update_auto_target(self, dt) + if self.user_control then + return + end + + -- <21> Animate the target before the user touches/clicks. + -- This keeps the example alive on the website even without interaction. + self.time = self.time + dt + + set_target(self, vmath.vector3( + 360 + math.cos(self.time * 1.35) * 170, + 395 + math.sin(self.time * 1.10) * 95, + 0 +)) +end + +local function update_joints(self) +-- <22> Update both mouse joints with the current target. +-- This is the key runtime control call. The joint was created once in init(), +-- but its target can be changed every frame. +b2d.joint.set_mouse_target(self.soft_joint, self.target) +b2d.joint.set_mouse_target(self.tight_joint, self.target) + +-- <23> Make sure the bodies are awake when the target moves. +-- Sleeping bodies may not visibly react until woken by the simulation. +b2d.body.set_awake(self.soft_body, true) +b2d.body.set_awake(self.tight_body, true) +end + +local function draw_connections(self) +-- <24> Query the current simulated body positions from Box2D. +-- We draw lines from the target to each body to visualize the stretch of +-- each mouse joint. +local soft_position = b2d.body.get_position(self.soft_body) +local tight_position = b2d.body.get_position(self.tight_body) + +draw_line(self.target, soft_position, SOFT_LINE_COLOR) +draw_line(self.target, tight_position, TIGHT_LINE_COLOR) + +-- <25> Draw a small cross at the target point so the user can clearly see +-- what the bodies are following. +draw_line(self.target + vmath.vector3(-18, 0, 0), self.target + vmath.vector3(18, 0, 0), TARGET_LINE_COLOR) +draw_line(self.target + vmath.vector3(0, -18, 0), self.target + vmath.vector3(0, 18, 0), TARGET_LINE_COLOR) +end + +function init(self) +-- <26> Run this script only when the active Box2D backend is V3. +-- The same collection can include both V2 and V3 scripts, but only one +-- should initialize depending on the current app manifest/backend. +self.active = b2d.get_version().major == 3 + +if not self.active then + return +end + +self.target_url = msg.url(nil, "target", nil) +self.time = 0 +self.user_control = false + +set_target(self, START_TARGET) +create_mouse_joints(self) + +-- <27> Show which backend-specific script is active. +label.set_text("/version_label#label", "Box2D V3 mouse joint") + +-- <28> Acquire input focus so this script receives mouse/touch input in +-- on_input(). +msg.post(".", "acquire_input_focus") +end + +function update(self, dt) +if not self.active then + return +end + +update_auto_target(self, dt) +update_joints(self) +draw_connections(self) +end + +function on_input(self, action_id, action) +if not self.active then + return +end + +-- <29> Mouse input commonly arrives with action_id == nil, while touch input +-- uses the "touch" binding. Both provide screen-space x/y coordinates here. +if (action_id == TOUCH or action_id == nil) and action.x and action.y then + self.user_control = true + set_target(self, vmath.vector3(action.x, action.y, 0)) +end +end + +function final(self) +if not self.active then + return +end + +-- <30> Destroy scripted joints explicitly when the script is finalized. +-- This keeps the example clean when the collection is unloaded or hot-reloaded. +b2d.joint.destroy(self.soft_joint) +b2d.joint.destroy(self.tight_joint) + +msg.post(".", "release_input_focus") +end + +--[[ +1. `@render:` is Defold's render socket. The "draw_line" message draws a temporary helper line. +2. A mouse joint follows a world-space target point. It does not require the mouse itself to be a physics body. +3. The target marker is only visual. Moving it makes the invisible target position understandable. +4. `b2d.body.set_transform()` directly sets the Box2D body position and angle. Good for reset/setup, not for continuous gameplay movement. +5. Clearing velocity prevents the reset state from inheriting old momentum. +6. `b2d.body.set_awake()` forces the body to participate in simulation immediately. +7. `b2d.body.set_gravity_scale(body, 0)` disables gravity for this body only. +8. `b2d.body.set_fixed_rotation(body, true)` prevents the body from rotating, keeping the example readable. +9. `b2d.body.set_linear_damping()` makes velocity decay over time, reducing endless drift. +10. `b2d.get_body()` converts a Defold collision object component URL into a native Box2D body handle. +11. `b2d.joint.create_mouse()` creates a mouse joint between two bodies. The second body is the one visibly pulled toward the target. +12. `target` is the initial world-space point the mouse joint tries to follow. +13. `max_force` caps the force used by the joint. Too low = very stretchy; higher = stronger pull. +14. `hertz` is the V3 spring frequency. Lower values feel softer, higher values feel tighter. +15. `damping_ratio` controls oscillation. Lower values bounce more; higher values settle faster. +16. `collide_connected = false` prevents the two bodies connected by the joint from colliding. +17. The tight joint uses the same API but different parameters, demonstrating how tuning changes the feel. +18. Higher `max_force` allows stronger correction toward the target. +19. Higher `hertz` makes the constraint respond faster. +20. Higher `damping_ratio` removes more bounce. +21. Automatic target motion makes the example self-demonstrating. +22. `b2d.joint.set_mouse_target()` updates the world target of an existing mouse joint every frame. +23. Waking bodies avoids cases where sleeping physics bodies do not react immediately to the changed target. +24. `b2d.body.get_position()` reads the current simulated world position from Box2D. +25. The target cross is a visual helper, not part of the physics simulation. +26. `b2d.get_version().major` lets one shared collection choose the correct backend-specific script. +27. `label.set_text()` updates the on-screen backend label. +28. `acquire_input_focus` is required before this script receives `on_input()` callbacks. +29. Pointer input takes over from automatic motion and drives the mouse-joint target directly. +30. `b2d.joint.destroy()` removes joints created through the scripted joint API. +]] \ No newline at end of file diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/controller.go b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/controller.go new file mode 100644 index 00000000..2030a34a --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/controller.go @@ -0,0 +1,8 @@ +components { + id: "box2d_v2" + component: "/example/box2d_mouse_joint_v2.script" +} +components { + id: "box2d_v3" + component: "/example/box2d_mouse_joint_v3.script" +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/soft_body.go b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/soft_body.go new file mode 100644 index 00000000..ded9d375 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/soft_body.go @@ -0,0 +1,38 @@ +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"body_soft\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/assets/images/sprites.atlas\"\n" + "}\n" + "" +} +embedded_components { + id: "collisionobject" + type: "collisionobject" + data: "type: COLLISION_OBJECT_TYPE_DYNAMIC\n" + "mass: 1.0\n" + "friction: 0.0\n" + "restitution: 0.05\n" + "group: \"dragged\"\n" + "mask: \"world\"\n" + "embedded_collision_shape {\n" + " shapes {\n" + " shape_type: TYPE_BOX\n" + " position {\n" + " }\n" + " rotation {\n" + " }\n" + " index: 0\n" + " count: 3\n" + " }\n" + " data: 60.0\n" + " data: 60.0\n" + " data: 10.0\n" + "}\n" + "linear_damping: 1.5\n" + "angular_damping: 2.0\n" + "" +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/target.go b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/target.go new file mode 100644 index 00000000..97116c49 --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/target.go @@ -0,0 +1,40 @@ +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"target\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/assets/images/sprites.atlas\"\n" + "}\n" + "" + scale { + x: 0.5 + y: 0.5 + } +} +embedded_components { + id: "collisionobject" + type: "collisionobject" + data: "type: COLLISION_OBJECT_TYPE_STATIC\n" + "mass: 0.0\n" + "friction: 0.0\n" + "restitution: 0.0\n" + "group: \"world\"\n" + "mask: \"dragged\"\n" + "embedded_collision_shape {\n" + " shapes {\n" + " shape_type: TYPE_BOX\n" + " position {\n" + " }\n" + " rotation {\n" + " }\n" + " index: 0\n" + " count: 3\n" + " }\n" + " data: 4.0\n" + " data: 4.0\n" + " data: 4.0\n" + "}\n" + "" +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/tight_body.go b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/tight_body.go new file mode 100644 index 00000000..892e932a --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/example/tight_body.go @@ -0,0 +1,38 @@ +embedded_components { + id: "sprite" + type: "sprite" + data: "default_animation: \"body_tight\"\n" + "material: \"/builtins/materials/sprite.material\"\n" + "textures {\n" + " sampler: \"texture_sampler\"\n" + " texture: \"/assets/images/sprites.atlas\"\n" + "}\n" + "" +} +embedded_components { + id: "collisionobject" + type: "collisionobject" + data: "type: COLLISION_OBJECT_TYPE_DYNAMIC\n" + "mass: 1.0\n" + "friction: 0.0\n" + "restitution: 0.05\n" + "group: \"dragged\"\n" + "mask: \"world\"\n" + "embedded_collision_shape {\n" + " shapes {\n" + " shape_type: TYPE_BOX\n" + " position {\n" + " }\n" + " rotation {\n" + " }\n" + " index: 0\n" + " count: 3\n" + " }\n" + " data: 60.0\n" + " data: 60.0\n" + " data: 10.0\n" + "}\n" + "linear_damping: 1.5\n" + "angular_damping: 2.0\n" + "" +} diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/game.project b/physics/box2d_mouse_joint_example/box2d_mouse_joint/game.project new file mode 100644 index 00000000..01e8d2be --- /dev/null +++ b/physics/box2d_mouse_joint_example/box2d_mouse_joint/game.project @@ -0,0 +1,36 @@ +[bootstrap] +main_collection = /example/box2d_mouse_joint.collectionc + +[script] +shared_state = 1 + +[display] +width = 720 +height = 720 +high_dpi = 1 + +[android] +input_method = HiddenInputField + +[physics] +gravity_y = 0.0 +scale = 0.01 +use_fixed_timestep = 1 + +[html5] +scale_mode = no_scale + +[input] +game_binding = /builtins/input/all.input_bindingc + +[project] +title = Box2D Mouse Joint + +[native_extension] +app_manifest = /box2D_V2.appmanifest + +[render] +clear_color_red = 0.160156 +clear_color_green = 0.164063 +clear_color_blue = 0.183594 + diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/setup.png b/physics/box2d_mouse_joint_example/box2d_mouse_joint/setup.png new file mode 100644 index 00000000..186ef9c4 Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/setup.png differ diff --git a/physics/box2d_mouse_joint_example/box2d_mouse_joint/thumbnail.webp b/physics/box2d_mouse_joint_example/box2d_mouse_joint/thumbnail.webp new file mode 100644 index 00000000..19d02515 Binary files /dev/null and b/physics/box2d_mouse_joint_example/box2d_mouse_joint/thumbnail.webp differ