@tool class_name ProAnimatedSprite2D extends AnimatedSprite2D @export var autostart := true @export var action_configs: Array[Dictionary] = [] @export var move_configs: Array[Dictionary] = [] # 0.0 means no light # negative value means subtract, positive value means add @export_range(-1.0, 1.0) var light_energy := 0.0: set(val): light2d.energy = abs(val) if val > 0: light2d.blend_mode = Light2D.BLEND_MODE_ADD else: light2d.blend_mode = Light2D.BLEND_MODE_SUB light_energy = val * modulate.a @export var debug_mov_animation := "" @export_tool_button("debug 刷新 mov 目标") var debug_mov_projection := _debug_mov_projection const ACTION_CONFIG = { "animation_intro": "", "intro_loop": 1, "animation_wait_time": 0.0, "animation_next": "" } # 参数 {duration} 目前无需求,暂且保留 const MOVE_CONFIG = { "animation": "", "velocity": Vector2.ZERO, "duration": 10000000.0, "movement_x": 0.0, "animation_next": "" } static func new_move_config() -> Dictionary: return MOVE_CONFIG.duplicate() static func new_action_config() -> Dictionary: return ACTION_CONFIG.duplicate() @onready var debug_mov_onion_sprite2d = $DebugMovOnionSkinSprite2D as Sprite2D # 从 intro 到 next 的配置信息 var auto_checkout_dict = { # intro -> {state_config instance} } # 播放 animation 的同时,进行移动 var animation_mov_dict = { # animation -> {velocity, ...} } var light2d := PointLight2D.new() func _ready() -> void: if light_energy != 0.0: add_child(light2d) frame_changed.connect(_on_frame_changed) if Engine.is_editor_hint(): _debug_mov_projection() return _load_config() debug_mov_onion_sprite2d.queue_free() if autostart and animation: # 制造一点错差 frame = randi() % sprite_frames.get_frame_count(animation) play() animation_changed.connect(_on_animation_start) animation_looped.connect(_on_animation_start) animation_finished.connect(_on_animation_finished) animation_looped.connect(_on_animation_finished) func _debug_mov_projection(): _load_config() if debug_mov_animation and animation_mov_dict.has(debug_mov_animation): var mov_config = animation_mov_dict[debug_mov_animation] # 展示 accumulated animation 的目标位置 debug_mov_onion_sprite2d.position.x = mov_config.movement_x debug_mov_onion_sprite2d.texture = sprite_frames.get_frame_texture(debug_mov_animation, 0) elif debug_mov_animation: push_warning("Debug move config not found:", debug_mov_animation) func _load_config(): # load auto_checkout_dict for i in range(action_configs.size()): action_configs[i].merge(ACTION_CONFIG) var state_config = action_configs[i] # 不必关闭循环 # if sprite_frames and sprite_frames.has_animation(state_config.animation_intro): # intro 的 animation 关闭循环 # sprite_frames.set_animation_loop(state_config.animation_intro, false) auto_checkout_dict[state_config.animation_intro] = action_configs[i] # load animation_frame_offsets for i in range(move_configs.size()): move_configs[i].merge(MOVE_CONFIG) var move_config = move_configs[i] if sprite_frames and sprite_frames.has_animation(move_config.animation): animation_mov_dict[move_config.animation] = move_config func _on_frame_changed(): if light_energy == 0.0: return var texture = sprite_frames.get_frame_texture(animation, frame) if not texture: return light2d.texture = texture light_energy = light_energy * modulate.a func _reset_loop(intro: String): for c in action_configs: if c.animation_intro == intro: auto_checkout_dict[intro].intro_loop = max(1, c.intro_loop) func _on_animation_finished() -> void: var intro = animation if auto_checkout_dict.has(intro): auto_checkout_dict[intro].intro_loop -= 1 if auto_checkout_dict[intro].intro_loop <= 0: # reset loop _reset_loop(intro) var wait_time = auto_checkout_dict[intro].animation_wait_time var next = auto_checkout_dict[intro].animation_next if wait_time > 0: pause() get_tree().create_timer(wait_time).timeout.connect(play.bind(next)) else: play(next) var velocity := Vector2.ZERO var mov_x := 0.0 var mov_x_next_animation := "" var accumulated_mov_x := 0.0 var prev_animation := "" func _on_animation_start(): if prev_animation != animation: accumulated_mov_x = 0.0 prev_animation = animation velocity = Vector2.ZERO mov_x = 0.0 mov_x_next_animation = "" if not animation or not animation_mov_dict.has(animation): return # velocity = animation_mov_dict[animation] * speed_scale velocity = animation_mov_dict[animation].velocity mov_x_next_animation = animation_mov_dict[animation].animation_next if mov_x_next_animation: # 只有在 next animation 存在时,mov_x 才有意义 mov_x = animation_mov_dict[animation].movement_x func _physics_process(delta: float) -> void: if Engine.is_editor_hint() or not velocity or not is_playing(): return var diff_x = velocity.x * delta if flip_h: position.x -= diff_x else: position.x += diff_x # 检查是否切换 animation if mov_x != 0.0 and mov_x_next_animation: accumulated_mov_x += diff_x if absf(accumulated_mov_x) >= absf(mov_x): if GlobalConfig.DEBUG: print( "切换 animation:", mov_x_next_animation, " accumulated_mov_x=", accumulated_mov_x ) play(mov_x_next_animation) if not velocity.y: return if flip_v: position.y -= velocity.y * delta else: position.y += velocity.y * delta # temporary set velocity func set_animation_velocity(anim: String, v: Vector2) -> void: if not animation_mov_dict.has(anim): animation_mov_dict[anim] = new_move_config() animation_mov_dict[anim].velocity = v if GlobalConfig.DEBUG: print("set_animation_velocity:", anim, v) func play_with_velocity(anim: String, velocity: Vector2) -> void: set_animation_velocity(anim, velocity) play(anim) # -1 means loop forever, 0 is invalid (which means no loop and return) func play_with_loop(anim: String, loop := -1, wait := 0.0, next := "") -> void: if loop < 0: auto_checkout_dict.erase(anim) sprite_frames.set_animation_loop(anim, true) elif loop == 0: return else: auto_checkout_dict[anim] = { "animation_intro": anim, "intro_loop": loop, "animation_wait_time": wait, "animation_next": next } # 设置完配置后播放 play(anim)