最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 框架的诞生-一:我想要的框架

    正文概述 掘金(AILHC)   2020-11-30   814

    我想要的框架是什么样的?

    框架的诞生-一:我想要的框架

    上一篇文章: 《框架的诞生-零:为什么写框架?》里有讲 什么是框架。

    框架是一个架子,在游戏程序中,抛开渲染层引擎框架,我们的指的框架就是支撑业务逻辑的架子,也是一个框框,规范和约束着开发人员。

    每个框架都有着自己的边界,解决特定领域问题的。

    那我们从去分析我遇到了什么问题,有什么需求,如何解决问题,如何实现需求。

    无论是小项目还是大项目,或者是一个会不断扩张的项目,都希望能够项目代码结构清晰有条理,管理好不同模块。

    我尝试过的方式

    Manager of Managers

    很多框架都用这种方式(包括我之前写的框架)

    这是刘钢老师的《Unity 项目架构设计与开发管理》中讲到的一种相对较好的方式 ▼

    框架的诞生-一:我想要的框架

    优点是:类似于分级结构,各司其职;比如音频管理,场景管理,关卡管理等,每一个都是一个单例脚本,配合使用。结构相对清晰。可以复用

    但我个人不太喜欢的点

    • 调用不方便,调用链太长,费手指。比如 UIManager.getInstance().showUI 或者 UIMgr.ins.showUI
      • 我想这样: m.uiMgr.showUI 少敲几个字母,也不想 import UIMgr
    • 无生命周期统一管理,单例初始化和依赖不太可控。惰性初始化,等到调用 getInstance 才初始化。
    • 控制台调试调用不方便,单例可能得单独绑定暴露到全局才能,每个模块都得这样做才行
    • 直接依赖不可动态替换
      • 有个模块我想在不同的平台切换成不同的实现,单例做不到。
    • 大多耦合引擎来开发,只能在同引擎项目进行复用

    模块字典挂载到全局变量 window

    这是我之前的框架使用的方式

    就是将所有的模块初始化,然后注入一个模块字典

    然后将字典挂载到全局的一个变量中。

    export class Main {
        constructor(){
            const moduleMap = {
    
            };
            moduleMap["uiMgr"] = new UIMgr();
            moduleMap["netMgr"] = new NetMgr();
    
            window["aa"] = moduleMap;
        }
    }
    class UIMgr {
    
    }
    class NetMgr {
    
    }
    

    优点:调用方便

    缺点: 有点危险,别人知道了可以在控制台调用进行调试。

    这些方式在模块管理上都有问题,先不考虑如何方便调用,先实现个如何管理模块的核心机制。

    一个具有生命周期的模块化机制

    pomelo 给我的灵感

    这个想法的灵感来自 pomelo 一个网易开发的基于 nodejs 的分布式服务器框架。

    pomelo 支持可插拔的 component 扩展架构

    用户只要实现 component 相关的接口: start, afterStart, stop, 就可以加载自定义的组件:

    app.load([name], comp, [opts])
    

    start, afterStart 这些生命周期 接口跟 cocos 和 Unity 的组件式接口很像。 主要是方便处理不同模块之间的依赖引用。比如:A 依赖了 B,但 B 还未初始化。

    各自的初始化,都在 start 里处理,然后在 afterStart 里进行依赖调用。

    可能对于不同的业务,这些生命周期可能不够用,可以根据具体业务进行扩展,满足自定义需求。

    比如登录业务相关的:

    C 模块依赖 A 和 B 登录后的数据状态,那么增加两个接口 onLoginInit,onAfterLoginInit。

    那么 A 和 B 实现 onLoginInit 接口进行登录初始化,C 在 onAfterLoginInit 接口进行依赖调用。

    怎么实现我想要的框架?

    模块生命周期图

    框架的诞生-一:我想要的框架

    接口设计

    declare global {
        namespace egf {
    
            interface IModule {
                /**模块名 */
                key?: string
                /**
                 * 当初始化时
                 */
                onInit?(app: IApp): void;
                /**
                 * 所有模块初始化完成时
                 */
                onAfterInit?(app: IApp): void;
                /**
                 * 模块停止时
                 */
                onStop?(): void;
            }
            type BootEndCallback = (isSuccess: boolean) => void;
            /**
             * 引导程序
             */
            interface IBootLoader {
                /**
                 * 引导
                 * @param app
                 */
                onBoot(app: IApp, bootEnd: BootEndCallback): void;
            }
            /**
             * 主程序
             */
            interface IApp<ModuleMap = any> {
                /**
                 * 程序状态
                 * 0 未启动 1 引导中, 2 初始化, 3 运行中
                 */
                state: number;
                /**
                 * 模块字典
                 */
                moduleMap: ModuleMap;
                /**
                 * 引导
                 * @param bootLoaders
                 */
                bootstrap(bootLoaders: egf.IBootLoader[]): Promise<boolean>;
                /**
                 * 初始化
                 */
                init(): void;
                /**
                 * 加载模块
                 * @param module
                 */
                loadModule(module: IModule | any, key?: keyof ModuleMap): void;
                /**
                 * 停止
                 */
                stop(): void;
                /**
                 * 获取模块实例
                 * @param moduleKey
                 */
                getModule<T extends IModule = any>(moduleKey: keyof ModuleMap): T;
                /**
                 * 判断有没有这个模块
                 * @param moduleKey
                 */
                hasModule(moduleKey: keyof ModuleMap): boolean;
    
            }
        }
    }
    
    // eslint-disable-next-line @typescript-eslint/semi
    export { }
    

    Bootloader: CatLib 给我的灵感

    这里有一个 bootloader 的东西我没有讲到,它的灵感来自 CatLib,一个我觉得很棒的 Unity 框架。

    这个机制是什么呢?以开发测试环境和生产环境举例。

    有一个 debugBootLoader,这个引导程序处理一些测试用的模块加载和初始化,杂七杂八的。

    当你发布生产环境时,可以通过 debug 变量屏蔽加载这个引导程序,也可以通过编译工具剔除这段代码。

    具体实现可以看:https://github.com/AILHC/EasyGameFrameworkOpen/tree/main/packages/core#readme

    怎么使用?

    具体使用请看 demo 工程

    cocoscreator2.x 的 demo github.com/AILHC/egf-c…

    cocoscreator3d 的 demo github.com/AILHC/egf-c…

    如何接入项目

    //FrameworkLoader.ts
    import { HelloWorld } from "../HelloWorld";
    export class FrameworkLoader implements egf.IBootLoader {
        onBoot(app: egf.IApp, bootEnd: egf.BootEndCallback): void {
            const helloWorld = new HelloWorld();
            app.loadModule(helloWorld);
            bootEnd(true);
        }
    
    }
    //AppMain.ts
    import { App } from "@ailhc/egf-core"
    import { FrameworkLoader } from "./boot-loaders/FrameworkLoader";
    import { setModuleMap, m } from "./ModuleMap";
    /**
     * 这是一种启动和初始化框架的方式,在cocos加载脚本时启动
     * 不依赖场景加载和节点组件挂载
     */
    export class AppMain {
        public static app: App<IModuleMap>;
        public static initFramework() {
            const app = new App<IModuleMap>();
            AppMain.app = app;
            app.bootstrap([new FrameworkLoader()]);
            setModuleMap(app.moduleMap);
            app.init();
            window["m"] = m;//挂在到全局,方便控制台调试,生产环境可以屏蔽=>安全
            m.helloWorld.say();
        }
    
    }
    AppMain.initFramework();
    
    

    接入项目很简单,new 一下,bootstrap,init 就可以了~

    注入模块也很简单

    //在UIMgr.ts开头增加个声明
    declare global {
        interface IModuleMap {
            uiMgr:UIMgr
        }
    }
    //在初始化地方,注入实例
    app.loadModule(UIMgr.getInstance(),"uiMgr");
    

    注入的模块是什么类型的,不限制,你可以将业务模块 比如 HeroModule 注入进去,那么业务模块之间就可以直接调用了。也不用担心 typescript 的循环引用了。

    举个栗子(随便的):

    // BattleModule.ts
    m.hero.showHero(1);
    
    //HeroModule.ts
    m.battle.startTestBattle(1);
    
    

    就像服务端的 rpc 调用一样。

    app.rpc.chat.chatRemote.kick(session, uid, player, function(data){
    });
    

    至于怎么使得接口调用更方便,这个看个人的喜好,我呢,用了一点点魔法,让自己用着舒服又有点安全感。具体实现细节请看 demo

    我想在 CocosCreator 和 C3d 中使用

    由于我的工作中是用 Laya 的,项目也用了这个框架。但我私底下都是玩 CocosCreator 和 CocosCreator3d 的(为什么啊?你懂得 ??)

    我不想在项目之间将源码拷贝来拷贝去,迭代更新同步麻烦。

    如果能像 npm 包一样 安装就好了。而且核心模块是一个模块,其他模块也是一个模块。

    于是我开发了一个模块编译发布的工具,开发之前以为很简单,实际上,踩坑了好久 ?。

    这个模块编译发布工具有什么功能?

    • 编译模块成 iife、commonjs、systemjs 格式的 js 文件
    • 自动生成单个.d.ts 声明文件

    这个 systemjs 格式的 js 文件可以让不支持 npm 包的 CocosCreator3d 可以像使用 npm 包一样使用。即使到时 Cocos3.0 支持 npm 了,使用方式也一模一样。使用 C3d1.2.0 发布 web,微信小游戏,验证运行没有问题。

    import { App } from '@ailhc/egf-core';//像引用npm包一样引用
    import { _decorator, Component, Node } from 'cc';
    import { m, setModuleMap } from './ModuleMap';
    import { FrameworkLoader } from './boot-loaders/FrameworkLoader';
    const { ccclass, property } = _decorator;
    @ccclass('AppMainComp')
    export class AppMainComp extends Component {
        /* class member could be defined like this */
        // dummy = '';
    
        /* use `property` decorator if your want the member to be serializable */
        // @property
        // serializableDummy = 0;
    
        onLoad() {
            this._initFramework();
        }
        private _initFramework() {
            const app = new App<IModuleMap>();
            // new TestImport();
            app.bootstrap([new FrameworkLoader()]);
            // app.bootstrap([new FrameworkLoader2()]);
            setModuleMap(app.moduleMap);
            app.init();
            window["m"] = m;//挂在到全局,方便控制台调试,生产环境可以屏蔽=>安全
            m.helloWorld.say();
        }
        start() {
    
        }
    
        // update (dt) {}
    }
    

    如何开发一个模块

    1. 克隆项目 git clone github.com/AILHC/EasyG…
    2. 复制 packages/package-template 项目,改文件夹名,改 package.json 里的项目名等信息
    3. npm install 初始化项目
    4. 然后用 typescript 进行开发,使用 index.ts 文件将所有代码 export(可以使用 export-typescript 插件自动化,插件版本必须是 0.0.5 之前的)
    5. 使用 egf build 进行编译发布

    总结一下框架有什么特性

    • 轻量级模块化机制
    • 模块生命周期
      • 让模块的初始化有序,依赖可控
    • 可面向接口编程
      • 方便具体实现细节可替换,模块可动态替换
    • 友好的类型声明
      • 点一下就有类型提示,传字符串获取模块也有类型提示,很香的。
    • 基于 TypeScript 与引擎无关
    • 每个模块库都是一个 npm 包
    • 模块库可以导出多种 js 格式,让 laya,ccc,c3d 使用,甚至给 Unity、Unreal 用(xLua 作者的 Puerts 了解一下?)

    这个框架可以做什么?

    特性

    • 基于轻量级无依赖的模块机制,可以为不同项目量身定制框架,可大可小。也可以根据项目的不同阶段进行渐进式扩展。还可以在项目的不同阶段轻易地接入

    • 面向接口编程的模块,底层组件可以无感知替换

    • 基于模块开发工具,我们可以开发和发布一个单独的对核心零依赖的模块,给不同的项目使用。

      • 方便别的项目引用
      • 方便开源
      • 方便做单元测试
    • 基于模块化机制和配套开发工具,大家可以为公司或者个人建立自己的模块库,在不同项目按需复用。

    架构设想 ▼

    框架的诞生-一:我想要的框架

    谢谢大家阅读我的文章,希望大家能有所收获。

    框架开发系列文章

    • 框架的诞生-零:为什么写框架?
    • 框架的诞生-一:我想要的框架
    • 不只是 UI 管理:通用显示管理
    • 让 fairygui 更好用的插件
    • 满足多种需求的通用对象池
    • 构建游戏/应用的神器:broadcast
    • 满足所有自定义需求的通用 socket 网络模块
    • 业务开发总结之状态管理
    • 待续。。。

    最后

    欢迎关注,更多内容持续更新

    公众号搜索:玩转游戏开发

    QQ 群: 1103157878

    博客主页: ailhc.github.io/

    github: github.com/AILHC


    起源地下载网 » 框架的诞生-一:我想要的框架

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元