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

312 lines
8.5 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name Hook
extends Node2D
## ================
## Export Field
## ================
@export var min_length := 140.0
@export var max_length := 200.0
@export var stretching_speed: float = 1400.0
@export_category("Hook Retract")
@export var retract_speed: float = 1800.0
@onready var line_2d: Line2D = %Line2D
@onready var ray: RayCast2D = %RayCast2D
@onready var tip_detector: Area2D = $TipDetector
var _tween: Tween
const GRAPABLE_GROUP = &"GRAPABLE"
signal stretching_finished(reach_limit: bool, anchor_node: Node2D)
## 钩爪击中物体信号target 是被击中的物体hit_pos 是击中点世界坐标hook 是钩爪实例
signal hook_hit(target: Node2D, hit_pos: Vector2, hook: Hook)
## ================
## Private Field
## ================
var _binded_hook_comp
var _is_stretching := false
var _stretching_dir := Vector2.ZERO
var _cached_cancel := false
var _current_length := 0.0
var _anchor: Node2D
var _dir_id: int = -1
# =================
# Life Cycle
# =================
func _ready() -> void:
ray.enabled = true
ray.target_position = Vector2.ZERO
## 初始化
func init(hook_comp: SpawnHookComponet, reset_to_target: bool) -> void:
_binded_hook_comp = hook_comp
if reset_to_target:
global_position = hook_comp.owner.global_position
# =================
# Stretch Control
# =================
func start_stretching(direction: Vector2) -> void:
_is_stretching = true
_cached_cancel = false
_stretching_dir = direction.normalized()
_current_length = 0.0
_current_velocity = Vector2.ZERO # 重置速度
_dir_id = _get_direction_id(direction,8)
func end_stretching(force_end: bool = false) -> bool:
if not force_end and _is_stretching:
if _current_length < min_length:
_cached_cancel = true
return false
_is_stretching = false
_stretching_dir = Vector2.ZERO
_current_velocity = Vector2.ZERO # 重置速度
return true
func is_stretching() -> bool:
return _is_stretching
## 获取当前飞行方向(可被外部复写)
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)
# =================
# Update
# =================
func _physics_process(delta: float) -> void:
if _is_stretching:
_update_stretching(delta)
func _process(_delta: float) -> void:
_update_line()
# =================
# Core Logic
# =================
## 钩爪当前速度
var _current_velocity: Vector2 = Vector2.ZERO
## 最大速度上限
@export var max_speed: float = 800.0
func _update_stretching(delta: float) -> void:
# 检测前端点的吸引力 (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
next_length = min(next_length, max_length)
# 预测末端位置:起点 + 方向 * 累计长度
var predicted_pos := global_position + velocity_dir * next_length
tip_detector.global_position = predicted_pos
# RayCast 也应该射到“累计长度”
ray.target_position = velocity_dir * next_length
ray.force_raycast_update()
# ===== 命中檢測(最高優先)=====
if ray.is_colliding():
var collider := ray.get_collider()
if collider is Node2D and collider.is_in_group(GRAPABLE_GROUP):
var hit_pos := ray.get_collision_point()
_current_length = global_position.distance_to(hit_pos)
ray.target_position = velocity_dir * _current_length
_handle_hit(collider as Node2D, hit_pos)
return
# ===== 没命中,才正式推進 =====
_current_length = next_length
# 取消邏輯
if _cached_cancel and _current_length >= min_length:
stretching_finished.emit(true, null)
end_stretching(true)
return
# 超過最大距離
if _current_length >= max_length:
stretching_finished.emit(true, null)
end_stretching(true)
## 在指定位置检测吸引力,返回 {"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}
func _handle_hit(target: Node2D, hit_pos: Vector2) -> void:
_is_stretching = false
_stretching_dir = Vector2.ZERO
_current_velocity = Vector2.ZERO # 重置速度
ray.target_position = to_local(hit_pos)
# 如果 target 有 on_hook_hit 方法,调用它(传入钩爪实例)
if target.has_method(&"on_hook_hit"):
target.on_hook_hit(hit_pos, self)
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)
hook_hit.emit(target, hit_pos, self)
## 釋放鉤爪(清理 Anchor 與狀態)
func _release_hook() -> void:
# 1. 停止拉伸(保險)
_is_stretching = false
_stretching_dir = Vector2.ZERO
_current_velocity = Vector2.ZERO # 重置速度
_cached_cancel = false
_current_length = 0.0
# 2. 清掉 Anchor
if _anchor and is_instance_valid(_anchor):
# 先脫離父節點,避免殘留引用問題
_anchor.get_parent().remove_child(_anchor)
_anchor.queue_free()
_anchor = null
# 3. 重置 Ray 與 Line視覺清乾淨
ray.target_position = Vector2.ZERO
_update_line()
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
# 当前末端位置(本地坐标)
var start_pos: Vector2 = ray.target_position
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(
ray,
"target_position",
Vector2.ZERO,
duration
)
_tween.finished.connect(func():
queue_free()
)
func _update_line() -> void:
if _anchor and is_instance_valid(_anchor):
# 关键:锚点是世界坐标固定的,把它换算到 Hook 的本地坐标
ray.target_position = to_local(_anchor.global_position)
line_2d.set_point_position(1, ray.target_position)
func _create_anchor_on_node(target: Node2D, hit_global_pos: Vector2) -> Node2D:
if _anchor and is_instance_valid(_anchor):
_anchor.queue_free()
_anchor = Node2D.new()
_anchor.name = "Anchor"
target.add_child(_anchor)
# 關鍵:固定命中點偏移
_anchor.position = target.to_local(hit_global_pos)
return _anchor
## 计算方向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