跳转到内容
主菜单
主菜单
移至侧栏
隐藏
导航
首页
最近更改
随机页面
MediaWiki帮助
泡泡
搜索
搜索
创建账号
登录
个人工具
创建账号
登录
查看“大厅功能”的源代码
页面
讨论
大陆简体
阅读
查看源代码
查看历史
工具
工具
移至侧栏
隐藏
操作
阅读
查看源代码
查看历史
常规
链入页面
相关更改
特殊页面
页面信息
←
大厅功能
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
管理员
您可以查看和复制此页面的源代码。
== 泡饭工坊模板demo地址 == 泡饭中,hall_test模板,有完整代码 * [[File:泡饭大厅工程.png]] 下载模板 我的工程打开hall_test模板 编译&启动(大厅) == 网页试玩链接多人demo体验地址 == [试玩链接]( https://subgame.popx.com/popion/opendev.html) * 如果是新号,在PC端ctrl +6,打开特殊窗口,输入 “isover 1”,点击按钮,可直接跳过新手流程! * [[File:跳过引导.png]] * 在游戏界面游玩中,找到demox,点击进入即可 * [[File:Demo地址.png]] == 老项目升级环境配置 == 如果本来的项目,是通过demox生成的,并没有大厅,需要按照如下的操作来升级。 如下的拷贝文件来源,是hall_test的项目 === 添加src/hall/index.ts的大厅服务器入口文件 === * [[File:添加hall入口.png]] === 修改打包配置rollup === * rollup.config.js新增打包大厅index.js的配置 * [[File:rollup配置.png]] * rollup.config.js加一下服务类的白名单 * [[File:rollup白名单.png]] === 修改html启动文件 === * 拷贝覆盖index.html到你的项目中,拷贝hall.html本地大厅文件到同一文件夹下 * [[File:html入口.png]] === 审核后台勾选大厅 === * [[File:勾选大厅.png]] <span style="color: red;">'''只有勾选了大厅后,登录流程才会走大厅的逻辑,否则是走的老的流程,直接进战斗房间'''</span> == 大厅游戏工作流说明 == 大厅工程相对于原来的client,server目录结构,多了一个hall目录,而且server实际上指的是host, 每个目录的代码文件入口都在各自的'''index.ts'''里 [[File:大厅文件结构.png]] 为了方便大家理解,我们用颜色来标记各个核心文件夹的工作流 <span style="color: orange;">'''client'''</span>用橙色,代表前端显示,前端逻辑 <span style="color: green;">'''hall'''</span>用绿色,代表大厅,大厅服务器逻辑 <span style="color: blue;">'''host'''</span>用蓝色,注意,为了不和开房间混淆,host代表的是战斗服务器逻辑,而不是和以前一样叫做房间 <span style="color: red;">'''client的入口代码最早执行,在里面做各种监听,也就是说,大厅期间,各种前端界面UI就已经能自己处理了,可以调用大厅的数据'''</span> <span style="color: red;">'''以下的流程,如果审核后台勾选了大厅,按照顺序执行,否则直接跳过大厅,到了《进入战斗》'''</span> === 游戏开始时 === ==== 进大厅前预加载资源 ==== <span style="color: orange;">'''client:'''</span>XPopManager.onHallWait --在进入大厅前,会有一个promise的异步接口供开发者使用去预加载资源,比如表格,比如读取文件配置等等。 <span style="color: red;">'''必须在加载完成的回调中resolve'''</span> <pre> //预加载资源 XPopManager.onHallWait = new Promise((resolve)=>{ GameManager.instance.onPreLoadRes(()=>{ resolve("Resource loaded successfully hall"); }); }); </pre> ==== 进入大厅 ==== <span style="color: orange;">'''client:'''</span>XPopManager.onHallStart --在大厅资源预加载完成后,会直接启动大厅服务器实例 <span style="color: green;">'''hall:'''</span>XPopManager.onHostHallStart --src/hall/index.ts 中监听的代码入口 === 大厅期间操作 === ==== 监听大厅内数据变化 ==== 大厅期间,hall的实例会监听各种玩家的事件,并且抛出回调,开发者可以在回调中处理hall的快照数据 hall的实例是指XPopManager.instance.xHostHall <span style="color: green;">'''hall:'''</span>hall.onUserEnter --大厅内有玩家进入 <span style="color: green;">'''hall:'''</span>hall.onUserLeave --大厅内有玩家离开 <span style="color: green;">'''hall:'''</span>hall.onUserMatchChange --大厅内匹配信息发生变化,主要是指倒计时的变化 <span style="color: green;">'''hall:'''</span>hall.onDbItemChange --大厅内存盘的数据变化,在进入时就会调用一次,获取到账号DB数据,'''存盘'''后面再说 ==== 开始匹配 ==== 匹配内容后面单独讲 ==== 匹配成功 ==== 后面匹配内容会单独讲, 匹配成功后,分配到了某个房间的实例(host),并且会把匹配时的透传参数通知给host实例 === 进入战斗 === <span style="color: blue;">'''host:'''</span>XPopManager.onHostStart --src/server/index.ts 中监听的代码入口,去处理host实例的各种回调 === 战斗期间操作 === ==== 监听host战斗服内数据变化 ==== host的实例是指XPopManager.instance.xHost <span style="color: blue;">'''host:'''</span>host.onUserEnter --当战斗服务器内有玩家进入 <span style="color: blue;">'''host:'''</span>host.onUserLeave --当战斗服务器内有玩家离开 <span style="color: blue;">'''host:'''</span>host.onUserMatchExtraDataReceived --当战斗服务器收到玩家透传信息(匹配时传入的ExtraData) <span style="color: blue;">'''host:'''</span>host.onDbItemChange --当战斗服务器收到存盘的数据变化,在进入时就会调用一次,获取到账号DB数据,'''存盘'''后面再说 === 战斗返回大厅 === <span style="color: orange;">'''client:'''</span>XPopManager.onGameLeave --返回到大厅,大厅的数据还在快照中,可以做一些结算的处理 想要回到大厅,需要手动调用'''XPopManager.instance.xRemote.LeaveGame ''' ,demo里有示例 <pre> XPopManager.instance.xRemote.LeaveGame(this, (code: number, ret: string) => { console.log('LeaveGame', code, ret); }); </pre> === 离开整个游戏 === <span style="color: orange;">'''client:'''</span>XPopManager.onExit --在试玩的时候,点击官方左上角的退出,会进入此回调函数 --平时本地调试开发时,不需要去处理这个回调 --但是试玩时需要在此销毁场内资源,否则,退出后再次进入,会发现上一回的模型还在 <pre> XPopManager.onExit = () => { model.destroy(); } </pre> == 前后端数据交互 == <span style="color: red;">'''交互只能client和hall,client和host之间互通信息,不能hall和host直接交互'''</span> === 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里面处理数据的方式不一样,具体可以看代码,不再赘述 <pre> this._remoteHall.RegisterSnapshot(`${Snap_HallData.tag}`, this._hallData); </pre> ==== client监听快照变化 ==== <pre> this._hallData.notice.OnSet = (bReset) => { XTipManager.Instance.showTip(this._hallData.notice.value); } </pre> ==== hall注册rpc回调 ==== <pre> this.hall.RegisterRpc('AddDbItem', this.onReceiveAddDbItem.bind(this)); </pre> ==== client发起rpc请求 ==== <pre> 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); } </pre> ==== hall处理rpc请求,修改快照 ==== <pre> private async onReceiveAddDbItem(call: ApiCall<ReqAddDbItem, ResAddDbItem>): Promise<void> { console.log('>>>>>>onReceiveAddDbItem'); // 修改某个快照,前端已经注册过处理函数 } </pre> === client和host的交互 === 所有的和战斗服的数据交互,依赖于 client内部代码:this._remoteBattle = XPopManager.instance.xRemote.game; host内部代码(server文件夹):this.host = XPopManager.instance.xHost; 所有的流程和hall一样处理 == 数据存盘 == 数据存盘对应的是玩家登录时的账号id,调用存盘后,就有账号的长期数据可以记录和读取了 === 存储 === <span style="color: red;">'''存储只能由hall或者host发起,两个服务器都有同名的存储接口'''</span> <span style="color: red;">'''该功能没有必要接入大厅,直接调用host中的代码接口也可以存储和读取'''</span> <span style="color: red;">'''调用该功能前,请务必根据自身gameid向我们内部申请一个对应的数字id来存盘使用'''</span> demo中,写在了hall里面,host里也可以写一份一样的,这样战斗和大厅的数据就都能存储了. 如下示例代码的发起方是client通过rpc去通知hall发起的,实际上可以在服务器计算过程中自己发起 说几个关键的参数: *dbID -- 需要开发者在我们的审核后台,获取了游戏id之后,把游戏id的字符串给我们的内部人员, -- 然后我们帮开发者映射生成一个数字的id,专门来存储该游戏的数据, -- 以后存盘读盘时就走的这个数字id *type -- 存盘的时候还有个type,支持填写0到9,可以用于开发者处理不同类型的数据分类。 -- 比如0代表货币类型,1代表背包道具,2代表头像。。等等 -- 当然,全存在一个type里也没影响,因为可以根据唯一的itemID去读自己的表格 *itemID -- 开发者存储时的自定义id,用于自己游戏内的道具表格映射,请保持id的唯一性,不要有同id的道具 <pre> 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: '未实现' }); } } </pre> === 读取 === 在hall和host中,都有一个监听函数 <pre> XPopManager.instance.xHostHall.onDbItemChange; XPopManager.instance.xHost.onDbItemChange; </pre> 每次当存盘的数据发生变化,都会进入该回调函数。 <span style="color: red;">'''hall和host连接成功后,也会先进入一次,所以进游戏就能获得存盘数据'''</span> 我们以hall里的代码举例 <pre> 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; } } </pre> *type -对应传入的type *key —对应存储的itemId *value: XDbItem 其中,XDbItem的结构是我们DB中固定的格式,外部只需要关心nID和nCount即可 nID对应存储时传入的唯一itemId nCount对应DB中该物品的数量 * [[File:db存储结构.png]] === KeyValue数据存盘 === 简单的物品类型并不足以支持复杂的游戏数据结构,我们也提供了和物品存储类似的数据结构存储 同样是在hall和host中都可以调用 监听的函数用'''onDbDataChange''' <pre> XPopManager.instance.xHostHall.onDbDataChange; XPopManager.instance.xHost.onDbDataChange; </pre> 存储的函数用SetDbDatas <pre> // 存储设置自定义key-value ret = await this.hall.SetDbData() // 存储整个map的keyvalue合集 ret = await this.hall.SetDbDatas() </pre> * [[File:存盘复杂数据.png]] 如函数参数中所示,存储的value是string类型, 那么可以通过JSON.stringify(obj)和JSON.parse(str)来相互转换,从而存储复杂数据 * 注意:如上是hall的存储读取示例,如果想用host,接口类似,在如下的论坛帖子里有演示。 [host存储数据](https://bbs.popx.com/t/keyvalue-ondbdatachange/207/14?u=qc_jump) == 匹配逻辑 == 匹配逻辑在hall中处理 === 创建匹配规则 === 匹配前,一定要创建至少一个匹配规则的名称,否则无法匹配 参考demo中,在hall代码的初始阶段就要设置 如下是添加了更加详细的注释代码,其中段位相关的规则可以先不添加 <pre> /** * 初始化匹配规则 */ private initMatchRule(): void { let rules = new XMatchRule(); // 需求人数 必填 rules.nRoleNum = Config.MatchNumMax; // 地图列表 - 匹配成功后,会从中随机创建一个地图 必填,如果填一张就是唯一一张图 rules.addMapName(this.matchMapName); // 选填更复杂的段位规则,根据段位来设置规则,如果不添加,就会无规则随意匹配 { for(let i =0;i<9;i++) { let duanLevel = i+1; this.addRulesDuan(rules,duanLevel) } } // 调用添加matchRuleName为key的匹配规则,可以调用添加多个, // 添加后,才可以通过this.hall.UserBeginMatch,根据matchRuleName进行匹配 this.hall.AddGameMatchRules(this.matchRuleName, rules, this); } </pre> *通过this.hall.AddGameMatchRules(this.matchRuleName, rules, this);来添加至少一条匹配规则, *其中matchRuleName为key,可以多次添加不同的key和rules *必须添加至少一条规则后,才可以通过this.hall.UserBeginMatch,<span style="color: red;">'''根据matchRuleName来映射到具体规则和地图列表,进行匹配'''</span> ==== 详细段位规则 ==== 上面的基础规则代码中,有一部分在添加段位规则,并且一个XMatchRule对应了多个段位规则 <pre> private addRulesDuan(rules: XMatchRule,duanLevel:number = 1):void { let danRule = new XDanRule(); // 匹配分数区间,匹配时,根据传入的分数,来找到符合区间的duanRule,找不到那就没有规则 danRule.nMinScore = (duanLevel - 1) * 1000; danRule.nMaxScore = duanLevel * 1000; // 比如初始分数区间为1000-2000, // 玩家是1500分,玩家就匹配到了这个段的规则 // 每个匹配规则里,可以分成多个段位,比如青铜,王者,通过id来区分各个段位的匹配规则 danRule.nDanID = duanLevel; // 1阶段匹配时间 danRule.nMatchTime1 = 15; // 1阶段时,可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000,添加后变成了900-2100 danRule.AddScore = 100; // 1阶段的时间结束,房间人还没满,会进入2阶段 // 2阶段匹配时间 danRule.nMatchTime2 = 15; // 2阶段时,可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000,添加后变成了1800-2200 danRule.AddScoreMax = 200; // 匹配2阶段后,是否塞人机数量 // 已经实现了把人机的数据塞进战斗房间 // 比如XMatchRule里的nRoleNum = 8,nAINum = 2,那么当房间人数为6时,就会进入游戏(2阶段后) danRule.nAINum = Config.MatchNumMax - Config.MatchNumMin > 0 ? Config.MatchNumMax - Config.MatchNumMin : 0; rules.addDanRule(danRule); } </pre> <pre> // 可以通过接口来获取具体的玩家信息,玩家信息中有字段来标记是否为机器人 // host接口 XPopManager.instance.xHost.GetUserInfo(userid).isRobot // 前端接口 XPopManager.instance.xRemote.GetUserInfo(userid).isRobot </pre> === 通过匹配规则匹配 === *<span style="color: red;">'''matchRuleName'''</span>必须要先添加 *rankScore对应匹配规则中的分数,根据实际开发的业务数据来,分数会对应到详细的段位规则里 *<span style="color: red;">'''ExtraData'''</span>是透传参数,可以在匹配成功进入战斗服中获取到 <pre> 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 }); } </pre> === 取消匹配 === <pre> 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 }); } </pre> === 匹配状态变化 === 开始匹配和取消匹配都会通过回调来通知给hall, 可以根据开始时间,来处理倒计时等显示的逻辑 <pre> // 绑定 this.hall.onUserMatchChange = this.onUserMatchChange.bind(this); </pre> <pre> 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); } </pre> === 匹配成功 === 匹配成功后会进入战斗服务器,host,上面的完整流程已经提过 并且会收到匹配时传入的透传参数<span style="color: red;">'''ExtraData'''</span> <pre> this.host.onUserMatchExtraDataReceived = this.onUserMatchExtraDataReceived.bind(this); </pre> == 开房间 == 开房间功能建议在试玩链接里体验,目前房间列表只在试玩里开放 === 接入开房间组件 === 参考demo中HallUI的代码,一行即可,在点击创建房间按钮后触发 <pre> XPopManager.instance.advancedComponentMgr.addComponent("CreateRoomComponent"); </pre> ====房主在大厅内点击创建房间==== * [[File:创建房间.png]] 创建房间可以选择地图,具体能选择的地图需要在审核后台配置 ====其他人加入房间==== 1.可以在房间界面内,接入邀请好友组件,拉取好友或者微信好友加入房间 <pre> /** * 组件--邀请 */ private Component_Invitation(): void { let invitation = XPopManager.instance.advancedComponentMgr.addComponent('InvitationComponent'); invitation.top = 60; invitation.right = 10; invitation.left = null; invitation.panel.top = 120; invitation.panel.bottom = null; invitation.panel.right = 0; invitation.panel.bangAdapter = true; invitation.panel.tweenType = 'Right'; } </pre> 2.其他人可以通过搜索房间列表加入房间 * [[File:创建房间2.png]] * [[File:创建房间3.png]] === 房间内显示逻辑 === *<span style="color: red;">'''强调一个概念,开房间组件,相当于是在大厅里,忽略了匹配过程,直接把玩家拉到了host里面'''</span> *<span style="color: red;">'''被拉进该房间的玩家,会完整的处理进入hall以及host的各种流程,也就是说所有生命周期回调都会有反应,一定是先进hall再进host'''</span> *<span style="color: red;">'''在房主创建房间进入时,以及其他人在大厅找到房间进入时,都会触发host的onUserEnter'''</span> * 匹配时,我们一直在强调匹配规则的key,在进入host时,我们可以根据是否存在匹配规则的key,来判断是匹配进入,还是房间组件进入 <pre> let ruleName: string = this.host.GetMatchRuleName(); </pre> * [[File:判断战斗快照.png]] *此时,界面内的显示需要根据host是否是房间状态来处理 -- 房间状态就显示各种座位的人员信息 -- 战斗状态就进入战斗逻辑(其实都是战斗服务器里的逻辑) -- 具体的状态管理可以参考demo代码,由开发者自己写状态机 == 启动本地大厅 == 本地调试时,登录界面,勾选本地大厅即可 * [[File:勾选本地大厅.png]] 只要勾选一个本地大厅就可以了,另一个账号登录时也会进入此大厅
返回
大厅功能
。
开关有限宽度模式