架構的誕生-一:我想要的架構
-
- 我想要的架構是什麼樣的?
-
- 我嘗試過的方式
-
- Manager of Managers
- 子產品字典挂載到全局變量 window
- 一個具有生命周期的子產品化機制
-
- pomelo 給我的靈感
- 怎麼實作我想要的架構?
-
-
- Bootloader: CatLib 給我的靈感
- 怎麼使用?
- 我想在 CocosCreator 和 C3d 中使用
-
- 這個子產品編譯釋出工具有什麼功能?
- 如何開發一個子產品
- 總結一下架構有什麼特性
-
- 這個架構可以做什麼?
-
- 特性
- 架構設想 ▼
- 架構開發系列文章
- 最後
- 最後
我想要的架構是什麼樣的?
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZwpmLvFGauV2dtc2bk9CXn1WavwVbvNmLkV3bsNWc51mL19Ga6dmbhV3ZtAXYuM3bj5CM4ADMzUTN1ITMtUzM1cjNz0CN3UDNtUTO3gDO1cTL3QTM0MDNtQXZrNWdi1ycldWYw1yZulGZvN2Lc9CX6MHc0RHaiojIsJye.jpg)
上一篇文章: 《架構的誕生-零:為什麼寫架構?》裡有講 什麼是架構。
架構是一個架子,在遊戲程式中,抛開渲染層引擎架構,我們的指的架構就是支撐業務邏輯的架子,也是一個框框,規範和限制着開發人員。
每個架構都有着自己的邊界,解決特定領域問題的。
那我們從去分析我遇到了什麼問題,有什麼需求,如何解決問題,如何實作需求。
無論是小項目還是大項目,或者是一個會不斷擴張的項目,都希望能夠項目代碼結構清晰有條理,管理好不同子產品。
我嘗試過的方式
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) {}
}
如何開發一個子產品
- 克隆項目 git clone https://github.com/AILHC/EasyGameFrameworkOpen.git
- 複制 packages/package-template 項目,改檔案夾名,改 package.json 裡的項目名等資訊
- npm install 初始化項目
- 然後用 typescript 進行開發,使用 index.ts 檔案将所有代碼 export(可以使用 export-typescript 插件自動化,插件版本必須是 0.0.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