You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

190 lines
5.4 KiB

extends Node2D
## 游戏主控:状态机 + 关卡列表 + 分数/生命权威数据 + 信号连接。
## 状态:MENU / PLAYING / LEVEL_CLEAR / GAME_OVER
##
## 流程:
## MENU → 空格开始 → PLAYING(关卡 1)
## PLAYING → 通关 → LEVEL_CLEAR(空格下一关)
## → 命数归零 → GAME_OVER(空格重开/回菜单)
## LEVEL_CLEAR → 最后一关 → 显示"通关" → 回 MENU
## GAME_OVER → 空格回 MENU
enum GameState { MENU, PLAYING, LEVEL_CLEAR, GAME_OVER }
# 关卡数据(按顺序加载)
const LEVELS: Array = [
preload("res://data/level_1.gd"),
preload("res://data/level_2.gd"),
preload("res://data/level_3.gd"),
]
# 场景切换
const MainMenuScene: PackedScene = preload("res://scenes/ui/main_menu.tscn")
const PowerUpScene: PackedScene = preload("res://scenes/power_up.tscn")
@export var initial_lives: int = 3
@onready var world: Node2D = $World
@onready var level: Node2D = $World/Level
@onready var paddle: CharacterBody2D = $World/Paddle
@onready var ball: CharacterBody2D = $World/Ball
@onready var lose_zone: Area2D = $World/LoseZone
@onready var hud: CanvasLayer = $UI/HUD
@onready var audio: Node = $Audio
@onready var power_ups: Node2D = $World/PowerUps
var state: GameState = GameState.MENU
var score: int = 0
var lives: int = initial_lives
var current_level_index: int = 0
var _remaining_bricks: int = 0
func _ready() -> void:
# 信号连接
ball.lost.connect(_on_ball_lost)
ball.brick_hit.connect(_on_ball_hit_brick)
lose_zone.body_entered.connect(_on_lose_zone_body_entered)
# 初始状态:菜单
_show_menu()
func _process(_delta: float) -> void:
match state:
GameState.MENU:
pass # 菜单场景自己处理输入
GameState.LEVEL_CLEAR:
if Input.is_key_pressed(KEY_SPACE):
_go_to_next_level()
GameState.GAME_OVER:
if Input.is_key_pressed(KEY_SPACE):
_show_menu()
# ───────── 状态切换 ─────────
func _show_menu() -> void:
state = GameState.MENU
clear_power_ups()
world.visible = false
hud.visible = false
# 加载菜单场景作为 UI 子节点(如果还没在)
var menu: Control = $UI.get_node_or_null("MainMenu")
if menu == null:
menu = MainMenuScene.instantiate()
menu.name = "MainMenu"
$UI.add_child(menu)
menu.start_pressed.connect(start_game)
menu.quit_pressed.connect(_on_quit_pressed)
menu.visible = true # 重新显示(可能之前被 hidden)
func _on_quit_pressed() -> void:
get_tree().quit()
func start_game() -> void:
# 主菜单"开始"按钮调用
# 关闭主菜单(先隐藏,再删除)
var menu: Control = $UI.get_node_or_null("MainMenu")
if menu:
menu.visible = false
menu.queue_free()
score = 0
lives = initial_lives
current_level_index = 0
paddle.reset_state()
world.visible = true
hud.visible = true
hud.update_score(score)
hud.update_lives(lives)
_load_current_level()
state = GameState.PLAYING
func _load_current_level() -> void:
clear_power_ups()
paddle.reset_state()
level.load_layout(LEVELS[current_level_index].LAYOUT)
_remaining_bricks = level.get_child_count()
ball.reset_to_paddle()
hud.show_message("Level %d — 按 空格 发球" % (current_level_index + 1))
func _go_to_next_level() -> void:
current_level_index += 1
if current_level_index >= LEVELS.size():
# 通关
_game_complete()
return
_load_current_level()
state = GameState.PLAYING
func _game_complete() -> void:
state = GameState.GAME_OVER
hud.show_message("通关!总分 %d — 按 空格 回主菜单" % score)
func _game_over() -> void:
state = GameState.GAME_OVER
hud.show_message("Game Over — 按 空格 回主菜单")
# ───────── 信号 ─────────
func _on_ball_lost() -> void:
audio.play_lost()
lives -= 1
hud.update_lives(lives)
if lives <= 0:
_game_over()
else:
ball.reset_to_paddle()
hud.show_message("按 空格 重新发球")
func _on_ball_hit_brick(brick: Node) -> void:
if not brick.has_method("hit"):
return
if not brick.destroyed.is_connected(_on_brick_destroyed):
brick.destroyed.connect(_on_brick_destroyed)
if not brick.power_up_dropped.is_connected(_on_brick_power_up_dropped):
brick.power_up_dropped.connect(_on_brick_power_up_dropped)
audio.play_hit()
brick.hit()
func _on_brick_power_up_dropped(spawn_position: Vector2) -> void:
spawn_power_up(spawn_position)
func _on_brick_destroyed(points: int) -> void:
score += points
_remaining_bricks -= 1
hud.update_score(score)
audio.play_break()
if _remaining_bricks <= 0 and state == GameState.PLAYING:
state = GameState.LEVEL_CLEAR
hud.show_message("Level Clear! — 按 空格 进下一关")
audio.play_win()
func _on_lose_zone_body_entered(body: Node) -> void:
if body == ball:
ball.lost.emit()
# ───────── 道具 ─────────
func spawn_power_up(position: Vector2) -> void:
var pu: Area2D = PowerUpScene.instantiate()
power_ups.add_child(pu)
pu.position = position
pu.collected.connect(_on_power_up_collected)
func _on_power_up_collected(kind: String) -> void:
audio.play_break() # 蹭原来的音效;有需要再单独加
match kind:
"wide_paddle":
paddle.widen()
hud.show_message("挡板变宽!")
"multi_ball":
hud.show_message("多球(暂未实现)")
"slow_ball":
hud.show_message("减速(暂未实现)")
"extra_life":
lives += 1
hud.update_lives(lives)
hud.show_message("+1 命")
_:
pass
func clear_power_ups() -> void:
for child in power_ups.get_children():
child.queue_free()