下落沙盒游戏实现无限2D世界的Chunk位置偏移问题求助
下落沙盒游戏无限世界Chunk位置重叠解决方案
问题描述
我正在开发一款下落沙盒游戏,想要实现类似Minecraft的无限世界。计划用World类存储所有生成的Chunk列表,但遇到了问题:所有Cell的位置基于Chunk类内的数组(比如cells[10,10]对应x=10,y=10,实际用一维数组,这里用二维数组便于说明),新增Chunk后Cell位置会重叠。我猜测可以扩大数组或绘制时偏移位置,但不确定这是否是正确做法,多方查找仍未理解正确实现方式。
附现有Chunk类代码:
public class Chunk { int Size = 5; int Width; int Height; Cell[] cells; Random r = new Random(); int[] direction = new int[2] {0, 1 }; int result = 0; public Chunk(int width, int height) { Width = width; Height = height; cells = new Cell[Width * Height]; } int GetCellIndex(int x, int y) { return x + y * Width; } Cell GetCellInLest(int index) { return cells[index]; } Cell GetCell(int x, int y) { return GetCellInLest(GetCellIndex(x, y)); } public void CellUpdated(int x, int y) { cells[GetCellIndex(x, y)].Updated = true; } public void CellNotUpdated(int x, int y) { cells[GetCellIndex(x, y)].Updated = false; } public void ReplaceCell(Cell cell, int x, int y, Cell cell1, int xto, int yto) { SetCell(x, y, cell1); SetCell(xto, yto, cell); } public void MoveCell(Cell cell, int x, int y, int xto, int yto) { SetCell(x, y, Elements.Air); SetCell(xto, yto, cell); } bool IsInBounds(int x, int y) { return x < Width && x > 0 && y < Height && y > 0; } bool IsEmpty(int x, int y) { return IsInBounds(x, y) && GetCell(x, y).Type == CellType.Empty; } public void SetCell(int x, int y, Cell cell) { cells[GetCellIndex(x, y)] = cell; } public void Update() { for (int j = Height - 1; j >= 0; j--) { for (int i = 0; i < Width; i++) { CellNotUpdated(i, j); } } for (int j = Height - 1; j >= 0; j--) { for (int i = 0; i < Width; i++) { var cell = GetCell(i, j); Rules(cell, i, j); } } } void Rules(Cell cell, int X, int Y) { if (cell.Updated == false) { cell.Updated = true; result = direction[r.Next(direction.Length)]; if (cell.Type == CellType.MovableSolid) { if (IsEmpty(X, Y + 1)) { MoveCell(cell, X, Y, X, Y + 1); } else if (IsEmpty(X + 1, Y + 1) && IsEmpty(X - 1, Y + 1)) { if (result == 0) { MoveCell(cell, X, Y, X + 1, Y + 1); } else { MoveCell(cell, X, Y, X - 1, Y + 1); } } else if (IsEmpty(X + 1, Y + 1) && IsEmpty(X + 1, Y)) { MoveCell(cell, X, Y, X + 1, Y + 1); } else if (IsEmpty(X - 1, Y + 1) && IsEmpty(X - 1, Y)) { MoveCell(cell, X, Y, X - 1, Y + 1); } } if (cell.Type == CellType.Liquid) { if (IsEmpty(X, Y + 1)) { MoveCell(cell, X, Y, X, Y + 1); } else if (IsEmpty(X + 1, Y + 1) && IsEmpty(X - 1, Y + 1)) { if (result == 0) { MoveCell(cell, X, Y, X + 1, Y + 1); } else { MoveCell(cell, X, Y, X - 1, Y + 1); } } else if (IsEmpty(X + 1, Y + 1) && IsEmpty(X + 1, Y)) { MoveCell(cell, X, Y, X + 1, Y + 1); } else if (IsEmpty(X - 1, Y + 1) && IsEmpty(X - 1, Y)) { MoveCell(cell, X, Y, X - 1, Y + 1); } else if (IsEmpty(X + 1, Y) && IsEmpty(X - 1, Y)) { if (result == 0) { MoveCell(cell, X, Y, X + 1, Y); } else { MoveCell(cell, X, Y, X - 1, Y); } } else if (IsEmpty(X + 1, Y)) { MoveCell(cell, X, Y, X + 1, Y); } else if (IsEmpty(X - 1, Y)) { MoveCell(cell, X, Y, X - 1, Y); } } if (cell.Type == CellType.Gas) { if (IsEmpty(X, Y - 1)) { MoveCell(cell, X, Y, X, Y - 1); } else if (IsEmpty(X + 1, Y - 1) && IsEmpty(X - 1, Y - 1)) { if (result == 0) { MoveCell(cell, X, Y, X + 1, Y - 1); } else { MoveCell(cell, X, Y, X - 1, Y - 1); } } else if (IsEmpty(X + 1, Y - 1) && IsEmpty(X + 1, Y)) { MoveCell(cell, X, Y, X + 1, Y + 1); } else if (IsEmpty(X - 1, Y - 1) && IsEmpty(X - 1, Y)) { MoveCell(cell, X, Y, X - 1, Y + 1); } else if (IsEmpty(X + 1, Y) && IsEmpty(X - 1, Y)) { if (result == 0) { MoveCell(cell, X, Y, X + 1, Y); } else { MoveCell(cell, X, Y, X - 1, Y); } } else if (IsEmpty(X + 1, Y)) { MoveCell(cell, X, Y, X + 1, Y); } else if (IsEmpty(X - 1, Y)) { MoveCell(cell, X, Y, X - 1, Y); } } } } public void Draw(SpriteBatch sb, Texture2D tex) { for (int j = 0; j < Height; j++) { for (int i = 0; i < Width; i++) { var cell = GetCell(i, j); if (cell.Type != CellType.Empty) { sb.Draw(tex, new Vector2((i * Size) + (2 * (Width * 2)), (j * Size) ), new Rectangle(0, 0, tex.Width, tex.Height), cell.Color, 0, Vector2.Zero, Size, SpriteEffects.None, 1); } } } } public void Paint(int x, int y, Cell cell) { if(IsInBounds(x, y)) { SetCell(x, y, cell); } } }
核心解决方案
解决这个问题的核心是给每个Chunk分配世界坐标偏移量,让每个Chunk在世界中有唯一的位置标识,彻底避免本地坐标重叠。具体分为三步:
1. 给Chunk添加世界坐标属性
修改Chunk类,增加用于标记自身在世界中位置的属性,构造函数传入该坐标:
public class Chunk { // 新增:Chunk在世界中的行列标识(比如(0,0)是初始Chunk,(1,0)是右侧相邻Chunk) public int ChunkX { get; private set; } public int ChunkY { get; private set; } int Size = 5; int Width; int Height; Cell[] cells; Random r = new Random(); int[] direction = new int[2] {0, 1 }; int result = 0; // 修改构造函数,传入Chunk的世界行列坐标 public Chunk(int width, int height, int chunkX, int chunkY) { Width = width; Height = height; ChunkX = chunkX; ChunkY = chunkY; cells = new Cell[Width * Height]; } // 原有方法保留,后续修改涉及坐标的逻辑 }
2. 修改Draw方法,基于世界坐标偏移绘制
让每个Chunk根据自身的世界坐标计算绘制起始位置,避免重叠:
public void Draw(SpriteBatch sb, Texture2D tex) { // 计算当前Chunk在世界中的起始绘制坐标 int worldStartX = ChunkX * Width * Size; int worldStartY = ChunkY * Height * Size; for (int j = 0; j < Height; j++) { for (int i = 0; i < Width; i++) { var cell = GetCell(i, j); if (cell.Type != CellType.Empty) { // 世界起始坐标 + 本地Cell偏移 = 最终绘制位置 float drawX = worldStartX + (i * Size); float drawY = worldStartY + (j * Size); sb.Draw(tex, new Vector2(drawX, drawY), new Rectangle(0, 0, tex.Width, tex.Height), cell.Color, 0, Vector2.Zero, Size, SpriteEffects.None, 1); } } } }
3. 处理跨Chunk的Cell移动逻辑
当Cell移动到当前Chunk边界外时,需要通过World类找到目标Chunk,并转换坐标:
首先在World类中实现Chunk查找逻辑:
public class World { // 用字典存储已生成的Chunk,键为Chunk的世界坐标 private Dictionary<(int chunkX, int chunkY), Chunk> _loadedChunks = new Dictionary<(int, int), Chunk>(); private int _chunkWidth = 16; // 假设每个Chunk的宽度是16个Cell private int _chunkHeight = 16; // 假设每个Chunk的高度是16个Cell private int _cellSize = 5; // 单个Cell的像素大小 // 根据世界坐标获取对应的Chunk和本地坐标 public (Chunk chunk, int localX, int localY) GetChunkAtWorldPos(int worldX, int worldY) { int chunkX = worldX / (_chunkWidth * _cellSize); int chunkY = worldY / (_chunkHeight * _cellSize); // 不存在则生成新Chunk if (!_loadedChunks.ContainsKey((chunkX, chunkY))) { _loadedChunks[(chunkX, chunkY)] = new Chunk(_chunkWidth, _chunkHeight, chunkX, chunkY); } // 计算本地坐标 int localX = (worldX % (_chunkWidth * _cellSize)) / _cellSize; int localY = (worldY % (_chunkHeight * _cellSize)) / _cellSize; return (_loadedChunks[(chunkX, chunkY)], localX, localY); } }
然后修改Chunk的IsEmpty和MoveCell方法,支持跨Chunk检测与操作:
// 修改IsEmpty方法,传入World实例用于跨Chunk检测 bool IsEmpty(int x, int y, World world) { if (x >= 0 && x < Width && y >= 0 && y < Height) { return GetCell(x, y).Type == CellType.Empty; } else { // 转换为世界坐标 int worldX = ChunkX * Width * Size + x * Size; int worldY = ChunkY * Height * Size + y * Size; var (targetChunk, localX, localY) = world.GetChunkAtWorldPos(worldX, worldY); return targetChunk.GetCell(localX, localY).Type == CellType.Empty; } } // 修改MoveCell方法,支持跨Chunk移动 public void MoveCell(Cell cell, int x, int y, int xto, int yto, World world) { if (xto >= 0 && xto < Width && yto >= 0 && yto < Height) { SetCell(x, y, Elements.Air); SetCell(xto, yto, cell); } else { // 目标在其他Chunk,转换为世界坐标 int targetWorldX = ChunkX * Width * Size + xto * Size; int targetWorldY = ChunkY * Height * Size + yto * Size; var (targetChunk, localXto, localYto) = world.GetChunkAtWorldPos(targetWorldX, targetWorldY); SetCell(x, y, Elements.Air); targetChunk.SetCell(localXto, localYto, cell); } }
最后修改Rules方法,调用上述修改后的方法时传入World实例即可。
额外注意事项
- 不要用扩大数组的方式实现无限世界,这会导致内存占用急剧膨胀,按需生成/卸载Chunk才是正确思路
- 可以将Chunk的
Random种子与ChunkX、ChunkY绑定,确保相同位置的Chunk生成内容一致,提升世界连贯性 - 世界坐标与本地坐标的转换逻辑要统一,避免出现计算错误
内容的提问来源于stack exchange,提问作者Yami
相关产品推荐
相关产品推荐

