前言
最近在项目中一直在开发即时消息的应用场景,本次将通过Laravel来构建一个即时的匹配系统,可适用于基本的即时对战小游戏,需要用到的知识点如下:
- Laravel
- Redis
- Laravel-echo
- 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,构建出来的应该会是这样。
完善后端匹配逻辑
提高一个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 //开启匹配队列消费
结尾
本篇主要通过后端角度出发,来使用队列技术,实现了用户的即时匹配系统,核心知识点主要在laravel队列和laravel-echo的使用,通过队列将匹配中的用户进行组合,创建一局新比赛,通过服务端广播NewGame,客户端监听NewGame事件来达到即时匹配的效果。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。