From b4c4a66ce817884bd284abf19a621a52dcbf5189 Mon Sep 17 00:00:00 2001 From: henry Date: Wed, 24 Jun 2026 14:48:02 +0100 Subject: [PATCH] Added effects manager and two effects to test attached to the player scene. --- resources/effects/flash_weapon_change.tres | 8 ++ resources/effects/invincibility_blink.tres | 13 +++ scenes/EffectComponent.tscn | 4 +- scenes/effects/flash_effect.gd | 27 ----- scenes/player.gd | 5 + scenes/player.tscn | 10 ++ scenes/world.tscn | 5 + scripts/{ => effects}/base_effect.gd | 12 +- scripts/{ => effects}/base_effect.gd.uid | 0 scripts/effects/blinker_effect.gd | 90 +++++++++++++++ scripts/effects/blinker_effect.gd.uid | 1 + scripts/{ => effects}/effects_component.gd | 25 +++-- .../{ => effects}/effects_component.gd.uid | 0 scripts/effects/flash_effect.gd | 105 ++++++++++++++++++ .../effects/flash_effect.gd.uid | 0 scripts/movement_component.gd | 2 + scripts/weapon_component.gd | 10 ++ shaders/flash.gdshader | 44 ++++++++ shaders/flash.gdshader.uid | 1 + 19 files changed, 322 insertions(+), 40 deletions(-) create mode 100644 resources/effects/flash_weapon_change.tres create mode 100644 resources/effects/invincibility_blink.tres delete mode 100644 scenes/effects/flash_effect.gd rename scripts/{ => effects}/base_effect.gd (53%) rename scripts/{ => effects}/base_effect.gd.uid (100%) create mode 100644 scripts/effects/blinker_effect.gd create mode 100644 scripts/effects/blinker_effect.gd.uid rename scripts/{ => effects}/effects_component.gd (51%) rename scripts/{ => effects}/effects_component.gd.uid (100%) create mode 100644 scripts/effects/flash_effect.gd rename {scenes => scripts}/effects/flash_effect.gd.uid (100%) create mode 100644 shaders/flash.gdshader create mode 100644 shaders/flash.gdshader.uid diff --git a/resources/effects/flash_weapon_change.tres b/resources/effects/flash_weapon_change.tres new file mode 100644 index 0000000..d8bd944 --- /dev/null +++ b/resources/effects/flash_weapon_change.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="FlashEffect" format=3 uid="uid://c86rdr6gtto66"] + +[ext_resource type="Shader" uid="uid://dp4svqyxg1dum" path="res://shaders/flash.gdshader" id="1_37i75"] +[ext_resource type="Script" uid="uid://deak7vcr7ss5l" path="res://scripts/effects/flash_effect.gd" id="2_y51sa"] + +[resource] +script = ExtResource("2_y51sa") +metadata/_custom_type_script = "uid://deak7vcr7ss5l" diff --git a/resources/effects/invincibility_blink.tres b/resources/effects/invincibility_blink.tres new file mode 100644 index 0000000..40fac80 --- /dev/null +++ b/resources/effects/invincibility_blink.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="BlinkerEffect" format=3 uid="uid://bld2k7san214b"] + +[ext_resource type="Script" uid="uid://cj4grtaun2h5a" path="res://scripts/effects/blinker_effect.gd" id="1_blink"] + +[resource] +script = ExtResource("1_blink") +blink_frequency = 12.0 +blink_length = 0.2 +min_alpha = 0.1 +start_ease = 0.1 +start_ease_mode = 0 +end_ease_mode = 0 +duration = 0.75 diff --git a/scenes/EffectComponent.tscn b/scenes/EffectComponent.tscn index bfd716f..5152fdf 100644 --- a/scenes/EffectComponent.tscn +++ b/scenes/EffectComponent.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://bq3xhbfgkt22l"] -[ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects_component.gd" id="1_ee0kt"] +[ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects/effects_component.gd" id="1_ee0kt"] -[node name="EffectsComponent" type="Node2D" unique_id=1393438576] +[node name="EffectsComponent" type="Node" unique_id=1393438576] script = ExtResource("1_ee0kt") diff --git a/scenes/effects/flash_effect.gd b/scenes/effects/flash_effect.gd deleted file mode 100644 index db8fb26..0000000 --- a/scenes/effects/flash_effect.gd +++ /dev/null @@ -1,27 +0,0 @@ -class_name FlashEffect -extends BaseEffect - -@export var flash_color: Color = Color.WHITE -@export var flash_speed: float = 10.0 - -var _timer: float = 0.0 -var _original_material: Material - -func start(target: Node) -> void: - _timer = 0.0 - if target is CanvasItem: - _original_material = target.material - # Apply a ShaderMaterial that handles the flash - var flash_material = ShaderMaterial.new() - flash_material.shader = preload("res://shaders/flash.gdshader") - target.material = flash_material - -func process(delta: float, target: Node) -> void: - _timer += delta - if target.material is ShaderMaterial: - var intensity = (sin(_timer * flash_speed) + 1.0) / 2.0 - target.material.set_shader_parameter("flash_intensity", intensity) - -func stop(target: Node) -> void: - if target is CanvasItem: - target.material = _original_material diff --git a/scenes/player.gd b/scenes/player.gd index f61a309..ace3c7a 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -19,6 +19,10 @@ var previous_position: Vector2 var ship_displacement: float var travel: float = 0 +# Effects Testing +var flash_effect = preload("res://resources/effects/flash_weapon_change.tres") +var blink_effect = preload("res://resources/effects/invincibility_blink.tres") + func _ready() -> void: ## Set initial start position @@ -50,3 +54,4 @@ func _process(delta) -> void: # Cycle weapon (testing key) if input_component.cycle_weapon: weapon_component.cycle_weapon() + $EffectsComponent.apply_effect(flash_effect) diff --git a/scenes/player.tscn b/scenes/player.tscn index b78146a..a323687 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -12,6 +12,7 @@ [ext_resource type="Resource" uid="uid://bhc6aja38vyr" path="res://resources/player_weapon_resources/vanguard.tres" id="9_ur7pv"] [ext_resource type="Script" uid="uid://7n1itonn35fm" path="res://scripts/weapon_shot.gd" id="10_d2wvv"] [ext_resource type="Resource" uid="uid://cck3gmnhu5agc" path="res://resources/player_weapon_resources/tri.tres" id="11_3v2ag"] +[ext_resource type="Script" uid="uid://65d3hbrpm21x" path="res://scripts/effects/effects_component.gd" id="12_effcomp"] [sub_resource type="AtlasTexture" id="AtlasTexture_tuyoq"] atlas = ExtResource("5_qlg0r") @@ -92,6 +93,8 @@ size = Vector2(24, 30) [sub_resource type="RectangleShape2D" id="RectangleShape2D_dqkch"] size = Vector2(6, 5.75) +[sub_resource type="Resource" id="Resource_jej6c"] + [node name="Player" type="Area2D" unique_id=652131079] script = ExtResource("1_ur7pv") @@ -155,6 +158,13 @@ unique_name_in_owner = true script = ExtResource("7_d2wvv") weapon_data = ExtResource("8_stock") available_weapons = Array[ExtResource("10_d2wvv")]([ExtResource("8_stock"), ExtResource("11_3v2ag"), ExtResource("9_ur7pv")]) +weapon_change_flash = SubResource("Resource_jej6c") metadata/_custom_type_script = "uid://ylmao2ndp22y" +[node name="EffectsComponent" type="Node" parent="." unique_id=1393438576 node_paths=PackedStringArray("target_node")] +unique_name_in_owner = true +script = ExtResource("12_effcomp") +target_node = NodePath("../Ship") +metadata/_custom_type_script = "uid://65d3hbrpm21x" + [connection signal="timeout" from="WeaponCooldown" to="." method="_on_weapon_cooldown_timeout"] diff --git a/scenes/world.tscn b/scenes/world.tscn index 616c55d..36f4ea1 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -14,6 +14,11 @@ script = ExtResource("2_k0juu") notification_scene = ExtResource("3_71j4m") +[node name="FPS" type="Label" parent="UI" unique_id=1606313100] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 23.0 + [node name="Level" type="Node2D" parent="." unique_id=934289771] [node name="Background" type="Node2D" parent="Level" unique_id=485120278] diff --git a/scripts/base_effect.gd b/scripts/effects/base_effect.gd similarity index 53% rename from scripts/base_effect.gd rename to scripts/effects/base_effect.gd index e871e76..60aaad1 100644 --- a/scripts/base_effect.gd +++ b/scripts/effects/base_effect.gd @@ -3,14 +3,20 @@ extends Resource @export var duration: float = 0.5 +var _elapsed: float = 0.0 + # Called when the effect is applied func start(target: Node) -> void: - pass + _elapsed = 0.0 # Called every frame (if needed) -func process(delta: float) -> void: - pass +func process(delta: float, target: Node) -> void: + _elapsed += delta # Clean up any changes to the target func stop(target: Node) -> void: pass + +# Returns true when the effect has run its course +func is_finished() -> bool: + return _elapsed >= duration diff --git a/scripts/base_effect.gd.uid b/scripts/effects/base_effect.gd.uid similarity index 100% rename from scripts/base_effect.gd.uid rename to scripts/effects/base_effect.gd.uid diff --git a/scripts/effects/blinker_effect.gd b/scripts/effects/blinker_effect.gd new file mode 100644 index 0000000..9684619 --- /dev/null +++ b/scripts/effects/blinker_effect.gd @@ -0,0 +1,90 @@ +class_name BlinkerEffect +extends BaseEffect + +## How many blink cycles per second. +@export_range(0.0, 60.0) var blink_frequency: float = 8.0 + +## Fraction of each cycle the sprite is visible (0.0–1.0). +@export_range(0.0, 1.0) var blink_length: float = 0.5 + +## Minimum alpha when "off" (0.0 = fully invisible, 1.0 = fully visible). +@export_range(0.0, 1.0) var min_alpha: float = 0.0 + +## Maximum alpha when "on" (0.0–1.0). +@export_range(0.0, 1.0) var max_alpha: float = 1.0 + +## Duration of the fade-in at the start of the effect (seconds). +@export_range(0.0, 5.0) var start_ease: float = 0.0 + +## Easing curve for the fade-in. +@export_enum("Linear", "Smooth", "Ease-In", "Ease-Out", "Ease-In-Out") var start_ease_mode: int = 1 + +## Duration of the fade-out at the end of the effect (seconds). +@export_range(0.0, 5.0) var end_ease: float = 0.0 + +## Easing curve for the fade-out. +@export_enum("Linear", "Smooth", "Ease-In", "Ease-Out", "Ease-In-Out") var end_ease_mode: int = 1 + +var _original_modulate: Color + + +func start(target: Node) -> void: + super.start(target) + if target is CanvasItem: + _original_modulate = target.modulate + + +func process(delta: float, target: Node) -> void: + super.process(delta, target) + if target is CanvasItem: + var alpha: float = _compute_alpha(_elapsed) + target.modulate = Color(_original_modulate.r, _original_modulate.g, _original_modulate.b, alpha) + + +func stop(target: Node) -> void: + if target is CanvasItem: + target.modulate = _original_modulate + + +## Compute the combined eased alpha for a given elapsed time. +func _compute_alpha(elapsed: float) -> float: + if duration <= 0.0: + return 1.0 + + var t: float = min(elapsed, duration) + + # Blink pulse: on/off cycling at the given frequency and duty cycle + var period := 1.0 / blink_frequency if blink_frequency > 0.0 else 1.0 + var phase := fmod(t, period) / period + + var pulse: float = min_alpha + if phase < blink_length: + var pulse_progress = phase / blink_length if blink_length > 0.0 else 1.0 + if pulse_progress < 0.5: + pulse = min_alpha + (max_alpha - min_alpha) * _ease_value(pulse_progress * 2.0, 2) + else: + pulse = min_alpha + (max_alpha - min_alpha) * _ease_value((1.0 - pulse_progress) * 2.0, 3) + + # Start ease: fade from fully visible into the blink pattern + if t < start_ease and start_ease > 0.0: + var fade_in = _ease_value(t / start_ease, start_ease_mode) + return lerp(max_alpha, pulse, fade_in) + + # End ease: fade from blink pattern back to fully visible + if t > duration - end_ease and end_ease > 0.0: + var fade_progress = (t - (duration - end_ease)) / end_ease + var fade_out = _ease_value(fade_progress, end_ease_mode) + return lerp(pulse, max_alpha, fade_out) + + return pulse + + +## Apply easing to a 0.0–1.0 normalized value based on the selected mode. +func _ease_value(value: float, mode: int) -> float: + match mode: + 0: return value # Linear + 1: return smoothstep(0.0, 1.0, value) # Smooth + 2: return ease(value, -2.0) # Ease-In (accelerating) + 3: return ease(value, 2.0) # Ease-Out (decelerating) + 4: return ease(value, 3.0) # Ease-In-Out (S-curve) + return value diff --git a/scripts/effects/blinker_effect.gd.uid b/scripts/effects/blinker_effect.gd.uid new file mode 100644 index 0000000..c87f585 --- /dev/null +++ b/scripts/effects/blinker_effect.gd.uid @@ -0,0 +1 @@ +uid://cj4grtaun2h5a diff --git a/scripts/effects_component.gd b/scripts/effects/effects_component.gd similarity index 51% rename from scripts/effects_component.gd rename to scripts/effects/effects_component.gd index c5e5308..361641f 100644 --- a/scripts/effects_component.gd +++ b/scripts/effects/effects_component.gd @@ -1,24 +1,33 @@ class_name EffectComponent -extends Node2D +extends Node -@export var target_node: Node +## The node this component applies effects to. +## If left null, defaults to the first parent CanvasItem. +@export var target_node: Node = null var active_effects: Array[BaseEffect] = [] +func _ready() -> void: + if target_node == null: + # Auto-resolve to the parent if nothing is set + target_node = get_parent() + func _process(delta: float) -> void: + if target_node == null: + return + # Process all active effects backwards so we can safely remove finished ones for i in range(active_effects.size() - 1, -1, -1): var effect = active_effects[i] effect.process(delta, target_node) - - # Example: simple time-based expiration - effect.duration -= delta - if effect.duration <= 0.0: + + if effect.is_finished(): remove_effect(effect) func apply_effect(effect: BaseEffect) -> void: - effect.start(target_node) - active_effects.append(effect) + var instance = effect.duplicate() + instance.start(target_node) + active_effects.append(instance) func remove_effect(effect: BaseEffect) -> void: effect.stop(target_node) diff --git a/scripts/effects_component.gd.uid b/scripts/effects/effects_component.gd.uid similarity index 100% rename from scripts/effects_component.gd.uid rename to scripts/effects/effects_component.gd.uid diff --git a/scripts/effects/flash_effect.gd b/scripts/effects/flash_effect.gd new file mode 100644 index 0000000..d681dd7 --- /dev/null +++ b/scripts/effects/flash_effect.gd @@ -0,0 +1,105 @@ +class_name FlashEffect +extends BaseEffect + +## Shader applied to the target sprite during the flash. +@export var flash_shader: Shader = preload("res://shaders/flash.gdshader") + +## Color of the flash overlay. +@export var flash_color: Color = Color.WHITE + +## How many flash cycles per second. +@export_range(0.0, 60.0) var flash_frequency: float = 10.0 + +## Fraction of each cycle the flash is visible (0.0–1.0, i.e. duty cycle). +@export_range(0.0, 1.0) var flash_length: float = 0.5 + +## How the flash blends with the original sprite. +@export_enum("Mix", "Additive", "Screen") var blend_mode: int = 0 + +## Additional brightness added to the flash color (0.0 = none, 1.0 = double). +@export_range(0.0, 1.0) var brightness_boost: float = 0.0 + +## Duration of the fade-in at the start of the effect (seconds). +@export_range(0.0, 5.0) var start_ease: float = 0.0 + +## Easing curve for the fade-in. +@export_enum("Linear", "Smooth", "Ease-In", "Ease-Out", "Ease-In-Out") var start_ease_mode: int = 1 + +## Duration of the fade-out at the end of the effect (seconds). +@export_range(0.0, 5.0) var end_ease: float = 0.0 + +## Easing curve for the fade-out. +@export_enum("Linear", "Smooth", "Ease-In", "Ease-Out", "Ease-In-Out") var end_ease_mode: int = 1 + +var _original_material: Material + + +func start(target: Node) -> void: + super.start(target) + if target is CanvasItem: + _original_material = target.material + var flash_material := ShaderMaterial.new() + flash_material.shader = flash_shader + flash_material.set_shader_parameter("flash_color", flash_color) + flash_material.set_shader_parameter("flash_intensity", 0.0) + flash_material.set_shader_parameter("blend_mode", blend_mode) + flash_material.set_shader_parameter("brightness_boost", brightness_boost) + target.material = flash_material + + +func process(delta: float, target: Node) -> void: + super.process(delta, target) + if target.material is ShaderMaterial: + var intensity := _compute_intensity(_elapsed) + target.material.set_shader_parameter("flash_intensity", intensity) + + +func stop(target: Node) -> void: + if target is CanvasItem: + target.material = _original_material + + +## Compute the combined eased flash intensity for a given elapsed time. +## Returns a value 0.0–1.0 where 1.0 means full flash visibility. +func _compute_intensity(elapsed: float) -> float: + if duration <= 0.0: + return 0.0 + + # Clamp elapsed to duration to avoid overshoot on the final frame + var t: float = min(elapsed, duration) + + # Overall envelope: fade in during start_ease, hold, fade out during end_ease + var envelope: float = 1.0 + if t < start_ease and start_ease > 0.0: + envelope = _ease_value(t / start_ease, start_ease_mode) + elif t > duration - end_ease and end_ease > 0.0: + var fade_progress = (t - (duration - end_ease)) / end_ease + envelope = 1.0 - _ease_value(fade_progress, end_ease_mode) + + # Flash pattern: on/off cycling at the given frequency and duty cycle + # Each flash pulse also eases in and out for a smoother look + var period := 1.0 / flash_frequency if flash_frequency > 0.0 else 1.0 + var phase := fmod(t, period) / period # 0.0–1.0 position within the current cycle + + var pulse: float = 0.0 + if phase < flash_length: + # Inside the "on" portion of this cycle — ease within the pulse itself + var pulse_progress = phase / flash_length if flash_length > 0.0 else 1.0 + # Ease in for the first half of the pulse (build up), ease out for the second (fade down) + if pulse_progress < 0.5: + pulse = _ease_value(pulse_progress * 2.0, 2) # Ease-In (build up) + else: + pulse = _ease_value((1.0 - pulse_progress) * 2.0, 3) # Ease-Out (fade down) + + return envelope * pulse + + +## Apply easing to a 0.0–1.0 normalized value based on the selected mode. +func _ease_value(value: float, mode: int) -> float: + match mode: + 0: return value # Linear + 1: return smoothstep(0.0, 1.0, value) # Smooth + 2: return ease(value, -2.0) # Ease-In (accelerating) + 3: return ease(value, 2.0) # Ease-Out (decelerating) + 4: return ease(value, 3.0) # Ease-In-Out (S-curve) + return value diff --git a/scenes/effects/flash_effect.gd.uid b/scripts/effects/flash_effect.gd.uid similarity index 100% rename from scenes/effects/flash_effect.gd.uid rename to scripts/effects/flash_effect.gd.uid diff --git a/scripts/movement_component.gd b/scripts/movement_component.gd index 0539438..655d778 100644 --- a/scripts/movement_component.gd +++ b/scripts/movement_component.gd @@ -30,11 +30,13 @@ func tick(delta: float): %Ship.frame = 2 %Ship/Thrusters.flip_h = true %Ship/Thrusters.animation = "banked" + %Ship/Thrusters.position.x = %Ship.position.x+1 elif input.x < 0: player.position.x = player.position.x-1 %Ship.frame = 1 %Ship/Thrusters.flip_h = false %Ship/Thrusters.animation = "banked" + %Ship/Thrusters.position.x = %Ship.position.x-1 else: %Ship.frame = 0 player.position.x = player.position.x diff --git a/scripts/weapon_component.gd b/scripts/weapon_component.gd index 37dc306..ab00c34 100644 --- a/scripts/weapon_component.gd +++ b/scripts/weapon_component.gd @@ -2,9 +2,12 @@ class_name WeaponComponent extends Node @export var weapon_data: WeaponShot = null @export var available_weapons: Array[WeaponShot] = [] +@export var weapon_change_flash: BaseEffect = null var _current_weapon_index: int = 0 +@onready var effects_component: EffectComponent = %EffectsComponent + func _ready() -> void: # Set the default weapon to be stock, i.e. index 1 @@ -36,10 +39,16 @@ func select_weapon_by_name(name: String) -> bool: print("Switched to: ", name) _emit_weapon_changed() + _trigger_change_flash() return true return false +func _trigger_change_flash() -> void: + if weapon_change_flash and effects_component: + effects_component.apply_effect(weapon_change_flash) + + func cycle_weapon() -> void: # Used for testing weapon cycling if available_weapons.is_empty(): @@ -50,3 +59,4 @@ func cycle_weapon() -> void: # Used for testing weapon cycling print("Switched to: ", weapon_data.shot_name) _emit_weapon_changed() + _trigger_change_flash() diff --git a/shaders/flash.gdshader b/shaders/flash.gdshader new file mode 100644 index 0000000..83fcd33 --- /dev/null +++ b/shaders/flash.gdshader @@ -0,0 +1,44 @@ +shader_type canvas_item; + +// Color of the flash overlay +uniform vec4 flash_color : source_color = vec4(1.0, 1.0, 1.0, 1.0); + +// Overall intensity of the flash (0.0 = none, 1.0 = full) +// Set dynamically by FlashEffect.gd each frame +uniform float flash_intensity : hint_range(0.0, 1.0) = 0.0; + +// How the flash blends with the original sprite +uniform int blend_mode : hint_enum("Mix", "Additive", "Screen") = 0; + +// Additional brightness added to the flash color (0.0 = none, 1.0 = double) +uniform float brightness_boost : hint_range(0.0, 1.0) = 0.0; + +void fragment() { + vec4 original = texture(TEXTURE, UV); + + vec3 flash_rgb = flash_color.rgb * (1.0 + brightness_boost); + + vec3 result; + float alpha = original.a; + + if (original.a == 0.0) { + // Skip processing for fully transparent pixels + COLOR = original; + } else if (blend_mode == 0) { + // Mix: Linear interpolation between original and flash color + result = mix(original.rgb, flash_rgb, flash_intensity); + // Modulate alpha by flash intensity for a flickering transparency effect + alpha = mix(original.a, flash_color.a, flash_intensity); + } else if (blend_mode == 1) { + // Additive: Flash adds light to the original, clamped to avoid HDR blowout + result = clamp(original.rgb + flash_rgb * flash_intensity, 0.0, 1.0); + // Additive naturally brightens, so boost alpha when flashing + alpha = clamp(original.a + flash_color.a * flash_intensity, 0.0, 1.0); + } else { + // Screen: Brightens without washing out highlights + result = original.rgb + (1.0 - original.rgb) * flash_rgb * flash_intensity; + alpha = clamp(original.a + (1.0 - original.a) * flash_color.a * flash_intensity, 0.0, 1.0); + } + + COLOR = vec4(result, alpha); +} diff --git a/shaders/flash.gdshader.uid b/shaders/flash.gdshader.uid new file mode 100644 index 0000000..82bde5d --- /dev/null +++ b/shaders/flash.gdshader.uid @@ -0,0 +1 @@ +uid://dp4svqyxg1dum