读一读

一般来说Bundle之间共同使用的资源会提取出来为一个公用Bundle,但是可能不注意的情况下会导致Bundle下的Prefab引用到了其他Bundle的资源,若这种情况一直积累,就有可能发生启动加载Bundle的时候连带引用加载了一堆Bundle,启动卡顿及占用内存。所以需要根据个项目不同的Bundle打包策略定制一个检测Bundle的错误引用关系,及时纠正。

一般来说都是按目录就区分Bundle的,大概的思路就是这样:

public void Start()
{
    //获取所有的Prefab和材质
    string[] listString = AssetDatabase.FindAssets("t:Prefab t:Material", new string[] { "Assets/AssetsPackage" });

    for (int i = 0; i < listString.Length; i++)
    {
        string cur = listString[i];

        List<string> refs = new List<string>();
        string assetPath = AssetDatabase.GUIDToAssetPath(cur);
        string headPath = GetHeadPath(assetPath);//获取当前资源应该在的Bundle

        string[] names = AssetDatabase.GetDependencies(new string[] { assetPath });  //依赖的东东
        foreach (var name in names)
        {
            bool is_common = false;
            //排除引用的是公用资源 commonRef需要定制(值为目录数组),有的第三方库,有的公用资源Bundle等,项目不同这个也不同
            foreach (var cc in commonRef)
            {
                if (name.Contains(cc))
                {
                    is_common = true;
                    break;
                }
            }

            if (!is_common)
            {
                if (name.Contains(headPath)) //不是用公用,看看引用的是不是自己所属的bundle
                {
                    is_common = true;
                }
            }

            //上面情况都不是的话应该是有问题的
            if (!is_common)
            {
                if (name == assetPath)
                {
                    is_common = true;
                }
            }

            if (!is_common)
                AddProblem(assetPath, name);
        }

        EditorUtility.DisplayProgressBar("Progress", (i + 1) + "/" + listString.Length, (i + 1) / listString.Length);

    }

    EditorUtility.ClearProgressBar();
    isDone = true;
    this.ShowNotification(new GUIContent("Done!"));
}



客户端的事件id可以用一个单例递增来,GetEventID每次都返回一个+1的id,区别对待就好了,这样可以有限防止内部通讯的id重复或者定义麻烦

Event.ON_WINDOW_CREATE = Messenger:GetEventId()
Event.ON_WINDOW_OPEN = Messenger:GetEventId()
Event.ON_WINDOW_CLOSE = Messenger:GetEventId()

思想:场景由一些渲染设置(雾、环境光、光晕等)、天空盒子材质球、地形、场景中的静态模型、烘焙的灯光贴图等构成的,在Uinty使用编辑器将这些内容设置好,排列好,渲染好,随后编写一个方法将这些设置或者是物体序列化成一个json文件,资源细化成一个一个prefab并以材质为分类命名好。当加载场景的时候,就加在那么一个json文件,设置场景中的渲染设置,还有将那些静态的模型物件加载出来,同一个材质的放置到同一的父物体下(用于动态的静态批处理),设置好物体的位置、旋转和缩放等。加载lightmap,渲染灯光,从而完成一个场景的加载。使用数据驱动的形式,当需要改变场景的某一个物体时,就可以直接修改配置文件和资源就行了,可以达到场景的热更新和一些渲染氛围的改变,满足节日性的更新。


一、序列化编辑好的场景为json数据

[MenuItem ("Custom/Scene/Selected GameObjects And To Json")]
static void SaveAllGameObjectToJson()
{
    if (m_GameObjects == null)
    {
        m_GameObjects = new Dictionary<string, GameObjectResInfo>();
    }
            
    string path_out = "Assets/Resources/Terrains/";
    Directory.CreateDirectory( path_out );
    char[] delimiter = {'/', '.'};
    string[] words = EditorApplication.currentScene.Split(delimiter);
            
    string sceneName = words[words.Length-2];
    string Json_out = path_out + FormatString(sceneName) + "/";
    if ( !Directory.Exists(Json_out) )
        Directory.CreateDirectory(Json_out);

    string[] files = Directory.GetFiles(Json_out, "*.*");
    foreach (string fileName in files)
    {
        if ( !fileName.Contains("obstruct") )
            File.Delete( fileName );
    }

    string strJsonFileName = Json_out + FormatString(sceneName) + "_json.txt";
    FileStream fs = new FileStream (strJsonFileName,FileMode.Create);
    StreamWriter out_stream = new StreamWriter (fs);

    //序列化的数据有
    /**
        * RenderSettings.fog 是否开启雾效
        * RenderSettings.fogMode.ToString()  雾的模式(线性、指数、指数的平方)
        * RenderSettings.fogColor.rgba  雾的颜色
        * RenderSettings.fogStartDistance 线性模式雾的起始距离
        * RenderSettings.fogEndDistance  线性模式雾的结束距离
        * RenderSettings.fogDensity  雾的强度
        * 
        * RenderSettings.ambientLight.rgba 环境光的颜色
        * RenderSettings.haloStrength  光晕强度
        * RenderSettings.flareStrength  耀光的强度
        * RenderSettings.skybox  字符串保存 天空盒子材质
        */

    //天空盒子材质的序列化
    UnityEngine.Material skyMat = RenderSettings.skybox;
    if (skyMat != null)
    {
        string skyPath = AssetDatabase.GetAssetPath(skyMat);
        string skyMatPath = Json_out + FormatString(skyMat.name) + ".mat";
        AssetDatabase.CopyAsset( skyPath, skyMatPath );
        //skyMat.name 保存材质的名字 对应的去复制到的资源目录中查找
    }

    //序列化烘焙好的光照贴图
    foreach (LightmapData data in LightmapSettings.lightmaps)
    {
        if (data.lightmapFar != null)
        {
            string nameFar = FormatString(sceneName + "_" + data.lightmapFar.name) + "_tex";
            string lightBinPath = Json_out + nameFar;
            string lightResPath = AssetDatabase.GetAssetPath( data.lightmapFar );
            int idx = lightResPath.LastIndexOf( '.' );
            lightBinPath = lightBinPath + lightResPath.Substring( idx, lightResPath.Length-idx );
            AssetDatabase.CopyAsset( lightResPath, lightBinPath );
                    
            //data.lightmapFar.name 名字
            //nameFar 光照贴图资源名字
        }
        if (data.lightmapNear != null)
        {
            string nameNear = FormatString(sceneName + "_" + data.lightmapNear.name) + "_tex";
            string lightBinPath = Json_out + nameNear;
            string lightResPath = AssetDatabase.GetAssetPath( data.lightmapNear );
            int idx = lightResPath.LastIndexOf( '.' );
            lightBinPath = lightBinPath + lightResPath.Substring( idx, lightResPath.Length-idx );
            AssetDatabase.CopyAsset( lightResPath, lightBinPath );

            //data.lightmapNear.name 名字
            //nameNear 光照贴图资源名字
        }
    }

    //序列化场景中被选中的物体
    UnityEngine.Object[] objects = Selection.GetFiltered(typeof(GameObject), SelectionMode.DeepAssets);//获取场景中选中的物体们
    int wuJianIndex = 0;
    foreach (GameObject obj in objects)
    {
        //如果物体有材质,会以材质的名字为前缀命名(combineDC),后面加载时分组,方便静态绑定
        string strName = combineDC + obj.name + ResIndex + ".prefab";
        string prefabFile = prefabsPath + strName;
        bool isExistPrefab = false;

        if (!isExistPrefab)
        {
            //创建prefab
            UnityEngine.Object objprefab = PrefabUtility.CreateEmptyPrefab(prefabFile);
            PrefabUtility.ReplacePrefab(obj, objprefab);
        }
        //根据obj.name的命名 确定它是什么类型的物体
        //序列化的信息有
        /**
            * 物体的类型
            * 物体的名字 材质为组的命名
            * 物体预设名字
            * 物体的位置
            * 物体的旋转
            * 物体的缩放
            * 如果物体是特殊类型,保存一些组件信息,例如灯光或则相机
            * */
    }
            
    out_stream.Flush();
    fs.Close();            
}


二、解析和加载场景

virtual public void Initial_local()
{
    ParseJson_local(); //解析json数据保存到对象中
        
    _LoadAssets_local(); //以材质为组加载物体 设置渲染设置

    SetSceneLightMap(m_mapTable.extralightmap);//设置光照贴图
        
    CorResMgr.Get().UnloadUnusedAssets();
}

protected virtual void _LoadAssets_local()
{
    Camera mainCam = CameraMagr.Get().GetMainCamera().GetCamera();
        
    SceneAsset asset;
    for(int i=0; i<m_AssetList.Count; i++)
    {
        asset = m_AssetList[i];
        if(asset == null)
            continue;
        switch(asset.Type)
        {
        case GameObjectType.Camera_Type:
        {
            if(asset.isLoadFinished)
                break;
            mainCam.transform.position = asset.Position;
            mainCam.transform.rotation = asset.Rotation;
            CameraMagr.Get().GetMainCamera().static_forward = mainCam.transform.forward;
            asset.isLoadFinished = true;
            break;
        }
        case GameObjectType.Light_Type:
        case GameObjectType.Water_Type:
        default:
            if(asset.isLoadFinished)
                break;
            _LoadAsset_local(asset);
            break;
        }
    }

    foreach(GameObject g in parentDic.Values)
    {
        if(g !=null && !g.name.Contains("eft") )
        {
            g.AddComponent<StaticBatch>();//静态批处理
                
        }
    }
}

protected void _LoadAsset_local(SceneAsset asset)
{
    try
    {
        UnityEngine.Object obj = CorResMgr.Get().LoadResource( asset.SourcePath );
            
        if(obj == null)
            return;
            
        //实例化 指定位置和旋转
        asset.instObject = (GameObject)UnityEngine.Object.Instantiate( obj, asset.Position, asset.Rotation );
            
        if(asset.instObject == null)
        {
            Debug.Log("Init map asset is error! path="+asset.SourcePath);
            return;
        }
            
        if(mapRoot == null)
        {
            mapRoot = new GameObject(Prefab);
            mapRoot.transform.localPosition = Vector3.zero;
            mapRoot.transform.localScale    = Vector3.one;
        }

        //根据不同的类型做不同的处理
        switch(asset.Type)
        {
        case GameObjectType.Actor_Type:
        case GameObjectType.Monster_Type:
        case GameObjectType.Generic_Type:
        {
            asset.instObject.transform.parent = mapRoot.transform;
            asset.instObject.transform.localScale = asset.Scale;
            break;
        }
        case GameObjectType.Building_Type:
        {
            string[] middleStr = asset.Name.Split('-');
            asset.instObject.transform.localScale = asset.Scale;
            if(parentDic.ContainsKey(middleStr[0]))
            {
                if(parentDic[middleStr[0]] ==null)
                {
                    GameObject go =new GameObject();
                    parentDic[middleStr[0]] = go;
                    parentDic[middleStr[0]].transform.localPosition = Vector3.zero;
                    parentDic[middleStr[0]].transform.localScale    = Vector3.one;
                    parentDic[middleStr[0]].transform.parent = mapRoot.transform;
                    parentDic[middleStr[0]].name =middleStr[0]+"Parent";
                    asset.instObject.transform.parent = parentDic[middleStr[0]].transform;
                }
                else
                {
                    asset.instObject.transform.parent = parentDic[middleStr[0]].transform;
                }
            }
            else
            {
                asset.instObject.transform.parent = mapRoot.transform;
            }
            break;
        }
            
        case GameObjectType.Light_Type:
        {
            asset.instObject.transform.parent = mapRoot.transform;
                
            Light light         = (Light)asset.instObject.GetComponent("Light");
            light.color         = asset.LightParam.Color;
            light.intensity     = asset.LightParam.Intensity;                        
            light.shadows         = (LightShadows)asset.LightParam.Shadow;
            light.shadowStrength= asset.LightParam.Strength;
            light.cullingMask = CONST_VALUE.SHADOWLAYER;
            light.tag = "dirLight";
            dirLightParam.intensity = asset.LightParam.Intensity;    
            dirLightParam.lightColor = asset.LightParam.Color;
            dirLightParam.rotation = asset.Rotation;
            WeatherSystem.Get().SetLightDir(light.transform.forward);
            break;
        }
        default:
        {
            asset.instObject.transform.parent = mapRoot.transform;
            break;
        }
        }
        asset.isLoadFinished = true;
        _InitAsset_Shader(asset);
    }
    catch(System.Exception exp)
    {
        Debug.LogWarning("_LoadAsset_local Error! path="+asset.SourcePath+" exp="+exp.Message);
        asset.isLoadFinished = false;
    }
}

public void SetSceneLightMap (string name = null)
{
    //场景的光照贴图
    if(m_LightmapList == null)
        return;
    List<LightmapData>    listLight = new List<LightmapData>();
        
    for(int i=0; i<m_LightmapList.Count; i++)
    {
        LightmapSet lightMap = m_LightmapList[i];
            
        LightmapData lightData = null;
        //far 
        if(!lightMap.m_bFarLoaded)
        {
            if(lightMap.m_NameFar == null || lightMap.m_NameFar == string.Empty)
            {
                lightMap.m_bFarLoaded = true;
            }
            else
            {
                if (!string.IsNullOrEmpty(name)) 
                {
                    lightMap.m_SourcePathFar = "Terrains/ExtraLightmap/" + name.Split('|')[i];
                }
                Texture2D texAsset = (Texture2D) CorResMgr.Get().LoadResource(lightMap.m_SourcePathFar);
                    
                if(texAsset != null)
                {
                    if(lightData == null)
                        lightData = new LightmapData();
                        
                    lightData.lightmapFar = texAsset;
                    lightMap.m_bFarLoaded = true;
                }
            }
        }
            
        //near
        if(!lightMap.m_bNearLoaded)
        {
            if(lightMap.m_NameNear == null || lightMap.m_NameNear == string.Empty)
            {
                lightMap.m_bNearLoaded = true;
            }
            else
            {
                Texture2D texAsset = (Texture2D) CorResMgr.Get().LoadResource(lightMap.m_SourcePathNear);
                    
                if(texAsset != null)
                {
                    if(lightData == null)
                        lightData = new LightmapData();
                        
                    lightData.lightmapNear = texAsset;
                    lightMap.m_bNearLoaded = true;
                }
            }
        }
        //fill
        if(lightData == null)
            return;
        listLight.Add(lightData);
    }
        
    //fill light map
    LightmapSettings.lightmaps = listLight.ToArray();
#if UNITY_5
    LightmapSettings.lightmapsMode = LightmapsMode.NonDirectional;
#endif
}

在说UI系统的时候,先说一下有点关联的配置管理器,因为有着那么一个配置是ui配置,它记录了ui面板的名称对应的prefab和使用的lua文件,还有一些与其他面板的互斥信息,还有其他情况该不该显示的信息等。

public class GameTableMagr 
    : Singleton<GameTableMagr>
{

    // 加载表格
    public void LoadTable()
    {
        LoadTable("ui", typeof(TableUI));//TableUI
    }
}


在GameTableMagr加载了ui的配置文件后,在GameUIManager单例管理器中去把配置的ui解析为一个一个的UIAsset信息,它保存了配置信息和面板的GameObject和面板的Behaviour等。

public class UIAsset
{
    public TableUI.UI     ui             = null;
    public GameObject     oUIObject    = null;
    public PanelBase    panel         = null;
    public float        timer        = 0;
    public AssetBundle bundle = null;
    public UILuaBehaviour luaBehaviour = null;
}

public class GameUIManager
    : Singleton<GameUIManager>
{

    Dictionary<string, UIAsset>         uiMap         = new Dictionary<string, UIAsset>();
    List<UIAsset>                        uiAllList   = new List<UIAsset>();

    
    public void InitAfterLoadData()
    {
        TableUI table = GameTableMagr.Get().GetTable("ui") as TableUI;
        
        for(int i=0; i<table.uiList.Count; i++)
        {
            if ( i == 1 )
                continue;
            
            UIAsset asset = new UIAsset();
            
            asset.ui = table.uiList[i];
            
            uiMap.Add(asset.ui.name, asset);
            
            uiAllList.Add( asset );
        }

        Resources.UnloadUnusedAssets();
        GC.Collect();
    }
}


UI系统围绕这一个叫做UICmdSystem的单例管理器进行,首先是很多的命令数组,每个命令都是一个字符串,代表的是一个事件。而每一个命令都对应一个UI面板的名称,在所有资源都加载完成和配置加载完成的时候就会初始化命令信息,通过命令的事件为key,对应的面板名字对应的UIAsset为值,放置到一个列表中,多对多的关系。

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

public class UICmd
{
    public string         key = string.Empty;
    public string        values = string.Empty;
}

public class UICmdSystem 
    : Singleton<UICmdSystem>
{
    Hashtable        cmdList = null;
    Queue<UICmd>    cmdQueue = null;
    
    string []    regCmd =
    {
        "update_gold",
    };
    
    string[,]    regUI =
    {
        {"update_gold",                 "Main"},
    };
    
    public void Initial()
    {
        cmdList = new Hashtable();
        cmdQueue= new Queue<UICmd>();
        
        for(int i=0; i<regCmd.Length; i++)
            RegCmd(regCmd[i]);
    }
    
    public void InitAfterLoadData()
    {
        for(int i=0; i<regUI.GetLength(0); i++)
            RegUICmd(regUI[i,0], regUI[i,1]);
    }
    
    void RegCmd(string key)
    {
        if(cmdList.ContainsKey(key))
            return;
        cmdList.Add(key, new List<UIAsset>());
    }
    
    void RegUICmd(string key, string panel)
    {
        if(cmdList.ContainsKey(key) == false)
        {
            Debug.LogError("[UICmdSystem] UICmd:" + key + ",not register!" );
            return;
        }

        UIAsset asset = GameUIManager.Get().GetUIAsset(panel);
        if(asset == null)
        {
            Debug.LogError("[UICmdSystem] UICmd:" + key + ",Panel:" + panel + ",not ui" );
            return;
        }
        
        List<UIAsset> list = cmdList[key] as List<UIAsset>;
        if(list.Contains(asset))
        {
            Debug.LogError("[UICmdSystem] UICmd:" + key + ",Panel:" + panel + ",has register" );
            return;
        }
        
        list.Add(asset);
    }
    
    public void AddCommand(UICmd cmd)
    {
        if(cmd == null)
            return;
        if(cmdList.ContainsKey(cmd.key) == false)
            return;
        cmdQueue.Enqueue(cmd);
    }
    
    public void Tick()//Tick方法会在Main里面的Update方法调用
    {        
        while(cmdQueue.Count != 0)
        {
            UICmd cmd = cmdQueue.Dequeue();
            //Debug.LogError(cmd.key + " | " + cmd.values);
            if(cmd == null)
                continue;
            
            List<UIAsset> uiAsset = cmdList[cmd.key] as List<UIAsset>;

            for(int i=0; i<uiAsset.Count; i++)
            {
                UIAsset asset = uiAsset[i];
                if(asset == null)
                    continue;
                if ((asset.panel == null && asset.luaBehaviour == null) || asset.oUIObject == null)
                    continue;
                if (asset.luaBehaviour != null) {
                    asset.luaBehaviour.HandleCommand(cmd);
                }
                if (asset.panel != null) {
                    asset.panel.HandleCommand(cmd);
                }
            }
        }
        cmdQueue.Clear();
    }
    
    public bool HaveCommand(UICmd cmd)
    {
        if(cmd == null)
            return false;
        if(cmdList.ContainsKey(cmd.key) == false)
            return false;
        
        UICmd[] cmds = cmdQueue.ToArray();
        for(int i = 0; i < cmds.Length; i ++)
        {
            if(cmd.key == cmds[i].key)
                return true;
        }
        return false;
    }
}


看上面的UICmdSystem就知道了,它当前已经知道哪些命令归哪些UI去处理了,当我们需要通知一个事件的时候,通过单例的UICmdSystem的AddCommand方法添加到列表里面,在Update里面就会取出来,找到命令key对应的UIAsset,再在里面找到脚本(UI面板),调用其的HandleCommand方法处理事件,并将UICmd传递进去,通过UICmd.values区分命令处理。

public override void ValueChange(UICmd cmd)//HandleCommand方法调用了ValueChange虚方法,用来给具体的Panel重写的
{
    if (!m_isInit)
        initUI();
    if (cmd.key.Equals("loginselect_ver"))
    {
        if (cmd.values.Equals("loadLocVer"))
        {
            curVersion.gameObject.SetActive(true);
            curVersion.text = CorResMgr.Get().locVer;
        }
        else if (cmd.values.Equals("update"))
        {
            curProgTxt.gameObject.SetActive(true);
            curProgTxt.text = CorResMgr.Get().infoTxt;
            progBar.gameObject.SetActive(true);
            progBar.value = 0.0f;
            
            updateProgCheck = StartCoroutine(updateProg());
        }
    }
}

首先是资源(我这里是UI,一个Panel为一个资源),对应着一个MonoBehaviour的脚本和一个功能一致的Lua脚本,将资源打包成AssetBundle,lua脚本不需要打包(加密应该是必须的),都放到远程服务器上。通过配置表Table(也是远程加载的)决定是使用MonoBehavoiur还是Lua脚本,使用一个UILuaBehaviour来承载保存lua的模块名,UILuaBehaviour是一些UI常用到的方法,从而在Lua脚本中可以获取这个脚本调用这些功能。


启动的时候,在GameLuaManager中初始化时实例化LuaState或LuaScriptMgr,调用Start方法开始,调用一个Lua脚本的管理器加载一些公用的依赖,以便后面lua脚本使用这些公用方法。

using UnityEngine;
using System.Collections;
using System.IO;
using System;

public class GameLuaManager : Singleton<GameLuaManager> {

	LuaScriptMgr luaScript = null;
	public void Initialize()
	{
        if (luaScript == null)
        {
            luaScript = new LuaScriptMgr();
            luaScript.Start();
            luaScript.DoFile("Logic/GameManager");
            Util.CallMethod("GameManager", "Main");
        }
	}

	public LuaScriptMgr LuaManager
	{
		get{return luaScript;}
	}
}


--Lua GameManager.lua
require "Common/define" --在这里就引入全局了,一些全局定义
require "Net/Network" --网络通信
require "Common/functions" --一些工具的方法
--管理器--
GameManager = {};
local this = GameManager;

function GameManager.Main()
	require("Net/Network").new()
end

--例如functions定义了一些打印日志的方法,Util是c#脚本warp映射到lua中的
--输出日志--
function log(str)
    Util.Log(str);
end

--错误日志--
function error(str) 
	Util.LogError(str);
end

--警告日志--
function warn(str) 
	Util.LogWarning(str);
end


然后各个管理器初始化完成,检查资源更新,会把需要的UI面板和对应的Lua脚本下载到本地上,GameUIManager会根据需求加载UI面板并通过配置查看是否使用lua从而添加UILuaBehaviour,UILuaBehaviour会根据面板的名字找到Lua模块,然后Awake,Start等方法触发是直接Call的Lua中的方法。

//GameUIManager
public PanelBase Show(string strName, bool useCoroutine = true, bool fromPakLoad = false, bool bNeedDarkFollow = false)
{
    UIAsset asset = GetUIAsset(strName);
    if (asset == null)
        return null;
    //控制主界面隐藏
    if (asset.ui.IsCloseMain)
    {
        GetUIAsset("Main").panel.gameObject.SetActive(false);
    }
    if (asset.ui.IsHideChar)
    {
        CharMagr.Get().GetLoader().IsVisible = false;
    }
    if (Main.gameState == Game_State_Type.GST_Fight)
    {
        if (asset.ui.fightShow == 0)
            return null;
    }

    HideAll(asset);

    if (asset.ui.enableDark)
        ShowDark(true , false ,asset);

    uiTimeList.Remove(asset);
    Main.Game.IsHoverUI = false;
    if (asset.oUIObject != null)
    {
        asset.oUIObject.SetActive(true);

        if (asset.panel != null)
        {
            asset.panel.OnShow();
        }
        else if (asset.luaBehaviour != null)
        {
            asset.luaBehaviour.OnShow();
        }

        uiActive.Add(asset);
        return asset.panel;
    }

    //主要在这里,首次加载的
    UnityEngine.Object obj = null;
    if (asset.ui.lua == "")
    {	// cs的界面
        obj = CorResMgr.Get().LoadResource(CONST_VALUE.UI_ROOT_PATH + asset.ui.prefab);
        if (obj == null)
            return null;

        asset.oUIObject = (GameObject)GameObject.Instantiate(obj);
        asset.oUIObject.transform.parent = root.transform;
        asset.oUIObject.transform.localScale = Vector3.one * CalcScale(asset);
        asset.oUIObject.transform.localPosition = Vector3.zero;
        asset.panel = asset.oUIObject.GetComponent<PanelBase>();
    }
    else
    {	// lua的界面
        obj = CorResMgr.Get().LoadResource(CONST_VALUE.UI_ROOT_PATH + asset.ui.prefab);
        if (obj == null)
            return null;

        asset.oUIObject = (GameObject)GameObject.Instantiate(obj);
        asset.oUIObject.transform.parent = root.transform;
        asset.oUIObject.transform.localScale = Vector3.one * CalcScale(asset);
        asset.oUIObject.transform.localPosition = Vector3.zero;

        GameLuaManager.Get().LuaManager.DoFile("UI/" + asset.ui.lua);//编译配置表指定Lua脚本
        //添加UILuaBehaviour脚本来做中间人去驱动调用lua方法
        asset.luaBehaviour = asset.oUIObject.AddComponent<UILuaBehaviour>();
    }

    UIPanel n_ObjPanel = asset.oUIObject.GetComponent<UIPanel>();
    if (n_ObjPanel)
        n_ObjPanel.depth = asset.ui.Depth;

    UIAnchor[] anchors = asset.oUIObject.GetComponentsInChildren<UIAnchor>();

    for (int i = 0; i < anchors.Length; i++)
    {
        anchors[i].uiCamera = uiCamera;
    }

    if (asset.panel != null)
    {
        asset.panel.OnShow();
    }
    else if (asset.luaBehaviour != null)
    {
        asset.luaBehaviour.OnShow();
    }
    
    uiActive.Add(asset);
    if (asset.panel != null)
    {
        asset.panel.m_UIName = strName;
    }
    obj = null;
    return asset.panel;
}


下面看一下UILuaBehaviour

public class UILuaBehaviour : PanelBase
{
    protected static bool initialize = false;

    private string data = null;

    private List<LuaFunction> buttons = new List<LuaFunction>();

    private string luaFileName = "";
    protected void Awake()
    {
        luaFileName = name.Split('_')[1]; //这里自动就知道模块lua的文件名了
        CallMethod("Awake", gameObject);//把gameobject传递给lua知道,然它知道控制的是谁
    }

    void Start()
    {
        if (initialize)
            return;

        CallMethod("Start");
        initialize = true;
    }

    public override void OnShow()
    {
        CallMethod("OnShow");
    }


    public virtual void OnHide()
    {
        CallMethod("OnHide");
    }

    public override void ValueChange(UICmd cmd)
    {
        CallMethod("ValueChange", cmd.key, cmd.values);
    }

    /// <summary>
    /// 添加单击事件
    /// </summary>
    public void AddClick(string ctrName, LuaFunction luafunc)
    {

        if (string.IsNullOrEmpty(ctrName))
            return;
        GameObject go = transform.FindChild(ctrName).gameObject;
        if (go == null)
            return;
        UIEventListener.Get(go).onClick = delegate (GameObject o)
        {
            luafunc.Call(go);
            buttons.Add(luafunc);
        };
    }

    /// <summary>
    /// 根据gameobject绑定按钮事件
    /// </summary>
    /// <param name="btn"></param>
    /// <param name="luafunc"></param>
    public void AddClickForObj(GameObject btn, LuaFunction luafunc)
    {
        if (btn == null)
            return;
        UIEventListener.Get(btn).onClick = delegate (GameObject o)
        {
            luafunc.Call(btn);
            buttons.Add(luafunc);
        };
    }


    /// <summary>
    /// 带一个整形参数的回调
    /// </summary>
    public void AddClickForObjWithIntParam(GameObject btn, LuaFunction luafunc, int iParam)
    {

        if (btn == null)
            return;
        UIEventListener.Get(btn).onClick = delegate (GameObject o)
        {
            luafunc.Call(iParam);
            buttons.Add(luafunc);
        };
    }

    /// <summary>
    /// 清除单击事件
    /// </summary>
    public void ClearClick()
    {
        for (int i = 0; i < buttons.Count; i++)
        {
            if (buttons[i] != null)
            {
                buttons[i].Dispose();
                buttons[i] = null;
            }
        }
    }

    /// <summary>
    /// 执行Lua方法
    /// </summary>
    protected object[] CallMethod(string func, params object[] args)
    {
        return Util.CallMethod(luaFileName, func, args);
    }

    public override void OnDestroy()
    {
        ClearClick();
        initialize = false;
        CallMethod("OnDestroy");
    }
}


最后看一下上面最常用到的Util.CallMethod方法,工具方法调用lua的方法

/// <summary>
/// 执行Lua方法
/// </summary>
public static object[] CallMethod(string module, string func, params object[] args) {
	LuaScriptMgr luaMgr = GameLuaManager.Get().LuaManager;
	if (luaMgr == null)
        return null;
	string funcName = module + "." + func;
	funcName = funcName.Replace("(Clone)", "");
        return luaMgr.CallLuaFunction(funcName, args);
}


最后看一个ui的lua脚本例子

--LoginZiJia.lua 
LoginZiJia = {} --这个模块,对应LoginZiJia这个UIPrefab
local this = LoginZiJia
local gameObject = nil
local playerName = ""
local playerSex = 0
local playerProfession = 0
local iP = ""
local port = 0

function LoginZiJia.Awake(obj)

	gameObject = obj --把实例化的GameObject传递过来了
end

function LoginZiJia.Start()

	playerName = GameObject.Find('Scale/Input_Name'):GetComponent('UIInput')
	playerName.text = "test2018"
	playerSex = GameObject.Find('Scale/Input_Sex'):GetComponent('UIInput')
	playerSex.text ='0'
	playerProfession = GameObject.Find('Scale/Input_Profession'):GetComponent('UIInput')
	playerProfession.text = '0'
	iP = GameObject.Find('Scale/Input_IP'):GetComponent('UIInput')
	iP.text = GameAPI.getZiJiaIP()
	port = GameObject.Find('Scale/Input_Port'):GetComponent('UIInput')
	port.text = tostring(GameAPI.getZiJiaPort())
	--获取UILuaBehaviour可以使用一些方便的方法
	local uilua = gameObject:GetComponent('UILuaBehaviour')
	uilua:AddClick('Scale/LoginBtn',this.onLogin)
	uilua:AddClick('Scale/LoginReturn',this.onBack)
end

在很多类型的游戏中,麻将等的桌子型游戏,都是把自己的相关内容放在自己屏幕下方的。而每个客户端都是一个个体,他们都是需要放置在下方的。

解决办法就是:

前端定义好方位的索引,比如说下方是0,下一个是右边1......等,简称为显示ID。还有一个就是真实的桌子ID了,有几位位置就有几个id,0,1,2,3等,简称椅子ID。自己分配到的ID是不确定的,可能是3也可能是4,所以需要做一个映射对应到显示ID。

那么这个映射就很简单了:(椅子ID - 我的椅子ID + 总的容量) % 总的容量

同理反映射就是:(显示ID + 我的椅子ID) % 总的容量


抽象场景中活动的物体,添加上一个叫做Entity的脚本的,Entity是一个继承自MonoBehaviour的脚本组件,用来控制播放动画和检测进入泉水,Entity拥有一个ientity或则是继承自ientity的iplayer和iselfplayer,ientity用来保存同步信息(Transform组件,血值,攻击,模型id等)和提取了一些公共的方法(进入站立,进入移动,移动中,释放技能(进入,前摇,释放),击飞,下落,更新数据,打开商店,创建和显示隐藏血条等),如果是特殊的会声明为virtual在player那里重写并可以扩展新的方法。ientity已经有了很多的行为了,现在就需要在某种情况调用某种行为了,所以ientity会拥有一个EntityFSM状态机,这些状态机状态其实是一个工具类,他们直接调用的还是具体某个ientity的某个方法,只要将ientity的状态设置为具体的某个EntityFSM

public interface EntityFSM
{
	bool CanNotStateChange{set;get;}
	FsmState State { get; }
	void Enter(Ientity entity , float stateLast);
	bool StateChange(Ientity entity , EntityFSM state);
	void Execute(Ientity entity);
	void Exit(Ientity Ientity);
}

public class EntityIdleFSM : EntityFSM
{
    public static readonly EntityFSM Instance = new EntityIdleFSM();
		
	public FsmState State
	{
		get
        {
            return FsmState.FSM_STATE_IDLE;
		}
	}
		
	public bool CanNotStateChange{
		set;get;
	}
		
	public bool StateChange(Ientity entity , EntityFSM fsm)
    {
		return CanNotStateChange;
	}
		
	public void Enter(Ientity entity , float last)
    {
        entity.OnEnterIdle();
	}
		
	public void Execute(Ientity entity)
    {
        if (EntityStrategyHelper.IsTick(entity, 3.0f))
        {
            entity.OnFSMStateChange(EntityFreeFSM.Instance);
        }
	}
		
	public void Exit(Ientity entity){
			
	}
}


在ientity中维护这一个EntityFSM,提供了转换的接口,在转换时修改这个EntityFSM的引用实例并调用这个EntityFSM的Enter方法,随后在Entity组件的Update方法中调用的ientity的Update方法再调用的EntityFSM的Update方法,而实际Update和Enter方法都会传入ientity,然后调用的实际还是ientity里的具体方法,只是EntityFSM的不同,调用的不同!

EntityFSM的状态有:EntityFSM、EntityDeadFSM、EntityForceMoveFSM、EntityFreeFSM、EntityIdleFSM、EntityLastingFSM、EntityLeadingFSM、EntityReleaseSkillFSM、EntityReliveFSM、EntityRunFSM、EntitySingFSM。


架构:Many Manager的方式,管理器全局唯一单例。EventCenter中心事件机制(中介者),负责NetMsgHandler、State、Controller、View(UI,Model)之间的解耦。

首先是ResourceUpdateManager进行资源的更新,随后GameStateManager(事先new了所有的状态)进入默认的LoginState(监听状态改变的事件,调用WindowManager的转换State方法(事先准备Windows和Windows的注册监听事件),调用LoginCtrl的进入方法),LoginCtrl注册监听事件后广播显示UI的消息,Window接受到后显示出来,然后UI交互,接受输入事件后广播传递给LoginCtrl,处理调用NetWorkManager发送消息,NetWorkManager收到回复后交给具体的NetMsgHandler处理,广播具体事件(LoginState或LoginCtrl处理),LoginCtrl处理就是进入循环了,如果是LoginState处理说明要转换状态了,设置GameStateManager要进入的状态,然后就回到开头了,只是状态不同了,控制器不同了,监听也不同了,Window不同了等。

主要的状态有:LoginState、UserState、LobbyState、RoomState、HeroState、LodingState、PlayState、OverState。除了PlayState状态,其实其他的都是UI和网络的事件、资源加载的处理。核心部分都在PlayState,而PlayState最重要的应该就是状态机。

LoginState为登陆状态,UserState为获取玩家信息状态,LobbyState为大厅状态,RoomState为房间状态,HeroState为选英雄状态,LodingState为加载状态,PlayState为游戏中状态,OverState为清算状态。归结为就是:打开游戏,登陆游戏,得到你的信息,进入大厅,匹配或自己开房间进入房间,开始后选英雄,选完英雄加载游戏场景,加载完了就可以玩了,玩完了就清算显示你的战绩,然后回到房间或大厅。