some shake

This commit is contained in:
2026-01-20 15:27:59 +01:00
parent 8d1e7ebb4f
commit c1ca0bf27b
143 changed files with 5704 additions and 6 deletions

View File

@@ -0,0 +1,91 @@
@tool
#class_name ShakerBase3D
extends Node3D
# 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 shakerPreset:ShakerPreset3D:
set = set_shaker_preset,
get = get_shaker_preset
# 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)
if shakerPreset != null:
shakerPreset.component_duration = duration
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
func set_shaker_preset(value: ShakerPreset3D) -> void:
shakerPreset = value
if shakerPreset != null:
shakerPreset.parent = self
shakerPreset.component_duration = duration
func get_shaker_preset() -> ShakerPreset3D:
return shakerPreset
# Handles timeline progress
func _on_timeline_progress(value: float) -> void:
timer = value
timeline_progress.emit(timer)

View File

@@ -0,0 +1 @@
uid://cna76yluotcdw

View File

@@ -0,0 +1,183 @@
@icon("res://addons/shaker/assets/ShakerEmitter3D.svg")
@tool
class_name ShakerEmitter3D
extends "res://addons/shaker/src/Vector3/ShakerBase3D.gd"
## It emits shake values and is received by ShakeEmitter3D.
# Exported variables
@export var emit: bool:
set = set_emit,
get = get_emit
@export var max_distance: float = 0.0:
set = set_max_distance,
get = get_max_distance
@export_exp_easing("attenuation") var distance_attenuation: float = 0.5:
set = set_distance_attenuation,
get = get_distance_attenuation
# Private variables
var emitting: bool = false
var _timer_offset: float = 0.0
var _fading_out: bool = false
var shake_offset_position: Vector3 = Vector3.ZERO
var shake_offset_rotation: Vector3 = Vector3.ZERO
var shake_offset_scale: Vector3 = Vector3.ZERO
var area3d: Area3D
var collision:CollisionShape3D
# Called when the node enters the scene tree for the first time
func _ready() -> void:
add_to_group("ShakerEmitter")
for child in get_children():
if child is Area3D:
area3d = child
if not area3d:
_create_area3d()
set_emit(emit)
# Creates an Area3D child node if one doesn't exist
func _create_area3d() -> void:
area3d = Area3D.new()
collision = CollisionShape3D.new()
add_child(area3d)
area3d.add_child(collision)
area3d.set_owner(get_tree().edited_scene_root)
collision.set_owner(get_tree().edited_scene_root)
area3d.name = "Area3D"
collision.name = "CollisionShape3D"
area3d.collision_layer = 1 << 9
area3d.collision_mask = 0
# Called every frame
func _process(delta: float) -> void:
if !Engine.is_editor_hint():
if emitting:
if shakerPreset != null:
if timer <= duration or 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:
if !Engine.is_editor_hint():
var _ease_in: float = 1.0
var _ease_out: float = 1.0
var _final_duration: float = duration if (duration > 0 and not _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)
if not (duration > 0) or _fading_out:
if _ease_out <= get_process_delta_time():
force_stop_shake()
var _shake_position: Vector3 = Vector3.ZERO
var _shake_rotation: Vector3 = Vector3.ZERO
var _shake_scale: Vector3 = Vector3.ZERO
if shakerPreset != null:
var _value: float = timer
var _strength: float = intensity * _ease_in * _ease_out
_shake_position += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.POSITION) * _strength)
_shake_rotation += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.ROTATION) * _strength * (PI/2.0))
_shake_scale += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.SCALE) * _strength)
shake_offset_position = _shake_position
shake_offset_rotation = _shake_rotation
shake_offset_scale = _shake_scale
# Starts the shake effect
func play_shake() -> void:
if !Engine.is_editor_hint():
if shakerPreset != null:
emitting = true
_fading_out = false
_initialize_timer_offset()
shake_started.emit()
func _initialize_timer_offset() -> void:
if !(duration > 0): _timer_offset = 0x80000
else: _timer_offset = 0.0
# Updates the gizmo
#func update_gizmo() -> void:
#if _gizmo:
#_gizmo._redraw()
# Stops the shake effect with a fade-out
func stop_shake() -> void:
if !Engine.is_editor_hint():
if not _fading_out:
_timer_offset = timer
_fading_out = true
shake_fading_out.emit()
# Immediately stops the shake effect
func force_stop_shake() -> void:
if emitting:
if emit: emit = false
_fading_out = false
emitting = false
set_progress(0.0)
shake_finished.emit()
# Returns configuration warnings
func _get_configuration_warnings() -> PackedStringArray:
if not get_children().any(func(child): return child is Area3D):
return ["First child must be Area3D"]
return []
# Sets the shake progress
func set_progress(value: float) -> void:
if !Engine.is_editor_hint():
timer = value
_progress_shake()
# Setter for emit property
func set_emit(value: bool) -> void:
emit = value
if !Engine.is_editor_hint():
if value:
play_shake()
elif timer > 0:
force_stop_shake()
# Getter for emit property
func get_emit() -> bool:
return emit
# Setter for max_distance property
func set_max_distance(value: float) -> void:
max_distance = value
notify_property_list_changed()
# Getter for max_distance property
func get_max_distance() -> float:
return max_distance
# Setter for distance_attenuation property
func set_distance_attenuation(value: float) -> void:
distance_attenuation = value
# Getter for distance_attenuation property
func get_distance_attenuation() -> float:
return distance_attenuation
# Validates properties
func _validate_property(property: Dictionary) -> void:
if property.name == "distance_attenuation":
if not (max_distance > 0):
property.usage = PROPERTY_USAGE_NONE

View File

@@ -0,0 +1 @@
uid://vo46i2pfq0uf

View File

@@ -0,0 +1,159 @@
@icon("res://addons/shaker/assets/ShakerReceiver3D.svg")
@tool
class_name ShakerReceiver3D
extends Node3D
## Transmits values from ShakerEmitter3D to ShakerComponent3D
# Fade-in easing
@export_exp_easing var enter_fade_in: float = 0.1:
set = set_fade_in,
get = get_fade_in
# Fade-out easing
@export_exp_easing("attenuation") var exit_fade_out: float = 3.0:
set = set_fade_out,
get = get_fade_out
# Private variables
var area3d: Area3D
var position_offset: Vector3 = Vector3.ZERO
var rotation_offset: Vector3 = Vector3.ZERO
var scale_offset: Vector3 = Vector3.ZERO
var emitter_list: Array[EmitterData]
# Called when the node enters the scene tree for the first time
func _ready() -> void:
_setup_area3d()
if !Engine.is_editor_hint():
add_to_group("ShakerReceiver")
_connect_signals()
# Sets up the Area3D node
func _setup_area3d() -> void:
for child in get_children():
if child is Area3D:
area3d = child
return
area3d = Area3D.new()
var collision = CollisionShape3D.new()
add_child(area3d)
area3d.add_child(collision)
area3d.set_owner(get_tree().edited_scene_root)
collision.set_owner(get_tree().edited_scene_root)
area3d.name = "Area3D"
collision.name = "CollisionShape3D"
area3d.collision_layer = 0
area3d.collision_mask = 1 << 9
# Connects signals
func _connect_signals() -> void:
area3d.area_entered.connect(on_area_entered)
area3d.area_exited.connect(on_area_exited)
# Called every frame
func _process(delta: float) -> void:
if !Engine.is_editor_hint():
position_offset = Vector3.ZERO
rotation_offset = Vector3.ZERO
scale_offset = Vector3.ZERO
if emitter_list.size() > 0:
for emitter_data in emitter_list:
_process_emitter(emitter_data, delta)
# Processes each emitter
func _process_emitter(emitter_data: EmitterData, delta: float) -> void:
if !Engine.is_editor_hint():
if is_instance_valid(emitter_data.emitter):
var ease_in: float = ease(emitter_data.timer, enter_fade_in)
var ease_out: float = ease(1.0 - (emitter_data.timer - emitter_data.fade_out_timer), exit_fade_out) if emitter_data.fade_out_timer != 0.0 else 1.0
emitter_data.ease_out_intensity = move_toward(emitter_data.ease_out_intensity, ease_out, delta)
ease_out = emitter_data.ease_out_intensity
var max_distance: float = emitter_data.emitter.max_distance
var distance: float = min(emitter_data.emitter.global_position.distance_to(global_position), max_distance) / max(max_distance, 0.001)
var attenuation: float = ease(1.0 - distance, emitter_data.emitter.distance_attenuation)
position_offset += emitter_data.emitter.shake_offset_position * ease_in * ease_out * attenuation
rotation_offset += emitter_data.emitter.shake_offset_rotation * ease_in * ease_out * attenuation
scale_offset += emitter_data.emitter.shake_offset_scale * ease_in * ease_out * attenuation
emitter_data.timer += delta
if ease_out <= delta:
emitter_list.erase(emitter_data)
else:
emitter_list.erase(emitter_data)
# Returns the current shake values
func get_value() -> Array[Vector3]:
return [position_offset, rotation_offset, scale_offset]
# Returns configuration warnings
func _get_configuration_warnings() -> PackedStringArray:
if not get_parent() is ShakerComponent3D:
return ["Parent must be ShakerComponent3D"]
var _ex:bool = false
for i in get_children():
if i is Area3D:
_ex = true
break
if !_ex:
return ["ShakerReceiver3D needs Area3D to work"]
return []
# Called when an area enters
func on_area_entered(area: Area3D) -> void:
var node = area.get_parent()
if node is ShakerEmitter3D:
var _exists = null
for index in emitter_list.size():
var data = emitter_list[index]
if data.emitter == node: _exists = data
if !_exists:
var data = EmitterData.new(node)
emitter_list.append(data)
else:
_exists.fade_out_timer = 0.0
# Called when an area exits
func on_area_exited(area: Area3D) -> void:
var node = area.get_parent()
if node is ShakerEmitter3D:
for index in emitter_list.size():
var data = emitter_list[index]
if data.emitter == node:
data.fade_out_timer = data.timer
break
# Setter for enter_fade_in
func set_fade_in(value: float) -> void:
enter_fade_in = value
# Getter for enter_fade_in
func get_fade_in() -> float:
return enter_fade_in
# Setter for exit_fade_out
func set_fade_out(value: float) -> void:
exit_fade_out = value
# Getter for exit_fade_out
func get_fade_out() -> float:
return exit_fade_out
# EmitterData inner class
class EmitterData:
var emitter: ShakerEmitter3D
var timer: float = 0.0
var fade_out_timer: float = 0.0
var ease_out_intensity:float = 1.0
func _init(_emitter: ShakerEmitter3D) -> void:
self.emitter = _emitter
func is_playing() -> bool:
for i:EmitterData in emitter_list:
return i.emitter.emitting
return false

View File

@@ -0,0 +1 @@
uid://csr4nkcrmm0bq

View File

@@ -0,0 +1,284 @@
@tool
@icon("res://addons/shaker/assets/Shaker3D.svg")
class_name ShakerComponent3D
extends "res://addons/shaker/src/Vector3/ShakerBase3D.gd"
## Allows you to apply shake effect to any 3D node according to position, rotation, scale
enum ShakeAddMode {
add,
override
}
# Custom target flag
@export var custom_target: bool = false:
set = set_custom_target,
get = get_custom_target
# Array of target Node3D objects
@export var Targets: Array[Node3D]
# Randomization flag
@export var randomize: bool = false:
set = set_randomize,
get = get_randomize
# Playing state
@export var is_playing: bool = false
@export var AutoPlay:bool = false
# Private variables
var _last_position_shake: Array[Vector3] = [Vector3.ZERO]
var _last_scale_shake: Array[Vector3] = [Vector3.ZERO]
var _last_rotation_shake: Array[Vector3] = [Vector3.ZERO]
var _timer_offset: float = 0.0
var _fading_out: bool = false
var _seed: float = 203445
var _external_shakes:Array[ExternalShake]
# 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_position_shake = [Vector3.ZERO]
_last_scale_shake = [Vector3.ZERO]
_last_rotation_shake = [Vector3.ZERO]
_external_shakes.clear()
_initalize_prev_positions()
is_playing = false
_initialize_timer_offset()
_fading_out = false
_initalize_target()
# Initializes previous positions for randomized shaking
func _initalize_prev_positions() -> void:
_last_position_shake.resize(Targets.size())
_last_position_shake.fill(Vector3.ZERO)
_last_scale_shake.resize(Targets.size())
_last_scale_shake.fill(Vector3.ZERO)
_last_rotation_shake.resize(Targets.size())
_last_rotation_shake.fill(Vector3.ZERO)
# Called every frame
func _process(delta: float) -> void:
if is_playing:
if shakerPreset != null || _external_shakes.size() > 0 || is_receiving_from_emitters():
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)
if (!(duration > 0) || _fading_out) && is_playing:
if _ease_out <= get_process_delta_time():
force_stop_shake()
var _shake_position: Array[Vector3] = []
var _shake_rotation: Array[Vector3] = []
var _shake_scale: Array[Vector3] = []
var _count:int =(Targets.size() if randomize else 1)
_shake_position.resize(_count)
_shake_position.fill(Vector3.ZERO)
_shake_rotation.resize(_count)
_shake_rotation.fill(Vector3.ZERO)
_shake_scale.resize(_count)
_shake_scale.fill(Vector3.ZERO)
for _index in _count:
var _randomized: float = (_seed * (float(_index+1) / Targets.size())) if randomize else 0.0
if _last_position_shake.size() != _count: _initalize_prev_positions()
# Shaker Preset
if shakerPreset != null:
var _value:float = timer + _randomized
var _strength:float = intensity * _ease_in * _ease_out
_shake_position[_index] += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.POSITION) * _strength)
_shake_rotation[_index] += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.ROTATION) * _strength * (PI/2.0))
_shake_scale[_index] += (shakerPreset.get_value(_value, ShakerPreset3D.Categories.SCALE) * _strength)
# External Shake Addition
for external_shake:ExternalShake in _external_shakes:
var _real_time:float = min(timer-external_shake.start_time, external_shake.duration)
_ease_in = ease(_real_time/external_shake.duration, external_shake.fade_in)
_ease_out = ease(1.0-(_real_time/external_shake.duration), external_shake.fade_out)
var _value:float = (_real_time*external_shake.speed) + _randomized
var _strength:float = external_shake.intensity * intensity * _ease_in * _ease_out
var _mode_value:Array[Vector3] = [Vector3.ZERO, Vector3.ZERO, Vector3.ZERO]
match external_shake.mode:
ShakeAddMode.add:
_mode_value = [_shake_position[_index], _shake_rotation[_index], _shake_scale[_index] ]
_shake_position[_index] = _mode_value[0] + (external_shake.preset.get_value(_value, ShakerPreset3D.Categories.POSITION) * _strength)
_shake_rotation[_index] = _mode_value[1] +(external_shake.preset.get_value(_value, ShakerPreset3D.Categories.ROTATION) * _strength * (PI/2.0))
_shake_scale[_index] = _mode_value[2] + (external_shake.preset.get_value(_value, ShakerPreset3D.Categories.SCALE) * _strength)
if _real_time >= external_shake.duration:
_external_shakes.erase(external_shake)
# Shake Emitter
for _child in get_children():
if _child is ShakerReceiver3D:
_shake_position[_index] += _child.position_offset
_shake_rotation[_index] += _child.rotation_offset
_shake_scale[_index] += _child.scale_offset
for index: int in Targets.size():
var target: Node3D = Targets[index]
if !is_instance_valid(target):
Targets.remove_at(index)
index-=1
if Targets.size() <= 0:
shake_finished.emit()
break
var _i: int = fmod(index, _shake_position.size())
target.position += -_last_position_shake[_i] + _shake_position[_i]
target.rotation += -_last_rotation_shake[_i] + _shake_rotation[_i]
target.scale += -_last_scale_shake[_i] + _shake_scale[_i]
_last_position_shake = _shake_position
_last_rotation_shake = _shake_rotation
_last_scale_shake = _shake_scale
# 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)
_reset()
shake_finished.emit()
# Starts the shake effect
func play_shake() -> void:
_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 _initialize_timer_offset() -> void:
if !(duration > 0): _timer_offset = 0x80000
else: _timer_offset = 0.0
func _initalize_target() -> void:
if !custom_target:
Targets.clear()
if get_parent() is Node3D:
Targets.append(get_parent())
# Placeholder for shake function
func shake(shaker_preset:ShakerPreset3D, _mode:ShakeAddMode=ShakeAddMode.add, duration:float=1.0, speed:float=1.0, intensity:float=1.0, fade_in:float=.25, fade_out:float=.25) -> void:
var external_shake:ExternalShake = ExternalShake.new()
external_shake.preset = shaker_preset
external_shake.duration = duration
external_shake.speed = speed
external_shake.intensity = intensity
external_shake.start_time = timer
external_shake.fade_in = fade_in
external_shake.fade_out = fade_out
external_shake.mode = _mode
_external_shakes.append(external_shake)
if Targets.is_empty():
_initalize_target()
is_playing = true
# 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 Node3D:
return ["Parent must be Node3D"]
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:
for index: int in Targets.size():
var target: Node3D = Targets[index]
var i = fmod(index, _last_position_shake.size())
target.position += -_last_position_shake[i]
target.rotation += -_last_rotation_shake[i]
target.scale += -_last_scale_shake[i]
_last_position_shake.fill(_last_position_shake[0])
_last_rotation_shake.fill(_last_rotation_shake[0])
_last_scale_shake.fill(_last_scale_shake[0])
randomize = value
randomize_shake()
func get_randomize() -> bool:
return randomize
class ExternalShake:
var preset:ShakerPreset3D
var duration:float = 1.0
var speed:float = 1.0
var intensity:float = 1.0
var start_time:float = 0.0
var fade_in:float = 0.25
var fade_out:float = 0.25
var mode:ShakeAddMode=ShakeAddMode.add
func is_receiving_from_emitters() -> bool:
for _child in get_children():
if _child is ShakerReceiver3D:
return _child.is_playing()
return false

View File

@@ -0,0 +1 @@
uid://dnlxsrumw6ygp