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:
@@ -0,0 +1,405 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
signal died
|
||||
|
||||
const KEY_SCENE := preload("res://scenes/prefabs/collectibles/key_gold.tscn")
|
||||
const KEY_SCRIPT := preload("res://scripts/key.gd")
|
||||
const KEY_ATLAS := preload("res://assets/game/tilesets/kenney_tiles_atlas.png")
|
||||
const KEY_REGION := Rect2(126, 18, 18, 18)
|
||||
|
||||
@export_category("移动参数")
|
||||
@export var move_speed: float = 75.0
|
||||
@export var acceleration: float = 600.0
|
||||
@export var deceleration: float = 800.0
|
||||
@export var jump_velocity: float = -190.0
|
||||
|
||||
@export_category("生命值")
|
||||
## 最大生命值(几颗心)
|
||||
@export var max_health: int = 3
|
||||
## 按 Z 用掉一个回血道具后,获得的无敌盾持续时间(秒)
|
||||
@export var shield_time: float = 5.0
|
||||
## 受伤后的无敌时间,避免站在尖刺上反复触发
|
||||
@export var invulnerable_time: float = 1.0
|
||||
## 受伤瞬间的染色
|
||||
@export var hurt_tint: Color = Color(1.0, 0.35, 0.35)
|
||||
@export var hurt_flash_time: float = 0.25
|
||||
## 有护盾时的染色
|
||||
@export var shield_tint: Color = Color(0.6, 0.95, 1.0, 1.0)
|
||||
## 死亡后的染色
|
||||
@export var dead_tint: Color = Color(0.85, 0.3, 0.3, 1.0)
|
||||
|
||||
@export_category("钥匙投掷")
|
||||
## 投掷初速度大小
|
||||
@export var throw_speed: float = 300.0
|
||||
## 投掷仰角(度)
|
||||
@export_range(0.0, 89.0, 1.0) var throw_angle_deg: float = 60.0
|
||||
## 预测弧线颜色
|
||||
@export var arc_color: Color = Color(1.0, 0.94, 0.45, 0.85)
|
||||
@export var arc_width: float = 1.5
|
||||
## 拿着钥匙时头顶显示一个小图标(不想显示就关掉)
|
||||
@export var show_carried_key: bool = true
|
||||
|
||||
@export_category("测试用飞行")
|
||||
## 开发期测试用:按 F 切换飞行,方便飞到各处查看碰撞效果。
|
||||
## 只在编辑器 / 调试版里生效;导出的正式版里按键无效,不会影响平衡。
|
||||
@export var debug_fly: bool = true
|
||||
@export var fly_speed: float = 90.0
|
||||
## 飞行时的角色染色,用来一眼看出飞行开着(不想染色就改成白色)
|
||||
@export var fly_tint: Color = Color(0.65, 0.9, 1.0)
|
||||
|
||||
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
@onready var collider: CollisionShape2D = $CollisionShape2D
|
||||
|
||||
var flying: bool = false
|
||||
var facing: int = 1
|
||||
var held_key: bool = false
|
||||
var is_aiming: bool = false
|
||||
var hurt_count: int = 0
|
||||
## 当前生命值(几颗心)
|
||||
var health: int = 3
|
||||
## 身上的回血道具数量(捡到的小球)
|
||||
var heal_items: int = 0
|
||||
## 无敌盾剩余时间,给 HUD 显示用
|
||||
var shield_left: float = 0.0
|
||||
## 生命值归零后进入死亡状态:不能操作,交给死亡界面
|
||||
var is_dead: bool = false
|
||||
|
||||
var _arc: Line2D = null
|
||||
var _landing_marker: Polygon2D = null
|
||||
var _carried_icon: Sprite2D = null
|
||||
## 最近一次扔出去、还没被回收的钥匙
|
||||
var _thrown_key: Node = null
|
||||
## 角色原点到底部的距离,传送时用来对齐脚底
|
||||
var _feet_offset: float = 10.5
|
||||
## 出生点,受伤后回到这里
|
||||
var _spawn_point := Vector2.ZERO
|
||||
var _invulnerable: float = 0.0
|
||||
var _hurt_flash: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
_build_throw_visuals()
|
||||
_measure_feet_offset()
|
||||
_spawn_point = global_position
|
||||
health = max_health
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if _invulnerable > 0.0:
|
||||
_invulnerable -= delta
|
||||
if _hurt_flash > 0.0:
|
||||
_hurt_flash -= delta
|
||||
if shield_left > 0.0:
|
||||
shield_left -= delta
|
||||
|
||||
if is_dead:
|
||||
# 死亡后不接受任何操作,只让重力把人放下来
|
||||
apply_gravity(delta)
|
||||
velocity.x = move_toward(velocity.x, 0.0, deceleration * delta)
|
||||
_update_tint()
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
# 正式版里不允许飞行;即使 toggle 开着也强制落回地面
|
||||
if flying and not fly_available():
|
||||
set_flying(false)
|
||||
|
||||
if flying:
|
||||
handle_fly_movement()
|
||||
else:
|
||||
apply_gravity(delta)
|
||||
handle_jump()
|
||||
handle_horizontal_movement(delta)
|
||||
|
||||
update_sprite_direction()
|
||||
_update_tint()
|
||||
if is_aiming:
|
||||
update_arc()
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
if fly_available() and event.is_action_pressed("fly") and not event.is_echo():
|
||||
set_flying(not flying)
|
||||
return
|
||||
|
||||
if event.is_action_pressed("use_heal") and not event.is_echo():
|
||||
use_heal_item()
|
||||
return
|
||||
|
||||
if event.is_action_pressed("warp_to_key") and not event.is_echo():
|
||||
warp_to_thrown_key()
|
||||
return
|
||||
|
||||
if event.is_action_pressed("squat") and not event.is_echo():
|
||||
if can_aim():
|
||||
is_aiming = true
|
||||
update_arc()
|
||||
elif event.is_action_released("squat"):
|
||||
if is_aiming:
|
||||
release_throw()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 生命值
|
||||
|
||||
## 由尖刺之类的危险物调用
|
||||
func hurt(_amount: int = 1) -> void:
|
||||
if is_dead or _invulnerable > 0.0:
|
||||
return
|
||||
hurt_count += 1
|
||||
health -= 1
|
||||
_invulnerable = invulnerable_time
|
||||
_hurt_flash = hurt_flash_time
|
||||
if health <= 0:
|
||||
health = 0
|
||||
_die()
|
||||
return
|
||||
# 没死:打回出生点
|
||||
global_position = _spawn_point
|
||||
velocity = Vector2.ZERO
|
||||
if flying:
|
||||
set_flying(false)
|
||||
if is_aiming:
|
||||
_cancel_aim()
|
||||
|
||||
|
||||
func _die() -> void:
|
||||
is_dead = true
|
||||
velocity = Vector2.ZERO
|
||||
if flying:
|
||||
set_flying(false)
|
||||
if is_aiming:
|
||||
_cancel_aim()
|
||||
died.emit()
|
||||
|
||||
|
||||
## 由可收集物调用
|
||||
func add_heal_item() -> void:
|
||||
heal_items += 1
|
||||
|
||||
|
||||
## 按 Z:消耗一个回血道具,回一颗心并获得无敌盾
|
||||
func use_heal_item() -> bool:
|
||||
if is_dead or heal_items <= 0:
|
||||
return false
|
||||
heal_items -= 1
|
||||
health = mini(health + 1, max_health)
|
||||
_invulnerable = maxf(_invulnerable, shield_time)
|
||||
shield_left = shield_time
|
||||
return true
|
||||
|
||||
|
||||
func _update_tint() -> void:
|
||||
if is_dead:
|
||||
sprite.modulate = dead_tint
|
||||
elif _hurt_flash > 0.0:
|
||||
sprite.modulate = hurt_tint
|
||||
elif shield_left > 0.0:
|
||||
sprite.modulate = shield_tint
|
||||
elif flying:
|
||||
sprite.modulate = fly_tint
|
||||
else:
|
||||
sprite.modulate = Color.WHITE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 钥匙投掷
|
||||
|
||||
func can_aim() -> bool:
|
||||
return held_key and not flying and not is_aiming
|
||||
|
||||
|
||||
## 由钥匙对象在碰到玩家时调用
|
||||
func collect_key() -> void:
|
||||
held_key = true
|
||||
_update_carried_icon()
|
||||
|
||||
|
||||
func throw_velocity() -> Vector2:
|
||||
var rad := deg_to_rad(throw_angle_deg)
|
||||
return Vector2(facing * throw_speed * cos(rad), -throw_speed * sin(rad))
|
||||
|
||||
|
||||
func update_arc() -> void:
|
||||
if not is_aiming or _arc == null:
|
||||
return
|
||||
# 用玩家自己的 collision_mask 做预测,所以"弧线能落到的地方"就是"玩家能站的地方"
|
||||
var result := KEY_SCRIPT.simulate_trajectory(
|
||||
get_world_2d().direct_space_state,
|
||||
global_position,
|
||||
throw_velocity(),
|
||||
collision_mask)
|
||||
_arc.points = result["points"]
|
||||
_arc.visible = _arc.points.size() > 1
|
||||
var landed: bool = result["landed"]
|
||||
if _landing_marker != null:
|
||||
_landing_marker.visible = landed
|
||||
if landed:
|
||||
_landing_marker.global_position = result["landing"]
|
||||
|
||||
|
||||
func release_throw() -> void:
|
||||
is_aiming = false
|
||||
_hide_throw_visuals()
|
||||
if not held_key:
|
||||
return
|
||||
|
||||
held_key = false
|
||||
_update_carried_icon()
|
||||
|
||||
var key: Node = KEY_SCENE.instantiate()
|
||||
# 这是玩家扔出去的钥匙,不要去擦关卡里的占位地砖
|
||||
key.erase_placeholder_on_ready = false
|
||||
# 让钥匙的落地判定和玩家完全一致
|
||||
key.flight_mask = collision_mask
|
||||
get_parent().add_child(key)
|
||||
key.global_position = global_position
|
||||
key.launch(throw_velocity())
|
||||
_thrown_key = key
|
||||
|
||||
|
||||
## 按 W:传送到扔出去的那把钥匙所在的位置,并顺手把它捡回来
|
||||
func warp_to_thrown_key() -> void:
|
||||
if _thrown_key == null or not is_instance_valid(_thrown_key):
|
||||
return
|
||||
|
||||
var key_pos: Vector2 = _thrown_key.global_position
|
||||
# 让玩家的脚底落在钥匙脚底所在的那个面上,避免直接嵌进地形
|
||||
global_position = Vector2(key_pos.x, key_pos.y + KEY_SCRIPT.FOOT_OFFSET - _feet_offset)
|
||||
velocity = Vector2.ZERO
|
||||
|
||||
if is_aiming:
|
||||
_cancel_aim()
|
||||
|
||||
_thrown_key.queue_free()
|
||||
_thrown_key = null
|
||||
held_key = true
|
||||
_update_carried_icon()
|
||||
|
||||
|
||||
func _cancel_aim() -> void:
|
||||
is_aiming = false
|
||||
_hide_throw_visuals()
|
||||
|
||||
|
||||
func _hide_throw_visuals() -> void:
|
||||
if _arc != null:
|
||||
_arc.visible = false
|
||||
if _landing_marker != null:
|
||||
_landing_marker.visible = false
|
||||
|
||||
|
||||
func _update_carried_icon() -> void:
|
||||
if _carried_icon != null:
|
||||
_carried_icon.visible = held_key
|
||||
|
||||
|
||||
func _measure_feet_offset() -> void:
|
||||
if collider == null or collider.shape == null:
|
||||
return
|
||||
var shape := collider.shape
|
||||
if shape is CapsuleShape2D:
|
||||
_feet_offset = collider.position.y + shape.height * 0.5
|
||||
elif shape is RectangleShape2D:
|
||||
_feet_offset = collider.position.y + shape.size.y * 0.5
|
||||
elif shape is CircleShape2D:
|
||||
_feet_offset = collider.position.y + shape.radius
|
||||
|
||||
|
||||
func _build_throw_visuals() -> void:
|
||||
# top_level = true 让它们忽略角色自身那个 rotation/scale 翻转,直接用世界坐标
|
||||
_arc = Line2D.new()
|
||||
_arc.width = arc_width
|
||||
_arc.default_color = arc_color
|
||||
_arc.visible = false
|
||||
add_child(_arc)
|
||||
_arc.top_level = true
|
||||
|
||||
_landing_marker = Polygon2D.new()
|
||||
var s := 4.0
|
||||
_landing_marker.polygon = PackedVector2Array([
|
||||
Vector2(0.0, -s), Vector2(s, 0.0), Vector2(0.0, s), Vector2(-s, 0.0)])
|
||||
_landing_marker.color = arc_color
|
||||
_landing_marker.visible = false
|
||||
add_child(_landing_marker)
|
||||
_landing_marker.top_level = true
|
||||
|
||||
if show_carried_key:
|
||||
var tex := AtlasTexture.new()
|
||||
tex.atlas = KEY_ATLAS
|
||||
tex.region = KEY_REGION
|
||||
_carried_icon = Sprite2D.new()
|
||||
_carried_icon.texture = tex
|
||||
_carried_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_carried_icon.position = Vector2(0.0, -15.0)
|
||||
_carried_icon.visible = false
|
||||
add_child(_carried_icon)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 测试用飞行
|
||||
|
||||
func fly_available() -> bool:
|
||||
return debug_fly and OS.is_debug_build()
|
||||
|
||||
|
||||
func set_flying(on: bool) -> void:
|
||||
flying = on
|
||||
velocity = Vector2.ZERO
|
||||
if on:
|
||||
_cancel_aim()
|
||||
|
||||
|
||||
func handle_fly_movement() -> void:
|
||||
# get_axis(负方向, 正方向),屏幕坐标里 y 向下为正,所以空格(跳跃)要放在负方向
|
||||
var direction := Vector2(
|
||||
Input.get_axis("move_left", "move_right"),
|
||||
Input.get_axis("jump", "squat")
|
||||
)
|
||||
if direction == Vector2.ZERO:
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
velocity = direction.normalized() * fly_speed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 正常移动
|
||||
|
||||
func apply_gravity(delta: float) -> void:
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
|
||||
|
||||
func handle_horizontal_movement(delta: float) -> void:
|
||||
var direction := Input.get_axis("move_left", "move_right")
|
||||
var target_speed := direction * move_speed
|
||||
|
||||
if direction != 0.0:
|
||||
velocity.x = move_toward(
|
||||
velocity.x,
|
||||
target_speed,
|
||||
acceleration * delta
|
||||
)
|
||||
else:
|
||||
velocity.x = move_toward(
|
||||
velocity.x,
|
||||
0.0,
|
||||
deceleration * delta
|
||||
)
|
||||
|
||||
|
||||
func handle_jump() -> void:
|
||||
# 只有踩在地面上才能起跳 —— 空中不再给第二次机会,不做多段跳
|
||||
if Input.is_action_just_pressed("jump") and is_on_floor():
|
||||
velocity.y = jump_velocity
|
||||
|
||||
|
||||
func update_sprite_direction() -> void:
|
||||
if velocity.x > 0.0:
|
||||
facing = 1
|
||||
sprite.flip_h = false
|
||||
elif velocity.x < 0.0:
|
||||
facing = -1
|
||||
sprite.flip_h = true
|
||||
Reference in New Issue
Block a user