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

285 lines
9.0 KiB
GDScript

@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