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,14 @@
|
||||
extends Area2D
|
||||
|
||||
## 走到就捡走,给玩家增加一次"回血道具"。
|
||||
## 碰撞掩码已经限制成只检测玩家那一层,所以进到这里的一定是玩家。
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
if body.has_method("add_heal_item"):
|
||||
body.add_heal_item()
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcemiea0m0t48
|
||||
@@ -0,0 +1,64 @@
|
||||
extends CanvasLayer
|
||||
|
||||
## 生命值归零后的死亡界面:画面渐黑 + 重新开始按钮。
|
||||
## 只负责显示和重启,死亡状态由玩家(is_dead)决定。
|
||||
|
||||
signal restart_requested
|
||||
|
||||
@export var fade_time: float = 1.0
|
||||
## 渐黑完成后再过多久显示标题和按钮
|
||||
@export var button_delay: float = 0.4
|
||||
|
||||
var _player: Node = null
|
||||
var _fade: ColorRect = null
|
||||
var _center: CenterContainer = null
|
||||
var _button: Button = null
|
||||
var _elapsed: float = 0.0
|
||||
var _dying: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
_fade = get_node_or_null("Fade")
|
||||
_center = get_node_or_null("Center")
|
||||
_button = get_node_or_null("Center/VBox/RestartButton")
|
||||
if _button != null:
|
||||
_button.pressed.connect(restart)
|
||||
if _fade != null:
|
||||
_fade.color = Color(0.0, 0.0, 0.0, 0.0)
|
||||
if _center != null:
|
||||
_center.visible = false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _dying:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
if _player == null:
|
||||
return
|
||||
if bool(_player.is_dead):
|
||||
_dying = true
|
||||
visible = true
|
||||
else:
|
||||
return
|
||||
|
||||
_elapsed += delta
|
||||
if _fade != null:
|
||||
_fade.color = Color(0.0, 0.0, 0.0, clampf(_elapsed / maxf(fade_time, 0.01), 0.0, 1.0))
|
||||
if _center != null and _elapsed >= fade_time + button_delay:
|
||||
_center.visible = true
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not _dying:
|
||||
return
|
||||
var key := event as InputEventKey
|
||||
if key != null and key.pressed and not key.echo:
|
||||
if key.physical_keycode == KEY_R or key.physical_keycode == KEY_ENTER:
|
||||
restart()
|
||||
|
||||
|
||||
func restart() -> void:
|
||||
restart_requested.emit()
|
||||
get_tree().paused = false
|
||||
get_tree().reload_current_scene()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b31yxkyym11xb
|
||||
@@ -0,0 +1,14 @@
|
||||
extends Area2D
|
||||
|
||||
## 尖刺:碰到就把角色打回出生点。
|
||||
## 碰撞掩码已经限制成只检测玩家那一层,所以进到这里的一定是玩家。
|
||||
@export var damage: int = 1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
if body.has_method("hurt"):
|
||||
body.hurt(damage)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkhda0ohbgla2
|
||||
@@ -0,0 +1,83 @@
|
||||
extends Node2D
|
||||
|
||||
const ATLAS := preload("res://assets/game/tilesets/kenney_tiles_atlas.png")
|
||||
const TILE := 18
|
||||
|
||||
## 隐藏通道:下面这些格子在关卡开始时会被"挖空",
|
||||
## 原本是实心石壁的地方就变成能穿过去的暗道。
|
||||
## 坐标是格坐标(格边长 18px),改这里就能挪位置。
|
||||
@export var hidden_cells: Array = [
|
||||
# ① 主线走廊上的石柱(第 13 列第 6~7 行)
|
||||
Vector2i(13, 6), Vector2i(13, 7),
|
||||
|
||||
# ② 低层暗道(第 11 行第 12~15 列)
|
||||
Vector2i(12, 11), Vector2i(13, 11), Vector2i(14, 11), Vector2i(15, 11),
|
||||
]
|
||||
|
||||
## 伪装贴图所在层:要低于角色(z=10)、高于地形(z=0~2)
|
||||
@export var cover_z_index: int = 3
|
||||
## 是否给洞口盖上"假墙"(关掉的话洞口会明显地露出来)
|
||||
@export var hide_with_cover: bool = true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var layers := _tile_layers()
|
||||
if layers.is_empty():
|
||||
return
|
||||
var base: TileMapLayer = layers[0]
|
||||
|
||||
# 先把盖子做好(此时参考的那块墙还在),再挖空
|
||||
var covers: Array = []
|
||||
for cell in hidden_cells:
|
||||
covers.append([cell, _make_cover(cell) if hide_with_cover else null])
|
||||
|
||||
for cell in hidden_cells:
|
||||
for layer in layers:
|
||||
layer.erase_cell(cell)
|
||||
|
||||
for pair in covers:
|
||||
var sprite: Sprite2D = pair[1]
|
||||
if sprite != null:
|
||||
add_child(sprite)
|
||||
sprite.global_position = base.to_global(base.map_to_local(pair[0]))
|
||||
|
||||
|
||||
func _tile_layers() -> Array:
|
||||
var out: Array = []
|
||||
for child in get_children():
|
||||
if child is TileMapLayer:
|
||||
out.append(child)
|
||||
return out
|
||||
|
||||
|
||||
## 借用正下方最近一块实心墙的贴图,做成一个完全没有碰撞的 Sprite2D
|
||||
func _make_cover(cell: Vector2i) -> Sprite2D:
|
||||
var found := Vector2i(-1, -1)
|
||||
for dy in range(1, 6):
|
||||
for layer in _tile_layers():
|
||||
var probe: Vector2i = cell + Vector2i(0, dy)
|
||||
var source_id: int = layer.get_cell_source_id(probe)
|
||||
if source_id == -1:
|
||||
continue
|
||||
var coords: Vector2i = layer.get_cell_atlas_coords(probe)
|
||||
var source := layer.tile_set.get_source(source_id) as TileSetAtlasSource
|
||||
if source == null:
|
||||
continue
|
||||
var data: TileData = source.get_tile_data(coords, 0)
|
||||
if data != null and data.get_collision_polygons_count(0) > 0:
|
||||
found = coords
|
||||
break
|
||||
if found.x >= 0:
|
||||
break
|
||||
if found.x < 0:
|
||||
return null
|
||||
|
||||
var atlas_texture := AtlasTexture.new()
|
||||
atlas_texture.atlas = ATLAS
|
||||
atlas_texture.region = Rect2(found.x * TILE, found.y * TILE, TILE, TILE)
|
||||
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = atlas_texture
|
||||
sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
sprite.z_index = cover_z_index
|
||||
return sprite
|
||||
@@ -0,0 +1 @@
|
||||
uid://dxwa7fxghp3a2
|
||||
@@ -0,0 +1,45 @@
|
||||
extends CanvasLayer
|
||||
|
||||
## 左上角的状态显示:3 颗心 + 回血道具数量 + 护盾剩余时间。
|
||||
## 每帧从场景里找带 "player" 组的玩家读取状态。
|
||||
|
||||
@export var full_heart_texture: Texture2D
|
||||
@export var empty_heart_texture: Texture2D
|
||||
|
||||
var _player: Node = null
|
||||
var _hearts: Array = []
|
||||
var _count_label: Label = null
|
||||
var _shield_label: Label = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for i in range(1, 4):
|
||||
var heart := get_node_or_null("Heart%d" % i)
|
||||
if heart != null:
|
||||
_hearts.append(heart)
|
||||
_count_label = get_node_or_null("CountLabel")
|
||||
_shield_label = get_node_or_null("ShieldLabel")
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
if _player == null:
|
||||
return
|
||||
|
||||
var health: int = int(_player.health)
|
||||
for i in _hearts.size():
|
||||
var heart: TextureRect = _hearts[i]
|
||||
heart.texture = full_heart_texture if i < health else empty_heart_texture
|
||||
|
||||
var items: int = int(_player.heal_items)
|
||||
if _count_label != null:
|
||||
_count_label.text = "x%d" % items
|
||||
_count_label.modulate = Color.WHITE if items > 0 else Color(1.0, 1.0, 1.0, 0.4)
|
||||
|
||||
if _shield_label != null:
|
||||
var shield: float = float(_player.shield_left)
|
||||
if shield > 0.0:
|
||||
_shield_label.text = "SHIELD %.1fs" % shield
|
||||
else:
|
||||
_shield_label.text = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://8o2g2u6clfh6
|
||||
+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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bx5nwb3eghex7
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0gltif7vthau
|
||||
@@ -0,0 +1,23 @@
|
||||
extends Area2D
|
||||
|
||||
## 弹起角色时给它的向上速度(负值 = 向上)。玩家正常跳跃是 -190,这里给约 1.6 倍。
|
||||
@export var bounce_velocity: float = -300.0
|
||||
## 角色的竖直速度要 >= 这个值才弹(正在往上冲的时候不弹,免得把人往下拍)
|
||||
@export var min_downward_speed: float = -1.0
|
||||
## 角色的中心必须比弹簧中心高出这么多像素,才算"从上面踩下来";从侧面撞过去不弹
|
||||
@export var top_zone_offset: float = 4.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
var character := body as CharacterBody2D
|
||||
if character == null:
|
||||
return
|
||||
if character.velocity.y < min_downward_speed:
|
||||
return
|
||||
if character.global_position.y > global_position.y - top_zone_offset:
|
||||
return
|
||||
character.velocity.y = bounce_velocity
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc10ax0n3w2sp
|
||||
Reference in New Issue
Block a user