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

Godot多线程区块加载系统问题:数组越界与位置偏移求助

Godot多线程区块加载系统问题修复

我在Godot中开发游戏的区块加载系统,尝试用多线程同时加载多个区块,实现代码如下:

int threadsToUtilise = 7;

Thread[] threads; // 存储线程的数组
ArrayMesh[] threadResults; // 保存WorldLoader.GetChunk返回的网格
Vector3I[] chunkIDsLoading; // 记录第i个线程正在加载的区块位置,后续用于定位网格

// 上述数组均初始化为threadsToUtilise(7)的大小

List<Vector3I> chunksToLoad = new(); // 存储所有待加载的区块位置

public override void _Process(double delta) // 每帧执行的方法
{
    for (var i = 0; i < threadsToUtilise; i++)
    {
        if (threads[i] == null){
            if (chunksToLoad.Count == 0) break;
            threads[i] = new Thread(() => { threadResults[i] = WorldLoader.GetChunk(chunksToLoad[0], false); });
            threads[i].Start();
            chunkIDsLoading[i] = chunksToLoad[0];
            chunksToLoad.RemoveAt(0);
        }
        else if (!threads[i].IsAlive) {
            if (threadResults[i] != null) {
                // 此处实现网格放置逻辑
            }
            threads[i] = null;
            threadResults[i] = null;
            chunkIDsLoading[i] = Vector3I.Zero;
        }
    }
}

当前系统可运行,但存在两个核心问题:

  • 随机触发“索引超出数组范围”错误,发生在创建线程并给threadResults[i]赋值的代码行,按逻辑数组初始化大小与循环条件匹配,不应出现此问题;
  • 区块有时能正确放置,有时会偏移1-2个区块位置,而生成网格的WorldLoader.GetChunk方法已验证功能正常。

问题原因与修复方案

1. 索引越界与位置偏移的共同根源:闭包变量捕获问题

代码中存在两个关键的闭包陷阱:

  • 线程的lambda表达式直接捕获循环变量i,但线程启动后主线程的循环会继续执行,i的值会被修改,当线程实际执行时,i可能已经超出数组范围,或者指向了其他线程的索引,导致threadResults[i]越界,同时也会让网格和对应的区块位置不匹配。
  • 线程内直接使用chunksToLoad[0],但主线程在启动线程后立刻执行chunksToLoad.RemoveAt(0),当线程开始执行时,chunksToLoad[0]已经变成了下一个待加载的区块位置,导致加载的区块和记录的chunkIDsLoading[i]不对应,出现位置偏移。

2. 修复后的代码

int threadsToUtilise = 7;

Thread[] threads;
ArrayMesh[] threadResults;
Vector3I[] chunkIDsLoading;

List<Vector3I> chunksToLoad = new();

public override void _Process(double delta)
{
    for (var i = 0; i < threadsToUtilise; i++)
    {
        if (threads[i] == null)
        {
            if (chunksToLoad.Count == 0) break;
            // 1. 捕获当前循环变量i的副本,避免线程执行时i值已改变
            int currentIndex = i;
            // 2. 先取出要加载的区块位置并保存到局部变量,避免主线程修改chunksToLoad影响线程
            Vector3I targetChunkPos = chunksToLoad[0];
            
            threads[currentIndex] = new Thread(() => 
            {
                threadResults[currentIndex] = WorldLoader.GetChunk(targetChunkPos, false);
            });
            threads[currentIndex].Start();
            chunkIDsLoading[currentIndex] = targetChunkPos;
            chunksToLoad.RemoveAt(0);
        }
        else if (!threads[i].IsAlive)
        {
            if (threadResults[i] != null)
            {
                // 此处用chunkIDsLoading[i]来定位网格即可
                // 示例:InstantiateMesh(threadResults[i], chunkIDsLoading[i]);
            }
            threads[i] = null;
            threadResults[i] = null;
            chunkIDsLoading[i] = Vector3I.Zero;
        }
    }
}

关键修改点

  • 创建线程前,将当前的i赋值给局部变量currentIndex,让线程的lambda捕获这个固定的副本,确保线程执行时访问的是正确的数组索引。
  • 提前将chunksToLoad[0]的值保存到局部变量targetChunkPos,线程内使用这个局部变量而不是直接访问chunksToLoad,避免主线程修改列表后导致线程加载错误的区块位置。

内容的提问来源于stack exchange,提问作者RandomFussel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 00:05:56