Skip to content

Instantly share code, notes, and snippets.

@etodd
Last active August 13, 2026 17:25
Show Gist options
  • Select an option

  • Save etodd/3fbc8224005b9e6dd8b8a2aed3efdf01 to your computer and use it in GitHub Desktop.

Select an option

Save etodd/3fbc8224005b9e6dd8b8a2aed3efdf01 to your computer and use it in GitHub Desktop.
My GDScript utility functions
class_name Util
extends Node
## Call this function every frame to move the given value toward the target in
## a smooth, springy, FRAMERATE INDEPENDENT way, such that it will take
## "duration" to reach within 1% of the target.
##
## Credit: https://x.com/FreyaHolmer/status/1757836988495847568
static func smooth_toward(
current: float,
target: float,
duration: float,
delta: float,
) -> float:
return lerpf(current, target, 1.0 - pow(0.01, delta / duration))
## Call this function every frame to move the given Vector3 toward the target
## in a smooth, springy, FRAMERATE INDEPENDENT way, such that it will take
## "duration" to reach within 1% of the target.
static func smooth_vector3_toward(
current: Vector3,
target: Vector3,
duration: float,
delta: float,
) -> Vector3:
var d: float = 1.0 - pow(0.01, delta / duration)
return Vector3(
lerpf(current.x, target.x, d),
lerpf(current.y, target.y, d),
lerpf(current.z, target.z, d),
)
## Call this function every frame to move the given Basis toward the target in
## a smooth, springy, FRAMERATE INDEPENDENT way, such that it will take
## "duration" to reach within 1% of the target.
static func smooth_basis_toward(
current: Basis,
target: Basis,
duration: float,
delta: float,
) -> Basis:
var current_rot: Quaternion = current.get_rotation_quaternion()
var target_rot: Quaternion = target.get_rotation_quaternion()
return Basis(current_rot.slerp(target_rot, 1.0 - pow(0.01, delta / duration)))
## Call this function every frame to move the given Transform3D toward the
## target in a smooth, springy, FRAMERATE INDEPENDENT way, such that it will
## take "duration" to reach within 1% of the target.
static func smooth_transform3d_toward(
current: Transform3D,
target: Transform3D,
duration: float,
delta: float,
) -> Transform3D:
return Transform3D(
smooth_basis_toward(current.basis, target.basis, duration, delta),
smooth_vector3_toward(current.origin, target.origin, duration, delta),
)
## Show or hide the given CanvasItem with a nice fade. The animation can be
## interrupted by another show_with_fade call and it will do the right thing.
static func show_with_fade(
control: CanvasItem,
show: bool = true,
duration: float = 0.25,
) -> void:
var existing: Variant = control.get_meta("show_with_fade_tween", false)
var existing_tween: Tween = existing as Tween if existing else null
var tweening: bool = existing_tween and existing_tween.is_running()
if tweening:
var tweening_target: bool = existing_tween.get_meta("target", false)
if tweening_target == show:
# already tweening toward desired state
return
existing_tween.kill()
if (not tweening) and (control.visible && control.modulate.a == 1.0) == show:
# already desired state
return
control.visible = true
var tween: Tween = control.create_tween()
control.set_meta("show_with_fade_tween", tween)
tween.set_meta("target", show)
if show:
control.modulate = Color.TRANSPARENT
tween.tween_property(control, "modulate", Color.WHITE, duration)
else:
tween.tween_property(control, "modulate", Color.TRANSPARENT, duration)
tween.tween_callback(control.hide)
await tween.finished
## Await this function to load a resource asynchronously.
static func load_async(
requester: Node,
path: String,
type_hint: String = "",
use_sub_threads: bool = false,
cache_mode: ResourceLoader.CacheMode = ResourceLoader.CACHE_MODE_REUSE
) -> Resource:
ResourceLoader.load_threaded_request(path, type_hint, use_sub_threads, cache_mode)
while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await requester.get_tree().process_frame
return ResourceLoader.load_threaded_get(path)
## Await this function to wait until the given function returns true.
static func wait(node: Node, f: Callable) -> void:
var tree: SceneTree = node.get_tree()
while not f.call():
await tree.process_frame
## Clamp a value in the range [-1, 1] to 0 if it is within the given dead zone.
static func dead_zone(x: float, zone: float) -> float:
if x < -zone:
return (x + zone) / (1.0 - zone)
elif x > zone:
return (x - zone) / (1.0 - zone)
else:
return 0.0
## Return the angle x plus or minus some multiple of PI*2 so that it is the
## closest possible to angle y.
static func closest_angle(x: float, y: float) -> float:
var result: float = x
while result > y + PI:
result -= PI * 2.0
while result < y - PI:
result += PI * 2.0
return result
## Await this function to wait for a certain dialogue title to be passed.
static func passed_title(title: String) -> void:
var t: String = ""
while t != title:
t = await DialogueManager.passed_title
## Returns true if the mouse event is should be passed into the given
## SubViewport via push_input(). The MeshInstance3D must be a QuadMesh.
static func pass_mouse_event_to_viewport(
event: InputEvent,
camera: Camera3D,
viewport: SubViewport,
quad_mesh: MeshInstance3D,
last_viewport_mouse_position: Vector2,
) -> bool:
if event is not InputEventMouse:
return false
# convert to mesh space
var quadSize: Vector2 = (quad_mesh.mesh as QuadMesh).size
var meshTransform: Transform3D = Transform3D(quad_mesh.global_basis, quad_mesh.to_global(Vector3(quadSize.x * -0.5, quadSize.y * 0.5, 0.0)))
var rayOrigin: Vector3 = camera.project_ray_origin(event.position)
var rayNormal: Vector3 = camera.project_ray_normal(event.position)
var intersection: Vector3 = ray_plane_intersection(rayOrigin, rayNormal, meshTransform.origin, meshTransform.basis.z) - meshTransform.origin
var reprojected: Vector2 = Vector2(intersection.dot(meshTransform.basis.x), -intersection.dot(meshTransform.basis.y))
var pos: Vector2 = reprojected * Vector2(viewport.size) / quadSize
if not Rect2(Vector2.ZERO, viewport.size).has_point(pos):
return false
event.position = pos
event.global_position = pos
if event is not InputEventMouseMotion:
return true
event.relative = pos - last_viewport_mouse_position
return true
## Calculates the intersection between a ray and a plane.
static func ray_plane_intersection(
ray_origin: Vector3,
ray_direction: Vector3,
plane_point: Vector3,
plane_normal: Vector3,
) -> Vector3:
var dot_product: float = ray_direction.dot(plane_normal)
if dot_product == 0:
# Ray is parallel to the plane, no intersection
return Vector3()
var t: float = (plane_point.dot(plane_normal) - ray_origin.dot(plane_normal)) / dot_product
if t < 0:
# Intersection is behind the ray origin, no intersection
return Vector3()
return ray_origin + ray_direction * t
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment