<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans-CN">
	<id>https://wiki.popx.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Liuweichen</id>
	<title>泡泡 - 用户贡献 [zh-cn]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.popx.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Liuweichen"/>
	<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php/%E7%89%B9%E6%AE%8A:%E7%94%A8%E6%88%B7%E8%B4%A1%E7%8C%AE/Liuweichen"/>
	<updated>2026-05-07T02:29:31Z</updated>
	<subtitle>用户贡献</subtitle>
	<generator>MediaWiki 1.42.1</generator>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=600</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=600"/>
		<updated>2025-03-24T07:27:51Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 打包与上传 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭下载模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E4%B8%8B%E8%BD%BD%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭运行模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E8%BF%90%E8%A1%8C%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;学会查看远端json文件地址&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
==== npm i ====  &lt;br /&gt;
项目工程中，npm i，安装依赖&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 上传|测试|提审|上线 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
==== 网页试玩链接 ====&lt;br /&gt;
&lt;br /&gt;
固定试玩链接为&lt;br /&gt;
&lt;br /&gt;
[试玩链接](https://subgame.popx.com/popion/opendev.html)&lt;br /&gt;
&lt;br /&gt;
随意输入账号密码登录，在游戏列表中找到游戏试玩。&lt;br /&gt;
&lt;br /&gt;
==== 真机测试 ====&lt;br /&gt;
&lt;br /&gt;
点击泡泡管理后台-&amp;gt;地图编辑&lt;br /&gt;
&lt;br /&gt;
游戏中选择版本管理-&amp;gt;点击提审&lt;br /&gt;
&lt;br /&gt;
过审后，就可以在 快手提审服 进行测试，测试资格可以和各自项目负责人对齐。&lt;br /&gt;
&lt;br /&gt;
注意：如果是竖屏游戏，需要用APP版本，进入 提审服 进行真机测试。&lt;br /&gt;
&lt;br /&gt;
=== 游戏上线发布 ===&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|过审后查看文档中发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=599</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=599"/>
		<updated>2025-03-24T07:26:58Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 玩法测试 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭下载模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E4%B8%8B%E8%BD%BD%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭运行模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E8%BF%90%E8%A1%8C%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;学会查看远端json文件地址&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
==== npm i ====  &lt;br /&gt;
项目工程中，npm i，安装依赖&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
==== 网页试玩链接 ====&lt;br /&gt;
&lt;br /&gt;
固定试玩链接为&lt;br /&gt;
&lt;br /&gt;
[试玩链接](https://subgame.popx.com/popion/opendev.html)&lt;br /&gt;
&lt;br /&gt;
随意输入账号密码登录，在游戏列表中找到游戏试玩。&lt;br /&gt;
&lt;br /&gt;
==== 真机测试 ====&lt;br /&gt;
&lt;br /&gt;
点击泡泡管理后台-&amp;gt;地图编辑&lt;br /&gt;
&lt;br /&gt;
游戏中选择版本管理-&amp;gt;点击提审&lt;br /&gt;
&lt;br /&gt;
过审后，就可以在 快手提审服 进行测试，测试资格可以和各自项目负责人对齐。&lt;br /&gt;
&lt;br /&gt;
注意：如果是竖屏游戏，需要用APP版本，进入 提审服 进行真机测试。&lt;br /&gt;
&lt;br /&gt;
=== 游戏上线发布 ===&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|过审后查看文档中发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=598</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=598"/>
		<updated>2025-03-17T08:58:51Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 修改host文件 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭下载模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E4%B8%8B%E8%BD%BD%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭运行模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E8%BF%90%E8%A1%8C%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;学会查看远端json文件地址&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
==== npm i ====  &lt;br /&gt;
项目工程中，npm i，安装依赖&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=597</id>
		<title>大厅功能</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=597"/>
		<updated>2025-03-12T07:18:58Z</updated>

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

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

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

		<summary type="html">&lt;p&gt;Liuweichen：​/* 打开泡饭体验项目 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭下载模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E4%B8%8B%E8%BD%BD%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
参考视频泡饭运行模板[https://formalhead.popx.com/popxVedio/paofanoperate/%E6%B3%A1%E9%A5%AD%E8%BF%90%E8%A1%8C%E6%A8%A1%E6%9D%BF.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;学会查看远端json文件地址&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=593</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=593"/>
		<updated>2025-03-06T09:39:45Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* sdkVersion&amp;gt;1.2.60的版本 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;学会查看远端json文件地址&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=592</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=592"/>
		<updated>2025-03-06T09:39:23Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* sdkVersion&amp;gt;1.2.60的版本 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
查看pacakge.json&lt;br /&gt;
&lt;br /&gt;
*查看当前的远端资源版本，比如&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
*具体有哪些资源版本可用如下命令&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
*如果用的泡饭工坊，直接在配制节目选择下拉框即可查看&lt;br /&gt;
&lt;br /&gt;
*把下面链接里的v1.0.15替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;确认好resVersion和远程链接&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;在上述json文件中，可以搜索文件名，来判断资源在不在美术包里&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=591</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=591"/>
		<updated>2025-03-06T09:35:48Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 有sdkVersion */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E4%B8%8A%E4%BC%A0%E6%B5%81%E7%A8%8B&amp;diff=590</id>
		<title>上传流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E4%B8%8A%E4%BC%A0%E6%B5%81%E7%A8%8B&amp;diff=590"/>
		<updated>2025-03-05T06:19:18Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 进入泡饭连接 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== 开发者游戏玩法审核地址 ===  &lt;br /&gt;
==== 进入泡饭连接 ====&lt;br /&gt;
&lt;br /&gt;
* https://webdev.popx.com&lt;br /&gt;
&lt;br /&gt;
==== 老的账号已经兼容 可直接登录 ====&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;已有账号直接登录，无需再注册&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 点击工具跳转后台 ====&lt;br /&gt;
&lt;br /&gt;
[[File:工具内跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;必须要有gameid才能运行游戏的正确流程&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ===&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请先创建游戏，绑定至少一张地图&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ===&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;首次创建玩法后，代码工程必须先publish一次，才可以调试&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ===&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;后续地图如果有改动，代码工程必须要publish，才能加载到新地图&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ===&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;只要publish就可以试玩，[试玩链接](https://subgame.popx.com/popion/opendev.html)&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ===&lt;br /&gt;
&lt;br /&gt;
= 上传流程 =  &lt;br /&gt;
== 登录账号 ==&lt;br /&gt;
* 开发者需要再网页端输入自己的账号和密码，和以前地编类似，找我们内部申请&lt;br /&gt;
* sdk版本填写和你项目中package.json一致，相当于美术资源的版本，比如 项目中&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;，就填写1.0.15&lt;br /&gt;
&lt;br /&gt;
== 新建一个游戏项目 ==&lt;br /&gt;
* 登录成功后，可在界面右上角选择“新建游戏”&lt;br /&gt;
* [[File:新建游戏.png]]&lt;br /&gt;
* 新建游戏时，需要先输入游戏项目的名称，(后续可随时修改)&lt;br /&gt;
* [[File:新建游戏名.png]]&lt;br /&gt;
* 新建完成后，可在游戏列表内找到刚刚新建的项目,点击“游戏信息”，可查看或修改游戏信息&lt;br /&gt;
* [[File:点击游戏信息.png]]&lt;br /&gt;
* 在游戏信息界面中，可修改诸多属性，项目图标必传，项目标签随意选，可以选两个，(这些后续都可随时修改)&lt;br /&gt;
&lt;br /&gt;
其中gameid和密钥是需要在package.json中记录的&lt;br /&gt;
&lt;br /&gt;
如下是创建游戏后生成的gameid和密钥&lt;br /&gt;
&lt;br /&gt;
* [[File:生成游戏id.png]]&lt;br /&gt;
&lt;br /&gt;
如下是需要修改项目中package.json的部分&lt;br /&gt;
&lt;br /&gt;
* [[File:gameid和密钥.png]]&lt;br /&gt;
&lt;br /&gt;
* 最下方不要勾选模式，除非后期接入了大厅&lt;br /&gt;
&lt;br /&gt;
* [[File:不要去勾选.png]]&lt;br /&gt;
&lt;br /&gt;
* 如果勾选了大厅会有如下报错&lt;br /&gt;
&lt;br /&gt;
* [[File:勾选大厅会有报错.png]]&lt;br /&gt;
&lt;br /&gt;
== 编辑地图 ==&lt;br /&gt;
&lt;br /&gt;
* [[File:编辑地图.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;玩法里必须绑定至少一张地图，所以先创建一个空的也行&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
具体的地图怎么编辑，可以看demo目录下的地图编辑器目录，里面有视频。&lt;br /&gt;
&lt;br /&gt;
== 玩法选择地图 == &lt;br /&gt;
&lt;br /&gt;
在地图编辑器里添加完地图后，退出来，在&#039;&#039;&#039;游戏信息界面&#039;&#039;&#039;可以选择地图&lt;br /&gt;
&lt;br /&gt;
* [[File:可用地图.png]]&lt;br /&gt;
&lt;br /&gt;
所有的自己编辑的地图都可以选择。&lt;br /&gt;
&lt;br /&gt;
=== 选择已有地图 ===&lt;br /&gt;
* [[File:自定义地图名.png]]&lt;br /&gt;
&lt;br /&gt;
选择一张地图后，会发现有如上图中的名称格式，&lt;br /&gt;
&lt;br /&gt;
=== 地图名 ===&lt;br /&gt;
&#039;&#039;&#039;原始地图名&#039;&#039;&#039;/&#039;&#039;&#039;自定义地图名&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
比如图中自定义地图名是&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么在自己本地测试时，网页上地图就需要填写&amp;quot;地图_0&amp;quot;，就可以进入该地图。&lt;br /&gt;
&lt;br /&gt;
自定义地图名可以修改。&lt;br /&gt;
&lt;br /&gt;
=== 多张地图 ===&lt;br /&gt;
可以添加多张地图，每一张添加的地图，都可以在测试时填写自定义地图名来加载到。&lt;br /&gt;
&lt;br /&gt;
试玩时，也可以在玩法里点击游戏，选择地图再进入。&lt;br /&gt;
&lt;br /&gt;
但是没有被添加进玩法的地图，不能被加载到。&lt;br /&gt;
&lt;br /&gt;
相当于每个gameid和它下面的地图是强绑定的。&lt;br /&gt;
&lt;br /&gt;
== 本地上传 ==&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;首次创建玩法后，必须先上传一次，才可以调试&amp;lt;/span&amp;gt;&lt;br /&gt;
* 若想上传代码，需要在本地执行&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;npm run publish&amp;lt;/span&amp;gt;(确保package.json中的gameid和密钥，与刚刚创建的游戏项目中的一致)&lt;br /&gt;
* [[File:Publish.png]]&lt;br /&gt;
&lt;br /&gt;
== 试玩 ==&lt;br /&gt;
* publish完成后，即可在版本管理中，看到自己的版本&lt;br /&gt;
&lt;br /&gt;
* 有个最新资源的链接地址，publish之后就可以直接玩&lt;br /&gt;
[试玩链接]( https://subgame.popx.com/popion/opendev.html)&lt;br /&gt;
* 如果你点击试玩按钮，试玩用开发服，如果是上述链接进来的，默认就是开发服&lt;br /&gt;
* [[File:选择开发服.png]]&lt;br /&gt;
* 选择服务器后，登录时可填写任意账号，登录成功后同时算作注册成功&lt;br /&gt;
&lt;br /&gt;
* 如果是新号，在PC端ctrl +6，打开特殊窗口，输入 “isover 1”,点击按钮，可直接跳过新手流程!&lt;br /&gt;
* [[File:跳过引导.png]]&lt;br /&gt;
&lt;br /&gt;
* 在游戏界面游玩中，找到你的玩法，点击进入即可&lt;br /&gt;
&lt;br /&gt;
* [[File:找到自己玩法.png]]&lt;br /&gt;
&lt;br /&gt;
* 想要其他人一起游玩，可以复制上面的链接，点击进入测试&lt;br /&gt;
&lt;br /&gt;
== 提审 ==&lt;br /&gt;
* publish成功后，在网页端即可看到开发版本，点击&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;提审&amp;lt;/span&amp;gt;，即可申请提审&lt;br /&gt;
* [[File:开发版本.png]]&lt;br /&gt;
* [[File:提审备注.png]]&lt;br /&gt;
* 提审后，可看到版本“待审核”字样，即表示已经成功提交申请，此时等待审核方完成审核即可&lt;br /&gt;
* [[File:待审核.png]]&lt;br /&gt;
* 审核通过后，可在游戏列表内看到绿色字样&lt;br /&gt;
* [[File:游戏列表内_审核通过.png]]&lt;br /&gt;
* 版本管理内也能看到审核通过的提示，此时可进行版本的发布&lt;br /&gt;
* [[File:版本发布.png]]&lt;br /&gt;
&lt;br /&gt;
== 发布 ==&lt;br /&gt;
* 选择需要发布的渠道&lt;br /&gt;
** 正式版本包括：&lt;br /&gt;
*** 1)泡泡先锋APP&lt;br /&gt;
*** 2)微信小游戏&lt;br /&gt;
*** 3)抖音小游戏&lt;br /&gt;
** 先行版本包括：&lt;br /&gt;
*** 1)快手小游戏&lt;br /&gt;
** &amp;lt;span style=&amp;quot;color: grey;&amp;quot;&amp;gt;海外版本包括：(暂未实装)&amp;lt;/span&amp;gt;&lt;br /&gt;
*** &amp;lt;span style=&amp;quot;color: grey;&amp;quot;&amp;gt;1)泡泡先锋海外版APP(暂未实装)&amp;lt;/span&amp;gt;&lt;br /&gt;
* 每个发布成功的渠道，都会有一个管理板块，可供开发者执行&lt;br /&gt;
** 1)版本回退：能够回退到上一个版本&lt;br /&gt;
** &amp;lt;span style=&amp;quot;color: grey;&amp;quot;&amp;gt;2)跨服发布(暂未实装)&amp;lt;/span&amp;gt;&lt;br /&gt;
** 1)下线维护：游戏下线，暂时对玩家不可见&lt;br /&gt;
[[File:版本管理三个按钮.png]]&lt;br /&gt;
&lt;br /&gt;
= 常见概念问题 =&lt;br /&gt;
&lt;br /&gt;
*- 编辑器中的“游戏”，里面有“玩法地图”和gameid，分别是什么意思？&lt;br /&gt;
&lt;br /&gt;
你可以理解为&amp;quot;游戏&amp;quot;对应的就是外部开发者的一个整体玩法，比如推箱子，推箱子的gameid就是固定的&amp;quot;1111&amp;quot;(举例)，gameid和游戏玩法强制一对一绑定。&lt;br /&gt;
&lt;br /&gt;
*- 程序package.json中，有gameSecretkey和gameid，什么意思？&lt;br /&gt;
&lt;br /&gt;
开发者最后开发完玩法，需要手动调用命令上传到远端指定的目录。gameid如上述和玩法强绑定，gameSecretkey对应gameid的密码，这俩都需要上传前填写。&lt;br /&gt;
&lt;br /&gt;
正确填写上传后，在玩法审核工具里就能看到上传成功。&lt;br /&gt;
&lt;br /&gt;
上传完成后，就已经能试玩了。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*- 编辑器中的“地图”，哪些是可用地图？&lt;br /&gt;
地图编辑器登录时会有个账号，比如账号名字User1，你通过这个账号，登录地编的某个服务器比如测试服，在这里制作上传了地图。或者其他人共享给了你地图，这两部分地图就是你账号的可用地图。&lt;br /&gt;
添加玩法地图时就能选到你地编里的可用地图。&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=589</id>
		<title>大厅功能</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=589"/>
		<updated>2025-02-28T05:57:43Z</updated>

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

		<summary type="html">&lt;p&gt;Liuweichen：​/* 常见工作流问题 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 注意切换分支 ====&lt;br /&gt;
&lt;br /&gt;
注意，如果是从已有git创建的分支，泡饭默认会用main分支&lt;br /&gt;
&lt;br /&gt;
请在编辑模式里，vscode，手动切换分支&lt;br /&gt;
&lt;br /&gt;
==== 如何分享泡饭链接 ====&lt;br /&gt;
&lt;br /&gt;
左侧页签我的工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
点击打开工程-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
左侧页签分享游戏-&amp;gt;&lt;br /&gt;
&lt;br /&gt;
服务器模式&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=587</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=587"/>
		<updated>2025-02-20T10:40:22Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 泡饭实用工具 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
泡饭内置了实用的工具，方便外部使用，提高开发者效率&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
&lt;br /&gt;
模型查看器会拉取最新的资源版本号，显示在左上角，&lt;br /&gt;
&lt;br /&gt;
可以比较方便的查看模型，特效，动作等等的资源&lt;br /&gt;
&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
&lt;br /&gt;
接入了deepseek-r1&lt;br /&gt;
&lt;br /&gt;
泡饭账号可以直接登录，可以体验对话&lt;br /&gt;
&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
&lt;br /&gt;
可以生成长的，短的音频文件&lt;br /&gt;
&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
可以上传2d图片，去生成glb格式和高斯格式的3d模型。&lt;br /&gt;
&lt;br /&gt;
下载后，glb能通过blender去导入，转化成可用的fbx文件。&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=586</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=586"/>
		<updated>2025-02-20T10:15:47Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 没账号的需要在泡饭注册账号 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
点击左上角个人信息&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
点击注册&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
注册页面&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E7%82%B9%E5%87%BB%E6%B3%A8%E5%86%8C.png&amp;diff=585</id>
		<title>文件:泡饭点击注册.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E7%82%B9%E5%87%BB%E6%B3%A8%E5%86%8C.png&amp;diff=585"/>
		<updated>2025-02-20T09:28:07Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=584</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=584"/>
		<updated>2025-02-20T09:27:55Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 没账号的需要在泡饭注册账号 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭未登录.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击注册.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E6%9C%AA%E7%99%BB%E5%BD%95.png&amp;diff=583</id>
		<title>文件:泡饭未登录.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E6%9C%AA%E7%99%BB%E5%BD%95.png&amp;diff=583"/>
		<updated>2025-02-20T09:26:34Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%85%B7.png&amp;diff=582</id>
		<title>文件:泡饭工具.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%85%B7.png&amp;diff=582"/>
		<updated>2025-02-20T09:25:15Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=581</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=581"/>
		<updated>2025-02-20T09:25:01Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 常见工作流问题 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 泡饭实用工具 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭工具.png]]&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
=== AI对话 ===&lt;br /&gt;
=== AI语音生成 ===&lt;br /&gt;
=== AI图转3d模型 ===&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=580</id>
		<title>大厅功能</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=580"/>
		<updated>2025-02-19T10:09:47Z</updated>

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

		<summary type="html">&lt;p&gt;Liuweichen：​/* 项目demo地址 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 老项目升级环境配置 ==&lt;br /&gt;
=== 项目demo地址 ===&lt;br /&gt;
泡饭中，hall_test模板，有完整代码&lt;br /&gt;
&lt;br /&gt;
* [[File:泡饭大厅工程.png]]&lt;br /&gt;
&lt;br /&gt;
下载模板&lt;br /&gt;
&lt;br /&gt;
我的工程打开hall_test模板&lt;br /&gt;
&lt;br /&gt;
编译&amp;amp;启动(大厅)&lt;br /&gt;
&lt;br /&gt;
=== demo体验地址 ===&lt;br /&gt;
&lt;br /&gt;
[试玩链接]( https://subgame.popx.com/popion/master.html)&lt;br /&gt;
&lt;br /&gt;
* 如果是新号，在PC端ctrl +6，打开特殊窗口，输入 “isover 1”,点击按钮，可直接跳过新手流程!&lt;br /&gt;
* [[File:跳过引导.png]]&lt;br /&gt;
&lt;br /&gt;
* 在游戏界面游玩中，找到demox，点击进入即可&lt;br /&gt;
* [[File:Demo地址.png]]&lt;br /&gt;
=== 添加src/hall/index.ts的大厅服务器入口文件 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:添加hall入口.png]]&lt;br /&gt;
&lt;br /&gt;
=== 修改打包配置rollup ===&lt;br /&gt;
&lt;br /&gt;
* rollup.config.js新增打包大厅index.js的配置&lt;br /&gt;
&lt;br /&gt;
* [[File:rollup配置.png]]&lt;br /&gt;
&lt;br /&gt;
* rollup.config.js加一下服务类的白名单&lt;br /&gt;
&lt;br /&gt;
* [[File:rollup白名单.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 修改html启动文件 ===&lt;br /&gt;
&lt;br /&gt;
* 拷贝覆盖index.html到你的项目中，拷贝hall.html本地大厅文件到同一文件夹下&lt;br /&gt;
&lt;br /&gt;
* [[File:html入口.png]]&lt;br /&gt;
&lt;br /&gt;
=== 审核后台勾选大厅 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:勾选大厅.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;只有勾选了大厅后，登录流程才会走大厅的逻辑，否则是走的老的流程，直接进战斗房间&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 大厅游戏工作流说明 ==&lt;br /&gt;
大厅工程相对于原来的client，server目录结构，多了一个hall目录，而且server实际上指的是host，&lt;br /&gt;
&lt;br /&gt;
每个目录的代码文件入口都在各自的&#039;&#039;&#039;index.ts&#039;&#039;&#039;里&lt;br /&gt;
&lt;br /&gt;
[[File:大厅文件结构.png]]&lt;br /&gt;
&lt;br /&gt;
为了方便大家理解，我们用颜色来标记各个核心文件夹的工作流&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client&#039;&#039;&#039;&amp;lt;/span&amp;gt;用橙色，代表前端显示，前端逻辑&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall&#039;&#039;&#039;&amp;lt;/span&amp;gt;用绿色，代表大厅，大厅服务器逻辑&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host&#039;&#039;&#039;&amp;lt;/span&amp;gt;用蓝色，注意，为了不和开房间混淆，host代表的是战斗服务器逻辑，而不是和以前一样叫做房间&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;client的入口代码最早执行，在里面做各种监听，也就是说，大厅期间，各种前端界面UI就已经能自己处理了，可以调用大厅的数据&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;以下的流程，如果审核后台勾选了大厅，按照顺序执行，否则直接跳过大厅，到了《进入战斗》&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 游戏开始时 ===  &lt;br /&gt;
&lt;br /&gt;
==== 进大厅前预加载资源 ====  &lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHallWait  &lt;br /&gt;
  &lt;br /&gt;
--在进入大厅前，会有一个promise的异步接口供开发者使用去预加载资源，比如表格，比如读取文件配置等等。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;必须在加载完成的回调中resolve&#039;&#039;&#039;&amp;lt;/span&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
//预加载资源  &lt;br /&gt;
XPopManager.onHallWait =  new Promise((resolve)=&amp;gt;{  &lt;br /&gt;
    GameManager.instance.onPreLoadRes(()=&amp;gt;{  &lt;br /&gt;
        resolve(&amp;quot;Resource loaded successfully hall&amp;quot;);  &lt;br /&gt;
    });  &lt;br /&gt;
});  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
==== 进入大厅 ====  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHallStart  &lt;br /&gt;
  &lt;br /&gt;
--在大厅资源预加载完成后，会直接启动大厅服务器实例  &lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHostHallStart  &lt;br /&gt;
  &lt;br /&gt;
--src/hall/index.ts 中监听的代码入口&lt;br /&gt;
&lt;br /&gt;
=== 大厅期间操作 ===  &lt;br /&gt;
  &lt;br /&gt;
==== 监听大厅内数据变化 ====  &lt;br /&gt;
大厅期间，hall的实例会监听各种玩家的事件，并且抛出回调，开发者可以在回调中处理hall的快照数据  &lt;br /&gt;
  &lt;br /&gt;
hall的实例是指XPopManager.instance.xHostHall  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserEnter  &lt;br /&gt;
  &lt;br /&gt;
--大厅内有玩家进入  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserLeave  &lt;br /&gt;
  &lt;br /&gt;
--大厅内有玩家离开  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserMatchChange  &lt;br /&gt;
  &lt;br /&gt;
--大厅内匹配信息发生变化，主要是指倒计时的变化  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onDbItemChange  &lt;br /&gt;
  &lt;br /&gt;
--大厅内存盘的数据变化，在进入时就会调用一次，获取到账号DB数据，&#039;&#039;&#039;存盘&#039;&#039;&#039;后面再说&lt;br /&gt;
&lt;br /&gt;
==== 开始匹配 ====  &lt;br /&gt;
  &lt;br /&gt;
匹配内容后面单独讲  &lt;br /&gt;
  &lt;br /&gt;
==== 匹配成功 ====  &lt;br /&gt;
后面匹配内容会单独讲，  &lt;br /&gt;
匹配成功后，分配到了某个房间的实例（host）,并且会把匹配时的透传参数通知给host实例  &lt;br /&gt;
  &lt;br /&gt;
=== 进入战斗 ===  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHostStart  &lt;br /&gt;
--src/server/index.ts 中监听的代码入口，去处理host实例的各种回调  &lt;br /&gt;
  &lt;br /&gt;
=== 战斗期间操作 ===  &lt;br /&gt;
==== 监听host战斗服内数据变化 ====  &lt;br /&gt;
host的实例是指XPopManager.instance.xHost&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserEnter&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器内有玩家进入&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserLeave&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器内有玩家离开&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserMatchExtraDataReceived&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器收到玩家透传信息(匹配时传入的ExtraData)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onDbItemChange  &lt;br /&gt;
  &lt;br /&gt;
--当战斗服务器收到存盘的数据变化，在进入时就会调用一次，获取到账号DB数据，&#039;&#039;&#039;存盘&#039;&#039;&#039;后面再说&lt;br /&gt;
&lt;br /&gt;
=== 战斗返回大厅 ===  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onGameLeave  &lt;br /&gt;
  &lt;br /&gt;
--返回到大厅，大厅的数据还在快照中，可以做一些结算的处理&lt;br /&gt;
&lt;br /&gt;
想要回到大厅，需要手动调用&#039;&#039;&#039;XPopManager.instance.xRemote.LeaveGame &#039;&#039;&#039;&lt;br /&gt;
，demo里有示例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XPopManager.instance.xRemote.LeaveGame(this, (code: number, ret: string) =&amp;gt; {&lt;br /&gt;
            console.log(&#039;LeaveGame&#039;, code, ret);&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 离开整个游戏 ===  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onExit&lt;br /&gt;
&lt;br /&gt;
--在试玩的时候，点击官方左上角的退出，会进入此回调函数&lt;br /&gt;
&lt;br /&gt;
--平时本地调试开发时，不需要去处理这个回调&lt;br /&gt;
&lt;br /&gt;
--但是试玩时需要在此销毁场内资源，否则，退出后再次进入，会发现上一回的模型还在&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onExit = () =&amp;gt; {&lt;br /&gt;
    model.destroy();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 ==&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;交互只能client和hall,client和host之间互通信息，不能hall和host直接交互&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
=== client和hall的交互 ===&lt;br /&gt;
所有的和大厅的数据交互，依赖于&lt;br /&gt;
&lt;br /&gt;
client内部代码：this._remoteHall = XPopManager.instance.xRemote.hall&lt;br /&gt;
&lt;br /&gt;
hall内部代码：this.hall = XPopManager.instance.xHostHall&lt;br /&gt;
&lt;br /&gt;
具体代码位置在各自文件夹内&lt;br /&gt;
&lt;br /&gt;
*1.client向hall注册快照，并且监听快照变化&lt;br /&gt;
*2.hall注册rcp请求的回调方法&lt;br /&gt;
*3.client向hall发起rpc请求&lt;br /&gt;
*4.hall处理rpc请求，hall修改快照&lt;br /&gt;
*5.client监听快照变化的回调，处理显示逻辑&lt;br /&gt;
&lt;br /&gt;
具体可以查看MessageManager中的代码&lt;br /&gt;
&lt;br /&gt;
==== client注册快照 ====&lt;br /&gt;
&lt;br /&gt;
代码里有私有和共有快照，实际上本质上都是最基本的注册快照，只是在hall里面处理数据的方式不一样，具体可以看代码，不再赘述&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this._remoteHall.RegisterSnapshot(`${Snap_HallData.tag}`, this._hallData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== client监听快照变化 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
       this._hallData.notice.OnSet = (bReset) =&amp;gt; {&lt;br /&gt;
            XTipManager.Instance.showTip(this._hallData.notice.value);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== hall注册rpc回调 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this.hall.RegisterRpc(&#039;AddDbItem&#039;, this.onReceiveAddDbItem.bind(this));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== client发起rpc请求 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public async Hall_Request_AddItem(type:DBItemType, itemID:ItemNormalID, itemNum:number, callback?: Function): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        let ret = await this._remoteHall.Request(&amp;quot;AddDbItem&amp;quot;, { type: type, actorID: this._userID, itemID: itemID, itemNum: itemNum });&lt;br /&gt;
        console.log(&#039;requestAddDbItem&#039;, ret);&lt;br /&gt;
        callback &amp;amp;&amp;amp; callback(ret);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== hall处理rpc请求，修改快照 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
private async onReceiveAddDbItem(call: ApiCall&amp;lt;ReqAddDbItem, ResAddDbItem&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        // 修改某个快照，前端已经注册过处理函数&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== client和host的交互 ===&lt;br /&gt;
所有的和战斗服的数据交互，依赖于&lt;br /&gt;
&lt;br /&gt;
client内部代码：this._remoteBattle = XPopManager.instance.xRemote.game;&lt;br /&gt;
&lt;br /&gt;
host内部代码（server文件夹）：this.host = XPopManager.instance.xHost;&lt;br /&gt;
&lt;br /&gt;
所有的流程和hall一样处理&lt;br /&gt;
&lt;br /&gt;
== 数据存盘 ==&lt;br /&gt;
数据存盘对应的是玩家登录时的账号id，调用存盘后，就有账号的长期数据可以记录和读取了&lt;br /&gt;
&lt;br /&gt;
=== 存储 ===&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;存储只能由hall或者host发起，两个服务器都有同名的存储接口&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;该功能没有必要接入大厅，直接调用host中的代码接口也可以存储和读取&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;调用该功能前，请务必根据自身gameid向我们内部申请一个对应的数字id来存盘使用&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
demo中，写在了hall里面，host里也可以写一份一样的，这样战斗和大厅的数据就都能存储了.&lt;br /&gt;
&lt;br /&gt;
如下示例代码的发起方是client通过rpc去通知hall发起的，实际上可以在服务器计算过程中自己发起&lt;br /&gt;
&lt;br /&gt;
说几个关键的参数：&lt;br /&gt;
&lt;br /&gt;
*dbID&lt;br /&gt;
&lt;br /&gt;
-- 需要开发者在我们的审核后台，获取了游戏id之后，把游戏id的字符串给我们的内部人员，&lt;br /&gt;
-- 然后我们帮开发者映射生成一个数字的id，专门来存储该游戏的数据，&lt;br /&gt;
-- 以后存盘读盘时就走的这个数字id&lt;br /&gt;
&lt;br /&gt;
*type&lt;br /&gt;
&lt;br /&gt;
-- 存盘的时候还有个type，支持填写0到9，可以用于开发者处理不同类型的数据分类。&lt;br /&gt;
-- 比如0代表货币类型，1代表背包道具，2代表头像。。等等&lt;br /&gt;
-- 当然，全存在一个type里也没影响，因为可以根据唯一的itemID去读自己的表格&lt;br /&gt;
&lt;br /&gt;
*itemID&lt;br /&gt;
&lt;br /&gt;
-- 开发者存储时的自定义id，用于自己游戏内的道具表格映射，请保持id的唯一性，不要有同id的道具&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private async onReceiveAddDbItem(call: ApiCall&amp;lt;ReqAddDbItem, ResAddDbItem&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        let userdata = this._usersMap.get(call.req.actorID);&lt;br /&gt;
        if (!userdata) {&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        if(call.req.type == DBItemType.Normal &amp;amp;&amp;amp; call.req.itemID == ItemNormalID.Gold){&lt;br /&gt;
            let ret;&lt;br /&gt;
            if(userdata.goldCount.value == -1){&lt;br /&gt;
                let item = new XDbItem();&lt;br /&gt;
                item.nID = call.req.itemID;&lt;br /&gt;
                item.nCount = call.req.itemNum;&lt;br /&gt;
                ret = await this.hall.AddDbItem(this.dbID, call.req.actorID, call.req.type, call.req.itemID, item, this);&lt;br /&gt;
            }else{&lt;br /&gt;
                ret = await this.hall.SetDbItemCount(this.dbID, call.req.actorID, call.req.type, call.req.itemID, call.req.itemNum+userdata.goldCount.value, this);&lt;br /&gt;
            }&lt;br /&gt;
            return call.succ({&lt;br /&gt;
                ret: ret.ret&lt;br /&gt;
            });&lt;br /&gt;
        }else{&lt;br /&gt;
            return call.succ({&lt;br /&gt;
                ret: &#039;未实现&#039;&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 读取 ===&lt;br /&gt;
&lt;br /&gt;
在hall和host中，都有一个监听函数&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.xHostHall.onDbItemChange;&lt;br /&gt;
XPopManager.instance.xHost.onDbItemChange;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次当存盘的数据发生变化，都会进入该回调函数。&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;hall和host连接成功后，也会先进入一次，所以进游戏就能获得存盘数据&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
我们以hall里的代码举例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   private onDbItemChange(type: number, key: number, value: XDbItem | null, iUserID: number, bReset: boolean):void{&lt;br /&gt;
        if(type == DBItemType.Normal &amp;amp;&amp;amp; key == ItemNormalID.Gold){&lt;br /&gt;
            let userdata = this._usersMap.get(iUserID);&lt;br /&gt;
            if (!userdata) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            userdata.goldCount.value = value.nCount;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*type&lt;br /&gt;
-对应传入的type&lt;br /&gt;
*key&lt;br /&gt;
—对应存储的itemId&lt;br /&gt;
*value: XDbItem&lt;br /&gt;
&lt;br /&gt;
其中，XDbItem的结构是我们DB中固定的格式，外部只需要关心nID和nCount即可&lt;br /&gt;
&lt;br /&gt;
nID对应存储时传入的唯一itemId&lt;br /&gt;
nCount对应DB中该物品的数量&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* [[File:db存储结构.png]]&lt;br /&gt;
&lt;br /&gt;
=== KeyValue数据存盘 ===&lt;br /&gt;
简单的物品类型并不足以支持复杂的游戏数据结构，我们也提供了和物品存储类似的数据结构存储&lt;br /&gt;
&lt;br /&gt;
同样是在hall和host中都可以调用&lt;br /&gt;
&lt;br /&gt;
监听的函数用&#039;&#039;&#039;onDbDataChange&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.xHostHall.onDbDataChange;&lt;br /&gt;
XPopManager.instance.xHost.onDbDataChange;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
存储的函数用SetDbDatas&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// 存储设置自定义key-value&lt;br /&gt;
ret = await this.hall.SetDbData()&lt;br /&gt;
// 存储整个map的keyvalue合集&lt;br /&gt;
ret = await this.hall.SetDbDatas()&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* [[File:存盘复杂数据.png]]&lt;br /&gt;
&lt;br /&gt;
如函数参数中所示，存储的value是string类型，&lt;br /&gt;
&lt;br /&gt;
那么可以通过JSON.stringify(obj)和JSON.parse(str)来相互转换，从而存储复杂数据&lt;br /&gt;
&lt;br /&gt;
* 注意:如上是hall的存储读取示例，如果想用host，接口类似，在如下的论坛帖子里有演示。&lt;br /&gt;
&lt;br /&gt;
[host存储数据](https://bbs.popx.com/t/keyvalue-ondbdatachange/207/14?u=qc_jump)&lt;br /&gt;
&lt;br /&gt;
== 匹配逻辑 ==&lt;br /&gt;
&lt;br /&gt;
匹配逻辑在hall中处理&lt;br /&gt;
&lt;br /&gt;
=== 创建匹配规则 ===&lt;br /&gt;
&lt;br /&gt;
匹配前，一定要创建至少一个匹配规则的名称，否则无法匹配&lt;br /&gt;
&lt;br /&gt;
参考demo中，在hall代码的初始阶段就要设置&lt;br /&gt;
&lt;br /&gt;
如下是添加了更加详细的注释代码，其中段位相关的规则可以先不添加&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * 初始化匹配规则&lt;br /&gt;
     */&lt;br /&gt;
    private initMatchRule(): void {&lt;br /&gt;
        let rules = new XMatchRule();&lt;br /&gt;
        // 需求人数	必填&lt;br /&gt;
        rules.nRoleNum = Config.MatchNumMax;&lt;br /&gt;
        // 地图列表 - 匹配成功后，会从中随机创建一个地图	必填,如果填一张就是唯一一张图&lt;br /&gt;
        rules.addMapName(this.matchMapName);&lt;br /&gt;
&lt;br /&gt;
        // 选填更复杂的段位规则,根据段位来设置规则，如果不添加，就会无规则随意匹配&lt;br /&gt;
        {&lt;br /&gt;
            for(let i =0;i&amp;lt;9;i++)&lt;br /&gt;
            {&lt;br /&gt;
                let duanLevel = i+1;&lt;br /&gt;
                this.addRulesDuan(rules,duanLevel)&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        // 调用添加matchRuleName为key的匹配规则，可以调用添加多个，&lt;br /&gt;
        // 添加后，才可以通过this.hall.UserBeginMatch，根据matchRuleName进行匹配&lt;br /&gt;
        this.hall.AddGameMatchRules(this.matchRuleName, rules, this);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*通过this.hall.AddGameMatchRules(this.matchRuleName, rules, this);来添加至少一条匹配规则，&lt;br /&gt;
&lt;br /&gt;
*其中matchRuleName为key，可以多次添加不同的key和rules&lt;br /&gt;
&lt;br /&gt;
*必须添加至少一条规则后，才可以通过this.hall.UserBeginMatch，&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;根据matchRuleName来映射到具体规则和地图列表，进行匹配&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
==== 详细段位规则 ====&lt;br /&gt;
上面的基础规则代码中，有一部分在添加段位规则，并且一个XMatchRule对应了多个段位规则&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private addRulesDuan(rules: XMatchRule,duanLevel:number = 1):void&lt;br /&gt;
    {&lt;br /&gt;
        let danRule = new XDanRule();&lt;br /&gt;
        // 匹配分数区间,匹配时，根据传入的分数，来找到符合区间的duanRule，找不到那就没有规则&lt;br /&gt;
        danRule.nMinScore = (duanLevel - 1) * 1000;&lt;br /&gt;
        danRule.nMaxScore = duanLevel * 1000;&lt;br /&gt;
&lt;br /&gt;
        // 比如初始分数区间为1000-2000，&lt;br /&gt;
        // 玩家是1500分，玩家就匹配到了这个段的规则&lt;br /&gt;
        &lt;br /&gt;
        // 每个匹配规则里，可以分成多个段位，比如青铜，王者，通过id来区分各个段位的匹配规则&lt;br /&gt;
        danRule.nDanID = duanLevel;&lt;br /&gt;
&lt;br /&gt;
        // 1阶段匹配时间&lt;br /&gt;
        danRule.nMatchTime1 = 15;&lt;br /&gt;
&lt;br /&gt;
        // 1阶段时，可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000，添加后变成了900-2100&lt;br /&gt;
        danRule.AddScore = 100;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        // 1阶段的时间结束，房间人还没满，会进入2阶段&lt;br /&gt;
       &lt;br /&gt;
        // 2阶段匹配时间&lt;br /&gt;
        danRule.nMatchTime2 = 15;&lt;br /&gt;
        // 2阶段时，可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000，添加后变成了1800-2200&lt;br /&gt;
        danRule.AddScoreMax = 200;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        // 匹配2阶段后，是否塞人机数量&lt;br /&gt;
        // 已经实现了把人机的数据塞进战斗房间&lt;br /&gt;
        // 比如XMatchRule里的nRoleNum = 8，nAINum = 2，那么当房间人数为6时，就会进入游戏（2阶段后）&lt;br /&gt;
        danRule.nAINum = Config.MatchNumMax - Config.MatchNumMin &amp;gt; 0 ? Config.MatchNumMax - Config.MatchNumMin : 0;&lt;br /&gt;
        rules.addDanRule(danRule);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   // 可以通过接口来获取具体的玩家信息，玩家信息中有字段来标记是否为机器人&lt;br /&gt;
   // host接口&lt;br /&gt;
   XPopManager.instance.xHost.GetUserInfo(userid).isRobot&lt;br /&gt;
   // 前端接口&lt;br /&gt;
   XPopManager.instance.xRemote.GetUserInfo(userid).isRobot&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 通过匹配规则匹配 ===&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;matchRuleName&#039;&#039;&#039;&amp;lt;/span&amp;gt;必须要先添加&lt;br /&gt;
*rankScore对应匹配规则中的分数，根据实际开发的业务数据来，分数会对应到详细的段位规则里&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;ExtraData&#039;&#039;&#039;&amp;lt;/span&amp;gt;是透传参数，可以在匹配成功进入战斗服中获取到&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
 private async onReceiveStartMatch(call: ApiCall&amp;lt;ReqStartMatch, ResStartMatch&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        let match = new XMatchRole(call.req.actorID);&lt;br /&gt;
        match.ruleName = this.matchRuleName;&lt;br /&gt;
        match.rankScore = 0;&lt;br /&gt;
        //匹配透传数据&lt;br /&gt;
        match.ExtraData = JSON.stringify({heroID:101});&lt;br /&gt;
        let ret = await this.hall.UserBeginMatch(call.req.actorID, match, this);&lt;br /&gt;
        return call.succ({&lt;br /&gt;
            ret: ret.ret&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 取消匹配 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private async onReceiveCancelMatch(call: ApiCall&amp;lt;ReqCancelMatch, ResCancelMatch&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        let ret = await this.hall.UserCancelMatch(call.req.actorID, this)&lt;br /&gt;
        return call.succ({&lt;br /&gt;
            ret: ret.ret&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 匹配状态变化 ===&lt;br /&gt;
开始匹配和取消匹配都会通过回调来通知给hall，&lt;br /&gt;
&lt;br /&gt;
可以根据开始时间，来处理倒计时等显示的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 绑定&lt;br /&gt;
this.hall.onUserMatchChange = this.onUserMatchChange.bind(this);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onUserMatchChange(iUserID: number, ruleName: string, matchTime: number): void {&lt;br /&gt;
        let userdata = this._usersMap.get(iUserID);&lt;br /&gt;
        if (!userdata) {&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        userdata.matchTime.value = matchTime;&lt;br /&gt;
        console.log(&#039;onUserMatchChange&#039;, iUserID, ruleName, matchTime);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 匹配成功 ===&lt;br /&gt;
匹配成功后会进入战斗服务器，host，上面的完整流程已经提过&lt;br /&gt;
&lt;br /&gt;
并且会收到匹配时传入的透传参数&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;ExtraData&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this.host.onUserMatchExtraDataReceived = this.onUserMatchExtraDataReceived.bind(this);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 开房间 ==&lt;br /&gt;
&lt;br /&gt;
开房间功能建议在试玩链接里体验，目前房间列表只在试玩里开放&lt;br /&gt;
&lt;br /&gt;
=== 接入开房间组件 ===&lt;br /&gt;
参考demo中HallUI的代码，一行即可,在点击创建房间按钮后触发&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.advancedComponentMgr.addComponent(&amp;quot;CreateRoomComponent&amp;quot;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
房主在大厅内点击创建房间&lt;br /&gt;
* [[File:创建房间.png]]&lt;br /&gt;
创建房间可以选择地图，具体能选择的地图需要在审核后台配置&lt;br /&gt;
&lt;br /&gt;
其他人加入房间&lt;br /&gt;
* [[File:创建房间2.png]]&lt;br /&gt;
* [[File:创建房间3.png]]&lt;br /&gt;
&lt;br /&gt;
=== 房间内显示逻辑 ===&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;强调一个概念，开房间组件，相当于是在大厅里，忽略了匹配过程，直接把玩家拉到了host里面&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;在房主创建房间进入时，以及其他人在大厅找到房间进入时，都会触发host的onUserEnter&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* 匹配时，我们一直在强调匹配规则的key，在进入host时，我们可以根据是否存在匹配规则的key，来判断是匹配进入，还是房间组件进入&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let ruleName: string = this.host.GetMatchRuleName();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* [[File:判断战斗快照.png]]&lt;br /&gt;
&lt;br /&gt;
*此时，界面内的显示需要根据host是否是房间状态来处理&lt;br /&gt;
-- 房间状态就显示各种座位的人员信息&lt;br /&gt;
-- 战斗状态就进入战斗逻辑（其实都是战斗服务器里的逻辑）&lt;br /&gt;
-- 具体的状态管理可以参考demo代码，由开发者自己写状态机&lt;br /&gt;
&lt;br /&gt;
== 启动本地大厅 ==&lt;br /&gt;
本地调试时，登录界面，勾选本地大厅即可&lt;br /&gt;
* [[File:勾选本地大厅.png]]&lt;br /&gt;
&lt;br /&gt;
只要勾选一个本地大厅就可以了，另一个账号登录时也会进入此大厅&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E5%A4%A7%E5%8E%85%E5%B7%A5%E7%A8%8B.png&amp;diff=578</id>
		<title>文件:泡饭大厅工程.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%B3%A1%E9%A5%AD%E5%A4%A7%E5%8E%85%E5%B7%A5%E7%A8%8B.png&amp;diff=578"/>
		<updated>2025-02-19T10:00:51Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=577</id>
		<title>大厅功能</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E5%A4%A7%E5%8E%85%E5%8A%9F%E8%83%BD&amp;diff=577"/>
		<updated>2025-02-19T10:00:19Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 老项目升级环境配置 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 老项目升级环境配置 ==&lt;br /&gt;
=== 项目demo地址 ===&lt;br /&gt;
泡饭中，hall_test模板，有完整代码&lt;br /&gt;
&lt;br /&gt;
* [[File:添加hall入口.png]]&lt;br /&gt;
&lt;br /&gt;
=== demo体验地址 ===&lt;br /&gt;
&lt;br /&gt;
[试玩链接]( https://subgame.popx.com/popion/master.html)&lt;br /&gt;
&lt;br /&gt;
* 如果是新号，在PC端ctrl +6，打开特殊窗口，输入 “isover 1”,点击按钮，可直接跳过新手流程!&lt;br /&gt;
* [[File:跳过引导.png]]&lt;br /&gt;
&lt;br /&gt;
* 在游戏界面游玩中，找到demox，点击进入即可&lt;br /&gt;
* [[File:Demo地址.png]]&lt;br /&gt;
=== 添加src/hall/index.ts的大厅服务器入口文件 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:添加hall入口.png]]&lt;br /&gt;
&lt;br /&gt;
=== 修改打包配置rollup ===&lt;br /&gt;
&lt;br /&gt;
* rollup.config.js新增打包大厅index.js的配置&lt;br /&gt;
&lt;br /&gt;
* [[File:rollup配置.png]]&lt;br /&gt;
&lt;br /&gt;
* rollup.config.js加一下服务类的白名单&lt;br /&gt;
&lt;br /&gt;
* [[File:rollup白名单.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 修改html启动文件 ===&lt;br /&gt;
&lt;br /&gt;
* 拷贝覆盖index.html到你的项目中，拷贝hall.html本地大厅文件到同一文件夹下&lt;br /&gt;
&lt;br /&gt;
* [[File:html入口.png]]&lt;br /&gt;
&lt;br /&gt;
=== 审核后台勾选大厅 ===&lt;br /&gt;
&lt;br /&gt;
* [[File:勾选大厅.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;只有勾选了大厅后，登录流程才会走大厅的逻辑，否则是走的老的流程，直接进战斗房间&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 大厅游戏工作流说明 ==&lt;br /&gt;
大厅工程相对于原来的client，server目录结构，多了一个hall目录，而且server实际上指的是host，&lt;br /&gt;
&lt;br /&gt;
每个目录的代码文件入口都在各自的&#039;&#039;&#039;index.ts&#039;&#039;&#039;里&lt;br /&gt;
&lt;br /&gt;
[[File:大厅文件结构.png]]&lt;br /&gt;
&lt;br /&gt;
为了方便大家理解，我们用颜色来标记各个核心文件夹的工作流&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client&#039;&#039;&#039;&amp;lt;/span&amp;gt;用橙色，代表前端显示，前端逻辑&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall&#039;&#039;&#039;&amp;lt;/span&amp;gt;用绿色，代表大厅，大厅服务器逻辑&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host&#039;&#039;&#039;&amp;lt;/span&amp;gt;用蓝色，注意，为了不和开房间混淆，host代表的是战斗服务器逻辑，而不是和以前一样叫做房间&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;client的入口代码最早执行，在里面做各种监听，也就是说，大厅期间，各种前端界面UI就已经能自己处理了，可以调用大厅的数据&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;以下的流程，如果审核后台勾选了大厅，按照顺序执行，否则直接跳过大厅，到了《进入战斗》&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 游戏开始时 ===  &lt;br /&gt;
&lt;br /&gt;
==== 进大厅前预加载资源 ====  &lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHallWait  &lt;br /&gt;
  &lt;br /&gt;
--在进入大厅前，会有一个promise的异步接口供开发者使用去预加载资源，比如表格，比如读取文件配置等等。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;必须在加载完成的回调中resolve&#039;&#039;&#039;&amp;lt;/span&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
//预加载资源  &lt;br /&gt;
XPopManager.onHallWait =  new Promise((resolve)=&amp;gt;{  &lt;br /&gt;
    GameManager.instance.onPreLoadRes(()=&amp;gt;{  &lt;br /&gt;
        resolve(&amp;quot;Resource loaded successfully hall&amp;quot;);  &lt;br /&gt;
    });  &lt;br /&gt;
});  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
==== 进入大厅 ====  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHallStart  &lt;br /&gt;
  &lt;br /&gt;
--在大厅资源预加载完成后，会直接启动大厅服务器实例  &lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHostHallStart  &lt;br /&gt;
  &lt;br /&gt;
--src/hall/index.ts 中监听的代码入口&lt;br /&gt;
&lt;br /&gt;
=== 大厅期间操作 ===  &lt;br /&gt;
  &lt;br /&gt;
==== 监听大厅内数据变化 ====  &lt;br /&gt;
大厅期间，hall的实例会监听各种玩家的事件，并且抛出回调，开发者可以在回调中处理hall的快照数据  &lt;br /&gt;
  &lt;br /&gt;
hall的实例是指XPopManager.instance.xHostHall  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserEnter  &lt;br /&gt;
  &lt;br /&gt;
--大厅内有玩家进入  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserLeave  &lt;br /&gt;
  &lt;br /&gt;
--大厅内有玩家离开  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onUserMatchChange  &lt;br /&gt;
  &lt;br /&gt;
--大厅内匹配信息发生变化，主要是指倒计时的变化  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: green;&amp;quot;&amp;gt;&#039;&#039;&#039;hall:&#039;&#039;&#039;&amp;lt;/span&amp;gt;hall.onDbItemChange  &lt;br /&gt;
  &lt;br /&gt;
--大厅内存盘的数据变化，在进入时就会调用一次，获取到账号DB数据，&#039;&#039;&#039;存盘&#039;&#039;&#039;后面再说&lt;br /&gt;
&lt;br /&gt;
==== 开始匹配 ====  &lt;br /&gt;
  &lt;br /&gt;
匹配内容后面单独讲  &lt;br /&gt;
  &lt;br /&gt;
==== 匹配成功 ====  &lt;br /&gt;
后面匹配内容会单独讲，  &lt;br /&gt;
匹配成功后，分配到了某个房间的实例（host）,并且会把匹配时的透传参数通知给host实例  &lt;br /&gt;
  &lt;br /&gt;
=== 进入战斗 ===  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onHostStart  &lt;br /&gt;
--src/server/index.ts 中监听的代码入口，去处理host实例的各种回调  &lt;br /&gt;
  &lt;br /&gt;
=== 战斗期间操作 ===  &lt;br /&gt;
==== 监听host战斗服内数据变化 ====  &lt;br /&gt;
host的实例是指XPopManager.instance.xHost&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserEnter&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器内有玩家进入&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserLeave&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器内有玩家离开&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onUserMatchExtraDataReceived&lt;br /&gt;
&lt;br /&gt;
--当战斗服务器收到玩家透传信息(匹配时传入的ExtraData)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: blue;&amp;quot;&amp;gt;&#039;&#039;&#039;host:&#039;&#039;&#039;&amp;lt;/span&amp;gt;host.onDbItemChange  &lt;br /&gt;
  &lt;br /&gt;
--当战斗服务器收到存盘的数据变化，在进入时就会调用一次，获取到账号DB数据，&#039;&#039;&#039;存盘&#039;&#039;&#039;后面再说&lt;br /&gt;
&lt;br /&gt;
=== 战斗返回大厅 ===  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onGameLeave  &lt;br /&gt;
  &lt;br /&gt;
--返回到大厅，大厅的数据还在快照中，可以做一些结算的处理&lt;br /&gt;
&lt;br /&gt;
想要回到大厅，需要手动调用&#039;&#039;&#039;XPopManager.instance.xRemote.LeaveGame &#039;&#039;&#039;&lt;br /&gt;
，demo里有示例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XPopManager.instance.xRemote.LeaveGame(this, (code: number, ret: string) =&amp;gt; {&lt;br /&gt;
            console.log(&#039;LeaveGame&#039;, code, ret);&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 离开整个游戏 ===  &lt;br /&gt;
&amp;lt;span style=&amp;quot;color: orange;&amp;quot;&amp;gt;&#039;&#039;&#039;client:&#039;&#039;&#039;&amp;lt;/span&amp;gt;XPopManager.onExit&lt;br /&gt;
&lt;br /&gt;
--在试玩的时候，点击官方左上角的退出，会进入此回调函数&lt;br /&gt;
&lt;br /&gt;
--平时本地调试开发时，不需要去处理这个回调&lt;br /&gt;
&lt;br /&gt;
--但是试玩时需要在此销毁场内资源，否则，退出后再次进入，会发现上一回的模型还在&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onExit = () =&amp;gt; {&lt;br /&gt;
    model.destroy();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 ==&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;交互只能client和hall,client和host之间互通信息，不能hall和host直接交互&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
=== client和hall的交互 ===&lt;br /&gt;
所有的和大厅的数据交互，依赖于&lt;br /&gt;
&lt;br /&gt;
client内部代码：this._remoteHall = XPopManager.instance.xRemote.hall&lt;br /&gt;
&lt;br /&gt;
hall内部代码：this.hall = XPopManager.instance.xHostHall&lt;br /&gt;
&lt;br /&gt;
具体代码位置在各自文件夹内&lt;br /&gt;
&lt;br /&gt;
*1.client向hall注册快照，并且监听快照变化&lt;br /&gt;
*2.hall注册rcp请求的回调方法&lt;br /&gt;
*3.client向hall发起rpc请求&lt;br /&gt;
*4.hall处理rpc请求，hall修改快照&lt;br /&gt;
*5.client监听快照变化的回调，处理显示逻辑&lt;br /&gt;
&lt;br /&gt;
具体可以查看MessageManager中的代码&lt;br /&gt;
&lt;br /&gt;
==== client注册快照 ====&lt;br /&gt;
&lt;br /&gt;
代码里有私有和共有快照，实际上本质上都是最基本的注册快照，只是在hall里面处理数据的方式不一样，具体可以看代码，不再赘述&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this._remoteHall.RegisterSnapshot(`${Snap_HallData.tag}`, this._hallData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== client监听快照变化 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
       this._hallData.notice.OnSet = (bReset) =&amp;gt; {&lt;br /&gt;
            XTipManager.Instance.showTip(this._hallData.notice.value);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== hall注册rpc回调 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this.hall.RegisterRpc(&#039;AddDbItem&#039;, this.onReceiveAddDbItem.bind(this));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== client发起rpc请求 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public async Hall_Request_AddItem(type:DBItemType, itemID:ItemNormalID, itemNum:number, callback?: Function): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        let ret = await this._remoteHall.Request(&amp;quot;AddDbItem&amp;quot;, { type: type, actorID: this._userID, itemID: itemID, itemNum: itemNum });&lt;br /&gt;
        console.log(&#039;requestAddDbItem&#039;, ret);&lt;br /&gt;
        callback &amp;amp;&amp;amp; callback(ret);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== hall处理rpc请求，修改快照 ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
private async onReceiveAddDbItem(call: ApiCall&amp;lt;ReqAddDbItem, ResAddDbItem&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        // 修改某个快照，前端已经注册过处理函数&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== client和host的交互 ===&lt;br /&gt;
所有的和战斗服的数据交互，依赖于&lt;br /&gt;
&lt;br /&gt;
client内部代码：this._remoteBattle = XPopManager.instance.xRemote.game;&lt;br /&gt;
&lt;br /&gt;
host内部代码（server文件夹）：this.host = XPopManager.instance.xHost;&lt;br /&gt;
&lt;br /&gt;
所有的流程和hall一样处理&lt;br /&gt;
&lt;br /&gt;
== 数据存盘 ==&lt;br /&gt;
数据存盘对应的是玩家登录时的账号id，调用存盘后，就有账号的长期数据可以记录和读取了&lt;br /&gt;
&lt;br /&gt;
=== 存储 ===&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;存储只能由hall或者host发起，两个服务器都有同名的存储接口&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;该功能没有必要接入大厅，直接调用host中的代码接口也可以存储和读取&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;调用该功能前，请务必根据自身gameid向我们内部申请一个对应的数字id来存盘使用&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
demo中，写在了hall里面，host里也可以写一份一样的，这样战斗和大厅的数据就都能存储了.&lt;br /&gt;
&lt;br /&gt;
如下示例代码的发起方是client通过rpc去通知hall发起的，实际上可以在服务器计算过程中自己发起&lt;br /&gt;
&lt;br /&gt;
说几个关键的参数：&lt;br /&gt;
&lt;br /&gt;
*dbID&lt;br /&gt;
&lt;br /&gt;
-- 需要开发者在我们的审核后台，获取了游戏id之后，把游戏id的字符串给我们的内部人员，&lt;br /&gt;
-- 然后我们帮开发者映射生成一个数字的id，专门来存储该游戏的数据，&lt;br /&gt;
-- 以后存盘读盘时就走的这个数字id&lt;br /&gt;
&lt;br /&gt;
*type&lt;br /&gt;
&lt;br /&gt;
-- 存盘的时候还有个type，支持填写0到9，可以用于开发者处理不同类型的数据分类。&lt;br /&gt;
-- 比如0代表货币类型，1代表背包道具，2代表头像。。等等&lt;br /&gt;
-- 当然，全存在一个type里也没影响，因为可以根据唯一的itemID去读自己的表格&lt;br /&gt;
&lt;br /&gt;
*itemID&lt;br /&gt;
&lt;br /&gt;
-- 开发者存储时的自定义id，用于自己游戏内的道具表格映射，请保持id的唯一性，不要有同id的道具&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private async onReceiveAddDbItem(call: ApiCall&amp;lt;ReqAddDbItem, ResAddDbItem&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        let userdata = this._usersMap.get(call.req.actorID);&lt;br /&gt;
        if (!userdata) {&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        if(call.req.type == DBItemType.Normal &amp;amp;&amp;amp; call.req.itemID == ItemNormalID.Gold){&lt;br /&gt;
            let ret;&lt;br /&gt;
            if(userdata.goldCount.value == -1){&lt;br /&gt;
                let item = new XDbItem();&lt;br /&gt;
                item.nID = call.req.itemID;&lt;br /&gt;
                item.nCount = call.req.itemNum;&lt;br /&gt;
                ret = await this.hall.AddDbItem(this.dbID, call.req.actorID, call.req.type, call.req.itemID, item, this);&lt;br /&gt;
            }else{&lt;br /&gt;
                ret = await this.hall.SetDbItemCount(this.dbID, call.req.actorID, call.req.type, call.req.itemID, call.req.itemNum+userdata.goldCount.value, this);&lt;br /&gt;
            }&lt;br /&gt;
            return call.succ({&lt;br /&gt;
                ret: ret.ret&lt;br /&gt;
            });&lt;br /&gt;
        }else{&lt;br /&gt;
            return call.succ({&lt;br /&gt;
                ret: &#039;未实现&#039;&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 读取 ===&lt;br /&gt;
&lt;br /&gt;
在hall和host中，都有一个监听函数&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.xHostHall.onDbItemChange;&lt;br /&gt;
XPopManager.instance.xHost.onDbItemChange;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次当存盘的数据发生变化，都会进入该回调函数。&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;hall和host连接成功后，也会先进入一次，所以进游戏就能获得存盘数据&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
我们以hall里的代码举例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   private onDbItemChange(type: number, key: number, value: XDbItem | null, iUserID: number, bReset: boolean):void{&lt;br /&gt;
        if(type == DBItemType.Normal &amp;amp;&amp;amp; key == ItemNormalID.Gold){&lt;br /&gt;
            let userdata = this._usersMap.get(iUserID);&lt;br /&gt;
            if (!userdata) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            userdata.goldCount.value = value.nCount;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*type&lt;br /&gt;
-对应传入的type&lt;br /&gt;
*key&lt;br /&gt;
—对应存储的itemId&lt;br /&gt;
*value: XDbItem&lt;br /&gt;
&lt;br /&gt;
其中，XDbItem的结构是我们DB中固定的格式，外部只需要关心nID和nCount即可&lt;br /&gt;
&lt;br /&gt;
nID对应存储时传入的唯一itemId&lt;br /&gt;
nCount对应DB中该物品的数量&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* [[File:db存储结构.png]]&lt;br /&gt;
&lt;br /&gt;
=== KeyValue数据存盘 ===&lt;br /&gt;
简单的物品类型并不足以支持复杂的游戏数据结构，我们也提供了和物品存储类似的数据结构存储&lt;br /&gt;
&lt;br /&gt;
同样是在hall和host中都可以调用&lt;br /&gt;
&lt;br /&gt;
监听的函数用&#039;&#039;&#039;onDbDataChange&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.xHostHall.onDbDataChange;&lt;br /&gt;
XPopManager.instance.xHost.onDbDataChange;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
存储的函数用SetDbDatas&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
// 存储设置自定义key-value&lt;br /&gt;
ret = await this.hall.SetDbData()&lt;br /&gt;
// 存储整个map的keyvalue合集&lt;br /&gt;
ret = await this.hall.SetDbDatas()&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* [[File:存盘复杂数据.png]]&lt;br /&gt;
&lt;br /&gt;
如函数参数中所示，存储的value是string类型，&lt;br /&gt;
&lt;br /&gt;
那么可以通过JSON.stringify(obj)和JSON.parse(str)来相互转换，从而存储复杂数据&lt;br /&gt;
&lt;br /&gt;
* 注意:如上是hall的存储读取示例，如果想用host，接口类似，在如下的论坛帖子里有演示。&lt;br /&gt;
&lt;br /&gt;
[host存储数据](https://bbs.popx.com/t/keyvalue-ondbdatachange/207/14?u=qc_jump)&lt;br /&gt;
&lt;br /&gt;
== 匹配逻辑 ==&lt;br /&gt;
&lt;br /&gt;
匹配逻辑在hall中处理&lt;br /&gt;
&lt;br /&gt;
=== 创建匹配规则 ===&lt;br /&gt;
&lt;br /&gt;
匹配前，一定要创建至少一个匹配规则的名称，否则无法匹配&lt;br /&gt;
&lt;br /&gt;
参考demo中，在hall代码的初始阶段就要设置&lt;br /&gt;
&lt;br /&gt;
如下是添加了更加详细的注释代码，其中段位相关的规则可以先不添加&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * 初始化匹配规则&lt;br /&gt;
     */&lt;br /&gt;
    private initMatchRule(): void {&lt;br /&gt;
        let rules = new XMatchRule();&lt;br /&gt;
        // 需求人数	必填&lt;br /&gt;
        rules.nRoleNum = Config.MatchNumMax;&lt;br /&gt;
        // 地图列表 - 匹配成功后，会从中随机创建一个地图	必填,如果填一张就是唯一一张图&lt;br /&gt;
        rules.addMapName(this.matchMapName);&lt;br /&gt;
&lt;br /&gt;
        // 选填更复杂的段位规则,根据段位来设置规则，如果不添加，就会无规则随意匹配&lt;br /&gt;
        {&lt;br /&gt;
            for(let i =0;i&amp;lt;9;i++)&lt;br /&gt;
            {&lt;br /&gt;
                let duanLevel = i+1;&lt;br /&gt;
                this.addRulesDuan(rules,duanLevel)&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        // 调用添加matchRuleName为key的匹配规则，可以调用添加多个，&lt;br /&gt;
        // 添加后，才可以通过this.hall.UserBeginMatch，根据matchRuleName进行匹配&lt;br /&gt;
        this.hall.AddGameMatchRules(this.matchRuleName, rules, this);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*通过this.hall.AddGameMatchRules(this.matchRuleName, rules, this);来添加至少一条匹配规则，&lt;br /&gt;
&lt;br /&gt;
*其中matchRuleName为key，可以多次添加不同的key和rules&lt;br /&gt;
&lt;br /&gt;
*必须添加至少一条规则后，才可以通过this.hall.UserBeginMatch，&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;根据matchRuleName来映射到具体规则和地图列表，进行匹配&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
==== 详细段位规则 ====&lt;br /&gt;
上面的基础规则代码中，有一部分在添加段位规则，并且一个XMatchRule对应了多个段位规则&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private addRulesDuan(rules: XMatchRule,duanLevel:number = 1):void&lt;br /&gt;
    {&lt;br /&gt;
        let danRule = new XDanRule();&lt;br /&gt;
        // 匹配分数区间,匹配时，根据传入的分数，来找到符合区间的duanRule，找不到那就没有规则&lt;br /&gt;
        danRule.nMinScore = (duanLevel - 1) * 1000;&lt;br /&gt;
        danRule.nMaxScore = duanLevel * 1000;&lt;br /&gt;
&lt;br /&gt;
        // 比如初始分数区间为1000-2000，&lt;br /&gt;
        // 玩家是1500分，玩家就匹配到了这个段的规则&lt;br /&gt;
        &lt;br /&gt;
        // 每个匹配规则里，可以分成多个段位，比如青铜，王者，通过id来区分各个段位的匹配规则&lt;br /&gt;
        danRule.nDanID = duanLevel;&lt;br /&gt;
&lt;br /&gt;
        // 1阶段匹配时间&lt;br /&gt;
        danRule.nMatchTime1 = 15;&lt;br /&gt;
&lt;br /&gt;
        // 1阶段时，可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000，添加后变成了900-2100&lt;br /&gt;
        danRule.AddScore = 100;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        // 1阶段的时间结束，房间人还没满，会进入2阶段&lt;br /&gt;
       &lt;br /&gt;
        // 2阶段匹配时间&lt;br /&gt;
        danRule.nMatchTime2 = 15;&lt;br /&gt;
        // 2阶段时，可以根据震荡区间来扩大搜索范围.比如本身是匹配1000-2000，添加后变成了1800-2200&lt;br /&gt;
        danRule.AddScoreMax = 200;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        // 匹配2阶段后，是否塞人机数量&lt;br /&gt;
        // 已经实现了把人机的数据塞进战斗房间&lt;br /&gt;
        // 比如XMatchRule里的nRoleNum = 8，nAINum = 2，那么当房间人数为6时，就会进入游戏（2阶段后）&lt;br /&gt;
        danRule.nAINum = Config.MatchNumMax - Config.MatchNumMin &amp;gt; 0 ? Config.MatchNumMax - Config.MatchNumMin : 0;&lt;br /&gt;
        rules.addDanRule(danRule);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   // 可以通过接口来获取具体的玩家信息，玩家信息中有字段来标记是否为机器人&lt;br /&gt;
   // host接口&lt;br /&gt;
   XPopManager.instance.xHost.GetUserInfo(userid).isRobot&lt;br /&gt;
   // 前端接口&lt;br /&gt;
   XPopManager.instance.xRemote.GetUserInfo(userid).isRobot&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 通过匹配规则匹配 ===&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;matchRuleName&#039;&#039;&#039;&amp;lt;/span&amp;gt;必须要先添加&lt;br /&gt;
*rankScore对应匹配规则中的分数，根据实际开发的业务数据来，分数会对应到详细的段位规则里&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;ExtraData&#039;&#039;&#039;&amp;lt;/span&amp;gt;是透传参数，可以在匹配成功进入战斗服中获取到&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
 private async onReceiveStartMatch(call: ApiCall&amp;lt;ReqStartMatch, ResStartMatch&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        console.log(&#039;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;onReceiveAddDbItem&#039;);&lt;br /&gt;
        let match = new XMatchRole(call.req.actorID);&lt;br /&gt;
        match.ruleName = this.matchRuleName;&lt;br /&gt;
        match.rankScore = 0;&lt;br /&gt;
        //匹配透传数据&lt;br /&gt;
        match.ExtraData = JSON.stringify({heroID:101});&lt;br /&gt;
        let ret = await this.hall.UserBeginMatch(call.req.actorID, match, this);&lt;br /&gt;
        return call.succ({&lt;br /&gt;
            ret: ret.ret&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 取消匹配 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private async onReceiveCancelMatch(call: ApiCall&amp;lt;ReqCancelMatch, ResCancelMatch&amp;gt;): Promise&amp;lt;void&amp;gt; {&lt;br /&gt;
        let ret = await this.hall.UserCancelMatch(call.req.actorID, this)&lt;br /&gt;
        return call.succ({&lt;br /&gt;
            ret: ret.ret&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 匹配状态变化 ===&lt;br /&gt;
开始匹配和取消匹配都会通过回调来通知给hall，&lt;br /&gt;
&lt;br /&gt;
可以根据开始时间，来处理倒计时等显示的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 绑定&lt;br /&gt;
this.hall.onUserMatchChange = this.onUserMatchChange.bind(this);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onUserMatchChange(iUserID: number, ruleName: string, matchTime: number): void {&lt;br /&gt;
        let userdata = this._usersMap.get(iUserID);&lt;br /&gt;
        if (!userdata) {&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        userdata.matchTime.value = matchTime;&lt;br /&gt;
        console.log(&#039;onUserMatchChange&#039;, iUserID, ruleName, matchTime);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 匹配成功 ===&lt;br /&gt;
匹配成功后会进入战斗服务器，host，上面的完整流程已经提过&lt;br /&gt;
&lt;br /&gt;
并且会收到匹配时传入的透传参数&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;ExtraData&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
this.host.onUserMatchExtraDataReceived = this.onUserMatchExtraDataReceived.bind(this);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 开房间 ==&lt;br /&gt;
&lt;br /&gt;
开房间功能建议在试玩链接里体验，目前房间列表只在试玩里开放&lt;br /&gt;
&lt;br /&gt;
=== 接入开房间组件 ===&lt;br /&gt;
参考demo中HallUI的代码，一行即可,在点击创建房间按钮后触发&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.instance.advancedComponentMgr.addComponent(&amp;quot;CreateRoomComponent&amp;quot;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
房主在大厅内点击创建房间&lt;br /&gt;
* [[File:创建房间.png]]&lt;br /&gt;
创建房间可以选择地图，具体能选择的地图需要在审核后台配置&lt;br /&gt;
&lt;br /&gt;
其他人加入房间&lt;br /&gt;
* [[File:创建房间2.png]]&lt;br /&gt;
* [[File:创建房间3.png]]&lt;br /&gt;
&lt;br /&gt;
=== 房间内显示逻辑 ===&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;强调一个概念，开房间组件，相当于是在大厅里，忽略了匹配过程，直接把玩家拉到了host里面&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;在房主创建房间进入时，以及其他人在大厅找到房间进入时，都会触发host的onUserEnter&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* 匹配时，我们一直在强调匹配规则的key，在进入host时，我们可以根据是否存在匹配规则的key，来判断是匹配进入，还是房间组件进入&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let ruleName: string = this.host.GetMatchRuleName();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* [[File:判断战斗快照.png]]&lt;br /&gt;
&lt;br /&gt;
*此时，界面内的显示需要根据host是否是房间状态来处理&lt;br /&gt;
-- 房间状态就显示各种座位的人员信息&lt;br /&gt;
-- 战斗状态就进入战斗逻辑（其实都是战斗服务器里的逻辑）&lt;br /&gt;
-- 具体的状态管理可以参考demo代码，由开发者自己写状态机&lt;br /&gt;
&lt;br /&gt;
== 启动本地大厅 ==&lt;br /&gt;
本地调试时，登录界面，勾选本地大厅即可&lt;br /&gt;
* [[File:勾选本地大厅.png]]&lt;br /&gt;
&lt;br /&gt;
只要勾选一个本地大厅就可以了，另一个账号登录时也会进入此大厅&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=576</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=576"/>
		<updated>2025-02-19T08:49:34Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 修改后的资源，git上传到远端 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=575</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=575"/>
		<updated>2025-02-19T08:49:26Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 修改后的资源，git上传到远端 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终要检查下如下内容，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.修改后的ui内容，比如修改了mainUI.scene,对应的，在res-extra/ui文件夹下，mainUI.json就会有修改&lt;br /&gt;
&lt;br /&gt;
2.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
3.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=574</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=574"/>
		<updated>2025-02-19T08:46:36Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 修改后的资源，git上传到远端 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
注意：提交的文件里，最终有两个部分要对应上，才能保证正常泡饭中正常使用。&lt;br /&gt;
&lt;br /&gt;
1.res-extra目录里，有刚刚通过npm run ui 命令，拷贝过来的laya工程中的文件。&lt;br /&gt;
&lt;br /&gt;
2.fileconfig.json里，有那些新增和改动的文件的配置。&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=573</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=573"/>
		<updated>2025-02-19T06:36:12Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 运行生成UI命令 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，UI资源使用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=572</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=572"/>
		<updated>2025-02-19T06:35:17Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 运行生成UI命令 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=571</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=571"/>
		<updated>2025-02-19T06:34:39Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 额外资源 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=570</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=570"/>
		<updated>2025-02-19T06:33:52Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 运行生成UI命令 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json，该文件会把额外的资源列表记录下来&lt;br /&gt;
&lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=569</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=569"/>
		<updated>2025-02-19T06:32:45Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 在编辑器中创建UI */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
&lt;br /&gt;
可以在指定目录下，去添加资源，一般来说，拼接scene即可，scene用到的资源要放在如下的指定目录中&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=568</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=568"/>
		<updated>2025-02-19T06:01:45Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 在编辑器中创建UI */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
&lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=567</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=567"/>
		<updated>2025-02-19T06:01:25Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 安装本地ui的编辑环境 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=566</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=566"/>
		<updated>2025-02-19T06:00:26Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 运行生成UI命令 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=565</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=565"/>
		<updated>2025-02-19T05:59:42Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 额外资源 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%96%AD%E7%82%B9.png&amp;diff=564</id>
		<title>文件:服务器断点.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%96%AD%E7%82%B9.png&amp;diff=564"/>
		<updated>2025-02-19T05:53:43Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%9C%8D%E5%8A%A1%E5%99%A8host.png&amp;diff=563</id>
		<title>文件:服务器host.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E6%9C%8D%E5%8A%A1%E5%99%A8host.png&amp;diff=563"/>
		<updated>2025-02-19T05:53:33Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=562</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=562"/>
		<updated>2025-02-19T05:53:19Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 本地服务器调试 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 服务器调试 ====&lt;br /&gt;
*启动运行时，会弹出一个host弹窗服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
&lt;br /&gt;
[[File:服务器host.png]]&lt;br /&gt;
&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
&lt;br /&gt;
*该文件是由服务器代码入口index.ts编译生成的js文件，注意，不止是index.ts，里面相关的引用代码都会编进去&lt;br /&gt;
&lt;br /&gt;
*虽然代码经过编译，但是没有混淆，可以通过函数名，找到js代码中的行号，打断点&lt;br /&gt;
&lt;br /&gt;
[[File:服务器断点.png]]&lt;br /&gt;
&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=561</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=561"/>
		<updated>2025-02-19T05:42:06Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 代码如何调试 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在网页上调试&lt;br /&gt;
&lt;br /&gt;
f12打开控制台，&lt;br /&gt;
&lt;br /&gt;
点击源代码，&lt;br /&gt;
&lt;br /&gt;
可以根据代码目录结构找到想要断点的文件，&lt;br /&gt;
&lt;br /&gt;
也可以快捷键&amp;quot;ctrl + p&amp;quot;输入文件名查找 （mac上是cmd + p）,&lt;br /&gt;
&lt;br /&gt;
找到后，点击具体左侧代码行数标签即可。&lt;br /&gt;
&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:网页断点调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E7%BD%91%E9%A1%B5%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95.png&amp;diff=560</id>
		<title>文件:网页断点调试.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E7%BD%91%E9%A1%B5%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95.png&amp;diff=560"/>
		<updated>2025-02-19T05:38:50Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=559</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=559"/>
		<updated>2025-02-19T05:33:40Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 前后端协议交互 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=558</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=558"/>
		<updated>2025-02-19T05:33:27Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 前后端协议交互 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#协议通信（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=557</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=557"/>
		<updated>2025-02-13T10:56:59Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 地图制作导出 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#npm run proto（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=556</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=556"/>
		<updated>2025-02-13T10:56:51Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 地图制作导出 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#npm run proto（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
教学视频已经上传到网页链接&lt;br /&gt;
&lt;br /&gt;
[新建地图](https://formalhead.popx.com/popxVedio/mapeditor/新建地图.mp4)&lt;br /&gt;
&lt;br /&gt;
[地块操作](https://formalhead.popx.com/popxVedio/mapeditor/地块操作.mp4)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[地块（水）](https://formalhead.popx.com/popxVedio/mapeditor/地块（水）.mp4)&lt;br /&gt;
&lt;br /&gt;
[地形设置（增加）](https://formalhead.popx.com/popxVedio/mapeditor/地形设置（增加）.mp4)&lt;br /&gt;
&lt;br /&gt;
[新地编添加预制件和编辑](https://formalhead.popx.com/popxVedio/mapeditor/新地编添加预制件和编辑.mp4)&lt;br /&gt;
&lt;br /&gt;
[预制件操作](https://formalhead.popx.com/popxVedio/mapeditor/预制件操作.mp4)&lt;br /&gt;
&lt;br /&gt;
[授权给他人使用](https://formalhead.popx.com/popxVedio/mapeditor/授权.mp4)&lt;br /&gt;
&lt;br /&gt;
[如何游戏内绑定后台玩法和地图](https://formalhead.popx.com/popxVedio/mapeditor/如何游戏内绑定后台玩法和地图.mp4)&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=555</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=555"/>
		<updated>2025-02-13T10:51:22Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 地图官网地址 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#npm run proto（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
在项目目录&#039;&#039;&#039;/地图编辑器操作说明&#039;&#039;&#039;中，有视频教程&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=554</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=554"/>
		<updated>2025-02-10T08:58:55Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 官方论坛 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#npm run proto（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图官网地址 ===&lt;br /&gt;
*[[上传流程|地图编辑器页签可以操作]]&lt;br /&gt;
&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
在项目目录&#039;&#039;&#039;/地图编辑器操作说明&#039;&#039;&#039;中，有视频教程&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 常见工作流问题 ==&lt;br /&gt;
=== 如何把本地的项目上传到泡饭 ===&lt;br /&gt;
==== 建议先上传项目到自己的git仓库 ====&lt;br /&gt;
==== 从git拉取 ====&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E7%82%B9%E5%87%BB%E4%BB%8Egit%E5%88%9B%E5%BB%BA%E5%B7%A5%E7%A8%8B.png&amp;diff=553</id>
		<title>文件:点击从git创建工程.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%96%87%E4%BB%B6:%E7%82%B9%E5%87%BB%E4%BB%8Egit%E5%88%9B%E5%BB%BA%E5%B7%A5%E7%A8%8B.png&amp;diff=553"/>
		<updated>2025-02-10T08:56:25Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A&amp;diff=552</id>
		<title>泡饭工坊</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A&amp;diff=552"/>
		<updated>2025-02-10T08:56:04Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 从git拉取 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号登录 ===&lt;br /&gt;
目前支持自定义账号，记住账号密码即可，后续会优化。&lt;br /&gt;
&lt;br /&gt;
== 用模板开发代码 ==&lt;br /&gt;
模板是泡饭的核心功能，开发者既可以快速预览学习模板的代码，又可以自己上传模板&lt;br /&gt;
&lt;br /&gt;
=== 模板仓库创建工程 ===&lt;br /&gt;
&lt;br /&gt;
[模板商店创建工程]&lt;br /&gt;
[模板商店]&lt;br /&gt;
&lt;br /&gt;
=== 模板上传 ===&lt;br /&gt;
&lt;br /&gt;
[模板上传设置]&lt;br /&gt;
&lt;br /&gt;
=== 模板成员 ===&lt;br /&gt;
&lt;br /&gt;
== 用git开发代码 ==&lt;br /&gt;
&lt;br /&gt;
=== 从git拉取 ===&lt;br /&gt;
&lt;br /&gt;
点击图中创建模板按钮，从git创建工程&lt;br /&gt;
&lt;br /&gt;
[[File:点击从git创建工程.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:git项目地址.png]]&lt;br /&gt;
&lt;br /&gt;
=== 上传到git ===&lt;br /&gt;
&lt;br /&gt;
=== git冲突解决 ===&lt;br /&gt;
&lt;br /&gt;
=== git代码上传到模板 ===&lt;br /&gt;
&lt;br /&gt;
== 模板的商业化支持 ==&lt;br /&gt;
&lt;br /&gt;
=== 设置公有代码 ===&lt;br /&gt;
&lt;br /&gt;
=== 设置私有代码 ===&lt;br /&gt;
&lt;br /&gt;
=== 代码引用的赞赏和收费 ===&lt;br /&gt;
&lt;br /&gt;
== 编码操作 ==&lt;br /&gt;
&lt;br /&gt;
=== 内置vscode ===&lt;br /&gt;
&lt;br /&gt;
=== 导入本地文件 ===&lt;br /&gt;
&lt;br /&gt;
单个文件&lt;br /&gt;
&lt;br /&gt;
文件夹&lt;br /&gt;
&lt;br /&gt;
=== 代码引用和注释 ===&lt;br /&gt;
&lt;br /&gt;
=== 编码快捷键操作 ===&lt;br /&gt;
&lt;br /&gt;
=== 自动编译 ===&lt;br /&gt;
&lt;br /&gt;
=== 调试和断点 ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 适配PGC项目 ==&lt;br /&gt;
&lt;br /&gt;
=== package.json设置 ===&lt;br /&gt;
&lt;br /&gt;
==== sdk版本 ====&lt;br /&gt;
==== 资源版本 ====&lt;br /&gt;
==== 游戏id和key ====&lt;br /&gt;
==== 第三方包支持 ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 启动模式和参数 ===&lt;br /&gt;
&lt;br /&gt;
==== 大厅 ====&lt;br /&gt;
==== 单机 ====&lt;br /&gt;
==== 服务器 ====&lt;br /&gt;
&lt;br /&gt;
=== 分享游戏链接 ===&lt;br /&gt;
&lt;br /&gt;
=== 内置工具 ===&lt;br /&gt;
&lt;br /&gt;
==== 模型查看器 ====&lt;br /&gt;
==== 开发者后台 ====&lt;br /&gt;
==== 快捷键修改 ====&lt;br /&gt;
&lt;br /&gt;
=== 帮助文档支持 ===&lt;br /&gt;
&lt;br /&gt;
==== 官方开发文档 ====&lt;br /&gt;
&lt;br /&gt;
==== 论坛生态 ====&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
	<entry>
		<id>https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=551</id>
		<title>泡饭工坊项目流程</title>
		<link rel="alternate" type="text/html" href="https://wiki.popx.com/index.php?title=%E6%B3%A1%E9%A5%AD%E5%B7%A5%E5%9D%8A%E9%A1%B9%E7%9B%AE%E6%B5%81%E7%A8%8B&amp;diff=551"/>
		<updated>2025-01-17T10:42:46Z</updated>

		<summary type="html">&lt;p&gt;Liuweichen：​/* 多人合作冲突的解决 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 泡饭工坊介绍 ==&lt;br /&gt;
===泡饭工坊是什么？===&lt;br /&gt;
泡饭是一套网页虚拟环境下的代码编辑工具，内嵌了多种预设和模板，集成了vscode来编辑代码，自动编译，可以方便的预览和分享给小伙伴体验。&lt;br /&gt;
=== 泡饭链接 ===&lt;br /&gt;
[链接](https://webdev.popx.com)&lt;br /&gt;
&lt;br /&gt;
=== 泡饭账号注册登录 ===&lt;br /&gt;
&lt;br /&gt;
==== 没账号的需要在泡饭注册账号 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新注册账号手机号.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;请记住您的账号密码，这关系到后续我们管理您的使用权限&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 和我们内部人员沟通，申请后台权限 ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;如需开通后台使用权限，将您的账号告知我们内部人员帮操作，等待通知完成后，重新刷新网页即可&#039;&#039;&#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 第一次需要创建一个工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新点击模板商店创建模板.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:新模板下载.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:新下载工程.png]]&lt;br /&gt;
&lt;br /&gt;
==== 有了工程后，打开工程 ====&lt;br /&gt;
&lt;br /&gt;
[[File:新打开我的工程.png]]&lt;br /&gt;
&lt;br /&gt;
== 打开泡饭体验项目 ==&lt;br /&gt;
&lt;br /&gt;
参考视频[1.泡饭运行环境演示.mp4]&lt;br /&gt;
&lt;br /&gt;
== 如何用泡饭开发自己的项目 ==&lt;br /&gt;
&lt;br /&gt;
=== 点击跳转后台 ===&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;&#039;&#039;&#039;就第一次需要创建打开一个工程，才能出现地图编辑器的跳转页面&#039;&#039;&#039;&amp;lt;/span&amp;gt;  ====&lt;br /&gt;
&lt;br /&gt;
[[File:新跳转后台.png]]&lt;br /&gt;
&lt;br /&gt;
=== 后台创建游戏，绑定地图 ===&lt;br /&gt;
* &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;需要泡饭跳转到后台，去新建游戏，生成独属于你的gameid和secret，这在后续的开发和上传时至关重要，请点击下方超链接，去参考其中新建gameid的部分&amp;lt;/span&amp;gt;&lt;br /&gt;
* [[上传流程|请看该链接中，新建游戏玩法的部分]]&lt;br /&gt;
&lt;br /&gt;
=== 修改package.json进入自己的游戏和地图 ===&lt;br /&gt;
- 点击左侧修改配置按钮&lt;br /&gt;
 [[File:新点击修改配置.png]]  &lt;br /&gt;
  - gameid 开发者后台中指定的游戏玩法&lt;br /&gt;
  - secretkey 游戏玩法对应的密钥&lt;br /&gt;
  - defaultMap 游戏玩法中，指定的地图，请在后台至少指定一张，默认填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
  - sdkVersion sdk版本号，点击修改可以弹出下拉框，一般选用release标签的稳定版本，修改后，会去修改对应的bridge依赖，并且下载&lt;br /&gt;
  - resVersion 美术资源版本号，会影响游戏内地图和模型的显示，一般选用最新的版本即可&lt;br /&gt;
&lt;br /&gt;
=== 首次运行需要先发布 ===&lt;br /&gt;
&lt;br /&gt;
当前的gameid是最新的，&lt;br /&gt;
&lt;br /&gt;
目前后台服务器还没有一个初始版本的记录，所以需要在package.json中，修改完gameid之后。&lt;br /&gt;
&lt;br /&gt;
首次运行要发布游戏（后续不需要每次发布，直接保存就可以编译调试）。&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
等待右上脚提示发布完成，即可启动进入游戏&lt;br /&gt;
&lt;br /&gt;
=== 启动项目 ===&lt;br /&gt;
&lt;br /&gt;
一般选用启动游戏（服务器模式）&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭点击启动游戏.png]] &lt;br /&gt;
&lt;br /&gt;
==== 登录界面 ====  &lt;br /&gt;
- 运行后，浏览器将显示登陆界面。  &lt;br /&gt;
  [[File:默认地图.png]]  &lt;br /&gt;
  - 账号密码随便输入，会创建新账号&lt;br /&gt;
  - 输入账号密码后，选择指定地图（可选），即可看到战斗服务器世界中有自己的人物。&lt;br /&gt;
  - 默认的demo工程中，如果没有修改gameid和secretKey，地图请填写&amp;quot;地图_0&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== 泡饭模板的多人合作 ==&lt;br /&gt;
&lt;br /&gt;
=== 发布模板 ===&lt;br /&gt;
&lt;br /&gt;
[[File:发布工程多人合作.png]]&lt;br /&gt;
&lt;br /&gt;
设置团队成员权限和名称&lt;br /&gt;
&lt;br /&gt;
[[File:设置团队成员权限.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 发布后管理项目 ===&lt;br /&gt;
&lt;br /&gt;
发布后可以在商店模板里去修改自己发布的项目。&lt;br /&gt;
&lt;br /&gt;
[[File:发布后编辑模板权限.png]]&lt;br /&gt;
&lt;br /&gt;
=== 其他成员就可以在模板商店拉取模板了 ===&lt;br /&gt;
&lt;br /&gt;
比如上图中的playername1的账号，就可以拉取模板了&lt;br /&gt;
&lt;br /&gt;
=== 打开项目 ===&lt;br /&gt;
&lt;br /&gt;
点击编辑按钮，切换到vscode运行环境。&lt;br /&gt;
&lt;br /&gt;
[[File:点击编辑按钮切换vscode.png]]&lt;br /&gt;
&lt;br /&gt;
=== 团队成员修改提交 ===&lt;br /&gt;
&lt;br /&gt;
我们尝试写第一行代码提交下&lt;br /&gt;
&lt;br /&gt;
[[File:写第一行代码.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:模板内git_commit.png]]&lt;br /&gt;
&lt;br /&gt;
具体在vscode中，怎么使用git来提交和拉取，可以参考如下文档&lt;br /&gt;
&lt;br /&gt;
[vscode使用git插件](https://blog.csdn.net/khhmd/article/details/144131405)&lt;br /&gt;
&lt;br /&gt;
=== 多人合作冲突的解决(遇到冲突再看，可以先跳过) ===&lt;br /&gt;
==== 选择远端的master分支来merge ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突merge.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge选择远端.png]]&lt;br /&gt;
&lt;br /&gt;
==== 根据需求去修改冲突文件 ====&lt;br /&gt;
&lt;br /&gt;
[[File:git冲突文件解决.png]]&lt;br /&gt;
&lt;br /&gt;
==== 冲突文件修改完成，resolve ====&lt;br /&gt;
[[File:git冲突保存.png]]&lt;br /&gt;
[[File:解决冲突完成.png]]&lt;br /&gt;
&lt;br /&gt;
==== 同步到远端 ====&lt;br /&gt;
&lt;br /&gt;
[[File:gitmerge同步到远端.png]]&lt;br /&gt;
&lt;br /&gt;
== 前端基础逻辑 ==  &lt;br /&gt;
  &lt;br /&gt;
根据以下步骤，一步步进入泡泡的游戏世界，即使你是新手也无需担心。  &lt;br /&gt;
  &lt;br /&gt;
=== 前端游戏入口 ===  &lt;br /&gt;
- &#039;&#039;&#039;/src/client/index.ts&#039;&#039;&#039; 文件中的 `XPopManager.onGameStart` 是前端的主要入口点。  &lt;br /&gt;
  &lt;br /&gt;
=== 创建第一个人物 ===  &lt;br /&gt;
在 `simpleGame` 类中，我们将创建并初始化第一个游戏人物。  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export class simpleGame {  &lt;br /&gt;
    private firstModel: XModelHero;  &lt;br /&gt;
  &lt;br /&gt;
    constructor() {  &lt;br /&gt;
        // 创建第一个人物  &lt;br /&gt;
        this.firstModel = new XModelHero();  &lt;br /&gt;
        let path = &#039;role/model_avatar_body_default&#039;;  &lt;br /&gt;
  &lt;br /&gt;
        // 加载人物模型并播放动画  &lt;br /&gt;
        this.firstModel.loadModel(path, null, this, (success) =&amp;gt; {  &lt;br /&gt;
            // 播放跑步动画  &lt;br /&gt;
            let actName = &amp;quot;act_avatar_base_run&amp;quot;;  &lt;br /&gt;
            this.firstModel.playAniUp(`${actName}_up`);  &lt;br /&gt;
            this.firstModel.playAniDown(`${actName}_down`);  &lt;br /&gt;
  &lt;br /&gt;
            // 摄像机跟随  &lt;br /&gt;
            let camera = XPopManager.instance.xScene.camera;  &lt;br /&gt;
            camera.focus = this.firstModel;  &lt;br /&gt;
  &lt;br /&gt;
            // 定时更新人物位置  &lt;br /&gt;
            XTimer.loop(100, this, this.updateModelPos.bind(this));  &lt;br /&gt;
        });  &lt;br /&gt;
    }  &lt;br /&gt;
  &lt;br /&gt;
    updateModelPos(delta: number, frame?: string) {  &lt;br /&gt;
        // 更新人物位置  &lt;br /&gt;
        this.firstModel.position.x += 0.1;  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
  &lt;br /&gt;
// 设置游戏启动时执行  &lt;br /&gt;
XPopManager.onGameStart = () =&amp;gt; {  &lt;br /&gt;
    new simpleGame();  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
*注意：请将原代码中的 &#039;&#039;&#039;new Main()&#039;&#039;&#039; 替换为 &#039;&#039;&#039;new simpleGame()&#039;&#039;&#039;; &lt;br /&gt;
上述的代码我们以最简单的方式，去掉一切服务器交互，添加一个基础模型到游戏中跑动&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;再提醒一次，每次有代码修改后，运行前需调用命令进行编译&amp;lt;/span&amp;gt;&lt;br /&gt;
[[#编译]]&lt;br /&gt;
&lt;br /&gt;
=== 播放人物的动作 === &lt;br /&gt;
我们的人物有上下半身，在知道动画名字的情况下，需要分上下半身来播放动画&lt;br /&gt;
&lt;br /&gt;
以&#039;act_avatar_plant_collect&#039;为例&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    let animbaseStr = &#039;act_avatar_plant_collect&#039;;&lt;br /&gt;
    role.model.playAniUp(`${animbaseStr}_up`);&lt;br /&gt;
    role.model.playAniDown(`${animbaseStr}_down`);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
那么如何快速查看已有哪些动画呢？&lt;br /&gt;
&lt;br /&gt;
[动作查看器](https://webdev.popx.com/v1/game/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=0bkyyeesxnzsrd9s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
=== 添加按键控制 ===&lt;br /&gt;
看demo中的代码，提供了一个MoveComponent类&lt;br /&gt;
摇杆的显示资源放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录中&lt;br /&gt;
&lt;br /&gt;
[[File:摇杆资源.png]]&lt;br /&gt;
&lt;br /&gt;
以下是示例中添加摇杆控制的代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        //摇杆背景&lt;br /&gt;
        let rocker = new XImage(&#039;res-extra/imgs/stick_bg.png&#039;);&lt;br /&gt;
        rocker.width = rocker.height = 164;&lt;br /&gt;
        rocker.pivotX = rocker.pivotY = 82;&lt;br /&gt;
        rocker.pos(178, -198);&lt;br /&gt;
        leftbottom.addChild(rocker);&lt;br /&gt;
        //摇杆方向&lt;br /&gt;
        let imgdir = new XImage(&#039;res-extra/imgs/stick_dir.png&#039;);&lt;br /&gt;
        imgdir.centerX = imgdir.centerY = 0;&lt;br /&gt;
        imgdir.anchorX = imgdir.anchorY = 0.5;&lt;br /&gt;
        imgdir.name = &amp;quot;dir&amp;quot;;&lt;br /&gt;
        rocker.addChild(imgdir);&lt;br /&gt;
        //摇杆中心点&lt;br /&gt;
        let node = new XImage(&#039;res-extra/imgs/stick_rocker.png&#039;);&lt;br /&gt;
        node.centerX = node.centerY = 0;&lt;br /&gt;
        node.anchorX = node.anchorY = 0.5;&lt;br /&gt;
        node.name = &amp;quot;node&amp;quot;;&lt;br /&gt;
        rocker.addChild(node);&lt;br /&gt;
        //移动&lt;br /&gt;
        let hero = this.heros.get(this.roleIndex);&lt;br /&gt;
        if (hero) {&lt;br /&gt;
            let movecomponent = rocker.addComponent(MoveComponent) as MoveComponent;&lt;br /&gt;
            movecomponent.init(hero, ctrlayer, rocker);&lt;br /&gt;
        }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
示例中，添加了一个触摸区域挂载在根节点的左下角，触摸反馈会根据&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递给节点&lt;br /&gt;
代码中根据摇杆方向变化来控制人物移动&lt;br /&gt;
其中涉及了部分修改玩家状态的服务器代码，可以先忽略&lt;br /&gt;
&lt;br /&gt;
=== 摄像机的跟随 ===&lt;br /&gt;
代码中，摄像机跟随人物移动，只需要简单的一行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let hero = new Xmodel();&lt;br /&gt;
//这是全局的摄像机&lt;br /&gt;
XPopManager.instance.xScene.camera.focus = hero;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
摄像机的旋转视角，demo中也提供了一个类&#039;&#039;&#039;CameraComponent&#039;&#039;&#039;&lt;br /&gt;
和摇杆的节点类似，通过&#039;&#039;&#039;XEvent&#039;&#039;&#039;来传递点击事件，&lt;br /&gt;
根据点击事件的拖动，来修改摄像机的旋转视角 &#039;&#039;&#039;xcamera.rotate&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    private onCtrlMouseMove(e: XEvent): void {&lt;br /&gt;
        if(this._touchId != e.touchId) return;&lt;br /&gt;
        let xcamera = XPopManager.instance.xScene.camera as XCamera;&lt;br /&gt;
        if(!xcamera)return;&lt;br /&gt;
        if(this._mouseX == 0) this._mouseX = e.stageX;&lt;br /&gt;
        if(this._mouseY == 0) this._mouseY = e.stageY;&lt;br /&gt;
        let offx = e.stageX-this._mouseX;&lt;br /&gt;
        let offy = e.stageY-this._mouseY;&lt;br /&gt;
        this._mouseX = e.stageX;&lt;br /&gt;
        this._mouseY = e.stageY;&lt;br /&gt;
        xcamera.rotate(offx, offy);&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
=== 砖块说明 ===&lt;br /&gt;
[[File:砖块说明.png]]&lt;br /&gt;
&lt;br /&gt;
如图所示，我们的世界地图由若干个方形大&#039;&#039;&#039;地块&#039;&#039;&#039;组成，&lt;br /&gt;
&lt;br /&gt;
每个地块里，可以划分成32*32的平面坐标，一层层叠高，可以有64层高，&lt;br /&gt;
&lt;br /&gt;
转化为三维坐标系就是32*64*32，对应xyz轴，&lt;br /&gt;
&lt;br /&gt;
地块上，可以添加以坐标为单位的&#039;&#039;&#039;砖块&#039;&#039;&#039;。&lt;br /&gt;
&lt;br /&gt;
其中，cubeId表示砖块的类型，比如草地就是id为1的砖块，该砖块具有可以踩踏行走的特性。&lt;br /&gt;
&lt;br /&gt;
某些其他的砖块具有别的特性，都通过cubeid来区分，当开发者接触地图编辑器时，就会有所了解。&lt;br /&gt;
&lt;br /&gt;
我们以Demo中，&#039;&#039;&#039;MoveComponent&#039;&#039;&#039;中的一段代码来参考&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
            // 游戏世界地图&lt;br /&gt;
            let scene = XPopManager.instance.xScene;&lt;br /&gt;
            // 判断水平方向是否有砖块&lt;br /&gt;
            // 根据坐标位置，找到世界中，其坐标对应的砖块id&lt;br /&gt;
            let cubeid = scene.getCube(dir.x ,position.y, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                // 根据砖块id，找到砖块的类型，判断其是否和人物碰撞阻挡&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.x = position.x;&lt;br /&gt;
                    dir.z = position.z;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            //如果没有砖块或者有无阻挡砖块，给一个向下的位移&lt;br /&gt;
            cubeid = scene.getCube(dir.x ,position.y-0.1, dir.z)&lt;br /&gt;
            if(cubeid){&lt;br /&gt;
                scene.getCubeInfo(cubeid, this.cubeInfo);&lt;br /&gt;
                if(!this.cubeInfo.collisionPlayer){&lt;br /&gt;
                    dir.y = position.y-0.1;&lt;br /&gt;
                }&lt;br /&gt;
            }else{&lt;br /&gt;
                dir.y = position.y-0.1;&lt;br /&gt;
            }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*再以图中为例，v3的坐标代表了地图上的位置，(24,3,26)&lt;br /&gt;
*如果需要设置模型在地图上的位置，用如下代码&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
let pos = new XVector3(x,y,z);&lt;br /&gt;
xModel.position = pos;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 资源加载说明 ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 演示加载图片 ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        // 测试加载图片&lt;br /&gt;
        // 创建一个全屏的挂载UI层&lt;br /&gt;
        let mainui = new XImage();&lt;br /&gt;
        mainui.left = mainui.right = mainui.top = mainui.bottom = 0;&lt;br /&gt;
        XPopManager.rootNode.addChild(mainui);&lt;br /&gt;
&lt;br /&gt;
        // 图片路径，如果是本地的额外资源，就加上&amp;quot;res-extra/&amp;quot;前缀，否则就是远端资源&lt;br /&gt;
        let path = &#039;avatarstart/ball_btn2.png&#039;;&lt;br /&gt;
        // 创建图片&lt;br /&gt;
        let img = new XImage(path);&lt;br /&gt;
        // 添加到父节点显示&lt;br /&gt;
        mainui.addChild(img);&lt;br /&gt;
        // 设置位置&lt;br /&gt;
        img.left = img.top = 100;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
编译，运行后，可以看到，添加了指定的图片&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载一.png]] &lt;br /&gt;
&lt;br /&gt;
=== 公共资源 ===&lt;br /&gt;
PGT为开发者提供了很多基础的图片，模型，音频等等的资源。&lt;br /&gt;
&lt;br /&gt;
====&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;怎么查看有哪些公共资源呢？&amp;lt;/span&amp;gt;====&lt;br /&gt;
&lt;br /&gt;
*打开项目目录下的package.json文件&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载二.png]] &lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;有sdkVersion&amp;lt;/span&amp;gt; =====&lt;br /&gt;
*调用查看当前的远端sdk版本，即资源版本，比如&amp;quot;sdkVersion&amp;quot;: &amp;quot;1.0.15&amp;quot;,&lt;br /&gt;
*具体有哪些资源版本可用，可以调用&#039;&#039;&#039;npm run sdklist &#039;&#039;&#039;来查看&lt;br /&gt;
*把下面链接里的v1.0.16替换成和你目前版本一致的数字，就可以查看远端资源列表&lt;br /&gt;
&lt;br /&gt;
[[File:sdkVersion.png]] &lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/sdk/v1.0.16/version.json)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;sdkVersion&amp;gt;1.2.60的版本&amp;lt;/span&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
同样是查看pacakge.json，但是字段换成了&amp;quot;resVersion&amp;quot;: &amp;quot;1.0.5&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;resVersion&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
如下是1.0.5版本的资源列表，可以修改数字查找不同版本&lt;br /&gt;
&lt;br /&gt;
[远端文件列表json](https://h5ppreview.jumpw.com/popion/popx-res/version-1.0.5.json)&lt;br /&gt;
&lt;br /&gt;
也可以用命令来查找显示不同的资源版本&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看资源版本列表&#039;&#039;&#039;：npm run reslist (只包含正式发布的版本)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;查看所有资源版本列表&#039;&#039;&#039;：npm run reslistall&lt;br /&gt;
&lt;br /&gt;
===== 查看json文件 =====&lt;br /&gt;
&lt;br /&gt;
*json文件左侧是游戏内加载的路径，右侧是实际的打过md5值的网络路径&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载四.png]] &lt;br /&gt;
&lt;br /&gt;
这就是我们刚刚演示的加载图片&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;avatarstart/ball_btn2.png&amp;quot;: &amp;quot;avatarstart/ball_btn2-2c717f83a3.png&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
*游戏内我们通过加载接口去加载左侧路径&lt;br /&gt;
*游戏外，直接在刚刚的cdnUrl后拼接带md5值的完整路径即可查看&lt;br /&gt;
&lt;br /&gt;
[查看网络资源](https://h5ppreview.jumpw.com/popion/sdk/v1.0.17/avatarstart/ball_btn2-2c717f83a3.png)&lt;br /&gt;
&lt;br /&gt;
[[File:资源加载五.png]] &lt;br /&gt;
&lt;br /&gt;
*音频文件同理，模型文件直接游戏内调用接口显示&lt;br /&gt;
&lt;br /&gt;
=== 额外资源 ===&lt;br /&gt;
最基础的demo中，会看到用于演示的摇杆资源是放在&#039;&#039;&#039;res-extra&#039;&#039;&#039;目录下的。&lt;br /&gt;
&lt;br /&gt;
一般来说，开发者的一些特制UI，图形，可以放在这里。&lt;br /&gt;
&lt;br /&gt;
加载时在路径前加上&amp;quot;res-extra/&amp;quot;即可。&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，额外资源调用前，要调用如下固定代码开启合图资源的配置，建议参考demo，在游戏入口就初始化调用一次&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        XResManager.instance.enableAtlas(&amp;quot;res-extra/fileconfig.json&amp;quot;,()=&amp;gt;{&lt;br /&gt;
           // 可以调用额外资源了&lt;br /&gt;
        });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
每次在调用命令，npm run ui时，会更新上述的&#039;&#039;&#039;fileconfig.json&#039;&#039;&#039;文件。&lt;br /&gt;
&lt;br /&gt;
[[#npm run ui ( 生成ui类 )]]&lt;br /&gt;
&lt;br /&gt;
==== npm run ui ( 生成ui类 ) ==== &lt;br /&gt;
如果开发者有额外的ui交互需求，希望在IDE中自己拼接UI&lt;br /&gt;
可以在指定目录下，&lt;br /&gt;
* &#039;&#039;&#039;ui_laya/laya/&#039;&#039;&#039; - 开发者可以自由扩展的ui资源目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/pages&#039;&#039;&#039; - 开发者创建的.scene文件目录&lt;br /&gt;
  * &#039;&#039;&#039;ui_laya/laya/assets/uiextra&#039;&#039;&#039; - secne文件中引用到的各种美术资源，包括音频和动画&lt;br /&gt;
&lt;br /&gt;
-运行后，会在client目录代码中，生成对应的XUIComponent类，[[#XUIComponent]]&lt;br /&gt;
&lt;br /&gt;
-运行后，会更新&#039;&#039;&#039;res-extra&#039;&#039;&#039;文件夹下的fileconfig.json&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;注意，我们目前不支持第三方模型文件，模型必须提交审核，不放在res-extra中&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 模型查看器 ===&lt;br /&gt;
*除了图片资源外，为了方便开发者使用，我们额外做了一个模型查看器&lt;br /&gt;
&lt;br /&gt;
[公共模型查看器](https://webdev.popx.com/client_host.html?author=qc_jump&amp;amp;project=resourceviewer_0926&amp;amp;diyserver=1&amp;amp;gameid=09kyye6gcmmda79s&amp;amp;editorFormal=true)&lt;br /&gt;
&lt;br /&gt;
== UI界面开发 == &lt;br /&gt;
&lt;br /&gt;
=== git拉取ui工程到本地 ===&lt;br /&gt;
&lt;br /&gt;
==== 泡饭的项目发布后获取到了git地址 ====&lt;br /&gt;
[[File:发布模板后获得git地址.png]]&lt;br /&gt;
&lt;br /&gt;
==== 查看git密钥 ====&lt;br /&gt;
&lt;br /&gt;
[[File:查看git密钥.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== 使用accesstoken来拉取项目到本地 ====&lt;br /&gt;
&lt;br /&gt;
===== 常见的git工具中=====&lt;br /&gt;
&lt;br /&gt;
git clone 地址&lt;br /&gt;
&lt;br /&gt;
输入泡饭账号和accesstoken即可拉取，注意不是泡饭的密码，是token&lt;br /&gt;
&lt;br /&gt;
===== 没有工具时用命令拉取说明=====&lt;br /&gt;
&lt;br /&gt;
[[File:如何使用accesstoken拉取.png]]&lt;br /&gt;
&lt;br /&gt;
=== 安装本地ui的编辑环境 ===&lt;br /&gt;
&lt;br /&gt;
==== laya 版本 v2.13.1 ====  &lt;br /&gt;
- [laya官方ide](https://ldc2.layabox.com/layadownload/?type=layaairide-LayaAir%20IDE%202.13.1)  &lt;br /&gt;
  &lt;br /&gt;
==== 安装项目自定义封装的cmd命令库 ====  &lt;br /&gt;
- 推荐安装方式：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
   npm i popxcmd -g --registry https://registry.popx.com&lt;br /&gt;
  &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== node 版本 v10.24.1 ====  &lt;br /&gt;
- &amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;必须使用指定的nodejs版本&amp;lt;/span&amp;gt;  ，推荐安装nvm以自由转换node版本。&lt;br /&gt;
  - [nvm下载及安装方式](https://blog.csdn.net/ppz8823/article/details/130862191)  &lt;br /&gt;
&lt;br /&gt;
*记得nvm需要换源，否则后续安装可能会出错，按照下述操作&lt;br /&gt;
&lt;br /&gt;
[[File:nvm换源.png]]&lt;br /&gt;
&lt;br /&gt;
*最下面两行修改node和npm的镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
node_mirror: https://npmmirror.com/mirrors/node/&lt;br /&gt;
npm_mirror: https://npmmirror.com/mirrors/npm/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*安装nvm后，命令行运行如下命令切换到node版本v10.24.1，一步步来:  &lt;br /&gt;
    &amp;lt;pre&amp;gt;  &lt;br /&gt;
    // 安装&lt;br /&gt;
    nvm install 10.24.1  &lt;br /&gt;
    // 等待安装完成后，切换&lt;br /&gt;
    nvm use 10.24.1  &lt;br /&gt;
    &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*nodejs也需要用下面的命令来换源，修改镜像地址&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm config set registry https://registry.popx.com/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 修改host文件 ====  &lt;br /&gt;
- 找到host文件，一般在`C:\WINDOWS\system32\drivers\etc`目录下。  &lt;br /&gt;
- 在文件末尾添加：  &lt;br /&gt;
  &amp;lt;pre&amp;gt;  &lt;br /&gt;
  199.232.68.133 raw.githubusercontent.com  &lt;br /&gt;
  &amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
=== 在编辑器中创建UI ===&lt;br /&gt;
==== 打开Laya工程 ====  &lt;br /&gt;
使用指定版本的Laya编辑器打开 `demo/ui_laya/ui_laya.laya` 项目。  &lt;br /&gt;
  &lt;br /&gt;
==== 添加.scene界面 ====  &lt;br /&gt;
在编辑器中，可以添加一个新的scene界面，并在其上放置UI元素。  &lt;br /&gt;
&lt;br /&gt;
[[File:新界面.png]]&lt;br /&gt;
&lt;br /&gt;
==== 界面添加UI组件 ====  &lt;br /&gt;
例如，添加一个名为 `btnFirst` 的按钮，并确保其 `var` 属性有值，以便后续导出。&lt;br /&gt;
&lt;br /&gt;
[[File:UI按钮有值.png]]&lt;br /&gt;
&lt;br /&gt;
添加后，记得保存，&#039;&#039;&#039;ctrl+S&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 额外资源的添加 ===&lt;br /&gt;
&lt;br /&gt;
注意，所有上述scene中用到的美术资源，必须放在Assets/uiextra/ 目录下&lt;br /&gt;
&lt;br /&gt;
否则导出ui时会失败&lt;br /&gt;
&lt;br /&gt;
=== 运行生成UI命令 ===  &lt;br /&gt;
在命令行中运行以下命令来生成UI代码：  &lt;br /&gt;
  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
npm run ui  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
运行成功后，生成的UI类将位于 `src/ui/popxMaxUI.ts` 文件中。&lt;br /&gt;
&lt;br /&gt;
[[File:UI生成代码.png]]&lt;br /&gt;
&lt;br /&gt;
其中，添加了一个和scene名相同的UI类，&lt;br /&gt;
并且，var有值的组件也一起生成了对应属性。&lt;br /&gt;
&lt;br /&gt;
=== 修改后的资源，git上传到远端 ===&lt;br /&gt;
&lt;br /&gt;
上传后，泡饭即可使用&lt;br /&gt;
&lt;br /&gt;
先git commit提交&lt;br /&gt;
注意推送时要用如下命令&lt;br /&gt;
&lt;br /&gt;
git push --force&lt;br /&gt;
&lt;br /&gt;
如果弹出输入框，账号输入泡饭账号名，密码输入accesstoken&lt;br /&gt;
&lt;br /&gt;
[[File:新泡饭推送到远端.png]]&lt;br /&gt;
&lt;br /&gt;
=== 代码控制XUIComponent ===&lt;br /&gt;
==== 注册UI类的控制文件 ====  &lt;br /&gt;
使用 `PopxBridge.regUIClass()` 方法在相应的类中注册UI类，并处理UI相关的按键逻辑。  &lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      //注册自定义的ui类&lt;br /&gt;
      PopxBridge.regUIClass(DemoUI.getClassName(), DemoUI);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
以下是我们新加UI的最基础代码，在界面显示的时候，注册点击事件，点击后打印日志&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class DemoUI extends uiextra.demoUIUI{&lt;br /&gt;
    &lt;br /&gt;
    constructor(){&lt;br /&gt;
        super();&lt;br /&gt;
      }&lt;br /&gt;
      &lt;br /&gt;
   onAwake(): void {&lt;br /&gt;
     &lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   onEnable(): void {&lt;br /&gt;
       // 设置在顶层可以被点击&lt;br /&gt;
       this.zOrder = 1;&lt;br /&gt;
       this.first_btn.on(XEvent.CLICK, this, this.onClickFirstBtn);&lt;br /&gt;
   }&lt;br /&gt;
&lt;br /&gt;
   private onClickFirstBtn(e:XEvent){&lt;br /&gt;
       console.log(&amp;quot;click first btn&amp;quot;);&lt;br /&gt;
   }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
==== 代码调用显示UI ====  &lt;br /&gt;
通过 `xUiManager.instance.openUI()` 方法来调用并显示UI界面。&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
      // 加载对应的json文件路径，并挂载在指定父节点上&lt;br /&gt;
      XUiManager.instance.openUI(&#039;res-extra/ui/demoUI&#039;, XPopManager.rootNode);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
-编译，运行&lt;br /&gt;
&lt;br /&gt;
[[File:UI点击按钮.png]]&lt;br /&gt;
&lt;br /&gt;
==== 代码调用加载组件 ====&lt;br /&gt;
*某些界面上的元素，比如卡牌，也可以类似做成一个scene，运行npm run ui 自动生成类&lt;br /&gt;
*比如类名为CardView&lt;br /&gt;
*注册和UI注册类似&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
PopxBridge.regUIClass(CardView.getClassName(), CardView);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*创建时，调用`XUiManager.instance.createView`&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XUiManager.instance.createView(path,parent,this.onLoadCardView,showData);&lt;br /&gt;
&lt;br /&gt;
onLoadCardView(view:XUIComponent)&lt;br /&gt;
{&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 前后端数据交互 == &lt;br /&gt;
&lt;br /&gt;
该篇会讲述服务器和前端的数据同步，如果想要先了解项目目录和工作流，建议先参考如下篇幅&lt;br /&gt;
[[#主要工程目录]]&lt;br /&gt;
&lt;br /&gt;
=== 进入房间（游戏世界） ===&lt;br /&gt;
如下是最基本的开发登录流程说明&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;  &lt;br /&gt;
|-  &lt;br /&gt;
! 步骤  &lt;br /&gt;
! 描述  &lt;br /&gt;
! 流向  &lt;br /&gt;
|-  &lt;br /&gt;
| 开始  &lt;br /&gt;
| 流程的开始  &lt;br /&gt;
| -&amp;gt; 登录页面&lt;br /&gt;
|-  &lt;br /&gt;
| 登录页面  &lt;br /&gt;
| 账号密码，选定地图，输入房间号（可不填）&lt;br /&gt;
| -&amp;gt; 远端服务器（官方） &lt;br /&gt;
|-  &lt;br /&gt;
| 远端服务器（官方） &lt;br /&gt;
| 获取玩家账户信息，分配到房间,房间就是我们的host代码  &lt;br /&gt;
| -&amp;gt; Client 和 -&amp;gt; Host&lt;br /&gt;
|-  &lt;br /&gt;
| Client   &lt;br /&gt;
| 监听XPopManager.onGameStart &lt;br /&gt;
| -&amp;gt; 前端逻辑开始  &lt;br /&gt;
|-  &lt;br /&gt;
| Host  &lt;br /&gt;
| 监听XPopManager.onHostStart  &lt;br /&gt;
| -&amp;gt; 房间战斗逻辑开始  &lt;br /&gt;
 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
远端服务器(官方)会处理最基本的登录，分配房间，创建游戏世界的逻辑，并且同步该世界中的游戏数据。&lt;br /&gt;
&lt;br /&gt;
通过&#039;&#039;&#039;XpopManager&#039;&#039;&#039;管理类，来向Client和Host抛出事件，开发者可以监听这些事件做处理。&lt;br /&gt;
&lt;br /&gt;
开发者需要关心的逻辑，就是Clint和Server（上图中Host）目录下的代码。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议交互 ===&lt;br /&gt;
项目采用proto来做相互通信，建议参考如下篇幅 [[#npm run proto（协议通信）]]&lt;br /&gt;
&lt;br /&gt;
=== 前后端共用配置 ===&lt;br /&gt;
前端&#039;&#039;&#039;src/client&#039;&#039;&#039;目录和后端&#039;&#039;&#039;src/server&#039;&#039;&#039;目录中，都可以调用如下代码来加载所有的表格配置&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
   CsvManager.ins.loadCsvs(this, () =&amp;gt; {&lt;br /&gt;
     // Do some thing after load&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
表格的加载逻辑和解析逻辑，可以参考CsvManager中的代码&lt;br /&gt;
&#039;&#039;&#039;src/shared/csv/CsvManager.ts&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;如何新增配置?&#039;&#039;&lt;br /&gt;
以demo中的&#039;&#039;&#039;item.csv&#039;&#039;&#039;为例.&lt;br /&gt;
*1.配置csv文件资源的根目录为&#039;&#039;&#039;res-extra/csv/&#039;&#039;&#039; ,在里面添加&#039;&#039;&#039;item.csv&#039;&#039;&#039;&lt;br /&gt;
*2.&#039;&#039;&#039;CsvManager.ts&#039;&#039;&#039;文件中，添加读取表格和解析表格的逻辑&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    // loadCsvs代码中，添加你需要解析的csv名称&lt;br /&gt;
    public loadCsvs(caller: any = null, listener: Function = null) {&lt;br /&gt;
        let assets = [&lt;br /&gt;
            this._prefix + &amp;quot;item.csv&amp;quot;,&lt;br /&gt;
        ];&lt;br /&gt;
        // XResManager加载本地资源&lt;br /&gt;
        XResManager.instance.loadText(assets, () =&amp;gt; {&lt;br /&gt;
            // 在加载完成的回调中，处理表格数据结构，生成CItemData，不再赘述&lt;br /&gt;
            this.onCompleted(caller, listener);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*3.解析表格得到的&#039;&#039;&#039;CItemData&#039;&#039;&#039;结构，定义在&#039;&#039;&#039;src/shared/dataMgr/&#039;&#039;&#039;中，可以被前后端一起调用&lt;br /&gt;
*4.如果是加载json文件，用&#039;&#039;&#039;XResManager.instance.loadText&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== 玩家战斗数据快照 ===&lt;br /&gt;
游戏战斗服务器中，每个玩家的实时信息，通过快照来更新&lt;br /&gt;
==== XVF_SnapData  ====&lt;br /&gt;
以demo中的&#039;&#039;&#039;Snap_UserData.ts&#039;&#039;&#039;为例&lt;br /&gt;
*该玩家数据中类中，包含的各种public变量，都是&#039;&#039;XVF_SnapData&#039;&#039;接口类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class Snap_UserData {&lt;br /&gt;
    //坐标旋转&lt;br /&gt;
    public position: XVF_SnapData&amp;lt;SyncPos&amp;gt;;&lt;br /&gt;
    //状态&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    //hp&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;快照类型数据，前端负责注册和监听，服务器负责修改&amp;lt;/span&amp;gt;&lt;br /&gt;
*每次该接口类型的数据发生变化时，会调用OnSet方法，这类数据的修改逻辑是：服务器修改，前端监听变化&lt;br /&gt;
** 前端登录成功后，服务器注册对应玩家数据&lt;br /&gt;
** 前端对数据中的某个XVF_SnapData做监听，注册OnSet方法&lt;br /&gt;
** 服务器战斗逻辑修改了该数据时，前端根据OnSet的逻辑做表现处理&lt;br /&gt;
&lt;br /&gt;
服务器先注册全局的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  let userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xHost.RegisterSnapshot(`UserData${i}`, userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
前端绑定同一个地址的快照&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  this._userData = new Snap_UserData();&lt;br /&gt;
  XPopManager.instance.xRemote.RegisterSnapshot(`UserData${index}`, this._userData);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*XVF_SnapData数据，如果是复杂类型，需要调用UpdateData()方法，前端才能触发OnSet&lt;br /&gt;
如下的SyncState就是复杂类型&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public state: XVF_SnapData&amp;lt;SyncState&amp;gt;;&lt;br /&gt;
    public setState(key:string, obj:string):void{&lt;br /&gt;
        this.state.value.key = key;&lt;br /&gt;
        this.state.value.obj = obj;&lt;br /&gt;
        this.state.UpdateData();&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
*XVF_SnapData如果是基础类型，不需要调用UpdateData()&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    public hp: XVF_SnapData&amp;lt;number&amp;gt;;&lt;br /&gt;
    public setHP(hp:number):void {&lt;br /&gt;
        this.hp.value = hp;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 代码如何调试 ===&lt;br /&gt;
==== 前端调试 ====&lt;br /&gt;
直接在vscode中打断点即可，&lt;br /&gt;
可以很方便的观察堆栈和变量信息&lt;br /&gt;
&lt;br /&gt;
[[File:前端调试.png]]&lt;br /&gt;
&lt;br /&gt;
==== 本地服务器调试 ====&lt;br /&gt;
*本地服务器在调用命令&#039;&#039;&#039;npm run server&#039;&#039;&#039;后，会启动一个Host网页&lt;br /&gt;
*只要是网页，那就能调试js代码&lt;br /&gt;
*打开网页的开发者工具（一般浏览器是f12）,找到&#039;&#039;&#039;index.js&#039;&#039;&#039;文件&lt;br /&gt;
*这时有个问题，发现打不了断点，原因是代码编译过程被混淆了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试一.png]]&lt;br /&gt;
&lt;br /&gt;
*我们需要关闭混淆的配置，在&#039;&#039;&#039;rollup.config.js&#039;&#039;&#039;中修改&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试二.png]]&lt;br /&gt;
&lt;br /&gt;
*修改plugins中，关闭sourcemap，不使用terser()压缩&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试三.png]]&lt;br /&gt;
&lt;br /&gt;
*修改完后，每次编译后运行的服务器Host代码就都可以断点了&lt;br /&gt;
&lt;br /&gt;
编译，启动服务器，运行&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm run build&lt;br /&gt;
npm run server&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*这次会发现，Host的网页代码有了行号，可以断点调试了&lt;br /&gt;
&lt;br /&gt;
[[File:服务器调试四.png]]&lt;br /&gt;
==== 调试注意事项 ====&lt;br /&gt;
*每次&#039;&#039;&#039;npm run build&#039;&#039;&#039;后，如果修改了服务器代码，可以在本地host的浏览器里，立刻刷新，F5或者shift+f5&lt;br /&gt;
*&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;如果修改了编译脚本来调试服务器，记得最后打包上传前，把rollup的代码改回来&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 地图的制作和选取 ==&lt;br /&gt;
=== 地图官网地址 ===&lt;br /&gt;
*[[上传流程|地图编辑器页签可以操作]]&lt;br /&gt;
&lt;br /&gt;
=== 地图制作导出 ===&lt;br /&gt;
在项目目录&#039;&#039;&#039;/地图编辑器操作说明&#039;&#039;&#039;中，有视频教程&lt;br /&gt;
&lt;br /&gt;
[[File:地图编辑视频.png]]&lt;br /&gt;
&lt;br /&gt;
=== 开发环境选择地图 ===&lt;br /&gt;
我们在登录界面，可以通过填写你gameid中的地图名字，来进入指定地图。&lt;br /&gt;
&lt;br /&gt;
所以必须要先创建gameid，并且选择至少一张地图，该地图才能在调试时进入。&lt;br /&gt;
&lt;br /&gt;
举例：&lt;br /&gt;
&lt;br /&gt;
*上传地图到某个游戏gameid时，会从你自己的&#039;&#039;&#039;可用地图&#039;&#039;&#039;中选择。&lt;br /&gt;
&lt;br /&gt;
- 可用地图指的是你自己制作上传了地图。或者其他人共享给了你地图。&lt;br /&gt;
&lt;br /&gt;
*比如你的可用地图名字是&amp;quot;推箱子&amp;quot;。&lt;br /&gt;
&lt;br /&gt;
*右边会有可填写的自定义地图名字，比如&amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
那么登录的时候就是填写 &amp;quot;第一关&amp;quot;&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;也就是说，地图做完后，必须要在审核平台上，添加到玩法里，生成自定义的名字，才可以在测试时填写进去生效 &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== 打包与上传 ==&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击发布游戏.png]]&lt;br /&gt;
&lt;br /&gt;
点击发布后，会把构建生成的压缩包上传到远端，就可以去试玩了。&lt;br /&gt;
&lt;br /&gt;
=== 玩法测试 ===&lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击打开审核后台.png]]&lt;br /&gt;
&lt;br /&gt;
* [[上传流程|查看文档中试玩/审核/发布的部分]]&lt;br /&gt;
&lt;br /&gt;
== 项目目录 ==  &lt;br /&gt;
  &lt;br /&gt;
=== 主要工程目录 ===  &lt;br /&gt;
对于开发者，需要关心和操作的目录如下：  &lt;br /&gt;
  &lt;br /&gt;
* &#039;&#039;&#039;node_modules/&#039;&#039;&#039; - 通过npm安装的PGC库文件，对外暴露的方法，可以通过bridge.d.ts来外部引用  &lt;br /&gt;
* &#039;&#039;&#039;res-extra/&#039;&#039;&#039; - 开发者除了PGC的远程资源外，需要额外使用到的资源目录，比如UI，图片，音频，表格，最后会打包上传到PGC远端资源服务器  &lt;br /&gt;
* &#039;&#039;&#039;ui/&#039;&#039;&#039; - 开发者自己在laya编辑器中，拼接的界面json文件，运行命令后，会转化为PGT指定的XUIComponent存放于此  &lt;br /&gt;
* &#039;&#039;&#039;src/&#039;&#039;&#039; - 开发者所有的战斗逻辑代码修改都在此进行  &lt;br /&gt;
  * &#039;&#039;&#039;src/client/&#039;&#039;&#039; - 前端代码，入口文件index.ts，监听XPopManager.onGameStart，前端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/server/&#039;&#039;&#039; - 后端代码，入口文件index.ts，监听XPopManager.onHostStart，后端游戏逻辑  &lt;br /&gt;
  * &#039;&#039;&#039;src/shared/&#039;&#039;&#039; - 前后端共用的数据结构，包括表格，协议，玩家数据&lt;br /&gt;
* &#039;&#039;&#039;dist/&#039;&#039;&#039; - 编译后，生成的运行文件，包含前后端代码&lt;br /&gt;
  * &#039;&#039;&#039;dist/client/&#039;&#039;&#039; - 编译后生成的混淆的前端代码  &lt;br /&gt;
  * &#039;&#039;&#039;dist/host/&#039;&#039;&#039; - 编译后服务端的代码文件，里面也包括表格数据的缓存&lt;br /&gt;
&lt;br /&gt;
=== 前后端协议通信生成 ===  &lt;br /&gt;
&lt;br /&gt;
[[File:泡饭点击生成协议.png]]&lt;br /&gt;
&lt;br /&gt;
==== 协议通信 ====  &lt;br /&gt;
该命令会用到tsrpc的proto协议，  &lt;br /&gt;
把&#039;&#039;&#039;src/shared/protocols&#039;&#039;&#039;下的指定格式文件，  &lt;br /&gt;
转换到&#039;&#039;&#039;src/shared/protocols/generated/serviceProto&#039;&#039;&#039;文件中，  &lt;br /&gt;
  &lt;br /&gt;
* 文件名为：Ptl{接口名}.ts  &#039;&#039;&#039;&amp;lt;span style=&amp;quot;color: red;&amp;quot;&amp;gt;（Ptl 是 Protocol 的缩写，l是小写的L）&amp;lt;/span&amp;gt;&#039;&#039;&#039;    &lt;br /&gt;
* 请求类型名为： Req{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
* 响应类型名为：Res{接口名}，通过 interface 或 type 定义，需 export  &lt;br /&gt;
  &lt;br /&gt;
例如，添加&#039;&#039;&#039;PtlPosition.ts&#039;&#039;&#039;文件  &lt;br /&gt;
文件代码如下  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
// 该接口用于向服务器同步自身的坐标  &lt;br /&gt;
export interface ReqPosition {  &lt;br /&gt;
    i: number;  &lt;br /&gt;
    x: number;  &lt;br /&gt;
    y: number;  &lt;br /&gt;
    z: number;  &lt;br /&gt;
    ry: number;  &lt;br /&gt;
}  &lt;br /&gt;
export interface ResPosition {  &lt;br /&gt;
    ret: string;  &lt;br /&gt;
    diy: string;  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;serviceProto.ts&#039;&#039;&#039;中会增加可外部调用的api类型  &lt;br /&gt;
&amp;lt;pre&amp;gt;  &lt;br /&gt;
export interface ServiceType {  &lt;br /&gt;
    api: {  &lt;br /&gt;
        &amp;quot;Position&amp;quot;: {  &lt;br /&gt;
            req: ReqPosition,  &lt;br /&gt;
            res: ResPosition  &lt;br /&gt;
        },  &lt;br /&gt;
    },  &lt;br /&gt;
    msg: {  &lt;br /&gt;
  &lt;br /&gt;
    }  &lt;br /&gt;
}  &lt;br /&gt;
&amp;lt;/pre&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;前端代码调用请求&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 前端可以用promise语法来调用同步位置的Request，异步处理服务器的返回值&lt;br /&gt;
   let remote = XPopManager.instance.xRemote;&lt;br /&gt;
   let ret = await remote.Request(&#039;Position&#039;, {i: 1, x:2, y:3, z:4, ry:5});&lt;br /&gt;
   if(ret &amp;amp;&amp;amp; ret.isSucc){&lt;br /&gt;
       console.log(ret.res.diy)&lt;br /&gt;
   }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
&#039;&#039;&#039;后端代码监听请求处理返回&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
// 服务器收到前端请求的处理返回RegisterRpc&lt;br /&gt;
      let host = XPopManager.instance.xHost;&lt;br /&gt;
      //当收到坐标变化请求&lt;br /&gt;
      host.RegisterRpc(&amp;quot;Position&amp;quot;, async (call: ApiCall&amp;lt;ReqPosition, ResPosition&amp;gt;) =&amp;gt; {&lt;br /&gt;
      let index = call.req.i;&lt;br /&gt;
      let userData = this.usersData.get(index);&lt;br /&gt;
      userData.setPosition(call.req.x, call.req.y, call.req.z, call.req.ry);&lt;br /&gt;
      return call.succ({&lt;br /&gt;
       ret: &amp;quot;Success&amp;quot;,&lt;br /&gt;
       diy: &amp;quot;自定义参数&amp;quot;&lt;br /&gt;
     });&lt;br /&gt;
   });&lt;br /&gt;
&amp;lt;/pre&amp;gt;   &lt;br /&gt;
如果想更多了解proto的结构和数据类型说明，可以参考官方链接  &lt;br /&gt;
[tsrpc-proto格式说明](https://tsrpc.cn/docs/server/service-proto.html)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 核心类目说明 ==&lt;br /&gt;
=== XPopManager ===&lt;br /&gt;
Bridge.d.ts中的核心类&lt;br /&gt;
&lt;br /&gt;
对游戏逻辑进行了结构化的拆封和管理，&lt;br /&gt;
&lt;br /&gt;
可以理解为，&lt;br /&gt;
&lt;br /&gt;
库文件内处理了各种玩家的逻辑，比如登录，加载地图，分配房间，快照数据同步等等,然后关键的几个节点，暴露给外部去执行游戏逻辑。&lt;br /&gt;
&lt;br /&gt;
开发者只需要去编写独立游戏模块的代码，其中，&#039;&#039;&#039;Xremote&#039;&#039;&#039;暴露给前端处理，&#039;&#039;&#039;Xhost&#039;&#039;&#039;暴露给后端处理&lt;br /&gt;
&lt;br /&gt;
下面是最重要的几个生命周期函数&lt;br /&gt;
;onGameStart:玩家已经完成了登录步骤，已经分配好了房间，通知前端游戏开始&lt;br /&gt;
;onHostStart:玩家已经完成了登录步骤，已经分配好了房间，通知后端游戏开始&lt;br /&gt;
;onGameOver:游戏结束（一般是玩家都离开了战斗房间）&lt;br /&gt;
;onUpdate:每一帧会调用更新&lt;br /&gt;
&lt;br /&gt;
我们在demo中，index.ts里，可以给这几个方法监听绑定函数，输出日志就可以查看&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
XPopManager.onGameStart = ()=&amp;gt;{&lt;br /&gt;
    new Main();&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameStart&amp;quot;); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onGameOver = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onGameOver&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onHostStart = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;qc,XPopManager.onHostStart&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
XPopManager.onUpdate = () =&amp;gt; {&lt;br /&gt;
    console.log(&amp;quot;XPopManager.onUpdate&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:XPopManager生命周期.png]]&lt;br /&gt;
&lt;br /&gt;
=== XRemote(前端) ===&lt;br /&gt;
*以下是最关键的生命周期和方法调用，调用逻辑可以自定义设计，后端代码同理&lt;br /&gt;
;UserID:玩家进入游戏的唯一id&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，前端可以在此根据返回的userId来同步XHost玩家的快照数据，创建角色等等&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，前端根据userId来处理模型的销毁&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XHost代码中绑定的地址一致，才可以收到OnSet回调&lt;br /&gt;
;Request:发送RPC请求，请求和回调的代码由protocol生成&lt;br /&gt;
&lt;br /&gt;
=== XHost(后端) ===&lt;br /&gt;
;onUserEnter:每当房间内玩家进入时会调用，初始化玩家身上的快照数据，可以根据人数来判断游戏阶段，比如准备阶段&lt;br /&gt;
;onUserLeave:每当房间内玩家离开时会调用，清理玩家数据，可以根据玩家人数来判断是否进入结算或者游戏结束&lt;br /&gt;
;RegisterSnapshot:用于注册绑定快照，快照类型开发者自定义，需要和XRemote代码中绑定的地址一致&lt;br /&gt;
;RegisterRpc:注册RPC回调&lt;br /&gt;
&lt;br /&gt;
=== XUIComponent(UI组件) ===&lt;br /&gt;
UI组件通过XUiManager.instance.OpenUI或者XUiManager.instance.CreateView加载显示&lt;br /&gt;
;showUI:调用后会设置active为true，当调用创建的方法，加载成功显示时也会调用一次，可以在创建时传参，每次主动调用也可以传参。&lt;br /&gt;
;hideUI:把该组件的active设置成false&lt;br /&gt;
;closeUI:把该组件从父节点移除，可以传参处理移除成功时的回调&lt;br /&gt;
=== XEventDispatcher(事件)===&lt;br /&gt;
我们可以根据已有的XEventDispatcher来封装一个单例，这样就能外部夸文件调用事件了。&lt;br /&gt;
如下为演示封装的单例&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
export class EventManager extends XEventDispatcher{&lt;br /&gt;
&lt;br /&gt;
    constructor () {&lt;br /&gt;
        super();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private static _instance: EventManager&lt;br /&gt;
    public static get instance(): EventManager {&lt;br /&gt;
        if (!this._instance) { this._instance = new EventManager(); }&lt;br /&gt;
        return this._instance;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的派发&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.dispatch(&amp;quot;new_event_name&amp;quot;, ...args);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
事件的监听&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
EventManager.instance.add(&amp;quot;new_event_name&amp;quot;, this, this.ParseNewEvent);&lt;br /&gt;
&lt;br /&gt;
private ParseNewEvent(...args)&lt;br /&gt;
{&lt;br /&gt;
  console.log(&amp;quot;parse new event&amp;quot;)&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 官方论坛 ==&lt;br /&gt;
*[论坛地址](https://bbs.popx.com/)&lt;br /&gt;
*可以由我们内部人员给出的邀请链接申请加入论坛。&lt;br /&gt;
*申请后，等待审核人员核实，批准后，即可浏览论坛内容，发表疑问贴。&lt;br /&gt;
*强烈建议多去论坛搜索关键字，发帖会有对应的专业人员回答问题，大家一起维护论坛生态。&lt;br /&gt;
*常见问题faq帖子会持续维护&lt;br /&gt;
&lt;br /&gt;
[论坛FAQ](https://bbs.popx.com/t/topic/53)&lt;/div&gt;</summary>
		<author><name>Liuweichen</name></author>
	</entry>
</feed>