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:
+125
@@ -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)
|
||||
Reference in New Issue
Block a user