Godot Version
4.4.1
Question
I stepped away from a project for a while and just picked it back up, so I’m a little rusty, but I will do my best to explain my problem.
Overall Goal:
I am creating a digital version of the Alethiometer, a divination tool from the His Dark Materials book series. It is a compass with 36 symbols placed along the dial. Their are 4 needles total. The player is able to rotate three of the needles to point to a specific symbol in order to frame a question, then click a button to start the reading. The 4th needle will then spin to three “random” symbols to communicate the answer.
Current Problem:
I was able to implement the three player-controlled needles, as well as the Answer Needle that spins to three random symbols. However, I want the Answer Needle to slow down or pause at each symbol so the player is able to easily register the result. Ideally, I would also like the game to record and store the rotation value so that the player can reference them at a later point, but that is probably a problem for later down the line as their would be multiple steps to implement that - just mentioning it in case it changes how you would solve my current problem.
I’ve tried a couple solutions, but no dice. Most recently, I attempted to use “await” in my process function (although I know that should not be done) to pause the needle once it reaches it’s target_rotation, before running the pick_new_target() function. In practice, all this does is stop the needle at the first target_rotation, and prevent the needle from continuing to the 2nd and 3rd rotations.
Would someone be able to point me in the right direction? Unfortunately, it’s been a while since I looked at this project, so I don’t remember everything I have tried so far.
extends Node2D
var question: bool = false
var stop: bool = false
var step: int = 0
var anchors: Array[Node2D] = []
var anchor_drawables: Array[Array] = []
# Which dial is currently selected? (-1 means "none selected")
var selected_dial: int = -1
# variables for reading results
#var answers: Array = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36"]
#var results: float = $Alethiometer/Answer.position
var answer1
var answer2
var answer3
var target_rotation: float = 0.0
var angular_speed = PI
var rotation_step: float = deg_to_rad(10)
var stop_time = 0
@export var speed_deg_per_sec: float = 90.0 # tweak: 45 = slow, 90 = medium, 180 = fast
var elapsed: float = 0.0
var rng := RandomNumberGenerator.new()
# Highlight style: we use "modulate" and "scale" (no extra art needed).
# NOTE: If a texture is very strongly colored (e.g., pure red needle),
# modulate may be subtle; scale still makes selection obvious.
@export var normal_modulate: Color = Color(1, 1, 1, 1) # slightly faded
@export var selected_modulate: Color = Color(0.8, 0.8, 0.8, 1) # brighter + warm
@export var normal_scale: Vector2 = Vector2.ONE
@export var selected_scale: Vector2 = Vector2(1.04, 1.04) # 4% bigger (noticeable but not huge)
# Optional: make the selected dial gently "pulse" so it's easier to spot.
@export var pulse_enabled: bool = true
@export var pulse_speed: float = 6.0 # higher = faster pulsing
@export var pulse_amount: float = 0.02 # 0.02 = +/-2% size change
@onready var timer: Timer = $Timer
@onready var alethiometer: Node = $Alethiometer
func _ready() -> void:
# Find Anchor1..AnchorN automatically, so adding more dials requires no code changes.
var i := 1
while true:
var anchor_name := "Anchor%d" % i
if not alethiometer.has_node(anchor_name):
break
var anchor := alethiometer.get_node(anchor_name) as Node2D
anchors.append(anchor)
# Cache all visible/drawable nodes (Sprite2D, TextureRect, Label, etc.) under this anchor.
anchor_drawables.append(_find_canvas_items(anchor))
i += 1
# Make sure the initial visuals match "nothing selected".
_apply_highlight() #is this redundant?
func _on_timer_timeout():
print("timeout")
_pick_new_target()
func _input(event: InputEvent) -> void:
# We use _input() instead of checking input every frame in _process().
# _input() is event-driven: it runs only when input happens.
print(selected_dial)
if selected_dial == -1:
return
# In _input(), we receive RAW input events (mouse, keys, wheel, etc.)
# The simplest reliable way to use InputMap actions here is:
# event.is_action_pressed("action_name")
#
# Using "pressed" works well for mouse wheel bindings too (wheel often has no "release" feel).
if event.is_action_pressed("clockwise"):
anchors[selected_dial].rotation += rotation_step
get_viewport().set_input_as_handled() # optional: prevents other nodes from reacting too
if event.is_action_pressed("counterclock"):
anchors[selected_dial].rotation += rotation_step
get_viewport().set_input_as_handled() # optional
func start_answer_spin() -> void:
print("start answer spin")
# Call this when you want to start the 3-step motion
question = true
stop = false
step = 0
target_rotation = $Alethiometer/Answer.rotation
print("starting rotation?: ", target_rotation)
#_pick_new_target()
_on_timer_timeout()
func _pick_new_target() -> void:
timer.start()
print("pick new target")
# Direction pattern: +, -, + (based on step 0,1,2)
var direction:= 1 if step % 2 == 0 else -1
print("direction: ", direction)
# Choose how far to rotate this step (not absolute 0..360!)
# Picking a delta makes "direction" meaningful and predictable.
var delta_deg:= rng.randi_range(10, 360) # tweak range as you like
var delta_rad:= deg_to_rad(delta_deg) * direction
# Target is current rotation + signed delta
target_rotation = $Alethiometer/Answer.rotation + delta_rad
func _process(delta):
#logic for stopping answer needle##
if question == true:
var speed:= deg_to_rad(speed_deg_per_sec)
# Move current rotation toward the target at a constant angular speed
$Alethiometer/Answer.rotation = move_toward($Alethiometer/Answer.rotation, target_rotation, speed * delta)
# Slow down as needle approaches target
#$Alethiometer/Answer.rotation = ease(target_rotation, 0.9)
# If we've basically reached the target, go to next step
if is_equal_approx($Alethiometer/Answer.rotation, target_rotation):
#if $Alethiometer/Answer.rotation == target_rotation:
print("target reached")
step += 1
if step == 1:
answer1 = target_rotation
print("answer: ", answer1)
print("step: ", step)
_on_timer_timeout()
##await get_tree().create_timer(1).timeout
#await is messing up original conditional statement (if rotation == target rotation), so the if statement keeps running through to the next "step" before await can complete and pick_new_target() can run? Can we solve this by running this outside process?
#_pick_new_target()
if step == 2:
answer2 = target_rotation
print("answer: ", answer2)
print("step: ", step)
_on_timer_timeout()
##await get_tree().create_timer(1).timeout
#_pick_new_target()
if step == 3:
answer3 = target_rotation
print("answer: ", answer3)
print("step: ", step)
_on_timer_timeout()
##await get_tree().create_timer(1).timeout
#_pick_new_target()
if step >= 3:
question = false
stop = true
print("step: ", step, "stopping at ", rad_to_deg($Alethiometer/Answer.rotation))
#else:
#_pick_new_target()
# Optional pulse animation for the selected dial.
# This runs every frame because animation needs continuous updates.
if not pulse_enabled:
return
if selected_dial == -1:
return
# Start from the selected base scale, then apply a tiny sine-wave pulse.
var t := Time.get_ticks_msec() / 1000.0
var pulse := 1.0 + sin(t * pulse_speed) * pulse_amount
anchors[selected_dial].scale = selected_scale * pulse
func _on_dial_pressed(index: int) -> void:
# Called by any dial button:
# Dial1 -> index 0, Dial2 -> index 1, etc.
selected_dial = index
_apply_highlight()
print(anchors)
print("index: ", index)
print("dial pressed")
print("selected dial from pressed signal: ", selected_dial)
func _apply_highlight() -> void:
# Update ALL dials so the selected one stands out.
print("apply highlight to ", selected_dial)
for i in range(anchors.size()):
var is_selected := (i == selected_dial)
# Base scale (pulse may animate on top of this later)
anchors[i].scale = selected_scale if is_selected else normal_scale
# Tint all drawable children (sprites, texture rects, labels, etc.)
var tint :Color = selected_modulate if is_selected else normal_modulate
for item in anchor_drawables[i]:
(item as CanvasItem).modulate = tint
func _find_canvas_items(root: Node) -> Array:
# Returns all CanvasItem descendants under root.
# CanvasItem = anything that can be drawn (Sprite2D, TextureRect, Label, etc.)
var result: Array = []
_collect_canvas_items_recursive(root, result)
return result
func _collect_canvas_items_recursive(n: Node, out: Array) -> void:
# Recursively walk the tree and gather CanvasItems.
for child in n.get_children():
if child is CanvasItem:
out.append(child)
_collect_canvas_items_recursive(child, out)
func _on_start_button_pressed():
#set_dials_to_false()
selected_dial = -1
_apply_highlight()
start_answer_spin()
question = true
func _on_result_button_pressed():
print("Answer: ", answer1, " ", answer2, " ", answer3)