9dffacd938
Player: move/jump (Space), test flight (F, debug builds only). Key: pickup, hold S to aim with live trajectory preview, release to throw, W to warp to the thrown key. Spring bounce. Spikes deal damage. 3-heart health, Z consumes a heal item for +1 heart and a 5s shield. HUD with hearts and item count. Death screen with fade-to-black and restart. Hidden passages. Assets: Kenney Pixel Platformer (CC0).
126 lines
3.6 KiB
GDScript
126 lines
3.6 KiB
GDScript
extends Area2D
|
||
|
||
signal collected
|
||
|
||
## 钥匙中心到"脚底"的距离:射线从脚底出发,落地时钥匙正好站在表面上
|
||
const FOOT_OFFSET := 7.0
|
||
const STEP := 1.0 / 60.0
|
||
const MAX_STEPS := 240
|
||
|
||
enum State { IDLE, THROWN, LANDED }
|
||
|
||
## 关卡里那把钥匙原本是用 TerrainBase 的一块地砖画的。
|
||
## 这个开关让钥匙对象在开场时把脚下那块占位地砖擦掉,避免两个钥匙重叠。
|
||
@export var erase_placeholder_on_ready: bool = true
|
||
## 落地后多久才允许再次被捡起
|
||
@export var settle_time: float = 0.4
|
||
## 飞行时用哪几层做落地判定。玩家投掷时会把它设成玩家自己的 collision_mask,
|
||
## 所以"钥匙能落在哪"严格等于"玩家能站在哪"。
|
||
@export var flight_mask: int = 1
|
||
|
||
var state: State = State.IDLE
|
||
var velocity := Vector2.ZERO
|
||
|
||
var _settle := 0.0
|
||
|
||
|
||
func _ready() -> void:
|
||
body_entered.connect(_on_body_entered)
|
||
if erase_placeholder_on_ready:
|
||
_erase_placeholder_tile()
|
||
_start_idle()
|
||
|
||
|
||
func _physics_process(_delta: float) -> void:
|
||
if state == State.LANDED:
|
||
_settle += STEP
|
||
if _settle >= settle_time:
|
||
_start_idle()
|
||
return
|
||
if state != State.THROWN:
|
||
return
|
||
|
||
var g := gravity()
|
||
var motion := velocity * STEP
|
||
var from := global_position + Vector2(0.0, FOOT_OFFSET)
|
||
var query := PhysicsRayQueryParameters2D.create(from, from + motion, flight_mask)
|
||
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
||
if hit.is_empty():
|
||
global_position += motion
|
||
velocity.y += g * STEP
|
||
else:
|
||
global_position = hit["position"] - Vector2(0.0, FOOT_OFFSET)
|
||
velocity = Vector2.ZERO
|
||
state = State.LANDED
|
||
_settle = 0.0
|
||
|
||
|
||
# ---------------------------------------------------------------- 对外接口
|
||
|
||
func launch(v: Vector2) -> void:
|
||
velocity = v
|
||
state = State.THROWN
|
||
monitoring = false
|
||
_settle = 0.0
|
||
|
||
|
||
func _start_idle() -> void:
|
||
state = State.IDLE
|
||
velocity = Vector2.ZERO
|
||
monitoring = true
|
||
|
||
|
||
func _on_body_entered(body: Node2D) -> void:
|
||
if state != State.IDLE:
|
||
return
|
||
if body.has_method("collect_key"):
|
||
body.collect_key()
|
||
collected.emit()
|
||
queue_free()
|
||
|
||
|
||
# ---------------------------------------------------------------- 轨迹模拟
|
||
|
||
static func gravity() -> float:
|
||
return float(ProjectSettings.get_setting("physics/2d/default_gravity", 980.0))
|
||
|
||
|
||
## 和 _physics_process 里的推进方式完全一致,玩家用它来画预览弧线,
|
||
## 所以预览落点和真正落点会重合。mask 就是玩家的碰撞掩码。
|
||
static func simulate_trajectory(
|
||
space: PhysicsDirectSpaceState2D,
|
||
start: Vector2,
|
||
vel: Vector2,
|
||
mask: int,
|
||
max_steps: int = MAX_STEPS) -> Dictionary:
|
||
var points := PackedVector2Array()
|
||
var p := start
|
||
var v := vel
|
||
var g := gravity()
|
||
points.append(p)
|
||
for i in max_steps:
|
||
var next := p + v * STEP
|
||
var query := PhysicsRayQueryParameters2D.create(
|
||
p + Vector2(0.0, FOOT_OFFSET), next + Vector2(0.0, FOOT_OFFSET), mask)
|
||
var hit := space.intersect_ray(query)
|
||
if not hit.is_empty():
|
||
var landing: Vector2 = hit["position"] - Vector2(0.0, FOOT_OFFSET)
|
||
points.append(landing)
|
||
return {"points": points, "landed": true, "landing": landing}
|
||
p = next
|
||
v.y += g * STEP
|
||
points.append(p)
|
||
return {"points": points, "landed": false, "landing": p}
|
||
|
||
|
||
# ---------------------------------------------------------------- 清理占位地砖
|
||
|
||
func _erase_placeholder_tile() -> void:
|
||
var root: Node = owner if owner != null else get_parent()
|
||
if root == null:
|
||
return
|
||
for layer in root.find_children("*", "TileMapLayer", true, false):
|
||
var cell: Vector2i = layer.local_to_map(layer.to_local(global_position))
|
||
if layer.get_cell_source_id(cell) != -1:
|
||
layer.erase_cell(cell)
|