3. 七种方块系统
2026/4/14大约 16 分钟
3. 俄罗斯方块——七种方块系统
3.1 方块系统的"身份证"
想象一下,每个人都有一个身份证,上面记录了姓名、性别、照片等信息。我们的七种方块也需要"身份证"——记录每种方块的形状、颜色、所有旋转状态。
这一章,我们要给每种方块建立完整的数据档案,并实现一个"方块工厂"来生成方块。
3.2 七种方块一览
我们先全面认识一下这七位"主角":
| 编号 | 类型 | 形状 | 颜色 | 矩阵大小 | 特点 |
|---|---|---|---|---|---|
| 0 | I | 长条 | 青色 | 4x4 | 最长,旋转后变竖条 |
| 1 | O | 正方形 | 黄色 | 2x2 | 旋转不变,最简单 |
| 2 | T | T形 | 紫色 | 3x3 | 能做T-Spin,高阶技巧 |
| 3 | S | S形 | 绿色 | 3x3 | 左开口,和Z相反 |
| 4 | Z | Z形 | 红色 | 3x3 | 右开口,和S相反 |
| 5 | J | J形 | 蓝色 | 3x3 | 左尾巴,和L相反 |
| 6 | L | L形 | 橙色 | 3x3 | 右尾巴,和J相反 |
方块的"骨架"和"皮肤"
方块数据可以分为两部分:
- 骨架:用数字矩阵表示的形状(0和1)
- 皮肤:方块的视觉颜色
骨架决定了方块的碰撞和逻辑,皮肤决定了玩家看到的样子。两者分离的好处是:以后想换皮肤(比如节日主题)只需要改颜色,不用动逻辑。
3.3 完整的方块数据定义
下面是所有七种方块的四种旋转状态数据。每种方块有0°、90°、180°、270°四种旋转状态。
C
using Godot;
/// <summary>
/// 方块类型枚举
/// </summary>
public enum PieceType
{
I = 0, O = 1, T = 2, S = 3, Z = 4, J = 5, L = 6
}
/// <summary>
/// 方块数据——存储所有方块的旋转状态和颜色
/// 可以理解为"方块百科全书"
/// </summary>
public static class PieceData
{
// ===== 所有方块的旋转状态数据 =====
// 格式:shapes[方块类型][旋转状态][行][列]
// 值为1表示有方格,0表示空
private static readonly int[, , ,] shapes = new int[7, 4, 4, 4]
{
// ===== I形方块 (PieceType.I = 0) =====
{
// 状态0: 水平
{ {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} },
// 状态1: 竖直
{ {0,0,1,0}, {0,0,1,0}, {0,0,1,0}, {0,0,1,0} },
// 状态2: 水平(下移一格)
{ {0,0,0,0}, {0,0,0,0}, {1,1,1,1}, {0,0,0,0} },
// 状态3: 竖直(右移一格)
{ {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} }
},
// ===== O形方块 (PieceType.O = 1) =====
{
// O形旋转后形状不变,四种状态一样
{ {1,1,0,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
{ {1,1,0,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
{ {1,1,0,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
{ {1,1,0,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} }
},
// ===== T形方块 (PieceType.T = 2) =====
{
// 状态0: T字朝上
{ {0,1,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
// 状态1: T字朝右
{ {0,1,0,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0} },
// 状态2: T字朝下
{ {0,0,0,0}, {1,1,1,0}, {0,1,0,0}, {0,0,0,0} },
// 状态3: T字朝左
{ {0,1,0,0}, {1,1,0,0}, {0,1,0,0}, {0,0,0,0} }
},
// ===== S形方块 (PieceType.S = 3) =====
{
// 状态0: 水平S
{ {0,1,1,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
// 状态1: 竖直S
{ {0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0} },
// 状态2: 水平S(下移)
{ {0,0,0,0}, {0,1,1,0}, {1,1,0,0}, {0,0,0,0} },
// 状态3: 竖直S(右移)
{ {1,0,0,0}, {1,1,0,0}, {0,1,0,0}, {0,0,0,0} }
},
// ===== Z形方块 (PieceType.Z = 4) =====
{
// 状态0: 水平Z
{ {1,1,0,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
// 状态1: 竖直Z
{ {0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0} },
// 状态2: 水平Z(下移)
{ {0,0,0,0}, {1,1,0,0}, {0,1,1,0}, {0,0,0,0} },
// 状态3: 竖直Z(右移)
{ {0,1,0,0}, {1,1,0,0}, {1,0,0,0}, {0,0,0,0} }
},
// ===== J形方块 (PieceType.J = 5) =====
{
// 状态0: J字朝上
{ {1,0,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
// 状态1: J字朝右
{ {0,1,1,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0} },
// 状态2: J字朝下
{ {0,0,0,0}, {1,1,1,0}, {0,0,1,0}, {0,0,0,0} },
// 状态3: J字朝左
{ {0,1,0,0}, {0,1,0,0}, {1,1,0,0}, {0,0,0,0} }
},
// ===== L形方块 (PieceType.L = 6) =====
{
// 状态0: L字朝上
{ {0,0,1,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
// 状态1: L字朝右
{ {0,1,0,0}, {0,1,0,0}, {0,1,1,0}, {0,0,0,0} },
// 状态2: L字朝下
{ {0,0,0,0}, {1,1,1,0}, {1,0,0,0}, {0,0,0,0} },
// 状态3: L字朝左
{ {1,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0} }
}
};
// ===== 每种方块对应的颜色 =====
private static readonly Color[] colors = new Color[]
{
GameConstants.ColorI, // I - 青色
GameConstants.ColorO, // O - 黄色
GameConstants.ColorT, // T - 紫色
GameConstants.ColorS, // S - 绿色
GameConstants.ColorZ, // Z - 红色
GameConstants.ColorJ, // J - 蓝色
GameConstants.ColorL // L - 橙色
};
// ===== 每种方块的矩阵大小 =====
private static readonly Vector2I[] sizes = new Vector2I[]
{
new Vector2I(4, 4), // I
new Vector2I(2, 2), // O(实际用2x2,但统一用4x4存储)
new Vector2I(3, 3), // T
new Vector2I(3, 3), // S
new Vector2I(3, 3), // Z
new Vector2I(3, 3), // J
new Vector2I(3, 3) // L
};
/// <summary>
/// 获取指定类型和旋转状态的方块形状
/// </summary>
public static int[,] GetShape(PieceType type, int rotation)
{
int t = (int)type;
int r = ((rotation % 4) + 4) % 4; // 确保rotation在0-3范围内
int[,] result = new int[4, 4];
for (int row = 0; row < 4; row++)
{
for (int col = 0; col < 4; col++)
{
result[row, col] = shapes[t, r, row, col];
}
}
return result;
}
/// <summary>
/// 获取方块颜色
/// </summary>
public static Color GetColor(PieceType type)
{
return colors[(int)type];
}
/// <summary>
/// 获取方块的矩阵大小
/// </summary>
public static Vector2I GetSize(PieceType type)
{
return sizes[(int)type];
}
/// <summary>
/// 获取方块类型的名称(用于调试)
/// </summary>
public static string GetName(PieceType type)
{
return type.ToString();
}
}GDScript
## 方块数据——存储所有方块的旋转状态和颜色
## 可以理解为"方块百科全书"
class_name PieceData
## 方块类型枚举
enum PieceType { I = 0, O = 1, T = 2, S = 3, Z = 4, J = 5, L = 6 }
# ===== 所有方块的旋转状态数据 =====
# 格式:SHAPES[方块类型][旋转状态] = 二维数组
# 值为1表示有方格,0表示空
const SHAPES: Dictionary = {
# ===== I形方块 =====
PieceType.I: [
[[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], # 水平
[[0,0,1,0], [0,0,1,0], [0,0,1,0], [0,0,1,0]], # 竖直
[[0,0,0,0], [0,0,0,0], [1,1,1,1], [0,0,0,0]], # 水平(下移)
[[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]] # 竖直(右移)
],
# ===== O形方块 =====
PieceType.O: [
[[1,1], [1,1]], # 旋转不变
[[1,1], [1,1]],
[[1,1], [1,1]],
[[1,1], [1,1]]
],
# ===== T形方块 =====
PieceType.T: [
[[0,1,0], [1,1,1], [0,0,0]], # T字朝上
[[0,1,0], [0,1,1], [0,1,0]], # T字朝右
[[0,0,0], [1,1,1], [0,1,0]], # T字朝下
[[0,1,0], [1,1,0], [0,1,0]] # T字朝左
],
# ===== S形方块 =====
PieceType.S: [
[[0,1,1], [1,1,0], [0,0,0]], # 水平S
[[0,1,0], [0,1,1], [0,0,1]], # 竖直S
[[0,0,0], [0,1,1], [1,1,0]], # 水平S(下移)
[[1,0,0], [1,1,0], [0,1,0]] # 竖直S(右移)
],
# ===== Z形方块 =====
PieceType.Z: [
[[1,1,0], [0,1,1], [0,0,0]], # 水平Z
[[0,0,1], [0,1,1], [0,1,0]], # 竖直Z
[[0,0,0], [1,1,0], [0,1,1]], # 水平Z(下移)
[[0,1,0], [1,1,0], [1,0,0]] # 竖直Z(右移)
],
# ===== J形方块 =====
PieceType.J: [
[[1,0,0], [1,1,1], [0,0,0]], # J字朝上
[[0,1,1], [0,1,0], [0,1,0]], # J字朝右
[[0,0,0], [1,1,1], [0,0,1]], # J字朝下
[[0,1,0], [0,1,0], [1,1,0]] # J字朝左
],
# ===== L形方块 =====
PieceType.L: [
[[0,0,1], [1,1,1], [0,0,0]], # L字朝上
[[0,1,0], [0,1,0], [0,1,1]], # L字朝右
[[0,0,0], [1,1,1], [1,0,0]], # L字朝下
[[1,1,0], [0,1,0], [0,1,0]] # L字朝左
]
}
# ===== 每种方块对应的颜色 =====
const COLORS: Dictionary = {
PieceType.I: GameConstants.COLOR_I, # 青色
PieceType.O: GameConstants.COLOR_O, # 黄色
PieceType.T: GameConstants.COLOR_T, # 紫色
PieceType.S: GameConstants.COLOR_S, # 绿色
PieceType.Z: GameConstants.COLOR_Z, # 红色
PieceType.J: GameConstants.COLOR_J, # 蓝色
PieceType.L: GameConstants.COLOR_L # 橙色
}
# ===== 每种方块的矩阵大小 =====
const SIZES: Dictionary = {
PieceType.I: Vector2i(4, 4),
PieceType.O: Vector2i(2, 2),
PieceType.T: Vector2i(3, 3),
PieceType.S: Vector2i(3, 3),
PieceType.Z: Vector2i(3, 3),
PieceType.J: Vector2i(3, 3),
PieceType.L: Vector2i(3, 3)
}
## 获取指定类型和旋转状态的方块形状
static func get_shape(type: int, rotation: int) -> Array:
var r: int = ((rotation % 4) + 4) % 4
return SHAPES[type][r].duplicate(true)
## 获取方块颜色
static func get_color(type: int) -> Color:
return COLORS[type]
## 获取方块的矩阵大小
static func get_size(type: int) -> Vector2i:
return SIZES[type]
## 获取方块类型的名称(用于调试)
static func get_name(type: int) -> String:
match type:
PieceType.I: return "I"
PieceType.O: return "O"
PieceType.T: return "T"
PieceType.S: return "S"
PieceType.Z: return "Z"
PieceType.J: return "J"
PieceType.L: return "L"
_: return "Unknown"3.4 方块生成器——7-Bag系统
你可能会想:方块随机生成不就行了?但纯随机有一个问题——有时候可能连续给你好几个相同的方块,让你根本没法玩。
7-Bag系统解决了这个问题。它的原理很简单:
- 把七种方块各放一个到"袋子"里
- 把袋子里的方块打乱顺序
- 按打乱后的顺序一个一个拿出来
- 袋子空了就重新装满、重新打乱
这样就能保证:每7个方块里,每种都恰好出现一次。不会连续出现太多相同方块,也不会很久都不出现某种方块。
想象一副扑克牌:你把所有牌洗好,一张一张翻开。你不会连续看到10张黑桃A,因为一副牌里只有4张A。
C
using System.Collections.Generic;
using Godot;
/// <summary>
/// 方块生成器——使用7-Bag随机系统
/// 确保每7个方块中每种恰好出现一次
/// </summary>
public partial class PieceSpawner : Node
{
// 当前袋子中剩余的方块
private Queue<PieceType> _bag = new Queue<PieceType>();
// 下一个方块队列(用于预览)
private Queue<PieceType> _nextQueue = new Queue<PieceType>();
// 预览队列长度
private const int QueueSize = 3;
/// <summary>
/// 初始化——填满预览队列
/// </summary>
public override void _Ready()
{
FillBag();
for (int i = 0; i < QueueSize; i++)
{
_nextQueue.Enqueue(_bag.Dequeue());
}
}
/// <summary>
/// 装满袋子——7种方块各一个,打乱顺序
/// </summary>
private void FillBag()
{
// 创建包含所有7种方块的列表
List<PieceType> allPieces = new List<PieceType>
{
PieceType.I, PieceType.O, PieceType.T,
PieceType.S, PieceType.Z, PieceType.J, PieceType.L
};
// Fisher-Yates洗牌算法
// 就像洗扑克牌一样,随机交换位置
var random = new Random();
for (int i = allPieces.Count - 1; i > 0; i--)
{
int j = random.Next(0, i + 1);
// 交换i和j位置的元素
PieceType temp = allPieces[i];
allPieces[i] = allPieces[j];
allPieces[j] = temp;
}
// 把打乱后的方块放入袋子
foreach (var piece in allPieces)
{
_bag.Enqueue(piece);
}
}
/// <summary>
/// 取出下一个方块
/// </summary>
public PieceType GetNextPiece()
{
// 如果袋子空了,重新装满
if (_bag.Count == 0)
{
FillBag();
}
// 从袋子中取出一个
PieceType piece = _bag.Dequeue();
// 把它加入队列尾部,并从队列头部取出"下一个"方块
_nextQueue.Enqueue(piece);
PieceType result = _nextQueue.Dequeue();
return result;
}
/// <summary>
/// 查看预览队列中的方块(不取出)
/// </summary>
public PieceType PeekPreview(int index)
{
if (index < 0 || index >= _nextQueue.Count)
return PieceType.T;
// 把队列转成数组来访问
var array = _nextQueue.ToArray();
return array[index];
}
/// <summary>
/// 重置生成器
/// </summary>
public void Reset()
{
_bag.Clear();
_nextQueue.Clear();
FillBag();
for (int i = 0; i < QueueSize; i++)
{
_nextQueue.Enqueue(_bag.Dequeue());
}
}
}GDScript
extends Node
## 方块生成器——使用7-Bag随机系统
## 确保每7个方块中每种恰好出现一次
# 当前袋子中剩余的方块
var _bag: Array = []
# 下一个方块队列(用于预览)
var _next_queue: Array = []
# 预览队列长度
const QUEUE_SIZE: int = 3
func _ready() -> void:
_fill_bag()
for i in range(QUEUE_SIZE):
_next_queue.append(_bag.pop_front())
## 装满袋子——7种方块各一个,打乱顺序
func _fill_bag() -> void:
# 创建包含所有7种方块的列表
var all_pieces: Array = [
PieceData.PieceType.I, PieceData.PieceType.O, PieceData.PieceType.T,
PieceData.PieceType.S, PieceData.PieceType.Z, PieceData.PieceType.J,
PieceData.PieceType.L
]
# Fisher-Yates洗牌算法
# 就像洗扑克牌一样,随机交换位置
for i in range(all_pieces.size() - 1, 0, -1):
var j: int = randi() % (i + 1)
# 交换i和j位置的元素
var temp = all_pieces[i]
all_pieces[i] = all_pieces[j]
all_pieces[j] = temp
# 把打乱后的方块放入袋子
_bag = all_pieces.duplicate()
## 取出下一个方块
func get_next_piece() -> int:
# 如果袋子空了,重新装满
if _bag.size() == 0:
_fill_bag()
# 从袋子中取出一个
var piece: int = _bag.pop_front()
# 把它加入队列尾部,并从队列头部取出"下一个"方块
_next_queue.append(piece)
var result: int = _next_queue.pop_front()
return result
## 查看预览队列中的方块(不取出)
func peek_preview(index: int) -> int:
if index < 0 or index >= _next_queue.size():
return PieceData.PieceType.T
return _next_queue[index]
## 重置生成器
func reset() -> void:
_bag.clear()
_next_queue.clear()
_fill_bag()
for i in range(QUEUE_SIZE):
_next_queue.append(_bag.pop_front())3.5 Piece——方块实例
PieceData 是"方块百科全书"(存储所有种类的数据),而 Piece 是某一个具体的方块实例。就像"物种"和"个体"的关系——百科全书里描述了猫长什么样,而你家那只橘猫就是一个具体的实例。
每个 Piece 实例需要记住:
- 它是哪种方块(类型)
- 它现在在哪里(网格坐标)
- 它旋转到了什么状态(0-3)
- 它的颜色是什么
C
using Godot;
/// <summary>
/// 方块实例——代表游戏区域中的一个具体方块
/// </summary>
public partial class Piece : Node2D
{
// ===== 属性 =====
/// <summary>方块类型</summary>
public PieceType Type { get; private set; }
/// <summary>在网格中的X坐标(列号)</summary>
public int GridX { get; set; }
/// <summary>在网格中的Y坐标(行号)</summary>
public int GridY { get; set; }
/// <summary>当前旋转状态(0-3)</summary>
public int Rotation { get; private set; }
/// <summary>方块颜色</summary>
public Color PieceColor { get; private set; }
/// <summary>获取当前形状的矩阵</summary>
public int[,] CurrentShape => PieceData.GetShape(Type, Rotation);
// ===== 初始化 =====
/// <summary>
/// 初始化方块
/// </summary>
public void Initialize(PieceType type, int startX, int startY)
{
Type = type;
GridX = startX;
GridY = startY;
Rotation = 0;
PieceColor = PieceData.GetColor(type);
GD.Print($"生成方块: {type} 在 ({startX}, {startY})");
QueueRedraw();
}
/// <summary>
/// 设置旋转状态
/// </summary>
public void SetRotation(int newRotation)
{
Rotation = ((newRotation % 4) + 4) % 4;
QueueRedraw();
}
/// <summary>
/// 顺时针旋转90度
/// </summary>
public void RotateClockwise()
{
SetRotation(Rotation + 1);
}
/// <summary>
/// 逆时针旋转90度
/// </summary>
public void RotateCounterClockwise()
{
SetRotation(Rotation - 1);
}
/// <summary>
/// 移动到指定位置
/// </summary>
public void MoveTo(int newX, int newY)
{
GridX = newX;
GridY = newY;
Position = new Vector2(
GridX * GameConstants.CellSize,
GridY * GameConstants.CellSize
);
}
/// <summary>
/// 绘制方块
/// </summary>
public override void _Draw()
{
var shape = CurrentShape;
int rows = shape.GetLength(0);
int cols = shape.GetLength(1);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if (shape[row, col] != 0)
{
// 绘制每个小方格
var rect = new Rect2(
col * GameConstants.CellSize,
row * GameConstants.CellSize,
GameConstants.CellSize,
GameConstants.CellSize
);
// 填充颜色
DrawRect(rect, PieceColor);
// 绘制亮边框(上方和左方)
DrawRect(rect, PieceColor.Lightened(0.3f), false, 2f);
// 绘制暗边框(下方和右方),营造立体感
var innerRect = rect.GrowIndividual(-2, -2, -2, -2);
DrawRect(innerRect, PieceColor.Darkened(0.2f), false, 1f);
}
}
}
}
/// <summary>
/// 获取方块所有格子在世界坐标中的位置
/// </summary>
public Vector2I[] GetCellPositions()
{
var shape = CurrentShape;
var positions = new System.Collections.Generic.List<Vector2I>();
int rows = shape.GetLength(0);
int cols = shape.GetLength(1);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if (shape[row, col] != 0)
{
positions.Add(new Vector2I(GridX + col, GridY + row));
}
}
}
return positions.ToArray();
}
}GDScript
extends Node2D
## 方块实例——代表游戏区域中的一个具体方块
# ===== 属性 =====
## 方块类型
var type: int = PieceData.PieceType.T
## 在网格中的X坐标(列号)
var grid_x: int = 0
## 在网格中的Y坐标(行号)
var grid_y: int = 0
## 当前旋转状态(0-3)
var rotation: int = 0
## 方块颜色
var piece_color: Color = Color.WHITE
## 获取当前形状的矩阵
var current_shape: Array:
get:
return PieceData.get_shape(type, rotation)
# ===== 初始化 =====
## 初始化方块
func initialize(piece_type: int, start_x: int, start_y: int) -> void:
type = piece_type
grid_x = start_x
grid_y = start_y
rotation = 0
piece_color = PieceData.get_color(piece_type)
print("生成方块: ", PieceData.get_name(type), " 在 (", start_x, ", ", start_y, ")")
queue_redraw()
## 设置旋转状态
func set_rotation(new_rotation: int) -> void:
rotation = ((new_rotation % 4) + 4) % 4
queue_redraw()
## 顺时针旋转90度
func rotate_clockwise() -> void:
set_rotation(rotation + 1)
## 逆时针旋转90度
func rotate_counter_clockwise() -> void:
set_rotation(rotation - 1)
## 移动到指定位置
func move_to(new_x: int, new_y: int) -> void:
grid_x = new_x
grid_y = new_y
position = Vector2(
grid_x * GameConstants.CELL_SIZE,
grid_y * GameConstants.CELL_SIZE
)
## 绘制方块
func _draw() -> void:
var shape = current_shape
for row in range(shape.size()):
for col in range(shape[row].size()):
if shape[row][col] != 0:
# 绘制每个小方格
var rect = Rect2(
col * GameConstants.CELL_SIZE,
row * GameConstants.CELL_SIZE,
GameConstants.CELL_SIZE,
GameConstants.CELL_SIZE
)
# 填充颜色
draw_rect(rect, piece_color)
# 绘制亮边框(上方和左方)
draw_rect(rect, piece_color.lightened(0.3), false, 2.0)
# 绘制暗边框(下方和右方),营造立体感
var inner_rect = rect.grow_individual(-2, -2, -2, -2)
draw_rect(inner_rect, piece_color.darkened(0.2), false, 1.0)
## 获取方块所有格子在世界坐标中的位置
func get_cell_positions() -> Array:
var shape = current_shape
var positions = []
for row in range(shape.size()):
for col in range(shape[row].size()):
if shape[row][col] != 0:
positions.append(Vector2i(grid_x + col, grid_y + row))
return positions3.6 Hold(暂存)功能
Hold(暂存) 允许玩家把当前方块"存起来",以后需要的时候再拿出来用。规则是:
- 每次放置方块后才能再次使用Hold
- 如果Hold区已经有方块了,就交换当前方块和Hold区的方块
- 如果Hold区是空的,就把当前方块存进去,直接生成下一个方块
C
/// <summary>
/// Hold(暂存)管理器
/// </summary>
public partial class HoldManager : Node
{
/// <summary>Hold区中的方块类型(null表示空)</summary>
private PieceType? _heldPiece = null;
/// <summary>本回合是否已经使用过Hold</summary>
private bool _usedThisTurn = false;
/// <summary>Hold状态变化时的信号</summary>
[Signal] public delegate void HoldChangedEventHandler(PieceType? pieceType);
/// <summary>
/// 尝试暂存当前方块
/// 返回:如果Hold区有方块,返回那个方块(用于交换)
/// 如果Hold区为空,返回null
/// </summary>
public PieceType? Hold(PieceType currentPiece)
{
if (_usedThisTurn) return currentPiece; // 本回合已使用过
_usedThisTurn = true;
if (_heldPiece == null)
{
// Hold区为空,直接存入
_heldPiece = currentPiece;
EmitSignal(SignalName.HoldChanged, null);
return null;
}
else
{
// Hold区有方块,交换
PieceType swapPiece = _heldPiece.Value;
_heldPiece = currentPiece;
EmitSignal(SignalName.HoldChanged, (int)currentPiece);
return swapPiece;
}
}
/// <summary>
/// 新方块锁定后重置Hold使用状态
/// </summary>
public void ResetHoldState()
{
_usedThisTurn = false;
}
/// <summary>
/// 获取当前Hold区的方块
/// </summary>
public PieceType? GetHeldPiece()
{
return _heldPiece;
}
/// <summary>
/// 重置Hold区(新游戏时调用)
/// </summary>
public void Reset()
{
_heldPiece = null;
_usedThisTurn = false;
EmitSignal(SignalName.HoldChanged, null);
}
}GDScript
extends Node
## Hold(暂存)管理器
## Hold区中的方块类型(-1表示空)
var _held_piece: int = -1
## 本回合是否已经使用过Hold
var _used_this_turn: bool = false
## Hold状态变化时的信号
signal hold_changed(piece_type: int)
## 尝试暂存当前方块
## 返回:如果Hold区有方块,返回那个方块类型(用于交换)
## 如果Hold区为空,返回-1
func hold(current_piece: int) -> int:
if _used_this_turn:
return current_piece # 本回合已使用过
_used_this_turn = true
if _held_piece == -1:
# Hold区为空,直接存入
_held_piece = current_piece
hold_changed.emit(-1)
return -1
else:
# Hold区有方块,交换
var swap_piece: int = _held_piece
_held_piece = current_piece
hold_changed.emit(current_piece)
return swap_piece
## 新方块锁定后重置Hold使用状态
func reset_hold_state() -> void:
_used_this_turn = false
## 获取当前Hold区的方块
func get_held_piece() -> int:
return _held_piece
## 重置Hold区(新游戏时调用)
func reset() -> void:
_held_piece = -1
_used_this_turn = false
hold_changed.emit(-1)3.7 本章小结
本章我们完成了方块系统的核心实现:
| 组件 | 说明 |
|---|---|
| PieceData | "方块百科全书"——存储7种方块的所有旋转状态和颜色 |
| PieceSpawner | "方块工厂"——使用7-Bag系统随机生成方块 |
| Piece | "方块实例"——代表游戏区域中的一个具体方块 |
| HoldManager | "暂存管理员"——管理Hold功能的交换逻辑 |
下一章我们将实现方块的旋转系统和墙踢检测。
