class_name Game extends Node2D ## 游戏总装:世界构建、HUD、生命与游戏结束流程。 ## 场景树全部由代码构建,入口场景只挂本脚本。 enum State { PLAYING, GAME_OVER } const VIEW_SIZE := Vector2(540.0, 960.0) const START_LIVES := 3 const BOMB_ICON := preload("res://assets/svg/bomb_icon.svg") const BOSS_BAR_WIDTH := 260.0 const BOMB_DAMAGE := 30 var score := 0 var lives := START_LIVES var state := State.PLAYING var player: Player var _hud_score: Label var _lives_box: Node2D var _overlay: CanvasLayer var _final_score_label: Label var _over_since_msec := 0 var _effects: Node2D var _enemy_bullets: Node2D var _enemies: Node2D var _boss: Boss var _boss_bar_bg: ColorRect var _boss_bar_fill: ColorRect var _warning: Label var _shake_amp := 0.0 var _bombs_box: Node2D var _flash_rect: ColorRect var _flash_tween: Tween func _ready() -> void: add_to_group("game") randomize() _build_world() _build_hud() _build_overlay() _update_lives() _update_score_label() func _process(delta: float) -> void: if state == State.GAME_OVER: var accept := Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("fire") if accept and Time.get_ticks_msec() - _over_since_msec > 800: get_tree().change_scene_to_file("res://scenes/title.tscn") return _update_boss_bar() _update_shake(delta) func _update_boss_bar() -> void: # 注意:已释放实例 == null 也为真,判存活必须用 is_instance_valid if not is_instance_valid(_boss): if _boss_bar_bg.visible: _boss_bar_bg.visible = false _boss_bar_fill.visible = false return var ratio := clampf(float(_boss.hp) / float(_boss.max_hp), 0.0, 1.0) _boss_bar_fill.size.x = BOSS_BAR_WIDTH * ratio func _update_shake(delta: float) -> void: if _shake_amp > 0.2: _shake_amp = lerpf(_shake_amp, 0.0, 8.0 * delta) position = Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0)) * _shake_amp elif _shake_amp != 0.0: _shake_amp = 0.0 position = Vector2.ZERO func _build_world() -> void: add_child(OceanScroll.new()) var player_bullets := Node2D.new() player_bullets.name = "PlayerBullets" player_bullets.add_to_group("player_bullets") add_child(player_bullets) _enemy_bullets = Node2D.new() _enemy_bullets.name = "EnemyBullets" _enemy_bullets.add_to_group("enemy_bullets") add_child(_enemy_bullets) _enemies = Node2D.new() _enemies.name = "Enemies" _enemies.add_to_group("enemies") add_child(_enemies) var powerups := Node2D.new() powerups.name = "Powerups" powerups.add_to_group("powerups") add_child(powerups) player = Player.new() player.variant = GameSession.plane_variant as Player.Variant player.position = VIEW_SIZE * Vector2(0.5, 0.82) player.hit.connect(_on_player_hit) add_child(player) _effects = Node2D.new() _effects.name = "Effects" add_child(_effects) add_child(WaveSpawner.new()) func _build_hud() -> void: var layer := CanvasLayer.new() layer.layer = 10 add_child(layer) _hud_score = Label.new() _hud_score.position = Vector2(16.0, 10.0) _hud_score.add_theme_font_size_override("font_size", 26) _hud_score.add_theme_color_override("font_color", Color.WHITE) _hud_score.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.6)) _hud_score.add_theme_constant_override("shadow_offset_x", 2) _hud_score.add_theme_constant_override("shadow_offset_y", 2) layer.add_child(_hud_score) _lives_box = Node2D.new() _lives_box.position = Vector2(520.0, 38.0) layer.add_child(_lives_box) # 炸弹计数(生命图标下方) _bombs_box = Node2D.new() _bombs_box.position = Vector2(520.0, 72.0) layer.add_child(_bombs_box) _update_bombs() # 炸弹白闪层(最顶层) var flash_layer := CanvasLayer.new() flash_layer.layer = 30 add_child(flash_layer) _flash_rect = ColorRect.new() _flash_rect.color = Color.WHITE _flash_rect.set_anchors_preset(Control.PRESET_FULL_RECT) _flash_rect.modulate.a = 0.0 flash_layer.add_child(_flash_rect) # Boss 血条(顶栏中央) _boss_bar_bg = ColorRect.new() _boss_bar_bg.position = Vector2(140.0, 54.0) _boss_bar_bg.size = Vector2(BOSS_BAR_WIDTH + 4.0, 14.0) _boss_bar_bg.color = Color(0.0, 0.0, 0.0, 0.65) _boss_bar_bg.visible = false layer.add_child(_boss_bar_bg) _boss_bar_fill = ColorRect.new() _boss_bar_fill.position = Vector2(142.0, 56.0) _boss_bar_fill.size = Vector2(BOSS_BAR_WIDTH, 10.0) _boss_bar_fill.color = Color(0.85, 0.2, 0.15) _boss_bar_fill.visible = false layer.add_child(_boss_bar_fill) # Boss 登场警告横幅 _warning = Label.new() _warning.text = "WARNING!!" _warning.position = Vector2(0.0, 330.0) _warning.size = Vector2(540.0, 70.0) _warning.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _warning.add_theme_font_size_override("font_size", 56) _warning.add_theme_color_override("font_color", Color(1.0, 0.25, 0.2)) _warning.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.7)) _warning.add_theme_constant_override("shadow_offset_x", 3) _warning.add_theme_constant_override("shadow_offset_y", 3) _warning.visible = false layer.add_child(_warning) func _build_overlay() -> void: _overlay = CanvasLayer.new() _overlay.layer = 20 var dim := ColorRect.new() dim.color = Color(0.0, 0.0, 0.0, 0.55) dim.set_anchors_preset(Control.PRESET_FULL_RECT) _overlay.add_child(dim) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) _overlay.add_child(center) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 18) vbox.alignment = BoxContainer.ALIGNMENT_CENTER center.add_child(vbox) var title := Label.new() title.text = "GAME OVER" title.add_theme_font_size_override("font_size", 52) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) _final_score_label = Label.new() _final_score_label.add_theme_font_size_override("font_size", 30) _final_score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(_final_score_label) var hint := Label.new() hint.text = "回车 / 空格 重新开始" hint.add_theme_font_size_override("font_size", 22) hint.modulate = Color(0.8, 0.8, 0.8) hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(hint) _overlay.visible = false add_child(_overlay) func add_score(value: int) -> void: score += value _update_score_label() func spawn_explosion(pos: Vector2, explosion_scale: float = 1.0) -> void: var explosion := Explosion.new() explosion.size = explosion_scale _effects.add_child(explosion) explosion.global_position = pos ## 生成关底 Boss 并显示血条 func spawn_boss(p_difficulty: float) -> Boss: var boss := Boss.new() boss.setup(p_difficulty) _enemies.add_child(boss) _boss = boss _boss_bar_bg.visible = true _boss_bar_fill.visible = true return boss ## Boss 登场前红色闪烁警告 func show_boss_warning() -> void: _warning.visible = true var tween := create_tween().set_loops(4) tween.tween_property(_warning, "modulate:a", 0.1, 0.22) tween.tween_property(_warning, "modulate:a", 1.0, 0.22) tween.finished.connect(func() -> void: _warning.visible = false) ## 屏幕震动(强度衰减) func shake(amp: float) -> void: _shake_amp = maxf(_shake_amp, amp) ## 清除全场敌方弹幕 func clear_enemy_bullets() -> void: for bullet in _enemy_bullets.get_children(): bullet.queue_free() ## 炸弹引爆:白闪 + 清弹 + 全场伤害 + 震屏(伤害数值由调用方计数) func detonate_bomb() -> void: _update_bombs() if _flash_tween != null and _flash_tween.is_valid(): _flash_tween.kill() _flash_rect.modulate.a = 0.85 _flash_tween = create_tween() _flash_tween.tween_property(_flash_rect, "modulate:a", 0.0, 0.45) \ .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) clear_enemy_bullets() for enemy in _enemies.get_children(): if enemy is EnemyBase: (enemy as EnemyBase).apply_damage(BOMB_DAMAGE) shake(7.0) ## 炸弹 +1(上限 Player.BOMBS_MAX) func add_bomb() -> void: if player != null and is_instance_valid(player): player.bombs = mini(player.bombs + 1, Player.BOMBS_MAX) _update_bombs() ## 拾取 B 道具:+1 炸弹并刷新 HUD func notify_bomb_pickup() -> void: _update_bombs() func _update_bombs() -> void: if _bombs_box == null: return for icon in _bombs_box.get_children(): icon.queue_free() var count := player.bombs if player != null else 0 for i in count: var icon := Sprite2D.new() icon.texture = BOMB_ICON icon.scale = Vector2.ONE * 0.8 icon.position = Vector2(-22.0 * i, 0.0) _bombs_box.add_child(icon) func _on_player_hit() -> void: if state != State.PLAYING or not player.active: return lives -= 1 _update_lives() spawn_explosion(player.global_position, 1.6) clear_enemy_bullets() player.die() if lives <= 0: state = State.GAME_OVER _show_game_over() else: await get_tree().create_timer(0.9).timeout if state == State.PLAYING: player.respawn(VIEW_SIZE * Vector2(0.5, 0.82)) func _show_game_over() -> void: _final_score_label.text = "得分 " + str(score).pad_zeros(6) _overlay.visible = true _over_since_msec = Time.get_ticks_msec() func _update_score_label() -> void: _hud_score.text = "SCORE " + str(score).pad_zeros(6) func _update_lives() -> void: for icon in _lives_box.get_children(): icon.queue_free() for i in maxi(lives - 1, 0): var icon := Sprite2D.new() icon.texture = Player.TEX icon.scale = Vector2.ONE * 0.42 icon.position = Vector2(-36.0 * i, 0.0) _lives_box.add_child(icon)