基于字节数组的地形中的流水

Posted

技术标签:

【中文标题】基于字节数组的地形中的流水【英文标题】:Flowing Water in Byte Array based Terrain 【发布时间】:2020-03-31 21:42:17 【问题描述】:

我在游戏中使用字节数组来描述地形,每个字节值代表不同的方块类型

我创建了一个网格来模拟水的扩散,我给一个块上的水量一个字节值,然后我通过增加一个块的值来扩散它,这使它扩散到具有少一点水,我做了一个小视频来展示:(值 55 是一堵墙,0 空气,1 到 4 水,它是自上而下的透视图)

https://www.youtube.com/watch?v=SOmnejfmPe0

但是我在完成它时遇到了麻烦,我不希望水位上升到 4 以上,当一个已经为 4 的块变为 5 时,我希望它流过所有其他块,直到所有包含的水值为 4。但是,使用我当前的代码,如果我设置条件以便水流,无论它的值为 4 或更大,它都会以无限循环结束。

这里是当前的相关代码:

 public Vector3Int PosToCheck;
    public IEnumerator FlowRoutine(int x, int y, int z)
    
        PosToCheck = KeepPosInBounds(new Vector3Int(x - 1, y, z));
        if ( GridArray[x,y,z] > GridArray[PosToCheck.x,PosToCheck.y,PosToCheck.z] + 1  && GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] != 55)
        
            GridArray[x, y, z] -= 1;
            GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] += 1;

            yield return StartCoroutine(FlowRoutine(PosToCheck.x, PosToCheck.y, PosToCheck.z));
        
        else
        
            PosToCheck = KeepPosInBounds(new Vector3Int(x, y + 1, z));
            if (GridArray[x,y,z] > GridArray[PosToCheck.x,PosToCheck.y,PosToCheck.z] + 1  && GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] != 55)
            
                GridArray[x, y, z] -= 1;
                GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] += 1;

                yield return StartCoroutine(FlowRoutine(PosToCheck.x, PosToCheck.y, PosToCheck.z));
            
            else
            
                PosToCheck = KeepPosInBounds(new Vector3Int(x + 1, y, z));
                if (GridArray[x,y,z] > GridArray[PosToCheck.x,PosToCheck.y,PosToCheck.z] + 1  && GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] != 55)
                
                    GridArray[x, y, z] -= 1;
                    GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] += 1;

                    yield return StartCoroutine(FlowRoutine(PosToCheck.x, PosToCheck.y, PosToCheck.z));
                
                else
                
                    PosToCheck = KeepPosInBounds(new Vector3Int(x , y-1, z));
                    if (GridArray[x,y,z] > GridArray[PosToCheck.x,PosToCheck.y,PosToCheck.z] + 1  && GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] != 55)
                    
                        GridArray[x, y, z] -= 1;
                        GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] += 1;

                        yield return StartCoroutine(FlowRoutine(PosToCheck.x, PosToCheck.y, PosToCheck.z));
                    
                
            
        

        UpdateAllButtons();

        yield return null;
    

    public Vector3Int KeepPosInBounds(Vector3Int newPos)
    
        if (newPos.x < 0)  newPos.x = XGridSize - 1; 
        if (newPos.y < 0)  newPos.y = YGridSize - 1; 
        if (newPos.z < 0)  newPos.z = ZGridSize - 1; 
        if (newPos.x >= XGridSize)  newPos.x = 0; 
        if (newPos.y >= YGridSize)  newPos.y = 0; 
        if (newPos.z >= ZGridSize)  newPos.z = 0; 

        return newPos;
    

这是我试图添加的条件,但它在所有可用块都为 4 之前中断,(水在 2 个块之间的无限循环中被捕获,无限地相互传递水)

    if ((GridArray[x,y,z] > GridArray[PosToCheck.x,PosToCheck.y,PosToCheck.z] + 1 || GridArray[x, y, z]>=WaterCap) && GridArray[PosToCheck.x, PosToCheck.y, PosToCheck.z] != 55)

不胜感激有关如何解决此问题的建议,谢谢!

【问题讨论】:

具有讽刺意味的是,我认为您可能需要为此考虑某种寻路算法。考虑几乎所有水 4 的世界,只有一个位置是水 5,一个位置是水 3。什么样的算法可以确定哪个 4 流入 3,以及哪个 4 流入 5 ;这样 5 最直接地与 3 相邻,以便整个地图可以在下一个流中成为水 4?似乎是一个棘手的挑战! 我将unity3d 放回去,因为这可能需要考虑 Unity 如何实现协程。我还添加了graph-theory 作为logic 的更具体的替代品。希望有人对此有所尝试。我对人们提出的解决方案很感兴趣。 啊,我也不是 100% 依赖于协程,我只是在使用它们,所以最终当我有大量的水同时流动时,我可以慢慢观察发生的事情 对于寻路解决方案,我将如何为某个水池收集所有可能的细胞?考虑到动态地形,我一开始不知道地图上的水应该或不应该在哪里流动。 【参考方案1】:

这是您正在寻找的行为吗?

假设你有一个字节类型的二维网格,你可以使用 DFS 或 BFS 来实现这一点。 在我的例子中,我创建了一个具有 Value 属性的 Cell 类,当设置该属性时,它会自动更新单元格的文本和颜色。

接下来,我制作了一个网格管理器来存储单元格的 2D 网格。触发功能驻留在管理器中。每当我触发某个单元格时,它都会运行一个 DFS 以从该单元格开始进行更新。 DFS 逻辑具有检查边界和墙壁的条件,并且还限制了水的溢出。

void Trigger(int x, int y)

    bool[,] visited = new bool[yCount, xCount];

    for(int i=0; i<yCount; i++)
    
        for(int j=0; j<xCount; j++)
        
            visited[i, j] = false;
        
    

    DfsUpdate(y, x, visited);


void DfsUpdate(int i, int j, bool[,] visited)

    // Check out of bounds
    if (i < 0 || i >= yCount) return;
    if (j < 0 || j >= xCount) return;

    if (visited[i, j]) return;

    // Mark as visited
    visited[i, j] = true;

    if (cells[i, j].Value == 55) // Its a wall
        return;

    if (cells[i, j].Value == 0)
    
        // Needs 1 update and return
        cells[i, j].Value++;
        return;
    

    // Restrict upto 4 if its water
    cells[i, j].Value = (byte) Mathf.Min(cells[i, j].Value + 1, 4);

    // Recursively call neighbouting cells
    DfsUpdate(i + 1, j, visited);
    DfsUpdate(i - 1, j, visited);
    DfsUpdate(i, j + 1, visited);
    DfsUpdate(i, j - 1, visited);

您也可以使用 BFS,但 DFS 在这里可以正常工作,并且在使用 DFS 的情况下实施更容易。

编辑:

现在需要点击 72 次才能填充网格。查看左下角的鼠标点击次数。

我更改了代码。现在,当我单击一个单元格时,如果它已经是 4(已填充),它会找到最近的未完全填充的网格并将 1 添加到它,否则它会自行填充。 在 Update 方法中,我以一定的速率迭代网格并检查是否有任何单元格具有相邻的单元格,使得它们的水位相差 2。我填充该单元格并将其标记为已更新,以便它们不会在当前帧中再次更新。

网格更新方法是一个简单的 BFS,如果存在的话,它会将最近的未填充邻居加 1。

在更新方法中,使用计时器,每当超过某个延迟时,我都会遍历网格以平衡水位差异。

这里是代码。

void GridUpdate(int i, int j)

    bool[,] visited = new bool[yCount, xCount];
    Queue<int> iQ = new Queue<int>();
    Queue<int> jQ = new Queue<int>();
    iQ.Enqueue(i);
    jQ.Enqueue(j);

    while(iQ.Count > 0)
    
        int si = iQ.Dequeue();
        int sj = jQ.Dequeue();

        if (!CheckBounds(si, sj)) continue;

        if (cells[si, sj].Value == 55) continue;

        if (cells[si, sj].Value < 4)
        
            cells[si, sj].Value++;
            return;
        

        // Up
        iQ.Enqueue(si - 1);
        jQ.Enqueue(sj);

        // Down
        iQ.Enqueue(si + 1);
        jQ.Enqueue(sj);

        // Left
        iQ.Enqueue(si);
        jQ.Enqueue(sj - 1);

        // Right
        iQ.Enqueue(si);
        jQ.Enqueue(sj + 1);
    


void Update()

    timer += Time.deltaTime * 1000f;
    if (timer >= updateDelay)
    
        timer -= updateDelay;
        bool[,] updated = new bool[yCount, xCount];

        for (int i = 0; i < yCount; i++)
        
            for (int j = 0; j < xCount; j++)
            
                if (updated[i, j] || cells[i,j].Value == 55) continue;

                if (CheckBounds(i-1, j) && !updated[i-1, j] && cells[i, j].Value - cells[i - 1, j].Value >= 2)
                
                    updated[i - 1, j] = true;
                    cells[i - 1, j].Value++;
                    cells[i, j].Value--;
                
                else if (CheckBounds(i+1, j) && !updated[i + 1, j] && cells[i, j].Value - cells[i + 1, j].Value >= 2)
                
                    updated[i + 1, j] = true;
                    cells[i + 1, j].Value++;
                    cells[i, j].Value--;
                
                else if (CheckBounds(i, j-1) && !updated[i, j-1] && cells[i, j].Value - cells[i, j-1].Value >= 2)
                
                    updated[i, j-1] = true;
                    cells[i, j-1].Value++;
                    cells[i, j].Value--;
                
                else if (CheckBounds(i, j+1) && !updated[i, j + 1] && cells[i, j].Value - cells[i, j + 1].Value >= 2)
                
                    updated[i, j + 1] = true;
                    cells[i, j + 1].Value++;
                    cells[i, j].Value--;
                
            
        
    


// Checks if a given i and j are not out of bounds of the grid
bool CheckBounds(int i, int j)

    if (j < 0 || j >= xCount) return false;
    if (i < 0 || i >= yCount) return false;
    return true;

希望这会有所帮助!

【讨论】:

感谢您的实施,看起来很棒!只有一个问题,我不想一键向系统中添加超过 1 个水。在您的示例中,您单击了 10 次,系统中的最终总水量为 72(每 4 水 18 个细胞)。有没有办法逐步填充单元格,以便您需要 72 次点击才能达到最终结果?再次感谢! 我对 72 次点击的想法有点困惑,因为如果一次点击一次填充多个存储桶,答案将小于 72。您是否只想填充一个存储桶并且当它完全充满时,选择一个连接的桶并开始填充它?这样,您将获得 72 次点击。但是,根据视频,这似乎不是您想要实现的目标。 在我的(可怜的对不起!)示例中,我从最北端的单元格溢出水,它一次流向其他单元格 1 个单位。将 1 个单位添加到新单元格并从前一个单元格中减去 1。正因为如此,它有点像沙子而不是水一样堆积起来。即使所有可用的单元格在开始到 2 之前填充到 1 也是最好的。我想要的是 1 次点击使 1 个单位的水流过系统,在这个设置中,它在 72 次点击后结束,这 18 个单元格以 4 容量. (在我发布的视频的底部,我有一个标签“total water=”,用于跟踪系统中的水量 完美,这就是我想要的,非常感谢!

以上是关于基于字节数组的地形中的流水的主要内容,如果未能解决你的问题,请参考以下文章

vb.net中的C ++ DLL Wrapper传递字节数组的字节数组?

如何将字节数组附加到Go中的字节片[重复]

从序列文件 RecordReader 返回的错误字节数组

将字节变量添加到字节数组中的正确方法

Java中的解析字节数组

如何从 micropython 中的字节数组/字节转换?