File size: 4,949 Bytes
84e8204 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | extends Node
##
## TimelineModule - keyframe timeline for objects and bones (Features 81-100).
##
## Backed by a real Godot Animation resource + AnimationPlayer, so playback,
## interpolation and looping use engine-native code (fast, cached). The module
## exposes a touch-friendly API: scrub(), add_keyframe(), play(), pause().
##
@export var duration: float = 5.0
@export var fps: int = 30
var _host: Node = null
var _player: AnimationPlayer = null
var _anim: Animation = null
var _playhead: float = 0.0
var _playing := false
var _key_count := 0
func _activate(host: Node) -> void:
_host = host
_ensure_player()
print("[Timeline] activated (%.1fs @ %d fps)" % [duration, fps])
func _deactivate() -> void:
_playing = false
_player = null
_anim = null
_host = null
func _ensure_player() -> void:
if _player != null and is_instance_valid(_player):
return
_anim = Animation.new()
_anim.length = duration
_anim.step = 1.0 / float(fps)
_player = AnimationPlayer.new()
_player.name = "TimelinePlayer"
if _host != null:
_host.add_child(_player)
var lib := AnimationLibrary.new()
lib.add_animation("main", _anim)
_player.add_animation_library("", lib)
_player.animation_finished.connect(_on_finished)
func _process(delta: float) -> void:
if not _playing or _player == null:
return
_playhead = _player.current_animation_position
StudioBus.timeline_playhead.emit(_playhead)
## --- Playback ---------------------------------------------------------------
func play(from: float = -1.0) -> void:
_ensure_player()
if from >= 0.0:
_player.seek(from, true)
_player.play("main")
_playing = true
StudioBus.playback_state_changed.emit(true)
func pause() -> void:
if _player != null:
_player.pause()
_playing = false
StudioBus.playback_state_changed.emit(false)
func stop() -> void:
if _player != null:
_player.stop()
_playhead = 0.0
_playing = false
StudioBus.playback_state_changed.emit(false)
## Jump the playhead to `time` seconds (scrubbing).
func scrub(time: float) -> void:
_ensure_player()
time = clampf(time, 0.0, duration)
_playhead = time
_player.seek(time, true)
StudioBus.timeline_playhead.emit(time)
func is_playing() -> bool:
return _playing
func playhead() -> float:
return _playhead
## --- Keyframes --------------------------------------------------------------
## Add a keyframe for a `property` (NodePath-style) on an object at `time`.
## For bones use property like "bones/Head:position" (Skeleton3D pose tracks).
func add_keyframe(target_node: Node3D, property: String, time: float, value: Variant) -> void:
_ensure_player()
time = clampf(time, 0.0, duration)
var path := _player.get_path_to(target_node)
if path == NodePath():
push_warning("Timeline: target is not under the player root")
return
var track := _find_or_create_track(String(path), property, value)
var key_idx := _anim.track_insert_key(track, time, value, 0.0)
if key_idx >= 0:
_key_count += 1
StudioBus.keyframe_added.emit(String(path), property, time)
StudioBus.scene_modified.emit()
func _find_or_create_track(node_path: String, property: String, value: Variant) -> int:
var full := NodePath(node_path + ":" + property)
for i in range(_anim.get_track_count()):
if _anim.track_get_path(i) == full:
return i
var type := Animation.TYPE_VALUE
if value is Transform3D:
type = Animation.TYPE_VALUE
_anim.add_track(type)
var idx := _anim.get_track_count() - 1
_anim.track_set_path(idx, full)
if value is Vector3:
_anim.track_set_interpolation_type(idx, Animation.INTERPOLATION_CUBIC)
return idx
## Convenience: key every transform channel of a bone of a Skeleton3D.
## Godot stores bone poses as 3D transforms, so we key position/rotation/scale.
func add_bone_keyframe(skeleton: Skeleton3D, bone_name: String, time: float) -> void:
if skeleton == null:
return
var bone_idx := skeleton.find_bone(bone_name)
if bone_idx < 0:
push_warning("Timeline: bone '%s' not found" % bone_name)
return
var skin_path := "%s:%s" % [_player.get_path_to(skeleton), bone_name]
var pose := skeleton.get_bone_pose(bone_idx)
add_keyframe(skeleton, "bones/" + bone_name + "/position", time, pose.origin)
# Store rotation as quaternion for clean interpolation.
add_keyframe(skeleton, "bones/" + bone_name + "/rotation", time, pose.basis.get_rotation_quaternion())
add_keyframe(skeleton, "bones/" + bone_name + "/scale", time, pose.basis.get_scale())
func delete_keyframe(track: int, key: int) -> void:
if track >= 0 and track < _anim.get_track_count():
_anim.track_remove_key(track, key)
_key_count = maxi(0, _key_count - 1)
func track_count() -> int:
return _anim.get_track_count() if _anim != null else 0
func key_count() -> int:
return _key_count
func set_duration(seconds: float) -> void:
duration = maxf(0.1, seconds)
_ensure_player()
_anim.length = duration
func _on_finished() -> void:
_playing = false
StudioBus.playback_state_changed.emit(false)
|