Cocos游戏MVC框架设计


框架设计原则

  1. 游戏场景中只放置启动脚本,不放内容,否则合作和维护麻烦,运行的时候只有一个场景容器
  2. 手动关联资源会导致大型项目无法维护
  3. 节点和预制体不手动挂载任何代码,可以把美术视图和程序同时开发,降低了维护难度,所有东西都可以从代码搜索得到。
  4. 程序员维护业务逻辑,数据来自策划,视图来自美术。视图由美术做好之后放在固定的地方,程序员必要的时候调用即可。

目录:
AssetsPackage:用来存放2D和3D资源
Scenes:用来存放场景,只有一个Main场景
Script:分为框架代码(可以重用),业务逻辑代码
Script/FrameWork:框架代码
Script/FrameWork/Utils:通用函数
Script/FrameWork/Manager:管理器
Script/3rd:第三方代码
Script/Game:业务逻辑

启动代码

场景中手动挂载的唯一代码,只放置框架的初始化代码

import { _decorator, Component, Node, TextAsset, Prefab } from 'cc';
import { EventMgr } from './Framework/Managers/EventMgr';
import { NetMgr } from './Framework/Managers/Net/NetMgr';
import { ProtoMgr } from './Framework/Managers/Net/ProtoMgr';
import { ResMgr } from './Framework/Managers/ResMgr';
import { SoundMgr } from './Framework/Managers/SoundMgr';
import { TimerMgr } from './Framework/Managers/TimerMgr';
import { UIMgr } from './Framework/Managers/UIMgr';
import { GameApp } from './Game/GameApp';
const { ccclass, property } = _decorator;

@ccclass('GameLanch')
export class GameLanch extends Component {
    public static Instance: GameLanch = null as unknown as GameLanch; 

    @property
    public isNetMode: boolean = false;

    @property
    private wsUrl: string = "ws://127.0.0.1:6081/ws"; 

    @property(TextAsset)
    private pbTexAsset: TextAsset | null = null;

    @property(Prefab)
    public UILoading: Prefab = null as unknown as Prefab; 


    onLoad(): void {
        if(GameLanch.Instance === null) {
            GameLanch.Instance = this;
        }
        else {
            this.destroy();
            return;
        }

        console.log("Game Lanching......");
        // 初始化框架逻辑: 资源管理,声音管理,网络管理
        this.node.addComponent(ResMgr);
        this.node.addComponent(SoundMgr);
        this.node.addComponent(TimerMgr);
        this.node.addComponent(EventMgr);
        this.node.addComponent(UIMgr);

        // 是否使用网络模块
        if(this.isNetMode) {
            this.node.addComponent(ProtoMgr).Init(this.pbTexAsset);
            this.node.addComponent(NetMgr).Init(this.wsUrl);
        }
        
        // end

        this.node.addComponent(GameApp);
        // end 

        // 检查更新我们的资源
        // end

        // 进入游戏里面去,逻辑模块入口
        GameApp.Instance.EnterGame();
        // end
    }
}


逻辑模块入口

游戏正式的逻辑从EnterGame开始,加载UI,进入场景


import { _decorator, Component, Node, AudioClip, AudioSource, Prefab } from 'cc';
import { EventMgr } from '../Framework/Managers/EventMgr';
import { ProtoMgr } from '../Framework/Managers/Net/ProtoMgr';
import { ResMgr } from '../Framework/Managers/ResMgr';
import { SoundMgr } from '../Framework/Managers/SoundMgr';
import { TimerMgr } from '../Framework/Managers/TimerMgr';
import { UIMgr } from '../Framework/Managers/UIMgr';
import { GameLanch } from '../GameLanch';
import { AuthProxy } from './ServerProxy/AuthProxy';
import { NetEventDispatcher } from './ServerProxy/NetEventDispatcher';

var resPkg = {
    "Sounds": [
        { assetType: AudioClip, urls: 
            ["CK_attack1", 
            "Qinbing_die"
        ]},
    ],

    "GUI": [
        {
            assetType: Prefab, 
            urls: [
                "UIPrefabs/LoginUI",
            ],
        },
    ],
    // "Sounds": AudioClip,
}

export class GameApp extends Component {
    
    public static Instance: GameApp = null as unknown as GameApp

    onLoad(): void {
        if(GameApp.Instance === null) {
            GameApp.Instance = this;
        }
        else {
            this.destroy();
            return;
        }

        // 实例化游戏网络事件模块
        if(GameLanch.Instance.isNetMode) {
            this.node.addComponent(NetEventDispatcher).Init();
            AuthProxy.Insance.Init();
        }
        // end 
    }

    // 游戏逻辑入口
    public EnterGame(): void {
        UIMgr.Instance.ShowUIPrefab(GameLanch.Instance.UILoading);

        ResMgr.Instance.preloadResPkg(resPkg, (now: any, total: any)=>{
            EventMgr.Instance.Emit("loadProgress", Math.floor(now * 100 / total));
        }, ()=>{
            this.EnterLoadingScene();
        });
    }

    public EnterLoadingScene(): void {
        console.log("EnterLoadingScene");
        
        /*
        // 释放测试
        ResMgr.Instance.releaseResPkg(resPkg);

        this.scheduleOnce(()=>{
            console.log(ResMgr.Instance.getAsset("Sounds", "CK_attack1"));
        }, 3)*/
        
        // 释放游戏地图
        // end

        // 释放游戏角色
        // end

        // 释放我们的游戏UI
        // end

        // 播放声音
        /*var as = this.node.addComponent(AudioSource);
        as.clip = ResMgr.Instance.getAsset("Sounds", "CK_attack1");
        as.loop = true;
        as.play();*/

        // var clip = ResMgr.Instance.getAsset("Sounds", "CK_attack1");
        // SoundMgr.Instance.playSound(clip);
        // SoundMgr.Instance.setMusicMute(false);
        // SoundMgr.Instance.playBgMusic(clip, true);
        // end

        // 释放我们的UI视图了
        UIMgr.Instance.ClearAll();
        UIMgr.Instance.ShowUIView("LoginUI");
        // end


        // 监听事件

        // end
    } 
}

资源管理模块

  1. 常用资源,进入游戏之前加载好
  2. 较大的资源,不常用,游戏中加载,加进度条
  3. 异步加载
  4. 加载场景的过程中,加载资源
  5. 提前配置好场景用的资源包,填好资源加载表(手写或代码生成)
  6. 准备好单个资源加载,释放的接口
  7. 提供获取资源的接口
    资源加载表:
    var resPkg = {
        "Sound":[
            {typeAsset:Prefab, urls: [路径1,路径2,路径3]},
            {typeAsset:Prefab, urls: [路径1,路径2,路径3]},
            ...
        ],
        "Common": Texture2D;
        ...
    };
/*
【注意】:
ab包对象没有释放,如果有必要可以考虑释放;
释放ab包对象,不会释放资源;

预加载资源使用接口:  preleadResPkg/releaseResPkg, 常驻内存资源,可以不写入释放资源包;
游戏中较大的资源,可以采用preleadAsset/releaseAsset 单个使用释放
游戏中获取资源: getAsset(); 
*/

import { _decorator, Component, Node, assetManager, AssetManager, Asset } from 'cc';

export class ResMgr extends Component {
    public static Instance: ResMgr = null as unknown as ResMgr;

    private totalAb: number = 0;
    private nowAb: number = 0;

    private now: number = 0;
    private total: number = 0;

    onLoad(): void {
        if(ResMgr.Instance === null) {
            ResMgr.Instance = this;
        }
        else { 
            this.destroy();
            return;
        }
    }

    private loadAndRef(abBundle: AssetManager.Bundle, url: string, 
                       typeAsset: any,
                       progress: Function, endFunc: Function): void {

        abBundle.load(url, typeAsset, (err: any, asset: Asset)=>{
            if (err) {
                console.log("load assets: ", err);
                return;
            }

            
            console.log("load asset success:", url);
            asset.addRef(); // 增加一个引用技术;

            this.now ++;
            if(progress) {
                progress(this.now, this.total);
            }
            if(this.now >= this.total) {
                if(endFunc) {
                    endFunc();
                }
            }
        });
    }

    private loadAssetsInUrls(abBundle: AssetManager.Bundle, typeAsset: any, urls: Array<string>, progress: Function, endFunc: Function): void {
        for(let i = 0; i < urls.length; i ++) {
            this.loadAndRef(abBundle, urls[i], typeAsset, progress, endFunc);
        }
    }

    private releaseAssetsInUrls(abBundle: AssetManager.Bundle, typeAsset: any, urls: Array<string>): void {
        for(let i = 0; i < urls.length; i ++) {
            // console.log(urls[i]);
            let asset: Asset = abBundle.get(urls[i]) as Asset;
            if(!asset) {
                continue;
            }

            // console.log(asset.refCount);
            asset.decRef(true);
        }
    }

    private preloadAssetsInAssetsBundles(resPkg: any, progress: Function, endFunc: Function): void {
        for(var key in resPkg) {
            var abBundle: AssetManager.Bundle = assetManager.getBundle(key) as AssetManager.Bundle;
            if(!abBundle) {
                continue;
            }

            if(resPkg[key] instanceof Array) {
                for(let i = 0; i < resPkg[key].length; i ++) {
                    // let info: any = abBundle.getDirWithPath("/");
                    // console.log(info);
                    this.loadAssetsInUrls(abBundle, resPkg[key][i].typeAsset, resPkg[key][i].urls, progress, endFunc);
                }
            }
            else {
                let typeAsset = resPkg[key];
                let infos = abBundle.getDirWithPath("/");
                let urls: any = [];
                for(let i = 0; i < infos.length; i ++) {
                    urls.push(infos[i].path);
                }

                this.loadAssetsInUrls(abBundle, typeAsset, urls, progress, endFunc);
            }
        }
    }

    /*
    var resPkg = {
        "Ab包名字": [
            { typeAsset: 资源类型, urls: []},
            { typeAsset: 资源类型, urls: []},
            ...
        ],

        "Ab包名字": 资源类型, 表示整包ab包按照一个类型加载;
        ... ..
    };

    progress(now, total)

    */
    public preloadResPkg(resPkg: any, progress: Function, endFunc: Function): void {
        this.totalAb = 0;
        this.nowAb = 0;

        this.total = 0;
        this.now = 0;

        for(var key in resPkg) {
            this.totalAb ++;
            
            if(resPkg[key] instanceof Array) {
                for(let i = 0; i < resPkg[key].length; i ++) {
                    this.total += resPkg[key][i].urls.length;
                }
            }
        }

        // 加载ab包
        for(var key in resPkg) {
            assetManager.loadBundle(key, (err, bundle: AssetManager.Bundle)=>{
                if(err) {
                    console.log("load bundle erro: ", err);
                    return;
                }

                this.nowAb ++;
                if(!(resPkg[key] instanceof Array)) {
                    let infos = bundle.getDirWithPath("/");
                    this.total += (infos.length);
                }

                if(this.nowAb >= this.totalAb) { // ab包加载完毕
                    this.preloadAssetsInAssetsBundles(resPkg, progress, endFunc);
                }
 
                

            });
        }
        // end
    }

    public releaseResPkg(resPkg: any): void {
        for(var key in resPkg) {
            let abBundle: AssetManager.Bundle = assetManager.getBundle(key) as AssetManager.Bundle;
            if(!abBundle) {
                continue;
            }

            if(resPkg[key] instanceof Array) {
                for(let i = 0; i < resPkg[key].length; i ++) {
                    this.releaseAssetsInUrls(abBundle, resPkg[key][i].typeAsset, resPkg[key][i].urls);
                }
            }
            else {
                let typeAsset = resPkg[key];
                let infos = abBundle.getDirWithPath("/");
                let urls: any = [];
                for(let i = 0; i < infos.length; i ++) {
                    urls.push(infos[i].path);
                }
                this.releaseAssetsInUrls(abBundle, typeAsset, urls);
            }
        }
    }
    //预加载单个资源
    public preloadAsset(abName: string, url: string, typeClass: any, endFunc: Function): void {
        assetManager.loadBundle(abName, (err, abBundle: AssetManager.Bundle)=>{
            if(err) {
                console.log(err);
                return;
            }

            abBundle.load(url, typeClass, (err, asset: Asset)=>{
                if(err) {
                    console.log(err);
                    return;
                }

                if(endFunc) {
                    endFunc();
                }
            });
        });
    }

    public releaseAsset(abName: string, url: string): void {
        var abBundle: AssetManager.Bundle = assetManager.getBundle(abName) as AssetManager.Bundle;
        if(!abBundle) {
            return;
        }

        abBundle.release(url);
    }

    // 同步接口, 前面已经加载好了的资源;使用资源
    public getAsset(abName: string, url: string): any {
        var abBundle: AssetManager.Bundle = assetManager.getBundle(abName) as AssetManager.Bundle;
        if(!abBundle) {
            return null;
        }

        return abBundle.get(url);
    }
}


声音管理模块

  1. 添加8个AudioSource来播放声音,一个AudioSource播放背景音乐
  2. 每次播放声音,使用一个没有播放声音的AudioSource来播放
  3. 所有AudioSource被使用时,顶掉最前面一个AudioSource的声音
    
    import { _decorator, Component, Node, AudioSource, AudioClip } from 'cc';
    export class SoundMgr extends Component {
        public static Instance: SoundMgr = null as unknown as SoundMgr;
        private static MAX_SOUNDS: number = 8; // 最大音效的数目
    
        private nowIndex: number = 0;
        private sounds: Array<AudioSource> = [];
        private bgMusic: AudioSource = null as unknown as AudioSource;
    
        private isMusicMute: boolean = false;
        private isSoundMute: boolean = false;
    
        onLoad(): void {
            if(SoundMgr.Instance === null) {
                SoundMgr.Instance = this;
            }
            else {
                this.destroy();
                return;
            }
            
            for(let i = 0; i < SoundMgr.MAX_SOUNDS; i ++) {
                var as = this.node.addComponent(AudioSource);
                this.sounds.push(as);
            }
    
            this.bgMusic = this.node.addComponent(AudioSource) as AudioSource;
    
            // 从本地存储里面把设置读出来, 0, 1
            var value = localStorage.getItem("GAME_MUSIC_MUTE");
            if(value) {
                let v = parseInt(value);
                this.isMusicMute = (v === 1)? true : false;
            }
    
            value = localStorage.getItem("GAME_SOUND_MUTE");
            if(value) {
                let v = parseInt(value);
                this.isSoundMute = (v === 1)? true : false;
            }
        }
    
        public playBgMusic(clip: AudioClip, isLoop: boolean): void {
            this.bgMusic.clip = clip;
            this.bgMusic.loop = isLoop;
            this.bgMusic.volume = (this.isMusicMute)? 0 : 1.0;
            this.bgMusic.play();
        }
    
        public stopBgMusic(): void {
            this.bgMusic.stop();
        }
    
        public playSound(clip: AudioClip): void {
            if(this.isSoundMute === true) {
                return;
            }
    
            var as = this.sounds[this.nowIndex];
            this.nowIndex ++;
            if(this.nowIndex >= SoundMgr.MAX_SOUNDS) {
                this.nowIndex = 0;
            }
    
            as.clip = clip;
            as.loop = false;
            as.play();
        }
    
        public playSoundOneShot(clip: AudioClip): void {
            var as = this.sounds[this.nowIndex];
            this.nowIndex ++;
            if(this.nowIndex >= SoundMgr.MAX_SOUNDS) {
                this.nowIndex = 0;
            }
    
            as.clip = clip;
            as.loop = false;
            as.playOneShot(clip);
        }
    
        public setMusicMute(isMute: boolean): void {
            this.isMusicMute = isMute;
            this.bgMusic.volume = (this.isMusicMute)? 0 : 1.0;
    
            // localStorage
            let value = (isMute)? 1 : 0;
            localStorage.setItem("GAME_MUSIC_MUTE", value.toString());
            // end
        }
    
        public setSoundsMute(isMute: boolean): void {
            this.isSoundMute = isMute;
    
            // localStorage
            let value = (isMute)? 1 : 0;
            localStorage.setItem("GAME_SOUND_MUTE", value.toString());
        }
    }
    
    

定时器管理


import { _decorator, Component, Node } from 'cc';

class TimerNode {
    public callback: Function = null as unknown as Function;
    public duration: number = 0; // 定时器触发的时间间隔;
    public delay: number = 0; // 第一次触发要隔多少时间;
    public repeat: number = 0; // 你要触发的次数;
    public passedTime: number = 0; // 这个Timer过去的时间;
    public param: any = null; // // 用户要传的参数
    public isRemoved: boolean = false; // 是否已经删除了
    public timerId: number = 0; // 标识这个timer的唯一Id号;
}

export class TimerMgr extends Component {
    public static Instance: TimerMgr = null as unknown as TimerMgr;
    
    private autoIncId: number = 1; // 自增长的id, 表示唯一的timerId;
    private timers: any = {}; // 这个timerId--->Timer对象隐映射
    private removeTimers:  Array<TimerNode> = [];
    private newAddTimers:  Array<TimerNode> = [];

    onLoad(): void {
        if(TimerMgr.Instance === null) {
            TimerMgr.Instance = this;
        }
        else {
            this.destroy();
            return;
        }
    }

    update(dt: number): void {
        // 把新加进来的放入到我们的表里面来
        for (let i = 0; i < this.newAddTimers.length; i++) {
            this.timers[this.newAddTimers[i].timerId] = this.newAddTimers[i];
        }
        this.newAddTimers.length = 0;
        // end

        for (let key in this.timers) {
            var timer = this.timers[key];
            if (timer.isRemoved) {
                this.removeTimers.push(timer);
                continue;
            }

            timer.passedTime += dt; // 更新一下timer时间
            if (timer.passedTime >= (timer.delay + timer.duration)) {
                // 做一次触发
                timer.callback(timer.param);
                timer.repeat--;
                timer.passedTime -= (timer.delay + timer.duration);
                timer.delay = 0; // 很重要;

                if (timer.repeat == 0)
                { // 触发次数结束,我们是不是要删除这个Timer; 
                    timer.isRemoved = true;
                    this.removeTimers.push(timer);
                }
                // end 
            }
        }

        // 结束以后,清理掉要删除的Timer;
        for (let i = 0; i < this.removeTimers.length; i++) {
            // this.timers.delete(this.removeTimers[i]);
            delete this.timers[this.removeTimers[i].timerId];
        }
        this.removeTimers.length = 0;
        // end
    }

    // [repeat < 0 or repeat == 0 表示的是无限触发]
    public ScheduleWithParams(func: Function, param: any, repeat: number, duration: number, delay: number = 0): number
    {
        let timer: TimerNode = new TimerNode();
        timer.callback = func;
        timer.param = param;
        timer.repeat = repeat;
        timer.duration = duration;
        timer.delay = delay;
        timer.passedTime = timer.duration;
        timer.isRemoved = false;

        timer.timerId = this.autoIncId;
        this.autoIncId ++;

        // this.timers.Add(timer.timerId, timer);
        this.newAddTimers.push(timer);

        return timer.timerId;
    }

    public Once(func: Function, delay: number): number
    {
        return this.Schedule(func, 1, 0, delay);
    }

    public ScheduleOnce(func: Function, param: any, delay: number): number {
        return this.ScheduleWithParams(func, param, 1, 0, delay);
    }

    // [repeat < 0 or repeat == 0 表示的是无限触发]
    public Schedule(func: Function, repeat: number, duration: number, delay: number = 0): number
    {
        return this.ScheduleWithParams(func, null, repeat, duration, delay);
    }

    public Unschedule(timerId: number) {
        if (!this.timers[timerId]) {
            return;
        }

        let timer: TimerNode = this.timers[timerId];
        timer.isRemoved = true;
    }
}

UI管理

原则:

  1. 每个UI视图,只有一个控制代码
  2. 控制代码里面的有一个数据成员表,能方便访问每个需要控制的UI节点
  3. 控制代码要提供常用的用户输入型组件的监听(button,滑动条)
  4. 控制代码规范命名 UI视图名字_Ctrl
    UI类的基类:
    
    import { _decorator, Component, Node, Button } from 'cc';
    
    export class UICtrl extends Component {
    
        protected view: any = {}; // 路径--->节点; this.view["路径"] --->获得节点;
        // 把UI中所有的节点都加载到一个表中,方便通过路径访问
        private loadAllNodeInView(root: any, path: string) {
            for(let i = 0; i < root.children.length; i ++) {
                this.view[path + root.children[i].name] = root.children[i];
                this.loadAllNodeInView(root.children[i], path + root.children[i].name + "/");
            }
        }
    
        onLoad(): void {
            this.loadAllNodeInView(this.node, "");
        }
    
        // 为按钮添加事件
        public AddButtonListener(viewName: string, caller: any, func: any) {
            var view_node = this.view[viewName];
            if (!view_node) {
                return;
            }
            
            var button = view_node.getComponent(Button);
            if (!button) {
                return;
            }
    
            view_node.on("click", func, caller);
        }
    
        // 其他UI事件, ....
    }
    
    
    
    UI管理器:
    
    import { _decorator, Component, Node, find, instantiate, Prefab } from 'cc';
    import { ResMgr } from './ResMgr';
    
    export class UIMgr extends Component {
        public static Instance: UIMgr = null as unknown as UIMgr;
    
        private canvas: Node = null as unknown as Node;
        private uiMap: any = {};
    
        onLoad(): void {
            if(UIMgr.Instance === null) {
                UIMgr.Instance = this;
            }
            else {
                this.destroy();
                return;
            }
    
            // 挂我们的UI视图的一个根节点;
            this.canvas = find("Canvas") as Node;
    
            // 特殊的挂载点, ....
            // end 
        }
        // 传入prefab显示UI
        public ShowUIPrefab(uiPrefab: Prefab, parent?: Node): void {
            var uiView: Node = instantiate(uiPrefab) as Node;
            parent = (!parent)? this.canvas : parent;
            parent.addChild(uiView);
    
            //往根节点上挂下UI视图脚本;
            console.log(uiPrefab, uiPrefab.data.name);
            uiView.addComponent(uiPrefab.data.name + "_Ctrl");
            this.uiMap[uiPrefab.data.name] = uiView;
        }
    
        // 传入string显示UI
        public ShowUIView(viewName: string, parent?: Node): void {
            // 实例化UI视图出来; 
            var uiPrefab = ResMgr.Instance.getAsset("GUI", "UIPrefabs/" + viewName);
            if(!uiPrefab) {
                console.log("cannot find ui Prefab: ", viewName);
                return;
            }
    
            var uiView: Node = instantiate(uiPrefab) as Node;
            parent = (!parent)? this.canvas : parent;
            parent.addChild(uiView);
            this.uiMap[viewName] = uiView;
            // console.log(uiView);
    
            //往根节点上挂下UI视图脚本;
            uiView.addComponent(uiPrefab.data.name + "_Ctrl");
    
            
        }
    
        public RemoveUI(ui_name: string) {
            if (this.uiMap[ui_name]) {
                this.uiMap[ui_name].destroy();
                this.uiMap[ui_name] = null;
            }
        }
    
        public ClearAll() {
            for (var key in this.uiMap) {
                if (this.uiMap[key]) {
                    this.uiMap[key].destroy();
                    this.uiMap[key] = null;
                }
            }
        }
    }
    
    

使用案例

import { _decorator, Component, Node, Label } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
const { ccclass, property } = _decorator;

import { UICtrl } from "./../../Framework/Managers/UICtrl";

@ccclass('UILoading_Ctrl')
export class UILoading_Ctrl extends UICtrl {
    private progressValue: Label = null as unknown as Label;
    
    onLoad(): void {
        super.onLoad();
        this.progressValue = this.view["Progress"].getComponent(Label);
        this.progressValue.string = "0%";
        EventMgr.Instance.AddEventListener("loadProgress", this, this.onProgressUpdate);
    }

    private onProgressUpdate(name: string, per: number): void {
        this.progressValue.string = per + "%";
    }

    onDestroy(): void {
        EventMgr.Instance.RemoveListenner("loadProgress", this, this.onProgressUpdate)
    }
}

事件的订阅和发布

import { _decorator, Component, Node } from 'cc';

export class EventMgr extends Component {

    public static Instance: EventMgr = null as unknown as EventMgr;

    // xxxx事件名字 ----》 【监听者1(caller, func),监听者2...】
    private events_map: any = {};

    onLoad(): void {
        if(EventMgr.Instance === null) {
            EventMgr.Instance = this;
        }
        else {
            this.destroy();
            return;
        }


    }

    // func(event_name: string, udata: any)
    public AddEventListener(eventName: string, caller: any, func: Function) {
        if (!this.events_map[eventName]) {
            this.events_map[eventName] = [];
        }

        var event_queue = this.events_map[eventName];
        event_queue.push({
            caller: caller,
            func: func
        });
    }

    public RemoveListenner(eventName: string, caller: any, func: Function) {
        if (!this.events_map[eventName]) {
            return;
        }

        var event_queue = this.events_map[eventName];
        for(var i = 0; i < event_queue.length; i ++) {
            var obj = event_queue[i];
            if (obj.caller == caller && obj.func == func) {
                event_queue.splice(i, 1);
                break;
            }
        }

        if (event_queue.length <= 0) {
            this.events_map[eventName] = null;
        }
    }
    
    public Emit(eventName: string, udata: any) {
        if (!this.events_map[eventName]) {
            return;
        }

        var event_queue = this.events_map[eventName];
        for(var i = 0; i < event_queue.length; i ++) {
            var obj = event_queue[i];
            obj.func.call(obj.caller, eventName, udata);
        }
    }
}

WebSocket网络模块

客户端网络模块需要做好三件事:

  1. 连接管理
  2. 接受数据
  3. 发送数据
    
    import { _decorator, Component, Node } from 'cc';
    import { EventMgr } from '../EventMgr';
    
    enum State {
        Disconnected = 0, // 断开连接
        Connecting = 1, // 正在连接
        Connected = 2, // 已经连接;
    };
    
    export class NetMgr extends Component {
        public static Instance: NetMgr = null as unknown as NetMgr;
        private url: string = "ws://127.0.0.1:6081/ws";
    
        private state: number = State.Disconnected;
        private sock: WebSocket|null = null;
    
        onLoad(): void {
            if(NetMgr.Instance === null) {
                NetMgr.Instance = this;
            }
            else {
                this.destroy();
                return;
            }
    
            this.state = State.Disconnected;
        }
    
        public Init(url: string): void {
            
            this.url = url;
            this.state = State.Disconnected;
        }
    
        // 调用这里发送打包好的数据
        public send_data(data_arraybuf: ArrayBuffer) {
            if (this.state === State.Connected && this.sock) {
                this.sock.send(data_arraybuf);
            }
        }
    
        private connect_to_server(): void {
            if (this.state !== State.Disconnected) {
                return;
            }
    
            // 抛出一个正在重新连接的事件;
            EventMgr.Instance.Emit("net_connecting", null);
    
            this.state = State.Connecting;
            this.sock = new WebSocket(this.url); // H5标准,底层做好了;
            this.sock.binaryType = "arraybuffer"; // blob, 二进制;
    
            this.sock.onopen = this._on_opened.bind(this);
            this.sock.onmessage = this._on_recv_data.bind(this);
            this.sock.onclose = this._on_socket_close.bind(this);
            this.sock.onerror = this._on_socket_err.bind(this);
        }
    
        // 这里接收的数据会被事件发布出去,protoMgr会订阅这个事件
        private _on_recv_data(event: any) {
            EventMgr.Instance.Emit("net_message", event.data);
        }
    
        private _on_socket_close(event: any) {
            this.close_socket();
        }
    
        private _on_socket_err(event: any) {
            this.close_socket();
        }
    
        public close_socket() {
            if (this.state === State.Connected) {
                if (this.sock !== null) {
                    this.sock.close();
                    this.sock = null;
                }
            }
            EventMgr.Instance.Emit("net_disconnect", null);
            this.state = State.Disconnected;
        }
    
        // 连接成功了
        private _on_opened(event: any) {
            this.state = State.Connected;
            console.log("connect to server: " + this.url + " sucess!");
            EventMgr.Instance.Emit("net_connect", null);
        }
    
        update (dt: number) {
            if (this.state !== State.Disconnected) {
                return;
            }
    
            this.connect_to_server();
        }
    }
    

ProMgr协议管理

配置动态解析描述文件:

  1. 项目中内置一个protobuf解释型runtime库,protobuf.js,这个文件有8723行,这里不展示,详见https://github.com/dcodeio/protobuf.js
  2. 资源文件中添加协议描述文件
  3. 实现ProMgr

import { _decorator, Component, Node, TextAsset } from 'cc';

declare const protobuf: any;

export class ProtoMgr extends Component {
    public static Instance: ProtoMgr = null as unknown as ProtoMgr;
    
    // 协议描述文件的文本对象
    private pbTexAsset: TextAsset|null = null;

    // 根据协议描述文本对象,我们生成一个动态解析的对象;
    private pb: any = null;

    public Init(pbTex: TextAsset|null): void {
        this.pbTexAsset = pbTex;
        this.pb = protobuf.parse(this.pbTexAsset);
    }

    onLoad(): void {
        if(ProtoMgr.Instance === null) {
            ProtoMgr.Instance = this;
        }
        else {
            this.destroy();
            return;
        }
    }
    // 调用该函数对对应名字的协议进行序列化,返回二进制,可以直接通过websocket发送
    public SerializeMsg(msgName: string, msgBody: any): Uint8Array {
        let rs = this.pb.root.lookupType(msgName);
        let msg = rs.create(msgBody);
        let buf = rs.encode(msg).finish();

        return buf;
    }
    // 二进制解码,生成对应的表
    public DeserializeMsg(msgName: string, msgBuf: Uint8Array): Object {
        let rs = this.pb.root.lookupType(msgName);
        let msg = rs.decode(msgBuf)

        return msg;
    }
}

和服务器对接

双方规定一个发送数据的格式:

服务号2字节 | 命令号 2字节 | 用户标识 4字节(客户端不用写数据) | 数据本体

import { _decorator, Component, Node, Game } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
import { NetMgr } from '../../Framework/Managers/Net/NetMgr';
import { ProtoMgr } from '../../Framework/Managers/Net/ProtoMgr';
import { Cmd } from './Cmd';
import { Stype } from './Stype';


export class NetEventDispatcher extends Component {
    public static Instance: NetEventDispatcher = null as unknown as NetEventDispatcher;

    public onLoad(): void {
        if(NetEventDispatcher.Instance === null) {
            NetEventDispatcher.Instance = this;
        }
        else {
            this.destroy();
            return;
        }

        
    }
    // 这里订阅了Websocket模块的接收数据
    public Init(): void {
        EventMgr.Instance.AddEventListener("net_message", this, this.onRecvMsg);
    }

    // 事件名字---》事件订阅传过来的 net_message
    // udata: 网络收到数据;
    private onRecvMsg(uname: string, udata: ArrayBuffer): void {
        // 获取服务号,命令号;
        var dataView = new DataView(udata);
        var stype = dataView.getInt16(0, true);
        var ctype = dataView.getInt16(2, true);

        // 获取我们的序列化后的二进制数据;
        var uint8Buf: Uint8Array = new Uint8Array(udata);
        var msgBuf = uint8Buf.subarray(4 + 4);

        // 反序列化二进制数据body 为一个对象
        var msgBody = ProtoMgr.Instance.DeserializeMsg(Cmd[ctype], msgBuf);

        // 根据服务号触发对应的事件,对应的服务模块根据命令号处理即可
        EventMgr.Instance.Emit(Stype[stype], {ctype: ctype, body: msgBody});
    }

    // 发送数据
    public sendMsg(stype: number, ctype: number, msg: any) {
        // step1: 序列化一个msg--->buf;
        // enum Cmd --->  {1: "eGuestLoginReq", "eGuestLoginReq": 1}
        let msgBuf = ProtoMgr.Instance.SerializeMsg(Cmd[ctype], msg);
        // end

        // step2: 按照协议,封装号我们的二进制数据包;
        var total_len = msgBuf.length + 2 + 2 + 4;
        var buf = new ArrayBuffer(total_len); // 内存;
        // DataView, 工具,buffer里面来写东西;
        var dataview = new DataView(buf);
        // [stype, ctype, 4, body Buf]
        dataview.setInt16(0, stype, true); // offset, stype
        dataview.setInt16(2, ctype, true); // offset = 2, ctype;
        dataview.setInt32(4, 0, true);
        // end
        var uint8Buf = new Uint8Array(buf);
        uint8Buf.set(msgBuf, 8);

        // step3: WebSocket发送出去
        NetMgr.Instance.send_data(buf);
        // end
    }
}

规定服务号和命令号内容

export enum Stype {
	INVALIDI_STYPE = 0,
	Auth = 1,
	System = 2,
	Logic = 3,
}

export enum Cmd {
	INVALID_CMD = 0,
	
	GuestLoginReq = 1,
	GuestLoginRes = 2,
	Relogin = 3,
	UserLostConn = 4,
	EditProfileReq = 5,
	EditProfileRes = 6,

	AccountUpgradeReq = 7,
	AccountUpgradeRes = 8,

	UnameLoginReq = 9,
	UnameLoginRes = 10,

	LoginOutReq = 11,
	LoginOutRes = 12,

	GetUgameInfoReq = 13,
	GetUgameInfoRes = 14,

	RecvLoginBonuesReq = 15,
	RecvLoginBonuesRes = 16,

	GetWorldRankUchipReq = 17,
	GetWorldRankUchipRes = 18,

	GetSysMsgReq = 19,
	GetSysMsgRes = 20,

	LoginLogicReq = 21,
	LoginLogicRes = 22,

	EnterZoneReq = 23,
	EnterZoneRes = 24,

	EnterMatch = 25,
	UserArrived = 26,

	ExitMatchReq = 27,
	ExitMatchRes = 28,
	UserExitMatch = 29,

	GameStart = 30,
	UdpTest = 31,
	LogicFrame = 32,
	NextFrameOpts = 33,
}

使用

import { EventMgr } from "../../Framework/Managers/EventMgr";
import { Cmd } from "./Cmd";
import { NetEventDispatcher } from "./NetEventDispatcher";
import { Stype } from "./Stype";



export class AuthProxy {
    public static Insance: AuthProxy = new AuthProxy();
    
    //该脚本专注于处理Auth服务
    public Init(): void {
        EventMgr.Instance.AddEventListener(Stype[Stype.Auth], this, this.onAuthServerReturn);
    }

    // 接收数据
    private onAuthServerReturn(eventName: string, msg: any): void {
      // 这里可以根据eventName进行不同的逻辑处理,这里可以使用case的写法
        console.log(msg);
    }
    // 发送登录请求
    public UserNameLogin(uname: string, upwd: string): void {
        // var md5Pwd = hex_md5(upwd);
        NetEventDispatcher.Instance.sendMsg(Stype.Auth, Cmd.UnameLoginReq, {uname: uname, upwd: upwd});
    }
}


游戏框架的首屏画面

  1. 游戏开始的时候需要添加一个加载进度画面,这个画面的资源要预先加载,做法:把Loading界面留在编辑器场景中,或者使用预制体挂载在场景节点中,注意不要打到AB包中。
import { _decorator, Component, Node, Label } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
const { ccclass, property } = _decorator;

import { UICtrl } from "./../../Framework/Managers/UICtrl";

@ccclass('UILoading_Ctrl')
export class UILoading_Ctrl extends UICtrl {
    private progressValue: Label = null as unknown as Label;
    
    onLoad(): void {
        super.onLoad();
        this.progressValue = this.view["Progress"].getComponent(Label);
        this.progressValue.string = "0%";
        EventMgr.Instance.AddEventListener("loadProgress", this, this.onProgressUpdate);
    }

    private onProgressUpdate(name: string, per: number): void {
        this.progressValue.string = per + "%";
    }

    onDestroy(): void {
        EventMgr.Instance.RemoveListenner("loadProgress", this, this.onProgressUpdate)
    }
}

Excel表格模块,加载CSV

  1. 策划使用Excel表格可以导出CSV文件给程序使用
  2. 程序把csv文件作为文本资源加载
  3. 解析CSV文件,每一行都是一个表{},存储键值对。总体是一个数组,存储各行的表
  4. 不仅提供数组访问一行的数据,还提供另外一个数据结构:表,通过主键访问,主键就是第一列的id号。

表格加载代码(直接放到工程中即可)

var CELL_DELIMITERS = [",", ";", "\t", "|", "^"];
var LINE_DELIMITERS = ["\r\n", "\r", "\n"];

var getter = function (index) {
    return ("d[" + index + "]");
};

var getterCast = function(value, index, cast, d) {

    if (cast instanceof Array) {
        if (cast[index] === "number") {
            return Number(d[index]);
        } else if (cast[index] === "boolean") {
            return d[index] === "true" ||  d[index] === "t" || d[index] === "1";
        } else {
            return d[index];
        }
    } else {
        if (!isNaN(Number(value))) {
            return Number(d[index]);
        } else if (value == "false" || value == "true" || value == "t" || value == "f") {
            return d[index] === "true" ||  d[index] === "t" || d[index] === "1";
        } else {
            return d[index];
        }
    }
};

var CSV = {
    //

    /* =========================================
        * Constants ===============================
        * ========================================= */

    STANDARD_DECODE_OPTS: {
        skip: 0,
        limit: false,
        header: false,
        cast: false,
        comment: ""
    },

    STANDARD_ENCODE_OPTS: {
        delimiter: CELL_DELIMITERS[0],
        newline: LINE_DELIMITERS[0],
        skip: 0,
        limit: false,
        header: false
    },

    quoteMark: '"',
    doubleQuoteMark: '""',
    quoteRegex: /"/g,

    /* =========================================
        * Utility Functions =======================
        * ========================================= */
    assign: function () {
        var args = Array.prototype.slice.call(arguments);
        var base = args[0];
        var rest = args.slice(1);
        for (var i = 0, len = rest.length; i < len; i++) {
            for (var attr in rest[i]) {
                base[attr] = rest[i][attr];
            }
        }

        return base;
    },

    map: function (collection, fn) {
        var results = [];
        for (var i = 0, len = collection.length; i < len; i++) {
            results[i] = fn(collection[i], i);
        }

        return results;
    },

    getType: function (obj) {
        return Object.prototype.toString.call(obj).slice(8, -1);
    },

    getLimit: function (limit, len) {
        return limit === false ? len : limit;
    },

    buildObjectConstructor: function(fields, sample, cast) {
        return function(d) {
            var object = new Object();
            var setter = function(attr, value) {
                return object[attr] = value;
            };
            if (cast) {
                fields.forEach(function(attr, idx) {
                    setter(attr, getterCast(sample[idx], idx, cast, d));
                });
            } else {
                fields.forEach(function(attr, idx) {
                    setter(attr, getterCast(sample[idx], idx, null, d));
                });
            }
            // body.push("return object;");
            // body.join(";\n");
            return object;
        };
    },

    buildArrayConstructor: function(fields, sample, cast) {
        return function(d) {
            var row = new Array(sample.length);
            var setter = function(idx, value) {
                return row[idx] = value;
            };
            if (cast) {
                fields.forEach(function(attr, idx) {
                    setter(attr, getterCast(sample[idx], idx, cast, d));
                });
            } else {
                fields.forEach(function(attr, idx) {
                    setter(attr, getterCast(sample[idx], idx, null, d));
                });
            }
            return row;
        };
    },

    frequency: function (coll, needle, limit) {
        if (limit === void 0) limit = false;

        var count = 0;
        var lastIndex = 0;
        var maxIndex = this.getLimit(limit, coll.length);

        while (lastIndex < maxIndex) {
            lastIndex = coll.indexOf(needle, lastIndex);
            if (lastIndex === -1) break;
            lastIndex += 1;
            count++;
        }

        return count;
    },

    mostFrequent: function (coll, needles, limit) {
        var max = 0;
        var detected;

        for (var cur = needles.length - 1; cur >= 0; cur--) {
            if (this.frequency(coll, needles[cur], limit) > max) {
                detected = needles[cur];
            }
        }

        return detected || needles[0];
    },

    unsafeParse: function (text, opts, fn) {
        var lines = text.split(opts.newline);

        if (opts.skip > 0) {
            lines.splice(opts.skip);
        }

        var fields;
        var constructor;

        function cells(lines) {
            var line = lines.shift();
            if (line.indexOf('"') >= 0) {// 含引号

                // 找到这行完整的数据, 找到对称的双引号
                var lastIndex = 0;
                var findIndex = 0;
                var count = 0;
                while (lines.length > 0) {
                    lastIndex = line.indexOf('"', findIndex);
                    if (lastIndex === -1 && count % 2 === 0) break;

                    if (lastIndex !== -1) {
                        findIndex = lastIndex + 1;
                        count++;
                    } else {
                        line = line + opts.newline + lines.shift();
                    }
                }

                var list = [];
                var item;

                var quoteCount = 0;

                var start = 0;
                var end = 0;
                var length = line.length;
                for (var key in line) {
                    if (!line.hasOwnProperty(key)) {
                        continue;
                    }

                    let numKey = parseInt(key);
                    var value = line[key];

                    if (numKey === 0 && value === '"') {
                        quoteCount++;
                        start = 1;
                    }

                    if (value === '"') {
                        quoteCount++;

                        if (line[numKey - 1] === opts.delimiter && start === numKey) {
                            start++;
                        }
                    }

                    if (value === '"' && quoteCount % 2 === 0) {

                        if (line[numKey + 1] === opts.delimiter || numKey + 1 === length) {
                            end = numKey;
                            item = line.substring(start, end);
                            list.push(item);
                            start = end + 2;
                            end = start;
                        }

                    }

                    if (value === opts.delimiter && quoteCount % 2 === 0) {
                        end = numKey;
                        if (end > start) {
                            item = line.substring(start, end);
                            list.push(item);
                            start = end + 1;
                            end = start;
                        } else if (end === start) {
                            list.push("");
                            start = end + 1;
                            end = start;
                        }
                    }

                }

                end = length;

                if (end >= start) {
                    item = line.substring(start, end);
                    list.push(item);
                }

                return list;
            } else {
                return line.split(opts.delimiter);
            }
        }

        if (opts.header) {
            if (opts.header === true) {
                opts.comment = cells(lines); // 第一行是注释
                opts.cast = cells(lines); // 第二行是数据类型
                fields = cells(lines);
            } else if (this.getType(opts.header) === "Array") {
                fields = opts.header;
            }

            constructor = this.buildObjectConstructor(fields, lines[0].split(opts.delimiter), opts.cast);
        } else {
            constructor = this.buildArrayConstructor(fields, lines[0].split(opts.delimiter), opts.cast);
        }

        while (lines.length > 0) {
            var row = cells(lines);
            if (row.length > 1) {
                fn(constructor(row), fields[0]);
            }
        }

        return true;
    },

    safeParse: function (text, opts, fn) {
        var delimiter = opts.delimiter;
        var newline = opts.newline;

        var lines = text.split(newline);
        if (opts.skip > 0) {
            lines.splice(opts.skip);
        }

        return true;
    },

    encodeCells: function (line, delimiter, newline) {
        var row = line.slice(0);
        for (var i = 0, len = row.length; i < len; i++) {
            if (row[i].indexOf(this.quoteMark) !== -1) {
                row[i] = row[i].replace(this.quoteRegex, this.doubleQuoteMark);
            }

            if (row[i].indexOf(delimiter) !== -1 || row[i].indexOf(newline) !== -1) {
                row[i] = this.quoteMark + row[i] + this.quoteMark;
            }
        }

        return row.join(delimiter);
    },

    encodeArrays: function(coll, opts, fn) {
        var delimiter = opts.delimiter;
        var newline = opts.newline;

        if (opts.header && this.getType(opts.header) === "Array") {
            fn(this.encodeCells(opts.header, delimiter, newline));
        }

        for (var cur = 0, lim = this.getLimit(opts.limit, coll.length); cur < lim; cur++) {
            fn(this.encodeCells(coll[cur], delimiter, newline));
        }

        return true;
    },

    encodeObjects: function (coll, opts, fn) {
        var delimiter = opts.delimiter;
        var newline = opts.newline;
        var header;
        var row;

        header = [];
        row = [];
        for (var key in coll[0]) {
            header.push(key);
            row.push(coll[0][key]);
        }

        if (opts.header === true) {
            fn(this.encodeCells(header, delimiter, newline));
        } else if (this.getType(opts.header) === "Array") {
            fn(this.encodeCells(opts.header, delimiter, newline));
        }

        fn(this.encodeCells(row, delimiter));

        for (var cur = 1, lim = this.getLimit(opts.limit, coll.length); cur < lim; cur++) {
            row = [];
            for (var key$1 = 0, len = header.length; key$1 < len; key$1++) {
                row.push(coll[cur][header[key$1]]);
            }

            fn(this.encodeCells(row, delimiter, newline));
        }

        return true;
    },

    parse: function (text, opts, fn) {
        var rows;

        if (this.getType(opts) === "Function") {
            fn = opts;
            opts = {};
        } else if (this.getType(fn) !== "Function") {
            rows = [];
            fn = rows.push.bind(rows);
        } else {
            rows = [];
        }

        opts = this.assign({}, this.STANDARD_DECODE_OPTS, opts);
        this.opts = opts;

        if (!opts.delimiter || !opts.newline) {
            var limit = Math.min(48, Math.floor(text.length / 20), text.length);
            opts.delimiter = opts.delimiter || this.mostFrequent(text, CELL_DELIMITERS, limit);
            opts.newline = opts.newline || this.mostFrequent(text, LINE_DELIMITERS, limit);
        }

        // modify by jl 由表自行控制不要含有双引号.提高解析效率
        return this.unsafeParse(text, opts, fn) &&
            (rows.length > 0 ? rows : true);
    },

    encode: function (coll, opts, fn) {
        var lines;

        if (this.getType(opts) === "Function") {
            fn = opts;
            opts = {};
        } else if (this.getType(fn) !== "Function") {
            lines = [];
            fn = lines.push.bind(lines);
        }

        opts = this.assign({}, this.STANDARD_ENCODE_OPTS, opts);

        if (opts.skip > 0) {
            coll = coll.slice(opts.skip);
        }

        return (this.getType(coll[0]) === "Array" ? this.encodeArrays : this.encodeObjects)(coll, opts, fn) &&
            (lines.length > 0 ? lines.join(opts.newline) : true);
    }
};

export default CSV;

CSV管理模块

import { _decorator, Component, Node } from 'cc';
import CSV from '../../3rd/CSVParser';

export class ExcelMgr extends Component {
    public static Instance: ExcelMgr = null as unknown as ExcelMgr;
    
    public onLoad(): void {
        if(ExcelMgr.Instance === null) {
            ExcelMgr.Instance = this;
        }
        else {
            this.destroy();
            return;
        }
    }

    loadCallback: Function = null as unknown as Function;
    cntLoad: number = 0;
    curLoad: number = 0;

   

    csvTablesLoaded: any = {};

    csvTables:any = {};
    csvTableForArr:any = {};
    tableCast:any = {};
    tableComment:any = {};

    addTable (tableName:string, tableContent:string, force?:boolean) {
        if (this.csvTables[tableName] && !force) {
            return;
        }
        // 把id作为主键的表
        var tableData = {};
        // 每一行的内容
        var tableArr = []; 
        // 表头
        var opts = { header: true };
        // 解析
        CSV.parse(tableContent, opts, function (row, keyname) {
            tableData[row[keyname]] = row;
            tableArr.push(row);
        });
        // 表头和注释
        this.tableCast[tableName] = (CSV as any).opts.cast;
        this.tableComment[tableName] = (CSV as any).opts.comment;
        // 添加到管理表的表中
        this.csvTables[tableName] = tableData;
        this.csvTableForArr[tableName] = tableArr;

        //this.csvTables[tableName].initFromText(tableContent);
    }

    getTableArr (tableName:string) {
        return this.csvTableForArr[tableName];
    }

    getTable (tableName:string) {
        return this.csvTables[tableName];
    }
    // 查询一条数据,key=value
    queryOne (tableName:string, key:string, value:any) {
        var table = this.getTable(tableName);
        if (!table) {
            return null;
        }
        
        if (key) {
            for (var tbItem in table) {
                if (!table.hasOwnProperty(tbItem)) {
                    continue;
                }

                if (table[tbItem][key] === value) {
                    return table[tbItem];
                }
            }
            
        } else {
            return table[value];
        }
    }
    // 通过id查询一条数据
    queryByID (tableName:string, ID:string) {
        return this.queryOne(tableName, null, ID);
    }
    // 查询符合条件 所有数据key=value
    queryAll (tableName:string, key:string, value:any) {
        var table = this.getTable(tableName);
        if (!table || !key) {
            return null;
        }

        var ret = {};
        for (var tbItem in table) {
            if (!table.hasOwnProperty(tbItem)) {
                continue;
            }

            if (table[tbItem][key] === value) {
                ret[tbItem] = table[tbItem];
            }
        }

        return ret;
    }

    queryIn (tableName:string, key:string, values:Array<any>) {
        var table = this.getTable(tableName);
        if (!table || !key) {
            return null;
        }

        var ret = {};
        var keys = Object.keys(table);
        var length = keys.length;
        for (var i = 0; i < length; i++) {
            var item = table[keys[i]];
            if (values.indexOf(item[key]) > -1) {
                ret[keys[i]] = item;
            }
        }

        return ret;
    }

    queryByCondition (tableName:string, condition: any) {
        if (condition.constructor !== Object) {
            return null;
        }

        var table = this.getTable(tableName);
        if (!table) {
            return null;
        }

        var ret = {};
        var tableKeys = Object.keys(table);
        var tableKeysLength = tableKeys.length;
        var keys = Object.keys(condition);
        var keysLength = keys.length;
        for (var i = 0; i < tableKeysLength; i++) {
            var item = table[tableKeys[i]];
            var fit = true;
            for (var j = 0; j < keysLength; j++) {
                var key = keys[j];
                fit = fit && (condition[key] === item[key]) && !ret[tableKeys[i]];
            }

            if (fit) {
                ret[tableKeys[i]] = item;
            }
        }

        return ret;
    }

    queryOneByCondition (tableName:string, condition: any) {
        if (condition.constructor !== Object) {
            return null;
        }

        var table = this.getTable(tableName);
        if (!table) {
            return null;
        }
        
        var keys = Object.keys(condition);
        var keysLength = keys.length;

        for (let keyName in table) {
            var item = table[keyName];

            var fit = true;
            for (var j = 0; j < keysLength; j++) {
                var key = keys[j];
                fit = fit && (condition[key] === item[key]);
            }

            if (fit) {
                return item;
            }
        }

        return null;
    }
}

使用:

// 游戏开始的逻辑
public initGame(csvText: string): void {
    // 加载一个表格数据到我们的Excel管理里面了;
    ExcelMgr.Instance.addTable("map", csvText);
    var excel = ExcelMgr.Instance.getTableArr("map");
    console.log(excel);
    // end

    var ret = ExcelMgr.Instance.queryOne("map", "mapName", "map008");
    console.log(ret);
}

战斗系统的三重设计

  1. 功能机制层:只提供功能,不提供策略。例如:行走组件、动画播放、攻击组件
  2. 策略层:基础角色控制、玩家角色控制、NPC策略、Boss策略,调用功能机制层,实现对应的动作:例如:移动,攻击,被攻击
  3. 行为决策层:玩家输入,NPC固定策略,网络同步策略,AI策略等,主要功能是在什么时机调用策略层的函数。

功能机制层

动画模块示例


import { _decorator, Component, Node, SkeletalAnimation } from 'cc';

export class AnimAction extends Component {
    public anim: SkeletalAnimation = null!;

    private state: number = 0;

    public static AnimState = {
        Invalid: -1,
        Idle: 0,
        Walk: 1,
        Skill: 2,
    };

    static animNames = ["free", "walk", "skill1"];

    public init(): void {
        this.anim = this.node.getComponentInChildren(SkeletalAnimation);
        this.state = AnimAction.AnimState.Invalid;
        this.setState(AnimAction.AnimState.Idle);
    }    

    public setState(state: number) {
        if(this.state === state) {
            return;
        }
        this.state = state;
        // console.log(AnimAction.animNames[this.state], "####");
        this.anim.crossFade(AnimAction.animNames[this.state]);
    }
}

攻击模块示例

import { _decorator, Component, Node } from 'cc';

export class AttackAction extends Component {

    private isAttack: boolean = false;
    private hurtTime: number = 0;
    private endTime: number = 0;
    private onHurt: Function = null!;
    private onEndAttack: Function = null!;

    private nowTime: number = 0;

    public init(): void {
        this.isAttack = false;
        this.hurtTime = 0;
        this.endTime = 0;
        this.onHurt = null;
        this.onEndAttack = null;
        this.nowTime = 0;
    }

    public doAttack(hurtTime: number, 
                    endTime: number, 
                    onHurt: Function, endAttack: Function): boolean {
        if(this.isAttack === true) {
            return false; // 失败,返回foce;
        }


        this.nowTime = 0;
        this.isAttack = true;
        this.hurtTime = hurtTime;
        this.endTime = endTime;
        this.onHurt = onHurt;
        this.onEndAttack = endAttack;

        return true;
    }

    update(dt: number): void {
        if(this.isAttack === false) {
            return;
        }

        this.nowTime += dt;
        if(this.nowTime >= this.hurtTime) { // 计算伤害
            if(this.onHurt) {
                this.onHurt();
                this.onHurt = null;
            }
        }

        if(this.nowTime >= this.endTime) {
            if(this.onEndAttack) {
                this.onEndAttack();
                this.onEndAttack = null;
            }

            this.isAttack = false;
        }
    }
}

策略层

基础角色控制模块示例


import { _decorator, Component, Node } from 'cc';
import { AnimAction } from './AnimAction';
import { AttackAction } from './AttackAction';
import { FightMgr } from './FightMgr';

export class CharactorCtrl extends Component {
    protected animAction: AnimAction = null!;
    protected attackAction: AttackAction = null!;

    // 战斗属性
    protected HP: number = 3;
    protected attack: number = 2;
    protected define: number = 1;
    protected isDead: boolean = false;
    // ....
    // end

    protected skillParams: any = {};

    public init(params: any): void {
        this.HP = params.HP;
        this.attack = params.attack;
        this.define = params.define;
        this.isDead = false;

        if(params.skill) {
            this.skillParams.endTime = params.skill.endTime;
            this.skillParams.hurtTime = params.skill.hurtTime;
            this.skillParams.skillAttack = params.skill.skillAttack;
            this.skillParams.attackR = params.skill.attackR;
        }

        // 功能组件我们要实例化
        this.animAction = this.node.addComponent(AnimAction);
        this.animAction.init();
        this.attackAction = this.node.addComponent(AttackAction);
        this.attackAction.init();
        // ...
        // end
    }

    public onDoSkillAction() {
        if(this.attackAction.doAttack(this.skillParams.hurtTime, this.skillParams.endTime, this.onComputeSkillHurt.bind(this), this.onSkillActionEnd.bind(this))) {
            
            this.animAction.setState(AnimAction.AnimState.Skill);
        }
    }

    public onHurt(attack: number): void {
        console.log(attack, this.define);
        var lost: number = attack - this.define;
        if(lost <= 0) {
            return;
        }

        this.HP -= lost;
        if(this.HP <= 0) {
            // this.node.removeFromParent();
            this.isDead = true;
            // 播放死亡动画
            console.log("FightMgr.Instance.destroyEnemy");
            FightMgr.Instance.destroyEnemy(this.node);
            // end
        }
    }

    public onComputeSkillHurt(): void {
        
        // 我就要从地图里面找到我所有的敌人;
        var enmeies = FightMgr.Instance.findObjectByRadius(this.node.getWorldPosition(), this.skillParams.attackR);
        for(var i = 0; i < enmeies.length; i ++) {
            var ctrl: CharactorCtrl = enmeies[i].getComponent(CharactorCtrl);
            ctrl.onHurt(this.skillParams.skillAttack);
        }
        // end
    }

    public onSkillActionEnd(): void {
        this.animAction.setState(AnimAction.AnimState.Idle);
    }
}


行为决策层

玩家操作模块示例


import { _decorator, Component, Node, systemEvent, SystemEvent } from 'cc';
import { CharactorCtrl } from './CharactorCtrl';

export class PlayerOpt extends Component {
    private ctrl: CharactorCtrl = null;

    public init(): void {
        this.ctrl = this.node.getComponent(CharactorCtrl);

        systemEvent.on(SystemEvent.EventType.TOUCH_START, this.onTouchStart, this);
    }

    private onTouchStart(): void {
        this.ctrl.onDoSkillAction();
    }
}

怪物AI模块示例


import { _decorator, Component, Node } from 'cc';
import { CharactorCtrl } from './CharactorCtrl';

export class AIOpt extends Component {
    private ctrl: CharactorCtrl = null;
    public init(): void {
        this.ctrl = this.node.getComponent(CharactorCtrl);
    }

    private doAI(): void {
        // 发一个技能
        // this.ctrl.onDoSkillAction();
        // end

        // 行走
        // end
    }

    update(dt: number): void {
        this.doAI();
    }
}

加载远程图片

this.img = this.node.getComponent(Sprite);

  var str = "http://lf9-sf-be-pack-sign.pglstatp-toutiao.com/ad.union.api/b5dd2924197bf4428751ceb9782e00da?x-expires=1942070400&x-signature=QhZdl6SPjxb4FCx4MGFj2RA4Fps%3D";
  assetManager.loadRemote(str, {ext:".jpg"}, (err, imgAsset: any)=>{
      
      if(err) {
          console.log("load error");
          return;
      }

      // console.log(imgAsset.data);
      var s = SpriteFrame.createWithImage(imgAsset);
      // console.log(s, imgAsset);
      this.img.spriteFrame = s;
  });
}

文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
  目录