用Swoole来写个联机对战游戏呀七异步匹配玩家

联机逻辑开发进度:■■■■■□□□□□□□

本章结束开发进度:■■■■■■■□□□□□

上一章的答案:

DataCenter类:

<?php
...
class DataCenter
{
    const PREFIX_KEY = "game";
    ...
    public static function getPlayerWaitListLen()
    {
        $key = self::PREFIX_KEY . ":player_wait_list";
        return self::redis()->lLen($key);
    }

    public static function pushPlayerToWaitList($playerId)
    {
        $key = self::PREFIX_KEY . ":player_wait_list";
        self::redis()->lPush($key, $playerId);
    }

    public static function popPlayerFromWaitList()
    {
        $key = self::PREFIX_KEY . ":player_wait_list";
        return self::redis()->rPop($key);
    }

    public static function getPlayerFd($playerId)
    {
        $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
        return self::redis()->get($key);
    }

    public static function setPlayerFd($playerId, $playerFd)
    {
        $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
        self::redis()->set($key, $playerFd);
    }

    public static function delPlayerFd($playerId)
    {
        $key = self::PREFIX_KEY . ":player_fd:" . $playerId;
        self::redis()->del($key);
    }

    public static function getPlayerId($playerFd)
    {
        $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
        return self::redis()->get($key);
    }

    public static function setPlayerId($playerFd, $playerId)
    {
        $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
        self::redis()->set($key, $playerId);
    }

    public static function delPlayerId($playerFd)
    {
        $key = self::PREFIX_KEY . ":player_id:" . $playerFd;
        self::redis()->del($key);
    }

    public static function setPlayerInfo($playerId, $playerFd)
    {
        self::setPlayerId($playerFd, $playerId);
        self::setPlayerFd($playerId, $playerFd);
    }
}

我们先来测试一下,前面所写的代码有没有问题,重新运行Server.php,并在浏览器打开游戏前端页面。

查看Redis中的键值:

127.0.0.1:6379> keys *
1) "game:player_fd:player_177"
2) "game:player_id:9"

可以看到,player_idplayer_fd都已经保存下来了。

发送一个匹配请求,并查看Redis中的键值:

127.0.0.1:6379> keys *
1) "game:player_fd:player_177"
2) "game:player_wait_list"
3) "game:player_id:9"
127.0.0.1:6379> lrange game:player_wait_list 0 -1
1) "player_177"

可以看到,匹配队列game:player_wait_list中已经成功存入了player_177

目前我们的匹配机制已经完成了:

  • 前端连接时发送player_id
  • 服务端连接时保存玩家信息
  • 前端发送code600的指令
  • 服务端将player_id放入匹配队列

剩下的操作就是:

  • 检测匹配队列长度,当长度大于等于2时,创建游戏房间

异步检测匹配队列

我们大部分游戏逻辑都是运行在worker里的,异步的玩家匹配可以减轻主程序worker的负担。关于Task机制不了解的童鞋,请先熟悉一下官方文档。

  • Swoole Task文档:https://wiki.swoole.com/wiki/…
  1. 根据官方文档,在Server类中完成Task机制的初始化。
<?php
...
class Server
{
    ...
    const CONFIG = [
        ...
        'task_worker_num' => 4,
        ...
    ];
    public function __construct()
    {
        ...
        $this->ws->on('task', [$this, 'onTask']);
        $this->ws->on('finish', [$this, 'onFinish']);
        ...
    }
    ...
    public function onTask($server, $taskId, $srcWorkerId, $data)
    {
    }
    public function onFinish($server, $taskId, $data)
    {
    }
}
...

我们什么时候会用Task进行匹配队列检测呢?其实就是把玩家放入匹配队列后。

Logic类:

<?php
...
class Logic
{
    public function matchPlayer($playerId)
    {
        //将用户放入队列中
        DataCenter::pushPlayerToWaitList($playerId);
        //发起一个Task尝试匹配
        //swoole_server->task(xxx);
    }
}

Server类:

<?php
...
class Server
{
    ...
    public function onTask($server, $taskId, $srcWorkerId, $data)
    {
        DataCenter::log("onTask", $data);
        //执行某些逻辑
    }
    ...
}
...

可以发现,onTask方法只是接收传递的$data,当我们有多种Task任务(匹配玩家、在线检测、游戏状态检查)时,我们的worker怎么区分每一个Task任务呢?其实就和客户端与服务端通信一样,我们可以根据一个code来区分。

Logic类:

<?php
...
class Logic
{
    public function matchPlayer($playerId)
    {
        //将用户放入队列中
        DataCenter::pushPlayerToWaitList($playerId);
        //发起一个Task尝试匹配
        //swoole_server->task(['code'=>'xxx']);
    }
}

Server类:

<?php
...
class Server
{
    ...
    public function onTask($server, $taskId, $srcWorkerId, $data)
    {
        DataCenter::log("onTask", $data);
        switch ($data['code']) {
            //执行task方法
            case 'xxx':
                //task->xxx();
                break;
            case 'yyy':
                //task->yyy();
                break;
        }
    }
    ...
}
...

从代码可以看出,我们现在缺了两种机制:

  • 全局获取Server对象:在Logic中获取swoole_server从而调用task()方法。
  • 增加Task管理类:需要一个类管理TaskcodeTask需要执行的逻辑方法。

全局获取Server对象

第一个比较好处理,我们在onWorkerStart的时候就能获取到swoole_server

  • 有童鞋可以会问,为什么不在onStart的时候获取?这是因为onStart回调的是Master进程,而onWorkerStart回调的是Worker进程,只有Worker进程才可以发起Task任务。有兴趣的童鞋请查阅文档:https://wiki.swoole.com/wiki/…
  1. DataCenter中新增静态变量$server
  2. onWorkerStart回调函数中,将$server保存到DataCenter中。

DataCenter类:

<?php
...
class DataCenter
{
    ...
    public static $server;
    ...
}

Server类:

<?php
...
class Server
{
    ...
    public function onWorkerStart($server, $workerId)
    {
        ...
        DataCenter::$server = $server;
    }
    ...
}
...

这样就解决了第一种问题,下面轮到第二个问题。

增加Task管理类

在项目Manager文件夹下,创建TaskManager类文件。

TaskManager类:

<?php
namespace App\Manager;

class TaskManager
{
}

后续所有跟task有关的常量、方法都归于这个类来管理。

  1. 设置一个常量TASK_CODE_FIND_PLAYER,用于发起寻找玩家task任务。
  2. 新增静态方法findPlayer(),当匹配队列长度大于等于2时,弹出队列前两个玩家的player_id并返回。

TaskManager类:

<?php
namespace App\Manager;

class TaskManager
{
    const TASK_CODE_FIND_PLAYER = 1;

    public static function findPlayer()
    {
        $playerListLen = DataCenter::getPlayerWaitListLen();
        if ($playerListLen >= 2) {
            $redPlayer = DataCenter::popPlayerFromWaitList();
            $bluePlayer = DataCenter::popPlayerFromWaitList();
            return [
                'red_player' => $redPlayer,
                'blue_player' => $bluePlayer
            ];
        }
        return false;
    }
}

现在前置准备就绪,可以将上面写过的伪代码改成真实代码啦~

  1. Logic类的matchPlayer()方法中,发起一个Task任务尝试匹配。
  2. Server类中根据传入的code,执行TaskManagerfindPlayer()方法。
  3. findPlayer()方法有值返回的时候,返回执行结果并携带上codeworker进程。

本章就到这里结束了,这次留的Homework可能有点难度,请童鞋们尽力完成。

当前目录结构:

HideAndSeek
├── app
│   ├── Lib
│   │   └── Redis.php
│   ├── Manager
│   │   ├── DataCenter.php
│   │   ├── Game.php
│   │   ├── Logic.php
│   │   └── TaskManager.php
│   ├── Model
│   │   ├── Map.php
│   │   └── Player.php
│   └── Server.php
├── composer.json
├── composer.lock
├── frontend
│   └── index.html
├── test.php
└── vendor
    ├── autoload.php
    └── composer

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理