3. 网格逻辑
2026/4/14大约 8 分钟
休闲益智——网格逻辑
想象你面前有一张 8x8 的方格纸,就像小时候做数学题用的那种格子本。每个格子里可以放一颗彩色糖果。现在问题是:怎么用代码来表示这张"格子纸"?怎么让电脑知道"第3行第5列放的是什么颜色的糖果"?
这就是本章要解决的问题——网格逻辑,它是三消游戏的地基。
二维数组:棋盘的"大脑"
什么是二维数组
你把一维数组想象成一排储物柜(柜子0、柜子1、柜子2......),那二维数组就是一面墙的储物柜——有行有列,你可以用"第几排第几个"来找到任何一个柜子。
在三消游戏里,棋盘就是一个二维数组:
棋盘示意图(8x8):
列: 0 1 2 3 4 5 6 7
行0: [红] [蓝] [绿] [红] [黄] [蓝] [绿] [紫]
行1: [蓝] [绿] [黄] [紫] [红] [绿] [蓝] [红]
行2: [绿] [红] [蓝] [绿] [紫] [黄] [红] [蓝]
行3: [黄] [紫] [红] [蓝] [绿] [红] [紫] [黄]
...每个格子里存一个数字:1 代表红色,2 代表蓝色,0 代表空格子(没有方块)。
本章你将学到
- 用二维数组表示 8x8 的网格棋盘
- 定义方块类型的枚举(enum)
- 初始化棋盘(随机填充,且不产生初始匹配)
- 网格坐标与屏幕坐标的互相转换
- 在屏幕上渲染棋盘
方块类型枚举
在写网格代码之前,先定义"有哪些种类的方块"。用枚举(enum)来管理,比直接写数字 1、2、3 更清晰。
C
/// <summary>
/// 方块类型枚举 —— 给每种方块一个"身份证号"。
/// 就像超市里商品分类:1号货架是水果,2号货架是蔬菜......
/// </summary>
public enum PieceType
{
Empty = 0, // 空格子(没有方块)
Red = 1, // 红色方块
Blue = 2, // 蓝色方块
Green = 3, // 绿色方块
Yellow = 4, // 黄色方块
Purple = 5 // 紫色方块
}
/// <summary>
/// 方块颜色配置 —— 把类型编号映射到实际的颜色值。
/// 就像一份"颜色对照表":查一下编号,就知道该用什么颜色。
/// </summary>
public static class PieceColors
{
public static readonly Color[] Colors = new Color[]
{
Color.Transparent, // Empty
new Color("#FF4444"), // Red
new Color("#4488FF"), // Blue
new Color("#44CC44"), // Green
new Color("#FFCC00"), // Yellow
new Color("#CC44FF"), // Purple
};
/// <summary>根据方块类型获取颜色</summary>
public static Color GetColor(PieceType type)
{
int index = (int)type;
if (index <= 0 || index >= Colors.Length)
return Color.Transparent;
return Colors[index];
}
}GDScript
## 方块类型枚举 —— 给每种方块一个"身份证号"
enum PieceType {
EMPTY = 0, ## 空格子
RED = 1, ## 红色
BLUE = 2, ## 蓝色
GREEN = 3, ## 绿色
YELLOW = 4, ## 黄色
PURPLE = 5 ## 紫色
}
## 方块颜色配置 —— 把类型编号映射到实际的颜色值
const PIECE_COLORS: Dictionary = {
PieceType.EMPTY: Color.TRANSPARENT,
PieceType.RED: Color("#FF4444"),
PieceType.BLUE: Color("#4488FF"),
PieceType.GREEN: Color("#44CC44"),
PieceType.YELLOW: Color("#FFCC00"),
PieceType.PURPLE: Color("#CC44FF"),
}
## 根据方块类型获取颜色
static func get_color(type: PieceType) -> Color:
if type in PIECE_COLORS:
return PIECE_COLORS[type]
return Color.TRANSPARENT棋盘初始化
初始化棋盘时有一个重要要求:不能在初始化时就产生匹配。否则游戏还没开始就已经有消除,这会让玩家困惑。
怎么做到?每次放一个方块时,检查它的左边两个和上边两个是否和它同色。如果是,就换一个颜色。
C
/// <summary>棋盘的行数</summary>
private const int Rows = 8;
/// <summary>棋盘的列数</summary>
private const int Cols = 8;
/// <summary>方块类型总数(不含空)</summary>
private const int TypeCount = 5;
/// <summary>
/// 棋盘数据:二维数组。
/// board[row, col] 存储方块类型
/// </summary>
private PieceType[,] board = new PieceType[Rows, Cols];
/// <summary>随机数生成器</summary>
private RandomNumberGenerator rng = new RandomNumberGenerator();
/// <summary>
/// 初始化棋盘 —— 随机填充,保证不产生初始匹配。
/// 思路:逐行逐列放方块,每次放之前检查左边两个和上面两个,
/// 如果和当前颜色一样就换一个颜色。
/// </summary>
private void InitializeBoard()
{
rng.Randomize();
for (int row = 0; row < Rows; row++)
{
for (int col = 0; col < Cols; col++)
{
PieceType candidate;
// 不断随机,直到找到一个不会产生匹配的颜色
do
{
candidate = (PieceType)rng.RandiRange(1, TypeCount);
}
while (WouldMatch(row, col, candidate));
board[row, col] = candidate;
}
}
}
/// <summary>
/// 检查在 (row, col) 放置 candidate 是否会产生匹配。
/// 只需要检查左边两个和上面两个(因为是从左到右、从上到下填充的)。
/// </summary>
private bool WouldMatch(int row, int col, PieceType candidate)
{
// 检查横向:左边两个是否和候选颜色相同
if (col >= 2
&& board[row, col - 1] == candidate
&& board[row, col - 2] == candidate)
{
return true;
}
// 检查纵向:上面两个是否和候选颜色相同
if (row >= 2
&& board[row - 1, col] == candidate
&& board[row - 2, col] == candidate)
{
return true;
}
return false;
}GDScript
## 棋盘的行数
const ROWS: int = 8
## 棋盘的列数
const COLS: int = 8
## 方块类型总数(不含空)
const TYPE_COUNT: int = 5
## 棋盘数据:二维数组
var board: Array = []
## 随机数生成器
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
## 初始化棋盘 —— 随机填充,保证不产生初始匹配
func initialize_board() -> void:
rng.randomize()
board.clear()
for row in range(ROWS):
var row_data: Array = []
for col in range(COLS):
var candidate: int
# 不断随机,直到找到一个不会产生匹配的颜色
while true:
candidate = rng.randi_range(1, TYPE_COUNT)
if not _would_match(row, col, candidate):
break
row_data.append(candidate)
board.append(row_data)
## 检查在 (row, col) 放置 candidate 是否会产生匹配
func _would_match(row: int, col: int, candidate: int) -> bool:
# 检查横向:左边两个是否和候选颜色相同
if col >= 2 \
and board[row][col - 1] == candidate \
and board[row][col - 2] == candidate:
return true
# 检查纵向:上面两个是否和候选颜色相同
if row >= 2 \
and board[row - 1][col] == candidate \
and board[row - 2][col] == candidate:
return true
return false为什么只检查左边和上面?
因为我们是从左到右、从上到下逐个填充的。当我们填到 (row, col) 时,右边的格子还是空的,下面的格子也还是空的。所以只需要检查左边和上面是否已经形成了三连。
坐标转换
棋盘有两种坐标需要频繁转换:
- 网格坐标(row, col):数据层用的坐标,比如"第3行第5列"
- 屏幕坐标(x, y):屏幕上的像素位置,比如 (360, 500)
这两种坐标就像"门牌号"和"GPS坐标"——门牌号用来在数据里找东西,GPS坐标用来在屏幕上定位。
C
/// <summary>单个方块的大小(像素)</summary>
private const int CellSize = 80;
/// <summary>方块之间的间距</summary>
private const int CellGap = 4;
/// <summary>棋盘左上角在屏幕上的起始位置</summary>
private const int BoardOffsetX = 40;
private const int BoardOffsetY = 200;
/// <summary>
/// 网格坐标 → 屏幕坐标
/// 把 (行, 列) 转成屏幕上的像素位置(方块的左上角)
/// </summary>
private Vector2 GridToScreen(int row, int col)
{
float x = BoardOffsetX + col * (CellSize + CellGap);
float y = BoardOffsetY + row * (CellSize + CellGap);
return new Vector2(x, y);
}
/// <summary>
/// 屏幕坐标 → 网格坐标
/// 把屏幕上的点击位置转换成 (行, 列)
/// </summary>
private Vector2I ScreenToGrid(Vector2 screenPos)
{
int col = (int)((screenPos.X - BoardOffsetX) / (CellSize + CellGap));
int row = (int)((screenPos.Y - BoardOffsetY) / (CellSize + CellGap));
// 检查是否在棋盘范围内
if (row >= 0 && row < Rows && col >= 0 && col < Cols)
{
return new Vector2I(row, col);
}
return new Vector2I(-1, -1); // 无效坐标
}GDScript
## 单个方块的大小(像素)
const CELL_SIZE: int = 80
## 方块之间的间距
const CELL_GAP: int = 4
## 棋盘左上角在屏幕上的起始位置
const BOARD_OFFSET_X: int = 40
const BOARD_OFFSET_Y: int = 200
## 网格坐标 → 屏幕坐标
func grid_to_screen(row: int, col: int) -> Vector2:
var x: float = BOARD_OFFSET_X + float(col) * (CELL_SIZE + CELL_GAP)
var y: float = BOARD_OFFSET_Y + float(row) * (CELL_SIZE + CELL_GAP)
return Vector2(x, y)
## 屏幕坐标 → 网格坐标
func screen_to_grid(screen_pos: Vector2) -> Vector2i:
var col: int = int((screen_pos.x - BOARD_OFFSET_X) / (CELL_SIZE + CELL_GAP))
var row: int = int((screen_pos.y - BOARD_OFFSET_Y) / (CELL_SIZE + CELL_GAP))
if row >= 0 and row < ROWS and col >= 0 and col < COLS:
return Vector2i(row, col)
return Vector2i(-1, -1)渲染棋盘
数据有了,但玩家看不到数据。我们需要把数据变成屏幕上能看到的彩色方块。这个过程叫做渲染。
用 Godot 的 ColorRect(彩色矩形)节点来渲染每个方块:
C
/// <summary>所有方块的显示节点</summary>
private ColorRect[,] blockViews = new ColorRect[Rows, Cols];
/// <summary>
/// 渲染整个棋盘 —— 为每个格子创建一个 ColorRect
/// </summary>
private void RenderBoard()
{
// 先清除旧的方块显示
foreach (var child in GetChildren())
{
child.QueueFree();
}
for (int row = 0; row < Rows; row++)
{
for (int col = 0; col < Cols; col++)
{
var rect = new ColorRect();
rect.Size = new Vector2(CellSize, CellSize);
rect.Color = PieceColors.GetColor(board[row, col]);
rect.Position = GridToScreen(row, col);
AddChild(rect);
blockViews[row, col] = rect;
}
}
}GDScript
## 所有方块的显示节点
var block_views: Array = []
## 渲染整个棋盘 —— 为每个格子创建一个 ColorRect
func render_board() -> void:
for child in get_children():
child.queue_free()
block_views.clear()
for row in range(ROWS):
var row_views: Array = []
for col in range(COLS):
var rect = ColorRect.new()
rect.size = Vector2(CELL_SIZE, CELL_SIZE)
rect.color = PieceColors.get_color(board[row][col])
rect.position = grid_to_screen(row, col)
add_child(rect)
row_views.append(rect)
block_views.append(row_views)更新单个格子
不需要每次都重绘整个棋盘。当只有一个格子发生变化时(比如交换或消除),只需要更新那个格子的颜色和位置:
// 更新单个格子的显示
blockViews[row, col].Color = PieceColors.GetColor(board[row, col]);
blockViews[row, col].Position = GridToScreen(row, col);下一章预告
网格数据结构和渲染都搞定了。下一章我们将实现三消游戏最核心的部分——方块的交换、匹配检测和消除。
