221 lines
5.5 KiB
GDScript
221 lines
5.5 KiB
GDScript
@tool
|
|
|
|
# A draggable item in the game that can be picked up and displayed with an outline effect.
|
|
class_name DraggableRigid extends RigidBody2D
|
|
|
|
# pass self
|
|
signal picked(node: DraggableRigid)
|
|
signal dropped(node: DraggableRigid)
|
|
|
|
# 如果设置 replaced_area_monitor, 则鼠标事件由 replaced_area_monitor 替代监听
|
|
@export var replaced_area_monitor: Area2D
|
|
# z +1 when picked (-1 when dropped)
|
|
# @export var z_up_on_picked := true
|
|
@export var sprite_offset := Vector2.ZERO:
|
|
set(val):
|
|
sprite_offset = val
|
|
if is_node_ready():
|
|
sprite.offset = sprite_offset
|
|
@export var freezing := false:
|
|
set(val):
|
|
freezing = val
|
|
if freezing:
|
|
if holding:
|
|
_drop()
|
|
elif touching:
|
|
_on_mouse_exited()
|
|
@export var texture: Texture2D:
|
|
# The texture to display for the item
|
|
set(val):
|
|
texture = val
|
|
if is_node_ready():
|
|
sprite.texture = texture
|
|
@export var limit_polygon: Polygon2D
|
|
|
|
@onready var sprite: Sprite2D = $Sprite2D
|
|
|
|
# Whether the item is currently being held by the player
|
|
var holding := false:
|
|
set(val):
|
|
if holding != val:
|
|
holding = val
|
|
freeze = val
|
|
var touching := false
|
|
|
|
# 静态变量优化:避免频繁的字符串比较
|
|
static var current_focusing_node: DraggableRigid = null
|
|
static var pending_enter_callables: Array[Callable] = []
|
|
|
|
# 缓存常量
|
|
const OUTLINE_THICKNESS := 1.0
|
|
const OUTLINE_TWEEN_DURATION := 0.2
|
|
const SHAKE_FPS := 15.0
|
|
const SHAKE_DURATION := 0.8
|
|
const SHAKE_DELTA := 12.0
|
|
const HUD_FADE_DURATION := 0.3
|
|
|
|
# 缓存变量
|
|
var _mouse_event_source: Node
|
|
var _outline_tween: Tween
|
|
var _shake_tween: Tween
|
|
|
|
func _ready() -> void:
|
|
sprite.texture = texture
|
|
sprite.offset = sprite_offset
|
|
if Engine.is_editor_hint():
|
|
return
|
|
# 初始化隐藏白边
|
|
sprite.material.set_shader_parameter("thickness", 0.0)
|
|
|
|
# 缓存事件源
|
|
if replaced_area_monitor:
|
|
_mouse_event_source = replaced_area_monitor
|
|
else:
|
|
_mouse_event_source = self
|
|
_mouse_event_source.mouse_entered.connect(_on_mouse_entered)
|
|
_mouse_event_source.mouse_exited.connect(_on_mouse_exited)
|
|
|
|
|
|
func is_focused() -> bool:
|
|
return current_focusing_node == self
|
|
|
|
|
|
func _on_mouse_entered() -> bool:
|
|
touching = true
|
|
if freezing or not is_visible_in_tree():
|
|
return false
|
|
if holding or is_focused():
|
|
return true
|
|
|
|
# 尝试获得 current_focusing_item
|
|
if current_focusing_node:
|
|
if not pending_enter_callables.has(_on_mouse_entered):
|
|
pending_enter_callables.append(_on_mouse_entered)
|
|
return false
|
|
|
|
current_focusing_node = self
|
|
_toggle_outline(true)
|
|
return true
|
|
|
|
|
|
func _on_mouse_exited() -> void:
|
|
touching = false
|
|
pending_enter_callables.erase(_on_mouse_entered)
|
|
# freezing 不影响 mouse exited
|
|
if is_focused() and not holding:
|
|
current_focusing_node = null
|
|
# 优化:使用 while 替代 for 循环,找到第一个成功的就停止
|
|
while pending_enter_callables.size() > 0:
|
|
var c = pending_enter_callables.pop_front()
|
|
if c.call():
|
|
break
|
|
_toggle_outline(false)
|
|
|
|
|
|
func _notification(what: int) -> void:
|
|
match what:
|
|
NOTIFICATION_PREDELETE:
|
|
if holding:
|
|
_drop()
|
|
elif touching:
|
|
_on_mouse_exited()
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if freezing or Engine.is_editor_hint() or not is_visible_in_tree():
|
|
return
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
|
if is_focused() and not holding:
|
|
get_viewport().set_input_as_handled()
|
|
_try_pick.call_deferred()
|
|
elif holding:
|
|
get_viewport().set_input_as_handled()
|
|
_drop()
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
if holding:
|
|
var target = get_global_mouse_position()
|
|
if limit_polygon:
|
|
target = Util.get_closest_point_on_polygon(target, limit_polygon.polygon)
|
|
# PhysicsServer2D.body_set_state(
|
|
# get_rid(), PhysicsServer2D.BODY_STATE_TRANSFORM, Transform2D.IDENTITY.translated(target)
|
|
# )
|
|
global_position = target
|
|
|
|
|
|
func _try_pick() -> void:
|
|
if not is_focused():
|
|
return
|
|
_toggle_outline(false)
|
|
holding = true
|
|
picked.emit(self)
|
|
|
|
|
|
func _drop() -> void:
|
|
if touching:
|
|
_toggle_outline(true)
|
|
if holding:
|
|
holding = false
|
|
if not touching:
|
|
current_focusing_node = null
|
|
for c in pending_enter_callables:
|
|
if c.call():
|
|
break
|
|
dropped.emit(self)
|
|
|
|
|
|
func _toggle_outline(display: bool) -> void:
|
|
# 避免重复创建 tween
|
|
if _outline_tween and _outline_tween.is_running():
|
|
_outline_tween.kill()
|
|
|
|
_outline_tween = create_tween()
|
|
var target_thickness := OUTLINE_THICKNESS if display else 0.0
|
|
_outline_tween.tween_property(
|
|
sprite.material, "shader_parameter/thickness", target_thickness, OUTLINE_TWEEN_DURATION
|
|
)
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
if touching:
|
|
_on_mouse_exited()
|
|
|
|
|
|
func force_hold() -> void:
|
|
if holding:
|
|
return
|
|
if not is_focused() and current_focusing_node:
|
|
current_focusing_node._drop()
|
|
_toggle_outline(false)
|
|
current_focusing_node = self
|
|
holding = true
|
|
picked.emit(self)
|
|
|
|
|
|
func invalid_shake() -> void:
|
|
if _shake_tween and _shake_tween.is_running():
|
|
_shake_tween.kill()
|
|
# 抖动效果
|
|
_shake_tween = create_tween()
|
|
var count := int(SHAKE_DURATION * SHAKE_FPS)
|
|
var delta_t := 1.0 / SHAKE_FPS
|
|
sprite.modulate = Color.INDIAN_RED
|
|
# 预计算随机值,避免在循环中多次调用 randf_range
|
|
for i in count:
|
|
var decay := exp(-float(i))
|
|
var offset := Vector2(
|
|
randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay,
|
|
randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay
|
|
)
|
|
_shake_tween.tween_property(sprite, "offset", sprite_offset + offset, delta_t).set_trans(
|
|
Tween.TRANS_CUBIC
|
|
)
|
|
|
|
_shake_tween.tween_callback(_reset_after_shake)
|
|
|
|
|
|
func _reset_after_shake() -> void:
|
|
sprite.modulate = Color.WHITE
|
|
sprite.offset = sprite_offset
|