From 9c31c4ad694482995e4700dc3162c306907695e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 07:49:20 +0000 Subject: [PATCH] Fix disk I/O performance issues causing frame hitches - Cache stamp sounds in stamp_system.gd and StampBarController.gd to avoid loading audio from disk on every stamp action - Cache missile textures in PotatoFactory.gd to avoid reloading on every missile creation during border runner gameplay - Reuse Timer in juicy_buttons.gd hover handler instead of creating/destroying on every mouse enter/exit - Use threaded scene loading in ShiftSummaryScreen.gd for smoother viewport transitions --- .../scripts/autoload/juicy_buttons.gd | 31 ++++++---- .../scripts/systems/PotatoFactory.gd | 61 +++++++++++-------- .../scripts/systems/ShiftSummaryScreen.gd | 23 ++++++- .../systems/stamp/StampBarController.gd | 34 +++++++---- .../scripts/systems/stamp/stamp_system.gd | 28 ++++++--- 5 files changed, 115 insertions(+), 62 deletions(-) diff --git a/godot_project/scripts/autoload/juicy_buttons.gd b/godot_project/scripts/autoload/juicy_buttons.gd index 512628f03..0b1481018 100644 --- a/godot_project/scripts/autoload/juicy_buttons.gd +++ b/godot_project/scripts/autoload/juicy_buttons.gd @@ -193,30 +193,37 @@ func _on_button_mouse_entered(button: Control, hover_defaults: Dictionary) -> vo sound_player.finished.connect(_free_audio_player.bind(sound_player)) sound_player.play() - # Start a process function on the button if it doesn't exist - if not button.has_node("FloatController"): - var timer = Timer.new() + # Reuse existing timer if present, otherwise create a new one + var timer = button.get_node_or_null("FloatController") + if timer: + # Timer exists - just restart it + timer.start() + else: + # Create new timer + timer = Timer.new() timer.set_name("FloatController") timer.wait_time = 0.016 # ~60fps timer.autostart = true button.add_child(timer) - - # Initialize spring physics variables - button.set_meta("velocity", 0.0) - button.set_meta( - "target_y", button.get_meta("original_position").y - hover_defaults.float_height - ) - button.set_meta("time_counter", 0.0) - # Connect timer to the float animation handler timer.timeout.connect(_on_float_timer_timeout.bind(button, hover_defaults)) + # Initialize/reset spring physics variables + button.set_meta("velocity", 0.0) + button.set_meta( + "target_y", button.get_meta("original_position").y - hover_defaults.float_height + ) + button.set_meta("time_counter", 0.0) + ## Handle the floating animation update on each timer tick func _on_float_timer_timeout(button: Control, hover_defaults: Dictionary) -> void: # Only animate if still hovering if not button.get_meta("is_hovering"): - button.get_node("FloatController").queue_free() + # Stop the timer instead of destroying it - it will be reused on next hover + var timer = button.get_node_or_null("FloatController") + if timer: + timer.stop() return var original_pos = button.get_meta("original_position") diff --git a/godot_project/scripts/systems/PotatoFactory.gd b/godot_project/scripts/systems/PotatoFactory.gd index 89bc5f029..eb9a01805 100644 --- a/godot_project/scripts/systems/PotatoFactory.gd +++ b/godot_project/scripts/systems/PotatoFactory.gd @@ -12,6 +12,9 @@ static var _gib_textures_loaded: bool = false # Performance: Cache explosion frames static var _explosion_frames: Array[Texture2D] = [] static var _explosion_frames_loaded: bool = false +# Performance: Cache missile frames to avoid disk I/O on every missile creation +static var _missile_frames: Array[Texture2D] = [] +static var _missile_frames_loaded: bool = false # Static function to create a new potato with random attributes @@ -313,39 +316,45 @@ static func create_pixel_explosion(position: Vector2, parent: Node, scale_multip return null -# Function to create missile sprites +# Performance: Load and cache missile frames once +static func _load_missile_frames() -> void: + if not _missile_frames_loaded: + for i in range(1, 3): # 2 frames + var texture_path = "res://assets/missiles/rocket_frame_%d.png" % i + var texture = load(texture_path) + if texture: + _missile_frames.append(texture) + else: + print("Failed to load missile frame: ", texture_path) + _missile_frames_loaded = true + + +# Function to create missile sprites (uses cached textures to avoid disk I/O) static func create_missile_sprite() -> AnimatedSprite2D: + # Ensure missile frames are cached + _load_missile_frames() + + if _missile_frames.is_empty(): + print("Failed to load missile frames") + return null + # Create sprite frames resource var sprite_frames = SpriteFrames.new() - # Load missile textures - var missile_frames = [] - for i in range(1, 3): # 2 frames - var texture_path = "res://assets/missiles/rocket_frame_%d.png" % i - var texture = load(texture_path) - if texture: - missile_frames.append(texture) - else: - print("Failed to load missile frame: ", texture_path) - - # Add animation - if missile_frames.size() > 0: - sprite_frames.add_animation("default") - for frame in missile_frames: - sprite_frames.add_frame("default", frame) + # Add animation using cached frames + sprite_frames.add_animation("default") + for frame in _missile_frames: + sprite_frames.add_frame("default", frame) - # Set FPS - sprite_frames.set_animation_speed("default", 10) # 10 FPS + # Set FPS + sprite_frames.set_animation_speed("default", 10) # 10 FPS - # Create animated sprite - var missile_sprite = AnimatedSprite2D.new() - missile_sprite.sprite_frames = sprite_frames - missile_sprite.play("default") + # Create animated sprite + var missile_sprite = AnimatedSprite2D.new() + missile_sprite.sprite_frames = sprite_frames + missile_sprite.play("default") - return missile_sprite - else: - print("Failed to load missile frames") - return null + return missile_sprite # AnimatedExplosion class for handling explosion animations diff --git a/godot_project/scripts/systems/ShiftSummaryScreen.gd b/godot_project/scripts/systems/ShiftSummaryScreen.gd index 7ba99054f..8b3197d1c 100644 --- a/godot_project/scripts/systems/ShiftSummaryScreen.gd +++ b/godot_project/scripts/systems/ShiftSummaryScreen.gd @@ -932,12 +932,15 @@ func transition_to_scene(scene_path: String): tween.tween_callback(_handle_scene_transition.bind(scene_path)) -# General utility function for transitions within viewport +# General utility function for transitions within viewport (uses threaded loading) func transition_within_viewport(scene_path: String): # Find the parent viewport container var viewport_container = find_parent_viewport_container() if viewport_container: + # Start loading the scene in the background during fade + ResourceLoader.load_threaded_request(scene_path) + # Create fade effect var fade_rect = ColorRect.new() fade_rect.color = Color(0, 0, 0, 0) @@ -950,12 +953,26 @@ func transition_within_viewport(scene_path: String): tween.tween_property(fade_rect, "color", Color(0, 0, 0, 1), 0.5) await tween.finished + # Wait for threaded load to complete + var scene_resource = null + while true: + var status = ResourceLoader.load_threaded_get_status(scene_path) + if status == ResourceLoader.THREAD_LOAD_LOADED: + scene_resource = ResourceLoader.load_threaded_get(scene_path) + break + elif status == ResourceLoader.THREAD_LOAD_FAILED: + push_error("Failed to load scene: " + scene_path) + fade_rect.queue_free() + return + # Small delay before checking again + await get_tree().create_timer(0.016).timeout + # Remove all current children from the viewport for child in viewport_container.get_children(): child.free() - # Instantiate the new scene - var new_scene = load(scene_path).instantiate() + # Instantiate the loaded scene + var new_scene = scene_resource.instantiate() viewport_container.add_child(new_scene) print("Transitioned within viewport to: " + scene_path) diff --git a/godot_project/scripts/systems/stamp/StampBarController.gd b/godot_project/scripts/systems/stamp/StampBarController.gd index cb44eef44..52a127fcc 100644 --- a/godot_project/scripts/systems/stamp/StampBarController.gd +++ b/godot_project/scripts/systems/stamp/StampBarController.gd @@ -422,21 +422,25 @@ func animate_stamp(stamp_type: String, target_position: Vector2): return_tween.tween_callback(func(): temp_stamp.queue_free()) -# Play a random stamp sound effect +# Performance: Pre-load stamp sounds into cache +func _cache_stamp_sounds() -> void: + if not _stamp_sounds_cached: + for i in range(1, 6): + var sound_path = "res://assets/audio/mechanical/stamp_sound_" + str(i) + ".mp3" + var sound = load(sound_path) + if sound: + _cached_stamp_sounds.append(sound) + _stamp_sounds_cached = true + + +# Play a random stamp sound effect (uses cached sounds to avoid disk I/O) func play_random_stamp_sound(): - var stamp_sounds = [] - - # Try to load sound effects - for i in range(1, 6): - var sound_path = "res://assets/audio/mechanical/stamp_sound_" + str(i) + ".mp3" - var sound = load(sound_path) - if sound: - stamp_sounds.append(sound) - else: - push_warning("Could not load stamp sound: " + sound_path) + # Ensure sounds are cached + if not _stamp_sounds_cached: + _cache_stamp_sounds() - if sfx_player and stamp_sounds.size() > 0: - sfx_player.stream = stamp_sounds[randi() % stamp_sounds.size()] + if sfx_player and _cached_stamp_sounds.size() > 0: + sfx_player.stream = _cached_stamp_sounds.pick_random() sfx_player.play() else: push_warning("STAMP CONTROLLER: NO AUDIO SETUP FOR STAMPS") @@ -487,6 +491,10 @@ func create_final_stamp(stamp_type: String, pos: Vector2): # Hover sound for stamp bar var hover_sound_stamp_bar = preload("res://assets/audio/ui_feedback/ui_hover_stamp_bar.mp3") +# Performance: Cache stamp sounds to avoid disk I/O on every stamp action +var _cached_stamp_sounds: Array[AudioStream] = [] +var _stamp_sounds_cached: bool = false + func _on_toggle_position_button_mouse_entered() -> void: _play_hover_sound(hover_sound_stamp_bar) diff --git a/godot_project/scripts/systems/stamp/stamp_system.gd b/godot_project/scripts/systems/stamp/stamp_system.gd index 9e62a9d67..446817fd9 100644 --- a/godot_project/scripts/systems/stamp/stamp_system.gd +++ b/godot_project/scripts/systems/stamp/stamp_system.gd @@ -35,6 +35,8 @@ var stamp_ink_sounds = [ preload("res://assets/audio/gameplay/stamp_ink_splatter.mp3"), preload("res://assets/audio/gameplay/stamp_ink_spray.mp3") ] +# Performance: Cache loaded stamp sounds to avoid disk I/O on every stamp +var _cached_stamp_sounds: Array[AudioStream] = [] # Shake effect var shake_callback: Callable @@ -48,6 +50,9 @@ func _init(audio: AudioStreamPlayer2D, shake_func: Callable): # Load stamp sounds - only stamp_sound_1.mp3 exists stamp_sounds.append("res://assets/audio/mechanical/stamp_sound_1.mp3") + # Performance: Pre-load and cache stamp sounds to avoid disk I/O during gameplay + _cache_stamp_sounds() + func _process(delta): # Update cooldown timer @@ -326,20 +331,27 @@ func _play_ink_sound(): ink_player.finished.connect(ink_player.queue_free) -# Play a random stamp sound -func play_random_stamp_sound(): - if audio_player and stamp_sounds.size() > 0: - # Actually load the audio files - var sound_files = [] +# Performance: Pre-load stamp sounds into cache +func _cache_stamp_sounds() -> void: + if _cached_stamp_sounds.is_empty(): for sound_path in stamp_sounds: var sound = load(sound_path) if sound: - sound_files.append(sound) + _cached_stamp_sounds.append(sound) else: push_warning("Could not load stamp sound: " + sound_path) - if sound_files.size() > 0: - audio_player.stream = sound_files[randi() % sound_files.size()] + +# Play a random stamp sound (uses cached sounds to avoid disk I/O) +func play_random_stamp_sound(): + if audio_player and _cached_stamp_sounds.size() > 0: + audio_player.stream = _cached_stamp_sounds.pick_random() + audio_player.play() + elif audio_player and stamp_sounds.size() > 0: + # Fallback: cache wasn't initialized, do it now + _cache_stamp_sounds() + if _cached_stamp_sounds.size() > 0: + audio_player.stream = _cached_stamp_sounds.pick_random() audio_player.play() else: push_warning("No stamp sounds could be loaded")