C# Unity3D中Firebase数据遍历foreach循环仅执行一次问题
问题根因
核心原因是Unity所有继承自UnityEngine.Object的API(包括Instantiate、GameObject操作、组件属性赋值等)严格限制只能在主线程调用,你代码里用的默认ContinueWith回调会在ThreadPool后台线程执行,触发跨线程访问异常后直接终止了整个任务逻辑,和你观察到的现象完全吻合:
snapshot.ChildrenCount能正确读到值4:这个属性是Firebase SDK拉取解析数据后自带的元数据,不依赖Unity主线程,后台线程读取完全正常- 第一次循环中Instantiate调用前的Debug.Log能正常输出:这部分代码还没触发Unity主线程访问限制,多数Unity版本允许后台线程调用Debug.Log输出文本,所以日志可以正常打印
- Instantiate执行后无日志、剩余3次循环完全不运行:调用Instantiate时会直接抛出
UnityException: Internal_CreateGameObject is not allowed to be called from a background thread异常,整个Task因未捕获的异常直接终止,后续所有代码不会再执行。
修复方案
方案1:使用Firebase自带的主线程回调扩展(推荐)
Firebase for Unity官方提供了ContinueWithOnMainThread扩展方法,会自动把回调逻辑调度到Unity主线程执行,完全满足Unity API的线程要求,只需要两步:
- 代码里引入命名空间:
using Firebase.Extensions; - 把原来的
ContinueWith替换成ContinueWithOnMainThread
修正后的核心代码示例:
using Firebase; using Firebase.Database; using Firebase.Extensions; using UnityEngine; public class TreeLoader : MonoBehaviour { public GameObject treePrefab; private DatabaseReference reference; private string scenarioName; void GetTrees() { reference.Child("Player").Child(scenarioName).GetValueAsync().ContinueWithOnMainThread(task => { if (task.IsFaulted || task.IsCanceled) { Debug.LogError($"拉取树木数据失败: {task.Exception}"); return; } DataSnapshot snapshot = task.Result; Debug.Log($"共加载到{snapshot.ChildrenCount}个树木节点"); // 遍历节点生成树木 foreach (DataSnapshot treeNode in snapshot.Children) { // 读取坐标、旋转参数,做类型校验避免脏数据中断逻辑 if (!treeNode.HasChild("posX") || !treeNode.HasChild("posY") || !treeNode.HasChild("posZ") || !treeNode.HasChild("rotX") || !treeNode.HasChild("rotY") || !treeNode.HasChild("rotZ") || !treeNode.HasChild("rotW")) { Debug.LogWarning($"跳过无效树木节点: {treeNode.Key},参数不全"); continue; } Vector3 pos = new Vector3( float.Parse(treeNode.Child("posX").Value.ToString()), float.Parse(treeNode.Child("posY").Value.ToString()), float.Parse(treeNode.Child("posZ").Value.ToString()) ); Quaternion rot = new Quaternion( float.Parse(treeNode.Child("rotX").Value.ToString()), float.Parse(treeNode.Child("rotY").Value.ToString()), float.Parse(treeNode.Child("rotZ").Value.ToString()), float.Parse(treeNode.Child("rotW").Value.ToString()) ); Debug.Log($"准备生成树木,坐标:{pos} 旋转:{rot}"); // 主线程调用Instantiate不会抛异常 Instantiate(treePrefab, pos, rot); Debug.Log($"树木{treeNode.Key}生成完成"); } }); } }
方案2:使用async/await语法(更简洁)
Unity 2018及以上版本支持async/await,Firebase的Task可以直接await,await后默认会回到Unity主线程,代码可读性更高:
async void GetTrees() { try { DataSnapshot snapshot = await reference.Child("Player").Child(scenarioName).GetValueAsync(); Debug.Log($"共加载到{snapshot.ChildrenCount}个树木节点"); // 遍历、Instantiate逻辑和上方示例一致,所有代码都在主线程执行 foreach (DataSnapshot treeNode in snapshot.Children) { // 省略参数校验、读取坐标旋转、Instantiate逻辑,和上方示例相同 } } catch (System.Exception e) { Debug.LogError($"加载树木失败: {e}"); } }
额外注意事项
- 不要在后台线程调用任何操作UnityEngine.Object的API,包括Instantiate、Transform赋值、SetActive等,否则必然抛线程访问异常
- 遍历数据时一定要加参数合法性校验,避免单个节点数据异常导致整个生成逻辑中断
- 处理Firebase Task时先判断
IsFaulted/IsCanceled状态,不要直接取Result,避免网络错误、权限错误导致未捕获异常
内容的提问来源于stack exchange,提问作者yumbert
相关产品推荐
相关产品推荐

