Unity Xlua热更新框架(六):场景管理

10. 实体管理与场景管理

10-1. 实体管理与场景管理

实体、场景、lua、声音等管理逻辑一样:1、xxxManager在Unity中管理这些模块,包括在Hierarchy中获取分组等,xxxManager最重要的作用是当gamestart.cs打开main.bytes执行的时候,给main.bytes提供接口(OpenUI等);2、xxxLogic运行该模块的逻辑,该模块只有框架,核心部分的实现都在xx.lua中实现,比如Update映射到lua的OnUpdate

实体管理部分:
在Scripts/Framework/Manager中创建EntityManager,Scripts/Framework/Behaviour中创建EntityLogic

private static EntityManager _entity;
public static EntityManager Entity
{
    
    
	get {
    
     return _entity; }
}

private static MySceneManager _scene;
public static MySceneManager Scene
{
    
    
    get {
    
     return _scene; }
}

public void Awake()
{
    
    
    _resource = this.gameObject.AddComponent<ResourceManager>();
    _lua = this.gameObject.AddComponent<LuaManager>();
    _ui = this.gameObject.AddComponent<UIManager>();
    _entity = this.gameObject.AddComponent<EntityManager>();
    _scene = this.gameObject.AddComponent<MySceneManager>();
}

添加Root-Entity节点

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EntityManager : MonoBehaviour
{
    
    
    //缓存实体
    Dictionary<string, GameObject> m_Entities = new Dictionary<string, GameObject>();
    //缓存实体分组的transform
    Dictionary<string, Transform> m_Groups = new Dictionary<string, Transform>();

    private Transform m_EntityParent;

    private void Awake()
    {
    
    
        m_EntityParent = this.transform.parent.Find("Entity");
    }

    /// <summary>
    /// 给Lua提供接口,方便添加实体分组
    /// </summary>
    /// <param name="groups">要添加的UI层级名称的list</param>
    public void SetEntityGroup(List<string> groups)
    {
    
    
        for (int i = 0; i < groups.Count; i++)
        {
    
    
            GameObject group = new GameObject("Group-" + groups[i]);
            group.transform.SetParent(m_EntityParent, false);
            m_Groups[groups[i]] = group.transform;
        }
    }

    /// <summary>
    /// 返回指定层级的transform
    /// </summary>
    /// <param name="group">分组名称</param>
    /// <returns>返回字典中对应分组名称的transform</returns>
    Transform GetGroup(string group)
    {
    
    
        if (!m_Groups.ContainsKey(group))
        {
    
    
            Debug.LogError("group is not exist");
        }
        return m_Groups[group];
    }

    /// <summary>
    /// 传入一个实体名字和lua名字,自动给ui预制体绑定C#脚本,自动执行lua脚本
    /// </summary>
    /// <param name="name">实体名字</param>
    /// <param name="luaName">lua名字</param>
    public void ShowEntity(string name, string group, string luaName)
    {
    
    
        GameObject entity = null;
        //如果prefab已经加载过了(从ab包取出放到Dictionary中),就只执行OnShow(Start),不在执行Init(Awake)
        if (m_Entities.TryGetValue(name, out entity))
        {
    
    
            EntityLogic logic = entity.GetComponent<EntityLogic>();
            logic.OnShow();
            return;
        }

        Manager.Resource.LoadPrefab(name, (UnityEngine.Object obj) =>
        {
    
    
            entity = Instantiate(obj) as GameObject;
            Transform parent = GetGroup(group);
            entity.transform.SetParent(parent, false);
            m_Entities.Add(name, entity);
            EntityLogic entityLogic = entity.AddComponent<EntityLogic>();
            entityLogic.Init(luaName);//Init对应Awake,OnShow对应Start
            entityLogic.OnShow();
        });
    }
}

LoadPrefab用来加载不同的预制体,预制体包含多种类型,effect,model等,需要根据传进来的内容按类型返回找到的资源

//因为prefab类型太多,必须保证传进来的就是指定类型的路径,例如effect的路径等
public void LoadPrefab(string assetName, Action<UnityEngine.Object> action = null)
{
    
    
    LoadAsset(assetName, action);
}
public class EntityLogic : LuaBehaviour
{
    
    
    Action m_LuaOnShow;
    Action m_LuaOnHide;
    public override void Init(string luaName)
    {
    
    
        base.Init(luaName);
        m_ScriptEnv.Get("OnShow", out m_LuaOnShow);
        m_ScriptEnv.Get("OnHide", out m_LuaOnHide);
    }

    public void OnOpen()
    {
    
    
        m_LuaOnShow?.Invoke();
    }
    public void OnClose()
    {
    
    
        m_LuaOnHide?.Invoke();
    }
    protected override void Clear()
    {
    
    
        base.Clear();
        m_LuaOnShow = null;
        m_LuaOnHide = null;
    }
}

场景管理部分:

  • 场景加载
    • 场景切换、叠加、激活
  • 场景卸载

Scripts-Framework-Behaviour创建SceneLogic,Scripts-Framework-Manager创建SceneManager

public class MySceneManager : MonoBehaviour
{
    
    
    private string m_LogicName = "[SceneLogic]";

    private void Awake()
    {
    
    
        //对应下面设置激活场景时候的监听
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
    }

    /// <summary>
    /// 切换场景的回调,从前面s1切换激活到s2场景
    /// </summary>
    /// <param name="s1">之前激活,现在被取消激活隐藏的场景</param>
    /// <param name="s2">现在新激活的场景</param>
    private void OnActiveSceneChanged(Scene s1, Scene s2)
    {
    
    
        if (!s1.isLoaded || !s2.isLoaded)
            return;
        SceneLogic logic1 = GetSceneLogic(s1);
        SceneLogic logic2 = GetSceneLogic(s2);

        logic1?.OnInActive();
        logic2?.OnActive();
    }

    /// <summary>
    /// 激活指定场景,对应SceneLogic的OnActive的调用
    /// </summary>
    /// <param name="sceneName"></param>
    public void SetActive(string sceneName)
    {
    
    
        Scene scene = SceneManager.GetSceneByName(sceneName);
        SceneManager.SetActiveScene(scene);
        //激活一个场景的时候,之前的场景会被隐藏,因此需要创建一个事件,用来监听哪个场景被取消激活了
    }

    /// <summary>
    /// 叠加加载场景的接口
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="luaName"></param>
    public void LoadScene(string sceneName, string luaName)
    {
    
    
        //场景是.unity,ab包解包完成后异步加载场景
        Manager.Resource.LoadScene(sceneName, (UnityEngine.Object obj) =>
        {
    
    
            StartCoroutine(StartLoadScene(sceneName, luaName, LoadSceneMode.Additive));
        });
    }
    /// <summary>
    /// 切换场景的接口
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="luaName"></param>
    public void ChangeScene(string sceneName, string luaName)
    {
    
    
        //场景是.unity,ab包解包完成后异步加载场景
        Manager.Resource.LoadScene(sceneName, (UnityEngine.Object obj) =>
        {
    
    
            //只加载单一的场景
            StartCoroutine(StartLoadScene(sceneName, luaName, LoadSceneMode.Single));
        });
    }

    /// <summary>
    /// 卸载场景
    /// </summary>
    /// <param name="sceneName"></param>
    public void UnLoadSceneAsync(string sceneName)
    {
    
    
        StartCoroutine(UnLoadScene(sceneName));
    }

    /// <summary>
    /// 检测场景是否已经加载
    /// </summary>
    /// <param name="sceneName">场景名</param>
    /// <returns></returns>
    private bool IsLoadedScene(string sceneName)
    {
    
    
        Scene scene = SceneManager.GetSceneByName(sceneName);
        return scene.isLoaded;
    }
    /// <summary>
    /// 加载场景
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="luaName"></param>
    /// <param name="mode"></param>
    /// <returns></returns>
    IEnumerator StartLoadScene(string sceneName, string luaName, LoadSceneMode mode)
    {
    
    
        //检测场景是否加载
        if (IsLoadedScene(sceneName))
            yield break;

        //异步操作协同程序
        AsyncOperation async = SceneManager.LoadSceneAsync(sceneName, mode);
        //allowSceneActivation允许场景一旦准备好就被激活,设置false的话,场景只加载90%,留下10%等待
        async.allowSceneActivation = true;
        yield return async;

        //获取加载成功的场景
        Scene scene = SceneManager.GetSceneByName(sceneName);
        //创建挂载场景逻辑脚本的物体,,每一个场景加载好,都会创建一个物体挂载SceneLogic脚本并移动到该场景中
        GameObject go = new GameObject(m_LogicName);
        //有可能加载了多个场景,把这个挂载场景逻辑脚本的物体移动到激活的场景中
        SceneManager.MoveGameObjectToScene(go, scene);

        SceneLogic logic = go.AddComponent<SceneLogic>();
		logic.SceneName = sceneName;
        logic.Init(luaName);
        logic.OnEnter();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="sceneName"></param>
    /// <returns></returns>
    private IEnumerator UnLoadScene(string sceneName)
    {
    
    
        Scene scene = SceneManager.GetSceneByName(sceneName);
        if (!scene.isLoaded)
        {
    
    
            Debug.LogError("scene not isloaded");
            yield break;
        }
        //因为由多个场景,要找个挂载SceneLogic脚本的物体到底在哪个物体身上
        SceneLogic logic = GetSceneLogic(scene);
        logic?.OnQuit(); //场景卸载调用lua的quit方法,,先调用lua的退出场景函数,再异步卸载场景
        AsyncOperation async = SceneManager.UnloadSceneAsync(scene);
        yield return async;
    }

    private SceneLogic GetSceneLogic(Scene scene)
    {
    
    
        GameObject[] gameObjects = scene.GetRootGameObjects();
        foreach (GameObject go in gameObjects)
        {
    
    
            if (go.name.CompareTo(m_LogicName) == 0)
            {
    
    
                SceneLogic logic = go.GetComponent<SceneLogic>();
                return logic;
            }
        }
        return null;
    }
}
public class SceneLogic : LuaBehaviour
{
    
    
	//lua会调用MySceneManager接口根据名字设置该场景激活
    public string SceneName;

    Action m_LuaActive;
    Action m_LuaInActive;
    Action m_LuaOnEnter;
    Action m_LuaOnQuit;
    public override void Init(string luaName)
    {
    
    
        base.Init(luaName);
        m_ScriptEnv.Get("OnActive", out m_LuaActive);//场景加载是否激活
        m_ScriptEnv.Get("OnInActive", out m_LuaInActive);
        m_ScriptEnv.Get("OnEnter", out m_LuaOnEnter);//第一次加载可以认为是首次进入场景Enter
        m_ScriptEnv.Get("OnQuit", out m_LuaOnQuit);
    }

    public void OnActive()
    {
    
    
        m_LuaActive?.Invoke();
    }
    public void OnInActive()
    {
    
    
        m_LuaInActive?.Invoke();
    }public void OnEnter()
    {
    
    
        m_LuaOnEnter?.Invoke();
    }
    public void OnQuit()
    {
    
    
        m_LuaOnQuit?.Invoke();
    }
    protected override void Clear()
    {
    
    
        base.Clear();
        m_LuaActive = null;
        m_LuaInActive = null;
        m_LuaOnEnter = null;
        m_LuaOnQuit = null;
    }
}

在场景中的Entity创建一个capsule,改名player,添加Character Controller,放进model作为预制体,,随便创建另外一个场景Test01放什么物体都行,保存到scene文件夹,,新建另一个场景Test02放一个地面
在luaScripts文件夹下面创建entity和scene文件夹,创建PlayerEntity.bytes,Scene01.bytes,Scene02.bytes
main.bytes加载TestUI时让TestUI运行TestUI.bytes,TestUI.bytes的OnOpen方法执行Manager.Scene:LoadScene加载场景Test01绑定执行Scene01.bytes并Manager.Scene:SetActive激活自己,这个Scene01.bytes又加载场景Test02绑定执行Scene02.bytes,,Scene02.bytes又加载ShowEntity加载了player.prefab并绑定了PlayerEntity.bytes

Manager = CS.Manager --引用C#里面定义的类
PathUtil = CS.PathUtil
Vector3 = CS.UnityEngine.Vector3
Input = CS.UnityEngine.Input
KeyCode = CS.UnityEngine.KeyCode
Time = CS.UnityEngine.Time

--定义UI层级
local ui_group = 
{
    
    
  "Main",
  "UI",
  "Box",
}

local entity_group = 
{
    
    
  "Player",
  "Monster",
  "Effect",
}

Manager.UI:SetUIGroup(ui_group)
Manager.Entity:SetEntityGroup(entity_group)
function Main()
  print("hello main")
  Manager.UI:OpenUI("TestUI", "UI", "ui.TestUI")

end
function OnInit()
    print("lua OnInit")
end

function OnOpen()
    print("lua OnOpen")
    Manager.Scene:LoadScene("Test01","scene.Scene01")
end

function Update()
    print("lua Update")
end

function OnClose()
    print("lua OnClose")
end
function OnInit()
    print("lua OnInit")
end

function OnActive()
    print("lua OnActive")
end

function OnEnter()
    print("lua OnEnter Scene01")
    Manager.Scene:LoadScene("Test02","scene.Scene02")
    Manager.Scene:SetActive(self.SceneName)
end

function Update()
    print("lua Update")
end

function OnInActive()
    print("lua OnInActive")
end

function OnQuit()
    print("lua OnQuit")
end
function OnInit()
    print("lua OnInit")
end

function OnActive()
    print("lua OnActive")
end

function OnEnter()
    print("lua OnEnter")
    Manager.Entity:ShowEntity(PathUtil.GetModelPath("Player"),"Player","entity.PlayerEntity")
end

function Update()
    print("lua Update")
end

function OnInActive()
    print("lua OnInActive")
end

function OnQuit()
    print("lua OnQuit")
end
function OnInit()
    print("lua OnInit")
end

function OnShow()
    print("lua OnShow")
    self.transform.localPosition = Vector3(0,2,0)
end

function Update()
    print("lua Update")
    if Input.GetKey(KeyCode.W) then
        self.transform:Translate(self.transform.forward * Time.deltaTime * 5)
    end
    if Input.GetKey(KeyCode.S) then
        self.transform:Translate(-self.transform.forward * Time.deltaTime * 5)
    end
    if Input.GetKey(KeyCode.A) then
        self.transform:Translate(-self.transform.right * Time.deltaTime * 5)
    end
    if Input.GetKey(KeyCode.D) then
        self.transform:Translate(self.transform.right * Time.deltaTime * 5)
    end
end

function OnHide()
    print("lua OnHide")
end

image.png给两个Test场景添加进去
Model-mat文件夹创建材质球,给他们赋材料,运行程序
image.png20221023_101243.gif

10-2. 场景管理测试

上面的实体测试实在GameMode == EditorMode模式下测试的,本框架为热更框架,需要进行测试
选择PackageMode测试,构建bundle,运行

InvalidOperationException: This method cannot be used on a streamed scene AssetBundle.
UnityEngine.AssetBundle.LoadAssetAsync报错
AssetBundleRequest bundleRequest = request.assetBundle.LoadAssetAsync(assetName);
结论:如果是场景scene.unity不能用这类loadxxx方法加载。
原因: 任何一个包含*.unity的资源包都属于动态场景资源包,对应assetBundle.isStreamedSceneAssetBundle = true,动态场景资源包是不允许手动去访问其内部的资源的。
重点: 如果你的资源需要通过代码访问,那么在打包时不要与*.unity文件捆绑为一个文件。

经过测试,Build Settings设置了Scene,并且AssetBundle也加载了Scene,使用SceneManager.LoadSceneAsync会优先加载AssetBundle中的Scene

而我们的项目加载了场景,但是并没有用一个obj接受这个场景,因此直接屏蔽调这句话即可。尽管没有接收这个obj,但是使用SceneManager在Editor模式下加载Build Settings中的场景,在Update模式下优先加载AssetBundle里的scene。scene加载测试,见博文

AssetBundle动态加载Scene场景_我是刘咩咩的博客

IEnumerator LoadBundleAsync(string assetName,Action<UObject> action = null)
{
    
    
    string bundleName = m_BundleInfos[assetName].BundleName;
    //这个是小写的bundle.ab的路径名
    string bundlePath = Path.Combine(PathUtil.BundleResourcePath, bundleName);
    List<string> dependences = m_BundleInfos[assetName].Dependeces;
    if(dependences != null && dependences.Count > 0)
    {
    
    
        //递归加载依赖bundle,因为依赖的资源目录名就是bundle资源名
        for (int i = 0; i < dependences.Count; i++)
        {
    
    
            yield return LoadBundleAsync(dependences[i]);
        }
    }
    //创建异步加载bundle申请
    AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(bundlePath);
    yield return request;

    if (assetName.EndsWith(".unity"))
    {
    
    
        action?.Invoke(null);
        yield break;
    }
    //从bundle申请加载指定路径名的文件,例如prefab
    AssetBundleRequest bundleRequest = request.assetBundle.LoadAssetAsync(assetName);
    yield return bundleRequest;

    //如果回调和request都不为空,语法糖
    action?.Invoke(bundleRequest?.asset);
}

猜你喜欢

转载自blog.csdn.net/weixin_42264818/article/details/128211329