Godot Version
4.2.1 Mono
Question
I wanted to make a little heartrate monitor UI, but I’m unsure how to go about it. How would you make something like this with adjustable BPM?
4.2.1 Mono
I wanted to make a little heartrate monitor UI, but I’m unsure how to go about it. How would you make something like this with adjustable BPM?
If it were me, I would start messing around with some sort of sin() or cos() function, and have the amplitude and period of the wave change at a fixed rate to kinda emulate a pulse line.
As to how to actually implement it in Godot, I’m still new so take this with a grain of salt, and use a Line2D node (or 3D I guess) and create and assign/update the points with the function I mentioned above
That could be a great tutorial to extend Custom drawing in 2D — Godot Engine (stable) documentation in English
ok so, I made a very simple version of this using a line2d that gets the point across. If you want to make it better I would check ECG stuff out and maybe make this a shader instead of gd script plus I am somewhat new to godot so the code might not be very optimized. Though this should be a good starting point for anyone who stumbled across this thread like I did.
@tool
extends Line2D
class_name Heartbeat
@export var spacing = 1.0
@export var speed = 1.0
@export var amp = 1.0
var time = 0.0
func _physics_process(delta):
time += delta
for i in points.size():
var sin_time = time * speed + i
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2) * amp
points[i].x = i * spacing
if i == points.size() - 1:
sin_time -= 1
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2) * amp
points[i].x = (i - 0.999) * spacing
if i == 0:
sin_time += 1
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2 + 1) * amp
points[i].x = 0.999 * spacing
func set_params(new_spacing = spacing, new_speed = speed, new_amp = amp):
spacing = new_spacing
speed = new_speed
amp = new_amp
Hi! I’ve been trying to adapt your code structure to have slight randomness in the speed and amp of the line while maintaining the ability for the overall value of the line properties to be changed in a way that communicates useful information to the player. Currently this is what my code looks like, but what happens is that the line starts out all right but then eventually approaches a set value.
@tool
extends Line2D
class_name Heartbeat
@export var set_spacing := 1.0
@export var set_speed := 1.0
@export var set_amp := 1.0
var amp :float
var speed:float
var spacing:float
var time := 0.0
var amp_approach := set_amp
var spacing_approach := set_spacing
var speed_approach := set_speed
func _ready() -> void:
speed = set_speed
amp = set_amp
spacing = set_spacing
func _physics_process(delta):
time += delta
if (speed > speed_approach && speed_approach > set_speed) || (speed < speed_approach && speed_approach < set_speed):
speed_approach = realize(set_speed)
else:
speed = move_toward(speed, speed_approach, .03)
#if spacing == spacing_approach:
# spacing_approach = realize(set_spacing)
#else:
# spacing = move_toward(spacing, spacing_approach, .3)
if (amp > amp_approach && amp_approach > set_amp) || (amp < amp_approach && amp_approach < set_amp):
amp_approach = (realize(set_amp))
else:
amp = move_toward(amp,amp_approach, .05)
for i in points.size():
var sin_time = time * speed + i
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2) * amp
points[i].x = i * spacing
if i == points.size() - 1:
sin_time -= 1
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2) * amp
points[i].x = (i - 0.999) * spacing
if i == 0:
sin_time += 1
points[i].y = sin(sin_time) * amp / 2 + cos(sin_time / 2 + 1) * amp
points[i].x = 0.999 * spacing
func set_params(new_spacing = spacing, new_speed = speed, new_amp = amp):
set_spacing = new_spacing
set_speed = new_speed
set_amp = new_amp
func realize(value : float) -> float:
return randfn(value, value/10)`
If anyone knows how to prevent this from happening I’d be super appreciative. I’ve never been much of a graph guy, so while I use cos and sin all the time at school I’m not as familiar with their graphed properties as a should be. I think the issue is that the if conditions are often triggered in unintended circumstances.
So im aware this isnt a solution to your code, Im not too clear on whats happening to be honest (not much of a graph guy either lol). So I came up with something else that might help.
@tool
extends Line2D
class_name Heartbeat
@export var spacing = 1.0
@export var speed = 1.0
@export var amp = 1.0
@export var change_speed = 1.0
@onready var r_amp = amp
var time = 0.0
func _physics_process(delta):
time += delta
r_amp = move_toward(r_amp, amp, change_speed * delta)
for i in points.size():
var sin_time = time * speed + i
var t_amp = r_amp + cos(sin_time / 10) * 1.2 + sin(sin_time / 25) * 2
points[i].y = sin(sin_time) * r_amp / 2 + cos(sin_time / 2) * t_amp
points[i].x = i * spacing
if i == points.size() - 1:
sin_time -= 1
points[i].y = sin(sin_time) * r_amp / 2 + cos(sin_time / 2) * r_amp
points[i].x = (i - 0.999) * spacing
if i == 0:
sin_time = time * speed + 1
points[i].y = sin(sin_time) * r_amp / 2 + cos(sin_time / 2 + 1) * r_amp
points[i].x = 0.999 * spacing
func set_params(new_spacing = spacing, new_speed = speed, new_amp = amp):
spacing = new_spacing
speed = new_speed
amp = new_amp
Two main differences from the old code
#1 is that i use move toward for amp similar to you (though just a tip, you want to use delta in the last move towards parameter times whatever speed you want) this way you can change amp in gameplay
#2 is that I added a sin and cos to the original cosines amp, this is just an example of something you could do to make it more random, though I would play around until you get something you like
You may notice there is no move toward for speed, and that is because of a fundamental flaw with the system in that its not actualy really a sin wave, and speed doesnt really represent frequency, just the time passed. If you try to move toward speed, or a frequency variable I added to test, it will go back or forward to where the wave should be, not good. Though feel free to expirement, because there is probably something im missing. (PS i wouldve probably used the built in graph stuff IF it wasnt getting a complete redesign soon)