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