You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

下落沙盒游戏实现无限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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 04:58:07