有那么一种需求, 就是玩家在开房间玩游戏的同时, 允许观众进入围观, 但是不能对观众开放玩家数据, 观众也不允许影响玩家的功能.
这篇文章就谈论一下这种需求的简单实现, Colyseus开发非常灵活, 所以方法不只一个, 本例不作为官方推荐方案, 只是抛砖引玉.

首先, 建立服务器项目, 最便捷的方法参考这里 https://discuss.colyseus.io/topic/633/右键菜单-在此创建-colyseus .

0_1650454213419_b57198ff-720b-4960-9e4a-e586c849c38f-image.png
用喜欢的IDE打开, 我喜欢用Intellij Idea 或者 webstorm.

0_1650454467558_8922f227-d86d-418e-9ae4-30f13c3d4aca-image.png
用 Git 备份到 Github 上面.

0_1650454630962_f8a49a93-ea67-435c-a48a-4bea9b3d9cc2-image.png

养成好习惯O(∩_∩)O

我们的思路很简单:
服务器里有两个不自动销毁的房间: 一个游戏室, 一个观众室.

  • 玩家进入游戏室, 两种房间显示玩家形象.
  • 玩家控制其在游戏室的坐标位置, 两种房间都可以看到其最新位置.
  • 观众室成员只能观看, 不能控制任何玩家形象.
  • 玩家退出游戏, 两种房间销毁其形象.

首先设计Schema, 我们习惯称其为 Room 的 state.
state

export class Player extends Schema {
    @type("number") x: number;
    @type("number") y: number;
}

export class MyRoomState extends Schema {
    @type({ map: Player }) players = new MapSchema<Player>();

}

需要同步给玩家的数据很简单, 只有 x, y 两个坐标点.
下面我们分别创建玩家房间和观众房间两个Room.

player room

onCreated

    let s = new MyRoomState();
    this.setState(s);
    this.autoDispose = false;

创建房间时, 设置好 state, 本例中不允许房间自动销毁.

onJoined

    let player = new Player()
    player.x = Math.random()*700+100;
    player.y = Math.random()*400+100;
    this.state.players.set(client.id, player);
    this.presence.publish("update", this.state);

玩家进入游戏房间时, 分配其一个随机位置, 存入 state, 然后把 state 广播到 "update" 频道.
如果不了解presence, 请参考 https://discuss.colyseus.io/topic/589/再谈-redis-presencehttps://docs.colyseus.io/colyseus/server/presence/ .

onMessage

      let player = this.state.players.get(client.id);
      player.x = message.x;
      player.y = message.y;
      this.state.players.set(client.id, player);
      this.presence.publish("update", this.state);

收到玩家移动数据时, 更新 state, 然后广播出去.

onLeave

    this.state.players.delete(client.id);
    this.presence.publish("update", this.state);

玩家离开游戏房间时, 删掉相应的 state 内容, 然后广播出去.

Audience Room
观众房间里并没有 state, 我们使用 "订阅 - 广播" 的方式把游戏房间的数据发送给观众客户端, 而且并不处理观众的任何消息.

  data:any = {};
  onCreate (options: any) {
    this.autoDispose = false;
    this.presence.subscribe("update", (d:any)=>{
      console.log(d);
      this.data = d;
      this.broadcast("update", this.data);
    });

  }

  onJoin (client: Client, options: any) {
    console.log(client.sessionId, "joined!");
    this.broadcast("update", this.data);
  }

到这里观众围观游戏的简单功能就已经实现了. 当然在生产环境代码应该更加复杂和完善, 但思路大体一致.

Arena

export default Arena({
    getId: () => "Your Colyseus App",

    initializeGameServer: (gameServer) => {
        /**
         * Define your room handlers:
         */
        gameServer.define('player_room', PlayerRoom);
        gameServer.define('audience_room', AudienceRoom);

    },

    options:{presence: new RedisPresence()},

    initializeExpress: (app) => {
        /**
         * Bind your custom express routes here:
         */
        app.get("/", (req, res) => {
            res.send("It's time to kick ass and chew bubblegum!");
        });

        /**
         * Bind @colyseus/monitor
         * It is recommended to protect this route with a password.
         * Read more: https://docs.colyseus.io/tools/monitor/
         */
        app.use("/colyseus", monitor());
    },


    beforeListen: () => {
        /**
         * Before before gameServer.listen() is called.
         */

        matchMaker.create("player_room").then((res)=>{
            matchMaker.create("audience_room").then((res2)=>{
                console.log(res.room.roomId, res2.room.roomId);
            })
        });



    }
});

脚本入口主文件中, 定义并建立两个房间, 然后关键一点是明确使用了 RedisPresence.
https://docs.colyseus.io/colyseus/server/presence/#redispresence-clientopts

Colyseus Arena

Colyseus Arena 是部署 Colyseus 服务器的最简单解决方案, 让您可以 Get To Fun Faster™

Arena Cloud 是一站式托管解决方案, 可让您专注于多人游戏开发. 它在服务器托管的基础上, 为开发人员提供企业级服务器管理, 基础设施建设, 开发运营和规模扩展等服务. 使用 Arena, 只需在直观的管理仪表板上点击几下, 即可完成 Colyseus 服务器的配置, 管理和更新.
点击这里注册 Arena Cloud 账户.
初级客户免费. 技术支持完善.

Arena Cloud 可以在全球多地使用, 并可应要求提供客户指定区域部署.

https://github.com/CocosGames/ColyseusAudience


客户端使用 Defold. 当然 Unity, Phaser, OpenCanvas, cocos creator 只要Colyseus支持的都行.
加入客户端 SDK
0_1650970616563_7bde3450-3116-4051-9500-0fb0b68d8888-image.png
https://github.com/colyseus/colyseus-defold/archive/refs/tags/0.14.1.zip
https://github.com/defold/extension-websocket/archive/refs/tags/3.0.0.zip
Defold需要两个库, 一个是 Colyseus 的 Defold 客户端, 一个是 Defold 的原生 Websocket 扩展库.

0_1652439899367_6fd3a92c-79ce-4aa2-bb41-0c35b800651e-image.png
新建一个GUI, 包括两个按钮, 方便用户选择作为玩家进入还是作为观众进入.

0_1652439936777_0acf2b94-775f-476e-a5cf-01a292d1fad6-image.png
每个玩家用 Defold 的logo 表示, 头上显示其 Id 信息.

0_1652439965019_dc177bb1-e552-4ebf-b3eb-f5b030e27a46-image.png
把它们整合到一起.

gui 脚本

function init(self)
	msg.post(".", "acquire_input_focus")
end

function on_input(self, action_id, action)
	if action_id == hash("touch") and action.pressed then
		local playerNode = gui.get_node("playerBtn")
		local audienceNode = gui.get_node("audienceBtn")
		if gui.pick_node(playerNode, action.x, action.y) then            
			msg.post("/players#example", "selected", {role = "player"})
			msg.post("/gui", "disable")
		elseif gui.pick_node(audienceNode, action.x, action.y) then
			msg.post("/players#example", "selected", {role = "audience"})
			msg.post("/gui", "disable")
		end
	end
end

客户端脚本

local Colyseus = require "colyseus.client"

local client
local xroom
local players = {}

function init(self)

    client = Colyseus.new("ws://localhost:2567")
    --while (not client) do end
end

function final(self)
    -- Add finalization code here
    -- Remove this function if not needed
    msg.post(".", "release_input_focus")
end

function update(self, dt)
   -- Add update code here
   -- Remove this function if not needed
end

function on_message(self, message_id, message, sender)
    print(message_id)
    pprint(message)
    if message_id == hash("selected") then
        if message.role == "player" then
            joinRoom("player")
        else
            joinRoom("audience")
        end
    end

end

function joinRoom(whichOne)
    if not client then return end
    client:join_or_create(whichOne.."_room", function(err, room)
      if (err) then
        print("JOIN ERROR: " .. err)
        return
      end
        xroom = room;
       room:on("statechange", function(state)
           updatePlayers(state.players.items)
       end
    )
        room:on_message("update", function(message)
            if message and message.players then
                updatePlayers(message.players)
            end
        end)
      print("successfully joined '" .. room.name .. "'")
        msg.post(".", "acquire_input_focus")
    end)
end

function updatePlayers(data)
    go.delete_all(players)
    for k, v in pairs(data) do
        if not players[key] then
            local p = vmath.vector3()
            p.x = v.x;
            p.y = v.y
            players[k] = factory.create("#factory", p)
            local id = tostring(players[k])
            id = string.sub(id,8,string.len(id)-1)
            label.set_text(id.."#playerName", k)
        end
    end
end

function on_input(self, action_id, action)
    if action_id==hash("touch") and client and xroom then
        xroom:send("move", { x=action.x, y=action.y })
    end
end

function on_reload(self)
   -- Add reload-handling code here
   -- Remove this function if not needed
end

好了, 启动 Redis server, Colyseus 和 Defold 看效果吧!
audiences
https://github.com/CocosGames/ColyseusAudience