Initial commit: Godot 4.7 pixel platformer prototype

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).
This commit is contained in:
2026-10-07 20:32:42 +08:00
commit 9dffacd938
558 changed files with 12258 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
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)