Skip to content

Instantly share code, notes, and snippets.

@levidavidmurray
Created February 22, 2025 14:12
Show Gist options
  • Save levidavidmurray/dd1b5e59c0aafdde9db6f1ec5caf250d to your computer and use it in GitHub Desktop.
Save levidavidmurray/dd1b5e59c0aafdde9db6f1ec5caf250d to your computer and use it in GitHub Desktop.
@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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment