如何使用C#在MonoGame框架中实现2D网格
MonoGame 通用2D网格实现方案(适配生命游戏/寻路/沙盒模拟)
这套实现和具体业务逻辑解耦,底层结构写完可以直接复用到所有2D网格类项目,基于C# + MonoGame编写,不需要额外依赖。
分步实现
1. 先写纯数据层核心结构(和渲染逻辑解耦)
不要一开始就绑定渲染逻辑,先把网格、格子的纯数据定义写清楚,这部分换任何C#游戏框架都能复用。
首先定义通用格子基类,预留扩展空间适配不同项目需求:
// 可按需扩展的通用格子基类 public class GridCell { // 网格内的整数坐标,从网格左上角(0,0)开始计数 public int GridX { get; set; } public int GridY { get; set; } // 对应MonoGame世界坐标系下的格子左上角坐标,渲染、交互检测直接调用 public Vector2 WorldPosition { get; set; } // 通用状态字段:生命游戏存活/死标记,寻路存通行代价/障碍标记,沙盒存材料ID public int State { get; set; } }
再定义网格核心类,存基础参数和格子容器:
public class BaseGrid { // 单个格子的像素边长,正方形网格统一用单值即可,长方形网格可拆为CellWidth/CellHeight public int CellSize { get; private set; } // 网格横向总格子数(列数)、纵向总格子数(行数) public int Columns { get; private set; } public int Rows { get; private set; } // 网格整体在世界坐标系下的偏移值,要调整网格在屏幕上的位置改这个参数即可 public Vector2 GridOrigin { get; private set; } // 存储所有格子的二维数组,规整网格场景下性能优于交错数组 private GridCell[,] _cells; }
2. 实现网格初始化逻辑
在网格类的构造方法里完成所有格子的生成,提前算好每个格子的世界坐标,避免后续每帧重复计算:
public BaseGrid(int cellSize, int columns, int rows, Vector2 gridOrigin) { CellSize = cellSize; Columns = columns; Rows = rows; GridOrigin = gridOrigin; _cells = new GridCell[columns, rows]; // 遍历生成所有格子 for (int x = 0; x < Columns; x++) { for (int y = 0; y < Rows; y++) { _cells[x, y] = new GridCell { GridX = x, GridY = y, // 世界坐标计算公式:网格原点 + 格子序号 * 单格尺寸 WorldPosition = new Vector2( GridOrigin.X + x * CellSize, GridOrigin.Y + y * CellSize ), State = 0 // 可按需修改默认初始状态 }; } } }
3. 实现所有项目通用的工具方法
这部分逻辑写完不用再改,所有网格类项目都能直接用:
- 坐标转换方法:实现世界坐标(比如鼠标位置)到对应格子的转换,做交互绘制、点击检测必用,内置边界校验避免数组越界
// 输入世界坐标,返回对应位置的格子,坐标不在网格范围内返回null public GridCell? GetCellAtWorldPosition(Vector2 worldPos) { int gridX = (int)Math.Floor((worldPos.X - GridOrigin.X) / CellSize); int gridY = (int)Math.Floor((worldPos.Y - GridOrigin.Y) / CellSize); if (gridX < 0 || gridX >= Columns || gridY < 0 || gridY >= Rows) return null; return _cells[gridX, gridY]; } // 按网格整数坐标取格子,内置边界校验 public GridCell? GetCell(int gridX, int gridY) { if (gridX < 0 || gridX >= Columns || gridY < 0 || gridY >= Rows) return null; return _cells[gridX, gridY]; }
- 邻接格子获取方法:生命游戏统计存活邻居、寻路遍历相邻节点、沙盒模拟元素扩散/反应都要用到,支持切换4邻接(上下左右)、8邻接(加四个斜角)
public List<GridCell> GetNeighbors(GridCell cell, bool includeDiagonals = false) { List<GridCell> neighbors = new List<GridCell>(); // 上下左右四个方向的坐标偏移 int[] dx = { 0, 0, -1, 1 }; int[] dy = { -1, 1, 0, 0 }; if (includeDiagonals) { // 追加斜角四个方向的偏移 dx = dx.Concat(new[] { -1, -1, 1, 1 }).ToArray(); dy = dy.Concat(new[] { -1, 1, -1, 1 }).ToArray(); } for (int i = 0; i < dx.Length; i++) { var neighbor = GetCell(cell.GridX + dx[i], cell.GridY + dy[i]); if (neighbor != null) neighbors.Add(neighbor); } return neighbors; }
4. MonoGame渲染层实现
不要每帧新建纹理,提前在Game类的LoadContent阶段创建1x1白色纯色纹理,所有矩形、线条渲染都复用这一个纹理,能大幅降低Draw Call,几百乘几百的大网格也能流畅跑:
private Texture2D _pixelTexture; protected override void LoadContent() { _pixelTexture = new Texture2D(GraphicsDevice, 1, 1); _pixelTexture.SetData(new[] { Color.White }); }
- 填充单个格子的示例:
// 传入SpriteBatch、要绘制的格子、填充颜色即可 spriteBatch.Draw( _pixelTexture, cell.WorldPosition, null, drawColor, 0f, Vector2.Zero, new Vector2(CellSize), SpriteEffects.None, 0f );
- 画网格线:不要逐格画边框,直接按行、列画直线性能更好,横向线从网格左边缘画到右边缘,纵向线从上边缘画到下边缘即可。
不同项目的适配方式
- 康威生命游戏:不需要修改底层结构,每帧遍历所有格子,调用
GetNeighbors(cell, true)统计8邻接内存活细胞数量,按规则计算下一代状态即可。注意用双缓冲存储状态:不要在遍历过程中直接修改当前帧的格子状态,先把下一代所有格子的状态存在单独的数组里,全部计算完成后再统一更新到网格上,避免同帧状态互相干扰导致结果错误。 - 路径寻路(A*/BFS/DFS):给
GridCell扩展GCost、HCost、ParentNode三个字段即可,State字段用来标记可通行、障碍、起点、终点,邻接方法默认用4邻接,直接在网格上写寻路逻辑即可。 - 沙盒模拟(粉末类游戏):把
State字段和不同材料ID做映射(0=空气、1=沙子、2=水、3=石头等),每帧按规则顺序遍历格子,调用邻接方法判断周围格子状态,实现重力、流动、元素反应逻辑即可。
常见避坑
- 不要混用网格整数坐标和世界浮点坐标,所有坐标转换统一用封装好的
GetCellAtWorldPosition方法,避免手动计算出现坐标偏移。 - 所有取格子、取邻接的逻辑必须做边界校验,否则鼠标点到网格外、取网格边缘格子的邻接时会直接抛出数组越界异常。
- 大尺寸网格不要逐格新建纹理,复用提前创建的1x1白纹理改颜色即可,否则会出现严重的性能卡顿。
内容的提问来源于stack exchange,提问作者Yami
相关产品推荐
相关产品推荐

