Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rumble Manager and Demo #557

Merged
merged 1 commit into from
Jun 4, 2024
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
logs
Thumbs.db
android/
.venv/
1 change: 1 addition & 0 deletions addons/godot-xr-tools/editor/icons/rumble.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions addons/godot-xr-tools/editor/icons/rumble.svg.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://dsexrsb8t0vi2"
path="res://.godot/imported/rumble.svg-104961f6551a931675972887ab17d2bc.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/godot-xr-tools/editor/icons/rumble.svg"
dest_files=["res://.godot/imported/rumble.svg-104961f6551a931675972887ab17d2bc.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
13 changes: 10 additions & 3 deletions addons/godot-xr-tools/player/poke/poke.tscn
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
[gd_scene load_steps=6 format=3 uid="uid://bjcxf427un2wp"]
[gd_scene load_steps=8 format=3 uid="uid://bjcxf427un2wp"]

[ext_resource type="Script" path="res://addons/godot-xr-tools/player/poke/poke.gd" id="1"]
[ext_resource type="Script" path="res://addons/godot-xr-tools/player/poke/poke_body.gd" id="2"]
[ext_resource type="Script" path="res://addons/godot-xr-tools/rumble/rumbler.gd" id="3_41fwo"]
[ext_resource type="Resource" uid="uid://brci6umrcd157" path="res://addons/godot-xr-tools/rumble/tap_rumble.tres" id="4_mskmk"]

[sub_resource type="SphereShape3D" id="1"]
resource_local_to_scene = true
Expand All @@ -14,7 +16,7 @@ height = 0.01
radial_segments = 32
rings = 16

[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_g1b1h"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hgik7"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.8, 0.8, 1, 0.5)
Expand All @@ -34,7 +36,12 @@ shape = SubResource("1")

[node name="MeshInstance" type="MeshInstance3D" parent="PokeBody"]
mesh = SubResource("2")
surface_material_override/0 = SubResource("StandardMaterial3D_g1b1h")
surface_material_override/0 = SubResource("StandardMaterial3D_hgik7")

[node name="Rumbler" type="Node" parent="."]
script = ExtResource("3_41fwo")
event = ExtResource("4_mskmk")

[connection signal="pointing_event" from="." to="Rumbler" method="rumble_pointer"]
[connection signal="body_contact_end" from="PokeBody" to="." method="_on_PokeBody_body_contact_end"]
[connection signal="body_contact_start" from="PokeBody" to="." method="_on_PokeBody_body_contact_start"]
11 changes: 11 additions & 0 deletions addons/godot-xr-tools/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ func _enter_tree():
"0.2,0.8,0.05",
0.7)

# Add input haptics_scale to the project settings
_define_project_setting(
"godot_xr_tools/input/haptics_scale",
TYPE_FLOAT,
PROPERTY_HINT_RANGE,
"0.0,1.0,0.1",
1.0)

# Add input y_axis_dead_zone to the project settings
_define_project_setting(
"godot_xr_tools/input/y_axis_dead_zone",
Expand Down Expand Up @@ -133,6 +141,9 @@ func _enter_tree():
add_autoload_singleton(
"XRToolsUserSettings",
"res://addons/godot-xr-tools/user_settings/user_settings.gd")
add_autoload_singleton(
"XRToolsRumbleManager",
"res://addons/godot-xr-tools/rumble/rumble_manager.gd")


func _exit_tree():
Expand Down
19 changes: 19 additions & 0 deletions addons/godot-xr-tools/rumble/rumble_event.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@icon("res://addons/godot-xr-tools/editor/icons/rumble.svg")
class_name XRToolsRumbleEvent
extends Resource

## XR Tools Rumble Event Resource

## Strength of the rumbling
@export_range(0, 1, 0.10) var magnitude: float = 0.5

## Whether the rumble can be active during a tree pause
@export var active_during_pause: bool = false

@export_category("Timing")

## Whether the rumble continues until cleared
@export var indefinite: bool = false

## Time to rumble (unless indefinite)
@export_range(10, 4000, 10) var duration_ms: int = 300
161 changes: 161 additions & 0 deletions addons/godot-xr-tools/rumble/rumble_manager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
extends Node


## XR Tools Rumble (Controllers) Manager Script
##
## This script uses the controller's existing rumble intensity variable,
## and allows you to rumble the controller for a certain amount
## of time 'beats'.
##
## Example: something hits you while you're mowing the lawn,
## i.e. a short intense rumble happens during long low rumble.


## Name in the OpenXR Action Map for haptics
const HAPTIC_ACTION := &"haptic" # TODO: Migrate

# Shorthand for all trackers, in use to be substituted with _queues.keys()
const ALL_TRACKERS := [&"all"]


# A Queue Per Haptic device (Dictionary<StringName, XRToolsRumbleManagerQueue>)
var _queues: Dictionary = {}


## Add support for is_xr_class
func is_xr_class(p_name: String) -> bool:
return p_name == "XRToolsRumbleManager"


## Get the default Haptics Scale value
func get_default_haptics_scale() -> float:
var default = 1.0

# Check if the project has overridden the addon's default
if ProjectSettings.has_setting("godot_xr_tools/input/haptics_scale"):
default = ProjectSettings.get_setting("godot_xr_tools/input/haptics_scale")

if default < 0.0 or default > 1.0:
# out of bounds? reset to default
default = 1.0

return default


## Used to convert gamepad magnitudes to equivalent XR haptic magnitude
func combine_magnitudes(weak: float, strong: float) -> float:
if strong >= 0.01:
return 0.5 + clamp(strong / 2, 0.0, 0.5)
return clamp(weak / 2, 0.0, 0.5)


# On Ready
func _ready():
if Engine.is_editor_hint():
return

# Some rumble events are active during pause
process_mode = PROCESS_MODE_ALWAYS

# Create a queues for standard controllers
_queues[&"left_hand"] = XRToolsRumbleManagerQueue.new()
_queues[&"right_hand"] = XRToolsRumbleManagerQueue.new()


# Determine how much to - and perform the - rumbles each tick
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return

# We'll be subtracting this from the event remaining ms
var delta_ms = int(delta * 1000)

for tracker_name in _queues:
var haptic_queue : XRToolsRumbleManagerQueue = _queues[tracker_name]

# default to noXRToolsRumbleManagerQueuensure it's a float, or it rounds to all or nothing!)
var magnitude: float = 0.0

# Iterate over the events
for key in haptic_queue.events.keys():
var event : XRToolsRumbleEvent = haptic_queue.events[key]

# if we're paused and it's not supposed to be active, skip
if get_tree().paused and not event.active_during_pause:
continue

# If we've passed the threshold from positive to negative, the event is done
if !event.indefinite and haptic_queue.time_remaining[key] < 0:
clear(key, [tracker_name])
continue

# Reduce the time remaining
haptic_queue.time_remaining[key] -= delta_ms

# If it's of greater magnitude, update left magnitude to be set
if event.magnitude > magnitude:
magnitude = event.magnitude

# scale the final magnitude
magnitude *= XRToolsUserSettings.haptics_scale

# Make that tracker rumble
if magnitude > 0:
XRServer.primary_interface.trigger_haptic_pulse(
HAPTIC_ACTION,
tracker_name, # if the tracker name isn't valid, it will error but continue
0,
magnitude,
0.1,
0)


# Add an event
func add(event_key: Variant, event: XRToolsRumbleEvent,
trackers: Array = ALL_TRACKERS) -> void:
if not event_key:
push_error("Event key is invalid!")
return

if not event:
clear(event_key, trackers)
return

# Substitube the shorthand for all trackers with the real thing
if trackers == ALL_TRACKERS:
trackers = _queues.keys()

for tracker in trackers:
if tracker is XRNode3D:
tracker = tracker.tracker

# Create queue first time a target is suggested
if not _queues.has(tracker):
_queues[tracker] = XRToolsRumbleManagerQueue.new()

# Add the event and it's remaining time to the respective queues
_queues[tracker].events[event_key] = event
_queues[tracker].time_remaining[event_key] = event.duration_ms


# Remove an event
func clear(event_key: Variant, trackers: Array = ALL_TRACKERS) -> void:
if not event_key:
push_error("Event key is invalid!")
return

# Substitube the shorthand for all trackers with the real thing
if trackers == ALL_TRACKERS:
trackers = _queues.keys()

for tracker in trackers:
if tracker is XRNode3D:
tracker = tracker.tracker

# Ignore if the queue doesn't exist
if not _queues.has(tracker):
continue

# Remove the event and it's remaining time from the respective queues
_queues[tracker].events.erase(event_key)
_queues[tracker].time_remaining.erase(event_key)
12 changes: 12 additions & 0 deletions addons/godot-xr-tools/rumble/rumble_manager_queue.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class_name XRToolsRumbleManagerQueue
extends Resource

# All currently-active events (Dictionary<Variant, XRToolsRumbleEvent>)
var events: Dictionary

# All currently-active events' time remaining (Dictionary<Variant, int>)
var time_remaining: Dictionary

func _init():
events = {}
time_remaining = {}
76 changes: 76 additions & 0 deletions addons/godot-xr-tools/rumble/rumbler.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/rumble.svg")
class_name XRToolsRumbler
extends Node

## XR Tools Rumbler
##
## A node you attach to handle (contain and make easy to activate/cancel)
## a particular rumble event.

## The details of this rumbler
@export var event: XRToolsRumbleEvent : set = _set_event

@export var target: XRNode3D

## Activate the event
func rumble() -> void:
if is_instance_valid(target):
XRToolsRumbleManager.add(self, event, [target.tracker])


## Cancel the event
func cancel() -> void:
XRToolsRumbleManager.clear(self)


## Rumble on the hand which owns the node
func rumble_hand(hand_child: Node3D) -> void:
var hand: XRNode3D = XRHelpers.get_xr_controller(hand_child)
if is_instance_valid(hand):
XRToolsRumbleManager.add(self, event, [hand.tracker])


## Cancel rumble for the hand which owns the node
func cancel_hand(hand_child: Node3D) -> void:
var hand: XRNode3D = XRHelpers.get_xr_controller(hand_child)
if is_instance_valid(hand):
XRToolsRumbleManager.clear(self, [hand.tracker])


## Activate the event, if provided the XR player body
func rumble_if_player_body(body: Node3D) -> void:
if is_instance_valid(body) and body is XRToolsPlayerBody:
rumble()


## Cancel rumble for the event, if provided the XR player body
func cancel_if_player_body(body: Node3D) -> void:
if is_instance_valid(body) and body is XRToolsPlayerBody:
cancel()


## Activate the event during an active pointer event
func rumble_pointer(event : XRToolsPointerEvent) -> void:
if event.event_type == XRToolsPointerEvent.Type.PRESSED:
rumble_hand(event.pointer)
elif event.event_type == XRToolsPointerEvent.Type.RELEASED:
cancel_hand(event.pointer)


func _set_event(p_event: XRToolsRumbleEvent) -> void:
event = p_event
update_configuration_warnings()


# This method verifies the hand has a valid configuration.
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()

# Check hand for animation player
if not event:
warnings.append("Rumbler must have a rumble event")

# Return warnings
return warnings

10 changes: 10 additions & 0 deletions addons/godot-xr-tools/rumble/tap_rumble.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="XRToolsRumbleEvent" load_steps=2 format=3 uid="uid://brci6umrcd157"]

[ext_resource type="Script" path="res://addons/godot-xr-tools/rumble/rumble_event.gd" id="1_r2qd4"]

[resource]
script = ExtResource("1_r2qd4")
magnitude = 0.5
active_during_pause = true
indefinite = false
duration_ms = 10
Loading
Loading