玩家匹配队列

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

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

上一章的答案

index.html

  1. var app = new Vue({
  2. el: '#app',
  3. data: {
  4. message: 'Hello Vue!',
  5. websock: null,
  6. },
  7. created() {
  8. this.initWebSocket();
  9. },
  10. destroyed() {
  11. this.websock.close() //离开路由之后断开websocket连接
  12. },
  13. methods: {
  14. initWebSocket() { //初始化websocket
  15. const wsuri = "ws://192.168.3.41:8811";
  16. this.websock = new WebSocket(wsuri);
  17. this.websock.onmessage = this.websocketonmessage;
  18. this.websock.onopen = this.websocketonopen;
  19. this.websock.onerror = this.websocketonerror;
  20. this.websock.onclose = this.websocketclose;
  21. },
  22. websocketonopen() { //连接建立之后执行send方法发送数据
  23. let actions = {"test": "12345"};
  24. this.websocketsend(actions);
  25. },
  26. websocketonerror() {//连接建立失败重连
  27. this.initWebSocket();
  28. },
  29. websocketonmessage(e) { //数据接收
  30. let message = JSON.parse(e.data);
  31. },
  32. websocketsend(Data) {//数据发送
  33. this.websock.send(JSON.stringify(Data));
  34. },
  35. websocketclose(e) { //关闭
  36. console.log('断开连接', e);
  37. },
  38. }
  39. })

记得将192.168.3.41改为你的IP地址哦。

Server类:

  1. <?php
  2. ...
  3. class Server
  4. {
  5. ...
  6. public function onWorkerStart($server, $workerId)
  7. {
  8. echo "server: onWorkStart,worker_id:{$server->worker_id}\n";
  9. }
  10. public function onOpen($server, $request)
  11. {
  12. DataCenter::log(sprintf('client open fd:%d', $request->fd));
  13. }
  14. public function onMessage($server, $request)
  15. {
  16. DataCenter::log(sprintf('client open fd:%d,message:%s', $request->fd, $request->data));
  17. $server->push($request->fd, 'test success');
  18. }
  19. public function onClose($server, $fd)
  20. {
  21. DataCenter::log(sprintf('client close fd:%d', $fd));
  22. }
  23. }
  24. ...

写完以上代码后,在虚拟机中重新运行Server,在浏览器访问前端页面并打开F12开发者模式。

6 玩家匹配队列 - 图1

前端成功地发出了数据,服务端返回的数据也都接收到了,我们成功打通了游戏前后端。

游戏数据管理

6 玩家匹配队列 - 图2

游戏的数据主要有两个存储方式:

  • 内存:用于管理每局的游戏对战数据。
  • Redis:用于实现匹配队列、保存玩家信息数据。

我们首先在DataCenter类中新增一个静态变量$global,所有的对战房间数据都将存储在这个变量中。

DataCenter类:

  1. class DataCenter
  2. {
  3. public static $global;
  4. ...
  5. }

目前我们的游戏还没有Redis连接方式,需要编写一个Redis驱动。

做题时间

  1. app目录下新建Lib目录,并新建一个Redis.php文件。
  2. 使用单例模式编写Redis驱动。
  3. DataCenter类中,新增静态方法redis()返回一个Redis实例。

Redis类:

  1. <?php
  2. namespace App\Lib;
  3. class Redis
  4. {
  5. protected static $instance;
  6. protected static $config = [
  7. 'host' => '127.0.0.1',
  8. 'port' => 6379,
  9. ];
  10. /**
  11. * 获取redis实例
  12. *
  13. * @return \Redis|\RedisCluster
  14. */
  15. public static function getInstance()
  16. {
  17. if (empty(self::$instance)) {
  18. $instance = new \Redis();
  19. $instance->connect(
  20. self::$config['host'],
  21. self::$config['port']
  22. );
  23. self::$instance = $instance;
  24. }
  25. return self::$instance;
  26. }
  27. }

DataCenter类:

  1. class DataCenter
  2. {
  3. ...
  4. public static function redis()
  5. {
  6. return Redis::getInstance();
  7. }
  8. ...
  9. }

到这里为止所有准备都做好了,下面开始正式进入到游戏功能开发。

进入匹配队列

我们首先要做的第一个功能就是游戏匹配。

6 玩家匹配队列 - 图3

赵童鞋的想法是:

  • 客户端发送一个匹配消息到服务端。
  • 服务端将玩家的ID放入一个Redis队列里。
  • 当队列里人数满足条件时,创建一个游戏房间。
  • 根据player_id获取连接fd,发送游戏数据。

WebSocket并不像普通的HTTP请求那样,一个功能对应一个接口,那我们要怎么区分发送和接收的消息呢?很简单,我们只要固定发送和接收数据的格式,其中加入一个参数code作为功能协议标识,所有的操作都根据发送和接收的code来进一步处理。

6 玩家匹配队列 - 图4

是不是看上去还挺好理解呢?我们先从前端入手。

做题时间

  1. 新增输入框,绑定Vue数据属性playerId,并默认生成一个随机ID。
  2. 新增按钮,绑定Vue方法matchPlayer,点击按钮时发送code600的数据到服务端。
  3. WebSocket连接的时候,发送玩家playerId到服务端。

index.html

  1. ...
  2. <div id="app">
  3. <label>
  4. 玩家ID
  5. <input type="text" :value="playerId">
  6. </label>
  7. <button @click="matchPlayer">匹配</button>
  8. </div>
  9. <script>
  10. var app = new Vue({
  11. ...
  12. data: {
  13. playerId: 'player_' + Math.round(Math.random() * 1000),
  14. ...
  15. },
  16. ...
  17. methods: {
  18. //匹配玩家
  19. matchPlayer() {
  20. let actions = {"code": 600};
  21. this.websocketsend(actions);
  22. },
  23. initWebSocket() { //初始化websocket
  24. const wsuri = "ws://192.168.3.41:8811?player_id=" + this.playerId;
  25. ...
  26. },
  27. ...
  28. }
  29. })
  30. </script>
  31. ...

前端代码不多,下面轮到服务端实现。

服务端将会涉及到三个类,也就是ServerLogicDataCenter,这三个类的调用顺序是:

  • 第一次连接时,Server将用户信息如playerId保存到DataCenter
  • Server接收到操作指令,调用Logic中的逻辑方法。
  • Logic中的逻辑调用DataCenter进行数据操作。

6 玩家匹配队列 - 图5

Server

我们采用自顶向下的方法来编写试试,不存在的方法也可以先写出来调用。

先从Server开始。从前端代码可以看到,在WebSocket建立连接的时候,前端会发送player_id到服务端,这个时候我们需要把player_id和连接fd保存在DataCenter,方便服务端后续向客户端发送消息时,可根据player_id获取连接fd。其他操作指令会带有一个code用来标识协议码,我们需要根据code600时,调用Logic进行匹配。

做题时间

  1. Server类初始化的时候,新建Logic对象并保存在私有变量$logic,用于调用Logic类中的方法。
  2. onOpen事件中,传递用户的player_idfdDataCentersetPlayerInfo()方法中进行保存。
  3. onMessage事件中,根据当前连接的fd获取player_id,当前端发送的消息中的code600时,调用Logic中的matchPlayer()方法
  4. onClose事件中,调用delPlayerInfo()方法根据$fd清除玩家信息。

Swoole WebSocket onMessage: wiki.swoole.com/wiki/page/4…

Server类:

  1. <?php
  2. ...
  3. class Server
  4. {
  5. const CLIENT_CODE_MATCH_PLAYER = 600;
  6. ...
  7. private $logic;
  8. public function __construct()
  9. {
  10. $this->logic = new Logic();
  11. ...
  12. }
  13. ...
  14. public function onOpen($server, $request)
  15. {
  16. DataCenter::log(sprintf('client open fd:%d', $request->fd));
  17. $playerId = $request->get['player_id'];
  18. DataCenter::setPlayerInfo($playerId, $request->fd);
  19. }
  20. public function onMessage($server, $request)
  21. {
  22. DataCenter::log(sprintf('client open fd:%d,message:%s', $request->fd, $request->data));
  23. $data = json_decode($request->data, true);
  24. $playerId = DataCenter::getPlayerId($request->fd);
  25. switch ($data['code']) {
  26. case self::CLIENT_CODE_MATCH_PLAYER:
  27. $this->logic->matchPlayer($playerId);
  28. break;
  29. }
  30. }
  31. public function onClose($server, $fd)
  32. {
  33. DataCenter::log(sprintf('client close fd:%d', $fd));
  34. DataCenter::delPlayerInfo($fd);
  35. }
  36. ...
  37. }
  38. ...

Logic

下面到Logic类,Logic其实代码不多,他需要实现的就是接收Server传递的消息并执行具体的逻辑。

做题时间

  1. 新增matchPlayer()方法,将Server传递过来的player_id放入DataCenter的匹配队列中。

    <?php … class Logic {

    1. public function matchPlayer($playerId)
    2. {
    3. DataCenter::pushPlayerToWaitList($playerId);
    4. }

    }

DataCenter

有了上述两个类的调用,我们的DataCenter需求就清晰很多了,需要实现用户信息的存取,需要实现一个队列的进出和长度查询,用于玩家匹配。

做题时间

  1. 新增常量PREFIX_KEY,作为所有Rediskey前缀,区别于其他应用缓存值。
  2. playerIdfd编写基于Redis实现的setgetdelete方法,需要实现playerIdfd可以互相查找。
  3. 实现匹配队列的pushpopgetLength方法。
  4. 完成Server调用的setPlayerInfo()方法,保存player_idfd。完成delPlayerInfo()方法,清除玩家数据。

本章作为第一个游戏功能开发,请童鞋们尽量完成Homework哦,其他功能如邀请、观战也将会是这个调用流程。

本章对应Github Commit第六章结束

当前目录结构:

  1. HideAndSeek
  2. ├── app
  3. ├── Lib
  4. └── Redis.php
  5. ├── Manager
  6. ├── DataCenter.php
  7. ├── Game.php
  8. └── Logic.php
  9. ├── Model
  10. ├── Map.php
  11. └── Player.php
  12. └── Server.php
  13. ├── composer.json
  14. ├── composer.lock
  15. ├── frontend
  16. └── index.html
  17. ├── test.php
  18. └── vendor
  19. ├── autoload.php
  20. └── composer