Files
hello-godot/scripts/game.gd
T

340 lines
9.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)