一般来说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为清算状态。归结为就是:打开游戏,登陆游戏,得到你的信息,进入大厅,匹配或自己开房间进入房间,开始后选英雄,选完英雄加载游戏场景,加载完了就可以玩了,玩完了就清算显示你的战绩,然后回到房间或大厅。