10. 打磨与发布
2026/4/14大约 7 分钟
10. 超级玛丽奥——打磨与发布
简介
这是超级玛丽奥项目的最后一章。我们将进行多关卡管理、动画打磨、性能优化和游戏导出——让这个项目从一个"能玩的原型"变成一个"可以分享给朋友"的完整游戏。
多关卡管理
超级玛丽奥有8个关卡(经典NES版本的关卡数)。每个关卡有不同的地形、敌人和难度。
关卡配置
| 关卡 | 名称 | 难度 | 特点 |
|---|---|---|---|
| 1-1 | 草原入门 | 简单 | 宽敞平坦,少量敌人,教学关 |
| 1-2 | 地下管道 | 简单 | 室内关卡,天花板较低 |
| 1-3 | 树顶跳跃 | 普通 | 大量平台跳跃 |
| 1-4 | 城堡冒险 | 中等 | Boss战(库巴) |
| 2-1 | 水上世界 | 中等 | 有水区和跳台 |
| 2-2 | 沙漠烈日 | 中等 | 移动平台 |
| 2-3 | 冰雪滑行 | 困难 | 冰面打滑 |
| 2-4 | 最终城堡 | 极难 | 最终Boss战 |
关卡管理器
// LevelManager.cs - 关卡管理
using Godot;
public partial class LevelManager : Node
{
// 关卡信息
[Export] public string LevelName { get; set; } = "1-1";
[Export] public string LevelTheme { get; set; } = "overworld"; // overworld / underground / castle
// 关卡中的检查点
private Vector2 _checkpointPosition;
// 是否已经通过检查点
private bool _passedCheckpoint = false;
public override void _Ready()
{
// 播放对应主题的背景音乐
PlayLevelMusic();
// 设置背景颜色
SetBackground();
}
// 播放关卡音乐
private void PlayLevelMusic()
{
var musicName = LevelTheme switch
{
"overworld" => "overworld_theme",
"underground" => "underground_theme",
"castle" => "castle_theme",
"water" => "water_theme",
_ => "overworld_theme"
};
AudioManager.Instance.PlayMusic(musicName);
}
// 设置背景
private void SetBackground()
{
var color = LevelTheme switch
{
"overworld" => new Color("5c94fc"), // 蓝色天空
"underground" => new Color("000000"), // 黑色
"castle" => new Color("000000"), // 黑色
"water" => new Color("5c94fc"), // 蓝色天空
_ => new Color("5c94fc")
};
// 设置世界背景色
ProjectSettings.SetSetting("rendering/environment/defaults/default_clear_color", color);
}
// 到达检查点
public void ReachCheckpoint(Vector2 checkpointPos)
{
if (_passedCheckpoint) return;
_passedCheckpoint = true;
_checkpointPosition = checkpointPos;
GD.Print($"到达检查点: {checkpointPos}");
// 播放音效
AudioManager.Instance.PlaySfx("1up");
}
// 玩家死亡后从检查点重生
public Vector2 GetSpawnPosition()
{
if (_passedCheckpoint)
{
return _checkpointPosition;
}
return new Vector2(48, 400); // 默认出生点
}
}# level_manager.gd - 关卡管理
extends Node
## 关卡信息
@export var level_name: String = "1-1"
@export var level_theme: String = "overworld"
# 关卡中的检查点
var _checkpoint_position: Vector2 = Vector2.ZERO
# 是否已经通过检查点
var _passed_checkpoint: bool = false
func _ready() -> void:
play_level_music()
set_background()
## 播放关卡音乐
func play_level_music() -> void:
var music_name: String
match level_theme:
"overground", "overworld":
music_name = "overworld_theme"
"underground":
music_name = "underground_theme"
"castle":
music_name = "castle_theme"
"water":
music_name = "water_theme"
_:
music_name = "overworld_theme"
AudioManager.play_music(music_name)
## 设置背景
func set_background() -> void:
var color: Color
match level_theme:
"overworld", "overground":
color = Color("5c94fc")
"underground":
color = Color.BLACK
"castle":
color = Color.BLACK
"water":
color = Color("5c94fc")
_:
color = Color("5c94fc")
# 设置世界背景色
RenderingServer.set_default_clear_color(color)
## 到达检查点
func reach_checkpoint(checkpoint_pos: Vector2) -> void:
if _passed_checkpoint:
return
_passed_checkpoint = true
_checkpoint_position = checkpoint_pos
print("到达检查点: %s" % str(checkpoint_pos))
AudioManager.play_sfx("1up")
## 玩家死亡后从检查点重生
func get_spawn_position() -> Vector2:
if _passed_checkpoint:
return _checkpoint_position
return Vector2(48, 400)动画打磨
跑步动画速度同步
跑步动画的播放速度应该和移动速度同步——跑得快,动画也快。
// 在 Player.cs 的 UpdateAnimation 中添加
private void UpdateAnimation()
{
// 根据移动速度调整动画速度
float speedRatio = Mathf.Abs(Velocity.X) / MaxSprintSpeed;
float animSpeed = Mathf.Lerp(0.5f, 1.5f, speedRatio);
_animatedSprite.SpeedScale = animSpeed;
if (!IsOnFloor())
{
_animatedSprite.Play(Velocity.Y < 0 ? "jump" : "fall");
}
else if (Mathf.Abs(Velocity.X) > 10)
{
_animatedSprite.Play(_isRunning ? "run" : "walk");
}
else
{
_animatedSprite.Play("idle");
}
}# 在 player.gd 的 update_animation 中添加
func update_animation() -> void:
# 根据移动速度调整动画速度
var speed_ratio: float = absf(velocity.x) / max_sprint_speed
var anim_speed: float = lerpf(0.5, 1.5, speed_ratio)
animated_sprite.speed_scale = anim_speed
if not is_on_floor():
animated_sprite.play("jump" if velocity.y < 0 else "fall")
elif absf(velocity.x) > 10:
animated_sprite.play("run" if is_running else "walk")
else:
animated_sprite.play("idle")敌人踩踏弹跳动画
玛丽奥踩到敌人时,应该有一个"压缩再弹起"的小动画,让打击感更强。
// 在 Player.cs 的 BounceAfterStomp 中添加
private void BounceAfterStomp()
{
// 踩踏弹跳
Velocity = new Vector2(Velocity.X, -200);
// 压缩再弹起的动画
var tween = CreateTween();
tween.TweenProperty(_animatedSprite, "scale", new Vector2(1.0f, 0.8f), 0.05f);
tween.TweenProperty(_animatedSprite, "scale", new Vector2(1.0f, 1.1f), 0.05f);
tween.TweenProperty(_animatedSprite, "scale", Vector2.One, 0.05f);
}# 在 player.gd 的 bounce_after_stomp 中添加
func bounce_after_stomp() -> void:
velocity = Vector2(velocity.x, -200)
# 压缩再弹起的动画
var tween = create_tween()
tween.tween_property(animated_sprite, "scale", Vector2(1.0, 0.8), 0.05)
tween.tween_property(animated_sprite, "scale", Vector2(1.0, 1.1), 0.05)
tween.tween_property(animated_sprite, "scale", Vector2.ONE, 0.05)粒子特效
跑步烟尘
玛丽奥在地上跑时,脚下会冒出小烟尘:
// RunDust.cs - 跑步烟尘
public partial class RunDust : GPUParticles2D
{
public override void _Ready()
{
Emitting = false;
OneShot = true;
Amount = 5;
Lifetime = 0.3f;
}
public void EmitDust()
{
Emitting = true;
}
}
// 在 Player.cs 中
private RunDust _runDust;
private float _dustTimer = 0.0f;
public override void _PhysicsProcess(double delta)
{
// ... 其他逻辑 ...
// 跑步烟尘
_dustTimer -= (float)delta;
if (IsOnFloor() && Mathf.Abs(Velocity.X) > 100 && _dustTimer <= 0)
{
_runDust.EmitDust();
_dustTimer = 0.2f;
}
}# run_dust.gd - 跑步烟尘
extends GPUParticles2D
func _ready() -> void:
emitting = false
one_shot = true
amount = 5
lifetime = 0.3
func emit_dust() -> void:
emitting = true
# 在 player.gd 中
var _run_dust: Node
var _dust_timer: float = 0.0
func _physics_process(delta: float) -> void:
# ... 其他逻辑 ...
# 跑步烟尘
_dust_timer -= delta
if is_on_floor() and absf(velocity.x) > 100 and _dust_timer <= 0:
_run_dust.emit_dust()
_dust_timer = 0.2性能优化
| 优化项 | 方法 | 效果 |
|---|---|---|
| 离屏敌人暂停 | 使用 VisibilityNotifier2D | 屏幕外的敌人不更新 |
| 对象池 | 金币和粒子使用池 | 减少实例化开销 |
| 纹理压缩 | 使用ETC2/ASTC格式 | 减少内存占用 |
| 固定帧率 | Engine.MaxFps = 60 | 节省电量和发热 |
// 敌人离屏暂停
public partial class Enemy : CharacterBody2D
{
private VisibilityNotifier2D _visibilityNotifier;
public override void _Ready()
{
_visibilityNotifier = GetNode<VisibilityNotifier2D>("VisibilityNotifier2D");
_visibilityNotifier.ScreenEntered += () => SetPhysicsProcess(true);
_visibilityNotifier.ScreenExited += () => SetPhysicsProcess(false);
}
}# 敌人离屏暂停
extends CharacterBody2D
func _ready() -> void:
var vis = $VisibilityNotifier2D as VisibilityNotifier2D
vis.screen_entered.connect(func(): set_physics_process(true))
vis.screen_exited.connect(func(): set_physics_process(false))移动端适配
虚拟按键布局
┌─────────────────────────────┐
│ │
│ 游戏画面区域 │
│ │
│ │
│ ┌──┐ ┌──┐ │
│ │← │ │ A│ │
│ ├──┤ └──┘ │
│ │→ │ ┌──┐ │
│ └──┘ │↓ │ │
│ └──┘ │
│ ┌──┐ ┌──┐ │
│ │B │ │RUN│ │
│ └──┘ └──┘ │
└─────────────────────────────┘移动端输入处理
// MobileInput.cs - 移动端输入
public partial class MobileInput : CanvasLayer
{
[Export] public TouchScreenButton LeftButton { get; set; }
[Export] public TouchScreenButton RightButton { get; set; }
[Export] public TouchScreenButton JumpButton { get; set; }
[Export] public TouchScreenButton RunButton { get; set; }
public Vector2 MoveDirection { get; private set; }
public bool JumpPressed { get; private set; }
public bool RunPressed { get; private set; }
public override void _Ready()
{
// 只在移动端显示
Visible = OS.HasFeature("mobile");
LeftButton.Pressed += () => MoveDirection = Vector2.Left;
LeftButton.Released += () => MoveDirection = Vector2.Zero;
RightButton.Pressed += () => MoveDirection = Vector2.Right;
RightButton.Released += () => MoveDirection = Vector2.Zero;
JumpButton.Pressed += () => JumpPressed = true;
JumpButton.Released += () => JumpPressed = false;
RunButton.Pressed += () => RunPressed = true;
RunButton.Released += () => RunPressed = false;
}
}# mobile_input.gd - 移动端输入
extends CanvasLayer
@export var left_button: TouchScreenButton
@export var right_button: TouchScreenButton
@export var jump_button: TouchScreenButton
@export var run_button: TouchScreenButton
var move_direction: Vector2 = Vector2.ZERO
var jump_pressed: bool = false
var run_pressed: bool = false
func _ready() -> void:
visible = OS.has_feature("mobile")
left_button.pressed.connect(func(): move_direction = Vector2.LEFT)
left_button.released.connect(func(): move_direction = Vector2.ZERO)
right_button.pressed.connect(func(): move_direction = Vector2.RIGHT)
right_button.released.connect(func(): move_direction = Vector2.ZERO)
jump_button.pressed.connect(func(): jump_pressed = true)
jump_button.released.connect(func(): jump_pressed = false)
run_button.pressed.connect(func(): run_pressed = true)
run_button.released.connect(func(): run_pressed = false)游戏导出
导出配置
| 平台 | 文件格式 | 特殊配置 |
|---|---|---|
| Windows | .exe | 64位,DirectX |
| macOS | .app | 通用二进制 |
| Linux | .x86_64 | X11 |
| Android | .apk | 横屏,触摸控制 |
| HTML5 | .html | 键盘控制 |
Android 横屏设置
在导出预设中设置:
| 设置项 | 值 |
|---|---|
| 屏幕方向 | 横屏(Landscape) |
| 保持屏幕开启 | 勾选 |
| immersive模式 | 勾选 |
发布前检查清单
| 检查项 | 说明 |
|---|---|
| 8个关卡全部可玩 | 从1-1到2-4都能正常通关 |
| 所有音效正常 | 跳跃、踩怪、金币等音效都正常 |
| 无敌星星正常 | 闪烁效果和持续时间正确 |
| 摄像机不往左移 | 确认限制左移功能正常 |
| 时间倒计时正常 | 时间归零时玩家死亡 |
| 分数正确计算 | 踩怪、吃道具、旗杆得分正确 |
| 100金币=1UP | 金币系统正常 |
| 移动端适配 | 虚拟按键可用 |
项目总结
超级玛丽奥项目完整架构:
超级玛丽奥
├── 核心系统
│ ├── GameManager(游戏管理器)
│ ├── AudioManager(音效管理器)
│ └── LevelManager(关卡管理器)
│
├── 玩家系统
│ ├── Player(玩家控制+物理)
│ ├── PlayerState(状态管理)
│ └── PlayerAnimation(动画系统)
│
├── 敌人系统
│ ├── Enemy(敌人基类)
│ ├── Goomba(蘑菇怪)
│ └── Koopa(乌龟+龟壳)
│
├── 道具系统
│ ├── Item(道具基类)
│ ├── Mushroom(变大蘑菇)
│ ├── FireFlower(火焰花)
│ ├── Star(无敌星星)
│ └── Coin(金币)
│
├── 关卡元素
│ ├── QuestionBlock(问号砖块)
│ ├── BrickBlock(普通砖块)
│ ├── Pipe(管道)
│ └── Flagpole(旗杆终点)
│
├── 摄像机系统
│ ├── GameCamera(滚动摄像机)
│ └── CameraBounds(边界管理)
│
├── UI系统
│ ├── HUD(抬头显示)
│ ├── PauseMenu(暂停菜单)
│ └── GameOverUI(游戏结束)
│
└── 特效系统
├── ScorePopup(分数弹出)
├── RunDust(跑步烟尘)
└── MobileInput(移动端控制)通过这个项目,你掌握了:
- 平台跳跃物理:加速度、摩擦力、可变高度跳跃
- 碰撞区分:踩踏 vs 侧面碰撞
- 状态管理:小/大/火焰/无敌四种状态
- TileMap关卡设计:用瓦片搭建完整关卡
- 滚动摄像机:跟随玩家、限制左移
- 多关卡系统:8关不同主题和难度
- 移动端适配:虚拟按键、横屏模式
