第23章 实战:平台跳跃游戏
23.1 项目概述
构建一个完整的平台跳跃游戏,包含:
- 角色移动、跳跃、冲刺、墙跳
- 关卡设计(TileMap)
- 收集品、存档点
- 敌人和 Boss
- UI(HUD、菜单)
- 视差背景
23.2 项目结构
scenes/
├── characters/
│ ├── player.tscn
│ └── enemies/
│ ├── slime.tscn
│ ├── bat.tscn
│ └── boss.tscn
├── levels/
│ ├── level_01.tscn
│ ├── level_02.tscn
│ └── boss_room.tscn
├── objects/
│ ├── coin.tscn
│ ├── checkpoint.tscn
│ ├── moving_platform.tscn
│ ├── spring.tscn
│ └── spike.tscn
└── ui/
├── hud.tscn
├── main_menu.tscn
└── pause_menu.tscn
23.3 关卡设计
gdscript
# level_01.tscn 节点结构
Level (Node2D)
├── Background (ParallaxBackground)
│ ├── Sky (ParallaxLayer) motion_scale = (0, 0)
│ ├── Mountains (ParallaxLayer) motion_scale = (0.2, 0.1)
│ └── Trees (ParallaxLayer) motion_scale = (0.6, 0.3)
├── TileMapLayer (背景层)
├── TileMapLayer (地形层) ← 碰撞在这里
├── TileMapLayer (装饰层)
├── TileMapLayer (前景层)
├── Objects
│ ├── Coins
│ ├── Checkpoints
│ ├── Spikes
│ └── MovingPlatforms
├── Enemies
│ ├── Slime1
│ ├── Slime2
│ └── Bat1
├── Player
└── UILayer (CanvasLayer)
└── HUD
23.4 移动平台
gdscript
extends AnimatableBody2D
@export var move_points: Array[Vector2] = []
@export var speed: float = 100.0
@export var wait_time: float = 1.0
var current_point: int = 0
var direction: int = 1
var is_waiting: bool = false
func _physics_process(delta: float) -> void:
if is_waiting or move_points.size() < 2:
return
var target := move_points[current_point]
var move_dir := (target - global_position).normalized()
global_position = global_position.move_toward(target, speed * delta)
if global_position.distance_to(target) < 1.0:
global_position = target
is_waiting = true
await get_tree().create_timer(wait_time).timeout
is_waiting = false
current_point += direction
if current_point >= move_points.size() or current_point < 0:
direction *= -1
current_point += direction * 2
23.5 陷阱设计
gdscript
# 尖刺
extends Area2D
@export var damage: int = 100 # 一击必杀
@export var knockback_force: float = 500.0
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
if body.has_method("take_damage"):
var knockback_dir := Vector2.UP
body.take_damage(damage, knockback_dir * knockback_force)
# 弹跳板
extends Area2D
@export var bounce_force: float = -800.0
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body is CharacterBody2D:
body.velocity.y = bounce_force
$AnimationPlayer.play("bounce")
23.6 收集品
gdscript
extends Area2D
@export var coin_value: int = 1
@export var float_amplitude: float = 5.0
@export var float_speed: float = 2.0
var start_y: float
func _ready() -> void:
start_y = position.y
body_entered.connect(_on_body_entered)
func _process(delta: float) -> void:
position.y = start_y + sin(Time.get_ticks_msec() * 0.001 * float_speed) * float_amplitude
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
GameManager.add_score(coin_value)
# 收集动画
var tween := create_tween()
tween.tween_property(self, "position:y", position.y - 30, 0.2)
tween.parallel().tween_property(self, "modulate:a", 0.0, 0.2)
tween.tween_callback(queue_free)
23.7 Boss 战
gdscript
extends CharacterBody2D
signal boss_defeated
@export var health: int = 100
@export var phase2_threshold: float = 0.5
@export var phase3_threshold: float = 0.25
var current_phase: int = 1
var attack_timer: float = 0.0
var attack_patterns: Array[Callable] = []
@onready var health_component: HealthComponent = $HealthComponent
@onready var anim: AnimatedSprite2D = $AnimatedSprite2D
func _ready() -> void:
health_component.died.connect(_on_died)
health_component.health_changed.connect(_on_health_changed)
attack_patterns = [_attack_slam, _attack_projectile, _attack_charge]
func _on_health_changed(current: int, maximum: int) -> void:
var ratio := float(current) / maximum
if ratio <= phase3_threshold and current_phase < 3:
enter_phase(3)
elif ratio <= phase2_threshold and current_phase < 2:
enter_phase(2)
func enter_phase(phase: int) -> void:
current_phase = phase
match phase:
2:
attack_timer = 0.0
anim.modulate = Color(1, 0.8, 0.8)
3:
anim.modulate = Color(1, 0.3, 0.3)
func _attack_slam() -> void:
anim.play("attack_slam")
await anim.animation_finished
# 地面冲击波
spawnShockwave()
func _attack_projectile() -> void:
anim.play("attack_projectile")
for i in 3:
spawn_projectile()
await get_tree().create_timer(0.3).timeout
func _attack_charge() -> void:
anim.play("charge")
await anim.animation_finished
func _on_died() -> void:
boss_defeated.emit()
# 死亡动画
modulate = Color(1, 1, 1, 1)
var tween := create_tween()
tween.tween_property(self, "modulate:a", 0.0, 1.0)
await tween.finished
queue_free()
23.8 关卡选择界面
gdscript
extends Control
@onready var level_buttons: Array[Button] = []
func _ready() -> void:
update_level_buttons()
func update_level_buttons() -> void:
for i in level_buttons.size():
var level_num := i + 1
var is_unlocked := GameManager.is_level_unlocked(level_num)
level_buttons[i].disabled = not is_unlocked
level_buttons[i].text = "关卡 %d" % level_num
if is_unlocked:
var stars := GameManager.get_level_stars(level_num)
level_buttons[i].text += "\n" + "★".repeat(stars) + "☆".repeat(3 - stars)
func _on_level_selected(level_num: int) -> void:
GameManager.current_level = level_num
get_tree().change_scene_to_file("res://scenes/levels/level_%02d.tscn" % level_num)