2. 项目搭建
休闲益智——项目搭建
上一章我们设计了三消游戏的核心玩法,现在打开 Godot,把项目搭起来。就像盖房子之前要先打地基、搭脚手架,项目搭建就是为后面的开发准备好"工作环境"。
创建项目
- 打开 Godot 编辑器
- 点击 新建,填写项目信息:
- 项目名称:
match_three(用英文名,避免中文路径带来各种麻烦) - 渲染器:选择
Forward+(默认)
- 项目名称:
- 点击 创建并编辑
项目设置
窗口大小
三消游戏通常是竖屏的(像手机那样),所以窗口比例要设成竖屏。
路径:项目 → 项目设置 → 常规 → 显示 → 窗口
| 设置项 | 值 | 说明 |
|---|---|---|
| 视口宽度 | 720 | 窗口宽度 |
| 视口高度 | 1280 | 窗口高度(竖屏比例) |
| 拉伸模式 | canvas_items | 缩放模式 |
| 拉伸宽高比 | keep | 保持比例不变形 |
为什么是 720x1280?
这是手机屏幕的典型比例(9:16)。我们以这个分辨率为基准开发,Godot 会自动缩放到不同尺寸的屏幕上。
输入映射
三消游戏的核心操作是点击选中方块和拖动交换方块。我们不需要复杂的输入映射,直接在代码里监听鼠标/触屏事件就够了——按下时记录起点,释放时判断方向,就能算出玩家想往哪个方向拖。
为什么不用输入映射?
输入映射更适合有多个按键绑定的操作(比如 WASD 控制移动)。三消游戏的输入更像是"手势识别"——玩家在屏幕上做了一个滑动动作,我们需要判断这个动作的方向。直接在代码里处理更简单。
项目文件结构
好的项目结构就像一个整洁的房间——东西放在该放的地方,找起来方便,维护起来轻松。
match_three/
├── scenes/ # 场景文件
│ ├── main.tscn # 主场景(游戏入口)
│ ├── main_menu.tscn # 主菜单
│ ├── game_board.tscn # 棋盘场景
│ ├── block.tscn # 单个方块场景
│ └── ui/ # UI 场景
│ ├── hud.tscn # 游戏内信息栏
│ ├── level_complete.tscn # 过关界面
│ └── game_over.tscn # 游戏结束界面
├── scripts/ # 脚本文件
│ ├── managers/ # 管理器
│ │ ├── game_manager.cs # 游戏主管理器
│ │ ├── score_manager.cs # 计分管理器
│ │ └── audio_manager.cs # 音效管理器
│ ├── game_board.cs # 棋盘逻辑
│ ├── block.cs # 方块逻辑
│ └── match_checker.cs # 匹配检测
├── resources/ # 资源文件
│ ├── level_configs/ # 关卡配置
│ │ ├── level_1.tres
│ │ └── level_2.tres
│ └── block_colors.tres # 方块颜色配置
├── assets/ # 素材资源
│ ├── sprites/ # 图片
│ ├── sounds/ # 音效
│ └── fonts/ # 字体
└── autoload/ # 自动加载(单例)为什么要分这么多文件夹
这不是过度设计。随着项目变大,如果所有文件都堆在一个文件夹里,你会花大量时间翻找文件。按功能分类是专业游戏开发的基本习惯。
创建游戏管理器(Autoload)
游戏管理器是整个游戏的大脑,负责协调各个子系统。它就像一个乐队的指挥——不演奏任何乐器,但让所有乐手协调配合。
创建 Autoload 脚本
- 在
scripts/managers/下创建game_manager.cs - 在
项目 → 项目设置 → Autoload中添加:- 名称:
GameManager - 路径:
res://scripts/managers/game_manager.cs
- 名称:
using Godot;
/// <summary>
/// 游戏主管理器——整个游戏的总指挥。
/// 就像一个乐队的指挥,不演奏任何乐器,但协调所有乐手。
/// </summary>
public partial class GameManager : Node
{
// 单例模式:让其他脚本可以通过 GameManager.Instance 访问
public static GameManager Instance { get; private set; }
// 游戏状态
public enum GameState
{
Menu, // 主菜单
Playing, // 游戏中
Paused, // 暂停
LevelComplete, // 过关
GameOver // 游戏结束
}
[Signal]
public delegate void StateChangedEventHandler(GameState newState);
[Signal]
public delegate void ScoreChangedEventHandler(int score);
[Signal]
public delegate void MovesChangedEventHandler(int moves);
private int _score = 0; // 当前分数
private int _movesLeft = 30; // 剩余步数
private int _currentLevel = 1; // 当前关卡
private GameState _state = GameState.Menu;
/// <summary>当前分数</summary>
public int Score
{
get => _score;
set
{
_score = value;
EmitSignal(SignalName.ScoreChanged, _score);
}
}
/// <summary>剩余步数</summary>
public int MovesLeft
{
get => _movesLeft;
set
{
_movesLeft = Mathf.Max(0, value);
EmitSignal(SignalName.MovesChanged, _movesLeft);
if (_movesLeft <= 0 && _state == GameState.Playing)
ChangeState(GameState.GameOver);
}
}
/// <summary>当前关卡编号</summary>
public int CurrentLevel => _currentLevel;
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree(); // 防止重复创建
return;
}
Instance = this;
}
/// <summary>切换游戏状态</summary>
public void ChangeState(GameState newState)
{
_state = newState;
EmitSignal(SignalName.StateChanged, newState);
GetTree().Paused = (newState == GameState.Paused);
}
/// <summary>开始新关卡</summary>
public void StartLevel(int level)
{
_currentLevel = level;
_score = 0;
_movesLeft = 30; // 默认30步,后续从关卡配置读取
ChangeState(GameState.Playing);
}
}extends Node
## 游戏主管理器——整个游戏的总指挥。
## 就像一个乐队的指挥,不演奏任何乐器,但协调所有乐手。
# 单例模式
@onready var instance: Node = self
# 游戏状态
enum GameState { MENU, PLAYING, PAUSED, LEVEL_COMPLETE, GAME_OVER }
signal state_changed(new_state: GameState)
signal score_changed(score: int)
signal moves_changed(moves: int)
var score: int = 0
var moves_left: int = 30
var current_level: int = 1
var state: GameState = GameState.MENU
func _ready():
if instance and instance != self:
queue_free()
return
instance = self
## 切换游戏状态
func change_state(new_state: GameState) -> void:
state = new_state
state_changed.emit(new_state)
get_tree().paused = (new_state == GameState.PAUSED)
## 开始新关卡
func start_level(level: int) -> void:
current_level = level
score = 0
moves_left = 30
change_state(GameState.PLAYING)占位素材方案
在游戏开发早期,我们不需要精美的美术素材。用纯色方块(ColorRect)就能做出完整可玩的版本。就像画素描先画火柴人一样——先把骨架搭好,再添砖加瓦。
方块颜色定义
| 颜色编号 | 颜色 | Godot 颜色值 | 名称 |
|---|---|---|---|
| 1 | 红色 | #FF4444 | 红宝石 |
| 2 | 蓝色 | #4488FF | 蓝水晶 |
| 3 | 绿色 | #44CC44 | 绿翡翠 |
| 4 | 黄色 | #FFCC00 | 金太阳 |
| 5 | 紫色 | #CC44FF | 紫葡萄 |
先占位,后替换
这是游戏开发的标准流程:先用简单素材把游戏做出来(能玩),然后再请美术替换成精美素材。如果一开始就等美术素材,可能游戏逻辑还没写完,项目就黄了。
创建游戏主场景
游戏主场景 game.tscn 的节点结构:
Main (Node2D) ← 游戏主场景根节点
├── Background (ColorRect) ← 背景色
├── GameBoard (Node2D) ← 棋盘(方块容器)
├── HUD (CanvasLayer) ← UI 层(始终在最前面)
│ ├── TopBar (HBoxContainer) ← 顶部信息栏
│ │ ├── ScoreLabel (Label) ← 分数
│ │ ├── MovesLabel (Label) ← 剩余步数
│ │ └── LevelLabel (Label) ← 关卡编号
│ └── TargetPanel (Panel) ← 关卡目标显示
└── Camera2D ← 摄像机为什么用 CanvasLayer 放 UI?
CanvasLayer 会让它的子节点始终渲染在画面最前面,不受棋盘中方块移动的影响。这就像把 UI 画在一张透明的玻璃纸上,盖在棋盘上面。
下一章预告
项目骨架搭好了,接下来构建棋盘的"神经系统"——用二维数组管理网格数据,把方块显示到屏幕上。
