天天看點

架構的誕生-一:我想要的架構

架構的誕生-一:我想要的架構

    • 我想要的架構是什麼樣的?
      • 我嘗試過的方式
        • Manager of Managers
        • 子產品字典挂載到全局變量 window
      • 一個具有生命周期的子產品化機制
        • pomelo 給我的靈感
    • 怎麼實作我想要的架構?
        • Bootloader: CatLib 給我的靈感
        • 怎麼使用?
      • 我想在 CocosCreator 和 C3d 中使用
        • 這個子產品編譯釋出工具有什麼功能?
        • 如何開發一個子產品
      • 總結一下架構有什麼特性
    • 這個架構可以做什麼?
      • 特性
      • 架構設想 ▼
    • 架構開發系列文章
    • 最後
    • 最後

我想要的架構是什麼樣的?

架構的誕生-一:我想要的架構

上一篇文章: 《架構的誕生-零:為什麼寫架構?》裡有講 什麼是架構。

架構是一個架子,在遊戲程式中,抛開渲染層引擎架構,我們的指的架構就是支撐業務邏輯的架子,也是一個框框,規範和限制着開發人員。

每個架構都有着自己的邊界,解決特定領域問題的。

那我們從去分析我遇到了什麼問題,有什麼需求,如何解決問題,如何實作需求。

無論是小項目還是大項目,或者是一個會不斷擴張的項目,都希望能夠項目代碼結構清晰有條理,管理好不同子產品。

我嘗試過的方式

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, 就可以加載自定義的元件:

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 https://github.com/AILHC/egf-ccc-empty

cocoscreator3d 的 demo https://github.com/AILHC/egf-ccc3d-empty

如何接入項目▼

//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 https://github.com/AILHC/EasyGameFrameworkOpen.git
  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 了解一下?)

這個架構可以做什麼?

特性

  • 基于輕量級無依賴的子產品機制,可以為不同項目量身定制架構,可大可小。也可以根據項目的不同階段進行漸進式擴充。還可以在項目的不同階段輕易地接入
  • 面向接口程式設計的子產品,底層元件可以無感覺替換
  • 基于子產品開發工具,我們可以開發和釋出一個單獨的對核心零依賴的子產品,給不同的項目使用。
    • 友善别的項目引用
    • 友善開源
    • 友善做單元測試
  • 基于子產品化機制和配套開發工具,大家可以為公司或者個人建立自己的子產品庫,在不同項目按需複用。

架構設想 ▼

架構的誕生-一:我想要的架構

謝謝大家閱讀我的文章,希望大家能有所收獲。

謝謝大家閱讀我的文章,希望大家能有所收獲。

架構開發系列文章

  • 架構的誕生-零:為什麼寫架構?
  • 架構的誕生-一:我想要的架構
  • 打破CocosCreator3d不能使用npm包的魔咒!!!
  • 架構的誕生-二:定位
  • 不隻是 UI 管理:通用顯示管理
  • 讓 fairygui 更好用的插件
  • 滿足多種需求的通用對象池
  • 建構遊戲/應用的神器:broadcast
  • 滿足所有自定義需求的通用 socket 網絡子產品
  • 業務開發總結之狀态管理
  • 待續。。。

最後

歡迎關注我的公衆号,更多内容持續更新

公衆号搜尋:玩轉遊戲開發

或掃碼:

架構的誕生-一:我想要的架構

QQ 群: 1103157878

部落格首頁: https://ailhc.github.io/

掘金: https://juejin.cn/user/3069492195769469

github: https://github.com/AILHC

最後

歡迎關注我的公衆号,更多内容持續更新

公衆号搜尋:玩轉遊戲開發

或掃碼:

架構的誕生-一:我想要的架構

QQ 群: 1103157878

部落格首頁: https://ailhc.github.io/

掘金: https://juejin.cn/user/3069492195769469

github: https://github.com/AILHC

繼續閱讀