preface

Earlier this month, I posted a few posts sharing my journey and some of my own thoughts on framing. Those who are interested can review through the link at the end of the article.

At the same time I released my first open source progressive H5 game development framework: EasyGameFrameworkOpen

Provides a powerful lightweight core library: module management library @ailHC/EGF-core. Seamless access to any engine game project

And a rollup-based typescript library builder: @ailhc/egf-cli, which can build js libraries for any engine project and a single. D. ts declaration file similar to cocos.

In the previous post, I also previewed it, and I will share it later. Subsequent shares are also about the design and implementation of other libraries for the framework.

It’s not out yet, but it’s being updated all the time.

You can pay attention to the framework of github repository, if you can, give a star oh.

Why did the pigeon take so long to update?

It was expected to be updated last week.

But nye ~(~ ▽ ~)~*

  1. First of all, I am busy at work, and I have things to deal with at home
  2. At the same time, this aspect that I’m going to talk about is a little bit more difficult, and I want to make it clear (forgive my childish touch).
  3. I want to add as many unit tests and demo examples as possible (after all, move towards more formal engineering)

Pigeon 🕊 : Don’t beep beep, pigeon is pigeon, hurry into the topic ~

Me: Ok

background

For most games, the UI interface is essential.

When the UI of the project is much more, and the requirements of the planners are strange, and the iteration is frequent, it can be a headache for us.

Next, I will share with my immature touch how I analyze and solve this headache problem.

The topic is subjective and open, everyone has their own business development experience and ideas, and I’m here to throw some mud at you.

🙏(•̀ ω •́)✧

Essential requirements

Let’s look at two scenarios

  1. The plan proposed a requirement: a text prompt interface B with colorful black background -> programmers to implement the logic of interface B

  2. Click A button -> Show B interface

These two scenarios are the two most common and essential requirements in our development:

  1. Implement UI interface logic

  2. Call interface

So the problem is the implementation of these two requirements, and I’ll talk about them separately

Implementation of UI logic

The implementation of UI logic is varied and the requirements are bizarre.

And the implementation may not be in place all at once, it may need iteration, rework, etc.

So in this case, I as the developer of UI logic, I want to

  1. I don’t care where the other business logic calls me or how it calls me, just by telling the outside world how to call me, okay
  2. Have enough degrees of freedom
    1. I don’t have to load prefab, I want to be able to load an image to dynamically new a Node to add component displays
    2. I don’t have to instantiate PreFab either. As a display node, I want to be able to draw my own display through the drawing API
    3. I don’t have to use Cocoscreator for rendering, I want to be able to write HTML rendering, fGUI rendering, android and ios native interface rendering
    4. I don’t necessarily use generic load release logic, I want to be able to customize the load and resource release logic
    5. You can control the node to be added to that parent.
    6. Can rely on multiple different types of resources, dynamically generated
  3. High enough extensibility without changing the function interface

This high degree of scalability, freedom, and transparency allows developers to focus on implementing and iterating UI logic efficiently.

  • You don’t have to worry about bugs from other people’s calls
  • You don’t have to get a headache from tying your hands
  • You don’t have to worry about trying to extend without changing the interface

The UI calls

We may have requirements for various UI calls during project development:

The most complex and common UI calls are display calls, such as

  1. Display page A
  2. Display B interface and pass data to B interface according to the display data interface of B interface, B render according to the data passed, and even pass various callbacks
  3. The C interface is displayed, and the callback is displayed after the C interface is called. The callback is completed, that is, the logic is executed after the interface is displayed

The other common ones are

  1. update
  2. hidden
  3. The destruction

There are also some special ones, such as: preloading specified interface (not displayed), obtaining specified interface dependent resources (used to batch load multiple interface dependent resources)

In addition to the need for functionality, there is also a need for interface extensibility

Because in different projects, or in different project phases, there may be additional requirements that need to extend the interface parameters. But if the interface has too many parameters, the call can be long and cumbersome.

So you want the interface to be more extensible, and you can do that by adding interfaces without changing the underlying layer.

Looks like he dug a hole for himself. But don’t those who make wheels dig holes for themselves?

After analyzing the requirements, we can proceed to design the interface

Interface design

A simple flow chart

Look at another UML diagram

Interface design of UI controller

The job of the UI controller is to implement the UI logic

 /** * Displays the configuration */
interface IShowConfig<
      TypeKey extendskeyof any = any, InitDataTypeMapType = any, ShowDataTypeMapType = any, > { typeKey? : TypeKey,/** * Passthrough initialization data */onInitData? : InitDataTypeMapType[ToAnyIndexKey<TypeKey, InitDataTypeMapType>]/** * force a reload */forceLoad? :boolean
    /** * Display data */onShowData? : ShowDataTypeMapType[ToAnyIndexKey<TypeKey, ShowDataTypeMapType>],/** Callback */ after calling controller instance onShowshowedCb? : CtrlInsCb;/** The controller is displayed after the callback */showEndCb? : VoidFunction;/** the display is cancelled */onCancel? : VoidFunction,/** The onLoad parameter is */onLoadData? :any./** If the instance is empty, the load fails, and the instance is successful */loadCb? : CtrlInsCb }interfaceICtrl<NodeType = any> { key? :string | any;
    /** loading */isLoading? :boolean;
    /** Already loaded */isLoaded? :boolean;
    /** Already initialized */isInited? :boolean;
    /** * already displayed */isShowed? :boolean;
    /** displays */needShow? :boolean
    /** need to load */needLoad? :boolean
    /** is displaying */isShowing? :boolean

    / processing of data to be loaded to * * *, * and calling in the display interface showDpc onLoadData merged, * * the Object is given priority to with the interface of the incoming. Assign (ins) onLoadData, CFG. OnLoadData); * * /onLoadData? :any;
    /** Get the resource */getRess? () :string[] | any[];
    /** * initialize *@param InitData Initializes data */onInit(config? : displayCtrl.IInitConfig):void;
    /** ** when displayed *@param ShowData Displays data */onShow(config? : displayCtrl.IShowConfig):void;
    /** ** when updated *@param UpdateData Updates data *@param EndCb End callback */
    onUpdate(updateData: any) :void;
    /** * Get the controller */
    getFace<T>(): ReturnCtrlType<T>;
    /** ** when hidden */
    onHide(): void;
    /** * Forcibly hide */
    forceHide(): void;
    /** ** when destroyed *@param destroyRes 
     */onDestroy(destroyRes? :boolean) :void;
    /** * get the display node */
    getNode(): NodeType;
}
Copy the code

This interface is independent of any engine’s interfaces and classes. Each engine project implements a corresponding base class

Managers and businesses don’t need to care how the UI logic is implemented, just call the interface and pass the data object according to the external data interface. Right

  1. OnLoadData this is general load transparent data, such as: tell the general load logic to display what load wait interface
  2. Initialization interface onInit, mainly used for instantiating display nodes, listening to UI interaction events, etc
  3. Display interface onShow, mainly is the UI display and according to the incoming data for rendering
  4. Update interface onUpdate, mainly based on the incoming data for rendering updates
  5. The getRess interface is used to obtain the resource information dependent on the UI interface for loading and releasing the general resource processing logic

The other interfaces are pretty simple, so I’m going to skip this and I’m going to focus on the onShow interface the design of the first version was

/ * * *@param OnShowData Passthrough data when called */onShow(onShowData? :any) :void;
Copy the code

Such a design, can only achieve custom transparent display data, but to achieve the expansion of the management logic without changing the onShow interface, it will be very troublesome

For example, I want to extend the manager to pass a display completion callback to the UI control: send the UI display completion event, let the UI logic finish the animation, or other deferred action after the call.

In the first version, only one parameter would be added: onShow(onShowData? :any,showEnd? :VoidFunction)

This time is to modify the ICtrl interface design. One is fine, but what if it becomes two, three, four parameters? It gets complicated. The current design can solve this problem by extending the interface of passthrough Config parameters without adding interface parameters or changing the interface

This was inspired by the Axios library, an easy-to-use, concise, and efficient HTTP library

About custom resource handling implementations

UI controllers with custom resource handling requirements need to implement this interface

/** * Resource handler */
interface IResHandler {
    /** * Load resources *@param config 
     */loadRes? (config: displayCtrl.IResLoadConfig):void;
    /** * Release resources *@param ctrlIns 
     */releaseRes? (ctrlIns? : ICtrl):void;
}
Copy the code

The manager then invokes the custom resource handling interface instead of going through generic resource handling

Such interface design can be said to give the UI logic implementor a great deal of freedom to focus on efficient implementation and iteration, as well as arbitrary extension, while the UI is managed

Extreme freedom comes at a price, just not out of the box. It’s really easy to implement interfaces, so it’s worth it.

See the egF-CCC-full implementation for a specific demo example

Moving on to the UI manager design

UI manager design

The job of the UI manager is to provide an interface for businesses to invoke the UI

interface IMgr<
    CtrlKeyMapType = any,
    InitDataTypeMapType = any,
    ShowDataTypeMapType = any,
    UpdateDataTypeMapType = any> {
    /** Controller key dictionary */
    keys: CtrlKeyMapType;
    /** * Controller singleton dictionary */
    sigCtrlCache: CtrlInsMap;
    /** * initialize *@param ResHandler resource handling */init(resHandler? : IResHandler):void;
    /** * Batch register controller classes *@param classMap 
     */
    registTypes(classes: CtrlClassMap | CtrlClassType[]): void;
    /** * Register the controller class *@param ctrlClass 
     * @param TypeKey If there is no static typeKey in ctrlClass, take the typeKey */regist(ctrlClass: CtrlClassType, typeKey? : keyof CtrlKeyMapType):void;
    /** * Whether to register *@param typeKey 
     */
    isRegisted<keyType extends keyof CtrlKeyMapType>(typeKey: keyType): boolean;
    /** * get the resource information of the registered class * read the static variable ress *@param typeKey 
     */
    getDpcRessInClass<keyType extends keyof CtrlKeyMapType>(typeKey: keyType): string[] | any[]
    /** * get the resource array of the singleton UI *@param typeKey 
     */
    getSigDpcRess<keyType extends keyof CtrlKeyMapType>(typeKey: keyType): string[] | any[];
    /** * Get/generate a singleton display controller example *@param TypeKey Key */
    getSigDpcIns<T, keyType extends keyof CtrlKeyMapType = any>(typeKey: keyType): displayCtrl.ReturnCtrlType<T>
    /** * Load Dpc *@param TypeKey typeKey * during registration@param LoadCfg transparent data and callback */
    loadSigDpc<T, keyType extends keyof CtrlKeyMapType = any>(typeKey: keyType, loadCfg? : displayCtrl.ILoadConfig): displayCtrl.ReturnCtrlType<T>;/** * Initializes the display controller *@param TypeKey The typeKey * used when registering a class@param initCfg displayCtrl.IInitConfig
     */
    initSigDpc<T, keyType extends keyof CtrlKeyMapType = any>( typeKey: keyType, initCfg? : displayCtrl.IInitConfig<keyType, InitDataTypeMapType> ): displayCtrl.ReturnCtrlType<T>;/** * display singleton display controller *@param TypeKey Class Key or display configuration IShowConfig *@param OnShowData Displays transparent data *@param ShowedCb shows complete callback (after onShow call) *@param OnInitData Initializes transparent data *@param ForceLoad Whether to force a reload *@param OnCancel When the display is cancelled */
    showDpc<T, keyType extends keyof CtrlKeyMapType = any>( typeKey: keyType | displayCtrl.IShowConfig<keyType, InitDataTypeMapType, ShowDataTypeMapType>, onShowData? : ShowDataTypeMapType[displayCtrl.ToAnyIndexKey<keyType, ShowDataTypeMapType>], showedCb? : displayCtrl.CtrlInsCb<T>, onInitData? : InitDataTypeMapType[displayCtrl.ToAnyIndexKey<keyType, InitDataTypeMapType>], forceLoad? :boolean, onLoadData? :any, loadCb? : displayCtrl.CtrlInsCb, onCancel? : VoidFunction ): displayCtrl.ReturnCtrlType<T>;/** * Update the controller *@param key UIkey
     * @param UpdateData Updates data */
    updateDpc<keyType extendskeyof CtrlKeyMapType>(key: keyType, updateData? : UpdateDataTypeMapType[ToAnyIndexKey<keyType, UpdateDataTypeMapType>]):void;
    /** * Hide the singleton controller *@param key 
     */
    hideDpc<keyType extends keyof CtrlKeyMapType>(key: keyType): void;
    /** * Destroys the singleton controller *@param key 
     * @param DestroyRes Destroys the resource */
    destroyDpc<keyType extendskeyof CtrlKeyMapType>(key: keyType, destroyRes? :boolean) :void;

    /** * instantiate display controller *@param TypeKey Key */
    insDpc<T, keyType extends keyof CtrlKeyMapType = any>(typeKey: keyType): ReturnCtrlType<T>;
    /** * Load the display controller *@param ins 
     * @param loadCfg 
     */loadDpcByIns(ins: displayCtrl.ICtrl, loadCfg? : ILoadConfig):void;
    /** * Initializes the display controller *@param ins 
     * @param initData 
     */
    initDpcByIns<keyType extendskeyof CtrlKeyMapType>( ins: displayCtrl.ICtrl, initCfg? : displayCtrl.IInitConfig<keyType, InitDataTypeMapType>):void
    /** * display displays the controller *@param ins 
     * @param showCfg* /
    showDpcByIns<keyType extendskeyof CtrlKeyMapType>( ins: displayCtrl.ICtrl, showCfg? : displayCtrl.IShowConfig<keyType, InitDataTypeMapType, ShowDataTypeMapType> ):void;
    /** * hide * by instance@param ins 
     */
    hideDpcByIns<T extends displayCtrl.ICtrl>(ins: T): void;
    /** * destroy * by instance@param ins 
     * @param DestroyRes Whether to destroy the resource */
    destroyDpcByIns<T extendsdisplayCtrl.ICtrl>(ins: T, destroyRes? :boolean, endCb? : VoidFunction):void;

    /** * Gets whether the singleton controller is *@param key 
     */
    isLoading<keyType extends keyof CtrlKeyMapType>(key: keyType): boolean
    /** * Gets whether the singleton controller is loaded@param key 
     */
    isLoaded<keyType extends keyof CtrlKeyMapType>(key: keyType): boolean;
    /** * Gets whether the singleton controller is initialized *@param key 
     */
    isInited<keyType extends keyof CtrlKeyMapType>(key: keyType): boolean;
    /** * Gets whether the singleton controller displays *@param key 
     */
    isShowed<keyType extends keyof CtrlKeyMapType>(key: keyType): boolean;
    /** * Get the controller class *@param typeKey 
     */
    getCtrlClass<keyType extends keyof CtrlKeyMapType>(typeKey: keyType): CtrlClassType<ICtrl>;
}
Copy the code

There’s some TypeScript type programming magic here. Extremely powerful type hints can be implemented. If you don’t understand it, you can ignore it and talk about interface design

When designing the UI management interface, always say: responsibility is to manage the UI, provide the interface to call the UI, and do nothing superfluous. Why is that? Because there are always a lot of functions in my mind that I want to put in, but in fact they are just unnecessary functions THAT I want

So the design is very restrained, the resource handling interface is outsourced, because the responsibility is to manage the UI and provide the interface to call the UI

Stack UI management is not done, because not all projects need it, just special needs.

All projects need to manage the UI and provide an interface to call the UI.

Now that we’re talking about managing the UI, what are the UIs in game development?

Dialog, Tips, Window, etc.

I’m abstracting it in terms of how many UI instances exist at once: there are only two types of UIs

  1. Singleton UI

    For example: general loading screen, nursing screen, character display screen, combat screen, etc., there is only one instance at a time

  2. Multi-instance UI for example: reward tips, attribute enhancement Tips

That business logic goes through the UI manager

  1. Call display UI, is like if there is a direct display, do not load to create a display
  2. Call show n UIs, just want to create n UIs to show different things at the same time

This abstract logic is common to every game project I can think of.

The UI management interface should be designed to provide an interface for calling singleton and multi-instance UIs. That’s

You’ve probably seen a lot of special type declarations, so let me briefly talk about what they do

Type hint optimization

If you want a more comfortable interface call experience, you have to squeeze the power that typescript’s type system provides (ps: Why not?).


//displayCtrl.IMgr
isLoading<keyType extends keyof CtrlKeyMapType>(key: keyType): boolean

/ / implement the UI
import { BaseDpCtrl } from "./base-dp-ctrl";
declare global {
    interface ITestCtrlKeyType {
        OnUpdateDpc: "OnUpdateDpc".OnShowDpc: "OnShowDpc".OnInitDpc: "OnInitDpc"
    }
    interface ITestCtrlShowDataMap {
        OnShowDpc: number
    }
    interface ITestCtrlInitDataMap {
        OnInitDpc: number
    }
    interface ITestCtrlUpdateDataMap {
        OnUpdateDpc: number}}export class OnUpdateDpc extends BaseDpCtrl {
    public static readonly typeKey: "OnUpdateDpc" = "OnUpdateDpc";
    public updateData: number;

    constructor() {
        super(a); }onUpdate(updateData: number) {
        this.updateData = updateData; }}export class OnShowDpc extends BaseDpCtrl {
    public static readonly typeKey: string = "OnShowDpc";
    public showData: number;

    constructor() {
        super(a); }onShow(config: displayCtrl.IShowConfig<"OnShowDpc", ITestCtrlShowDataMap>) {
        this.showData = config.onShowData;
        super.onShow(config)
    }

}
export class OnInitDpc extends BaseDpCtrl {
    public static readonly typeKey: "OnInitDpc" = "OnInitDpc";
    public initData: number;

    constructor() {
        super(a); }onInit(config: displayCtrl.IInitConfig<"OnInitDpc", ITestCtrlInitDataMap>) {
        this.initData = config.onInitData; }}// Instantiate UI manager
const dpcMgr = new DpcMgr<ITestCtrlKeyType,ITestCtrlInitDataMap,ITestCtrlShowDataMap,ITestCtrlUpdateDataMap>();// Inject type ITestCtrlKeyType
        dpcMgr.init({
            loadRes: (config) = >{ config.complete(); }}); dpcMgr.regist(OnUpdateDpc); dpcMgr.regist(OnInitDpc); dpcMgr.regist(OnShowDpc);// Call a simple
dpcMgr.isLoading("")// When the double quotation marks are typed, the type prompts to select OnUpdateDpc, OnShowDpc, OnInitDpc

// call a complex
dpcMgr.showDpc("")OnUpdateDpc, OnShowDpc, OnInitDpc,
// Also, when you need to pass onShowData, there will be an onShowData prompt corresponding to UIkey
dpcMgr.showDpc("OnShowDpc", {})Copy the code

Why design such type hints?

  1. I want the UI logic implementer to be more focused, just add a declaration at the top and the business logic call knows what data to pass. You don’t have to dig through a file for an interface
declare global {
    interface ITestCtrlKeyType {
        OnUpdateDpc: "OnUpdateDpc".OnShowDpc: "OnShowDpc".OnInitDpc: "OnInitDpc"
    }
    interface ITestCtrlShowDataMap {
        OnShowDpc: number
    }
    interface ITestCtrlInitDataMap {
        OnInitDpc: number
    }
    interface ITestCtrlUpdateDataMap {
        OnUpdateDpc: number}}Copy the code
  1. I want UI callers: business logic to be more comfortable, dependency free, import free

Reference for typescript type programming

  • What you need to know to write TypeScript tool types

  • TypeScript tool type

  • Drill into TypeScript’s type system

  • TypeScript series (iii) From programming languages to Conditional Types

Other possibilities

Although this article is about the UI management framework

But the direction in my code and interface design is not limited to UI management, you can manage all kinds of abstract display units

UI is just one of them, and this abstract display unit could be

  1. A widget
  2. The protagonist
  3. The enemy
  4. The bullet
  5. , etc.

It’s not just a UI framework, it’s a general display management framework

conclusion

Regarding the design of UI framework, two essential requirements of UI business implementation are proposed

  1. Efficient, flexible and focused UI logic implementation
  2. Efficient, flexible, and highly extensible UI management

Analyze the essential requirements and propose more detailed requirements

According to the requirements of design and implementation of a cross-engine zero dependence, high efficiency, flexibility and high extensible UI framework.

Applicable to all game engine projects, can be expanded according to their own needs

It also provides super comfortable type hints for interface calls by exploring typescript type programming

Concrete implementation logic and CocosCreator2.4.2 demo, you can move framework

GitHub Repository: EasyGameFrameworkOpen

Hope you can give a star, thank you ~

Thank you for reading my article ~

Have a great weekend

Framework development series

  • The Birth of Frameworks – Zero: Why write Frameworks?

  • The birth of the Frame – ONE: I want the frame

  • Break the spell that CocosCreator3d can’t use NPM packs!!

  • The birth of the framework – ii: Positioning

  • Universal game UI framework design and implementation

  • A plugin to make Fairygui better

  • A common pool of objects that meets multiple needs

  • Building a game/app artifact: Broadcast

  • A generic socket network module that meets all custom requirements

  • Status management of business development summaries

  • To be continued…

The last

Welcome to pay attention to my public account, more content to continue to update

Public number search: play game development

Or scan code:

QQ group: 1103157878 (welcome to discuss blowing water)

Blog homepage: ailhc.github. IO /

The Denver nuggets: juejin. Cn/user / 306949…

github: github.com/AILHC