Created
February 22, 2025 14:12
-
-
Save levidavidmurray/dd1b5e59c0aafdde9db6f1ec5caf250d to your computer and use it in GitHub Desktop.
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
@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