Last active
August 13, 2026 17:25
-
-
Save etodd/3fbc8224005b9e6dd8b8a2aed3efdf01 to your computer and use it in GitHub Desktop.
My GDScript utility functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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