前言

最近在项目中一直在开发即时消息的应用场景,本次将通过Laravel来构建一个即时的匹配系统,可适用于基本的即时对战小游戏,需要用到的知识点如下:

  1. Laravel
  2. Redis
  3. Laravel-echo
  4. VueJs

下面开始实战操作:

安装项目依赖

composer install predis //安装redis扩展包

npm install laravel-echo //安装laravel-echo客户端


npm install -g laravel-echo-server //安装laravel-echo-server 服务端

具体部署教程可参照之前写的博客:
打造你的Laravel即时应用

创建比赛系统

在我们的这个基础系统中,需要用户(user)和比赛(game),比赛主要是用于将匹配到的用户放到一起,下面将通过3张表来完成这个系统的的初始设计。

1.比赛表设计

$table->unsignedInteger('user_id')->index();
$table->unsignedInteger('rival_id')->index()->comment('对手ID');
$table->unsignedInteger('winner_id')->default(0)->comment('胜利者ID');
$table->unsignedInteger('reward')->default(0)->comment('奖励');
$table->json('data')->nullable()->comment('比赛数据');
$table->tinyInteger('status')->default(0)->comment('比赛状态');
$table->timestamps();
$table->timestamp('end_at')->nullable()->comment('结束时间');

2.用户匹配表设计

$table->increments('id');
$table->unsignedInteger('user_id')->unique();
$table->tinyInteger('online_status')->default(0);
$table->timestamp('online_at')->nullable()->index();
$table->unsignedInteger('in_game_id')->default(0)->comment('在房间ID');
$table->timestamps();

构建视图

我们通过Vue来实现一个简单的视图,运用到了props、axios等基础组件来实现.

<template>
  <div class="game-status">
    <div class="user-info">
      <div class="text-left">您的游戏信息如下:</div>
      <div class="clearfix">
        <div class="float-left" v-if="me"">您的昵称:{{ me.name }}</div>
        <div class="float-right" v-if="rival">对手昵称:{{ rival.name }}</div>
      </div>
    </div>
    <div class="alert alert-success" role="alert">{{ gameStatus }}</div>
    <button type="button" class="btn btn-success">开始匹配</button>
  </div>
</template>

<script>
import Echo from "laravel-echo";
export default {
  props: ["user"],
  data() {
    return {
      gameStatus: "等待匹配中",
      isMatching: true,
      meId: null,
      me: null,
      rival: null
    };
  },

  mounted() {
    this.me = JSON.parse(this.user);
    console.log(this.me);
    this.meId = this.me.id;
  },
};
</script>

如果你使用的css框架是bootstrap,构建出来的应该会是这样。

image

完善后端匹配逻辑

提高一个matchUser的接口给前端来进行调用,首先会去查询user_games的记录,不存在则创建一条,稍后再查询当前是否有正在进行中的比赛,存在的话,则抛出异常提醒用户,否则进入匹配队列。

//查找或创建用户
$userGame = UserGame::firstOrCreate(['user_id' => $user->id]);
//是否有游戏中,防止重复请求
$game = $userGame->hasPlaying();
throw_if(!is_null($game), GameException::class, '您已在PK中,请进入游戏!');
//用户上线
$userGame->online();
//分发到匹配队列中
dispatch(new MatchGame($this))->onQueue('game-match');

当用户成功进入匹配队列后,我们会将所有匹配中的用户进行对手分配,每次获取两个用户为一局比赛,逻辑如下:

UserGame::with('user')->onlined()->orderBy('online_at')->chunk(100, function ($collect) {
    foreach ($collect->chunk(2) as $userGames) {
        if (count($userGames) < 2) {
            continue;
        }

        $user1 = $userGames->first()->user;
        $user2 = $userGames->last()->user;

        $game = $this->gameService->createGame($user1, $user2);
        //发送socket通知双方用户开始
        $this->gameService->sendSocket($game, 'NewGame');
    }
});

完善前端匹配接口 && 挂载Echo

当匹配接口和匹配队列完成后,就开始对视图进行绑定及挂载Echo socket通信。

<template>
  <div class="game-status">
    <div class="user-info">
      <div class="text-left">您的游戏信息如下:</div>
      <div class="clearfix">
        <div class="float-left" v-if="me"">您的昵称:{{ me.name }}</div>
        <div class="float-right" v-if="rival">对手昵称:{{ rival.name }}</div>
      </div>
    </div>
    <div class="alert alert-success" role="alert">{{ gameStatus }}</div>
    <button type="button" class="btn btn-success" @click="matchGame">开始匹配</button>
  </div>
</template>

<script>
import Echo from "laravel-echo";
export default {
  props: ["user"],
  data() {
    return {
      gameStatus: "等待匹配中",
      isMatching: true,
      meId: null,
      me: null,
      rival: null
    };
  },

  mounted() {
    this.me = JSON.parse(this.user);
    console.log(this.me);
    this.meId = this.me.id;
  },

  methods: {
    matchGame: function() {
      let _this = this;
      this.isMatching = false;
      //1.发送匹配其他用户请求
      this.$axios.get("/match?user_id=" + this.meId).then(res => {
        _this.gameStatus = "正在匹配中";
      });

      this.mountedUserEventListen();
    },
    mountedUserEventListen: function() {
      //监听当前用户Channel && NewGame event
      let echo = this.initEcho();
      let _this = this;
      echo.private("App.User." + this.meId).listen("NewGame", function(e) {
        let game = e.game;
        _this.gameStatus = "用户匹配成功";
        _this.rival = _this.meId == game.user_id ? game.rival : game.user;
      });
    },
    initEcho: function() {
      if (window.Echo == null) {
        window.io = require("socket.io-client");
        window.Echo = new Echo({
          broadcaster: "socket.io",
          host: window.location.hostname + ":6001",
          auth: {
            headers: {
              Authorization: "Bearer " + this.me.api_token
            }
          }
        });

        return window.Echo;
      }
    }
  }
};
</script>

效果展示

运行指令:

laravel-echo-server start   //开启socket服务

php artisan queue:work //开启队列消费

php artisan queue:work --queue=game-match //开启匹配队列消费

image

结尾

本篇主要通过后端角度出发,来使用队列技术,实现了用户的即时匹配系统,核心知识点主要在laravel队列和laravel-echo的使用,通过队列将匹配中的用户进行组合,创建一局新比赛,通过服务端广播NewGame,客户端监听NewGame事件来达到即时匹配的效果。


Sinming
310 声望21 粉丝

Bug总工程师