Make gamepad vibration work on web export

Input.start_joy_vibration() is a documented engine no-op on Godot's
HTML5 export (never wired to the browser side). Controls.vibrate()
wraps it: native platforms go straight through as before, web drives
the browser's Gamepad Haptic Actuator API directly via JavaScriptBridge.
Gamepad movement/ability input already worked on desktop browsers via
the standard Web Gamepad API, so only vibration needed a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
This commit is contained in:
2026-09-10 17:35:54 +03:00
parent 91d88cd5c7
commit cd54242de0
3 changed files with 32 additions and 5 deletions
+2 -3
View File
@@ -835,9 +835,8 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
health_changed.emit(_hp, _max_hp) health_changed.emit(_hp, _max_hp)
hit_taken.emit(cause) hit_taken.emit(cause)
camera_pivot.call(&"trigger_hit", 0.7) camera_pivot.call(&"trigger_hit", 0.7)
# Vibrate controller 0 with half weak strength, full strong strength, for 0.5 seconds # Vibrate controller 0 with half weak strength, full strong strength, for 0.2 seconds
if DP.b("gamepad_vibration"): Controls.vibrate(0, 0.5, 1, 0.2)
Input.start_joy_vibration(0, 0.5, 1, 0.2)
# A spear gores a spot — blood spurts from the entry wound (where it struck the # A spear gores a spot — blood spurts from the entry wound (where it struck the
# bull). Melee gores pass no position and skip the spurt; gore can be turned off. # bull). Melee gores pass no position and skip the spurt; gore can be turned off.
+1 -2
View File
@@ -11,5 +11,4 @@ func _process(_delta: float) -> void:
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"): if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
bear_gate_open = true bear_gate_open = true
$AnimationPlayer.play("Gate_opening") $AnimationPlayer.play("Gate_opening")
if DP.b("gamepad_vibration"): Controls.vibrate(0, 0.5, .5, 7)
Input.start_joy_vibration(0, 0.5, .5, 7)
+29
View File
@@ -222,6 +222,35 @@ func rigid_skin_enabled() -> bool:
return DP.b("use_rigid_skin") return DP.b("use_rigid_skin")
## Rumble a connected gamepad, gated on the DP "gamepad_vibration" flag. Off web this is a thin
## wrapper over Input.start_joy_vibration(); on web that call is a documented engine no-op
## (godotengine/godot#96985 — never wired to the browser side), so there we drive the Gamepad
## Haptic Actuator API ourselves. Safe no-op wherever it's unsupported (Safari, most Firefox
## versions, or a controller with no rumble motors) — desktop Chromium/Edge is the target.
func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, duration: float) -> void:
if not DP.b("gamepad_vibration"):
return
if not OS.has_feature("web"):
Input.start_joy_vibration(device, weak_magnitude, strong_magnitude, duration)
return
JavaScriptBridge.eval("""
(function(){
try {
var pads = navigator.getGamepads ? navigator.getGamepads() : [];
var pad = pads[%d];
var act = pad && pad.vibrationActuator;
if (act && act.playEffect) {
act.playEffect('dual-rumble', {
duration: %d,
weakMagnitude: %f,
strongMagnitude: %f
});
}
} catch (e) {}
})();
""" % [device, int(duration * 1000.0), weak_magnitude, strong_magnitude], true)
## Returns the first keyboard event bound to an action, or null. ## Returns the first keyboard event bound to an action, or null.
func get_key_event(action: StringName) -> InputEventKey: func get_key_event(action: StringName) -> InputEventKey:
for event: InputEvent in InputMap.action_get_events(action): for event: InputEvent in InputMap.action_get_events(action):