A quick way would be to hold an array referencing ui nodes, iterate over it to set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
and only allow the relevant one for the current state of the tutorial.
Code for a root node attached script
@onready var groups : Array = [[$Slider1, $Button1],
[$Slider2, $Button2]]
@onready var ui_nodes : Array = groups.reduce(func (accum, group): return accum + group, [])
var active_group : int = 0
func _ready():
update_filter()
func update_filter():
# Disable all
for control_node in ui_nodes:
control_node.set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
# Re-enable relevant ones
for control_node in groups[active_group]:
control_node.set_mouse_filter(Control.MOUSE_FILTER_STOP)
# Arbitrarilly connect next tutorial step
connect_states()
func connect_states():
var slider = groups[active_group][0]
var button = groups[active_group][1]
# Next tutorial is reached if slider value == 50 or button is pressed
slider.value_changed.connect(func(new_value): if new_value == 50: next_tutorial_state())
button.pressed.connect(func(): next_tutorial_state())
func next_tutorial_state():
# Increment active_group within bounds
active_group = min(groups.size()-1, active_group+1)
update_filter()
But according to what you’re looking for, as Exerion suggests, you’d need to have a Control
node covering the entire viewport but manually punch holes in it for visuals and whether to let the input event propagate up the node tree. A good understanding of Inputs might help but that would look something like so:
Code for a 'Control' node placed last in node tree attached script
extends Control
const opaque_color := Color(.1,.1,.1,.8)
@onready var groups : Array = [[$"../Slider1", $"../Button1"],
[$"../Slider2", $"../Button2"]]
var allow := false
var active_group : int = 0
var focus_rect : Rect2
func _ready():
# Set control to full viewport size (can be done in editor instead)
set_anchors_preset(Control.PRESET_FULL_RECT)
# Propagate mouse input by default (can also be done in editor instead)
set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
# Init tutorial state
update_filter()
func _input(event): # override
# Update whether to allow or not input propagation based on focusing rectangle
if event is InputEventMouse:
allow = focus_rect.has_point(event.position)
# Tag input as already handled if disallowed
if !allow:
get_viewport().set_input_as_handled()
func update_filter():
update_focus_rect()
connect_states()
queue_redraw()
func connect_states():
var slider = groups[active_group][0]
var button = groups[active_group][1]
# Next tutorial is reached if slider value == 50 or button is pressed
slider.value_changed.connect(func(new_value): if new_value == 50: next_tutorial_state())
button.pressed.connect(func(): next_tutorial_state())
func next_tutorial_state():
# Increment active_group within bounds
active_group = min(groups.size()-1, active_group+1)
update_filter()
func update_focus_rect():
# Initialize a new focused rectangle based on the first ui node in group
focus_rect = Rect2(groups[active_group][0].position, groups[active_group][0].size)
# Grow the rectangle to fit all ui nodes within group
for ui_node in groups[active_group].slice(1):
focus_rect.position.x = min(focus_rect.position.x, ui_node.position.x)
focus_rect.position.y = min(focus_rect.position.y, ui_node.position.y)
focus_rect.end.x = max(focus_rect.size.x, ui_node.position.x + ui_node.size.x)
focus_rect.end.y = max(focus_rect.size.y, ui_node.position.y + ui_node.size.y)
# Add some margins
focus_rect.position -= Vector2.ONE * 20
focus_rect.end += Vector2.ONE * 40
func _draw():
# Draw opaque screen around focus rectangle
var up := Rect2( Vector2.ZERO, Vector2(size.x, focus_rect.position.y))
var left := Rect2( Vector2(0.0, focus_rect.position.y), Vector2(focus_rect.position.x, focus_rect.size.y))
var right := Rect2(Vector2(focus_rect.end.x, focus_rect.position.y), Vector2(size.x - focus_rect.end.x, focus_rect.size.y))
var down := Rect2( Vector2(0.0, focus_rect.end.y), Vector2(size.x, size.y - focus_rect.end.y))
for rect in [up, left, right, down]:
draw_rect(rect, opaque_color, true)
There’s a small caveat though, if you started moving a slider or pressing a button and go outside the “allowed area”, nodes will not register “released” mouse button state and will stay in a “pressed” state when going back to it.
Ultimately you can make a mix to keep best of both worlds.
The scripts can be easily tested on any scene with 2 HSlider
s and 2 Button
s as named in the scripts.