2. 项目搭建
2. 俄罗斯方块——项目搭建
2.1 什么是项目搭建?
想象你要盖一栋房子。在砌第一块砖之前,你需要先做好地基、搭好脚手架、规划好每个房间的用途。项目搭建就是给我们的游戏做这些准备工作。
在Godot中,项目搭建主要包括:
- 创建项目:在Godot中新建一个游戏项目
- 规划目录:把文件分门别类地放好
- 创建场景骨架:搭建游戏的基本框架
- 定义常量:把游戏中的固定数值(如网格大小)集中管理
2.2 创建Godot项目
第一步:新建项目
- 打开Godot 4.x引擎
- 点击"新建项目"(New Project)
- 项目名称填写:
Tetris - 选择渲染模式:2D(因为我们做的是2D游戏)
- 选择一个你喜欢的项目保存路径
- 点击"创建并编辑"(Create & Edit)
第二步:设置项目参数
打开菜单 项目 → 项目设置(Project → Project Settings),进行以下配置:
| 设置项 | 值 | 说明 |
|---|---|---|
| 应用程序 → 名称 | 俄罗斯方块 | 游戏显示名称 |
| 显示 → 窗口 → 宽度 | 480 | 游戏窗口宽度 |
| 显示 → 窗口 → 高度 | 720 | 游戏窗口高度 |
| 显示 → 窗口 → 不可拉伸 | 勾选 | 固定窗口大小 |
| 输入 → 键盘映射 | 见下方 | 添加游戏按键 |
第三步:配置输入映射
在"项目设置"中切换到"输入映射"(Input Map)标签页,添加以下按键:
| 动作名称 | 按键 |
|---|---|
move_left | 左方向键、A |
move_right | 右方向键、D |
soft_drop | 下方向键、S |
hard_drop | 空格 |
rotate_cw | 上方向键、W |
rotate_ccw | Z |
hold | C、Shift |
pause | Esc、P |
2.3 目录结构规划
好的目录结构就像好的收纳习惯——东西放得整齐,找起来就快。我们按照功能模块来组织文件:
res:// ← 项目根目录
├── scenes/ ← 场景文件夹
│ ├── main.tscn ← 主场景(游戏入口)
│ ├── game_board.tscn ← 游戏面板(网格区域)
│ ├── piece.tscn ← 方块场景
│ └── ui/ ← UI场景
│ ├── hud.tscn ← 游戏内界面(分数、等级)
│ ├── pause_menu.tscn ← 暂停菜单
│ ├── game_over.tscn ← 游戏结束画面
│ └── main_menu.tscn ← 主菜单
├── scripts/ ← 脚本文件夹
│ ├── game_manager.gd/.cs ← 游戏管理器(总控)
│ ├── game_board.gd/.cs ← 游戏面板逻辑
│ ├── piece.gd/.cs ← 方块逻辑
│ ├── grid.gd/.cs ← 网格数据管理
│ ├── piece_data.gd/.cs ← 方块数据定义
│ ├── scoring.gd/.cs ← 计分逻辑
│ └── constants.gd/.cs ← 常量定义
├── resources/ ← 资源文件夹
│ └── themes/ ← UI主题
├── assets/ ← 素材文件夹
│ ├── sprites/ ← 图片素材
│ ├── fonts/ ← 字体
│ └── audio/ ← 音效
│ ├── sfx/ ← 音效文件
│ └── music/ ← 背景音乐
└── autoload/ ← 自动加载(全局单例)
└── game_manager.gd/.cs ← 全局游戏管理器在Godot中创建这些文件夹:右键点击文件系统面板的 res:// → 新建文件夹。
2.4 常量定义
在写游戏逻辑之前,我们先定义一些常量——游戏中不会改变的固定数值。把常量集中放在一个文件里,方便统一管理和修改。
想象一下:如果网格大小写死在代码的各个角落里,以后想改成12列怎么办?你得一个一个去找、一个一个地改,很容易遗漏。但如果你定义了一个常量 Columns = 10,以后只需要改这一个地方就行了。
/// <summary>
/// 游戏常量定义
/// 所有不随游戏状态改变的固定数值都放在这里
/// </summary>
public static class GameConstants
{
// ===== 网格相关 =====
/// <summary>网格列数(宽度)</summary>
public const int GridColumns = 10;
/// <summary>网格行数(高度)</summary>
public const int GridRows = 20;
/// <summary>每个格子的像素大小</summary>
public const int CellSize = 32;
// ===== 方块相关 =====
/// <summary>方块从哪一行开始出现(从顶部往下数)</summary>
public const int SpawnRow = 0;
/// <summary>方块出现时的水平居中偏移</summary>
public const int SpawnColumn = 3;
// ===== 下落速度 =====
/// <summary>等级1时每格下落时间(毫秒)</summary>
public const float BaseDropInterval = 1000f;
/// <summary>每升一级减少的下落时间(毫秒)</summary>
public const float SpeedIncreasePerLevel = 75f;
/// <summary>最快下落间隔(毫秒),防止太快看不清</summary>
public const float MinDropInterval = 50f;
// ===== 计分 =====
/// <summary>每消多少行升一级</summary>
public const int LinesPerLevel = 10;
// ===== 预览队列 =====
/// <summary>Next队列中显示的方块数量</summary>
public const int PreviewCount = 3;
// ===== 锁定延迟 =====
/// <summary>方块触底后还能移动的时间(毫秒)</summary>
public const float LockDelay = 500f;
/// <summary>锁定延迟期间最大移动次数</summary>
public const int MaxLockResets = 15;
// ===== 动画 =====
/// <summary>消行动画持续时间(秒)</summary>
public const float LineClearDuration = 0.3f;
/// <summary>行消除闪烁次数</summary>
public const int LineClearFlashCount = 3;
// ===== 颜色(用于方块的视觉区分) =====
/// <summary>I形方块颜色(青色)</summary>
public static readonly Godot.Color ColorI = new Godot.Color("00f0f0");
/// <summary>O形方块颜色(黄色)</summary>
public static readonly Godot.Color ColorO = new Godot.Color("f0f000");
/// <summary>T形方块颜色(紫色)</summary>
public static readonly Godot.Color ColorT = new Godot.Color("a000f0");
/// <summary>S形方块颜色(绿色)</summary>
public static readonly Godot.Color ColorS = new Godot.Color("00f000");
/// <summary>Z形方块颜色(红色)</summary>
public static readonly Godot.Color ColorZ = new Godot.Color("f00000");
/// <summary>J形方块颜色(蓝色)</summary>
public static readonly Godot.Color ColorJ = new Godot.Color("0000f0");
/// <summary>L形方块颜色(橙色)</summary>
public static readonly Godot.Color ColorL = new Godot.Color("f0a000");
/// <summary>Ghost方块颜色(半透明投影)</summary>
public static readonly Godot.Color GhostColor = new Godot.Color(1, 1, 1, 0.2f);
/// <summary>网格线颜色</summary>
public static readonly Godot.Color GridLineColor = new Godot.Color(1, 1, 1, 0.1f);
}## 游戏常量定义
## 所有不随游戏状态改变的固定数值都放在这里
class_name GameConstants
# ===== 网格相关 =====
## 网格列数(宽度)
const GRID_COLUMNS: int = 10
## 网格行数(高度)
const GRID_ROWS: int = 20
## 每个格子的像素大小
const CELL_SIZE: int = 32
# ===== 方块相关 =====
## 方块从哪一行开始出现(从顶部往下数)
const SPAWN_ROW: int = 0
## 方块出现时的水平居中偏移
const SPAWN_COLUMN: int = 3
# ===== 下落速度 =====
## 等级1时每格下落时间(毫秒)
const BASE_DROP_INTERVAL: float = 1000.0
## 每升一级减少的下落时间(毫秒)
const SPEED_INCREASE_PER_LEVEL: float = 75.0
## 最快下落间隔(毫秒),防止太快看不清
const MIN_DROP_INTERVAL: float = 50.0
# ===== 计分 =====
## 每消多少行升一级
const LINES_PER_LEVEL: int = 10
# ===== 预览队列 =====
## Next队列中显示的方块数量
const PREVIEW_COUNT: int = 3
# ===== 锁定延迟 =====
## 方块触底后还能移动的时间(毫秒)
const LOCK_DELAY: float = 500.0
## 锁定延迟期间最大移动次数
const MAX_LOCK_RESETS: int = 15
# ===== 动画 =====
## 消行动画持续时间(秒)
const LINE_CLEAR_DURATION: float = 0.3
## 行消除闪烁次数
const LINE_CLEAR_FLASH_COUNT: int = 3
# ===== 颜色(用于方块的视觉区分) =====
## I形方块颜色(青色)
const COLOR_I: Color = Color("00f0f0")
## O形方块颜色(黄色)
const COLOR_O: Color = Color("f0f000")
## T形方块颜色(紫色)
const COLOR_T: Color = Color("a000f0")
## S形方块颜色(绿色)
const COLOR_S: Color = Color("00f000")
## Z形方块颜色(红色)
const COLOR_Z: Color = Color("f00000")
## J形方块颜色(蓝色)
const COLOR_J: Color = Color("0000f0")
## L形方块颜色(橙色)
const COLOR_L: Color = Color("f0a000")
## Ghost方块颜色(半透明投影)
const GHOST_COLOR: Color = Color(1, 1, 1, 0.2)
## 网格线颜色
const GRID_LINE_COLOR: Color = Color(1, 1, 1, 0.1)2.5 GameManager——游戏管理器
GameManager(游戏管理器) 是整个游戏的"总指挥"。它负责协调所有其他系统——什么时候生成方块、什么时候计分、什么时候结束游戏,都由它说了算。
你可以把GameManager想象成一个乐队的指挥:它不负责演奏某个具体乐器,但它决定了谁在什么时候演奏。
2.5.1 自动加载配置
为了让GameManager在任何场景中都能访问,我们需要把它设置为自动加载(Autoload):
- 菜单栏 → 项目 → 项目设置 → 自动加载 标签页
- 在"路径"中选择
scripts/game_manager.gd(或.cs) - 在"名称"中填写
GameManager - 点击"添加"
这样,GameManager 就成了全局单例,在任何脚本中都可以通过 GameManager 直接访问。
2.5.2 GameManager脚本
using Godot;
/// <summary>
/// 游戏管理器——整个游戏的"总指挥"
/// 管理游戏状态、分数、等级,协调各个子系统
/// </summary>
public partial class GameManager : Node
{
// ===== 信号定义 =====
/// <summary>分数变化时发出</summary>
[Signal] public delegate void ScoreChangedEventHandler(int newScore);
/// <summary>等级变化时发出</summary>
[Signal] public delegate void LevelChangedEventHandler(int newLevel);
/// <summary>消行时发出(参数:消除的行数)</summary>
[Signal] public delegate void LinesClearedEventHandler(int linesCleared);
/// <summary>游戏状态变化时发出</summary>
[Signal] public delegate void StateChangedEventHandler(int newState);
// ===== 游戏状态 =====
public enum GameState
{
Menu, // 主菜单
Playing, // 游戏中
Paused, // 暂停
GameOver // 游戏结束
}
private GameState _currentState = GameState.Menu;
// ===== 游戏数据 =====
private int _score = 0;
private int _level = 1;
private int _totalLinesCleared = 0;
// ===== 属性 =====
public GameState CurrentState
{
get => _currentState;
set => SetState(value);
}
public int Score => _score;
public int Level => _level;
public int TotalLinesCleared => _totalLinesCleared;
/// <summary>
/// 当前等级的下落间隔(秒)
/// </summary>
public float CurrentDropInterval
{
get
{
float interval = GameConstants.BaseDropInterval
- (_level - 1) * GameConstants.SpeedIncreasePerLevel;
return Mathf.Max(interval, GameConstants.MinDropInterval) / 1000f;
}
}
public override void _Ready()
{
GD.Print("GameManager 已初始化");
}
/// <summary>
/// 开始新游戏——重置所有数据
/// </summary>
public void StartGame()
{
_score = 0;
_level = 1;
_totalLinesCleared = 0;
SetState(GameState.Playing);
EmitSignal(SignalName.ScoreChanged, _score);
EmitSignal(SignalName.LevelChanged, _level);
}
/// <summary>
/// 添加分数
/// </summary>
public void AddScore(int points)
{
_score += points;
EmitSignal(SignalName.ScoreChanged, _score);
}
/// <summary>
/// 处理消行——更新行数和等级
/// </summary>
public void OnLinesCleared(int lines)
{
if (lines <= 0) return;
_totalLinesCleared += lines;
EmitSignal(SignalName.LinesCleared, lines);
// 检查是否升级
int newLevel = (_totalLinesCleared / GameConstants.LinesPerLevel) + 1;
if (newLevel > _level)
{
_level = newLevel;
EmitSignal(SignalName.LevelChanged, _level);
}
}
/// <summary>
/// 切换暂停状态
/// </summary>
public void TogglePause()
{
if (_currentState == GameState.Playing)
SetState(GameState.Paused);
else if (_currentState == GameState.Paused)
SetState(GameState.Playing);
}
/// <summary>
/// 游戏结束
/// </summary>
public void GameOver()
{
SetState(GameState.GameOver);
}
/// <summary>
/// 返回主菜单
/// </summary>
public void ReturnToMenu()
{
SetState(GameState.Menu);
}
/// <summary>
/// 切换游戏状态
/// </summary>
private void SetState(GameState newState)
{
if (_currentState == newState) return;
GameState oldState = _currentState;
_currentState = newState;
GetTree().Paused = (newState == GameState.Paused);
EmitSignal(SignalName.StateChanged, (int)newState);
GD.Print($"游戏状态: {oldState} → {newState}");
}
}extends Node
## 游戏管理器——整个游戏的"总指挥"
## 管理游戏状态、分数、等级,协调各个子系统
# ===== 信号定义 =====
## 分数变化时发出
signal score_changed(new_score: int)
## 等级变化时发出
signal level_changed(new_level: int)
## 消行时发出
signal lines_cleared(lines_count: int)
## 游戏状态变化时发出
signal state_changed(new_state: int)
# ===== 游戏状态 =====
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var _current_state: GameState = GameState.MENU
# ===== 游戏数据 =====
var _score: int = 0
var _level: int = 1
var _total_lines_cleared: int = 0
func _ready() -> void:
print("GameManager 已初始化")
## 当前等级的下落间隔(秒)
func get_drop_interval() -> float:
var interval: float = GameConstants.BASE_DROP_INTERVAL \
- (_level - 1) * GameConstants.SPEED_INCREASE_PER_LEVEL
return maxf(interval, GameConstants.MIN_DROP_INTERVAL) / 1000.0
## 开始新游戏——重置所有数据
func start_game() -> void:
_score = 0
_level = 1
_total_lines_cleared = 0
set_state(GameState.PLAYING)
score_changed.emit(_score)
level_changed.emit(_level)
## 添加分数
func add_score(points: int) -> void:
_score += points
score_changed.emit(_score)
## 处理消行——更新行数和等级
func on_lines_cleared(lines: int) -> void:
if lines <= 0:
return
_total_lines_cleared += lines
lines_cleared.emit(lines)
# 检查是否升级
var new_level: int = (_total_lines_cleared / GameConstants.LINES_PER_LEVEL) + 1
if new_level > _level:
_level = new_level
level_changed.emit(_level)
## 切换暂停状态
func toggle_pause() -> void:
if _current_state == GameState.PLAYING:
set_state(GameState.PAUSED)
elif _current_state == GameState.PAUSED:
set_state(GameState.PLAYING)
## 游戏结束
func game_over() -> void:
set_state(GameState.GAME_OVER)
## 返回主菜单
func return_to_menu() -> void:
set_state(GameState.MENU)
## 切换游戏状态
func set_state(new_state: GameState) -> void:
if _current_state == new_state:
return
_current_state = new_state
get_tree().paused = (new_state == GameState.PAUSED)
state_changed.emit(new_state)
print("游戏状态变化: ", new_state)2.6 Grid——网格数据管理
Grid 是管理游戏区域数据的核心类。它就像一个仓库管理员,记录着每个格子是空的还是被占了,以及被哪种颜色的方块占了。
using Godot;
using System.Collections.Generic;
/// <summary>
/// 网格数据管理——管理10x20的游戏区域
/// </summary>
public partial class Grid : Node2D
{
// 网格数据:grid[x, y] = 颜色索引(0表示空)
private int[,] _cells;
// 消行时的回调
[Signal] public delegate void LinesClearedEventHandler(
int linesCleared, List<int> clearedRows);
public override void _Ready()
{
Initialize();
}
/// <summary>
/// 初始化空网格
/// </summary>
public void Initialize()
{
_cells = new int[GameConstants.GridColumns, GameConstants.GridRows];
QueueRedraw();
}
/// <summary>
/// 检查指定位置的格子是否为空
/// </summary>
public bool IsCellEmpty(int x, int y)
{
// 超出边界视为"有东西"(当作墙壁)
if (x < 0 || x >= GameConstants.GridColumns)
return false;
if (y >= GameConstants.GridRows)
return false;
if (y < 0)
return true; // 顶部上方是空的(方块可以出现)
return _cells[x, y] == 0;
}
/// <summary>
/// 将方块的格子写入网格(方块落定时调用)
/// </summary>
public void LockPiece(int[,] pieceShape, int pieceX, int pieceY, int colorIndex)
{
int rows = pieceShape.GetLength(0);
int cols = pieceShape.GetLength(1);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if (pieceShape[row, col] != 0)
{
int gridX = pieceX + col;
int gridY = pieceY + row;
if (gridX >= 0 && gridX < GameConstants.GridColumns
&& gridY >= 0 && gridY < GameConstants.GridRows)
{
_cells[gridX, gridY] = colorIndex;
}
}
}
}
}
/// <summary>
/// 检查方块在指定位置是否合法(没有越界和重叠)
/// </summary>
public bool IsValidPosition(int[,] pieceShape, int pieceX, int pieceY)
{
int rows = pieceShape.GetLength(0);
int cols = pieceShape.GetLength(1);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if (pieceShape[row, col] != 0)
{
int gridX = pieceX + col;
int gridY = pieceY + row;
if (!IsCellEmpty(gridX, gridY))
return false;
}
}
}
return true;
}
/// <summary>
/// 检查并消除满行
/// </summary>
public int ClearLines()
{
List<int> clearedRows = new List<int>();
for (int y = GameConstants.GridRows - 1; y >= 0; y--)
{
bool isFull = true;
for (int x = 0; x < GameConstants.GridColumns; x++)
{
if (_cells[x, y] == 0)
{
isFull = false;
break;
}
}
if (isFull)
{
clearedRows.Add(y);
}
}
// 如果没有满行,直接返回
if (clearedRows.Count == 0) return 0;
// 从下往上消除
clearedRows.Sort((a, b) => b.CompareTo(a));
foreach (int row in clearedRows)
{
// 该行以上所有行下移一格
for (int y = row; y > 0; y--)
{
for (int x = 0; x < GameConstants.GridColumns; x++)
{
_cells[x, y] = _cells[x, y - 1];
}
}
// 最顶行清空
for (int x = 0; x < GameConstants.GridColumns; x++)
{
_cells[x, 0] = 0;
}
}
EmitSignal(SignalName.LinesCleared, clearedRows.Count, clearedRows);
QueueRedraw();
return clearedRows.Count;
}
/// <summary>
/// 获取指定行是否被完全填满
/// </summary>
public bool IsRowFull(int y)
{
for (int x = 0; x < GameConstants.GridColumns; x++)
{
if (_cells[x, y] == 0)
return false;
}
return true;
}
/// <summary>
/// 获取格子颜色索引
/// </summary>
public int GetCellColor(int x, int y)
{
if (x < 0 || x >= GameConstants.GridColumns) return 0;
if (y < 0 || y >= GameConstants.GridRows) return 0;
return _cells[x, y];
}
/// <summary>
/// 绘制网格和已锁定的方块
/// </summary>
public override void _Draw()
{
// 绘制网格线
DrawGridLines();
// 绘制已锁定的方块
for (int x = 0; x < GameConstants.GridColumns; x++)
{
for (int y = 0; y < GameConstants.GridRows; y++)
{
if (_cells[x, y] != 0)
{
var color = GetColorByIndex(_cells[x, y]);
DrawFilledCell(x, y, color);
}
}
}
}
private void DrawGridLines()
{
var width = GameConstants.GridColumns * GameConstants.CellSize;
var height = GameConstants.GridRows * GameConstants.CellSize;
for (int x = 0; x <= GameConstants.GridColumns; x++)
{
DrawLine(
new Vector2(x * GameConstants.CellSize, 0),
new Vector2(x * GameConstants.CellSize, height),
GameConstants.GridLineColor
);
}
for (int y = 0; y <= GameConstants.GridRows; y++)
{
DrawLine(
new Vector2(0, y * GameConstants.CellSize),
new Vector2(width, y * GameConstants.CellSize),
GameConstants.GridLineColor
);
}
}
private void DrawFilledCell(int x, int y, Color color)
{
var rect = new Rect2(
x * GameConstants.CellSize,
y * GameConstants.CellSize,
GameConstants.CellSize,
GameConstants.CellSize
);
DrawRect(rect, color);
// 绘制一个稍亮的边框,让方块更有立体感
DrawRect(rect, color.Lightened(0.3f), false, 2f);
}
private Color GetColorByIndex(int index)
{
return index switch
{
1 => GameConstants.ColorI,
2 => GameConstants.ColorO,
3 => GameConstants.ColorT,
4 => GameConstants.ColorS,
5 => GameConstants.ColorZ,
6 => GameConstants.ColorJ,
7 => GameConstants.ColorL,
_ => Colors.White
};
}
}extends Node2D
## 网格数据管理——管理10x20的游戏区域
# 网格数据:cells[x][y] = 颜色索引(0表示空)
var _cells: Array = []
# 消行时的回调
signal lines_cleared(lines_count: int, cleared_rows: Array)
func _ready() -> void:
initialize()
## 初始化空网格
func initialize() -> void:
_cells = []
for x in range(GameConstants.GRID_COLUMNS):
var column = []
for y in range(GameConstants.GRID_ROWS):
column.append(0)
_cells.append(column)
queue_redraw()
## 检查指定位置的格子是否为空
func is_cell_empty(x: int, y: int) -> bool:
# 超出边界视为"有东西"(当作墙壁)
if x < 0 or x >= GameConstants.GRID_COLUMNS:
return false
if y >= GameConstants.GRID_ROWS:
return false
if y < 0:
return true # 顶部上方是空的(方块可以出现)
return _cells[x][y] == 0
## 将方块的格子写入网格(方块落定时调用)
func lock_piece(piece_shape: Array, piece_x: int, piece_y: int, color_index: int) -> void:
for row in range(piece_shape.size()):
for col in range(piece_shape[row].size()):
if piece_shape[row][col] != 0:
var grid_x: int = piece_x + col
var grid_y: int = piece_y + row
if grid_x >= 0 and grid_x < GameConstants.GRID_COLUMNS \
and grid_y >= 0 and grid_y < GameConstants.GRID_ROWS:
_cells[grid_x][grid_y] = color_index
## 检查方块在指定位置是否合法
func is_valid_position(piece_shape: Array, piece_x: int, piece_y: int) -> bool:
for row in range(piece_shape.size()):
for col in range(piece_shape[row].size()):
if piece_shape[row][col] != 0:
var grid_x: int = piece_x + col
var grid_y: int = piece_y + row
if not is_cell_empty(grid_x, grid_y):
return false
return true
## 检查并消除满行
func clear_lines() -> int:
var cleared_rows: Array = []
for y in range(GameConstants.GRID_ROWS - 1, -1, -1):
var is_full: bool = true
for x in range(GameConstants.GRID_COLUMNS):
if _cells[x][y] == 0:
is_full = false
break
if is_full:
cleared_rows.append(y)
if cleared_rows.size() == 0:
return 0
# 从下往上消除
cleared_rows.sort()
cleared_rows.reverse()
for row in cleared_rows:
var y = row
while y > 0:
for x in range(GameConstants.GRID_COLUMNS):
_cells[x][y] = _cells[x][y - 1]
y -= 1
for x in range(GameConstants.GRID_COLUMNS):
_cells[x][0] = 0
lines_cleared.emit(cleared_rows.size(), cleared_rows)
queue_redraw()
return cleared_rows.size()
## 绘制网格和已锁定的方块
func _draw() -> void:
_draw_grid_lines()
for x in range(GameConstants.GRID_COLUMNS):
for y in range(GameConstants.GRID_ROWS):
if _cells[x][y] != 0:
var color = _get_color_by_index(_cells[x][y])
_draw_filled_cell(x, y, color)
func _draw_grid_lines() -> void:
var width = GameConstants.GRID_COLUMNS * GameConstants.CELL_SIZE
var height = GameConstants.GRID_ROWS * GameConstants.CELL_SIZE
for x in range(GameConstants.GRID_COLUMNS + 1):
draw_line(
Vector2(x * GameConstants.CELL_SIZE, 0),
Vector2(x * GameConstants.CELL_SIZE, height),
GameConstants.GRID_LINE_COLOR
)
for y in range(GameConstants.GRID_ROWS + 1):
draw_line(
Vector2(0, y * GameConstants.CELL_SIZE),
Vector2(width, y * GameConstants.CELL_SIZE),
GameConstants.GRID_LINE_COLOR
)
func _draw_filled_cell(x: int, y: int, color: Color) -> void:
var rect = Rect2(
x * GameConstants.CELL_SIZE,
y * GameConstants.CELL_SIZE,
GameConstants.CELL_SIZE,
GameConstants.CELL_SIZE
)
draw_rect(rect, color)
draw_rect(rect, color.lightened(0.3), false, 2.0)
func _get_color_by_index(index: int) -> Color:
match index:
1: return GameConstants.COLOR_I
2: return GameConstants.COLOR_O
3: return GameConstants.COLOR_T
4: return GameConstants.COLOR_S
5: return GameConstants.COLOR_Z
6: return GameConstants.COLOR_J
7: return GameConstants.COLOR_L
_: return Color.WHITE2.7 场景骨架搭建
2.7.1 主场景 (main.tscn)
主场景是整个游戏的"容器",它包含了所有子场景:
Main (Node) → 附加脚本: game_manager.gd/.cs
├── Background (ColorRect) ← 背景色
├── GameBoard (Node2D) ← 游戏面板
│ ├── Grid (Node2D) ← 网格
│ └── ActivePiece (Node2D) ← 当前方块
├── SidePanel (Control) ← 侧边面板
│ ├── HoldDisplay ← 暂存方块显示
│ ├── NextQueue ← 下一个方块预览
│ └── ScoreLabel ← 分数显示
├── HUD (CanvasLayer) ← 游戏内UI
├── PauseMenu (CanvasLayer) ← 暂停菜单
├── GameOverScreen (CanvasLayer) ← 游戏结束画面
└── MainMenu (CanvasLayer) ← 主菜单2.7.2 GameBoard场景
GameBoard是游戏的核心区域,负责管理网格和方块:
GameBoard (Node2D) → 附加脚本: game_board.gd/.cs
├── Grid (Node2D) → 附加脚本: grid.gd/.cs
└── ActivePiece (Node2D) → 附加脚本: piece.gd/.cs2.7.3 GameBoard脚本
using Godot;
/// <summary>
/// 游戏面板——管理游戏区域内的方块和网格交互
/// </summary>
public partial class GameBoard : Node2D
{
[Export] public Grid GridNode { get; set; }
private Piece _activePiece;
/// <summary>
/// 尝试生成新方块
/// </summary>
public bool SpawnPiece(PieceData.PieceType type)
{
var shape = PieceData.GetShape(type, 0);
int startX = GameConstants.SpawnColumn;
int startY = GameConstants.SpawnRow;
// 检查是否可以放置
if (!GridNode.IsValidPosition(shape, startX, startY))
{
// 游戏结束!
GameManager.Instance.GameOver();
return false;
}
// 创建方块节点
_activePiece = new Piece();
_activePiece.Initialize(type, startX, startY);
AddChild(_activePiece);
return true;
}
/// <summary>
/// 锁定当前方块到网格
/// </summary>
public void LockCurrentPiece()
{
if (_activePiece == null) return;
int colorIndex = (int)_activePiece.Type + 1;
var shape = _activePiece.CurrentShape;
GridNode.LockPiece(shape, _activePiece.GridX, _activePiece.GridY, colorIndex);
// 移除方块节点
_activePiece.QueueFree();
_activePiece = null;
// 检查消行
int lines = GridNode.ClearLines();
if (lines > 0)
{
GameManager.Instance.OnLinesCleared(lines);
int score = ScoringRules.CalculateScore(lines, GameManager.Instance.Level);
GameManager.Instance.AddScore(score);
}
}
}extends Node2D
## 游戏面板——管理游戏区域内的方块和网格交互
@export var grid_node: Grid
var _active_piece: Piece = null
## 尝试生成新方块
func spawn_piece(type: int) -> bool:
var shape = PieceData.get_shape(type, 0)
var start_x = GameConstants.SPAWN_COLUMN
var start_y = GameConstants.SPAWN_ROW
# 检查是否可以放置
if not grid_node.is_valid_position(shape, start_x, start_y):
GameManager.game_over()
return false
# 创建方块节点
_active_piece = Piece.new()
_active_piece.initialize(type, start_x, start_y)
add_child(_active_piece)
return true
## 锁定当前方块到网格
func lock_current_piece() -> void:
if _active_piece == null:
return
var color_index = _active_piece.type + 1
var shape = _active_piece.current_shape
grid_node.lock_piece(shape, _active_piece.grid_x, _active_piece.grid_y, color_index)
# 移除方块节点
_active_piece.queue_free()
_active_piece = null
# 检查消行
var lines = grid_node.clear_lines()
if lines > 0:
GameManager.on_lines_cleared(lines)
var score = ScoringRules.calculate_score(lines, GameManager.level)
GameManager.add_score(score)2.8 本章小结
在本章中,我们搭建了整个俄罗斯方块项目的基础框架:
| 内容 | 说明 |
|---|---|
| 创建项目 | 在Godot中新建2D项目,配置窗口和输入映射 |
| 目录结构 | 按功能模块组织:scenes、scripts、assets |
| 常量定义 | 集中管理网格大小、速度、颜色等固定数值 |
| GameManager | 全局游戏管理器,管理状态、分数、等级 |
| Grid | 网格数据管理,处理碰撞检测和消行 |
| 场景骨架 | Main → GameBoard → Grid + Piece 的层级结构 |
下一章我们将实现七种方块的数据系统和方块生成逻辑。
