用Swoole来写个联机对战游戏呀!(八)创建游戏房间

栏目: PHP · 发布时间: 5年前

内容简介:童鞋们的作业完成情况如何呢?

Logic 类:

<?php
...
class Logic
{
    public function matchPlayer($playerId)
    {
        ...
        //发起一个Task尝试匹配
        DataCenter::$server->task(['code' => TaskManager::TASK_CODE_FIND_PLAYER]);
    }
}
复制代码

Server 类:

<?php
...
class Server
{
    ...
    public function onTask($server, $taskId, $srcWorkerId, $data)
    {
        DataCenter::log("onTask", $data);
        $result = [];
        switch ($data['code']) {
            case TaskManager::TASK_CODE_FIND_PLAYER:
                $ret = TaskManager::findPlayer();
                if (!empty($ret)) {
                    $result['data'] = $ret;
                }
                break;
        }
        if (!empty($result)) {
            $result['code'] = $data['code'];
            return $result;
        }
    }
    ...
}
...
复制代码

童鞋们的作业完成情况如何呢?

我们来再次梳理一下目前的匹配功能进度:

  • 前端连接时发送 player_id
  • 服务端连接时保存玩家信息
  • 前端发送 code600 的指令
  • 服务端将 player_id 放入匹配队列
  • 服务端发起一个 task 进行玩家匹配,当寻找到两个玩家时返回两个 player_idworker 进程

那下一步就很明显了,就是创建游戏房间。

创建房间分析

  1. Server 类的 onFinish() 方法中,根据传入的 code ,执行 LogiccreateRoom() 方法。

Server 类:

<?php
...
class Server
{
    ...
    public function onFinish($server, $taskId, $data)
    {
        DataCenter::log("onFinish", $data);
        switch ($data['code']) {
            case TaskManager::TASK_CODE_FIND_PLAYER:
                $this->logic->createRoom($data['data']['red_player'], 
                                         $data['data']['blue_player']);
                break;
        }
    }
}
...
复制代码

显然,下一步就是完成这个 createRoom() 方法匹配机制就大功告成了。但是真的这么简单吗?下面我们要思考一件事情。

我们的匹配队列是存放在 Redis 中的,无论哪个 worker 都可以读取,但游戏数据是存放在内存中的,在启动 Swoole Worker 时设置了 'worker_num' => 4worker 是多进程的,这会产生什么效果呢?就是进程内存隔离。

比如, A玩家 进入了 worker_1 ,数据保存在 worker_1 进程内存中,而 B玩家 进入了 worker_2 ,数据保存在 worker_2 进程内存中。他们的匹配队列用的却是同一个 Redis List ,假如我们选择了 worker_1 进行游戏数据存放,那么 B玩家 将会读取不到内存中的游戏数据。

要解决这个问题有几个容易的方法:

Redis
worker

显然, A 方法过于粗暴,没想到竟说出如此 粗鄙之语 !而 B 方法扩展性不好,当有成千上万玩家的时候,我们的 Redis 分分钟就挂给你看。这样下来只能选择 C 方法来实践。

Swoole 为我们提供了一个 bind() 方法,就可将连接绑定到固定的一个 worker 来处理。不了解 bind() 方法的童鞋请先阅读一下官方文档,尤其是 时序问题

那么我们创建房间的流程就是:

  • 生成一个房间 room_id
  • task 寻找到的两位玩家连接的 fd 绑定到 room_id 算出的同一个 int
  • 通知玩家 room_id
  • 前端获取到 room_id 后,发起开始游戏请求

绑定玩家连接

  1. 想要使用 bind() 方法,需先将 dispatch_mode 设置为 5
  2. 完成 LogiccreateRoom() 方法,生成一个 room_id ,绑定连接 fd
  3. 获取 $server 对象,向两个玩家分别发送房间 room_id

Server 类:

<?php
...
class Server
{
    ...
    const CONFIG = [
        ...
        'dispatch_mode' => 5,
        ...
    ];
    ...
}
...
复制代码

Logic 类:

<?php
...
class Logic
{
    ...
    public function createRoom($redPlayer, $bluePlayer)
    {
        $roomId = uniqid('room_');
        $this->bindRoomWorker($redPlayer, $roomId);
        $this->bindRoomWorker($bluePlayer, $roomId);
    }

    private function bindRoomWorker($playerId, $roomId)
    {
        $playerFd = DataCenter::getPlayerFd($playerId);
        DataCenter::$server->bind($playerFd, crc32($roomId));
        DataCenter::$server->push($playerFd, $roomId);
    }
}
复制代码

童鞋们发现问题了吗?

没错,我们的 push() 方法直接就把 room_id 发过去了。又是这种问题:接收方无法识别该消息是何种消息。那么我们要如何处理呢?还是老套路,加 code 协议码。一个更好的办法是,找一个类来专门管理发送相关的变量和方法。

Manager 文件夹下,新建 Sender 类文件。

Sender 类:

<?php
namespace App\Manager;
class Sender
{
}
复制代码
  1. Sender 类中新增 MSG_ROOM_ID 常量,作为发送 room_idcode
  2. 新增方法 sendMessage($playerId, $code, $data = []) ,通过传入的 $playerId 发送固定格式的消息到客户端。比较常规的内容需要有: codemsgdata
  3. bindRoomWorker() 中发送房间 room_id 的代码改为使用 Sender 发送。

Sender 类:

<?php
...
class Sender
{
    const MSG_ROOM_ID = 1001;

    const CODE_MSG = [
        self::MSG_ROOM_ID => '房间ID',
    ];

    public static function sendMessage($playerId, $code, $data = [])
    {
        $message = [
            'code' => $code,
            'msg' => self::CODE_MSG[$code] ?? '',
            'data' => $data
        ];
        $playerFd = DataCenter::getPlayerFd($playerId);
        if (empty($playerFd)) {
            return;
        }
        DataCenter::$server->push($playerFd, json_encode($message));
    }
}
复制代码

Logic 类:

<?php
...
class Logic
{
    ...
    private function bindRoomWorker($playerId, $roomId)
    {
        $playerFd = DataCenter::getPlayerFd($playerId);
        DataCenter::$server->bind($playerFd, crc32($roomId));
        Sender::sendMessage($playerId, Sender::MSG_ROOM_ID, ['room_id' => $roomId]);
    }
}
复制代码

这下我们的前端就能通过接收的 code 来判断,究竟这条 message房间ID 或者是 游戏数据

我们来测试一下目前为止的代码有没有问题。重启 Server 服务器,在浏览器打开两个游戏前端页面并点击匹配按钮。

[root@localhost app]# php Server.php 
master start (listening on 0.0.0.0:8811)
server: onWorkStart,worker_id:4
server: onWorkStart,worker_id:5
server: onWorkStart,worker_id:6
server: onWorkStart,worker_id:7
server: onWorkStart,worker_id:0
server: onWorkStart,worker_id:1
server: onWorkStart,worker_id:2
server: onWorkStart,worker_id:3
[2019-04-21 15:59:46][INFO]: client open fd:3
[2019-04-21 15:59:50][INFO]: client open fd:3,message:{"code":600}
[2019-04-21 15:59:50][INFO]: onTask {"code":1}
[2019-04-21 15:59:50][INFO]: onFinish {"data":{"red_player":"player_177","blue_player":"player_181"},"code":1}
PHP Warning:  Swoole\WebSocket\Server::push(): the connected client of connection[9] is not a websocket client or closed. in /mnt/htdocs/HideAndSeek_teach/app/Manager/Sender.php on line 31
复制代码

显然,程序报错了。这是因为我们启动服务器时,没有清除之前的残余玩家信息,所以 push() 时报错了。

初始化玩家数据

  1. DataCenter 中新增 initDataCenter() 方法清除 Redis 中的残余数据。
  2. onStart 的时候调用 initDataCenter() 方法。

DataCenter 类:

<?php
...
class DataCenter
{
    ...
    public static function initDataCenter()
    {
        //清空匹配队列
        $key = self::PREFIX_KEY . ':player_wait_list';
        self::redis()->del($key);
        //清空玩家ID
        $key = self::PREFIX_KEY . ':player_id*';
        $values = self::redis()->keys($key);
        foreach ($values as $value) {
            self::redis()->del($value);
        }
        //清空玩家FD
        $key = self::PREFIX_KEY . ':player_fd*';
        $values = self::redis()->keys($key);
        foreach ($values as $value) {
            self::redis()->del($value);
        }
    }
    ...
}
复制代码

Server 类:

<?php
...
class Server
{
    ...
    public function onStart($server)
    {
        ...
        DataCenter::initDataCenter();
    }
    ...
}
...
复制代码

现在再来一次,重启 Server 服务器,在浏览器打开两个游戏前端页面并点击匹配按钮。

用Swoole来写个联机对战游戏呀!(八)创建游戏房间

可以看到,服务端成功发送 room_id

发送开始游戏指令

  1. Vue 的数据属性中新增 roomId ,用于保存服务端发送的 room_id
  2. 新增方法 startRoom() ,当服务端发来 room_id 消息时,发送 code 以及 room_id 到服务端开始游戏。

本章留的Homework是前端功能,但是比较简单,请童鞋们尽力完成哦。

当前目录结构:

HideAndSeek
├── app
│   ├── Lib
│   │   └── Redis.php
│   ├── Manager
│   │   ├── DataCenter.php
│   │   ├── Game.php
│   │   ├── Logic.php
│   │   ├── Sender.php
│   │   └── TaskManager.php
│   ├── Model
│   │   ├── Map.php
│   │   └── Player.php
│   └── Server.php
├── composer.json
├── composer.lock
├── frontend
│   └── index.html
├── test.php
└── vendor
    ├── autoload.php
    └── composer
复制代码
用Swoole来写个联机对战游戏呀!(八)创建游戏房间

以上所述就是小编给大家介绍的《用Swoole来写个联机对战游戏呀!(八)创建游戏房间》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

网络多人游戏架构与编程

网络多人游戏架构与编程

格雷泽 (Joshua Glazer)、马达夫 (Sanjay Madhav) / 王晓慧、张国鑫 / 人民邮电出版社 / 2017-10-1 / CNY 109.00

本书是一本深入探讨关于网络多人游戏编程的图书。 全书分为13章,从网络游戏的基本概念、互联网、伯克利套接字、对象序列化、对象复制、网络拓扑和游戏案例、延迟、抖动和可靠性、改进的延迟处理、可扩展性、安全性、真实世界的引擎、玩家服务、云托管专用服务器等方面深入介绍了网络多人游戏开发的知识,既全面又详尽地剖析了众多核心概念。 本书的多数示例基于C++编写,适合对C++有一定了解的读者阅读。本......一起来看看 《网络多人游戏架构与编程》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具