@tool @icon("res://addons/shaker/assets/Shaker.svg") class_name ShakerComponent extends Node # Allows you to apply a shake effect to any variable of a node of any type # Custom target flag @export var custom_target: bool = false: set = set_custom_target, get = get_custom_target # Array of target Node objects @export var Targets: Array[Node] # Randomization flag @export var randomize: bool = false: set = set_randomize, get = get_randomize @export var AutoPlay:bool = false # Playing state @export var play:bool = false: set(value): play = value if value: play_shake() elif timer > 0: force_stop_shake() var is_playing: bool = false # Shake intensity @export_range(0.0, 1.0, 0.001, "or_greater") var intensity: float = 1.0: set = set_intensity, get = get_intensity # Shake duration @export var duration: float = 0.00: set = set_duration, get = get_duration # Shake speed @export_range(0.0, 1.0, 0.001, "or_greater") var shake_speed: float = 1.0: set = set_shake_speed, get = get_shake_speed # Fade-in easing @export_exp_easing var fade_in: float = 0.25: set = set_fade_in, get = get_fade_in # Fade-out easing @export_exp_easing("attenuation") var fade_out: float = 0.25: set = set_fade_out, get = get_fade_out # Shaker preset @export var shakerProperty:Array[ShakerProperty]: set = set_shaker_property, get = get_shaker_property # Timer for shake progress var timer: float = 0.0: set = _on_timeline_progress # SIGNALS signal timeline_progress(progress: float) signal shake_started signal shake_finished signal shake_fading_out func set_intensity(value: float) -> void: intensity = max(value, 0.0) func get_intensity() -> float: return intensity func set_duration(value: float) -> void: duration = max(value, 0.0) notify_property_list_changed() func get_duration() -> float: return duration func set_shake_speed(value: float) -> void: shake_speed = max(value, 0.001) notify_property_list_changed() func get_shake_speed() -> float: return shake_speed func set_fade_in(value: float) -> void: fade_in = value func get_fade_in() -> float: return fade_in func set_fade_out(value: float) -> void: fade_out = value func get_fade_out() -> float: return fade_out # Handles timeline progress func _on_timeline_progress(value: float) -> void: timer = value timeline_progress.emit(timer) # Private variables var _timer_offset: float = 0.0 var _fading_out: bool = false var _seed: float = 203445 var _last_values:Array[Dictionary] # Called when the node enters the scene tree for the first time func _ready() -> void: set_process_input(false) set_process_internal(false) set_process_shortcut_input(false) set_process_unhandled_input(false) set_physics_process(false) set_physics_process_internal(false) add_to_group("ShakerComponent") if !Engine.is_editor_hint(): if AutoPlay: play_shake() # Resets the shaker to its initial state func _reset() -> void: _last_values = [] is_playing = false _initialize_timer_offset() _fading_out = false _initalize_target() # Called every frame func _process(delta: float) -> void: if is_playing: if shakerProperty != null: if timer <= duration || duration == 0.0: _progress_shake() timer += delta * shake_speed else: force_stop_shake() else: if timer > 0: force_stop_shake() # Progresses the shake effect func _progress_shake() -> void: var _ease_in: float = 1.0 var _ease_out: float = 1.0 var _final_duration: float = duration if (duration > 0 && !_fading_out) else 1.0 _ease_in = ease(timer/_final_duration, fade_in) _ease_out = ease(1.0-(max((timer-_timer_offset), 0.0))/_final_duration, fade_out) # Check all targets for i:int in Targets.size(): var target:Node = Targets[i] if !is_instance_valid(target): Targets.remove_at(i) i -= 1 if Targets.size() <= 0: shake_finished.emit() break if (!(duration > 0) || _fading_out) && is_playing: if _ease_out <= get_process_delta_time(): force_stop_shake() var _count:int = Targets.size() # if randomize else 1) var _value_temp:Array[Dictionary] = [] for i in _count: _value_temp.append({}) for _index:int in _count: var _randomized:float = (_seed * (float(_index+1) / Targets.size())) if randomize else 0.0 var target:Node = Targets[_index] if _index > _last_values.size()-1: _last_values.append({}) # Shaker Preset for shake:ShakerProperty in shakerProperty: if shake: if !shake.property_name.is_empty() && shake.shake_type: var _value:float = timer + _randomized var _add_value = (shake.get_value(_value)) var current_value = target.get(shake.property_name) if typeof(current_value) == typeof(current_value): if !(_last_values[_index].has(shake.property_name)): _last_values[_index][shake.property_name] = {} _last_values[_index][shake.property_name]["value"] = _add_value * 0.0 var _prev_temp = (_add_value * 0.0) if !(_value_temp[_index].has(shake.property_name)) else _value_temp[_index][shake.property_name]["value"] _value_temp[_index][shake.property_name] = {} _value_temp[_index][shake.property_name]["value"] = _prev_temp + _add_value _value_temp[_index][shake.property_name]["blend_mode"] = shake.shake_type.BlendingMode else: push_error("Variable value type is %s but Shake type is %s" % [type_string(current_value), type_string(_add_value)]) var _strength:float = intensity * _ease_in * _ease_out for index:int in Targets.size(): var target:Node = Targets[index] var i:int = fmod(index, _value_temp.size()) var property = _value_temp[i] for property_index:int in property.size(): var property_name:StringName = property.keys()[property_index] if !property_name.is_empty(): var value = property[property_name]["value"] var blend_mode = property[property_name]["blend_mode"] var current_value = target.get(property_name)-_last_values[i][property_name]["value"] var default_value = current_value match blend_mode: ShakerTypeBase.BlendingModes.Add: current_value += value ShakerTypeBase.BlendingModes.Multiply: current_value *= value ShakerTypeBase.BlendingModes.Subtract: current_value -= value ShakerTypeBase.BlendingModes.Max: if typeof(current_value) == TYPE_VECTOR2 || typeof(current_value) == TYPE_VECTOR2I: current_value.x = max(current_value.x, value.x) current_value.y = max(current_value.y, value.y) elif typeof(current_value) == TYPE_VECTOR3 || typeof(current_value) == TYPE_VECTOR3I: current_value.x = max(current_value.x, value.x) current_value.y = max(current_value.y, value.y) current_value.z = max(current_value.z, value.z) else: current_value = max(current_value, value) ShakerTypeBase.BlendingModes.Min: if typeof(current_value) == TYPE_VECTOR2 || typeof(current_value) == TYPE_VECTOR2I: current_value.x = min(current_value.x, value.x) current_value.y = min(current_value.y, value.y) elif typeof(current_value) == TYPE_VECTOR3 || typeof(current_value) == TYPE_VECTOR3I: current_value.x = min(current_value.x, value.x) current_value.y = min(current_value.y, value.y) current_value.z = min(current_value.z, value.z) else: current_value = min(current_value, value) ShakerTypeBase.BlendingModes.Average: current_value = (current_value + value) * 0.5 ShakerTypeBase.BlendingModes.Override: current_value = value if current_value != null: var _added_value = (current_value - default_value) * _strength target.set(property_name, default_value + _added_value ) _value_temp[i][property_name]["value"] = _added_value else: push_error(name," Variable Error: ",target," has no variable named \"",property_name,"\"") _last_values = _value_temp # Stops the shake effect with a fade-out func stop_shake() -> void: if !_fading_out: _timer_offset = timer _fading_out = true shake_fading_out.emit() # Immediately stops the shake effect func force_stop_shake() -> void: if is_playing || _fading_out: set_progress(0.0) is_playing = false _fading_out = false _last_values.clear() shake_finished.emit() func set_shaker_property(value:Array[ShakerProperty]) -> void: shakerProperty = value func get_shaker_property() -> Array[ShakerProperty]: return shakerProperty # Starts the shake effect func play_shake() -> void: if shakerProperty != null: _initalize_target() randomize_shake() is_playing = !is_playing if Engine.is_editor_hint() else true _fading_out = false _initialize_timer_offset() shake_started.emit() func randomize_shake() -> void: _seed = randf_range(10000, 99999) func _initalize_target() -> void: if !custom_target: Targets.clear() if get_parent() is Node: Targets.append(get_parent()) # Validates property visibility func _validate_property(property: Dictionary) -> void: if property.name == "Targets" || property.name == "randomize": if !custom_target: property.usage = PROPERTY_USAGE_NONE if property.name == "fade_time": if duration > 0: property.usage = PROPERTY_USAGE_NONE # Get configuration warnings func _get_configuration_warnings() -> PackedStringArray: if !custom_target: if not get_parent() is Node: return ["Parent must be Node"] return [] # Sets the shake progress func set_progress(value: float) -> void: timer = value _progress_shake() # Custom setter and getter functions for @export variables func set_custom_target(value: bool) -> void: custom_target = value notify_property_list_changed() func get_custom_target() -> bool: return custom_target func set_randomize(value: bool) -> void: if custom_target && Targets.size() > 1: if _last_values.size() > 0: for index: int in Targets.size(): var target:Node = Targets[index] var i:int = fmod(index, _last_values.size()) for shake:ShakerProperty in shakerProperty: var current_value = target.get(shake.property_name) target.set(shake.property_name, current_value - _last_values[i][shake.property_name]["value"]) _last_values.clear() randomize = value randomize_shake() func _initialize_timer_offset() -> void: if !(duration > 0): _timer_offset = 0x80000 else: _timer_offset = 0.0 func get_randomize() -> bool: return randomize