Files
MovementTests/addons/shaker/src/shaker_component.gd
2026-01-20 15:27:59 +01:00

348 lines
10 KiB
GDScript

@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