加载场景有两种方法,一种是常规的build settings添加scene,一种是通过AssetBundle加载Scene。

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneTest : MonoBehaviour
{
    void Start()
    {
        SceneManager.LoadSceneAsync("xxx1", LoadSceneMode.Additive);
        SceneManager.LoadSceneAsync("xxx2", LoadSceneMode.Additive);
        SceneManager.LoadSceneAsync("xxx3", LoadSceneMode.Additive);
    }
}

 上面是标准的异步加载场景,如果不在build settings中添加场景,直接加载scene,就会报下面的错。

Unity3d 动态加载资源 unity 动态加载场景_Unity3d 动态加载资源

因此对于需要热更新的项目,如果场景.unity资源变更了,无法在build settings中重新设置,只能用Assetbundle加载场景。 

下面是打ab包的工具(BuildAssetBundleTool放到Scripts/Editor文件夹下)和标准化路径工具。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;

/// <summary>
/// 构建工具类:
/// 创建了三种构建方法windows android ios。
/// Build方法:从所有路径查找文件,排除meta后,把每个文件名作为被打包资源名和bundle名(当然一个bundle可以打包多个文件)
/// </summary>
public class BuildAssetBundleTool : Editor
{
    //如何使用Build呢,直接添加工具栏
    [MenuItem("Tools/Build Windows Bundle")]
    static void BundleWindowsBuild()
    {
        Build(BuildTarget.StandaloneWindows);
    }

    //如何使用Build呢,直接添加工具栏
    [MenuItem("Tools/Build Android Bundle")]
    static void BundleAndroidBuild()
    {
        Build(BuildTarget.Android);
    }

    //如何使用Build呢,直接添加工具栏
    [MenuItem("Tools/Build IOS Bundle")]
    static void BundleIOSBuild()
    {
        Build(BuildTarget.iOS);
    }

    //为了能够构建多平台,需要把目标平台作为参数传入。
    static void Build(BuildTarget target)
    {
        //主要目的是收集这个build信息,需要打哪些文件,需要给bundle包用一个什么样的名字,BuildAssetBundles函数用到这个Build数组
        List<AssetBundleBuild> assetBundleBuilds = new List<AssetBundleBuild>();

        //第一步搜索出我们这个所有文件的文件名Directory.GetDirectories和Directory.GetFiles对应两种打包策略一个获取文件夹一个获取文件,GetFiles比较简单
        //searchPattern通配符,*是默认 
        string[] files = Directory.GetFiles(PathUtil.BuildResourcesPath, "*", SearchOption.AllDirectories);
        //所有文件都找出来了,需要排除调meta文件
        for (int i = 0; i < files.Length; i++)
        {
            if (files[i].EndsWith(".meta"))
            { 
                continue; 
            }
            //创建一个需要build的Bundle
            AssetBundleBuild assetBundle = new AssetBundleBuild();

            //处理出来的路径斜杠可能不同。需要规范一下
            string fileName = PathUtil.GetStandardPath(files[i]);

            string assetName = PathUtil.GetUnityPath(fileName);//获取unity相对路径
			//一个assetBundle可以打包多个文件,这里只放一个文件
            assetBundle.assetNames = new string[] { assetName };//assetBundle是一个相对路径文件名

			//创建包名
            string bundleName = fileName.Replace(PathUtil.BuildResourcesPath, "").ToLower();
            assetBundle.assetBundleName = bundleName + ".ab";//Bundle需要后缀是.ab,,,,,,,,至此,Bundle的信息收集完了,需要放进list

            assetBundleBuilds.Add(assetBundle);
        }

        //为什么不用另一个重载BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform),是因为需要自己去资源设置bundle名打标签,很麻烦
        //第二个参数把list转为array数组
        //第三个参数是压缩格式,选择默认
        //第四个参数是目标平台,先选择win
        if(Directory.Exists(PathUtil.BundleOutPath))
        {
            //判断是否有路径,如果有这个文件夹,就删掉文件,,递归recursive删掉所有文件和子文件。
            Directory.Delete(PathUtil.BundleOutPath, true);
        }
        Directory.CreateDirectory(PathUtil.BundleOutPath);//删除路径后,创建路径

        BuildPipeline.BuildAssetBundles(PathUtil.BundleOutPath, assetBundleBuilds.ToArray(), BuildAssetBundleOptions.None, target);
    }
}
/// <summary>
/// 路径工具类:
/// 1定义了所有用到的路径
/// 2返回标准路径或返回unity下的几个文件夹的相对路径
/// </summary>
//因为所有路径都要用到,所以写入一个只读变量中,用来后期访问
public class PathUtil
{
    //为什么要把Application定义出来,因为每一次访问都需要GC一次,定义出来就访问一次;
    public static readonly string AssetPath = Application.dataPath;

    //只读的,需要打Bundle的目录
    public static readonly string BuildResourcesPath = AssetPath + "/BuildResources/";

    //Bundle输出目录
    public static readonly string BundleOutPath = Application.streamingAssetsPath;

    /// <summary>
    /// 获取Unity的相对路径
    /// </summary>
    /// <param name="path">绝对路径</param>
    /// <returns></returns>
    public static string GetUnityPath(string path)
    {
        if(string.IsNullOrEmpty(path))
        {
            return string.Empty;
        }
        //从Assets位置拿到相对目录
        return path.Substring(path.IndexOf("Assets"));
    }

    /// <summary>
    /// 获取标准路径
    /// </summary>
    /// <param name="path">路径</param>
    /// <returns></returns>
    public static string GetStandardPath(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return string.Empty;
        }
        //先处理空格,在处理反斜杠
        return path.Trim().Replace("\\", "/");
    }
}

这两个工具放到文件夹后,在unity工具栏的位置出现Editor新工具,选择第一个构建Bundle,就可以把创建好的场景打包到StreamingAssets路径下,,,在SceneTest中通过这个路径,加载AssetBundle包,就可以动态加载场景了。

Unity3d 动态加载资源 unity 动态加载场景_bundle_02

 重新编写SceneTest场景加载的脚本,挂到起始场景的任意物体上,就可以动态加载场景了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class sceneTest : MonoBehaviour
{
    // Start is called before the first frame update
    IEnumerator Start()
    {
        yield return LoadSceneAB("xxx1");
        yield return LoadSceneAB("xxx2");
        yield return LoadSceneAB("xxx3");
    }

    IEnumerator LoadSceneAB(string sceneName) 
    {
        string assetBundleName = "scenes/" + sceneName +".unity.ab";
        string path = Application.dataPath + "/StreamingAssets/" + assetBundleName;

        AssetBundleCreateRequest myLoadedAssetBundle = AssetBundle.LoadFromFileAsync(path);

        yield return myLoadedAssetBundle;
        
        if (myLoadedAssetBundle != null)
        {
            SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
        }
    }
}

Unity3d 动态加载资源 unity 动态加载场景_bundle_03

 运行前后

Unity3d 动态加载资源 unity 动态加载场景_加载_04

 如果该场景中依赖于其他物体、材质等,也是通过打包才能获取的,那么需要将其他物体也通过AssetBundle获取,获取方法需要编写一个依赖文件,说明此资源依赖于哪些资源,并通过递归的方法,加载这些依赖资源的ab包,下次再写这个地方的实现。