extends Node ## Emitted when the WebXR primary is changed (either by the user or auto detected). signal webxr_primary_changed (value) enum WebXRPrimary { AUTO, THUMBSTICK, TRACKPAD, } @export_group("Input") ## User setting for snap-turn @export var snap_turning : bool = true ## User setting for y axis dead zone @export var y_axis_dead_zone : float = 0.1 ## User setting for y axis dead zone @export var x_axis_dead_zone : float = 0.2 @export_group("Player") ## User setting for player height @export var player_height : float = 1.85: set = set_player_height @export_group("WebXR") ## User setting for WebXR primary @export var webxr_primary : WebXRPrimary = WebXRPrimary.AUTO: set = set_webxr_primary ## Settings file name to persist user settings var settings_file_name : String = "user://xtools_user_settings.json" ## Records the first input to generate input (thumbstick or trackpad). var webxr_auto_primary := 0 # Called when the node enters the scene tree for the first time. func _ready(): # var webxr_interface = XRServer.find_interface("WebXR") # if webxr_interface: # XRServer.tracker_added.connect(self._on_webxr_tracker_added) _load() ## Reset to default values func reset_to_defaults() -> void: # Reset to defaults. # Where applicable we obtain our project settings snap_turning = XRTools.get_default_snap_turning() y_axis_dead_zone = XRTools.get_y_axis_dead_zone() x_axis_dead_zone = XRTools.get_x_axis_dead_zone() player_height = XRTools.get_player_standard_height() webxr_primary = WebXRPrimary.AUTO webxr_auto_primary = 0 ## Set the player height property func set_player_height(new_value : float) -> void: player_height = clamp(new_value, 1.0, 2.5) ## Set the WebXR primary func set_webxr_primary(new_value : WebXRPrimary) -> void: webxr_primary = new_value if webxr_primary == WebXRPrimary.AUTO: if webxr_auto_primary == 0: # Don't emit the signal yet, wait until we detect which to use. pass else: webxr_primary_changed.emit(webxr_auto_primary) else: webxr_primary_changed.emit(webxr_primary) ## Gets the WebXR primary (taking into account auto detection). func get_real_webxr_primary() -> WebXRPrimary: if webxr_primary == WebXRPrimary.AUTO: return webxr_auto_primary return webxr_primary ## Save the settings to file func save() -> void: # Convert the settings to a dictionary var settings := { "input" : { "default_snap_turning" : snap_turning, "y_axis_dead_zone" : y_axis_dead_zone, "x_axis_dead_zone" : x_axis_dead_zone }, "player" : { "height" : player_height }, "webxr" : { "webxr_primary" : webxr_primary, } } # Convert the settings dictionary to text var settings_text := JSON.stringify(settings) # Attempt to open the settings file for writing var file := FileAccess.open(settings_file_name, FileAccess.WRITE) if not file: push_warning("Unable to write to %s" % settings_file_name) return # Write the settings text to the file file.store_line(settings_text) file.close() ## Get the action associated with a WebXR primary choice static func get_webxr_primary_action(primary : WebXRPrimary) -> String: match primary: WebXRPrimary.THUMBSTICK: return "thumbstick" WebXRPrimary.TRACKPAD: return "trackpad" _: return "auto" ## Load the settings from file func _load() -> void: # First reset our values reset_to_defaults() # Skip if no settings file found if !FileAccess.file_exists(settings_file_name): return # Attempt to open the settings file for reading var file := FileAccess.open(settings_file_name, FileAccess.READ) if not file: push_warning("Unable to read from %s" % settings_file_name) return # Read the settings text var settings_text := file.get_as_text() file.close() # Parse the settings text and verify it's a dictionary var settings_raw = JSON.parse_string(settings_text) if typeof(settings_raw) != TYPE_DICTIONARY: push_warning("Settings file %s is corrupt" % settings_file_name) return # Parse our input settings var settings : Dictionary = settings_raw if settings.has("input"): var input : Dictionary = settings["input"] if input.has("default_snap_turning"): snap_turning = input["default_snap_turning"] if input.has("y_axis_dead_zone"): y_axis_dead_zone = input["y_axis_dead_zone"] if input.has("x_axis_dead_zone"): x_axis_dead_zone = input["x_axis_dead_zone"] # Parse our player settings if settings.has("player"): var player : Dictionary = settings["player"] if player.has("height"): player_height = player["height"] # Parse our WebXR settings if settings.has("webxr"): var webxr : Dictionary = settings["webxr"] if webxr.has("webxr_primary"): webxr_primary = webxr["webxr_primary"] ## Used to connect to tracker events when using WebXR. func _on_webxr_tracker_added(tracker_name: StringName, _type: int) -> void: if tracker_name == &"left_hand" or tracker_name == &"right_hand": var tracker := XRServer.get_tracker(tracker_name) tracker.input_vector2_changed.connect(self._on_webxr_vector2_changed) ## Used to auto detect which "primary" input gets used first. func _on_webxr_vector2_changed(name: String, _vector: Vector2) -> void: if webxr_auto_primary == 0: if name == "thumbstick": webxr_auto_primary = WebXRPrimary.THUMBSTICK elif name == "trackpad": webxr_auto_primary = WebXRPrimary.TRACKPAD if webxr_auto_primary != 0: # Let the developer know which one is chosen. webxr_primary_changed.emit(webxr_auto_primary) ## Helper function to remap input vector with deadzone values func get_adjusted_vector2(p_controller, p_input_action): var vector = Vector2.ZERO var original_vector = p_controller.get_vector2(p_input_action) if abs(original_vector.y) > y_axis_dead_zone: vector.y = remap(abs(original_vector.y), y_axis_dead_zone, 1, 0, 1) if original_vector.y < 0: vector.y *= -1 if abs(original_vector.x) > x_axis_dead_zone: vector.x = remap(abs(original_vector.x), x_axis_dead_zone, 1, 0, 1) if original_vector.x < 0: vector.x *= -1 return vector