大厅功能

来自泡泡
Liuweichen留言 | 贡献2024年9月5日 (四) 10:29的版本 →‎匹配成功

老项目升级环境配置

项目demo地址

[git地址](https://git.jumpw.com/popx_demo/demox.git)

分支:hall_test

添加src/hall/index.ts的大厅服务器入口文件

修改打包配置rollup

  • rollup.config.js新增打包大厅index.js的配置
  • rollup.config.js加一下服务类的白名单


修改html启动文件

  • 拷贝覆盖index.html到你的项目中,拷贝hall.html本地大厅文件到同一文件夹下

审核后台勾选大厅

只有勾选了大厅后,登录流程才会走大厅的逻辑,否则是走的老的流程,直接进战斗房间

大厅游戏工作流说明

大厅工程相对于原来的client,server目录结构,多了一个hall目录,而且server实际上指的是host,

每个目录的代码文件入口都在各自的index.ts

为了方便大家理解,我们用颜色来标记各个核心文件夹的工作流

client用橙色,代表前端显示,前端逻辑

hall用绿色,代表大厅,大厅服务器逻辑

host用蓝色,注意,为了不和开房间混淆,host代表的是战斗服务器逻辑,而不是和以前一样叫做房间

client的入口代码最早执行,在里面做各种监听,也就是说,大厅期间,各种前端界面UI就已经能自己处理了,可以调用大厅的数据

以下的流程,如果审核后台勾选了大厅,按照顺序执行,否则直接跳过大厅,到了《进入战斗》


游戏开始时

进大厅前预加载资源

client:XPopManager.onHallWait

--在进入大厅前,会有一个promise的异步接口供开发者使用去预加载资源,比如表格,比如读取文件配置等等。

必须在加载完成的回调中resolve

  
//预加载资源  
XPopManager.onHallWait =  new Promise((resolve)=>{  
    GameManager.instance.onPreLoadRes(()=>{  
        resolve("Resource loaded successfully hall");  
    });  
});  

进入大厅

client:XPopManager.onHallStart

--在大厅资源预加载完成后,会直接启动大厅服务器实例

hall:XPopManager.onHostHallStart

--src/hall/index.ts 中监听的代码入口

大厅期间操作

监听大厅内数据变化

大厅期间,hall的实例会监听各种玩家的事件,并且抛出回调,开发者可以在回调中处理hall的快照数据

hall的实例是指XPopManager.instance.xHostHall

hall:hall.onUserEnter

--大厅内有玩家进入

hall:hall.onUserLeave

--大厅内有玩家离开

hall:hall.onUserMatchChange

--大厅内匹配信息发生变化,主要是指倒计时的变化

hall:hall.onDbItemChange

--大厅内存盘的数据变化,在进入时就会调用一次,获取到账号DB数据,存盘后面再说

开始匹配

匹配内容后面单独讲

匹配成功

后面匹配内容会单独讲, 匹配成功后,分配到了某个房间的实例(host),并且会把匹配时的透传参数通知给host实例

进入战斗

host:XPopManager.onHostStart --src/server/index.ts 中监听的代码入口,去处理host实例的各种回调

战斗期间操作

监听host战斗服内数据变化

host的实例是指XPopManager.instance.xHost host:host.onUserEnter

--当战斗服务器内有玩家进入

host:host.onUserLeave

--当战斗服务器内有玩家离开

host:host.onUserMatchExtraDataReceived

--当战斗服务器收到玩家透传信息(匹配时传入的ExtraData)

host:host.onDbItemChange

--当战斗服务器收到存盘的数据变化,在进入时就会调用一次,获取到账号DB数据,存盘后面再说

战斗返回大厅

client:XPopManager.onGameLeave

--返回到大厅,大厅的数据还在快照中,可以做一些结算的处理

想要回到大厅,需要手动调用XPopManager.instance.xRemote.LeaveGame ,demo里有示例

        XPopManager.instance.xRemote.LeaveGame(this, (code: number, ret: string) => {
            console.log('LeaveGame', code, ret);
        });

离开整个游戏

client:XPopManager.onExit

--在试玩的时候,点击官方左上角的退出,会进入此回调函数

--平时本地调试开发时,不需要去处理这个回调

--但是试玩时需要在此销毁场内资源,否则,退出后再次进入,会发现上一回的模型还在

XPopManager.onExit = () => {
    model.destroy();
}

前后端数据交互

交互只能client和hall,client和host之间互通信息,不能hall和host直接交互

client和hall的交互

所有的和大厅的数据交互,依赖于

client内部代码:this._remoteHall = XPopManager.instance.xRemote.hall

hall内部代码:this.hall = XPopManager.instance.xHostHall

具体代码位置在各自文件夹内

  • 1.client向hall注册快照,并且监听快照变化
  • 2.hall注册rcp请求的回调方法
  • 3.client向hall发起rpc请求
  • 4.hall处理rpc请求,hall修改快照
  • 5.client监听快照变化的回调,处理显示逻辑

具体可以查看MessageManager中的代码

client注册快照

代码里有私有和共有快照,实际上本质上都是最基本的注册快照,只是在hall里面处理数据的方式不一样,具体可以看代码,不再赘述

this._remoteHall.RegisterSnapshot(`${Snap_HallData.tag}`, this._hallData);

client监听快照变化

       this._hallData.notice.OnSet = (bReset) => {
            XTipManager.Instance.showTip(this._hallData.notice.value);
        }

hall注册rpc回调

this.hall.RegisterRpc('AddDbItem', this.onReceiveAddDbItem.bind(this));

client发起rpc请求

    public async Hall_Request_AddItem(type:DBItemType, itemID:ItemNormalID, itemNum:number, callback?: Function): Promise<void> {
        let ret = await this._remoteHall.Request("AddDbItem", { type: type, actorID: this._userID, itemID: itemID, itemNum: itemNum });
        console.log('requestAddDbItem', ret);
        callback && callback(ret);
    }

hall处理rpc请求,修改快照

private async onReceiveAddDbItem(call: ApiCall<ReqAddDbItem, ResAddDbItem>): Promise<void> {
        console.log('>>>>>>onReceiveAddDbItem');
        // 修改某个快照,前端已经注册过处理函数
    }

client和host的交互

所有的和战斗服的数据交互,依赖于

client内部代码:this._remoteBattle = XPopManager.instance.xRemote.game;

host内部代码(server文件夹):this.host = XPopManager.instance.xHost;

所有的流程和hall一样处理

数据存盘

数据存盘对应的是玩家登录时的账号id,调用存盘后,就有账号的长期数据可以记录和读取了

存储

存储只能由hall或者host发起,两个服务器都有同名的存储接口

demo中,写在了hall里面,host里也可以写一份一样的,这样战斗和大厅的数据就都能存储了.

如下示例代码的发起方是client通过rpc去通知hall发起的,实际上可以在服务器计算过程中自己发起

说几个关键的参数:

  • dbID

-- 需要开发者在我们的审核后台,获取了游戏id之后,把游戏id的字符串给我们的内部人员, -- 然后我们帮开发者映射生成一个数字的id,专门来存储该游戏的数据, -- 以后存盘读盘时就走的这个数字id

  • type

-- 存盘的时候还有个type,支持填写0到9,可以用于开发者处理不同类型的数据分类。 -- 比如0代表货币类型,1代表背包道具,2代表头像。。等等 -- 当然,全存在一个type里也没影响,因为可以根据唯一的itemID去读自己的表格

  • itemID

-- 开发者存储时的自定义id,用于自己游戏内的道具表格映射,请保持id的唯一性,不要有同id的道具

    private async onReceiveAddDbItem(call: ApiCall<ReqAddDbItem, ResAddDbItem>): Promise<void> {
        console.log('>>>>>>onReceiveAddDbItem');
        let userdata = this._usersMap.get(call.req.actorID);
        if (!userdata) {
            return;
        }
        if(call.req.type == DBItemType.Normal && call.req.itemID == ItemNormalID.Gold){
            let ret;
            if(userdata.goldCount.value == -1){
                let item = new XDbItem();
                item.nID = call.req.itemID;
                item.nCount = call.req.itemNum;
                ret = await this.hall.AddDbItem(this.dbID, call.req.actorID, call.req.type, call.req.itemID, item, this);
            }else{
                ret = await this.hall.SetDbItemCount(this.dbID, call.req.actorID, call.req.type, call.req.itemID, call.req.itemNum+userdata.goldCount.value, this);
            }
            return call.succ({
                ret: ret.ret
            });
        }else{
            return call.succ({
                ret: '未实现'
            });
        }
    }

读取

在hall和host中,都有一个监听函数

XPopManager.instance.xHostHall.onDbItemChange;
XPopManager.instance.xHost.onDbItemChange;

每次当存盘的数据发生变化,都会进入该回调函数。 hall和host连接成功后,也会先进入一次,所以进游戏就能获得存盘数据

我们以hall里的代码举例

   private onDbItemChange(type: number, key: number, value: XDbItem | null, iUserID: number, bReset: boolean):void{
        if(type == DBItemType.Normal && key == ItemNormalID.Gold){
            let userdata = this._usersMap.get(iUserID);
            if (!userdata) {
                return;
            }
            userdata.goldCount.value = value.nCount;
        }
    }
  • type

-对应传入的type

  • key

—对应存储的itemId

  • value: XDbItem

其中,XDbItem的结构是我们DB中固定的格式,外部只需要关心nID和nCount即可

nID对应存储时传入的唯一itemId nCount对应DB中该物品的数量


匹配逻辑

匹配逻辑在hall中处理

创建匹配规则

匹配前,一定要创建至少一个匹配规则,否则无法匹配

参考demo中,如下是添加了更加详细的注释代码

    /**
     * 初始化匹配规则
     */
    private initMatchRule(): void {
        let rules = new XMatchRule();
        // 需求人数	必填
        rules.nRoleNum = Config.MatchNumMax;
        // 地图列表 - 匹配成功后,会从中随机创建一个地图	必填,如果填一张就是唯一一张图
        rules.addMapName(this.matchMapName);

        // 选填更复杂的匹配规则
        {
            let danRule = new XDanRule();
            // 匹配分数区间
            danRule.nMinScore = 0;
            danRule.nMaxScore = 10000;
            danRule.nDanID = 1;
            // 如果填写的分数区间排不到人,可以扩大分数区间
            danRule.AddScore = 0;
            danRule.AddScoreMax = 0;
            // 1阶段匹配时间
            danRule.nMatchTime1 = 15;
            // 2阶段匹配时间
            danRule.nMatchTime2 = 15;
            // 匹配2阶段后,是否塞人机数量,目前先填0即可
            danRule.nAINum = Config.MatchNumMax - Config.MatchNumMin > 0 ? Config.MatchNumMax - Config.MatchNumMin : 0;
            // addDanRule: 段位匹配规则	选填
            rules.addDanRule(danRule);
        }

    
        // 调用添加matchRuleName为key的匹配规则,可以调用添加多个,
        // 添加后,才可以通过this.hall.UserBeginMatch,根据matchRuleName进行匹配
        this.hall.AddGameMatchRules(this.matchRuleName, rules, this);
    }
  • 通过this.hall.AddGameMatchRules(this.matchRuleName, rules, this);来添加至少一条匹配规则,
  • 其中matchRuleName为key,可以多次添加不同的key和rules
  • 必须添加至少一条规则后,才可以通过this.hall.UserBeginMatch,根据matchRuleName来映射到具体规则和地图列表,进行匹配

通过匹配规则匹配

  • matchRuleName必须要先添加
  • rankScore对应上面匹配规则中的分数,根据实际开发的业务数据来
  • ExtraData是透传参数,可以在匹配成功进入战斗服中获取到
 private async onReceiveStartMatch(call: ApiCall<ReqStartMatch, ResStartMatch>): Promise<void> {
        console.log('>>>>>>onReceiveAddDbItem');
        let match = new XMatchRole(call.req.actorID);
        match.ruleName = this.matchRuleName;
        match.rankScore = 0;
        //匹配透传数据
        match.ExtraData = JSON.stringify({heroID:101});
        let ret = await this.hall.UserBeginMatch(call.req.actorID, match, this);
        return call.succ({
            ret: ret.ret
        });
    }

取消匹配

    private async onReceiveCancelMatch(call: ApiCall<ReqCancelMatch, ResCancelMatch>): Promise<void> {
        let ret = await this.hall.UserCancelMatch(call.req.actorID, this)
        return call.succ({
            ret: ret.ret
        });
    }

匹配状态变化

开始匹配和取消匹配都会通过回调来通知给hall,

可以根据开始时间,来处理倒计时等显示的逻辑

// 绑定
this.hall.onUserMatchChange = this.onUserMatchChange.bind(this);
    private onUserMatchChange(iUserID: number, ruleName: string, matchTime: number): void {
        let userdata = this._usersMap.get(iUserID);
        if (!userdata) {
            return;
        }
        userdata.matchTime.value = matchTime;
        console.log('onUserMatchChange', iUserID, ruleName, matchTime);
    }

匹配成功

匹配成功后会进入战斗服务器,host,上面的完整流程已经提过

并且会收到匹配时传入的透传参数ExtraData

this.host.onUserMatchExtraDataReceived = this.onUserMatchExtraDataReceived.bind(this);