Created
September 17, 2025 08:45
-
-
Save andyman/3c0199e6adf0904c7fbe542a0ef23579 to your computer and use it in GitHub Desktop.
A simple quick-n-dirty XR microgesture detector made during a game jam for Godot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extends Node | |
| class_name XRMicrogestureDetector | |
| @export var is_right : bool = false | |
| @export var hand : Node3D | |
| @export var thumb_tip_node : Node3D | |
| @export var thumb_distal_node : Node3D | |
| @export var index_mid_node : Node3D | |
| @export var index_tip_node : Node3D | |
| # swipe parameters | |
| @export var close_enough_to_center: float = 0.01 | |
| @export var swipe_threshold: float = 0.02 # how far thumb must move relative to start | |
| @export var swipe_time_limit: float = 0.3 # max time allowed for swipe (seconds) | |
| @export var swipe_confirm_frames: int = 3 # require N consecutive frames over threshold | |
| # visuals feedback | |
| @export var left_arrow : MeshInstance3D | |
| @export var right_arrow : MeshInstance3D | |
| @export var center_dot : MeshInstance3D | |
| @export var swiping_scale_multiplier : float = 1.1 | |
| @export var swiped_arrow_scale_multiplier : float = 1.3 | |
| @export var idle_material : Material | |
| @export var swiping_material : Material | |
| @export var swiped_material : Material | |
| # swipe signals | |
| signal left_swipe(hand: String) | |
| signal right_swipe(hand: String) | |
| # internal | |
| var timer: float = 0.0 | |
| var swipe_frames: int = 0 | |
| var left_swipe_material_timer = 0.0 | |
| var right_swipe_material_timer = 0.0 | |
| var left_arrow_base_scale : Vector3 | |
| var right_arrow_base_scale : Vector3 | |
| var center_dot_base_scale : Vector3 | |
| func _ready() -> void: | |
| left_arrow_base_scale = left_arrow.scale | |
| right_arrow_base_scale = right_arrow.scale | |
| center_dot_base_scale = center_dot.scale | |
| func _physics_process(delta: float) -> void: | |
| _process_hand(delta) | |
| func _process_hand(delta: float) -> void: | |
| # --- get joint positions --- | |
| var thumb_tip : Vector3 = thumb_tip_node.global_position | |
| var thumb_distal : Vector3 = thumb_distal_node.global_position | |
| var index_mid : Vector3 = index_mid_node.global_position | |
| var index_tip : Vector3 = index_tip_node.global_position | |
| # convert to hand-local space | |
| var thumb_tip_local : Vector3 = hand.to_local(thumb_tip) | |
| var thumb_distal_local : Vector3 = hand.to_local(thumb_distal) | |
| var thumb_central_local : Vector3 = (thumb_tip_local + thumb_distal_local) * 0.5 | |
| var index_mid_local = hand.to_local(index_mid) | |
| var index_tip_local = hand.to_local(index_tip) | |
| var near_center : bool = (thumb_tip_local - index_mid_local).length() < close_enough_to_center or (thumb_distal_local - index_mid_local).length() < close_enough_to_center or (thumb_central_local - index_mid_local).length() < close_enough_to_center | |
| # if we're near the center then keep resetting the timer | |
| if (near_center): | |
| timer = 0.0 | |
| swipe_frames = 0 | |
| elif (timer < swipe_time_limit): | |
| timer += delta | |
| var swipe_dir : Vector3 = (index_tip_local - index_mid_local).normalized() | |
| if (is_right): | |
| swipe_dir = -swipe_dir | |
| var projection_total : float = (thumb_tip_local - index_mid_local).dot(swipe_dir) | |
| var hand_name : String = "right" if is_right else "left" | |
| if projection_total > swipe_threshold: | |
| swipe_frames += 1 | |
| if swipe_frames >= swipe_confirm_frames: | |
| swipe_frames = 0 | |
| left_arrow.scale = left_arrow_base_scale * swiped_arrow_scale_multiplier | |
| timer = swipe_time_limit + 1.0 | |
| left_swipe_material_timer = 0.25 | |
| emit_signal("left_swipe", hand_name) | |
| elif projection_total < -swipe_threshold: | |
| swipe_frames += 1 | |
| if swipe_frames >= swipe_confirm_frames: | |
| swipe_frames = 0 | |
| right_arrow.scale = right_arrow_base_scale * swiped_arrow_scale_multiplier | |
| timer = swipe_time_limit + 1.0 | |
| right_swipe_material_timer = 0.25 | |
| emit_signal("right_swipe", hand_name) | |
| if (timer < swipe_time_limit): | |
| left_arrow.scale = left_arrow.scale.lerp(left_arrow_base_scale * swiping_scale_multiplier, delta * 20.0) | |
| right_arrow.scale = right_arrow.scale.lerp(right_arrow_base_scale * swiping_scale_multiplier, delta * 20.0) | |
| center_dot.scale = center_dot.scale.lerp(center_dot_base_scale * swiping_scale_multiplier, delta * 20.0) | |
| left_arrow.material_override = swiping_material | |
| right_arrow.material_override = swiping_material | |
| center_dot.material_override = swiping_material | |
| else: | |
| left_arrow.scale = left_arrow.scale.lerp(left_arrow_base_scale, delta * 3.0) | |
| right_arrow.scale = right_arrow.scale.lerp(right_arrow_base_scale, delta * 3.0) | |
| center_dot.scale = center_dot.scale.lerp(center_dot_base_scale, delta * 3.0) | |
| left_arrow.material_override = swiped_material if (left_swipe_material_timer > 0.0) else idle_material | |
| right_arrow.material_override = swiped_material if (right_swipe_material_timer > 0.0) else idle_material | |
| center_dot.material_override = idle_material | |
| left_swipe_material_timer -= delta | |
| right_swipe_material_timer -= delta | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment