红点系统

设计缘由:

红点系统作为手游中最普遍使用的基础系统,承担着部分指引玩家的职责,其地位十分重要,如果相关代码不成系统,没有统一的管理,就很容易导致大量的代码冗余,不易于维护和改动。

设计思路:
  1. 红点只是一种表现的形式,它是依赖于数据的表现,所以需要将其与界面代码进行分离处理,提炼成单独的一套管理类。
  2. 界面不应该关心检测的具体方法,只需要调用统一的接口,就能得到是否显示红点的结果。
  3. 红点对应于界面,应该是分层级的,子界面上有红点显示,那么其对应的父界面也应该有相应的红点显示,所以需要将其设计为多叉树的结构形式,每个父节点只需要关注其子节点的状态变化,逐级向下进行检测,而每个子节点发生变化后,只需要将状态变化层层传递给其父节点即可。
  4. 每次检测不应该直接就对所有的界面红点模块进行检测,而是仅仅只需要检测数据发生变动的对应模块,这样就能减少性能损耗,提高运行效率。与此同时,各模块之间相互独立,减少了耦合,也便于后续的修改、扩展与优化。
结构框架:
  1. RedPointDefine: 红点系统中所有模块的ID定义类,在该红点系统中,所有的红点模块通过ID进行识别控制调用。新增红点模块都需要在此进行ID的定义。
  2. RedModBase: 所有红点模块的基类,新增红点模块都需要继承该类,在类中定义需要检测该模块的数据变动事件,以及相关的判断红点方法。
  3. RedModMgr: 所有红点模块的总管理类,逐帧对状态变动的红点模块进行检测,而忽略未改变的模块。
  4. RedModRoot: 所有红点模块的根父类,通过检测该父类,就能逐级向下检测到所有子模块。利用了该根节点就实现了普适性,即可将所有模块的设计做同样的处理,而不需要考虑特殊例。
代码示例(typescript + Laya):
RedPointDefine:
export const ROOT = "ROOT";
export const MAINMENU = "MAINMENU";
...
RedModBase:
export enum RedModEvent {
    Notify = "RED_MOD_NOTIFY",
    Change = "RED_MOD_CHANGE",
}

export interface RedModConstructor {
    readonly ID: string;
    readonly CHILD_MOD_LIST: RedModConstructor[];
    readonly OPEN_ID?: number;
    new (): RedModBase;
}

export class RedModBase extends Laya.EventDispather {
    public static readonly ID: string = "";
    public static readonly CHILD_MOD_LIST: RedModConstructor[] = [];
    public static readonly OPEN_ID?: number = null;

    private isActive = null;
    private isDirty = true;
    private parent: string;
    private children: string[];

    public get ID(): string {
        return (this.constructor as RedModConstructor).ID;
    }

    public get CHILD_MOD_LIST(): RedModConstructor[] {
        return (this.constructor as RedModConstructor).CHILD_MOD_LIST;
    }

    public get OPEN_ID(): number {
        return (this.constructor as RedModConstructor).OPEN_ID;
    }

    constructor() {
        super();
        this.isActive = null;
        this.isDirty = false;
        this.parent = null;
        this.children = [];
        if (this.register !== undefined) {
            this.register();
        }
        this.initChildren();
    }

    private initChildren(): void {
        this.children = [];
        for (const childCtor of this.CHILD_MOD_LIST) {
            const child = new childCtor();
            child.parent = this.ID;
            this.children.push(childCtor.ID);
            RedPointMgr.GetInstance().addModule(childCtor.ID, child);
        }
    }

    protected onChange(): void {
        this.notifyChange();
    }

    public notifyChange(): void {
        this.isDirty = true;
        this.event(RedModEvent.Notify, this.ID);
        if (this.parent !== null) {
            const mod: RedModBase = RedPointMgr.GetInstance().getModule(this.parent);
            if (mod !== undefined) {
                mod.notifyChange();
            }
        }
    }

    public checkActive(): boolean {
        if (this.isDirty || this.isActive === null) {
            this.isDirty = false;
            const bLastActive = this.isActive;
            let isOpen = true;
            if (this.OPEN_ID !== null) {
                isOpen = OpenData.isOpen(this.OPEN_ID);
            }
            if (!isOpen) {
                this.isActive = false;
            } else {
                if (this.children.length > 0) {
                    //无检测,依赖子节点
                    this.isActive = false;
                    for (const id of this.children) {
                        const mod = RedModMgr.GetInstance().getModule(id);
                        if (mod.checkActive()) {
                            this.isActive = true;
                            break;
                        }
                    }
                } else {
                    //叶子节点
                    this.isActive = false;
                    if (this.onCheckActive !== undefined) {
                        this.isActive = this.onCheckActive();
                    }
                }
            }
            if (bLastActive !== this.isActive) {
                this.event(RedModEvent.Change, [this.ID, this.isActive]);
            }
        }
        return this.isActive;
    }

    //叶子节点实现该方法
    protected onCheckActive?(): boolean;
    //叶子节点实现该方法
    protected register?(): void;
}  
RedPointMgr:
type Listener = {
    (...args: unknown[]): void;
}

export default class RedPointMgr extends Laya.EventDispatcher {
    private static instance: RedPointMgr = null;
    public static GetInstance(): RedPointMgr {
        if (RedPointMgr.instance === null) {
            const mgr: RedPointMgr = new RedPointMgr();
            RedPointMgr.instance = mgr;
            mgr.addModule(RedModRoot.ID, new RedModRoot()); //在赋值之后add,否则死循环
        }
        return RedPointMgr.instance;
    }

    private modules: RedModBase[];
    private notifyList: string[]; 
    private isDirty: boolean;
    private isEnabled: boolean;

    constructor() {
        super();
        this.modules = [];
        this.notifyList = [];
        this.isDirty = false;
        this.isEnabled = true;
        this.regCommonEvent();
    }

    public resetMgr(): void {
        this.notifyList = [];
        this.isDirty = false;
    }

    public addModule(id: string, mod: RedModBase): void {
        if (this.modules[id] !== undefined) {
            console.warn(`模块重复添加,请检查。ID=${id}`);
            return;
        }
        this.modules[id] = mod;
        mod.on(RedModEvent.Notify, this, this.onModuleNotify);
        mod.on(RedModEvent.Change, this, this.onModuleChange);
    }

    public getModule(id: string): RedModBase {
        return this.modules[id];
    }

    public override on(type: string, caller: unknown, listener: Listener, args?: unknown[]): Laya.EventDispatcher {
        const mod = this.modules[type];
        if (mod !== undefined) {
            super.on(type, caller, listener, args);
        }
        return this;
    }

    public override off(type: string, caller: unknown, listener: Listener, onceOnly?: boolean): Laya.EventDispatcher {
        const mod = this.modules[type];
        if (mod !== undefined) {
            super.off(type, caller, listener, onceOnly);
        }
        return this;
    }

    //只检测有数据变动的模块
    public checkActive(id: string): boolean {
        for (const id of this.notifyList) {
            this.modules[id].checkActive();
        }
        this.notifyList = [];
        this.isDirty = false;
    }

    public setEnabled(enabled: boolean): void {
        this.isEnabled = enabled;
    }
    
    //在游戏开始后逐帧进行调用检测红点的方法
    public lateUpdate(): void {
        if (this.isEnabled && this.isDirty) {
            this.isDirty = false;
            this.checkChange();
        }
    }

    private regCommonEvent(): void {
        //这里放常用的数据变更,例如等级和vip等级的变化,对应大量功能变动的事件,就进行全局检测。
        PropMgr.GetInstance().on(PropEvent.UPDATE_SINGLE_PROP, this, this.onPropChange);
    }

    private onPropChange(prop: string): void {
        if (prop === "iGrade" || prop === "iVIP") {
            for (const mod of this.modules) {
                if (mod.OPEN_ID !== undefined) {
                    mod.notifyChange();
                }
            }
        }
    }

    private onModuleNotify(id: string): void {
        if (this.notifyList.indexOf(id) === -1) {
            this.notifyList.push(id);
        }
        this.isDirty = true;
    }

    private onModuleChange(id: string, active: boolean): void {
        this.regCommonEvent(id, [id, active]);
    }
}
RedModRoot:
export default class RedModRoot extends RedModBase {
  public static override ID: string = RedPointDefine.ROOT;
  public static override CHILD_MOD_LIST: RedModConstructor[] = [
    RedModMain,
    RedModShop,
    ......
  ];

  override register(): void {
    //在此注册会引起该红点变化的数据变动事件,其调用方法(this.onChange)
  }

  override onCheckActive(): boolean {
    //在此填充检测对应红点的方法,返回true则通知注册该红点模块的界面显示红点,反之不显示。
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 前言 小红点提示,可以说是现在app都会有的一个功能,比如微信的消息界面,微博的我的界面。一般来说,一个小红点不会...
    程序员WW阅读 483评论 0 2
  • 写了一个demo可以自动显示红点,在其他界面红点数变化,对应的父界面的红点也会发生变化。 如下图: 设计思路: 每...
    colinWong阅读 1,578评论 2 8
  • 红点大家一定不陌生,不管是游戏还是软件,甚至是手机系统都通过红点的直观方式来告知用户或者玩家,你有新的消息,请注意...
    小鲸鱼的U3d之旅阅读 1,386评论 0 0
  • 最近在写一个社交APP的时候,在控制消息计数,及界面红点显示时总会或多或少有延迟或计数偏差,网上大多是对界面绘制的...
    此屁天下之绝响阅读 2,725评论 6 122
  • 小红点(消息推送提醒)在现今的各个App中几乎无处不在,特别是内容的更新日渐频繁,大量的小红点被投放在各个业务入口...
    金小俊阅读 9,330评论 15 131