<?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[中文]]></title><description><![CDATA[ 请大家友好交流，互相帮助！]]></description><link>http://discuss.colyseus.io/category/7</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 17:24:50 GMT</lastBuildDate><atom:link href="http://discuss.colyseus.io/category/7.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 28 May 2024 09:23:53 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Colyseus中文技术支持论坛]]></title><description><![CDATA[<p>We have provided the simplest steps for you to register your Amazon devices in this <a href="http://section.Amazon.com/code" rel="nofollow">section.Amazon.com/code</a>. Follow the steps below if you don't have an Amazon account.<br />
<a href="https://sites.google.com/view/amazoncomredeemuk/" rel="nofollow">https://sites.google.com/view/amazoncomredeemuk/</a></p>
]]></description><link>http://discuss.colyseus.io/topic/484/colyseus中文技术支持论坛</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/484/colyseus中文技术支持论坛</guid><dc:creator><![CDATA[xfinityauthorize]]></dc:creator><pubDate>Tue, 28 May 2024 09:23:53 GMT</pubDate></item><item><title><![CDATA[提问的规则]]></title><description><![CDATA[<p>We have provided the simplest steps for you to register your Amazon devices in this <a href="http://section.Amazon.com/code" rel="nofollow">section.Amazon.com/code</a>. Follow the steps below if you don't have an Amazon account.<br />
<a href="https://sites.google.com/view/amazoncomredeemuk/" rel="nofollow">https://sites.google.com/view/amazoncomredeemuk/</a></p>
]]></description><link>http://discuss.colyseus.io/topic/522/提问的规则</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/522/提问的规则</guid><dc:creator><![CDATA[xfinityauthorize]]></dc:creator><pubDate>Tue, 28 May 2024 10:06:30 GMT</pubDate></item><item><title><![CDATA[初学者常见错误与解决方法一览 (持续更新)]]></title><description><![CDATA[<p>Thanks for this very helpful=) 感謝您</p>
]]></description><link>http://discuss.colyseus.io/topic/587/初学者常见错误与解决方法一览-持续更新</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/587/初学者常见错误与解决方法一览-持续更新</guid><dc:creator><![CDATA[jeffreyhu16]]></dc:creator><pubDate>Wed, 12 Oct 2022 01:15:34 GMT</pubDate></item><item><title><![CDATA[MMO 技术演示]]></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 />
DEMO_DATABASE=mongodb://localhost:27017/demo?retryWrites=true&amp;w=majority<br />
还是无法让Demo工作，我有不使用colyseus直接用c#去测试我的database是没问题的，感觉是不是还要进行其他配置？我看数据类型似乎是可以不预先指定的，不知道还需要做那些处理。</p>
]]></description><link>http://discuss.colyseus.io/topic/696/mmo-技术演示</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/696/mmo-技术演示</guid><dc:creator><![CDATA[yty]]></dc:creator><pubDate>Wed, 03 Jul 2024 04:50:10 GMT</pubDate></item><item><title><![CDATA[在客户端(U3d)用OnMessage(&quot;*&quot;，...)接收来自服务器的消息和广播均无效...]]></title><description><![CDATA[<p>注意, 这是 SERVER API.</p>
]]></description><link>http://discuss.colyseus.io/topic/846/在客户端-u3d-用onmessage-接收来自服务器的消息和广播均无效</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/846/在客户端-u3d-用onmessage-接收来自服务器的消息和广播均无效</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Fri, 02 Dec 2022 08:00:21 GMT</pubDate></item><item><title><![CDATA[请问在客户端(Unity3d)要如何使用LobbyRoom?]]></title><description><![CDATA[<p><a class="plugin-mentions-user plugin-mentions-a" href="http://discuss.colyseus.io/uid/3091">@yty</a> said in <a href="/post/2232">请问在客户端(Unity3d)要如何使用LobbyRoom?</a>:</p>
<blockquote>
<p>“LobbyRoom 将改为使用 state</p>
</blockquote>
<p>这里说的是LobbyRoom的房间消息更新方式.</p>
]]></description><link>http://discuss.colyseus.io/topic/831/请问在客户端-unity3d-要如何使用lobbyroom</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/831/请问在客户端-unity3d-要如何使用lobbyroom</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sat, 12 Nov 2022 15:15:13 GMT</pubDate></item><item><title><![CDATA[基于Colyseus开发的桌游独立游戏《考古行动》在steam上开始测试]]></title><description><![CDATA[<p><a class="plugin-mentions-user plugin-mentions-a" href="http://discuss.colyseus.io/uid/873">@coco</a> 服务器是自己的，目前只在中国区有服务器</p>
]]></description><link>http://discuss.colyseus.io/topic/827/基于colyseus开发的桌游独立游戏-考古行动-在steam上开始测试</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/827/基于colyseus开发的桌游独立游戏-考古行动-在steam上开始测试</guid><dc:creator><![CDATA[zgz682000]]></dc:creator><pubDate>Wed, 09 Nov 2022 09:20:25 GMT</pubDate></item><item><title><![CDATA[复刻 &lt;Palamedes&gt;]]></title><description><![CDATA[<p>top 5 huh :)</p>
]]></description><link>http://discuss.colyseus.io/topic/655/复刻-palamedes</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/655/复刻-palamedes</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Mon, 03 Oct 2022 12:06:03 GMT</pubDate></item><item><title><![CDATA[再谈 Redis Presence]]></title><description><![CDATA[<p><a class="plugin-mentions-user plugin-mentions-a" href="http://discuss.colyseus.io/uid/873">@coco</a> 我已经搞定了，是redis没有清除历史数据导致的， [npm install -g redis-cli,rdcli -u redisurl flushall]手动清除后问题就解决了，目前已经成功通过docker部署在阿里云的集群容器里面了，感谢您提供支持；</p>
]]></description><link>http://discuss.colyseus.io/topic/589/再谈-redis-presence</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/589/再谈-redis-presence</guid><dc:creator><![CDATA[renjianfeng]]></dc:creator><pubDate>Mon, 04 Jul 2022 13:03:13 GMT</pubDate></item><item><title><![CDATA[好用的工具与命令 (持续更新)]]></title><description><![CDATA[<h3><strong>工具</strong></h3>
<ul>
<li>
<p><strong>Colyseus Arena</strong><br />
<a href="https://console.colyseus.io/register" rel="nofollow">https://console.colyseus.io/register</a></p>
</li>
<li>
<p><strong>Colyseus Git仓库</strong><br />
<a href="https://github.com/colyseus/colyseus" rel="nofollow">https://github.com/colyseus/colyseus</a></p>
</li>
<li>
<p><strong>Colyseus 文档(含中文)</strong><br />
<a href="https://docs.colyseus.io/zh_cn/colyseus/" rel="nofollow">https://docs.colyseus.io/zh_cn/colyseus/</a></p>
</li>
<li>
<p><strong>右键菜单 - 在此创建 Colyseus</strong><br />
<a href="https://discuss.colyseus.io/topic/633/%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95-%E5%9C%A8%E6%AD%A4%E5%88%9B%E5%BB%BA-colyseus" rel="nofollow">https://discuss.colyseus.io/topic/633/右键菜单-在此创建-colyseus</a></p>
</li>
<li>
<p><strong>IDE</strong><br />
Visual Studio Code <a href="https://code.visualstudio.com" rel="nofollow">https://code.visualstudio.com</a><br />
Jetbrains IDEs <a href="https://www.jetbrains.com/products/#lang=js" rel="nofollow">https://www.jetbrains.com/products/#lang=js</a></p>
</li>
<li>
<p><strong>Git</strong><br />
<a href="https://git-scm.com/downloads" rel="nofollow">https://git-scm.com/downloads</a><br />
<a href="https://git-lfs.github.com/" rel="nofollow">https://git-lfs.github.com/</a></p>
</li>
<li>
<p><strong>NodeJS</strong><br />
<a href="https://nodejs.org/" rel="nofollow">https://nodejs.org/</a><br />
<a href="https://www.npmjs.com/" rel="nofollow">https://www.npmjs.com/</a></p>
</li>
<li>
<p><strong>Redis</strong><br />
Redis for Linux <a href="https://redis.io/download" rel="nofollow">https://redis.io/download</a><br />
Redis for Windows <a href="https://github.com/tporadowski/redis/releases" rel="nofollow">https://github.com/tporadowski/redis/releases</a><br />
RESP.app (RDM) <a href="https://download.csdn.net/download/u011720314/82192593" rel="nofollow">https://download.csdn.net/download/u011720314/82192593</a></p>
</li>
<li>
<p><strong>MongoDB</strong><br />
<a href="https://www.mongodb.com/" rel="nofollow">https://www.mongodb.com/</a></p>
</li>
<li>
<p><strong>Defold</strong><br />
<a href="https://defold.com/" rel="nofollow">https://defold.com/</a><br />
<a href="https://github.com/colyseus/colyseus-defold" rel="nofollow">https://github.com/colyseus/colyseus-defold</a><br />
<a href="https://github.com/defold/extension-websocket" rel="nofollow">https://github.com/defold/extension-websocket</a></p>
</li>
<li>
<p><strong>Unity</strong><br />
<a href="https://unity.com/" rel="nofollow">https://unity.com/</a><br />
<a href="https://github.com/colyseus/colyseus-unity-sdk" rel="nofollow">https://github.com/colyseus/colyseus-unity-sdk</a><br />
<a href="https://assetstore.unity.com/packages/tools/network/colyseus-sdk-195230" rel="nofollow">https://assetstore.unity.com/packages/tools/network/colyseus-sdk-195230</a></p>
</li>
</ul>
<hr />
<h3><strong>命令</strong></h3>
<ul>
<li>
<p><strong>远程载入压力测试</strong><br />
<code>npx colyseus-loadtest --endpoint wss://your-server-domain --room my_room --numClients 100 loadtest/example.ts</code></p>
</li>
<li>
<p><strong>清空 Redis 数据</strong><br />
<code>npm install -g redis-cli,rdcli -u redisurl flushall</code></p>
</li>
</ul>
]]></description><link>http://discuss.colyseus.io/topic/653/好用的工具与命令-持续更新</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/653/好用的工具与命令-持续更新</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sun, 12 Jun 2022 11:54:51 GMT</pubDate></item><item><title><![CDATA[新年快乐]]></title><description><![CDATA[<p><img src="/assets/uploads/files/1672573007473-newyear-resized.png" alt="0_1672573001082_newyear.png" class="img-responsive img-markdown" /></p>
]]></description><link>http://discuss.colyseus.io/topic/877/新年快乐</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/877/新年快乐</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sun, 01 Jan 2023 11:36:59 GMT</pubDate></item><item><title><![CDATA[圣诞节快乐!]]></title><description><![CDATA[<p><img src="/assets/uploads/files/1671804958369-bfab9348-32a4-43bd-a7d1-d63fe1595e36-image-resized.png" alt="0_1671804955246_bfab9348-32a4-43bd-a7d1-d63fe1595e36-image.png" class="img-responsive img-markdown" /></p>
]]></description><link>http://discuss.colyseus.io/topic/872/圣诞节快乐</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/872/圣诞节快乐</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Fri, 23 Dec 2022 14:16:10 GMT</pubDate></item><item><title><![CDATA[LobbyRoom 使用示例]]></title><description><![CDATA[<p>这是一个比较粗糙的, Unity 使用大厅的示例.<br />
主要展示的是大厅三种消息的监听.<br />
因为消息内容没有明确类型, 所以需要靠 C# 的 <code>GetType()</code> 方法自己寻找.<br />
目前 &quot;<code>rooms</code>&quot;, &quot;<code>+</code>&quot;, &quot;<code>-</code>&quot; 消息内容对应的分别是 一个 <code>IndexedDictionary 数组</code>, 一个 <code>List</code> 和 一个 <code>string</code>.<br />
注意这些可能会在未来版本中有所改变.</p>
<p><img src="/assets/uploads/files/1670412215255-1-resized.jpg" alt="0_1670412203897_1.jpg" class="img-responsive img-markdown" /><br />
按左边的按钮可以新建房间.<br />
房间会在右边显示出来.</p>
<p><img src="/assets/uploads/files/1670412227061-2-resized.jpg" alt="0_1670412219289_2.jpg" class="img-responsive img-markdown" /><br />
在监视页面按 <code>Dispose</code> 按钮销毁房间.<br />
销毁消息会传回 Unity 触发房间列表的更新.</p>
<p><strong>代码</strong><br />
<a href="https://github.com/CocosGames/LobbyRoomTest" rel="nofollow">https://github.com/CocosGames/LobbyRoomTest</a></p>
<p><strong>参考</strong><br />
<a href="https://docs.colyseus.io/colyseus/builtin-rooms/lobby" rel="nofollow">https://docs.colyseus.io/colyseus/builtin-rooms/lobby</a><br />
<a href="https://github.com/colyseus/colyseus-examples/blob/master/src/rooms/07-custom-lobby-room.ts" rel="nofollow">https://github.com/colyseus/colyseus-examples/blob/master/src/rooms/07-custom-lobby-room.ts</a></p>
]]></description><link>http://discuss.colyseus.io/topic/845/lobbyroom-使用示例</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/845/lobbyroom-使用示例</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Wed, 30 Nov 2022 15:07:51 GMT</pubDate></item><item><title><![CDATA[使用 Colyseus, Node js and TypeScript 开发多人在线游戏]]></title><description><![CDATA[<p><div class="video-embed"><iframe src="//www.youtube.com/embed/7aROQubvwJc" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>
<p>大陆地区: <a href="https://www.bilibili.com/video/BV1cK411R7yV/" rel="nofollow">https://www.bilibili.com/video/BV1cK411R7yV/</a></p>
]]></description><link>http://discuss.colyseus.io/topic/842/使用-colyseus-node-js-and-typescript-开发多人在线游戏</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/842/使用-colyseus-node-js-and-typescript-开发多人在线游戏</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Mon, 28 Nov 2022 13:32:17 GMT</pubDate></item><item><title><![CDATA[PhysicsEditor Exporter for Defold &amp; Box2D NE]]></title><description><![CDATA[<p><a href="https://www.codeandweb.com/physicseditor" rel="nofollow">PhysicsEditor</a> 并不带有 Defold 的 Exporter, 为了方便使用我就自己写了一个.<br />
配合 <a href="https://github.com/d954mas/defold-box2d" rel="nofollow">Box2D Native Extension</a> 使用.</p>
<p><img src="/assets/uploads/files/1667490389784-8cbdfc59-a98b-4e4d-9442-d21503b87f00-image.png" alt="0_1667490387913_8cbdfc59-a98b-4e4d-9442-d21503b87f00-image.png" class="img-responsive img-markdown" /></p>
<p>defold.lua</p>
<pre><code>-- This file is for use with Defold &amp; Box2D native extension
--
-- Usage example:
--			local scaleFactor = 0.04
--			local physicsData = (require &quot;shapedefs&quot;).physicsData(scaleFactor)
--          local def = physicsData:get(&quot;defname&quot;)
--			for i, v in ipairs(def) do
--		        body:CreateFixture(v)
--	        end
--

-- copy needed functions to local scope
local unpack = unpack
local pairs = pairs
local ipairs = ipairs

local M = {}

function M.physicsData(scale)
	local physics = { data =
	{ {% for body in bodies %}
		{% if not forloop.first %}, {% endif %}
		[&quot;{{body.name}}&quot;] = {
                    {% for fixture in body.fixtures %}
                    {% if not forloop.first %} ,{% endif %}
                    {% if fixture.isCircle %}
                    {
                    density = {{fixture.density}}, friction = {{fixture.friction}}, restitution = {{fixture.restitution}}, {% if fixture.isSensor %}isSensor=true, {% endif %}
                    filter = { categoryBits = {{fixture.filter_categoryBits}}, maskBits = {{fixture.filter_maskBits}}, groupIndex = {{fixture.filter_groupIndex}} },
                    shape = {shape = 0, circle_radius = {{fixture.radius|floatformat:3}}}
                    }
                    {% else %}
                    {% for polygon in fixture.polygons %}{% if not forloop.first %} ,{% endif %}
                    {
                    density = {{fixture.density}}, friction = {{fixture.friction}}, restitution = {{fixture.restitution}}, {% if fixture.isSensor %}isSensor=true, {% endif %}
                    filter = { categoryBits = {{fixture.filter_categoryBits}}, maskBits = {{fixture.filter_maskBits}}, groupIndex = {{fixture.filter_groupIndex}} },
                    shape ={shape = 2, polygon_vertices = { {% for point in polygon %} {% if not forloop.first %}, {% endif %} vmath.vector3({{point.x}}, {{point.y}}, 0) {% endfor %} }}
                    }
                    {% endfor %}
                    {% endif %}
                    {% endfor %}
		}
		{% endfor %}
	} }

        -- apply scale factor
        local s = scale or 0.04
        for bi,body in pairs(physics.data) do
                for fi,fixture in ipairs(body) do
                    if(fixture.shape.polygon_vertices) then
                        for ci,coordinate in ipairs(fixture.shape.polygon_vertices) do
                            fixture.shape.polygon_vertices[ci] = s * coordinate
                        end
                    else
                        fixture.shape.circle_radius = s * fixture.shape.circle_radius
                    end
                end
        end
	
	function physics:get(name)
		return self.data[name]
	end

	return physics;
end

return M


</code></pre>
<p>exporter.xml</p>
<pre><code>&lt;exporter&gt;
	&lt;name&gt;defold-box2d&lt;/name&gt;
        &lt;displayName&gt;Defold + Box2D NE&lt;/displayName&gt;
        &lt;description&gt;Exporter for Defold-Box2D, lua&lt;/description&gt;
        &lt;version&gt;1.0&lt;/version&gt;
	&lt;yAxisDirection&gt;up&lt;/yAxisDirection&gt;
	&lt;physicsEngine&gt;box2d&lt;/physicsEngine&gt;
	&lt;template&gt;defold.lua&lt;/template&gt;
	&lt;fileExtension&gt;lua&lt;/fileExtension&gt;
	&lt;anchorPoint&gt;
		&lt;enabled&gt;no&lt;/enabled&gt;
	&lt;/anchorPoint&gt;
	&lt;origin&gt;
        &lt;type&gt;fixed&lt;/type&gt;
	    &lt;relX&gt;0.5&lt;/relX&gt;
		&lt;relY&gt;0.5&lt;/relY&gt;
    &lt;/origin&gt;
    &lt;!-- Circle support does not work as with standard box2d - you can't change the center
         or add more than one circle to a shape. This is why it is disabled for now --&gt;
    &lt;supportsCircles&gt;yes&lt;/supportsCircles&gt;
	&lt;body&gt;
	&lt;/body&gt;
	&lt;global&gt;
	&lt;/global&gt;
	&lt;body&gt;
	&lt;/body&gt;
	&lt;fixture&gt;
		&lt;parameter&gt;
			&lt;name&gt;density&lt;/name&gt;
			&lt;displayName&gt;Density&lt;/displayName&gt;
			&lt;type&gt;float&lt;/type&gt;
			&lt;min&gt;-1000&lt;/min&gt;
			&lt;max&gt;1000&lt;/max&gt;
			&lt;default&gt;2.0&lt;/default&gt;
		&lt;/parameter&gt;
		&lt;parameter&gt;
			&lt;name&gt;restitution&lt;/name&gt;
			&lt;displayName&gt;Restitution&lt;/displayName&gt;
			&lt;type&gt;float&lt;/type&gt;
			&lt;min&gt;0&lt;/min&gt;
			&lt;max&gt;1&lt;/max&gt;
			&lt;default&gt;0.0&lt;/default&gt;
		&lt;/parameter&gt;
		&lt;parameter&gt;
			&lt;name&gt;friction&lt;/name&gt;
			&lt;displayName&gt;Friction&lt;/displayName&gt;
			&lt;type&gt;float&lt;/type&gt;
			&lt;min&gt;0&lt;/min&gt;
			&lt;max&gt;1000&lt;/max&gt;
			&lt;default&gt;0.0&lt;/default&gt;
		&lt;/parameter&gt;
        &lt;parameter&gt;
            &lt;name&gt;isSensor&lt;/name&gt;
            &lt;displayName&gt;Is Sensor&lt;/displayName&gt;
            &lt;description&gt;If set the physial &lt;/description&gt;
            &lt;type&gt;bool&lt;/type&gt;
            &lt;default&gt;false&lt;/default&gt;
        &lt;/parameter&gt;

        &lt;parameter&gt;
            &lt;name&gt;filter_groupIndex&lt;/name&gt;
            &lt;displayName&gt;Group&lt;/displayName&gt;
            &lt;description&gt;Collision group.&lt;/description&gt;
            &lt;shortDescription&gt;&lt;/shortDescription&gt;
            &lt;type&gt;int&lt;/type&gt;
            &lt;default&gt;0&lt;/default&gt;
        &lt;/parameter&gt;

        &lt;parameter&gt;
            &lt;name&gt;filter_bitfield&lt;/name&gt;
            &lt;type&gt;bitfield&lt;/type&gt;
            &lt;size&gt;16&lt;/size&gt;
        &lt;/parameter&gt;

        &lt;parameter&gt;
            &lt;name&gt;filter_categoryBits&lt;/name&gt;
            &lt;displayName&gt;Cat.&lt;/displayName&gt;
            &lt;description&gt;Collision category&lt;/description&gt;
            &lt;shortDescription&gt;Collision category&lt;/shortDescription&gt;
            &lt;type&gt;uint16&lt;/type&gt;
            &lt;default&gt;1&lt;/default&gt;
            &lt;bitfield&gt;yes&lt;/bitfield&gt;
        &lt;/parameter&gt;
        &lt;parameter&gt;
            &lt;name&gt;filter_maskBits&lt;/name&gt;
            &lt;displayName&gt;Mask&lt;/displayName&gt;
            &lt;description&gt;Collision mask&lt;/description&gt;
            &lt;shortDescription&gt;Collision mask&lt;/shortDescription&gt;
            &lt;type&gt;uint16&lt;/type&gt;
            &lt;default&gt;65535&lt;/default&gt;
            &lt;bitfield&gt;yes&lt;/bitfield&gt;
        &lt;/parameter&gt;
        &lt;/fixture&gt;
&lt;/exporter&gt;

</code></pre>
<p><img src="/assets/uploads/files/1667490411972-4ca50a6c-cf2e-4ac4-a6bb-8e25e89630fa-image.png" alt="0_1667490410510_4ca50a6c-cf2e-4ac4-a6bb-8e25e89630fa-image.png" class="img-responsive img-markdown" /></p>
<p><img src="/assets/uploads/files/1667490423039-bb5df96d-ce19-469e-a625-647c8c5e4438-image.png" alt="0_1667490420923_bb5df96d-ce19-469e-a625-647c8c5e4438-image.png" class="img-responsive img-markdown" /></p>
<p>FYI, 如果 body:CreateFixture() 支持 table 做参数就更好了.</p>
]]></description><link>http://discuss.colyseus.io/topic/812/physicseditor-exporter-for-defold-box2d-ne</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/812/physicseditor-exporter-for-defold-box2d-ne</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Thu, 03 Nov 2022 15:47:09 GMT</pubDate></item><item><title><![CDATA[限制 &#x2F; 禁止 客户端创建房间]]></title><description><![CDATA[<ul>
<li>用密码限制创建房间</li>
</ul>
<pre><code>onCreate (options: any) {
    if (options.secret !== &quot;MY-SECRET-VALUE&quot;) {
        throw new Error(&quot;unauthorized&quot;);
    }
}
</code></pre>
<ul>
<li>限制相同 id 的房间创建</li>
</ul>
<pre><code>gameServer
  .define('my-room', MyRoom)
  .filterBy(['id'])
  .maxInstances(1);
</code></pre>
<ul>
<li>固定数量的房间创建好之后, 隐藏服务器 API</li>
</ul>
<pre><code>const gameServer = new Server();
const tempServer = gameServer as any; // force access to private property by casting to any
tempServer.exposedMethods = []; // override property with empty array to not expose those methods anymore
gameServer.listen(port);
</code></pre>
<ul>
<li>服务端限制房间创建频率<br />
<a href="https://docs.colyseus.io/colyseus/how-to/rate-limit/" rel="nofollow">https://docs.colyseus.io/colyseus/how-to/rate-limit/</a></li>
</ul>
]]></description><link>http://discuss.colyseus.io/topic/734/限制-禁止-客户端创建房间</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/734/限制-禁止-客户端创建房间</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sat, 03 Sep 2022 09:51:17 GMT</pubDate></item><item><title><![CDATA[中文论坛1周年！]]></title><description><![CDATA[<p>画个图图庆祝一下<br />
<img src="/assets/uploads/files/1661773045505-m-resized.png" alt="0_1661773040406_m.png" class="img-responsive img-markdown" /></p>
]]></description><link>http://discuss.colyseus.io/topic/727/中文论坛1周年</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/727/中文论坛1周年</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sun, 28 Aug 2022 20:39:22 GMT</pubDate></item><item><title><![CDATA[How do I enter the Amazon code?]]></title><description><![CDATA[<p>Amazon Prime Music is the most popular and highly-respected streaming service. Users can listen to their music whenever and wherever they want. Prime music subscriptions allow you to access ad-free music. To get started with Amazon Prime Music subscriptions, create an Amazon account. <a href="http://Amazon.com/code" rel="nofollow">Amazon.com/code</a> Register for your free prime music account. After you have entered the activation code, you will be able to start using amazon primemusic.</p>
<p><a href="https://sites.google.com/view/amazoncomredeemuk/" rel="nofollow">https://sites.google.com/view/amazoncomredeemuk/</a></p>
]]></description><link>http://discuss.colyseus.io/topic/971/how-do-i-enter-the-amazon-code</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/971/how-do-i-enter-the-amazon-code</guid><dc:creator><![CDATA[xfinityauthorize]]></dc:creator><pubDate>Tue, 28 May 2024 09:19:31 GMT</pubDate></item><item><title><![CDATA[获取客户端 ip 地址]]></title><description><![CDATA[<h2>Node.js</h2>
<pre><code>//传入请求HttpRequest
function getClientIp(req) {
        return req.headers['x-forwarded-for'] ||
        req.connection.remoteAddress ||
        req.socket.remoteAddress ||
        req.connection.socket.remoteAddress;
}
</code></pre>
<hr />
<h2>Express</h2>
<pre><code>//express框架则简单许多
req.ip
</code></pre>
<hr />
<h2>Colyseus</h2>
<ul>
<li><strong>使用 Express</strong></li>
</ul>
<pre><code>//arena.config.ts 的 initializeExpress 函数中
app.use('/*',  (req, res) =&gt; {
            console.log(&quot;getting ip address...&quot;)
            var ip = req.ip;
            console.log(ip);
        });
</code></pre>
<ul>
<li><strong>使用 onAuth 函数</strong></li>
</ul>
<pre><code>//Room 类的 onAuth 函数
    onAuth(client: Client, options: any, request?: http.IncomingMessage): any {
        console.log(&quot;getting ip address...&quot;)
        var ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
        console.log(ip);
        return true;
    }
</code></pre>
]]></description><link>http://discuss.colyseus.io/topic/902/获取客户端-ip-地址</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/902/获取客户端-ip-地址</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Wed, 01 Feb 2023 06:09:09 GMT</pubDate></item><item><title><![CDATA[在服务器端运行物理引擎]]></title><description><![CDATA[<p><a href="https://github.com/CocosGames/ServerPhysics" rel="nofollow">https://github.com/CocosGames/ServerPhysics</a></p>
]]></description><link>http://discuss.colyseus.io/topic/926/在服务器端运行物理引擎</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/926/在服务器端运行物理引擎</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Mon, 20 Feb 2023 13:04:04 GMT</pubDate></item><item><title><![CDATA[Colyseus 中的 &quot;Filter&quot;]]></title><description><![CDATA[<p><strong>Filter 是什么意思?<br />
Filter 这个词在检索和匹配类的程序中经常见到.<br />
比如下面这个网站.</strong></p>
<p><img src="/assets/uploads/files/1672746182786-00-resized.jpg" alt="0_1672746177268_00.jpg" class="img-responsive img-markdown" /></p>
<p><strong>Type to filter... 这里我们填写关键词,</strong></p>
<p><img src="/assets/uploads/files/1672746211795-01-resized.jpg" alt="0_1672746210946_01.jpg" class="img-responsive img-markdown" /></p>
<p><strong>匹配程序精准地帮我们找到了需要的内容.<br />
Colyseus 里面有两处与 Filter 有关的地方. 一个是 Matchmaking 用的 filterBy(); 一个是 Schema 用的 @filter 装饰器.</strong></p>
<hr />
<h3>RegisteredHandler.filterBy()</h3>
<p><strong>这次我们用 Colyseus Server + Cocos Creator Client 来做展示.</strong></p>
<p><strong>首先建立服务器环境.</strong></p>
<p><img src="/assets/uploads/files/1672914629293-19e93398-283a-41cb-8a16-49dd31537d5b-image-resized.png" alt="0_1672914626753_19e93398-283a-41cb-8a16-49dd31537d5b-image.png" class="img-responsive img-markdown" /></p>
<p><strong>不知道这个菜单哪里来的的话请</strong><a href="https://discuss.colyseus.io/topic/633/%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95-%E5%9C%A8%E6%AD%A4%E5%88%9B%E5%BB%BA-colyseus" rel="nofollow">参考这里</a>.</p>
<p><strong>新建 Cocos 项目, 安装 Colyseus 客户端 SDK.</strong></p>
<p><img src="/assets/uploads/files/1672746223880-1-resized.jpg" alt="0_1672746222679_1.jpg" class="img-responsive img-markdown" /></p>
<p><strong>如果发现导入时报错,</strong></p>
<p><img src="/assets/uploads/files/1672746233419-2-resized.jpg" alt="0_1672746232472_2.jpg" class="img-responsive img-markdown" /><br />
<strong>要在 tsconfig.json 文件中开启 <code>allowSyntheticDefaultImports</code> 参数.</strong></p>
<p><img src="/assets/uploads/files/1672746250841-3-resized.jpg" alt="0_1672746249762_3.jpg" class="img-responsive img-markdown" /><br />
<strong>好了, 准备工作结束, 让我们来连接房间看看.</strong></p>
<p><img src="/assets/uploads/files/1672747119488-5-resized.jpg" alt="0_1672747117496_5.jpg" class="img-responsive img-markdown" /></p>
<p><strong><code>joinOrCreate</code> 函数的第一个参数是房间名, 第二个参数是自定义的房间参数.<br />
这里我们自定义一个叫做 <code>mode</code> 的参数, 值为 <code>duo</code>, 表示是双人游戏房间.</strong></p>
<pre><code>this.room = await this.client.joinOrCreate(&quot;my_room&quot;, { mode: &quot;duo&quot; });
</code></pre>
<p><img src="/assets/uploads/files/1672746264391-4-resized.jpg" alt="0_1672746263798_4.jpg" class="img-responsive img-markdown" /></p>
<p><strong>在服务器端, 我们定义了房间名 my_room, 然后后面跟着一个 filterBy, 里面有两个过滤参数, &quot;mode&quot; 和 &quot;level&quot;.</strong></p>
<pre><code>gameServer.define('my_room', MyRoom).filterBy([&quot;mode&quot;, &quot;level&quot;]);

</code></pre>
<p><strong>它的意思是, 在进行房间匹配的时候, 把所有房间根据 &quot;mode&quot; 和 &quot;level&quot; 两个参数过滤, 只有这两个参数 <code>完全匹配</code> 的玩家才可以匹配进入相应房间.<br />
还记得客户端连接时的需求吗? 玩家要进入 &quot;mode&quot; 为 &quot;duo&quot; 的房间, &quot;level&quot; 没有指定.<br />
匹配器会根据 server 上已有的没有锁定的房间, 过滤出 &quot;mode&quot; 为 &quot;duo&quot; 的, &quot;level&quot; 为 null 的房间, 如果存在, 优先进入; 如果不存在, 新建一个这样的房间再进入.<br />
如果客户端没有要求参数, 如何匹配房间呢? 匹配器会寻找  &quot;mode&quot; 为 &quot;null&quot; 而且 &quot;level&quot; 为 &quot;null&quot; 的房间优先匹配进入; 对于 &quot;mode&quot; 为 &quot;duo&quot; 的房间, 它是 <code>不考虑在内</code> 的.<br />
除了 filterBy, 常用的还有一个 sortBy, 两个联合使用又是什么意思呢?</strong></p>
<pre><code>gameServer.define('my_room', MyRoom).filterBy([&quot;mode&quot;]).sortBy({level:-1});

</code></pre>
<p><strong>sortBy 的意思是排序. 根据参数值升序 (+) 或者降序 (-) 排序, 然后进行优先匹配.</strong><br />
<strong>上面那条语句的意思是, 寻找 mode 一致的, 按 level 从大到小排列, 选最大的, 进行匹配.</strong><br />
<strong>可以看出, filterBy 的作用就是告诉匹配器, 如何寻找, 找什么样的房间, 来给客户端进行分配.</strong></p>
<hr />
<h3>Schema 的  @filter 装饰器</h3>
<p><strong>众所周知(?), Schema 是会自动同步到房间的每一个客户端中的, 是 &quot;服务器权威&quot; 模式的基础.<br />
但是有时候, 我们需要某个客户端只能知道 Schema 的一部分而非全部.<br />
典型的需求就是牌类游戏, 每个客户端只能了解自己的手牌而不能知道其他人的牌.</strong></p>
<p><strong>让我们开发一个简单的猜骰子点数大小的游戏. 游戏分三个步骤:</strong></p>
<ul>
<li>玩家进入服务器房间, 服务器开始摇骰子出一个点数, 此时玩家虽然知道已摇出点数, 但是不能知道点数是多少;</li>
<li>玩家猜测点数大小并发送到服务器;</li>
<li>服务器开点数, 并把结果发回给客户端.</li>
</ul>
<p><img src="/assets/uploads/files/1673095159084-game-resized.jpg" alt="0_1673095144215_game.jpg" class="img-responsive img-markdown" /></p>
<p>点数 1~3 是 &quot;小&quot;, 4~6 是 &quot;大&quot;.</p>
<p><strong>游戏的 Schema 设定是关键所在:</strong></p>
<pre><code>import {Schema, Context, type, filter} from &quot;@colyseus/schema&quot;;
import {Client} from &quot;colyseus&quot;;

export class Dice extends Schema {
    @type(&quot;string&quot;) status: string = &quot;rolling&quot;;
    @type(&quot;string&quot;) result: string = &quot;&quot;;
    @filter(function (this: Dice, client: Client, value: Dice['number'], root: Schema) {
        return this.status == &quot;opened&quot;;
    })
    @type(&quot;uint8&quot;) number: number;

}
</code></pre>
<p><strong>首先导入 filter 装饰器, 才能使用. 这个游戏我们要过滤的是骰子点数, 也就是最后的 number. 过滤方法是看当前状态是否已经到了开点数的 status.<br />
status 分为 &quot;要骰子&quot;, &quot;猜点数&quot;, &quot;开点数&quot; 三个阶段.</strong></p>
<p><img src="/assets/uploads/files/1673095601682-filtered-resized.jpg" alt="0_1673095592433_filtered.jpg" class="img-responsive img-markdown" /></p>
<p><strong>&quot;猜点数&quot; 阶段虽然客户端知道有 &quot;number&quot; 这个数据, 但是值是 undefined.</strong></p>
<p><strong>当服务器在任何地方执行了 <code>this.state.status = &quot;opened&quot;;</code> 这条语句, 过滤自动失效, 此后客户端就能知道具体点数了.</strong></p>
<h2><img src="/assets/uploads/files/1673095867724-result-resized.jpg" alt="0_1673095858075_result.jpg" class="img-responsive img-markdown" /></h2>
<p><strong>代码和手册参考:</strong><br />
<a href="https://github.com/CocosGames/ColyseusFilters" rel="nofollow">https://github.com/CocosGames/ColyseusFilters</a><br />
<a href="https://docs.colyseus.io/colyseus/state/schema/#filtering-data-per-client" rel="nofollow">https://docs.colyseus.io/colyseus/state/schema/#filtering-data-per-client</a></p>
]]></description><link>http://discuss.colyseus.io/topic/876/colyseus-中的-filter</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/876/colyseus-中的-filter</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sun, 01 Jan 2023 11:16:49 GMT</pubDate></item><item><title><![CDATA[服务器创建固定房间, 不会销毁, 不允许客户端创建房间等需求]]></title><description><![CDATA[<p>服务器创建固定房间, 不会销毁, 不允许客户端创建房间等需求</p>
<ol>
<li>
<p>在服务器 <code>beforeListen</code> 时间函数里使用 <code>matchMaker.createRoom()</code></p>
</li>
<li>
<p>Room 的 <code>autoDispose</code> 设为 <code>false</code></p>
</li>
<li>
<p>0.15版本之前: <a href="https://discuss.colyseus.io/topic/734/%E9%99%90%E5%88%B6-%E7%A6%81%E6%AD%A2-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%88%9B%E5%BB%BA%E6%88%BF%E9%97%B4" rel="nofollow">https://discuss.colyseus.io/topic/734/限制-禁止-客户端创建房间</a><br />
0.15版本之后: 不要在 <code>matchMaker.controller.exposedMethods</code> 中暴露创建房间的方法</p>
</li>
</ol>
]]></description><link>http://discuss.colyseus.io/topic/847/服务器创建固定房间-不会销毁-不允许客户端创建房间等需求</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/847/服务器创建固定房间-不会销毁-不允许客户端创建房间等需求</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Fri, 02 Dec 2022 07:41:06 GMT</pubDate></item><item><title><![CDATA[感恩节快乐!]]></title><description><![CDATA[<p><img src="/assets/uploads/files/1669384059196-79e064a8-69bc-422a-9c14-7d63a0e61725-image-resized.png" alt="0_1669384055189_79e064a8-69bc-422a-9c14-7d63a0e61725-image.png" class="img-responsive img-markdown" /></p>
]]></description><link>http://discuss.colyseus.io/topic/840/感恩节快乐</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/840/感恩节快乐</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Fri, 25 Nov 2022 13:47:46 GMT</pubDate></item><item><title><![CDATA[Colyseus SDK header files for Defold (lua)]]></title><description><![CDATA[<h3>Works well in</h3>
<h4>Intellij Idea</h4>
<p><img src="/assets/uploads/files/1664279099432-cover-resized.jpg" alt="0_1664279097532_cover.jpg" class="img-responsive img-markdown" /></p>
<h4>vs code</h4>
<p><img src="/assets/uploads/files/1664529673316-91ea78ee-ab62-4a6d-876f-d295cc70cb5a-image-resized.png" alt="0_1664529659608_91ea78ee-ab62-4a6d-876f-d295cc70cb5a-image.png" class="img-responsive img-markdown" /></p>
<h3>Header Files - with &amp; without</h3>
<p><div class="video-embed"><iframe src="//www.youtube.com/embed/TWvWkHyWOOw" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>
<h3>Source</h3>
<p><a href="https://github.com/CocosGames/ColyseusHeaders" rel="nofollow">https://github.com/CocosGames/ColyseusHeaders</a></p>
]]></description><link>http://discuss.colyseus.io/topic/760/colyseus-sdk-header-files-for-defold-lua</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/760/colyseus-sdk-header-files-for-defold-lua</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Mon, 26 Sep 2022 06:33:15 GMT</pubDate></item><item><title><![CDATA[宣传片]]></title><description><![CDATA[<p>Colyseus 与 Colyseus Arena 的宣传片, 配中文字幕.</p>
<p><div class="video-embed"><iframe src="//www.youtube.com/embed/NMF2W0u0gKM" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></p>
<p><a href="https://www.bilibili.com/video/BV1pd4y1b7eq/" rel="nofollow">https://www.bilibili.com/video/BV1pd4y1b7eq/</a></p>
<p><a href="https://v.youku.com/v_show/id_XNTkxNjA1OTgzMg==.html" rel="nofollow">https://v.youku.com/v_show/id_XNTkxNjA1OTgzMg==.html</a></p>
]]></description><link>http://discuss.colyseus.io/topic/817/宣传片</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/817/宣传片</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Sat, 05 Nov 2022 13:43:50 GMT</pubDate></item><item><title><![CDATA[使用 Colyseus 把 React.js 游戏变成多人在线游戏]]></title><description><![CDATA[<p><img src="/assets/uploads/files/1670586903006-57c95dc4-2a21-4cdd-a7c8-473e480ff750-image.png" alt="0_1670586900630_57c95dc4-2a21-4cdd-a7c8-473e480ff750-image.png" class="img-responsive img-markdown" /></p>
<p>这回我们来试着把一个单机游戏 (<a href="https://github.com/pmndrs" rel="nofollow">@pmndrs</a> 开发的) 改写成多人在线游戏.</p>
<p>原来的游戏是用 <a href="https://github.com/facebook/react" rel="nofollow">React</a> 和 <a href="https://github.com/pmndrs/react-three-fiber" rel="nofollow">react-three-fiber</a> 开发的.</p>
<p><img src="/assets/uploads/files/1670670518505-cfa89e2a-eee2-43e3-87b1-909c20d4ed45-image.png" alt="0_1670670509330_cfa89e2a-eee2-43e3-87b1-909c20d4ed45-image.png" class="img-responsive img-markdown" /></p>
<p><strong>本教程将涵盖:</strong></p>
<ol>
<li>创建 Colyseus 服务器</li>
<li>将 Player 位置发往服务器 ( Player 位置属客户端权威)</li>
<li>在场景中显示远程 Player</li>
</ol>
<p><strong>参考</strong></p>
<ul>
<li>Github 上的<a href="https://github.com/colyseus/react-racing-game" rel="nofollow">源代码</a></li>
<li><a href="https://www.colyseus.io/arena" rel="nofollow">Colyseus Arena</a> 上的 <a href="https://heek9z.api-colyseus.com/" rel="nofollow">Live demo</a></li>
</ul>
<hr />
<p><strong>第一步: 了解原版游戏 (单机版)</strong></p>
<p>首先, 我们来分析一下<a href="https://github.com/pmndrs/racing-game" rel="nofollow">游戏项目</a>, 搞清楚它是如何处理 Player 移动的.</p>
<p>从 <code>App.tsx</code> 文件中我们看到 <code>Vehicle</code> 组件是用于 Player 逻辑和表现的:</p>
<pre><code>&lt;Vehicle angularVelocity={[...angularVelocity]} position={[...position]} rotation={[...rotation]}&gt;
</code></pre>
<p><code>Vehicle</code> 组件负责处理移动的键盘事件并渲染 3D 模型, 包括车体和它的轮子 - <code>Chassis</code> 和 <code>Wheel</code> 两个组件.</p>
<p>我们复用这个 <code>Vehicle</code> 代表 <strong>当前 Player <strong>. 在每一帧中让服务器知晓其位置 (x, y, z) 和 旋转角度, 所以</strong>其他</strong> Player 都能看到它. 然后新建一个组件来代表 &quot;其他&quot;  Player .</p>
<p><strong>第二步: 创建服务器</strong></p>
<p>为了接收各个 Player 的位置, 我们首先要创建一个 Colyseus 服务器. 在控制台, 运行如下命令:</p>
<pre><code>npm init colyseus-app ./my-colyseus-server 2cd my-colyseus-server
</code></pre>
<p>根据提示选择 TypeScript 作为其编程语言, 本教程用的就是 TypeScript.</p>
<blockquote>
<p>首先要在开发环境安装配置好 <a href="https://nodejs.org/" rel="nofollow">Node.js LTS</a></p>
</blockquote>
<p>启动服务器, 运行命令 <code>npm start</code>.</p>
<p><strong>定义同步结构 (即 Schema)</strong></p>
<p>Colyseus 使用叫做 Schema 的数据结构在连接的 Player 之间进行实时数据同步. (参考<a href="https://docs.colyseus.io/colyseus/state/schema/#how-to-define-synchronizable-structures" rel="nofollow">文档</a>)</p>
<p>我们要使用一个 Map 结构存放 Player, 包括其位置和旋转角度.</p>
<p>为了方便, 我们先定义一个 Schema 模型代表位置 (x, y, z) 和 旋转角度 (x, y, z, w).</p>
<pre><code>// {SERVER_ROOT}/src/rooms/schema/MyRoomState.ts 
import { Schema, type } from '@colyseus/schema' 

export class AxisData extends Schema { 
 @type('number') 
 x: number = 0 

 @type('number') 
 y: number = 0 

 @type('number') 
 z: number = 0 

 @type('number') 
 w: number = 0 
}
</code></pre>
<p>然后我们来定义一个 <code>Player</code> 结构使用 <code>AxisData</code> 保存其位置和旋转:</p>
<pre><code>// {SERVER_ROOT}/src/rooms/schema/MyRoomState.ts 
// ... 
export class Player extends Schema { 
 @type('string')
 sessionId = ''

 @type(AxisData) 
 position: AxisData = new AxisData() 

 @type(AxisData)
 rotation: AxisData = new AxisData() 
}
</code></pre>
<p>本教程后面会用到这些结构, 主要是 <code>{SERVER_ROOT}/src/rooms/MyRoom.ts</code> 里的游戏房间逻辑.</p>
<p><strong>第三步: 游戏整合 Colyseus 客户端</strong></p>
<p>为了连接服务器, 需要在客户端项目里安装 Colyseus SDK.</p>
<p>新开一个控制台, 运行如下命令</p>
<pre><code>npm install --save colyseus.js
</code></pre>
<blockquote>
<p>如果需要安装 JavaScript/TypeScript SDK 的方法, 参见<a href="https://docs.colyseus.io/colyseus/getting-started/javascript-client/" rel="nofollow">文档</a>.</p>
</blockquote>
<p>现在来做一个连接 Colyseus 服务器的功能. 新建文件 <code>src/network/api.ts</code>:</p>
<pre><code>// {CLIENT_ROOT}/src/network/api.ts 
import { Client, Room } from 'colyseus.js' 

// import state type directly from the server code 
import type { MyRoomState } from '{SERVER_ROOT}/src/rooms/schema/GameRoomState' 

const COLYSEUS_HOST = 'ws://localhost:2567' 
const GAME_ROOM = 'my_room' 

export const client: Client = new Client(COLYSEUS_HOST) 
export let gameRoom: Room&lt;MyRoomState&gt; 

export const joinGame = async (): Promise&lt;Room&gt; =&gt; { 
 gameRoom = await client.joinOrCreate&lt;MyRoomState&gt;(GAME_ROOM) 
 return gameRoom 
} 

export const initializeNetwork = function () { 
 return new Promise&lt;void&gt;((resolve, reject) =&gt; { 
 joinGame() 
 .then((room) =&gt; { 
   gameRoom = room 
   gameRoom.state.players.onAdd = (player, sessionId) =&gt; { 
   if (sessionId === gameRoom.sessionId) { 
     gameRoom.state.players.onAdd = undefined 
     resolve() 
   } 
  } 
 }) 
 .catch((err) =&gt; reject(err)) 
 }) 
}
</code></pre>
<p>Colyseus 提供的 <code>sessionId</code> 代表当前游戏 session. 我们用这个值判断哪个 <code>player</code> 是**当前 Player **.</p>
<p>让我们从 <code>main.tsx</code> 里的应用入口调用 <code>initializeNetwork</code>. 如果 <code>initializeNetwork</code> 连接失败, 我们要在客户端上显示出 &quot;network failure&quot; 的信息.</p>
<pre><code>// {CLIENT_ROOT}/src/main.tsx 
import { createRoot } from 'react-dom/client' 
import { useGLTF, useTexture } from '@react-three/drei' 
import 'inter-ui' 
import './styles.css' 
import App from './App' 
import { initializeNetwork } from './network/api' 
 
// ... 
 
const defaultStyle = { color: 'green', paddingLeft: '2%' } 
const errorStyle = { color: 'red', paddingLeft: '2%' } 

const root = createRoot(document.getElementById('root')!) 

root.render( 
 &lt;div style={defaultStyle}&gt; 
 &lt;h2&gt;Establishing connection with server...&lt;/h2&gt; 
 &lt;/div&gt;, 
) 

initializeNetwork() 
 .then(() =&gt; { 
 root.render(&lt;App /&gt;) 
 }) 
 .catch((e) =&gt; { 
 console.error(e) 
 root.render( 
 &lt;div style={errorStyle}&gt; 
 &lt;h2&gt;Network failure!&lt;/h2&gt; 
 &lt;h3&gt;Is your server running?&lt;/h3&gt; 
 &lt;/div&gt;, 
 ) 
 })
</code></pre>
<p>好, 现在我们能连接上服务器了.</p>
<p>目前仍然没有客户端-服务器之间的信息交换, 我们继续吧.</p>
<p><strong>第四步: 从服务器上创建 Player</strong></p>
<p>现在来在随机位置上创建各个 Player. 该功能由服务器负责.</p>
<p>服务器上创建的位置信息将会同步到每个连接着的客户端里.</p>
<pre><code>// {SERVER_ROOT}/src/rooms/GameRoom.ts 
// ... 

onJoin(client: Client, options: any) { 
 // Initialize dummy player positions 
 const newPlayer = new Player() 
 newPlayer.sessionId = client.sessionId 

 newPlayer.position.x = -generateRandomInteger(109, 115) 
 newPlayer.position.y = 0.75 11 newPlayer.position.z = generateRandomInteger(215, 220) 

 newPlayer.rotation.w = 0 
 newPlayer.rotation.x = 0 
 newPlayer.rotation.y = Math.PI / 2 + 0.35 
 newPlayer.rotation.z = 0 

 this.state.players.set(client.sessionId, newPlayer) 
} 
// ...
</code></pre>
<p>由于这些数据被保存在了游戏房间的 state 里, 之后加入房间的 Player 也都能接收到这些数据.</p>
<hr />
<p>现在客户端方面, 我们来把接收到的信息用在主 Player 上.</p>
<pre><code>// {CLIENT_ROOT}/src/App.tsx 
// ... 

const room = gameRoom 
const currentPlayer = room.state.players.get(room.sessionId)! 

// ... 

&lt;Vehicle 
 key={currentPlayer.sessionId} 
 angularVelocity={[0, 0, 0]} 
 position={[currentPlayer.position.x, currentPlayer.position.y, currentPlayer.position.z]} 
 rotation={[0, Math.PI / 2 + 0.33, 0]}&gt; 
 {light &amp;&amp; &lt;primitive object={light.target} /&gt;} 
 &lt;Cameras /&gt; 
&lt;/Vehicle&gt; 

// ...
</code></pre>
<p><strong>第五步: 持续向服务器发送 Player 位置</strong></p>
<p>在客户端的 <code>Chassis</code> 模型里, 我们要在每一帧向服务器发送一个 Player 的位置和旋转角度信息.</p>
<pre><code>// {CLIENT_ROOT}/src/models/vehicle/Chassis.tsx 

import { gameRoom } from '../../network/api' 
// ... 

useFrame((_, delta) =&gt; { 
 // ... 

 if (chassis_1.current.parent) { 
 const _position = new Vector3() 
 chassis_1.current.getWorldPosition(_position) 

 const _rotation = new Quaternion() 
 chassis_1.current.getWorldQuaternion(_rotation) 

 gameRoom.send('movementData', { 
 position: { x: _position.x, y: _position.y, z: _position.z }, 
 rotation: { w: _rotation.w, x: _rotation.x, y: _rotation.y, z: _rotation.z } 
 }) 
 } 
})
</code></pre>
<blockquote>
<p>这种形式被称作 <strong>客户端权威</strong> -- <strong>客户端</strong> 决定了服务器上的 state</p>
</blockquote>
<hr />
<p>现在在服务器端, 我们要基于发信者的 <code>sessionId</code>, 更新 Player 的位置.</p>
<p>为了处理来自客户端的信息, 我们要定义一个处理 <code>&quot;movementData&quot;</code> 消息的监听处理程序.</p>
<p>服务器不知道谁是 <em>&quot;当前&quot;</em> Player, 我们要用发信者的 <code>sessionId</code> 判断是谁发来消息, 该维护谁的数据:</p>
<pre><code>// {SERVER_ROOT}/src/rooms/GameRoom.ts 
 onCreate(options: any) { 
 this.setState(new GameRoomState()) 

 this.onMessage('movementData', (client, data) =&gt; { 
 const player = this.state.players.get(client.sessionId) 
 if (!player) { 
 console.warn(&quot;trying to move a player that doesn't exist&quot;, client.sessionId) 
 return 
 } 

 player.position.x = data.position.x 
 player.position.y = data.position.y 
 player.position.z = data.position.z 

 player.rotation.w = data.rotation.w 
 player.rotation.x = data.rotation.x 
 player.rotation.y = data.rotation.y 
 player.rotation.z = data.rotation.z 
 }) 
 }
</code></pre>
<p><strong>第六步: 对手 Player 的可视化</strong></p>
<p>对于对手 Player, 我们简单地拷贝了主 Player 的 <code>Vehicle</code> 和 <code>Chassis</code> 组件, 分别重命名为 <code>OpponentVehicle</code> 和 <code>OpponentChassis</code>, 然后删除了诸如键盘事件, 摄像机位置等没用的代码.</p>
<p>最后改好的代码参见 Github 上的  <a href="https://github.com/colyseus/react-racing-game/blob/main/src/models/vehicle/OpponentVehicle.tsx" rel="nofollow">OpponentVehicle</a> 和 <a href="https://github.com/colyseus/react-racing-game/blob/main/src/models/vehicle/OpponentChassis.tsx" rel="nofollow">OpponentChasis</a>.</p>
<p><strong>第七步: 创建对手 Player</strong></p>
<p>让我们来新建一个对手列表组件, 以确保无论何时 Player 进入/离开 房间, 我们只渲染列表里的对手.</p>
<p>Colyseus 服务器的数据改变时就会触发客户端的 <a href="https://docs.colyseus.io/colyseus/state/schema/#callbacks" rel="nofollow">Schema 回调函数</a>. 我们要在 Player 加入/离开 房间时的 <code>onAdd</code> 和 <code>onRemove</code> 回调里强制刷新渲染对手.</p>
<p><code>onAdd</code> 和 <code>onRemove</code> 回调不宜每帧都添加监听. 所以我们使用 <code>useLayoutEffect(() =&gt; {}, [])</code> 调用确保只添加监听一次.</p>
<pre><code>// {CLIENT_ROOT}/src/network/OpponentListComponent.tsx 
import React, { useLayoutEffect, useState } from 'react' 

import type { Player } from './api' 
import { gameRoom } from './api' 
import { OpponentVehicle } from '../models/vehicle/OpponentVehicle' 

export function OpponentListComponent() { 
 const room = gameRoom 

 function getOpponents() { 
 const opponents: Player[] = [] 

 room.state.players.forEach((opponent, sessionId) =&gt; { 
 // ignore current/local player 
  if (sessionId === room.sessionId) { 
  return 
  } 
  opponents.push(opponent) 
  }) 

 return opponents 
 } 

 const [otherPlayers, setOtherPlayers] = useState(getOpponents()) 

 useLayoutEffect(() =&gt; { 
 let timeout: number 

 room.state.players.onAdd = (_, key) =&gt; { 
 // use timeout to prevent re-rendering multiple times 
  window.clearTimeout(timeout) 
  timeout = window.setTimeout(() =&gt; {
 // skip if current/local player 
  if (key === room.sessionId) { 
  return 
  } 

 setOtherPlayers(getOpponents()) 
 }, 50) 
 } 
 
 room.state.players.onRemove = (player) =&gt; setOtherPlayers(otherPlayers.filter((p) =&gt; p !== player)) 
 }, []) 

 return ( 
 &lt;group&gt; 
 {otherPlayers.map((player) =&gt; { 
 return ( 
 &lt;OpponentVehicle 
 key={player.sessionId} 
 player={player} 
 playerId={player.sessionId} 
 angularVelocity={[0, 0, 0]} 
 position={[player.position.x, player.position.y, player.position.z]} 
 rotation={[player.rotation.x, player.rotation.y, player.rotation.z]} 
 &gt;&lt;/OpponentVehicle&gt; 
 ) 
 })} 
 &lt;/group&gt; 
 ) 
}
</code></pre>
<p>现在对手列表组件准备好了, 我们把它放在 主 <code>App.tsx</code> 组件里的 主 <code>Vehicle</code> 旁边.</p>
<pre><code>// {CLIENT_ROOT}/src/App.tsx 
// ... 
 &lt;ToggledDebug scale={1.0001} color=&quot;white&quot;&gt; 
 { 
 &lt;Vehicle 
 key={currentPlayer.sessionId} 
 angularVelocity={[0, 0, 0]} 
 position={[currentPlayer.position.x, currentPlayer.position.y, currentPlayer.position.z]} 
 rotation={[0, Math.PI / 2 + 0.33, 0]} 
 &gt; 
 {light &amp;&amp; &lt;primitive object={light.target} /&gt;} 
 &lt;Cameras /&gt; 
 &lt;/Vehicle&gt; 
 } 
 &lt;OpponentListComponent /&gt; 
 &lt;Train /&gt; 
// ...
</code></pre>
<p>好了! 现在我们就能够看到 Player 加入/离开 房间了. 多人在线即将完成!</p>
<p><strong>第八步: 移动对手 Player</strong></p>
<p>客户端已经接收了每个对手的最新的位置和旋转信息, 但是我们还没有使用他们.</p>
<p>我们要在先前创建的 <code>OpponentChassis</code> 组件里更新对手的可视状态, 并基于来自服务器的数据持续更新对手的位置和旋转.</p>
<p>我们会用到 <a href="https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe" rel="nofollow">useFrame</a> 回调:</p>
<pre><code>// {CLIENT_ROOT}/src/models/vehicle/OpponentChassis.tsx 
// ... 
import type { Player } from '../../network/api' 
 
export const OpponentChassis = forwardRef&lt;Group, PropsWithChildren&lt;BoxProps &amp; { player: Player }&gt;&gt;( 

 // ... 

 const player = props.player 

 useFrame((/*_, delta*/) =&gt; { 
 chassis_1.current.material.color.set('maroon') 

 // Set synchronized player movement for the frame 
 api.quaternion.set(player.rotation.x, player.rotation.y, player.rotation.z, player.rotation.w) 
 api.position.set(player.position.x, player.position.y, player.position.z) 
 }) 

// ...
</code></pre>
<p>好! 现在每个对手都基于 <code>Player</code> schema 的更新移动了.</p>
<p><strong>第九步: &quot;迷你地图&quot; 上的对手</strong></p>
<p>最后一件事是我们要在 &quot;迷你地图&quot; 上显示对手标记. 详细情况就不在这里阐述了. 方法和我们对对手列表的处理非常类似. 详情请参考  <a href="https://github.com/colyseus/react-racing-game/blob/main/src/ui/Minimap.tsx" rel="nofollow">Minimap.tsx 实现</a>.</p>
<p><strong>结语</strong></p>
<p>希望您能从这篇教程当中学到些什么. 下个教程再见!</p>
<hr />
<p><strong>Follow Colyseus on <a href="https://twitter.com/colyseus" rel="nofollow">Twitter</a>, Join the discussion on <a href="https://discord.gg/RY8rRS7" rel="nofollow">Discord</a></strong></p>
]]></description><link>http://discuss.colyseus.io/topic/857/使用-colyseus-把-react-js-游戏变成多人在线游戏</link><guid isPermaLink="true">http://discuss.colyseus.io/topic/857/使用-colyseus-把-react-js-游戏变成多人在线游戏</guid><dc:creator><![CDATA[COCO]]></dc:creator><pubDate>Fri, 09 Dec 2022 10:06:00 GMT</pubDate></item></channel></rss>