Created
February 22, 2025 14:12
Revisions
-
levidavidmurray created this gist
Feb 22, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,345 @@ @tool class_name ProceduralBridge extends Node3D ############################## ## EXPORT VARIABLES ############################## @export var physics_server: bool = false: set(value): physics_server = value PhysicsServer3D.set_active(value) @export var bridge_length: float = 0.0: set(value): bridge_length = value _update_supports() @export var bridge_path_length: float = 0.0: set(value): bridge_path_length = value _update_path() @export var max_path_gap: float = 0.0: set(value): max_path_gap = value _update_path() @export var support_gap: float = 1.0: set(value): support_gap = value _update_support_transforms() @export var support_y_offset: float = 0.0: set(value): support_y_offset = value _update_support_transforms() @export_category("Control Nodes") @export var length_control: Node3D @export var support_control: Marker3D @export_category("Resources") @export var catwalk_material: Material @export var path_plank_meshes: Array[ArrayMesh] @export var support_plank_meshes: Array[ArrayMesh] @export var sfx_plank_add: AudioStream @export var sfx_plank_remove: AudioStream ############################## ## VARIABLES ############################## var support_plank_length: float var path_container: Node3D var support_container: Node3D var current_path_length: float = 0.0 var last_sfx_plank_add_time: float = 0.0 var last_sfx_plank_remove_time: float = 0.0 # { "action": "add"/"remove", "plank": MeshInstance3D, "index": int } var tween_queue: Array[Dictionary] var tween_timer: float = 0.0 ############################## ## LIFECYCLE METHODS ############################## func _ready(): _ensure_containers() func _process(delta): if length_control: bridge_path_length = -length_control.position.z if support_control: support_gap = abs(support_control.position.x) * 2.0 support_y_offset = support_control.position.y _check_queue(delta) func _physics_process(delta): if Engine.get_physics_frames() % 4 == 0: _check_bridge_gaps() ############################## ## PRIVATE METHODS ############################## func _check_queue(delta: float): # Accumulate time tween_timer += delta # When enough time has passed and there is an action waiting, dequeue one and execute it var queue_size = tween_queue.size() if queue_size == 0: return var next = tween_queue.front() var timer_interval = remap(queue_size, 0, 20, 0.03, 0.008) if next.action == "add": timer_interval = 0.0 if tween_timer >= timer_interval: tween_timer = 0.0 var entry = tween_queue.pop_front() match entry.action: "add": var index = entry.index var add_delay = lerp(0.05, 0.3, index / 10.0) _add_plank_tween(entry.plank, add_delay) "remove": _remove_plank_tween(entry.plank) func _check_bridge_gaps(): _ensure_path_container() var collision_shapes: Array[CollisionShape3D] var last_shape: Shape3D = null for child in get_children(): if child is CollisionShape3D: if child.shape == last_shape: child.shape = last_shape.duplicate() collision_shapes.append(child as CollisionShape3D) last_shape = child.shape for plank in path_container.get_children(): plank = plank as MeshInstance3D if not plank.mesh: continue var plank_aabb = plank.mesh.get_aabb() var plank_visible = true for col in collision_shapes: # check if plank is within collision shape var shape = col.shape as BoxShape3D var plank_shape_aabb = col.global_transform.affine_inverse() * plank.global_transform * plank_aabb var shape_aabb = shape.get_debug_mesh().get_aabb() if shape_aabb.intersects(plank_shape_aabb): plank_visible = false break plank.visible = plank_visible func _queue_tween(action: String, plank: MeshInstance3D): var action_count = 0 for entry in tween_queue: if entry.action == action: action_count += 1 plank.set_meta("queued_action", action) tween_queue.append({"action": action, "plank": plank, "index": action_count}) func _add_plank_tween(plank: MeshInstance3D, delay: float = 0.0): plank.transparency = 0.0 plank.position.y = 0.6 await G.wait(delay) if G.get_time() - last_sfx_plank_add_time > 0.1: G.wait(0.075).connect(func(): if plank.visible: G.play_sound_at(sfx_plank_add, plank.global_position) ) last_sfx_plank_add_time = G.get_time() var tween = create_tween() tween.tween_property(plank, "position:y", 0.0, 0.15) tween.tween_property(plank, "position:y", 0.1, 0.05) tween.tween_property(plank, "position:y", 0.0, 0.05) tween.tween_property(plank, "position:y", 0.1, 0.05) tween.tween_property(plank, "position:y", 0.0, 0.05) tween.finished.connect(func(): plank.remove_meta("tween") plank.remove_meta("queued_action") var scale_tween = create_tween() scale_tween.tween_property(plank, "scale", Vector3(1.0, 0.8, 0.9), 0.05) scale_tween.tween_property(plank, "scale", Vector3.ONE, 0.1) ) plank.set_meta("tween", tween) func _remove_plank_tween(plank: MeshInstance3D): if plank.has_meta("tween") and plank.get_meta("tween") is Tween: plank.get_meta("tween").kill() var direction: int = 1 if randf() < 0.5: direction = -1 if G.get_time() - last_sfx_plank_remove_time > 0.075 and plank.visible: G.wait(0.075).connect(func(): G.play_sound_at(sfx_plank_remove, plank.global_position, -5) ) last_sfx_plank_remove_time = G.get_time() var dir_pos_x = plank.position.x dir_pos_x += randf_range(1.0, 3.0) * direction var dir_pos_y = randf_range(0.3, 3.0) var dir_pos_z = plank.position.z + randf_range(-1.0, 1.0) var dir_pos = Vector3(dir_pos_x, dir_pos_y, dir_pos_z) var force_dir = plank.global_position.direction_to(to_global(dir_pos)) var rb = RigidBody3D.new() var col = CollisionShape3D.new() var shape = BoxShape3D.new() shape.size = plank.mesh.get_aabb().size col.shape = shape add_child(rb) rb.add_child(col) rb.global_transform = plank.global_transform plank.reparent(rb) var time = G.randf_range(1.75, 2.5) rb.gravity_scale = 2.0 var force = force_dir * randf_range(5.0, 10.0) rb.continuous_cd = true rb.apply_impulse(force, Vector3(-0.75, 0, 0)) var ang = randf_range(PI * 2.0, PI * 4.0) rb.apply_torque_impulse(Vector3(0, 0, ang * direction)) var tween = create_tween() tween.tween_property(plank, "transparency", 1.0, time * 0.2).set_delay(time * 0.8) G.wait(time).connect(rb.free) func _update_path(): _ensure_path_container() if path_plank_meshes.size() == 0: return var total_length: float = 0.0 var last_plank: MeshInstance3D if path_container.get_child_count() > 0: last_plank = path_container.get_child(path_container.get_child_count() - 1) as MeshInstance3D total_length = last_plank.get_meta("length_at_plank") if bridge_path_length < current_path_length: # loop over path children, remove any that are out of bounds for plank in path_container.get_children(): plank = plank as MeshInstance3D var mesh = plank.mesh as ArrayMesh var aabb = mesh.get_aabb() var plank_length = aabb.size.x var z_pos = -plank.position.z # use plank_length as a buffer to remove planks that are out of bounds if z_pos > bridge_path_length + plank_length: # plank.free() if plank.has_meta("queued_action") and plank.get_meta("queued_action") == "add": var index = tween_queue.find_custom(func(e): return e.plank == plank) if index >= 0: tween_queue.remove_at(index) _queue_tween("remove", plank) elif not plank.has_meta("queued_action"): _queue_tween("remove", plank) current_path_length = bridge_path_length else: var i = 0 while total_length < bridge_path_length: var plank = MeshInstance3D.new() var mesh = G.pick_random(path_plank_meshes) as ArrayMesh plank.mesh = mesh plank.set_surface_override_material(0, catwalk_material) path_container.add_child(plank) var aabb = mesh.get_aabb() var plank_length = aabb.size.x var gap = G.randf_range(0.0, max_path_gap) var z_pos = total_length + gap plank.position = Vector3(0.0, 0.0, -z_pos) plank.rotation_degrees = Vector3(0, 90, 0) total_length += plank_length + gap plank.transparency = 1.0 _queue_tween("add", plank) plank.set_meta("length_at_plank", total_length) i += 1 if i > 1000: printerr("Infinite loop") break current_path_length = total_length func _update_support_transforms(): _ensure_support_container() if support_plank_length == 0: _calculate_support_length() var support_count = support_container.get_child_count() for i in range(0, support_count, 2): var left_support = support_container.get_child(i) as MeshInstance3D var right_support = support_container.get_child(i + 1) as MeshInstance3D var z_pos = left_support.position.z left_support.position = Vector3(-support_gap / 2.0, support_y_offset, z_pos) right_support.position = Vector3(support_gap / 2.0, support_y_offset, z_pos) func _update_supports(): _ensure_support_container() if support_plank_length == 0: _calculate_support_length() for child in support_container.get_children(): child.free() var support_count = int(bridge_length / support_plank_length) for i in range(support_count): var left_support = MeshInstance3D.new() left_support.mesh = support_plank_meshes[0] left_support.set_surface_override_material(0, catwalk_material) var z_pos = (i * support_plank_length) + (support_plank_length / 2.0) var right_support = left_support.duplicate() support_container.add_child(left_support) support_container.add_child(right_support) var half_gap = support_gap / 2.0 left_support.position = Vector3(-half_gap, support_y_offset, -z_pos) right_support.position = Vector3(half_gap, support_y_offset, -z_pos) func _calculate_support_length(): if support_plank_meshes.size() == 0: return var mesh = support_plank_meshes[0] var aabb = mesh.get_aabb() print(aabb.size) support_plank_length = aabb.size.z func _ensure_containers(): _ensure_path_container() _ensure_support_container() func _ensure_path_container(): if path_container: return if has_node("PathContainer"): path_container = get_node("PathContainer") else: path_container = Node3D.new() path_container.name = "PathContainer" add_child(path_container) func _ensure_support_container(): if support_container: return if has_node("SupportContainer"): support_container = get_node("SupportContainer") else: support_container = Node3D.new() support_container.name = "SupportContainer" add_child(support_container)