Heartrate monitor UI

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?
image

1 Like

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

1 Like

Data with ECG curves can be found here: ECG dataset | Kaggle

1 Like

That could be a great tutorial to extend Custom drawing in 2D — Godot Engine (stable) documentation in English

1 Like

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)