Unity UI xlua 热更:还原塞尔达旷野之息 (持续更新:已补充箭头动效)

4 篇文章 0 订阅

整了个小Demo仿照《塞尔达传说:旷野之息》,实现 鼠标悬停在Button上时,能够改变Button-Text颜色,并且在Button前显示一个小箭头

934dfedffee04a52b7f5a308014f101b.gif

标题鼠标指针悬停和移走,改变标题颜色和箭头显示

Unity框架性内容使用C#编写,这些部分均是未来不考虑热更新的配置型的程序,例如如何将UI的Object连接到Lua成为Lua可以调用的变量。Button文字的变色,箭头的显示,这些业务逻辑(考虑到未来可能需要热更新)全部由Lua实现以后可以在不更新整包软件的情况下,通过热更新的方式只更新Lua脚本来重新编写UI界面的业务逻辑,例如Button直接隐藏,位置修改,scale放大,文字颜色修改rgba等。

附C#代码和Lua代码,,,详细的如何配置Lua解释器运行lua脚本和unity的热更新实现,会在下一篇笔记中全部展示。代码中的UILogic脚本以及Manager.Resource.LoadUI等函数三个脚本属于之后会详细展示的笔记。

//挂到需要检测鼠标悬停的UI物体上
public class UIMenuPointerHover : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    UISelect uiSelect;
    void Awake()
    {
        uiSelect = this.GetComponentInParent<UISelect>();
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        (uiSelect.m_OnPointerClickOrHover)?.Invoke(this.gameObject.name.Replace(" ", ""));
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        (uiSelect.m_OnPointerExit)?.Invoke(this.gameObject.name.Replace(" ", ""));
    }
}
//通过UIManager挂到ButtonGroup上
public class UISelect : LuaBehaviour
{
    //自定义类,连接C# 与 Lua
    [System.Serializable]
    public class Injection
    {
        public string name;
        public GameObject button;
        public GameObject arrow;
    }

    [SerializeField] private List<Injection> buttons;
    [SerializeField] private Image ornament;

    Action m_OnMenuUIUpdate;
    Action m_OnMenuUIOpen;
    public UnityEngine.Events.UnityAction<string> m_OnPointerClickOrHover;
    public UnityEngine.Events.UnityAction<string> m_OnPointerExit;

    //初始化配置,创建C#与Lua的变量关系
    public override void Init(string luaName)
    {
        base.Init(luaName);
        m_ScriptEnv.Get("OnMenuUIUpdate", out m_OnMenuUIUpdate);
        m_ScriptEnv.Get("OnMenuUIOpen", out m_OnMenuUIOpen);
        m_ScriptEnv.Get("OnPointerClickOrHover", out m_OnPointerClickOrHover);
        m_ScriptEnv.Get("OnPointerExit", out m_OnPointerExit);

        buttons = new List<Injection>();
        for (int i = 0; i < 6; i++)
        {
            Injection temp = new Injection();
            temp.name = this.gameObject.transform.GetChild(i).name.Replace(" ","");
            temp.button = this.gameObject.transform.GetChild(i).gameObject;
            temp.button.gameObject.AddComponent<UIMenuPointerHover>(); 
            temp.arrow = this.gameObject.transform.GetChild(i).gameObject.transform.GetChild(1).gameObject;

            buttons.Add(temp);
            m_ScriptEnv.Set(buttons[i].name + "_button", buttons[i].button);
            m_ScriptEnv.Set(buttons[i].name + "_arrow", buttons[i].arrow);
        }
    }

    //调用Lua的自定义的Open函数,类似于Awake
    public void OnOpen()
    {
        m_OnMenuUIOpen?.Invoke();
    }

    //调用Lua的自定义的Update
    public override void Update()
    {
        m_OnMenuUIUpdate?.Invoke();
    }

    //删除回调
    protected override void Clear()
    {
        base.Clear();
        m_OnMenuUIUpdate = null;
        m_OnMenuUIOpen = null;
        m_OnPointerClickOrHover = null;
        m_OnPointerExit = null;
    }
}
//Lua的脚本类
public class LuaBehaviour : MonoBehaviour
{
    //全局只能有一个LuaEnv
    private LuaEnv m_LuaEnv = Manager.Lua.LuaEnv;
    protected LuaTable m_ScriptEnv;
    //private Action m_LuaAwake;
    //private Action m_LuaStart;
    private Action m_LuaInit;
    private Action m_LuaUpdate;
    private Action m_LuaOnDestroy;

    public string luaName;
    void Awake()
    {
        m_ScriptEnv = m_LuaEnv.NewTable();

        // 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
        LuaTable meta = m_LuaEnv.NewTable();
        meta.Set("__index", m_LuaEnv.Global);
        m_ScriptEnv.SetMetaTable(meta);
        meta.Dispose();

        m_ScriptEnv.Set("self", this);
    }

    //用Init来代替unity的awake
    public virtual void Init(string luaName)
    {
        m_LuaEnv.DoString(Manager.Lua.GetLuaScript(luaName), luaName, m_ScriptEnv);
        m_ScriptEnv.Get("Update", out m_LuaUpdate);
        m_ScriptEnv.Get("OnInit", out m_LuaInit);

        m_LuaInit?.Invoke();
    }
    // Start直接删掉
    // Update is called once per frame
    public virtual void Update()
    {
        m_LuaUpdate?.Invoke();
    }

    //父类的需要是保护级,因为子类更特殊有其他需要进行的操作
    protected virtual void Clear()
    {
        m_LuaOnDestroy = null;
        //运行环境释放掉
        m_ScriptEnv?.Dispose();
        m_ScriptEnv = null;
        m_LuaInit = null;
        m_LuaUpdate = null;
    }

    //两个不一定同时触发,退出的时候不会调用OnDestroy,所以都要写Clear
    private void OnDestroy()
    {
        m_LuaOnDestroy?.Invoke();
        Clear();
    }
    private void OnApplicationQuit()
    {
        Clear();
    }
}

UI = CS.UnityEngine.UI
local uiButtonName = 
{
    Continue = nil, 
    NewGame = nil, 
    PlayinMasterMode = nil, 
    Options = nil, 
    amiibo = nil, 
    DLCPurchased = nil
}
local uiArrowName = 
{
    Continue = nil, 
    NewGame = nil, 
    PlayinMasterMode = nil, 
    Options = nil, 
    amiibo = nil, 
    DLCPurchased = nil
}

function OnMenuUIUpdate()
    --print("OnMenuUIUpdate")
end

--第一次打开UI进行配置,等价于Awake
function OnMenuUIOpen()
    uiButtonName.Continue = Continue_button;
    uiArrowName.Continue = Continue_arrow;

    uiButtonName.NewGame = NewGame_button;
    uiArrowName.NewGame = NewGame_arrow;

    uiButtonName.PlayinMasterMode = PlayinMasterMode_button;
    uiArrowName.PlayinMasterMode = PlayinMasterMode_arrow;

    uiButtonName.Options = Options_button;
    uiArrowName.Options = Options_arrow;

    uiButtonName.amiibo = amiibo_button;
    uiArrowName.amiibo = amiibo_arrow;

    uiButtonName.DLCPurchased = DLCPurchased_button;
    uiArrowName.DLCPurchased = DLCPurchased_arrow;
end

--鼠标悬停执行的函数
function OnPointerClickOrHover(buttonName)
    GetButtonInTable(buttonName).transform:GetChild(0).gameObject:GetComponent(typeof(UI.Text)).color = CS.UnityEngine.Color(238/255,237/255,228/255,255/255)
    GetArrowInTable(buttonName):SetActive(true)
end

--鼠标移开执行的函数
function OnPointerExit(buttonName)
    GetButtonInTable(buttonName).transform:GetChild(0).gameObject:GetComponent(typeof(UI.Text)).color = CS.UnityEngine.Color(188/255,185/255,169/255,255/255)
    GetArrowInTable(buttonName):SetActive(false)
end

function GetButtonInTable(name)
    for k,v in pairs(uiButtonName) do
        if k == name then
            return v
        end
    end
end

function GetArrowInTable(name)
    for k,v in pairs(uiArrowName) do
        if k == name then
            return v
        end
    end
end

自己的一点学习心得:如果项目需要热更新,重要的是分辨出什么框架逻辑或者脚本内容是死的,如果是死的就用C#,然后活的东西可以热更新的,取用Lua写,例如配置变量都是死的,但是如何实现UI业务逻辑的部分是活的,全部由C#去调用Lua里面的函数。


补充:

看视频突然注意到UI界面选中悬停的时候,小箭头是晃动的,就用Lua补充了个协程把相关功能实现一下,嗯,对效果满意很多。。。。UI音效回头再加,没找到合适的音效。

鼠标悬停,箭头晃动标题
UI = CS.UnityEngine.UI
local uiButtonName = 
{
    Continue = nil, 
    NewGame = nil, 
    PlayinMasterMode = nil, 
    Options = nil, 
    amiibo = nil, 
    DLCPurchased = nil
}
local uiArrowName = 
{
    Continue = nil, 
    NewGame = nil, 
    PlayinMasterMode = nil, 
    Options = nil, 
    amiibo = nil, 
    DLCPurchased = nil
}
local startCoroutine = false
local arrow = nil
local startPos = nil

function OnMenuUIUpdate()
    if startCoroutine == true then
        coroutine.resume(ArrowMove,arrow,startPos)
    end
end

--第一次打开UI进行配置,等价于Awake
function OnMenuUIOpen()
    uiButtonName.Continue = Continue_button;
    uiArrowName.Continue = Continue_arrow;

    uiButtonName.NewGame = NewGame_button;
    uiArrowName.NewGame = NewGame_arrow;

    uiButtonName.PlayinMasterMode = PlayinMasterMode_button;
    uiArrowName.PlayinMasterMode = PlayinMasterMode_arrow;

    uiButtonName.Options = Options_button;
    uiArrowName.Options = Options_arrow;

    uiButtonName.amiibo = amiibo_button;
    uiArrowName.amiibo = amiibo_arrow;

    uiButtonName.DLCPurchased = DLCPurchased_button;
    uiArrowName.DLCPurchased = DLCPurchased_arrow;
end

--鼠标悬停执行的函数
function OnPointerClickOrHover(buttonName)
    GetButtonInTable(buttonName).transform:GetChild(0).gameObject:GetComponent(typeof(UI.Text)).color = CS.UnityEngine.Color(238/255,237/255,228/255,255/255)
    arrow = GetArrowInTable(buttonName)
    arrow:SetActive(true)
    startCoroutine = true;
    startPos = GetArrowInTable(buttonName).transform.position
    coroutine.resume(ArrowMove,arrow,startPos)
end

--鼠标移开执行的函数
function OnPointerExit(buttonName)
    GetButtonInTable(buttonName).transform:GetChild(0).gameObject:GetComponent(typeof(UI.Text)).color = CS.UnityEngine.Color(188/255,185/255,169/255,255/255)
    GetArrowInTable(buttonName).transform.position = startPos
    GetArrowInTable(buttonName):SetActive(false)
    startCoroutine = false;
end

function GetButtonInTable(name)
    for k,v in pairs(uiButtonName) do
        if k == name then
            return v
        end
    end
end

function GetArrowInTable(name)
    for k,v in pairs(uiArrowName) do
        if k == name then
            return v
        end
    end
end

--协程用来实现button下面的小箭头左右晃动,当OnPointerExit执行时,resume传入新arrow obj和arrow的pos进来,重新执行协程的移动功能
ArrowMove = coroutine.create(
    function (obj, startPos)
        ::label:: print("coroutine")
        while (obj.transform.position - (startPos + CS.UnityEngine.Vector3(7,0,0))).x >= 0.5 or 
        (obj.transform.position - (startPos + CS.UnityEngine.Vector3(7,0,0))).x <= -0.5 do
            obj.transform.position = obj.transform.position + CS.UnityEngine.Vector3(7,0,0) * Time.deltaTime * 3
            local newObj, newStartPos = coroutine.yield()
            if newObj ~= obj then
                obj = newObj
                startPos = newStartPos
                goto label
            end
        end
        obj.transform.position = startPos + CS.UnityEngine.Vector3(7,0,0)

        tempPos = obj.transform.position
        while (obj.transform.position - (tempPos + CS.UnityEngine.Vector3(-7,0,0))).x >= 0.5 or 
        (obj.transform.position - (tempPos + CS.UnityEngine.Vector3(-7,0,0))).x <= -0.5 do
            obj.transform.position = obj.transform.position + CS.UnityEngine.Vector3(-7,0,0) * Time.deltaTime * 3
            local newObj, newStartPos = coroutine.yield()
            if newObj ~= obj then
                obj = newObj
                startPos = newStartPos
                goto label
            end
        end
        obj.transform.position = startPos
        
        if startCoroutine == false then
            local newObj, newStartPos = coroutine.yield()
            if newObj ~= obj then
                obj = newObj
                startPos = newStartPos
                goto label
            end
        else
            goto label
        end
    end
)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值