godot-plateformer/addons/reedcomponent/grap_hook/garpping_hook_v_2.gd

315 lines
8.8 KiB
GDScript3
Raw Normal View History

2025-12-31 16:24:11 +08:00
class_name Hook
extends Node2D
2025-12-31 13:07:31 +08:00
2026-01-16 17:23:58 +08:00
@onready var line_2d: Line2D = %Line2D
@onready var shape_cast_2d: ShapeCast2D = %ShapeCast2D
@onready var tip_detector: Area2D = $TipDetector
2025-12-31 13:07:31 +08:00
## ================
## Export Field
## ================
2026-01-16 17:23:58 +08:00
##钩爪最短长度
2025-12-31 16:24:11 +08:00
@export var min_length := 140.0
2026-01-16 17:23:58 +08:00
##钩爪最大长度
2025-12-31 16:24:11 +08:00
@export var max_length := 200.0
2026-01-16 17:23:58 +08:00
##钩爪伸出速度
2025-12-31 16:24:11 +08:00
@export var stretching_speed: float = 1400.0
2026-01-16 17:23:58 +08:00
## 最大速度上限
@export var max_speed: float = 800.0
2025-12-31 13:07:31 +08:00
2026-01-11 00:57:27 +08:00
@export_category("Hook Retract")
@export var retract_speed: float = 1800.0
2025-12-31 13:07:31 +08:00
2026-01-16 17:23:58 +08:00
## 钩爪当前速度
var _current_velocity: Vector2 = Vector2.ZERO
2026-01-11 00:57:27 +08:00
var _tween: Tween
2025-12-31 13:07:31 +08:00
const GRAPABLE_GROUP = &"GRAPABLE"
2025-12-31 16:24:11 +08:00
signal stretching_finished(reach_limit: bool, anchor_node: Node2D)
2026-01-15 13:01:31 +08:00
## 钩爪击中物体信号target 是被击中的物体hit_pos 是击中点世界坐标hook 是钩爪实例
signal hook_hit(target: Node2D, hit_pos: Vector2, hook: Hook)
2025-12-31 13:07:31 +08:00
## ================
## Private Field
## ================
var _binded_hook_comp
2025-12-31 16:24:11 +08:00
var _is_stretching := false
var _stretching_dir := Vector2.ZERO
var _cached_cancel := false
var _current_length := 0.0
2025-12-31 13:07:31 +08:00
var _anchor: Node2D
2026-01-06 23:18:36 +08:00
var _dir_id: int = -1
2025-12-31 16:24:11 +08:00
# =================
# Life Cycle
# =================
2025-12-31 13:07:31 +08:00
func _ready() -> void:
2026-01-16 17:23:58 +08:00
shape_cast_2d.enabled = true
shape_cast_2d.target_position = Vector2.ZERO
2025-12-31 16:24:11 +08:00
## 初始化
func init(hook_comp: SpawnHookComponet, reset_to_target: bool) -> void:
2025-12-31 13:07:31 +08:00
_binded_hook_comp = hook_comp
if reset_to_target:
2025-12-31 16:24:11 +08:00
global_position = hook_comp.owner.global_position
# =================
# Stretch Control
# =================
2025-12-31 13:07:31 +08:00
func start_stretching(direction: Vector2) -> void:
_is_stretching = true
_cached_cancel = false
2025-12-31 16:24:11 +08:00
_stretching_dir = direction.normalized()
_current_length = 0.0
2026-01-15 16:09:20 +08:00
_current_velocity = Vector2.ZERO # 重置速度
2026-01-06 23:18:36 +08:00
_dir_id = _get_direction_id(direction,8)
2025-12-31 13:07:31 +08:00
func end_stretching(force_end: bool = false) -> bool:
if not force_end and _is_stretching:
2025-12-31 16:24:11 +08:00
if _current_length < min_length:
2025-12-31 13:07:31 +08:00
_cached_cancel = true
return false
2025-12-31 16:24:11 +08:00
2025-12-31 13:07:31 +08:00
_is_stretching = false
_stretching_dir = Vector2.ZERO
2026-01-15 16:09:20 +08:00
_current_velocity = Vector2.ZERO # 重置速度
2025-12-31 13:07:31 +08:00
return true
func is_stretching() -> bool:
return _is_stretching
2026-01-15 13:01:31 +08:00
## 获取当前飞行方向(可被外部复写)
func get_stretching_dir() -> Vector2:
return _stretching_dir.normalized()
## 设置飞行方向(供外部物体修改)
func set_stretching_dir(dir: Vector2) -> void:
_stretching_dir = dir.normalized()
_dir_id = _get_direction_id(_stretching_dir, 8)
2025-12-31 16:24:11 +08:00
# =================
# Update
# =================
2025-12-31 13:07:31 +08:00
func _physics_process(delta: float) -> void:
if _is_stretching:
_update_stretching(delta)
2025-12-31 16:24:11 +08:00
func _process(_delta: float) -> void:
_update_line()
# =================
# Core Logic
# =================
2025-12-31 13:07:31 +08:00
2026-01-15 16:09:20 +08:00
2025-12-31 13:07:31 +08:00
func _update_stretching(delta: float) -> void:
2026-01-15 16:09:20 +08:00
# 检测前端点的吸引力 (direction, strength)
var attract_config := _get_attraction_at(global_position + _current_velocity.normalized() * _current_length)
# 初始速度(沿当前方向持续向前)
if _current_velocity == Vector2.ZERO:
_current_velocity = get_stretching_dir() * stretching_speed
# 应用吸引力作为加速度
if attract_config.strength > 0:
var acceleration : Vector2 = attract_config.dir * attract_config.strength
_current_velocity += acceleration * delta
# 限制最大速度
if _current_velocity.length() > max_speed:
_current_velocity = _current_velocity.normalized() * max_speed
# 基于更新后的速度预测“下一帧累计长度”
var velocity_dir := _current_velocity.normalized()
var velocity_mag := _current_velocity.length()
var next_length := _current_length + velocity_mag * delta
2025-12-31 16:24:11 +08:00
next_length = min(next_length, max_length)
2026-01-15 16:09:20 +08:00
# 预测末端位置:起点 + 方向 * 累计长度
var predicted_pos := global_position + velocity_dir * next_length
tip_detector.global_position = predicted_pos
2026-01-15 13:01:31 +08:00
2026-01-16 17:23:58 +08:00
# ShapeCast2D 也应该射到"累计长度"
shape_cast_2d.target_position = velocity_dir * next_length
shape_cast_2d.force_shapecast_update()
2025-12-31 16:24:11 +08:00
# ===== 命中檢測(最高優先)=====
2026-01-16 17:23:58 +08:00
if shape_cast_2d.is_colliding():
var collider := shape_cast_2d.get_collider(0)
2025-12-31 16:24:11 +08:00
if collider is Node2D and collider.is_in_group(GRAPABLE_GROUP):
2026-01-16 17:23:58 +08:00
var hit_pos := shape_cast_2d.get_collision_point(0)
2025-12-31 16:24:11 +08:00
_current_length = global_position.distance_to(hit_pos)
2026-01-16 17:23:58 +08:00
shape_cast_2d.target_position = velocity_dir * _current_length
2025-12-31 16:24:11 +08:00
_handle_hit(collider as Node2D, hit_pos)
2025-12-31 13:07:31 +08:00
return
2025-12-31 16:24:11 +08:00
2026-01-15 16:09:20 +08:00
# ===== 没命中,才正式推進 =====
2025-12-31 16:24:11 +08:00
_current_length = next_length
# 取消邏輯
if _cached_cancel and _current_length >= min_length:
stretching_finished.emit(true, null)
2025-12-31 13:07:31 +08:00
end_stretching(true)
return
2025-12-31 16:24:11 +08:00
# 超過最大距離
if _current_length >= max_length:
stretching_finished.emit(true, null)
end_stretching(true)
2026-01-15 16:09:20 +08:00
## 在指定位置检测吸引力,返回 {"dir": Vector2, "strength": float}
func _get_attraction_at(pos: Vector2) -> Dictionary:
var total_force := Vector2.ZERO
# 检测 Areas
for area in tip_detector.get_overlapping_areas():
if area is Node2D and area.has_method("get_attraction_force"):
total_force += area.get_attraction_force(pos)
elif area.owner is Node2D and area.owner.has_method("get_attraction_force"):
total_force += area.owner.get_attraction_force(pos)
# 检测 Bodies
for body in tip_detector.get_overlapping_bodies():
if body is Node2D and body.has_method("get_attraction_force"):
total_force += body.get_attraction_force(pos)
elif body.owner is Node2D and body.owner.has_method("get_attraction_force"):
total_force += body.owner.get_attraction_force(pos)
# 返回 {"dir": direction, "strength": magnitude}
if total_force != Vector2.ZERO:
return {"dir": total_force.normalized(), "strength": total_force.length()}
return {"dir": Vector2.ZERO, "strength": 0.0}
2025-12-31 16:24:11 +08:00
func _handle_hit(target: Node2D, hit_pos: Vector2) -> void:
_is_stretching = false
_stretching_dir = Vector2.ZERO
2026-01-15 16:09:20 +08:00
_current_velocity = Vector2.ZERO # 重置速度
2025-12-31 13:07:31 +08:00
2026-01-16 17:23:58 +08:00
shape_cast_2d.target_position = to_local(hit_pos)
2025-12-31 13:07:31 +08:00
2026-01-15 13:01:31 +08:00
# 如果 target 有 on_hook_hit 方法,调用它(传入钩爪实例)
if target.has_method(&"on_hook_hit"):
2026-01-15 13:01:31 +08:00
target.on_hook_hit(hit_pos, self)
2025-12-31 16:24:11 +08:00
var reach_max := is_equal_approx(_current_length, max_length)
var anchor := _create_anchor_on_node(target, hit_pos)
stretching_finished.emit(reach_max, anchor)
2026-01-15 13:01:31 +08:00
hook_hit.emit(target, hit_pos, self)
2025-12-31 16:24:11 +08:00
## 釋放鉤爪(清理 Anchor 與狀態)
2026-01-11 00:57:27 +08:00
func _release_hook() -> void:
2025-12-31 16:24:11 +08:00
# 1. 停止拉伸(保險)
_is_stretching = false
_stretching_dir = Vector2.ZERO
2026-01-15 16:09:20 +08:00
_current_velocity = Vector2.ZERO # 重置速度
2025-12-31 16:24:11 +08:00
_cached_cancel = false
_current_length = 0.0
# 2. 清掉 Anchor
2025-12-31 13:07:31 +08:00
if _anchor and is_instance_valid(_anchor):
2025-12-31 16:24:11 +08:00
# 先脫離父節點,避免殘留引用問題
_anchor.get_parent().remove_child(_anchor)
2025-12-31 13:07:31 +08:00
_anchor.queue_free()
2025-12-31 16:24:11 +08:00
_anchor = null
2025-12-31 13:07:31 +08:00
2026-01-16 17:23:58 +08:00
# 3. 重置 ShapeCast2D 與 Line視覺清乾淨
shape_cast_2d.target_position = Vector2.ZERO
2025-12-31 16:24:11 +08:00
_update_line()
2025-12-31 13:07:31 +08:00
2026-01-11 00:57:27 +08:00
func release_hook_with_transition(has_trans: bool) -> void:
# 先停止一切拉伸逻辑
_is_stretching = false
_stretching_dir = Vector2.ZERO
_cached_cancel = false
# 如果不需要动画,直接释放
if not has_trans:
_release_hook()
queue_free()
return
# ===== 需要回收动画 =====
# 先清 Anchor但不影响末端视觉
if _anchor and is_instance_valid(_anchor):
_anchor.get_parent().remove_child(_anchor)
_anchor.queue_free()
_anchor = null
# 当前末端位置(本地坐标)
2026-01-16 17:23:58 +08:00
var start_pos: Vector2 = shape_cast_2d.target_position
2026-01-11 00:57:27 +08:00
var distance := start_pos.length()
if distance <= 0.001:
queue_free()
return
# 根据速度算 tween 时间
var duration := distance / retract_speed
if _tween and _tween.is_valid():
_tween.kill()
_tween = get_tree().create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN)
_tween.tween_property(
2026-01-16 17:23:58 +08:00
shape_cast_2d,
2026-01-11 00:57:27 +08:00
"target_position",
Vector2.ZERO,
duration
)
_tween.finished.connect(func():
queue_free()
)
2025-12-31 16:24:11 +08:00
func _update_line() -> void:
2026-01-11 00:57:27 +08:00
if _anchor and is_instance_valid(_anchor):
# 关键:锚点是世界坐标固定的,把它换算到 Hook 的本地坐标
2026-01-16 17:23:58 +08:00
shape_cast_2d.target_position = to_local(_anchor.global_position)
2026-01-11 00:57:27 +08:00
2026-01-16 17:23:58 +08:00
line_2d.set_point_position(1, shape_cast_2d.target_position)
2025-12-31 13:07:31 +08:00
2025-12-31 16:24:11 +08:00
func _create_anchor_on_node(target: Node2D, hit_global_pos: Vector2) -> Node2D:
if _anchor and is_instance_valid(_anchor):
_anchor.queue_free()
2025-12-31 13:07:31 +08:00
2025-12-31 16:24:11 +08:00
_anchor = Node2D.new()
_anchor.name = "Anchor"
target.add_child(_anchor)
2025-12-31 13:07:31 +08:00
2025-12-31 16:24:11 +08:00
# 關鍵:固定命中點偏移
_anchor.position = target.to_local(hit_global_pos)
2025-12-31 13:07:31 +08:00
return _anchor
2026-01-06 23:18:36 +08:00
## 计算方向id私有
func _get_direction_id(direction: Vector2, sector_count: int) -> int:
if direction == Vector2.ZERO:
return -1
var angle = rad_to_deg(direction.angle())
angle += 90.0
var sector_size = 360.0 / sector_count
angle += sector_size / 2.0
angle = fposmod(angle, 360.0)
return int(angle / sector_size)
## 获取方向id
func get_direction_id() -> int:
return _dir_id