Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,22 @@ Broadcasts a custom event.
#### `onTrigger(meshName, callback)`
Sets up collision/trigger detection for a mesh.

#### `rumbleController(controller = "ANY", strength = 1, durationMs = 200)`
Triggers rumble/haptic feedback on supported gamepads and controllers.

**Parameters:**
- `controller` (string): `"ANY"`, `"LEFT"`, or `"RIGHT"`.
- `strength` (number): Intensity from `0` to `1`.
- `durationMs` (number): Rumble duration in milliseconds.

**Returns:**
- `Promise<boolean>`: `true` when a rumble command was sent, otherwise `false` (for unsupported devices/browsers).

**Example:**
```javascript
await rumbleController("ANY", 0.8, 250);
```

## Examples

For a complete working example, see [example.html](example.html) in the repository, which demonstrates a full Flock XR application with character movement, physics, and camera controls.
Expand Down Expand Up @@ -575,4 +591,4 @@ Most Flock functions are asynchronous and should be awaited. If a mesh or resour

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on contributing to Flock XR.
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on contributing to Flock XR.
68 changes: 68 additions & 0 deletions api/xr.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,74 @@ export const flockXR = {
color: "white",
});
},
async rumbleController(controller = "ANY", strength = 1, durationMs = 200) {
const normalizedController = String(controller || "ANY").toUpperCase();
const normalizedStrength = Math.min(
1,
Math.max(0, Number.isFinite(strength) ? strength : 1),
);
const normalizedDuration = Math.max(
0,
Math.floor(Number.isFinite(durationMs) ? durationMs : 200),
);

if (typeof navigator === "undefined" || !navigator.getGamepads) {
return false;
}

const gamepads = Array.from(navigator.getGamepads() || []).filter(Boolean);
if (!gamepads.length) {
return false;
}

const matchesController = (gamepad) => {
if (normalizedController === "ANY") {
return true;
}

const hand = String(gamepad.hand || "").toLowerCase();
const id = String(gamepad.id || "").toLowerCase();
if (normalizedController === "LEFT") {
return hand === "left" || id.includes("left");
}
if (normalizedController === "RIGHT") {
return hand === "right" || id.includes("right");
}
return true;
};

const targetPad = gamepads.find(matchesController);
if (!targetPad) {
return false;
}

const actuator =
targetPad.vibrationActuator || targetPad.hapticActuators?.[0] || null;
if (!actuator) {
return false;
}

try {
if (typeof actuator.playEffect === "function") {
await actuator.playEffect("dual-rumble", {
startDelay: 0,
duration: normalizedDuration,
weakMagnitude: normalizedStrength,
strongMagnitude: normalizedStrength,
});
return true;
}

if (typeof actuator.pulse === "function") {
await actuator.pulse(normalizedStrength, normalizedDuration);
return true;
}
} catch {
return false;
}

return false;
},
exportMesh(meshName, format) {
//meshName = "scene";

Expand Down
38 changes: 37 additions & 1 deletion blocks/xr.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,41 @@ export function defineXRBlocks() {

},
};
}

Blockly.Blocks["rumble_controller"] = {
init: function () {
this.jsonInit({
type: "rumble_controller",
message0: translate("rumble_controller"),
args0: [
{
type: "field_dropdown",
name: "CONTROLLER",
options: [
getDropdownOption("ANY"),
getDropdownOption("LEFT"),
getDropdownOption("RIGHT"),
],
},
{
type: "input_value",
name: "STRENGTH",
check: "Number",
},
{
type: "input_value",
name: "DURATION_MS",
check: "Number",
},
],
previousStatement: null,
nextStatement: null,
colour: categoryColours["Scene"],
tooltip: getTooltip("rumble_controller"),
});
this.setHelpUrl(getHelpUrlFor(this.type));
this.setStyle('scene_blocks');

},
};
}
2 changes: 2 additions & 0 deletions flock.js
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ export const flock = {
setCameraBackground:
this.setCameraBackground?.bind(this),
setXRMode: this.setXRMode?.bind(this),
rumbleController: this.rumbleController?.bind(this),
applyForce: this.applyForce?.bind(this),
moveByVector: this.moveByVector?.bind(this),
glideTo: this.glideTo?.bind(this),
Expand Down Expand Up @@ -1083,6 +1084,7 @@ export const flock = {
"setSky",
"setFog",
"setCameraBackground",
"rumbleController",
"lightIntensity",
"lightColor",
"create3DText",
Expand Down
18 changes: 18 additions & 0 deletions generators/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -3517,6 +3517,24 @@ export function defineGenerators() {
return `await setXRMode("${mode}");\n`;
};

javascriptGenerator.forBlock["rumble_controller"] = function (block) {
const controller = block.getFieldValue("CONTROLLER");
const strength =
javascriptGenerator.valueToCode(
block,
"STRENGTH",
javascriptGenerator.ORDER_NONE,
) || "1";
const durationMs =
javascriptGenerator.valueToCode(
block,
"DURATION_MS",
javascriptGenerator.ORDER_NONE,
) || "200";

return `await rumbleController("${controller}", ${strength}, ${durationMs});\n`;
};

javascriptGenerator.forBlock["camera_control"] = function (block) {
const key = block.getFieldValue("KEY");
const action = block.getFieldValue("ACTION");
Expand Down
3 changes: 3 additions & 0 deletions locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export default {
// Custom block translations - XR blocks
device_camera_background: "use %1 camera as background",
set_xr_mode: "set XR mode to %1",
rumble_controller: "rumble %1 strength %2 for %3 ms",

// Blockly message overrides for English
LISTS_CREATE_WITH_INPUT_WITH: "list",
Expand Down Expand Up @@ -633,6 +634,8 @@ export default {
"Use the device camera as the background for the scene. Works on both mobile and desktop.",
set_xr_mode_tooltip:
"Set the XR mode for the scene.\nOptions: VR, AR, Magic Window.",
rumble_controller_tooltip:
"Trigger controller rumble on supported gamepads using strength from 0 to 1 and duration in milliseconds.",

// Dropdown option translations
AWAIT_option: "await",
Expand Down
23 changes: 23 additions & 0 deletions toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,29 @@ const toolboxSceneXR = {
type: "set_xr_mode",
keyword: "xr",
},
{
kind: "block",
type: "rumble_controller",
keyword: "rumble",
inputs: {
STRENGTH: {
shadow: {
type: "math_number",
fields: {
NUM: 1,
},
},
},
DURATION_MS: {
shadow: {
type: "math_number",
fields: {
NUM: 200,
},
},
},
},
},
{
kind: "block",
type: "export_mesh",
Expand Down