2. 项目搭建
2026/4/14大约 7 分钟
2. 超级玛丽奥——项目搭建
简介
和坦克大战一样,项目搭建是打地基。在写具体的跳跃、敌人、道具代码之前,我们需要先把项目结构搭好、把物理参数设好、把基本场景创建出来。
超级玛丽奥和坦克大战最大的区别在于物理系统——坦克是在网格上移动的,而玛丽奥是自由移动的,需要处理重力、加速度、摩擦力等物理概念。
项目目录结构
res://
├── scenes/ # 场景文件夹
│ ├── main.tscn # 主场景
│ ├── level.tscn # 关卡场景模板
│ ├── player.tscn # 玩家场景
│ ├── enemies/ # 敌人场景
│ │ ├── goomba.tscn # 蘑菇怪
│ │ ├── koopa.tscn # 乌龟
│ │ └── piranha.tscn # 食人花
│ ├── items/ # 道具场景
│ │ ├── mushroom.tscn # 蘑菇
│ │ ├── fire_flower.tscn # 火焰花
│ │ ├── star.tscn # 星星
│ │ └── coin.tscn # 金币
│ ├── blocks/ # 砖块场景
│ │ ├── question_block.tscn # 问号砖块
│ │ ├── brick_block.tscn # 普通砖块
│ │ └── ground_block.tscn # 地面砖块
│ ├── effects/ # 特效场景
│ │ ├── coin_effect.tscn # 金币弹出效果
│ │ ├── score_popup.tscn # 分数弹出
│ │ └── brick_debris.tscn # 砖块碎片
│ ├── environment/ # 环境场景
│ │ ├── pipe.tscn # 管道
│ │ ├── flagpole.tscn # 旗杆
│ │ └── castle.tscn # 城堡
│ └── ui/ # UI场景
│ ├── hud.tscn # 游戏HUD
│ ├── main_menu.tscn # 主菜单
│ └── game_over.tscn # 游戏结束
│
├── scripts/ # 脚本文件夹
│ ├── managers/ # 管理器
│ │ ├── game_manager.gd # 游戏管理器
│ │ ├── audio_manager.gd # 音效管理器
│ │ └── coin_counter.gd # 金币计数器
│ ├── player/ # 玩家
│ │ ├── player.gd # 玩家主脚本
│ │ └── player_state.gd # 玩家状态管理
│ ├── enemies/ # 敌人
│ │ ├── enemy.gd # 敌人基类
│ │ ├── goomba.gd # 蘑菇怪
│ │ └── koopa.gd # 乌龟
│ ├── items/ # 道具
│ │ ├── item.gd # 道具基类
│ │ ├── mushroom.gd # 蘑菇
│ │ └── fire_flower.gd # 火焰花
│ ├── blocks/ # 砖块
│ │ ├── question_block.gd # 问号砖块
│ │ └── brick_block.gd # 普通砖块
│ └── camera/ # 摄像机
│ └── game_camera.gd # 游戏摄像机
│
├── assets/ # 资源文件夹
│ ├── sprites/ # 图片
│ ├── audio/ # 音频
│ └── tilesets/ # 瓦片集
│
├── data/ # 数据文件夹
│ └── levels/ # 关卡数据
│
└── autoload/ # 自动加载
└── game_manager.gd # 游戏管理器项目参数设置
点击 项目 → 项目设置,配置以下参数:
显示设置
| 设置项 | 值 | 说明 |
|---|---|---|
| 窗口宽度 | 800 | 游戏画面宽度 |
| 窗口高度 | 480 | 游戏画面高度 |
| 拉伸模式 | canvas_items | 窗口缩放时画面跟着缩放 |
| 拉伸方面 | keep | 保持画面比例 |
| 窗口标题 | Super Mario | 窗口标题栏文字 |
物理设置(非常重要)
超级玛丽奥的物理参数决定了游戏的"手感"。以下是一组经典的手感参数:
| 设置项 | 值 | 说明 |
|---|---|---|
| 物理 → 2D → 默认重力X | 0 | 水平方向没有重力 |
| 物理 → 2D → 默认重力Y | 980 | 向下的重力加速度(像素/秒^2) |
输入映射
| 动作 | 按键 |
|---|---|
| move_left | A / 左箭头 |
| move_right | D / 右箭头 |
| jump | Space / W / 上箭头 |
| run | Shift |
| crouch | S / 下箭头 |
| shoot | J |
碰撞层
| 层编号 | 名称 | 说明 |
|---|---|---|
| 1 | Ground | 地面和平台 |
| 2 | Player | 玩家 |
| 3 | Enemies | 敌人 |
| 4 | Items | 道具 |
| 5 | Projectiles | 火球等飞行物 |
| 6 | Blocks | 砖块 |
| 7 | WorldBounds | 世界边界 |
游戏管理器
// GameManager.cs - 游戏管理器
using Godot;
public partial class GameManager : Node
{
// ========== 单例 ==========
public static GameManager Instance { get; private set; }
// ========== 游戏常量 ==========
public const int COINS_FOR_LIFE = 100; // 100金币=1条命
public const int STARTING_LIVES = 3; // 初始生命数
public const int STARTING_TIME = 400; // 每关初始时间(秒)
public const int STAR_DURATION = 10; // 无敌星星持续时间
// ========== 游戏状态枚举 ==========
public enum GameState
{
MainMenu,
Playing,
LevelComplete,
GameOver,
Paused
}
// ========== 状态变量 ==========
private GameState _currentState = GameState.MainMenu;
public GameState CurrentState
{
get => _currentState;
set
{
_currentState = value;
EmitSignal(SignalName.StateChanged, (int)value);
}
}
// 当前关卡
public int CurrentLevel { get; set; } = 1;
public int TotalLevels { get; set; } = 8;
// 生命数
public int Lives { get; set; } = STARTING_LIVES;
// 金币数
public int Coins { get; set; } = 0;
// 分数
public int Score { get; set; } = 0;
// 最高分
public int HighScore { get; set; } = 0;
// 当前关卡剩余时间
public int TimeLeft { get; set; } = STARTING_TIME;
// ========== 信号 ==========
[Signal] public delegate void StateChangedEventHandler(int newState);
[Signal] public delegate void ScoreChangedEventHandler(int newScore);
[Signal] public delegate void LivesChangedEventHandler(int newLives);
[Signal] public delegate void CoinsChangedEventHandler(int newCoins);
[Signal] public delegate void TimeChangedEventHandler(int newTime);
// ========== 生命周期 ==========
public override void _EnterTree()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
}
public override void _Ready()
{
LoadHighScore();
GD.Print("GameManager 初始化完成");
}
// 时间倒计时
public override void _Process(double delta)
{
if (CurrentState != GameState.Playing) return;
// 每秒减1
_timeAccumulator += delta;
if (_timeAccumulator >= 1.0)
{
_timeAccumulator -= 1.0;
TimeLeft--;
EmitSignal(SignalName.TimeChanged, TimeLeft);
// 时间用完
if (TimeLeft <= 0)
{
PlayerDied();
}
}
}
private double _timeAccumulator = 0.0;
// ========== 游戏控制 ==========
// 开始新游戏
public void StartNewGame()
{
Score = 0;
Lives = STARTING_LIVES;
Coins = 0;
CurrentLevel = 1;
TimeLeft = STARTING_TIME;
CurrentState = GameState.Playing;
}
// 加载指定关卡
public void LoadLevel(int level)
{
CurrentLevel = Mathf.Clamp(level, 1, TotalLevels);
TimeLeft = STARTING_TIME;
CurrentState = GameState.Playing;
}
// 加分
public void AddScore(int points)
{
Score += points;
EmitSignal(SignalName.ScoreChanged, Score);
if (Score > HighScore)
{
HighScore = Score;
SaveHighScore();
}
}
// 加金币
public void AddCoin()
{
Coins++;
EmitSignal(SignalName.CoinsChanged, Coins);
AddScore(200);
// 100金币=1条命
if (Coins >= COINS_FOR_LIFE)
{
Coins -= COINS_FOR_LIFE;
Lives++;
EmitSignal(SignalName.LivesChanged, Lives);
}
}
// 玩家死亡
public void PlayerDied()
{
Lives--;
EmitSignal(SignalName.LivesChanged, Lives);
if (Lives <= 0)
{
CurrentState = GameState.GameOver;
}
else
{
// 重新开始当前关卡
GetTree().ReloadCurrentScene();
}
}
// 关卡通过
public void LevelComplete()
{
// 剩余时间转为分数
AddScore(TimeLeft * 50);
CurrentState = GameState.LevelComplete;
}
// 下一关
public void NextLevel()
{
if (CurrentLevel >= TotalLevels)
{
// 全部通关
CurrentState = GameState.GameOver;
}
else
{
LoadLevel(CurrentLevel + 1);
}
}
// 暂停
public void TogglePause()
{
if (CurrentState == GameState.Playing)
{
CurrentState = GameState.Paused;
GetTree().Paused = true;
}
else if (CurrentState == GameState.Paused)
{
CurrentState = GameState.Playing;
GetTree().Paused = false;
}
}
// ========== 存档 ==========
private void SaveHighScore()
{
var data = new Godot.Collections.Dictionary { { "high_score", HighScore } };
using var file = FileAccess.Open("user://mario_save.json", FileAccess.ModeFlags.Write);
file?.StoreString(Json.Stringify(data));
}
private void LoadHighScore()
{
if (!FileAccess.FileExists("user://mario_save.json")) return;
using var file = FileAccess.Open("user://mario_save.json", FileAccess.ModeFlags.Read);
if (file == null) return;
var json = new Json();
if (json.Parse(file.GetAsText()) == Error.Ok)
{
var data = json.Data.AsGodotDictionary();
if (data.ContainsKey("high_score"))
HighScore = (int)data["high_score"];
}
}
}# game_manager.gd - 游戏管理器
extends Node
# ========== 单例 ==========
var instance: Node = null
# ========== 游戏常量 ==========
const COINS_FOR_LIFE: int = 100 # 100金币=1条命
const STARTING_LIVES: int = 3 # 初始生命数
const STARTING_TIME: int = 400 # 每关初始时间(秒)
const STAR_DURATION: int = 10 # 无敌星星持续时间
# ========== 游戏状态枚举 ==========
enum GameState {
MAIN_MENU,
PLAYING,
LEVEL_COMPLETE,
GAME_OVER,
PAUSED
}
# ========== 状态变量 ==========
var current_state: int = GameState.MAIN_MENU:
set(value):
current_state = value
state_changed.emit(value)
# 当前关卡
var current_level: int = 1
var total_levels: int = 8
# 生命数
var lives: int = STARTING_LIVES
# 金币数
var coins: int = 0
# 分数
var score: int = 0
# 最高分
var high_score: int = 0
# 当前关卡剩余时间
var time_left: int = STARTING_TIME
# ========== 信号 ==========
signal state_changed(new_state: int)
signal score_changed(new_score: int)
signal lives_changed(new_lives: int)
signal coins_changed(new_coins: int)
signal time_changed(new_time: int)
# ========== 生命周期 ==========
func _enter_tree() -> void:
if instance != null and instance != self:
queue_free()
return
instance = self
func _ready() -> void:
load_high_score()
print("GameManager 初始化完成")
# 时间倒计时
var _time_accumulator: float = 0.0
func _process(delta: float) -> void:
if current_state != GameState.PLAYING:
return
# 每秒减1
_time_accumulator += delta
if _time_accumulator >= 1.0:
_time_accumulator -= 1.0
time_left -= 1
time_changed.emit(time_left)
# 时间用完
if time_left <= 0:
player_died()
# ========== 游戏控制 ==========
## 开始新游戏
func start_new_game() -> void:
score = 0
lives = STARTING_LIVES
coins = 0
current_level = 1
time_left = STARTING_TIME
current_state = GameState.PLAYING
## 加载指定关卡
func load_level(level: int) -> void:
current_level = clampi(level, 1, total_levels)
time_left = STARTING_TIME
current_state = GameState.PLAYING
## 加分
func add_score(points: int) -> void:
score += points
score_changed.emit(score)
if score > high_score:
high_score = score
save_high_score()
## 加金币
func add_coin() -> void:
coins += 1
coins_changed.emit(coins)
add_score(200)
# 100金币=1条命
if coins >= COINS_FOR_LIFE:
coins -= COINS_FOR_LIFE
lives += 1
lives_changed.emit(lives)
## 玩家死亡
func player_died() -> void:
lives -= 1
lives_changed.emit(lives)
if lives <= 0:
current_state = GameState.GAME_OVER
else:
# 重新开始当前关卡
get_tree().reload_current_scene()
## 关卡通过
func level_complete() -> void:
# 剩余时间转为分数
add_score(time_left * 50)
current_state = GameState.LEVEL_COMPLETE
## 下一关
func next_level() -> void:
if current_level >= total_levels:
current_state = GameState.GAME_OVER
else:
load_level(current_level + 1)
## 暂停
func toggle_pause() -> void:
if current_state == GameState.PLAYING:
current_state = GameState.PAUSED
get_tree().paused = true
elif current_state == GameState.PAUSED:
current_state = GameState.PLAYING
get_tree().paused = false
# ========== 存档 ==========
func save_high_score() -> void:
var data = {"high_score": high_score}
var file = FileAccess.open("user://mario_save.json", FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(data))
func load_high_score() -> void:
if not FileAccess.file_exists("user://mario_save.json"):
return
var file = FileAccess.open("user://mario_save.json", FileAccess.READ)
if file == null:
return
var json = JSON.new()
if json.parse(file.get_as_text()) == OK:
var data = json.data
if "high_score" in data:
high_score = data.high_score关卡场景结构
每个关卡场景的结构:
Level (Node2D) # 关卡根节点
├── TileMapLayer (Ground) # 地面和平台层
├── TileMapLayer (Blocks) # 砖块和问号砖块层
├── TileMapLayer (Decorations) # 装饰层(云朵、灌木)
│
├── Player (CharacterBody2D) # 玩家
├── Enemies (Node2D) # 敌人容器
│ ├── Goomba (RigidBody2D) # 蘑菇怪
│ └── Koopa (RigidBody2D) # 乌龟
│
├── Items (Node2D) # 道具容器
├── Effects (Node2D) # 特效容器
│
├── Environment (Node2D) # 环境物体
│ ├── Pipe (StaticBody2D) # 管道
│ └── Flagpole (Area2D) # 旗杆
│
├── WorldBounds (StaticBody2D) # 世界边界(防止掉出地图底部)
├── DeathZone (Area2D) # 死亡区域(掉坑检测)
│
├── Camera (Camera2D) # 摄像机
└── HUD (CanvasLayer) # 游戏界面主场景脚本
// Main.cs - 主场景控制器
using Godot;
public partial class Main : Control
{
[Export] public Control MainMenu { get; set; }
[Export] public Control GameContainer { get; set; }
[Export] public Control GameOverPanel { get; set; }
private Node _currentLevel;
public override void _Ready()
{
ShowPanel("main_menu");
GameManager.Instance.StateChanged += OnStateChanged;
}
private void ShowPanel(string name)
{
MainMenu.Visible = (name == "main_menu");
GameContainer.Visible = (name == "game");
GameOverPanel.Visible = (name == "game_over");
}
private void OnStateChanged(int state)
{
var s = (GameManager.GameState)state;
switch (s)
{
case GameManager.GameState.Playing:
LoadLevel(GameManager.Instance.CurrentLevel);
ShowPanel("game");
break;
case GameManager.GameState.LevelComplete:
// 显示关卡通过界面,然后加载下一关
var timer = GetTree().CreateTimer(2.0);
timer.Timeout += GameManager.Instance.NextLevel;
break;
case GameManager.GameState.GameOver:
ShowPanel("game_over");
break;
case GameManager.GameState.MainMenu:
ShowPanel("main_menu");
break;
}
}
private void LoadLevel(int level)
{
if (_currentLevel != null)
{
_currentLevel.QueueFree();
}
var scene = GD.Load<PackedScene>($"res://scenes/levels/level_{level}.tscn");
if (scene != null)
{
_currentLevel = scene.Instantiate();
GameContainer.AddChild(_currentLevel);
}
}
private void OnStartButtonPressed() => GameManager.Instance.StartNewGame();
private void OnRetryButtonPressed() => GameManager.Instance.StartNewGame();
private void OnQuitButtonPressed() => GetTree().Quit();
}# main.gd - 主场景控制器
extends Control
@export var main_menu: Control
@export var game_container: Control
@export var game_over_panel: Control
var current_level: Node = null
func _ready() -> void:
show_panel("main_menu")
GameManager.state_changed.connect(on_state_changed)
func show_panel(name: String) -> void:
main_menu.visible = (name == "main_menu")
game_container.visible = (name == "game")
game_over_panel.visible = (name == "game_over")
func on_state_changed(state: int) -> void:
match state:
GameManager.GameState.PLAYING:
load_level(GameManager.current_level)
show_panel("game")
GameManager.GameState.LEVEL_COMPLETE:
# 显示关卡通过界面,然后加载下一关
var timer = get_tree().create_timer(2.0)
timer.timeout.connect(GameManager.next_level)
GameManager.GameState.GAME_OVER:
show_panel("game_over")
GameManager.GameState.MAIN_MENU:
show_panel("main_menu")
func load_level(level: int) -> void:
if current_level != null:
current_level.queue_free()
var scene = load("res://scenes/levels/level_%d.tscn" % level)
if scene != null:
current_level = scene.instantiate()
game_container.add_child(current_level)
func _on_start_button_pressed() -> void:
GameManager.start_new_game()
func _on_retry_button_pressed() -> void:
GameManager.start_new_game()
func _on_quit_button_pressed() -> void:
get_tree().quit()下一章预告
项目搭建完毕!下一章我们将实现超级玛丽奥最核心的部分——玩家物理系统,包括加速度、摩擦力和最重要的"可变高度跳跃"。
