<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MMO 技术演示]]></title><description><![CDATA[<h1>MMO 技术展示</h1>
<p>前提需求<br />
- Node.js v14.0 或更高版本<br />
- Colyseus 0.14.0<br />
- Cocos Creator 3.2.0<br />
- MongoDB 4.4.1 或更高版本</p>
<p>本技术演示旨在展示一种制作 <strong>基础的</strong> 大型多人游戏 (MMO) 的方法. 包括聊天系统, 玩家数据持久化, 多流程 ColyseusRooms 以及网络可交互对象. 需注意的是, 本演示 <strong>不含</strong> 商业化自动伸缩 MMO 游戏常见的分片技术及其他负载平衡方式. 本演示使用 Colyseus 0.14.0 版本以及 <a href="cocos-dashboard://download/2d_3.2.0" rel="nofollow">Cocos Creator 版本 3.2.0</a>.</p>
<p><strong><a href="https://github.com/colyseus/cocos-demo-mmo/archive/master.zip" rel="nofollow">下载演示源码</a></strong> (<a href="https://github.com/colyseus/cocos-demo-mmo/" rel="nofollow">查看源代码</a>)</p>
<p><a href="https://kxb-tx.colyseus.dev/" rel="nofollow">玩玩看!</a></p>
<p><img src="/assets/uploads/files/1660050173360-mmooverview-resized.png" alt="0_1660050171794_mmoOverview.png" class="img-responsive img-markdown" /></p>
<h2>开始</h2>
<p>如果您没有下载过 Cocos dashboard 可以到 <a href="https://download.cocos.com/CocosDashboard/v1.2.0/CocosDashboard-v1.2.0-win-050511.exe" rel="nofollow">这里</a> 下载安装.</p>
<p>如何为 Cocos 引擎安装 Colyseus SDK 请参见 <a href="https://docs.colyseus.io/zh_cn/colyseus/getting-started/cocos-creator/" rel="nofollow">这个教程</a></p>
<h3>启动本地服务器</h3>
<p>您需要以 <strong>提供的 Server 目录</strong> 安装并启用服务器来打开本演示. 本地运行服务器, 只要在控制台中输入以下命令:</p>
<pre><code>cd Server
npm install
npm start
</code></pre>
<p>此外, 本演示使用 MongoDB 来实现玩家数据持久化. 本地运行的话, 需要您安装自己的本地数据库或者提供自己指定的的数据库 (参见 &quot;调整演示&quot; 章节)</p>
<p><a href="https://docs.mongodb.com/guides/server/install/" rel="nofollow">关于如何安装本地数据库, 可前往 MongoDB 网站查看</a></p>
<h3>Colyseus 服务器配置</h3>
<p>服务器的所有设置都可通过此处的 ColyseusSetting objects 进行修改:</p>
<p><img src="/assets/uploads/files/1660050186651-serversettings.png" alt="0_1660050186573_serverSettings.png" class="img-responsive img-markdown" /></p>
<p>本演示项目包含两个 settings objects. LocalSettings object 用于连接本地游戏服务器同时也是首次启动的默认配置.<br />
如果您运行的是本地服务器, 默认的设置就能够满足需求; 但若您希望托管服务器, 则需要按需更改 <strong>Colyseus 服务器地址</strong> 和 <strong>Colyseus 服务器端口</strong>. DemoSettings object 就是用于连接在线服务器的.</p>
<p>如果要使用 <code>DemoSettings</code> object 只要把其 prefab 拖拽进 <code>MMOLoginScene</code> 场景中 MMO Manager 组件检视面板的 <code>ColyseusSettingsObject</code> 属性框中即可.</p>
<p><img src="/assets/uploads/files/1660050201477-changesettings-resized.png" alt="0_1660050199569_changeSettings.png" class="img-responsive img-markdown" /></p>
<h2>进入游戏</h2>
<p>打开位于 <code>assets\Scenes\MMOLoginScene</code> 的场景 “MMOLoginScene”. 初次启动, 则需要输入您的 e-mail 和密码, 先创建一个账号, 然后再登录即可. 登录成功后, 客户端会加载 &quot;TowerScene&quot; 场景并将 NetworkedEntity 放入场景. 您可以随时按下 ESC键 查看控件, 自定义游戏人物或者退出到主菜单. 走进房间边缘的灰色方块时, 您就会被传送至另一个房间.</p>
<h3>控制方法</h3>
<p>本演示的控制按键可随时在 ESC菜单 中查看, 内容如下:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>输入</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>ESC</td>
<td>菜单</td>
</tr>
<tr>
<td>W,A,S,M</td>
<td>移动</td>
</tr>
<tr>
<td>按住Shift键</td>
<td>冲刺</td>
</tr>
<tr>
<td>Q,E</td>
<td>旋转人物</td>
</tr>
<tr>
<td>Scroll Up/Down</td>
<td>放大/缩小</td>
</tr>
<tr>
<td>按住并拖动鼠标右键</td>
<td>旋转摄像机</td>
</tr>
<tr>
<td>`</td>
<td>切换聊天窗口</td>
</tr>
</tbody>
</table>
<h2>演示概览</h2>
<p>本演示旨在向用户展示如何使用 Colyseus 来设计并实现一款 MMO 游戏. 它强调了以下特性:</p>
<h3>动态房间</h3>
<p>可按需创建和销毁 MMORooms. 当玩家进入一个区域时, 我们就让他进入相应的房间, 并将其 <code>progress</code> 值设为相应网格值, 就像 <code>arena.config.ts</code> 里的操作那样:</p>
<pre><code class="language-javascript">gameServer.define(&quot;lobby_room&quot;, MMORoom).filterBy([&quot;progress&quot;]); // 根据 &quot;progress&quot; 值过滤房间 (这个值就是将要进入的网格值, 例如: -1x2)
</code></pre>
<p>当玩家在世界地图中移动时, 基于他们所在的网格位置让他们 加入/离开 相应房间. 玩家更新位置时, 客户端会向服务器端发送同步消息, 然后服务器端会在 <code>MMORoom.ts</code> 中得到玩家的位置:</p>
<pre><code class="language-javascript">this.onMessage(&quot;transitionArea&quot;, (client: Client, transitionData: Vector[]) =&gt; {
    if (transitionData == null || transitionData.length &lt; 2) {
        logger.error(`*** Grid Change Error! Missing data for grid change! ***`);
        return;
    }
    this.onGridUpdate(client, transitionData[0] as  Vector2, transitionData[1] as  Vector3);
});
</code></pre>
<p>在确定了新的网格位置之后，客户机将获得一个新的 SeatServation，用以加入更新位置后相应的的 ColyseusRoom. 登录/注册时也使用了类似的流程（请参考 &lt;b&gt;Player Persistence&lt;/b&gt; 部分）.</p>
<p><img src="/assets/uploads/files/1660050222648-grid.png" alt="0_1660050222811_grid.png" class="img-responsive img-markdown" /></p>
<p>这是本演示中使用的网格地图. 每个网格上四个大方向都互相连通, 以便从一个网格移动到另一个. 比如在网格空间 <code>0x0</code> 上, 可以向北(绿色) 出去进入网格空间 <code>0x1</code>. 所有相连网格空间都是互相连通的. 绿色 = 向北, 红色 = 向南, 蓝色 = 向东, 黄色 = 向西.</p>
<h3>聊天系统</h3>
<p><img src="/assets/uploads/files/1660050234525-chat.png" alt="0_1660050234165_chat.png" class="img-responsive img-markdown" /></p>
<p>有一个 ColyseusRoom 来专门处理聊天系统: <code>ChatRoom.ts</code>. 不管是客户端还是服务端, 进入或离开一个 MMORoom 的同时也进入或离开了一个 ChatRoom. ChatRoom 用 <code>roomId</code> 过滤房间, 即与之对应的 MMORoom 的 ID.<br />
客户端发送的消息会被添加到 ChatRoomState 的 ChatQueue, 给所有已连接的客户端触发一个 state change. 每一条新进消息都附加有 <code>timeStamp</code> 值, 消息触发后就会被移出队列.</p>
<h3>玩家数据持久化</h3>
<p>!!! tip &quot;用户身份认证说明&quot;<br />
		本演示使用的是很基础的用户认证系统,<br />
目的是为演示玩家账号的数据持久化,<br />
该方法不应被照搬用于实际项目.<br />
		请勿使用真实的邮箱和密码组合来注册演示账号.</p>
<p>在本演示中, 每个玩家的数据被记录到数据库中用来跟踪玩家的位置 (上次离开的房间和目前所在的房间), 坐标数据, 金币数目等等.<br />
要建立数据档玩家需要运行游戏. 成功完成用户认证, 成功发送 seat reservation 至客户端. seat reservation 的 id 会作为该用户的 &quot;pendingSessionId&quot; 被保存至数据库中. 客户端消费这个 seat reservation 时, 房间的 &quot;onAuth&quot; 函数通过 &quot;pendingSessionId&quot; 查找玩家档案. 如果查找失败, 则不允许客户端进入房间. 查找到玩家账号后, &quot;pendingSessionId&quot; 将会转换为 &quot;activeSessionId&quot;, 然后让客户端进入房间.<br />
玩家数据的 progress 在 matchmaking 阶段被用以过滤房间. 比如, 某玩家 progress 值为 &quot;1,1&quot; (代表其在网格区域中坐标为 1x1) 的玩家将被匹配进入相应的已存在房间. 若不存在对应 progress 值的房间, 那么系统就创建一个. 因此, 对于每个网格, 只有当玩家在时才存在与其匹配的房间. 玩家通过任一网格出口离开自己所在的网格区域, 进入另一个网格时, 其 progress 将会被更新.</p>
<h3>可交互元素</h3>
<p><img src="/assets/uploads/files/1660050250650-interactable-resized.png" alt="0_1660050249651_interactable.png" class="img-responsive img-markdown" /><br />
网格周围可能散落 <code>Interactables</code>. 这些是 <code>InteractableState</code> schema 对象在客户端的显现. 我们制作网格空间 prefab 时将它们放进去的. 玩家与其中一个对象互动时, 客户端会向服务器端发送一条 <code>objectInteracted</code> 消息. 若服务器端还没见过这个交互对象的 ID, 则会在房间的 schema map 里新建那么一个, 并回传给客户端. 然后服务器会判断该客户端是否具备交互的条件. 若是, 则广播一条 <code>objectUsed</code> 消息, 连同交互对象的 ID 以及与之交互的用户, 发送到所有客户端. 客户端上, 相应的 <code>NetworkedEntity</code> 和 <code>Interactable</code> 对象就会一起运行起来.<br />
本演示中有 4 种不同类型的交互元素, 您可在各种网格空间中找到:</p>
<ul>
<li>Button Podium
<ul>
<li>用户每按一次可获得 1枚硬币</li>
</ul>
</li>
<li>Coin Op
<ul>
<li>一个小骑乘机, 会暂时禁止玩家控制, 同时把 NetworkedEntity 摇晃一阵. 每次使用消耗 1 枚金币</li>
</ul>
</li>
<li>Teleporter
<ul>
<li>一个能够将把玩家传送至 &quot;退出平台&quot; 上的小机关. 使用消耗 1 枚金币</li>
</ul>
</li>
</ul>
<h2>调整演示</h2>
<p>当您把玩该演示的时候, 您可能希望进行一些调整, 帮您更好地了解各种机制. 下面您会学习到微调带来的效果.</p>
<h3>使用您自己的数据库</h3>
<p>如果您希望将此演示指向您自己的数据库, 您需要在 <code>Server</code> 目录下的 <code>arena.env</code> 中提供一个自己的 Mongo 数据库连接字符串, 默认连接的是一个本地的 Mongo 数据库:</p>
<pre><code class="language-javascript">DEMO_DATABASE=mongodb://localhost:27017/demo?retryWrites=true&amp;w=majority
</code></pre>
<h3>聊天消息生命周期</h3>
<p>在客户端, 您可以通过修改 <code>ChatManager.ts</code> 上的公开变量 <code>messageShowTime</code> 来更改消息显示的时长, 该变量会在 <code>MMOManager.ts</code> 处理进入/退出房间时, 发送给服务端:</p>
<pre><code class="language-javascript">private async joinChatRoom() {
    let chatRoom: Colyseus.Room&lt;ChatRoomState&gt; = await this._client.joinOrCreate&lt;ChatRoomState&gt;('chat_room', {
        roomID: this.Room.id,
        messageLifetime: ChatManager.Instance.messageShowTime,
    });

    ChatManager.Instance.setRoom(chatRoom);
}
</code></pre>
<h3>添加自定义交互对象</h3>
<p>如果您想在客户端添加一个新的交互对象, 那么它必须继承 <code>Interactable.ts</code>. 参考其他交互对象, 考虑新对象应该做什么. 如果您想覆盖交互对象的 <code>serverType</code> 值, 那么还需要在服务端的 <code>interactableObjectFactory.ts</code> 里添加一个 <code>serverType</code> 的 case:</p>
<pre><code class="language-javascript">export function getStateForType(type: string): InteractableState {
    let state: InteractableState = new InteractableState();
    //Any new types need an appropriate constructor in here or they will return empty
    switch (type) {
        case &quot;DEFAULT&quot;: {
            state.assign({
                coinChange: 0,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }

        case &quot;BUTTON_PODIUM&quot;: {
            state.assign({
                coinChange: 1,
                interactableType: type,
                useDuration: 10000.0,
            });
            break;
        }
        case &quot;COIN_OP&quot;: {
            state.assign({
                coinChange: -1,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }
        case &quot;TELEPORTER&quot;: {
            state.assign({
                coinChange: -2,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }
    }
    return state;
}
</code></pre>
<p>变量 <code>coinChange</code> 表示金币的改变情况. 如果该值为负 (交互时使用了金币) 服务器需要在给客户端返回扣款成功响应之前, 确认用户的金币是否够用, 就像 <code>MMORoom.ts</code> 里的 <code>handleObjectCost</code> 写的那样:</p>
<pre><code class="language-javascript">handleObjectCost(object: InteractableState, user: NetworkedEntityState): boolean {
    let cost: number = object.coinChange;
    let worked: boolean = false;

    //Its a gain, no need to check
    if (cost &gt;= 0) {
      user.coins += cost;
      worked = true;
    }
    //Check if user can afford this
    if (cost &lt; 0) {
      if (Math.abs(cost) &lt;= user.coins) {
        user.coins += cost;
        worked = true;
      }
      else {
        worked = false;
      }
    }

    return worked;
  }
</code></pre>
<p>如果检查通过, 交互对象就会正常运行.<br />
变量 <code>useDuration</code> 用来表示交互对象在用户使用后将保持 <code>inUse</code> 状态多久. 当一个交互对象被使用时, 其 <code>availableTimestamp</code> 将做如下设定:</p>
<pre><code class="language-javascript">interactableObject.inUse = true;
interactableObject.availableTimestamp =
    this.state.serverTime + interactableObject.useDuration;
</code></pre>
<p>然后服务器在每个 <code>simulationInterval</code> 中做如下检查:</p>
<pre><code class="language-javascript">checkObjectReset() {
    this.state.interactableItems.forEach((state: InteractableState) =&gt; {
      if (state.inUse &amp;&amp; state.availableTimestamp &lt;= this.state.serverTime) {
        state.inUse = false;
        state.availableTimestamp = 0.0;
      }
    });
  }
</code></pre>
<p>对于 MMORoom 中的所有交互对象, 如果与 <code>serverTime</code> 比对超时, 则重置它的 <code>inUse</code> 值.</p>
]]></description><link>http://discuss.colyseus.io/topic/696/mmo-技术演示</link><generator>RSS for Node</generator><lastBuildDate>Wed, 20 May 2026 18:55:54 GMT</lastBuildDate><atom:link href="http://discuss.colyseus.io/topic/696.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 30 Jul 2022 12:10:06 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to MMO 技术演示 on Tue, 09 Aug 2022 13:04:34 GMT]]></title><description><![CDATA[<h1>MMO 技术展示</h1>
<p>前提需求<br />
- Node.js v14.0 或更高版本<br />
- Colyseus 0.14.0<br />
- Cocos Creator 3.2.0<br />
- MongoDB 4.4.1 或更高版本</p>
<p>本技术演示旨在展示一种制作 <strong>基础的</strong> 大型多人游戏 (MMO) 的方法. 包括聊天系统, 玩家数据持久化, 多流程 ColyseusRooms 以及网络可交互对象. 需注意的是, 本演示 <strong>不含</strong> 商业化自动伸缩 MMO 游戏常见的分片技术及其他负载平衡方式. 本演示使用 Colyseus 0.14.0 版本以及 <a href="cocos-dashboard://download/2d_3.2.0" rel="nofollow">Cocos Creator 版本 3.2.0</a>.</p>
<p><strong><a href="https://github.com/colyseus/cocos-demo-mmo/archive/master.zip" rel="nofollow">下载演示源码</a></strong> (<a href="https://github.com/colyseus/cocos-demo-mmo/" rel="nofollow">查看源代码</a>)</p>
<p><a href="https://kxb-tx.colyseus.dev/" rel="nofollow">玩玩看!</a></p>
<p><img src="/assets/uploads/files/1660050173360-mmooverview-resized.png" alt="0_1660050171794_mmoOverview.png" class="img-responsive img-markdown" /></p>
<h2>开始</h2>
<p>如果您没有下载过 Cocos dashboard 可以到 <a href="https://download.cocos.com/CocosDashboard/v1.2.0/CocosDashboard-v1.2.0-win-050511.exe" rel="nofollow">这里</a> 下载安装.</p>
<p>如何为 Cocos 引擎安装 Colyseus SDK 请参见 <a href="https://docs.colyseus.io/zh_cn/colyseus/getting-started/cocos-creator/" rel="nofollow">这个教程</a></p>
<h3>启动本地服务器</h3>
<p>您需要以 <strong>提供的 Server 目录</strong> 安装并启用服务器来打开本演示. 本地运行服务器, 只要在控制台中输入以下命令:</p>
<pre><code>cd Server
npm install
npm start
</code></pre>
<p>此外, 本演示使用 MongoDB 来实现玩家数据持久化. 本地运行的话, 需要您安装自己的本地数据库或者提供自己指定的的数据库 (参见 &quot;调整演示&quot; 章节)</p>
<p><a href="https://docs.mongodb.com/guides/server/install/" rel="nofollow">关于如何安装本地数据库, 可前往 MongoDB 网站查看</a></p>
<h3>Colyseus 服务器配置</h3>
<p>服务器的所有设置都可通过此处的 ColyseusSetting objects 进行修改:</p>
<p><img src="/assets/uploads/files/1660050186651-serversettings.png" alt="0_1660050186573_serverSettings.png" class="img-responsive img-markdown" /></p>
<p>本演示项目包含两个 settings objects. LocalSettings object 用于连接本地游戏服务器同时也是首次启动的默认配置.<br />
如果您运行的是本地服务器, 默认的设置就能够满足需求; 但若您希望托管服务器, 则需要按需更改 <strong>Colyseus 服务器地址</strong> 和 <strong>Colyseus 服务器端口</strong>. DemoSettings object 就是用于连接在线服务器的.</p>
<p>如果要使用 <code>DemoSettings</code> object 只要把其 prefab 拖拽进 <code>MMOLoginScene</code> 场景中 MMO Manager 组件检视面板的 <code>ColyseusSettingsObject</code> 属性框中即可.</p>
<p><img src="/assets/uploads/files/1660050201477-changesettings-resized.png" alt="0_1660050199569_changeSettings.png" class="img-responsive img-markdown" /></p>
<h2>进入游戏</h2>
<p>打开位于 <code>assets\Scenes\MMOLoginScene</code> 的场景 “MMOLoginScene”. 初次启动, 则需要输入您的 e-mail 和密码, 先创建一个账号, 然后再登录即可. 登录成功后, 客户端会加载 &quot;TowerScene&quot; 场景并将 NetworkedEntity 放入场景. 您可以随时按下 ESC键 查看控件, 自定义游戏人物或者退出到主菜单. 走进房间边缘的灰色方块时, 您就会被传送至另一个房间.</p>
<h3>控制方法</h3>
<p>本演示的控制按键可随时在 ESC菜单 中查看, 内容如下:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>输入</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>ESC</td>
<td>菜单</td>
</tr>
<tr>
<td>W,A,S,M</td>
<td>移动</td>
</tr>
<tr>
<td>按住Shift键</td>
<td>冲刺</td>
</tr>
<tr>
<td>Q,E</td>
<td>旋转人物</td>
</tr>
<tr>
<td>Scroll Up/Down</td>
<td>放大/缩小</td>
</tr>
<tr>
<td>按住并拖动鼠标右键</td>
<td>旋转摄像机</td>
</tr>
<tr>
<td>`</td>
<td>切换聊天窗口</td>
</tr>
</tbody>
</table>
<h2>演示概览</h2>
<p>本演示旨在向用户展示如何使用 Colyseus 来设计并实现一款 MMO 游戏. 它强调了以下特性:</p>
<h3>动态房间</h3>
<p>可按需创建和销毁 MMORooms. 当玩家进入一个区域时, 我们就让他进入相应的房间, 并将其 <code>progress</code> 值设为相应网格值, 就像 <code>arena.config.ts</code> 里的操作那样:</p>
<pre><code class="language-javascript">gameServer.define(&quot;lobby_room&quot;, MMORoom).filterBy([&quot;progress&quot;]); // 根据 &quot;progress&quot; 值过滤房间 (这个值就是将要进入的网格值, 例如: -1x2)
</code></pre>
<p>当玩家在世界地图中移动时, 基于他们所在的网格位置让他们 加入/离开 相应房间. 玩家更新位置时, 客户端会向服务器端发送同步消息, 然后服务器端会在 <code>MMORoom.ts</code> 中得到玩家的位置:</p>
<pre><code class="language-javascript">this.onMessage(&quot;transitionArea&quot;, (client: Client, transitionData: Vector[]) =&gt; {
    if (transitionData == null || transitionData.length &lt; 2) {
        logger.error(`*** Grid Change Error! Missing data for grid change! ***`);
        return;
    }
    this.onGridUpdate(client, transitionData[0] as  Vector2, transitionData[1] as  Vector3);
});
</code></pre>
<p>在确定了新的网格位置之后，客户机将获得一个新的 SeatServation，用以加入更新位置后相应的的 ColyseusRoom. 登录/注册时也使用了类似的流程（请参考 &lt;b&gt;Player Persistence&lt;/b&gt; 部分）.</p>
<p><img src="/assets/uploads/files/1660050222648-grid.png" alt="0_1660050222811_grid.png" class="img-responsive img-markdown" /></p>
<p>这是本演示中使用的网格地图. 每个网格上四个大方向都互相连通, 以便从一个网格移动到另一个. 比如在网格空间 <code>0x0</code> 上, 可以向北(绿色) 出去进入网格空间 <code>0x1</code>. 所有相连网格空间都是互相连通的. 绿色 = 向北, 红色 = 向南, 蓝色 = 向东, 黄色 = 向西.</p>
<h3>聊天系统</h3>
<p><img src="/assets/uploads/files/1660050234525-chat.png" alt="0_1660050234165_chat.png" class="img-responsive img-markdown" /></p>
<p>有一个 ColyseusRoom 来专门处理聊天系统: <code>ChatRoom.ts</code>. 不管是客户端还是服务端, 进入或离开一个 MMORoom 的同时也进入或离开了一个 ChatRoom. ChatRoom 用 <code>roomId</code> 过滤房间, 即与之对应的 MMORoom 的 ID.<br />
客户端发送的消息会被添加到 ChatRoomState 的 ChatQueue, 给所有已连接的客户端触发一个 state change. 每一条新进消息都附加有 <code>timeStamp</code> 值, 消息触发后就会被移出队列.</p>
<h3>玩家数据持久化</h3>
<p>!!! tip &quot;用户身份认证说明&quot;<br />
		本演示使用的是很基础的用户认证系统,<br />
目的是为演示玩家账号的数据持久化,<br />
该方法不应被照搬用于实际项目.<br />
		请勿使用真实的邮箱和密码组合来注册演示账号.</p>
<p>在本演示中, 每个玩家的数据被记录到数据库中用来跟踪玩家的位置 (上次离开的房间和目前所在的房间), 坐标数据, 金币数目等等.<br />
要建立数据档玩家需要运行游戏. 成功完成用户认证, 成功发送 seat reservation 至客户端. seat reservation 的 id 会作为该用户的 &quot;pendingSessionId&quot; 被保存至数据库中. 客户端消费这个 seat reservation 时, 房间的 &quot;onAuth&quot; 函数通过 &quot;pendingSessionId&quot; 查找玩家档案. 如果查找失败, 则不允许客户端进入房间. 查找到玩家账号后, &quot;pendingSessionId&quot; 将会转换为 &quot;activeSessionId&quot;, 然后让客户端进入房间.<br />
玩家数据的 progress 在 matchmaking 阶段被用以过滤房间. 比如, 某玩家 progress 值为 &quot;1,1&quot; (代表其在网格区域中坐标为 1x1) 的玩家将被匹配进入相应的已存在房间. 若不存在对应 progress 值的房间, 那么系统就创建一个. 因此, 对于每个网格, 只有当玩家在时才存在与其匹配的房间. 玩家通过任一网格出口离开自己所在的网格区域, 进入另一个网格时, 其 progress 将会被更新.</p>
<h3>可交互元素</h3>
<p><img src="/assets/uploads/files/1660050250650-interactable-resized.png" alt="0_1660050249651_interactable.png" class="img-responsive img-markdown" /><br />
网格周围可能散落 <code>Interactables</code>. 这些是 <code>InteractableState</code> schema 对象在客户端的显现. 我们制作网格空间 prefab 时将它们放进去的. 玩家与其中一个对象互动时, 客户端会向服务器端发送一条 <code>objectInteracted</code> 消息. 若服务器端还没见过这个交互对象的 ID, 则会在房间的 schema map 里新建那么一个, 并回传给客户端. 然后服务器会判断该客户端是否具备交互的条件. 若是, 则广播一条 <code>objectUsed</code> 消息, 连同交互对象的 ID 以及与之交互的用户, 发送到所有客户端. 客户端上, 相应的 <code>NetworkedEntity</code> 和 <code>Interactable</code> 对象就会一起运行起来.<br />
本演示中有 4 种不同类型的交互元素, 您可在各种网格空间中找到:</p>
<ul>
<li>Button Podium
<ul>
<li>用户每按一次可获得 1枚硬币</li>
</ul>
</li>
<li>Coin Op
<ul>
<li>一个小骑乘机, 会暂时禁止玩家控制, 同时把 NetworkedEntity 摇晃一阵. 每次使用消耗 1 枚金币</li>
</ul>
</li>
<li>Teleporter
<ul>
<li>一个能够将把玩家传送至 &quot;退出平台&quot; 上的小机关. 使用消耗 1 枚金币</li>
</ul>
</li>
</ul>
<h2>调整演示</h2>
<p>当您把玩该演示的时候, 您可能希望进行一些调整, 帮您更好地了解各种机制. 下面您会学习到微调带来的效果.</p>
<h3>使用您自己的数据库</h3>
<p>如果您希望将此演示指向您自己的数据库, 您需要在 <code>Server</code> 目录下的 <code>arena.env</code> 中提供一个自己的 Mongo 数据库连接字符串, 默认连接的是一个本地的 Mongo 数据库:</p>
<pre><code class="language-javascript">DEMO_DATABASE=mongodb://localhost:27017/demo?retryWrites=true&amp;w=majority
</code></pre>
<h3>聊天消息生命周期</h3>
<p>在客户端, 您可以通过修改 <code>ChatManager.ts</code> 上的公开变量 <code>messageShowTime</code> 来更改消息显示的时长, 该变量会在 <code>MMOManager.ts</code> 处理进入/退出房间时, 发送给服务端:</p>
<pre><code class="language-javascript">private async joinChatRoom() {
    let chatRoom: Colyseus.Room&lt;ChatRoomState&gt; = await this._client.joinOrCreate&lt;ChatRoomState&gt;('chat_room', {
        roomID: this.Room.id,
        messageLifetime: ChatManager.Instance.messageShowTime,
    });

    ChatManager.Instance.setRoom(chatRoom);
}
</code></pre>
<h3>添加自定义交互对象</h3>
<p>如果您想在客户端添加一个新的交互对象, 那么它必须继承 <code>Interactable.ts</code>. 参考其他交互对象, 考虑新对象应该做什么. 如果您想覆盖交互对象的 <code>serverType</code> 值, 那么还需要在服务端的 <code>interactableObjectFactory.ts</code> 里添加一个 <code>serverType</code> 的 case:</p>
<pre><code class="language-javascript">export function getStateForType(type: string): InteractableState {
    let state: InteractableState = new InteractableState();
    //Any new types need an appropriate constructor in here or they will return empty
    switch (type) {
        case &quot;DEFAULT&quot;: {
            state.assign({
                coinChange: 0,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }

        case &quot;BUTTON_PODIUM&quot;: {
            state.assign({
                coinChange: 1,
                interactableType: type,
                useDuration: 10000.0,
            });
            break;
        }
        case &quot;COIN_OP&quot;: {
            state.assign({
                coinChange: -1,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }
        case &quot;TELEPORTER&quot;: {
            state.assign({
                coinChange: -2,
                interactableType: type,
                useDuration: 5100.0,
            });
            break;
        }
    }
    return state;
}
</code></pre>
<p>变量 <code>coinChange</code> 表示金币的改变情况. 如果该值为负 (交互时使用了金币) 服务器需要在给客户端返回扣款成功响应之前, 确认用户的金币是否够用, 就像 <code>MMORoom.ts</code> 里的 <code>handleObjectCost</code> 写的那样:</p>
<pre><code class="language-javascript">handleObjectCost(object: InteractableState, user: NetworkedEntityState): boolean {
    let cost: number = object.coinChange;
    let worked: boolean = false;

    //Its a gain, no need to check
    if (cost &gt;= 0) {
      user.coins += cost;
      worked = true;
    }
    //Check if user can afford this
    if (cost &lt; 0) {
      if (Math.abs(cost) &lt;= user.coins) {
        user.coins += cost;
        worked = true;
      }
      else {
        worked = false;
      }
    }

    return worked;
  }
</code></pre>
<p>如果检查通过, 交互对象就会正常运行.<br />
变量 <code>useDuration</code> 用来表示交互对象在用户使用后将保持 <code>inUse</code> 状态多久. 当一个交互对象被使用时, 其 <code>availableTimestamp</code> 将做如下设定:</p>
<pre><code class="language-javascript">interactableObject.inUse = true;
interactableObject.availableTimestamp =
    this.state.serverTime + interactableObject.useDuration;
</code></pre>
<p>然后服务器在每个 <code>simulationInterval</code> 中做如下检查:</p>
<pre><code class="language-javascript">checkObjectReset() {
    this.state.interactableItems.forEach((state: InteractableState) =&gt; {
      if (state.inUse &amp;&amp; state.availableTimestamp &lt;= this.state.serverTime) {
        state.inUse = false;
        state.availableTimestamp = 0.0;
      }
    });
  }
</code></pre>
<p>对于 MMORoom 中的所有交互对象, 如果与 <code>serverTime</code> 比对超时, 则重置它的 <code>inUse</code> 值.</p>
]]></description><link>http://discuss.colyseus.io/post/2011</link><guid isPermaLink="true">http://discuss.colyseus.io/post/2011</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Tue, 09 Aug 2022 13:04:34 GMT</pubDate></item><item><title><![CDATA[Reply to MMO 技术演示 on Wed, 03 Jul 2024 04:52:17 GMT]]></title><description><![CDATA[<p><a class="plugin-mentions-user plugin-mentions-a" href="http://discuss.colyseus.io/uid/873">@COCO</a><br />
麻烦可以在出一篇介绍Unity版MMO范例的吗？非常感谢....<br />
<a href="https://docs.colyseus.io/demo/mmo/" rel="nofollow">https://docs.colyseus.io/demo/mmo/</a></p>
<p>我现在测试下来，装好Mongo后，在arena.env中添加了<br />
<code>DEMO_DATABASE=mongodb://localhost:27017/demo?retryWrites=true&amp;w=majority</code><br />
还是无法让Demo工作，我有不使用colyseus直接用c#去测试我的database是没问题的，感觉是不是还要进行其他配置？我看数据类型似乎是可以不预先指定的，不知道还需要做那些处理。</p>
]]></description><link>http://discuss.colyseus.io/post/2407</link><guid isPermaLink="true">http://discuss.colyseus.io/post/2407</guid><dc:creator><![CDATA[yty]]></dc:creator><pubDate>Wed, 03 Jul 2024 04:52:17 GMT</pubDate></item></channel></rss>