[Unity3D热更框架] LuaMVC之C#与Lua混编

1.C#与Lua混编的思考

  为什么要单独聊这个问题,因为我觉得很多新手都不清楚在一个项目中该全部用Lua来写,还是部分用Lua部分用C#,而且什么逻辑用Lua,什么逻辑用C#。以下的内容将帮助你解决这个疑惑。

1.1 C#和Lua应该占据什么样的比例

  在最初接触Lua时候我相信多数人都会遇到一个问题,就是既然C#也可以写业务逻辑Lua也可以写业务逻辑,那当我们遇到一个业务时,到底该用C#还是Lua呢?这个问题困扰了我很久,我从成熟的uluaFramework那里学习到似乎框架的部分用C#写,业务逻辑全部使用Lua来完成。从别的项目经验中我也了解到对于性能要求较高的模块用C#写(比如游戏中的战斗逻辑),其余模块用Lua来写。

  当我再次了解到Lua的时候是腾讯的XLua开源项目,其中的热补丁特性让我重新开始思考过去对于C#和Lua混编占比的思考,似乎用热补丁的话项目就全部使用C#编写,需要修复bug或者是热更的部分用Hotfix的方式(Lua注入C#代码)来完成似乎是更加合适的。因为C#编码速度确实是要比Lua快很多,强类型语言以及强大的IDE是Lua无法比拟的。但是似乎Hotfix方式也并非是最合适的,因为如果在整个项目中想依靠Hotfix(热补丁)的方式来热更或修复bug,就需要在开发项目期间就对C#代码做很多特性标注,而且在编译时也会产生不少的额外代码。

  但是无意之中的翻阅XLua教程的时候看到一句话”如果lua测的实现的部分都以delegate和interface的方式提供,使用方可以完全和xLua解耦:由一个专门的模块负责xlua的初始化以及delegate、interface的映射,然后把这些delegate和interface设置到要用到它们的地方。“,关于C#与Lua混编我有了新的主意。

  像观察者设计模式一样,当有事件注入到观察者中时才会由观察者来执行。把C#框架部分当作是观察者,把Lua部分只当作是注入到其中执行的事件,那么在编码时无论有没有Lua代码都不在重要,因为没有Lua代码时,框架仍会以C#逻辑进行执行,当需要Lua来写业务逻辑时,Lua部分的逻辑会注入到C#框架中,随同框架整体运行。这样就保证了我们开发业务时,可先用C#进行开发,在后期上线以后或者是需要热更的情况下,新增的业务逻辑用Lua编写即可。这样就可以前期用C#提高开发效率,后期依旧可用Lua实现热更,甚至修复已有的C#代码Bug。

  有了以上依赖Lua注入到C#框架的想法之后,如我们前几篇博客所写的,我们需要选用PureMVC框架,PureMVC框架利用事件通知的机制可完美地实现C#与Lua的互相融合切无耦合。有了LuaMVC中这种机制之后,我觉得Lua和C#占比已经不在是一个令人头疼的问题。
  以下是LuaMVC我们应该用的混编思路。

  • 前期开发用C#
  • 上线后新增功能用Lua,修复已有C#bug可用阻断事件通知或是XLua.Hotfix
  • 上线后依然不确定的逻辑用Lua编写
  • 对性能要求高的用C#编写

1.2 根据思考确定我们这样的整合方式

  确定混编思路之后,设计框架成了该思考的内容

  • Lua的入门应该在何处?
  • Lua代码如何注入到PureMVC(C#框架)?
  • 如何让事件通知在C#代码和Lua代码中自由流通起来?

  确定以上三点内容之后,LuaMVC将呈现出主体框架,C#部分调用Lua入口代码,Lua入口代码执行会将Lua代码注入到PureMVC框架,然后事件通知会通过PureMVC框架再通知调用Lua代码,完成流转。
  下文我们挑核心部分进行详细阐述。

2.PureMVCS与XLua的整合

2.1 如何整合

  根据上述分析之后,我们大概可以清楚整合PureMVC和XLua需要完成以下内容:

  • 给Lua提供一个入口函数
  • Lua代码可映射到C#中(映射到接口或委托)
  • 将映射对象注入到PureMVC,由PureMVC统一控制事件流程

2.2 具体操作

2.2.1 Lua入口

  像PureMVC一样,框架会有一个整体的入口,方便框架的流程控制,PrueMVC中提供了一个ApplicationFacade类型用于初始化PureMVC框架。对应Lua我们用LuaFacade.lua脚本来模拟C#中的ApplicationFacade类型,将LuaFacade做为Lua函数入口,Lua代码的加载都由LuaFacade来负责,这样也方便我们对框架的统一管理。LuaFacade.lua代码应该由PrueMVC框架初始化时调用,初始化LuaFacade.lua代码的工作也由LuaApplicationFacade.cs脚本来完成,具体设计如下。

  • LuaApplicationFacade调用LuaFacade.lua
namespace LuaMVC
{
    [LuaCallCSharp]
    public class LuaApplicationFacade : LuaFacade
    {
        public static LuaEnv luaEnv = new LuaEnv();
        private LuaTable scriptEnv = null;
        private Action ondestroy = null;

        public LuaApplicationFacade()
        {
            StartUp(); 
        }
        public void StartUp()
        {   
            this.scriptEnv = luaEnv.NewTable();
            LuaTable meta = luaEnv.NewTable();
            meta.Set("__index", luaEnv.Global);
            scriptEnv.SetMetaTable(meta);
            meta.Dispose();
            scriptEnv.Set("self", this);
            // 这里由luaEnv虚拟环境加载LuaFacade.lua脚本
            luaEnv.DoString(LoadLua("LuaFacade"), "LuaFacade", scriptEnv); 
            // 映射LuaFacade中的方法到委托,执行委托
            Action awake = scriptEnv.Get("awake");
            ondestroy = scriptEnv.Get("ondestroy");
            if (null != awake)
                awake();
        }
        public void ShutDown()
        {
            if (null != ondestroy)
                ondestroy();
            scriptEnv.Dispose();
        }  

        // 加载Lua脚本的方法
        private string LoadLua(string filePath)
        {
            string fullPath = Application.persistentDataPath + "/LuaScripts/" + filePath + ".lua";
            if(!File.Exists(fullPath))
                fullPath = Application.streamingAssetsPath + "/LuaScripts/" + filePath + ".lua";
            return File.ReadAllText(fullPath);
        } 
    }
}
  • LuaFacade.lua详细设计
-- 此方法映射到C#委托调用
function awake()
    print('lua part framework start up.')
end

function ondestroy()
    print('lua part framework shut down.')
end 

  LuaApplicationFacade.cs和ApplicationFacade.cs脚本统一由ProgramEntry.cs脚本调用,便于统一管理。

  • ProgramEntry.cs详细设计
namespace LuaMVC
{
    public class ProgramEntry : Program
    { 
        private void Start
        {   
            facade = new ApplicationFacade();
             // LuaApplicationFacade提供Lua函数的入口
            luaFacade = new LuaApplicationFacade(); 
            facade.StartUp(this);  
        } 
    }
}

  经过上述简单的设计,在新场景中新建一个GameObject,挂载ProgramEntry.cs,点击运行,即可看到控制台(Console)输出”lua part framework start up.“。接下来我们需要完成如何将Lua代码映射到C#接口或者委托。

2.2.2 Lua代码映射到C#对象

  在PrueMVCS框架中我们分为View-Model-Controller-Service四个模块,在Lua部分的框架中我们也沿用这种思路以方便将Lua代码映射注入到PureMVC中,因此我们需要LuaView-LuaModel-LuaController-LuaService四个模块来保存Lua记录映射到C#对象。LuaView记录Lua中的Mediator.lua脚本,LuaModel记录Lua中的Proxy.lua脚本,LuaController记录Lua中的Command.lua脚本,LuaService记录LuaHandler.lua脚本(整体思路和PureMVC中的一样View记录Mediator.cs子类,Controller记录Command.cs子类等),Lua我们以下以LuaView(类似PureMVC中的View)为例进行讲解。

  • Lua代码如何映射到C#对象

  之前的博客中已经讲解过Lua和C#互相调用的方式,已经XLua是提供了无GC的调用方式。下面我们看如何将一个标准的Lua的Mediator脚本映射到C#对象中。

Lua中的Mediator脚本模板

-- 设置页面
SettingPageMediator = {}
SettingPageMediator.NAME = "SettingPageMediator"

SettingPageMediator.ListNotificationInterests = {}

SettingPageMediator.HandleNotification = function(notification)

end

SettingPageMediator.OnRegister = function()

end

SettingPageMediator.OnRemove = function()

end

return SettingPageMediator

  我们已经学习过C#如何调用Lua中的表、属性和方法,在调用之前我们需要先使用lua虚拟机加载Lua脚本,然后用Get将Lua成员映射到C#对象中。注意:Get映射的是全局的表、属性和方法,映射非全局的表、属性和方法需要使用GetInPath,同理赋值也是一样。下面我们将SettingPageMediator.lua脚本映射到LuaMediator构造的对象中。


namespace LuaMVC
{
    public interface ILuaMediator 
    {
        string NAME { get; set; }
        IList<string> ListNotificationInterests { get; set; }
        HandleNotification HandleNotification { get; set; }
        Action OnRegister { get; set; }
        Action OnRemove { get; set; }
    }

    // Lua中Mediator要映射到LuaMediator构造的对象中
    public class LuaMediator : ILuaMediator
    {
        public string NAME { get; set; }
        public IList<string> ListNotificationInterests { get; set; }
        public HandleNotification HandleNotification { get; set; } 
        public Action OnRegister { get; set; }
        public Action OnRemove { get; set; }
    }
}

namespace LuaMVC
{ 
    [LuaCallCSharp]
    public class LuaApplicationFacade : LuaFacade
    {
        // 全局唯一Lua虚拟机
        public static LuaEnv luaEnv = new LuaEnv();

        // 将Lua中的Mediator对象映射到C#构造的LuaMediator对象中,由LuaView进行管理
        public void RegisterLuaMediator(string mediatorName)
        {
            // 加载Lua脚本
            luaEnv.DoString("require '" + mediatorName + "'"); 
            // 构造LuaMediator对象
            ILuaMediator mediator = new LuaMediator();
            // 以为五行代码将对应的表和方法映射到mediator对象中
            mediator.NAME = luaEnv.Global.GetInPath<string>(mediatorName + ".NAME");
            mediator.ListNotificationInterests = luaEnv.Global.GetInPathstring>>(mediatorName + ".ListNotificationInterests");
            mediator.OnRegister = luaEnv.Global.GetInPath(mediatorName + ".OnRegister");
            mediator.OnRemove = luaEnv.Global.GetInPath(mediatorName + ".OnRemove");
            mediator.HandleNotification = luaEnv.Global.GetInPath(mediatorName + ".HandleNotification");
            // 注入到PureMVC由PureMVC统一进行事件通知机制的管理 类似于PureMVC中RegisterMediator方法
            base.RegisterLuaMediator(mediator);
        }
    }
}

  经过上述简单的代码描述Lua中的SettingPageMediator脚本已经映射到mediator对象中,下面我们解析RegisterLuaMediator方法具体是如何与PureMVC实现统一的。

2.2.3 对象注入PureMVC


namespace LuaMVC
{
     [LuaCallCSharp]
    public class LuaFacade : ILuaFacade
    {
        // 会在LuaApplicationFacade初始化自动赋值,详细见源码
        protected ILuaView m_luaView = null;

        public void RegisterLuaMediator(ILuaMediator luaMediator)
        {
            // 调用LuaView中的ResigerMediator
            m_luaView.RegisterMediator(luaMediator);
        } 
    }
}

namespace LuaMVC
{
    public class LuaView : ILuaView
    {
        // 记录以注册的所有LuaMediator对象
        private IDictionary<string,ILuaMediator> m_luaMediators = new Dictionary<string, ILuaMediator>();

        // 注册LuaMediator
        public void RegisterMediator(ILuaMediator luaMediator)
        {
            lock (m_syncRoot)
            {
                if (m_luaMediators.ContainsKey(luaMediator.NAME))
                    return;
                m_luaMediators.Add(luaMediator.NAME,luaMediator);
                if( null != luaMediator.OnRegister)
                    luaMediator.OnRegister();
                // 这里是核心关键,会按照LuaMediator中监听的所有事件类型,分别注册到观察者的管理类中
                if (luaMediator.ListNotificationInterests.Count > 0)
                {
                    for (int i = 0; i < luaMediator.ListNotificationInterests.Count; i++)
                        RegisterObserver(luaMediator.ListNotificationInterests[i], new Observer(luaMediator.HandleNotification));
                }
            }
        }

        public void RegisterObserver(string notificationName, IObserver luaObserver)
        {
            // Observers是Observer对象的管理类 Observer也是事件通知执行的最后一站,注册的事件都会被Observers调用执行
            Observers.Instance.RegisterObserver(notificationName, luaObserver);
        }

        // 注销的接口
        public void RemoveMediator(string mediatorName)
        {

        }
    }
}

namespace LuaMVC
{
    public class Observers : IObservers
    {
        // 记录所有的观察者
        private IDictionary<string,IList> m_observers = new Dictionary<string, IList>();

        // 执行事件通知
        // 每一条SendNotification代码都会调用此方法
        public void NotifyObservers(INotification notification)
        {
            lock (m_syncRoot)
            {
                IList observers = null;
                if (m_observers.TryGetValue(notification.Name, out observers))
                {
                    // 检索到对应通知的事件列表,遍历执行委托
                    for (int i = 0; i < observers.Count; i++)
                        observers[i].NotifyObserver(notification);
                }
            }
        }

        // 注册观察者
        public void RegisterObserver(string notificationName, IObserver observer)
        {
            lock (m_syncRoot)
            {
                if (!m_observers.ContainsKey(notificationName))
                    m_observers[notificationName] = new List();
                m_observers[notificationName].Add(observer);
            }
        }
    }
}

// 观察者类,其中记录的事件最终要调用的方法。比如Mediator类中的HandleNotification方法。LuaMediator.lua脚本中HandlerNotification,以及Command中的Execute方法。不同模块的事件都以相同的方式有Observers管理着。
namespace LuaMVC
{
    public interface IObserver
    {
        HandleNotification InvokeMethod { get; set; }
        void NotifyObserver(INotification notification);
    }

    public class Observer : IObserver
    {
        public Observer( HandleNotification invokeMethod )
        {
            this.InvokeMethod = invokeMethod;
        }
        public HandleNotification InvokeMethod { get; set; }

        public void NotifyObserver(INotification notification)
        {
            if (null != InvokeMethod)
                InvokeMethod(notification);
        }
    }
}

  以上虽然只写了Lua中的Mediator脚本是如何从映射到注入到LuaView,再由Observers管理。其实LuaMVC框架中的Mediator(View)、Command(Controller)、Proxy(Model)、Handler(Service)、LuaMediator(LuaView)、LuaCommand(LuaController)、LuaProxy(LuaModel)、LuaHandler(LuaService)它们之间都是采用了相同的原理由事件通知机制和观察者机制管理着,才实现了模块之间的解耦和Lua与C#之间代码的解耦。

  以上的描述对于新手可能依旧比较混乱,下一篇博客中我们现有已讲诉的框架部分编写一个小案例,更加直观的体会事件流转的机制和LuaMVC到底帮开发者封装了什么功能。

4.关于LuaMVC框架

源码 : https://github.com/ll4080333/luaMVC
如果对你有用,记得点一波Star,关注博客哦。

  LuaMVC是我在项目种的经验总结,如果恰巧你也需要这样的框架来快速开发,那你可以期待后续的更新哦。
  如果你有什么更好的意见与建议欢迎加留言或者加群:LuaMVC/SpringGUI交流群 593906968 。

你可能感兴趣的:(LuaMVC)