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
相关产品推荐
相关产品推荐

