将模块插件化以及在工程中的控制
模块化不管是对工程管理还是开发来说, 都是百利而无一害的, 从开发层面来看, 它强行让开发人员在开发的时候要考虑到跟其它模块的解耦, 考虑在其它环境下的泛用性和复用性, 直接就能提高开发水平, 并且在开新项目或者移植的时候, 能够拿来就用, 或者单独使用, 或者拼凑使用这些模块, 要什么功能就下哪个插件, 这就是模块化的好处, 并且输入输出接口能够稳定的话, 其它调用到这些模块的人员也会轻松很多.
然而现在我们的工程也是一锅粥, 在前面的开发中也没有想过这些...
现在如果进行模块化, 不但需要进行前期的代码解耦, 中期的代码剥离和提供大量外部注入, 到后期的管理工具都需要做起来, 才能完成一个正常的模块化过程, 现在用的 Unity3D 它官方提供的插件下载 / 更新 / 管理方案就跟我想的差不多, 想要什么就下载即可, 所有下载来的插件都是能够不依赖外部环境运行的, 这就非常方便了, 我还记得以前魔兽世界插件, 很多插件会依赖其它库, 比如 ACE 函数库, 你下了插件没有库结果没有用, 它不是一个自恰的...
这两天试了一下, 感觉还是直接通过 SVN 进行版本控制比较方便, 在 Repositories 下创建不同的工程目录, 每个工程就是一个模块的代码库, 通过 Unity 打开工程后编辑修改从开发工程中提取出来的代码, 修改成一个能够自恰的工程, 这些模块就能独立运作了.
然后是模块管理工具, 我的想法是也创建一个SVN仓库, 在一个工程的 CheckOut 中再进行管理工具的 CehckOut, 而管理工具提供可视化的选项, 可以选择相应的模块进行下载和更新, 这样每个模块只要加个文档让别人知道需要初始化的东西然后就能使用就行了.
--------------------- 开工的分界线 -----------------------
于是就开始尝试看看吧, 先找两个比较典型的模块来制作 :
1. 资源打包加载模块 [ ResourceModule ]
2. UI 模块 [ UIModule ]
显然资源打包是一个对其它东西没有依赖的模块, 只需要提供接口即可, 比较简单, 而 UI 模块很依赖于资源加载, 我们就需要给它提供注入加载资源的方法才能运行的模块.
在 SVN 上先创建它们的目录, 大致如下 :
这里额外的一个 BasicProject 就是管理工具的工程, 它作为起点来提供模块下载控制.
资源加载模块没什么好说的, 本身就是独立的, UI 模块只需要使用外部提供的加载过程即可, 看看有改动的地方 :
public sealed class UIManager : MonoSingleton<UIManager> { private static System.Func<string, string, GameObject> ms_loadModule = null; // set load method public static void ImplementLoadModule(System.Func<string, string, GameObject> loadModule) { ms_loadModule = loadModule; } public T LoadUI<T>(string prefabLoadPath) where T : UIBase { if(ms_loadModule == null) { throw new System.NotImplementedException("Resource Load Module Not Implement !!!"); // Tips } T ui = null; var go = ms_loadModule.Invoke(prefabLoadPath, UIPoolName); if(go) { ui = go.GetComponent<T>(); // ... } return ui; } }
这里可以通过静态设置读取方式, 添加错误提示的话, 使用起来基本不会出错了.
当然其它模块肯定没这么给你面子, 肯定到处调用其它模块的代码, 这就需要后续再看了, 至少基础的底层代码是可以做到的.
然后是控制工具, BasicProject 其实就是一个编辑器工具, 它提供图形界面的话就可以方便控制了, 于是写到一个 EditorWindow 里面, 它的逻辑意外的很简单, 首先在里面用代码写死一些模块名称和相应的 SVN 地址, 来表示模块下载地址, 然后通过查询远程版本和本地版本, 来决定是否需要下载或是更新, 当然是由用户自己选择了, 结构大致如下 :
public class ModuleStatus { public string remotePath; public int remoteVersion; public string localPath; public int localVersion; } public static readonly Dictionary<string, string> Modules = new Dictionary<string, string>() { { "BasicProject", "https://XXXX/svn/BasicProject/Assets/BasicProject" }, { "ResourceModule", "https://XXXX/svn/ResourceModule/Assets/ResourceModule" }, { "UIModule", "https://XXXX/svn/UIModule/Assets/UIModule" }, };
因为它把自己的工程 BasicProject 也加入到插件列表中了, 它就能更新它自己了, 所以这个列表等于是可以正常被更新的, 并非写死的了. 它的初始化逻辑如下 :
private Dictionary<string, ModuleStatus> m_existsModules = new Dictionary<string, ModuleStatus>(); private HashSet<string> m_noExistsModules = new HashSet<string>(); private volatile bool _inited = false; private volatile int _initCount = 0; private volatile int _initedCount = 0; private void OnEnable() { Load(); } private void Load() { UnitySVN.StopAllThreads(); _inited = false; _initCount = 0; _initedCount = 0; m_existsModules.Clear(); m_noExistsModules.Clear(); m_noExistsModules.UnionWith(Modules.Keys); var folders = Directory.GetDirectories(Application.dataPath, "*.*", SearchOption.AllDirectories); foreach(var folder in folders) { var module = Path.GetFileName(folder); if(Modules.ContainsKey(module)) { _initCount++; UnitySVN.IsVersioned(folder, (_versioned) => { if(_versioned) { var status = new ModuleStatus() { localPath = CommonEditorUtils.LeftSlash(folder), remotePath = Modules[module], }; UnitySVN.GetVersionNumber(@status.remotePath, (_ver) => { status.remoteVersion = _ver; }); UnitySVN.GetVersionNumber(@status.localPath, (_ver) => { status.localVersion = _ver; }); lock(m_existsModules) { m_existsModules[module] = status; } lock(m_noExistsModules) { m_noExistsModules.Remove(module); } } _initedCount++; if(_initedCount >= _initCount) { _inited = true; } }); } } }
SVN 相关的后面再说, 初始化首选去找模块相应的名称的文件夹, 然后查询是否在版本控制之下, 如果没有就加入到 m_noExistsModules 列表中, 显示为可下载. 如果存在就加入到 m_existsModules 中, 这里面包含了相关信息, 包括远程 SVN 地址, 远程版本号, 本地地址, 本地版本号等, 如果版本对不上, 显示为可更新 :
显示 ResourceModule 和 UIModule 都处于可下载状态, 如果点击下载, 会得到下面的图 :
显示的 Version 是本地版本号, 然后我们在其它工程中对 ResourceModule 进行更新, 使它提升版本号, 然后再打开面板看 :
显示可更新, 点击后工程就更新到相应版本了 :
把模块都下载下来之后, 就可以进行UI加载了, 看看要怎样初始化 :
因为有了UI模块, 可以继承一个 UIBase :
public class MainUI : UIModule.UIBase { }
加载之前只要初始化读取模块给 UIManager 就行了
using UnityEngine; using ResourceModule; using UIModule; public class Test : MonoBehaviour { private void Awake() { UIManager.ImplementLoadModule(PrefabLoader.Instance.Spawn); } // Use this for initialization void Start() { UIManager.Instance.Get<MainUI>("MainUI"); } }
因为旧工程中的加载UI和这里一样, 区别只是需要对 UIManager 设置一个 LoadModule 的函数, 之前就是直接嵌入 PrefabLoader 的, 没有太大区别.
经过这些改动, 就能获得一个解耦的模块代码了, 并且能随用随下, 其实对于复杂工程来说反而是好事, 提高了开发人员的程序设计能力, 减少耦合...
------------------- SVN -----------------
之前的各种逻辑直接调用的 TortoiseSVN 的扩展命令 /c , 为了泛用性修改为直接调用 svn 的命令行的方式了, 只有一个需要注意的地方, 命令行一个命令的长度为8192长度, 通过一些方法可以加大到32KB, 可是很麻烦也没有根本解决问题, 因为一般使用绝对路径来传输, 像下面这样 :
svn add E:\XXXX\OOOO\Assets\Test.cs.meta E:\XXXX\OOOO\Assets\Test.cs
文件多了肯定完蛋, 写一个循环上传的过程就行了.